Skip to content

Companions

Understanding Drift's companion classes for inserts and updates


What is it?

Companion Classes are generated classes in Drift that provide a type-safe way to insert, update, and delete records. They use the Value<T> wrapper to handle optional fields, default values, and partial updates. Every table automatically gets a companion class named {TableName}Companion.

Think of Companions like "order forms" – you fill in the fields you want to set, leave others blank to use defaults, and submit it to create or update a record.

// 👇 Using a companion
await into(users).insert(
  UsersCompanion.insert(
    name: 'John Doe',           // Required field
    email: 'john@example.com',  // Required field
    age: Value(25),             // Optional field (provided)
    // isActive: Value(true)    // Optional field (uses default)
  ),
);

// Generated Companion (simplified)
class UsersCompanion extends UpdateCompanion<User> {
  final Value<int> id;
  final Value<String> name;
  final Value<String> email;
  final Value<int?> age;
  final Value<bool> isActive;

  const UsersCompanion({
    this.id = const Value.absent(),
    this.name = const Value.absent(),
    this.email = const Value.absent(),
    this.age = const Value.absent(),
    this.isActive = const Value.absent(),
  });

  UsersCompanion.insert({
    this.id = const Value.absent(),
    required String name,
    required String email,
    this.age = const Value.absent(),
    this.isActive = const Value.absent(),
  }) : super.insert();
}

What's happening here? - Companion Class – Generated for each table - Value – Wraps optional and default values - insert constructor – For creating new records - Absent values – Use default or skip during update


Why does it exist?

  • Type Safety – All fields are strongly typed
  • Partial Updates – Update only specific fields
  • Default Handling – Automatically use defaults
  • Null Handling – Explicitly set NULL values
  • IDE Support – Autocomplete and validation
  • Error Prevention – Required fields must be provided

Companion Structure

Understanding the generated companion

// lib/database/tables/users.dart
class Users extends Table {
  IntColumn get id => integer().autoIncrement()();
  TextColumn get name => text()();
  TextColumn get email => text().unique()();
  IntColumn get age => integer().nullable()();
  BoolColumn get isActive => boolean().withDefault(const Constant(true))();
  DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
}

// Generated UsersCompanion (complete)
class UsersCompanion extends UpdateCompanion<User> {
  // 👇 All fields as Value<T>
  final Value<int> id;
  final Value<String> name;
  final Value<String> email;
  final Value<int?> age;
  final Value<bool> isActive;
  final Value<DateTime> createdAt;

  // 👇 Default constructor (all absent)
  const UsersCompanion({
    this.id = const Value.absent(),
    this.name = const Value.absent(),
    this.email = const Value.absent(),
    this.age = const Value.absent(),
    this.isActive = const Value.absent(),
    this.createdAt = const Value.absent(),
  });

  // 👇 Insert constructor (required fields)
  UsersCompanion.insert({
    this.id = const Value.absent(),
    required String name,
    required String email,
    this.age = const Value.absent(),
    this.isActive = const Value.absent(),
    this.createdAt = const Value.absent(),
  }) : super.insert();

  // 👇 Methods for serialization
  @override
  Map<String, Expression> toColumns(bool nullToAbsent) {
    // ... internal mapping
  }
}

Insert Operations

Using companions for inserts

Basic Insert

// 👇 Insert with required fields only
await into(users).insert(
  UsersCompanion.insert(
    name: 'John Doe',
    email: 'john@example.com',
    // age: uses default (NULL)
    // isActive: uses default (true)
    // createdAt: uses default (current time)
  ),
);

// Generated SQL:
// INSERT INTO users (name, email) VALUES ('John Doe', 'john@example.com')
// (Defaults used for other columns)

Insert with All Fields

// 👇 Insert with all fields explicitly
await into(users).insert(
  UsersCompanion.insert(
    name: 'Jane Smith',
    email: 'jane@example.com',
    age: Value(28),
    isActive: Value(true),
    createdAt: Value(DateTime(2024, 1, 1)),
  ),
);

// Generated SQL:
// INSERT INTO users (name, email, age, is_active, created_at) 
// VALUES ('Jane Smith', 'jane@example.com', 28, 1, 1704067200)

Insert with Explicit NULL

