Replace
Insert or replace records in Drift
What is it?
Replace is an insert operation that either inserts a new record or replaces an existing record if a conflict occurs (usually on a unique constraint or primary key). When a conflict happens, the existing record is deleted and the new record is inserted in its place.
Think of Replace like "overwriting a file" – if the file exists, you delete the old one and save the new version; if it doesn't exist, you just save the new file.
// 👇 Replace a user
await into(users).insert(
UsersCompanion.insert(
id: Value(1), // Existing ID
name: 'John Updated',
email: 'john.new@example.com',
),
mode: InsertMode.replace, // 👈 Replace on conflict
);
// What happens:
// If user with ID 1 exists: DELETE old, INSERT new
// If user with ID 1 doesn't exist: INSERT new
// Generated SQL:
// INSERT OR REPLACE INTO users (id, name, email)
// VALUES (1, 'John Updated', 'john.new@example.com')
What's happening here? -
InsertMode.replace– Replace on conflict - Conflict detection – Based on PRIMARY KEY or UNIQUE constraints - Delete + Insert – Existing record removed, new record added - Atomic operation – Happens in a single database operation
Why does it exist?
- Upsert Simplicity – Insert or replace without complex logic
- Data Synchronization – Sync external data sources
- Cache Updates – Update cached data
- Idempotent Operations – Safe to run multiple times
- Data Migration – Replace existing records
- Conflict Resolution – Handle duplicates automatically
Basic Replace Operations
Simple replace operations
Replace by Primary Key
// 👇 Replace user by ID
await into(users).insert(
UsersCompanion.insert(
id: Value(1), // 👈 Primary key
name: 'John Doe',
email: 'john.doe@example.com',
age: Value(30),
),
mode: InsertMode.replace,
);
// Generated SQL:
// INSERT OR REPLACE INTO users (id, name, email, age)
// VALUES (1, 'John Doe', 'john.doe@example.com', 30)
Replace by Unique Constraint
// 👇 Replace by unique email
await into(users).insert(
UsersCompanion.insert(
email: 'john@example.com', // 👈 Unique constraint
name: 'John Updated',
age: Value(25),
),
mode: InsertMode.replace,
);
// If email 'john@example.com' exists:
// 1. Delete existing record with that email
// 2. Insert new record with new data
Replace with All Fields
// 👇 Full replace (all fields provided)
await into(users).insert(
UsersCompanion.insert(
id: Value(1),
name: 'Jane Smith',
email: 'jane@example.com',
age: Value(28),
isActive: Value(true),
isVerified: Value(true),
createdAt: Value(DateTime(2024, 1, 1)),
),
mode: InsertMode.replace,
);
Replace with Partial Data
// 👇 Replace with partial data (missing fields use defaults)
await into(users).insert(
UsersCompanion.insert(
id: Value(1),
name: 'John Updated',
email: 'john.updated@example.com',
// Missing fields will use database defaults
),
mode: InsertMode.replace,
);
Replace vs Other Operations
Understanding the differences
Replace vs Insert with Conflict
// 👇 Insert with conflict (insert or ignore)
await into(users).insert(
UsersCompanion.insert(
id: Value(1),
name: 'John',
email: 'john@example.com',
),
onConflict: DoNothing(), // Skip if conflict
);
// 👇 Insert with update (upsert)
await into(users).insert(
UsersCompanion.insert(
id: Value(1),
name: 'John Updated',
email: 'john@example.com',
),
onConflict: DoUpdate(
target: users.id,
update: UsersCompanion(
name: Value('John Updated'),
),
),
);
// 👇 Replace (delete + insert)
await into(users).insert(
UsersCompanion.insert(
id: Value(1),
name: 'John Updated',
email: 'john.new@example.com',
),
mode: InsertMode.replace,
);
Replace vs Update
// 👇 Update (only changes specified fields)
await (update(users)..where((u) => u.id.equals(1)))
.write(UsersCompanion(
name: Value('John Updated'),
// Other fields remain unchanged
));
// 👇 Replace (completely replaces record)
await into(users).insert(
UsersCompanion.insert(
id: Value(1),
name: 'John Updated',
email: 'john.new@example.com',
// All fields must be provided or use defaults
),
mode: InsertMode.replace,
);
Advanced Replace Patterns
Complex replace operations
Pattern 1: Replace with Cascade
// 👇 Replace automatically handles cascade deletes
class Posts extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get title => text()();
IntColumn get userId => integer()
.references(Users, #id, onDelete: KeyAction.cascade)();
}
// When replacing a user, old posts are deleted (cascade)
await into(users).insert(
UsersCompanion.insert(
id: Value(1),
name: 'New User',
email: 'new@example.com',
),
mode: InsertMode.replace,
);
// What happens:
// 1. Old user with ID 1 is deleted (posts cascade delete)
// 2. New user with ID 1 is inserted
Pattern 2: Replace with Timestamp
// 👇 Replace with updated timestamp
class Users extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text()();
TextColumn get email => text().unique()();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get updatedAt => dateTime().nullable()();
}
// Replace will keep or reset timestamps
await into(users).insert(
UsersCompanion.insert(
email: 'john@example.com',
name: 'John',
createdAt: Value(DateTime(2024, 1, 1)), // Keep original
updatedAt: Value(DateTime.now()), // Set new update time
),
mode: InsertMode.replace,
);
Pattern 3: Sync External Data
// 👇 Sync users from external API
Future<void> syncUsersFromApi(List<ApiUser> apiUsers) async {
for (final apiUser in apiUsers) {
await into(users).insert(
UsersCompanion.insert(
id: Value(apiUser.id),
name: apiUser.name,
email: apiUser.email,
age: Value(apiUser.age),
isActive: Value(apiUser.isActive),
updatedAt: Value(DateTime.now()),
),
mode: InsertMode.replace,
);
}
}
// This will:
// - Insert new users
// - Replace existing users with new data
// - Maintain same IDs
Pattern 4: Replace with Validation
// 👇 Replace with data validation
Future<void> replaceUserWithValidation({
required int id,
required String name,
required String email,
int? age,
}) async {
// Validate data
if (name.length < 2) {
throw ArgumentError('Name must be at least 2 characters');
}
if (!email.contains('@')) {
throw ArgumentError('Invalid email format');
}
if (age != null && (age < 0 || age > 150)) {
throw ArgumentError('Invalid age');
}
// Replace user
await into(users).insert(
UsersCompanion.insert(
id: Value(id),
name: name,
email: email,
age: Value(age),
updatedAt: Value(DateTime.now()),
),
mode: InsertMode.replace,
);
}
Real-World Example
Complete e-commerce replace system
// lib/database/database.dart
@DriftDatabase(tables: [Users, Products, Orders, OrderItems])
class AppDatabase extends _$AppDatabase {
AppDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection());
@override
int get schemaVersion => 1;
// ==================== USER REPLACES ====================
// 👇 Replace user (sync from external source)
Future<void> replaceUserFromSync({
required int id,
required String name,
required String email,
int? age,
bool isActive = true,
}) async {
await into(users).insert(
UsersCompanion.insert(
id: Value(id),
name: name,
email: email,
age: Value(age),
isActive: Value(isActive),
updatedAt: Value(DateTime.now()),
),
mode: InsertMode.replace,
);
}
// 👇 Replace user with returning
Future<User?> replaceUserReturning({
required int id,
required String name,
required String email,
int? age,
}) async {
final results = await into(users).insertReturning(
UsersCompanion.insert(
id: Value(id),
name: name,
email: email,
age: Value(age),
updatedAt: Value(DateTime.now()),
),
mode: InsertMode.replace,
);
return results.isNotEmpty ? results.first : null;
}
// ==================== PRODUCT REPLACES ====================
// 👇 Replace product (from inventory sync)
Future<void> replaceProductFromInventory({
required int id,
required String sku,
required String name,
required double price,
required int stock,
String? description,
}) async {
await into(products).insert(
ProductsCompanion.insert(
id: Value(id),
sku: sku,
name: name,
price: price,
stock: Value(stock),
description: Value(description),
updatedAt: Value(DateTime.now()),
),
mode: InsertMode.replace,
);
}
// 👇 Replace multiple products
Future<void> replaceProductsBatch(List<ProductSyncData> products) async {
await transaction(() async {
for (final product in products) {
await into(products).insert(
ProductsCompanion.insert(
id: Value(product.id),
sku: product.sku,
name: product.name,
price: product.price,
stock: Value(product.stock),
description: Value(product.description),
updatedAt: Value(DateTime.now()),
),
mode: InsertMode.replace,
);
}
});
}
// 👇 Replace product with stock adjustment
Future<void> replaceProductWithStockHistory({
required int id,
required String sku,
required String name,
required double price,
required int newStock,
}) async {
// Get old stock before replace
final oldProduct = await (select(products)
..where((p) => p.id.equals(id)))
.getSingleOrNull();
final oldStock = oldProduct?.stock ?? 0;
// Replace product
await into(products).insert(
ProductsCompanion.insert(
id: Value(id),
sku: sku,
name: name,
price: price,
stock: Value(newStock),
updatedAt: Value(DateTime.now()),
),
mode: InsertMode.replace,
);
// Record stock change (if you have a stock history table)
if (oldStock != newStock) {
await _recordStockChange(id, oldStock, newStock);
}
}
Future<void> _recordStockChange(int productId, int oldStock, int newStock) async {
// Record stock change in history table
await customInsert(
'''
INSERT INTO stock_history (product_id, old_stock, new_stock, changed_at)
VALUES (?, ?, ?, ?)
''',
variables: [
Variable.withInt(productId),
Variable.withInt(oldStock),
Variable.withInt(newStock),
Variable.withString(DateTime.now().toIso8601String()),
],
).go();
}
// ==================== BULK REPLACE ====================
// 👇 Bulk replace (sync entire catalog)
Future<void> syncProductCatalog(List<CatalogItem> catalog) async {
await transaction(() async {
// Replace all products
for (final item in catalog) {
await into(products).insert(
ProductsCompanion.insert(
id: Value(item.id),
sku: item.sku,
name: item.name,
price: item.price,
stock: Value(item.stock),
description: Value(item.description),
isActive: Value(item.isActive),
updatedAt: Value(DateTime.now()),
),
mode: InsertMode.replace,
);
}
});
}
// 👇 Batch replace with insertAll
Future<void> syncUsersBatch(List<UserSyncData> users) async {
final companions = users.map((user) =>
UsersCompanion.insert(
id: Value(user.id),
name: user.name,
email: user.email,
age: Value(user.age),
isActive: Value(user.isActive),
updatedAt: Value(DateTime.now()),
)
).toList();
// insertAll with replace mode
await into(users).insertAll(
companions,
mode: InsertMode.replace,
);
}
// 👇 Replace or insert with conflict handling
Future<void> replaceOrUpdateProduct({
required int id,
required String sku,
required String name,
required double price,
int? stock,
}) async {
try {
// Try replace first
await into(products).insert(
ProductsCompanion.insert(
id: Value(id),
sku: sku,
name: name,
price: price,
stock: stock != null ? Value(stock) : const Value.absent(),
updatedAt: Value(DateTime.now()),
),
mode: InsertMode.replace,
);
} catch (e) {
// If replace fails, update instead
await (update(products)..where((p) => p.id.equals(id)))
.write(ProductsCompanion(
name: Value(name),
price: Value(price),
stock: stock != null ? Value(stock) : const Value.absent(),
updatedAt: Value(DateTime.now()),
));
}
}
// ==================== RELATIONSHIP REPLACE ====================
// 👇 Replace order with all its items
Future<void> replaceOrderWithItems({
required int orderId,
required int userId,
required List<OrderItemData> items,
required String shippingAddress,
}) async {
await transaction(() async {
// Calculate total
final total = items.fold(
0.0,
(sum, item) => sum + item.price * item.quantity,
);
// Replace order
await into(orders).insert(
OrdersCompanion.insert(
id: Value(orderId),
orderNumber: Value('ORD-$orderId'),
userId: userId,
total: total,
shippingAddress: shippingAddress,
status: Value('pending'),
updatedAt: Value(DateTime.now()),
),
mode: InsertMode.replace,
);
// Replace order items (delete old, insert new)
// First, delete existing items
await (delete(orderItems)..where((i) => i.orderId.equals(orderId))).go();
// Then insert new items
for (final item in items) {
await into(orderItems).insert(
OrderItemsCompanion.insert(
orderId: orderId,
productId: item.productId,
quantity: item.quantity,
unitPrice: item.price,
subtotal: item.price * item.quantity,
total: item.price * item.quantity,
),
);
}
});
}
// ==================== CACHE MANAGEMENT ====================
// 👇 Replace cache data
Future<void> updateProductCache(List<ProductCacheData> products) async {
// Replace all products in cache table
await into(productCache).insertAll(
products.map((p) => ProductCacheCompanion.insert(
id: Value(p.id),
name: p.name,
price: p.price,
stock: p.stock,
lastUpdated: Value(DateTime.now()),
)).toList(),
mode: InsertMode.replace,
);
}
// 👇 Incremental cache update
Future<void> updateCacheIncremental(List<ProductCacheData> products) async {
for (final product in products) {
// Check if exists
final existing = await (select(productCache)
..where((c) => c.id.equals(product.id)))
.getSingleOrNull();
if (existing == null) {
// Insert new
await into(productCache).insert(
ProductCacheCompanion.insert(
id: product.id,
name: product.name,
price: product.price,
stock: product.stock,
),
);
} else {
// Replace with latest
await into(productCache).insert(
ProductCacheCompanion.insert(
id: product.id,
name: product.name,
price: product.price,
stock: product.stock,
lastUpdated: Value(DateTime.now()),
),
mode: InsertMode.replace,
);
}
}
}
}
// ==================== DATA CLASSES ====================
class ProductSyncData {
final int id;
final String sku;
final String name;
final double price;
final int stock;
final String? description;
ProductSyncData({
required this.id,
required this.sku,
required this.name,
required this.price,
required this.stock,
this.description,
});
}
class UserSyncData {
final int id;
final String name;
final String email;
final int? age;
final bool isActive;
UserSyncData({
required this.id,
required this.name,
required this.email,
this.age,
this.isActive = true,
});
}
class CatalogItem {
final int id;
final String sku;
final String name;
final double price;
final int stock;
final String? description;
final bool isActive;
CatalogItem({
required this.id,
required this.sku,
required this.name,
required this.price,
required this.stock,
this.description,
this.isActive = true,
});
}
class OrderItemData {
final int productId;
final int quantity;
final double price;
OrderItemData({
required this.productId,
required this.quantity,
required this.price,
});
}
class ProductCacheData {
final int id;
final String name;
final double price;
final int stock;
ProductCacheData({
required this.id,
required this.name,
required this.price,
required this.stock,
});
}
Replace vs Update vs Upsert
When to use each operation
// 👇 UPDATE: Modify specific fields
await (update(users)..where((u) => u.id.equals(1)))
.write(UsersCompanion(
name: Value('John Updated'),
// Other fields stay the same
));
// 👇 UPSERT: Insert or update specific fields
await into(users).insert(
UsersCompanion.insert(
id: Value(1),
name: 'John Updated',
age: Value(30),
),
onConflict: DoUpdate(
target: users.id,
update: UsersCompanion(
name: Value('John Updated'),
age: Value(30),
),
),
);
// 👇 REPLACE: Delete and insert entire record
await into(users).insert(
UsersCompanion.insert(
id: Value(1),
name: 'John Updated',
email: 'john.new@example.com',
age: Value(30),
),
mode: InsertMode.replace,
);
Best Practices
- Use replace for synchronization – External data sources
- Use replace for complete updates – When all fields change
- Use update for partial updates – When only some fields change
- Use replace for cache updates – Refresh cached data
- Handle foreign keys – Cascade deletes will trigger
- Provide all required fields – Or ensure defaults exist
- Use transactions – For multiple replaces
- Test replace behavior – Understand what gets replaced
Common Mistakes
Mistake 1: Using replace when update is better
Wrong:
// 🚫 Replaces entire record unnecessarily
await into(users).insert(
UsersCompanion.insert(
id: Value(1),
name: 'John Updated',
// Missing other fields will use defaults
),
mode: InsertMode.replace,
);
Correct:
// ✅ Use update for partial changes
await (update(users)..where((u) => u.id.equals(1)))
.write(UsersCompanion(
name: Value('John Updated'),
// Other fields unchanged
));
Mistake 2: Forgetting foreign key cascade
Wrong:
// 🚫 May accidentally delete related data
await into(users).insert(
UsersCompanion.insert(
id: Value(1),
name: 'New Name',
),
mode: InsertMode.replace,
);
// If ON DELETE CASCADE is set, related data is deleted!
Correct:
// ✅ Check foreign key constraints first
// Or use ON DELETE RESTRICT to prevent accidental deletion
Mistake 3: Not providing all fields
Wrong:
// 🚫 Missing required fields may use defaults
await into(users).insert(
UsersCompanion.insert(
id: Value(1),
name: 'John',
// email not provided! May use default or fail
),
mode: InsertMode.replace,
);
Correct:
// ✅ Provide all required fields
await into(users).insert(
UsersCompanion.insert(
id: Value(1),
name: 'John',
email: 'john@example.com', // Always provide
),
mode: InsertMode.replace,
);
Summary
| Operation | Behavior | Best For |
|---|---|---|
| Update | Modify specific fields | Partial changes |
| Upsert | Insert or update | Insert with conflict handling |
| Replace | Delete + Insert | Complete record replacement |
Next Steps
Now you understand replace, let's dive deeper:
- Upsert – Insert or update with conflict handling
- Update – Updating existing records
- Delete – Deleting records
Did You Know?
-
Replace is INSERT OR REPLACE – Under the hood in SQLite
-
Replace deletes then inserts – Not a true update
-
Replace triggers DELETE and INSERT triggers – If you have them
-
Replace can cascade delete – With foreign key constraints
-
Replace resets auto-increment – When replacing primary keys
-
Replace is atomic – Happens in one database operation
-
Replace is not SQL-standard – It's a SQLite extension
-
Replace can be used with returning – Returns inserted data