// 👇 Insert with NULL for optional field
await into(users).insert(
  UsersCompanion.insert(
    name: 'Bob Wilson',
    email: 'bob@example.com',
    age: const Value(null), // 👈 Explicitly set NULL
  ),
);

// Generated SQL:
// INSERT INTO users (name, email, age) VALUES ('Bob Wilson', 'bob@example.com', NULL)

Insert with Multiple Records

// 👇 Batch insert
await into(users).insertAll([
  UsersCompanion.insert(
    name: 'User 1',
    email: 'user1@example.com',
  ),
  UsersCompanion.insert(
    name: 'User 2',
    email: 'user2@example.com',
  ),
  UsersCompanion.insert(
    name: 'User 3',
    email: 'user3@example.com',
    age: Value(30),
  ),
]);

Update Operations

Using companions for updates

Full Update

// 👇 Update all fields
await (update(users)..where((u) => u.id.equals(1)))
  .write(UsersCompanion(
    name: Value('John Updated'),
    email: Value('john.updated@example.com'),
    age: Value(26),
    isActive: Value(false),
    createdAt: Value(DateTime(2024, 1, 1)),
  ));

// Generated SQL:
// UPDATE users SET 
//   name = 'John Updated',
//   email = 'john.updated@example.com',
//   age = 26,
//   is_active = 0,
//   created_at = 1704067200
// WHERE id = 1

Partial Update

// 👇 Update only specific fields
await (update(users)..where((u) => u.id.equals(1)))
  .write(UsersCompanion(
    name: Value('John Updated'), // 👈 Only update name
    // Other fields use Value.absent()
  ));

// Generated SQL:
// UPDATE users SET name = 'John Updated' WHERE id = 1
// (Other columns unchanged)

Conditional Update

// 👇 Update based on condition
await (update(users)..where((u) => u.isActive.equals(true)))
  .write(UsersCompanion(
    isActive: Value(false),
    updatedAt: Value(DateTime.now()), // If you had an updatedAt column
  ));

Update with NULL

// 👇 Set field to NULL
await (update(users)..where((u) => u.id.equals(1)))
  .write(UsersCompanion(
    age: const Value(null), // 👈 Set age to NULL
  ));

Delete Operations

Deleting records (companions are optional)

Simple Delete

// 👇 Delete by condition
await (delete(users)..where((u) => u.id.equals(1))).go();

// 👇 Delete all
await delete(users).go();

// 👇 Delete with multiple conditions
await (delete(users)
  ..where((u) => u.isActive.equals(false))
  ..where((u) => u.age.isBiggerOrEqualValue(100)))
  .go();

Delete with Returning

// 👇 Delete and return deleted rows
final deleted = await (delete(users)..where((u) => u.id.equals(1)))
  .returning();

print('Deleted user: ${deleted.first}');

Advanced Companion Patterns

Complex patterns with companions

Pattern 1: Conditional Insert

Future<int> createOrUpdateUser({
  required int id,
  required String name,
  String? email,
  int? age,
  bool? isActive,
}) async {
  // Check if user exists
  final existing = await (select(users)..where((u) => u.id.equals(id)))
      .getSingleOrNull();

  if (existing == null) {
    // Insert new
    return await into(users).insert(
      UsersCompanion.insert(
        name: name,
        email: email ?? '$name@example.com',
        age: Value(age),
        isActive: Value(isActive ?? true),
      ),
    );
  } else {
    // Update existing
    await (update(users)..where((u) => u.id.equals(id)))
      .write(UsersCompanion(
        name: Value(name),
        email: email != null ? Value(email) : const Value.absent(),
        age: age != null ? Value(age) : const Value.absent(),
        isActive: isActive != null ? Value(isActive) : const Value.absent(),
      ));
    return id;
  }
}

Pattern 2: Bulk Update with Companion

Future<void> bulkUpdateUsers(List<UserUpdate> updates) async {
  await transaction(() async {
    for (final update in updates) {
      await (update(users)..where((u) => u.id.equals(update.id)))
        .write(UsersCompanion(
          name: update.name != null ? Value(update.name!) : const Value.absent(),
          age: update.age != null ? Value(update.age!) : const Value.absent(),
          isActive: update.isActive != null 
              ? Value(update.isActive!) 
              : const Value.absent(),
        ));
    }
  });
}

class UserUpdate {
  final int id;
  final String? name;
  final int? age;
  final bool? isActive;

  UserUpdate({required this.id, this.name, this.age, this.isActive});
}

Pattern 3: Upsert (Insert or Update)

Future<int> upsertUser(UsersCompanion companion) async {
  // 👇 Try to find user by email
  final email = companion.email.value;
  if (email == null) {
    return await into(users).insert(companion);
  }

  final existing = await (select(users)..where((u) => u.email.equals(email)))
      .getSingleOrNull();

  if (existing == null) {
    // Insert
    return await into(users).insert(companion);
  } else {
    // Update
    await (update(users)..where((u) => u.id.equals(existing.id)))
      .write(companion);
    return existing.id;
  }
}

// Usage
final id = await upsertUser(
  UsersCompanion.insert(
    name: 'John Doe',
    email: 'john@example.com',
    age: Value(30),
  ),
);

Pattern 4: Safe Update with Validation

Future<void> safeUpdateUser(int id, UsersCompanion companion) async {
  // 👇 Validate before update
  if (companion.name.value != null) {
    final name = companion.name.value!;
    if (name.length < 2) {
      throw Exception('Name must be at least 2 characters');
    }
  }

  if (companion.email.value != null) {
    final email = companion.email.value!;
    if (!email.contains('@')) {
      throw Exception('Invalid email format');
    }
  }

  if (companion.age.value != null) {
    final age = companion.age.value!;
    if (age < 0 || age > 150) {
      throw Exception('Invalid age range');
    }
  }

  // 👇 Perform update
  await (update(users)..where((u) => u.id.equals(id)))
    .write(companion);
}

// Usage
await safeUpdateUser(1, UsersCompanion(
  name: Value('John'),
  age: Value(25),
));

Companion with Relations

Using companions with relations

// lib/database/tables/posts.dart
class Posts extends Table {
  IntColumn get id => integer().autoIncrement()();
  TextColumn get title => text()();
  TextColumn get content => text()();
  IntColumn get userId => integer().references(Users, #id)();
  BoolColumn get isPublished => boolean().withDefault(const Constant(false))();
  DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
}

// lib/database/database.dart
@DriftDatabase(tables: [Users, Posts])
class AppDatabase extends _$AppDatabase {
  // 👇 Create user and post in one transaction
  Future<int> createUserWithPost({
    required String userName,
    required String userEmail,
    required String postTitle,
    required String postContent,
  }) async {
    return await transaction(() async {
      // 1️⃣ Create user
      final userId = await into(users).insert(
        UsersCompanion.insert(
          name: userName,
          email: userEmail,
        ),
      );

      // 2️⃣ Create post for user
      await into(posts).insert(
        PostsCompanion.insert(
          title: postTitle,
          content: postContent,
          userId: userId,
        ),
      );

      return userId;
    });
  }

  // 👇 Update user and their posts
  Future<void> updateUserWithPosts(int userId, {
    required String userName,
    required String postTitle,
  }) async {
    await transaction(() async {
      // 1️⃣ Update user
      await (update(users)..where((u) => u.id.equals(userId)))
        .write(UsersCompanion(name: Value(userName)));

      // 2️⃣ Update user's posts
      await (update(posts)..where((p) => p.userId.equals(userId)))
        .write(PostsCompanion(title: Value(postTitle)));
    });
  }

  // 👇 Delete user and their posts (cascade)
  Future<void> deleteUserWithPosts(int userId) async {
    await transaction(() async {
      // Delete user - ON DELETE CASCADE will delete posts
      await (delete(users)..where((u) => u.id.equals(userId))).go();
    });
  }
}

Real-World Example

Complete e-commerce companion usage

// lib/database/tables/users.dart
class Users extends Table {
  IntColumn get id => integer().autoIncrement()();
  TextColumn get username => text().unique()();
  TextColumn get email => text().unique()();
  TextColumn get fullName => text().nullable()();
  TextColumn get passwordHash => text()();
  IntColumn get age => integer().nullable()();
  BoolColumn get isActive => boolean().withDefault(const Constant(true))();
  BoolColumn get isVerified => boolean().withDefault(const Constant(false))();
  BoolColumn get isAdmin => boolean().withDefault(const Constant(false))();
  DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
  DateTimeColumn get updatedAt => dateTime().nullable()();
}

// lib/database/tables/products.dart
class Products extends Table {
  IntColumn get id => integer().autoIncrement()();
  TextColumn get sku => text().unique()();
  TextColumn get name => text()();
  TextColumn get description => text().nullable()();
  RealColumn get price => real().customConstraint('CHECK (price >= 0)')();
  IntColumn get stock => integer().withDefault(const Constant(0))();
  BoolColumn get isActive => boolean().withDefault(const Constant(true))();
  DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
  DateTimeColumn get updatedAt => dateTime().nullable()();
}

// lib/database/tables/orders.dart
class Orders extends Table {
  IntColumn get id => integer().autoIncrement()();
  TextColumn get orderNumber => text().unique()();
  IntColumn get userId => integer().references(Users, #id)();
  RealColumn get total => real()();
  TextColumn get status => text()();
  DateTimeColumn get orderDate => dateTime().withDefault(currentDateAndTime)();
  DateTimeColumn get shippedDate => dateTime().nullable()();
}

// lib/database/database.dart
@DriftDatabase(tables: [Users, Products, Orders])
class EcommerceDatabase extends _$EcommerceDatabase {
  EcommerceDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection());

  @override
  int get schemaVersion => 1;

  // 👇 Create user
  Future<int> createUser({
    required String username,
    required String email,
    required String passwordHash,
    String? fullName,
    int? age,
    bool isAdmin = false,
  }) async {
    return await into(users).insert(
      UsersCompanion.insert(
        username: username,
        email: email,
        passwordHash: passwordHash,
        fullName: Value(fullName),
        age: Value(age),
        isAdmin: Value(isAdmin),
        // isActive: true (default)
        // isVerified: false (default)
      ),
    );
  }

  // 👇 Update user
  Future<void> updateUser(
    int userId, {
    String? username,
    String? email,
    String? fullName,
    int? age,
    bool? isActive,
    bool? isVerified,
  }) async {
    await (update(users)..where((u) => u.id.equals(userId)))
      .write(UsersCompanion(
        username: username != null ? Value(username) : const Value.absent(),
        email: email != null ? Value(email) : const Value.absent(),
        fullName: fullName != null ? Value(fullName) : const Value.absent(),
        age: age != null ? Value(age) : const Value.absent(),
        isActive: isActive != null ? Value(isActive) : const Value.absent(),
        isVerified: isVerified != null ? Value(isVerified) : const Value.absent(),
        updatedAt: Value(DateTime.now()),
      ));
  }

  // 👇 Update user by partial fields only
  Future<void> updateUserProfile(int userId, {
    String? fullName,
    int? age,
  }) async {
    final companion = UsersCompanion(
      fullName: fullName != null ? Value(fullName) : const Value.absent(),
      age: age != null ? Value(age) : const Value.absent(),
      updatedAt: Value(DateTime.now()),
    );

    await (update(users)..where((u) => u.id.equals(userId)))
      .write(companion);
  }

  // 👇 Create product
  Future<int> createProduct({
    required String sku,
    required String name,
    String? description,
    required double price,
    int stock = 0,
  }) async {
    return await into(products).insert(
      ProductsCompanion.insert(
        sku: sku,
        name: name,
        description: Value(description),
        price: price,
        stock: Value(stock),
      ),
    );
  }

  // 👇 Update product stock
  Future<void> updateProductStock(int productId, int newStock) async {
    await (update(products)..where((p) => p.id.equals(productId)))
      .write(ProductsCompanion(
        stock: Value(newStock),
        updatedAt: Value(DateTime.now()),
      ));
  }

  // 👇 Create order with automatic total calculation
  Future<int> createOrder(
    int userId,
    List<OrderItemInput> items,
  ) async {
    return await transaction(() async {
      // Calculate total
      double total = 0;
      for (final item in items) {
        final product = await (select(products)
          ..where((p) => p.id.equals(item.productId)))
          .getSingle();
        total += product.price * item.quantity;
      }

      // Create order
      final orderId = await into(orders).insert(
        OrdersCompanion.insert(
          orderNumber: 'ORD-${DateTime.now().millisecondsSinceEpoch}',
          userId: userId,
          total: total,
          status: 'pending',
        ),
      );

      // Update product stock (reduce)
      for (final item in items) {
        final product = await (select(products)
          ..where((p) => p.id.equals(item.productId)))
          .getSingle();

        await (update(products)..where((p) => p.id.equals(product.id)))
          .write(ProductsCompanion(
            stock: Value(product.stock - item.quantity),
            updatedAt: Value(DateTime.now()),
          ));
      }

      return orderId;
    });
  }

  // 👇 Cancel order (restore stock)
  Future<void> cancelOrder(int orderId) async {
    await transaction(() async {
      // 1️⃣ Get order items (simplified - you'd have an OrderItems table)
      // 2️⃣ Restore stock
      // 3️⃣ Update order status

      await (update(orders)..where((o) => o.id.equals(orderId)))
        .write(OrdersCompanion(
          status: Value('cancelled'),
        ));
    });
  }

  // 👇 Complex upsert
  Future<int> upsertProduct(ProductsCompanion companion) async {
    final sku = companion.sku.value;
    if (sku == null) throw Exception('SKU is required');

    final existing = await (select(products)
      ..where((p) => p.sku.equals(sku)))
      .getSingleOrNull();

    if (existing == null) {
      // Insert new product
      return await into(products).insert(companion);
    } else {
      // Update existing product
      await (update(products)..where((p) => p.id.equals(existing.id)))
        .write(companion);
      return existing.id;
    }
  }

  // 👇 Safe delete with validation
  Future<void> safeDeleteUser(int userId) async {
    // Check if user has orders
    final hasOrders = await (select(orders)
      ..where((o) => o.userId.equals(userId)))
      .count() > 0;

    if (hasOrders) {
      throw Exception('Cannot delete user with existing orders');
    }

    await (delete(users)..where((u) => u.id.equals(userId))).go();
  }
}

class OrderItemInput {
  final int productId;
  final int quantity;

  OrderItemInput({required this.productId, required this.quantity});
}

Best Practices

  • Use insert constructor – For creating new records
  • Use Value.absent() – To skip fields in updates
  • Use Value(null) – To explicitly set NULL
  • Use transactions – For multiple operations
  • Validate before update – Use companion with validation
  • Use copyWith – For data class modifications
  • Document companions – Explain required fields
  • Test companions – Verify insert/update behavior

Common Mistakes

Mistake 1: Using insert constructor for updates

Wrong:

// 🚫 Using insert constructor for updates
await update(users).write(
  UsersCompanion.insert( // Wrong constructor
    name: 'John',
  ),
);

Correct:

// ✅ Use default constructor for updates
await update(users).write(
  UsersCompanion(
    name: Value('John'),
  ),
);

Mistake 2: Forgetting Value wrapper

Wrong:

// 🚫 Missing Value wrapper
await update(users).write(
  UsersCompanion(
    name: 'John', // ❌ Should be Value('John')
  ),
);

Correct:

// ✅ Use Value wrapper
await update(users).write(
  UsersCompanion(
    name: Value('John'),
  ),
);

Mistake 3: Using Value.absent() in inserts

Wrong:

// 🚫 Absent doesn't work in insert
await into(users).insert(
  UsersCompanion.insert(
    name: Value.absent(), // ❌ Required field can't be absent
  ),
);

Correct:

// ✅ Provide all required fields
await into(users).insert(
  UsersCompanion.insert(
    name: 'John', // Required field
    email: 'john@example.com', // Required field
  ),
);


Summary

Method Purpose Use Case
insert constructor Create new record UsersCompanion.insert()
default constructor Update existing UsersCompanion()
Value.absent() Skip field Updates only
Value(value) Set field Inserts and updates
Value(null) Set NULL Optional fields

Next Steps

Now you understand companions, let's dive deeper:


Did You Know?

  • Companions are generated – Don't manually edit them

  • Insert constructor validates required fields – At compile-time

  • Absent values in inserts use defaults – Not NULL

  • Default constructor allows partial updates – Only provided fields update

  • Value.absent() skips the field entirely – In UPDATE statements

  • Value(null) explicitly sets NULL – Even in optional fields

  • Companions are immutable – Use copyWith to modify

  • Companions can be used in transactions – Atomic operations