Skip to content

Commit 3fb57df

Browse files
committed
Update version to 1.3.0
1 parent 5d30922 commit 3fb57df

File tree

13 files changed

+385
-18
lines changed

13 files changed

+385
-18
lines changed

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ The foundation of the ecosystem. It provides all the core functionality for sche
1818
- **Type-Safe Queries**: Build complex SQL queries with type safety and autocompletion.
1919
- **Streaming Queries**: Create reactive queries that automatically emit new results when underlying data changes.
2020
- **Conflict-Free Sync**: Built-in support for data synchronization using a Hybrid Logical Clock (HLC) to ensure conflict-free, last-write-wins data merging.
21+
- **Graceful Error Handling**: Robust constraint violation handling during bulk operations with configurable strategies.
2122
- **File Management**: Integrated support for attaching and managing files linked to database records.
2223

2324
### 📱 Flutter Integration (`declarative_sqlite_flutter`)
@@ -48,12 +49,12 @@ To get started, add the necessary packages to your `pubspec.yaml`:
4849
dependencies:
4950
flutter:
5051
sdk: flutter
51-
declarative_sqlite: ^1.0.2
52-
declarative_sqlite_flutter: ^1.0.2
52+
declarative_sqlite: ^1.3.0
53+
declarative_sqlite_flutter: ^1.3.0
5354

5455
dev_dependencies:
5556
build_runner: ^2.4.10
56-
declarative_sqlite_generator: ^1.0.2
57+
declarative_sqlite_generator: ^1.3.0
5758
```
5859
5960
For a detailed guide, please refer to our [**official documentation**](https://graknol.github.io/declarative_sqlite/).

declarative_sqlite/CHANGELOG.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,28 @@
1+
## 1.3.0
2+
3+
### Features
4+
- **Constraint Violation Handling in bulkLoad**: Added graceful constraint violation handling with `ConstraintViolationStrategy`
5+
- `throwException` (default): Maintains existing behavior - throws on constraint violations
6+
- `skip`: Silently skips problematic rows and continues processing valid ones
7+
- Comprehensive constraint detection for unique, primary key, check, foreign key, and NOT NULL violations
8+
- Detailed logging for monitoring and debugging constraint violation events
9+
- **Fixed Unique Constraint Generation**: Resolved issue where unique keys weren't properly creating `CREATE UNIQUE INDEX` statements
10+
- Unique constraints now generate proper SQL: `CREATE UNIQUE INDEX uniq_table_column ON table (column)`
11+
- Improved migration script generation to handle both regular indexes and unique constraints
12+
13+
### Developer Experience
14+
- Enhanced bulkLoad method for server synchronization scenarios
15+
- Better error handling and logging for constraint violations
16+
- Safer bulk loading operations with granular control over error handling
17+
18+
### Bug Fixes
19+
- Fixed missing unique constraint generation in schema migration scripts
20+
- Improved constraint violation detection and categorization
21+
22+
### Documentation
23+
- Updated bulkLoad method documentation with constraint violation handling examples
24+
- Enhanced migration guide with unique constraint best practices
25+
126
## 1.2.0
227

328
### Features

declarative_sqlite/lib/declarative_sqlite.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,12 @@ export 'src/record_map_factory_registry.dart';
2929
export 'src/files/filesystem_file_repository.dart';
3030

3131
// Schema classes
32-
export 'src/schema/db_table.dart';
3332
export 'src/schema/schema.dart';
33+
export 'src/schema/db_table.dart';
34+
export 'src/schema/db_view.dart';
35+
export 'src/schema/db_column.dart';
36+
export 'src/schema/db_key.dart';
37+
export 'src/schema/live_schema.dart';
3438

3539
// Streaming queries
3640
export 'src/streaming/query_dependency_analyzer.dart';

declarative_sqlite/lib/src/declarative_database.dart

Lines changed: 96 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,15 @@ import 'sync/hlc.dart';
2626
import 'sync/dirty_row_store.dart';
2727
import 'sync/dirty_row.dart';
2828

29+
/// Strategy for handling constraint violations during bulk load operations
30+
enum ConstraintViolationStrategy {
31+
/// Throw the original exception (default behavior)
32+
throwException,
33+
34+
/// Silently skip the problematic row and continue processing
35+
skip,
36+
}
37+
2938
/// A declarative SQLite database.
3039
class DeclarativeDatabase {
3140
/// The underlying sqflite database.
@@ -1085,8 +1094,23 @@ class DeclarativeDatabase {
10851094
///
10861095
/// Rows processed by this method are NOT marked as dirty, as they represent
10871096
/// data coming from the server rather than local changes to be synchronized.
1097+
///
1098+
/// ## Constraint Violation Handling
1099+
///
1100+
/// The [onConstraintViolation] parameter controls how constraint violations
1101+
/// are handled when they occur:
1102+
///
1103+
/// - `ConstraintViolationStrategy.throwException` (default): Throws the original exception
1104+
/// - `ConstraintViolationStrategy.skip`: Silently skips the problematic row
1105+
///
1106+
/// The `skip` strategy is useful when loading data from a server where some
1107+
/// rows might conflict with local data, and you want to preserve existing
1108+
/// local data while still loading non-conflicting rows.
10881109
Future<void> bulkLoad(
1089-
String tableName, List<Map<String, Object?>> rows) async {
1110+
String tableName,
1111+
List<Map<String, Object?>> rows, {
1112+
ConstraintViolationStrategy onConstraintViolation = ConstraintViolationStrategy.throwException,
1113+
}) async {
10901114
final tableDef = _getTableDefinition(tableName);
10911115
final pkColumns = tableDef.keys
10921116
.where((k) => k.isPrimary)
@@ -1144,17 +1168,45 @@ class DeclarativeDatabase {
11441168
}
11451169

11461170
if (valuesToUpdate.isNotEmpty) {
1147-
await _update(
1148-
tableName,
1149-
valuesToUpdate,
1150-
now,
1151-
where: 'system_id = ?',
1152-
whereArgs: [systemId],
1153-
);
1171+
try {
1172+
await _update(
1173+
tableName,
1174+
valuesToUpdate,
1175+
now,
1176+
where: 'system_id = ?',
1177+
whereArgs: [systemId],
1178+
);
1179+
} catch (e) {
1180+
if (_isConstraintViolation(e)) {
1181+
await _handleConstraintViolation(
1182+
onConstraintViolation,
1183+
e,
1184+
tableName,
1185+
'UPDATE',
1186+
row,
1187+
);
1188+
} else {
1189+
rethrow;
1190+
}
1191+
}
11541192
}
11551193
} else {
11561194
// INSERT logic - mark as server origin
1157-
await _insertFromServer(tableName, row, hlcClock.now());
1195+
try {
1196+
await _insertFromServer(tableName, row, hlcClock.now());
1197+
} catch (e) {
1198+
if (_isConstraintViolation(e)) {
1199+
await _handleConstraintViolation(
1200+
onConstraintViolation,
1201+
e,
1202+
tableName,
1203+
'INSERT',
1204+
row,
1205+
);
1206+
} else {
1207+
rethrow;
1208+
}
1209+
}
11581210
}
11591211
}
11601212

@@ -1171,6 +1223,41 @@ class DeclarativeDatabase {
11711223
}
11721224
return await dirtyRowStore!.getAll();
11731225
}
1226+
1227+
/// Checks if an exception is a constraint violation
1228+
bool _isConstraintViolation(dynamic exception) {
1229+
final errorMessage = exception.toString().toLowerCase();
1230+
return errorMessage.contains('constraint') ||
1231+
errorMessage.contains('unique') ||
1232+
errorMessage.contains('primary key') ||
1233+
errorMessage.contains('foreign key') ||
1234+
errorMessage.contains('check constraint') ||
1235+
errorMessage.contains('not null constraint') ||
1236+
errorMessage.contains('sqlite_constraint');
1237+
}
1238+
1239+
/// Handles constraint violations based on the specified strategy
1240+
Future<void> _handleConstraintViolation(
1241+
ConstraintViolationStrategy strategy,
1242+
dynamic exception,
1243+
String tableName,
1244+
String operation,
1245+
Map<String, Object?> row,
1246+
) async {
1247+
switch (strategy) {
1248+
case ConstraintViolationStrategy.throwException:
1249+
throw exception;
1250+
1251+
case ConstraintViolationStrategy.skip:
1252+
// Log the skip and continue silently
1253+
developer.log(
1254+
'⚠️ Skipping row due to constraint violation in $operation on table $tableName: ${exception.toString()}',
1255+
name: 'BulkLoadConstraintViolation',
1256+
level: 900, // WARNING level
1257+
);
1258+
return;
1259+
}
1260+
}
11741261
}
11751262

11761263
// Static helper methods for settings

declarative_sqlite/lib/src/migration/generate_migration_scripts.dart

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,17 @@ List<String> _generateCreateTableScripts(CreateTable change) {
8383
scripts.add(_generateCreateTableScript(change));
8484

8585
final indexKeys = table.keys.where((k) => k.type == KeyType.indexed);
86+
final uniqueKeys = table.keys.where((k) => k.type == KeyType.unique);
87+
8688
if (indexKeys.isNotEmpty) {
8789
developer.log(' 📊 Creating ${indexKeys.length} indexes for table ${table.name}', name: 'Migration');
8890
}
8991

92+
if (uniqueKeys.isNotEmpty) {
93+
developer.log(' 📊 Creating ${uniqueKeys.length} unique constraints for table ${table.name}', name: 'Migration');
94+
}
95+
96+
// Create regular indexes
9097
for (final key in indexKeys) {
9198
var indexName = 'idx_${table.name}_${key.columns.join('_')}';
9299
if (indexName.length > 62) {
@@ -101,6 +108,21 @@ List<String> _generateCreateTableScripts(CreateTable change) {
101108
'CREATE INDEX $indexName ON ${table.name} (${key.columns.join(', ')});');
102109
}
103110

111+
// Create unique indexes
112+
for (final key in uniqueKeys) {
113+
var indexName = 'uniq_${table.name}_${key.columns.join('_')}';
114+
if (indexName.length > 62) {
115+
final hash =
116+
sha1.convert(utf8.encode(indexName)).toString().substring(0, 10);
117+
indexName = 'uniq_${table.name}_$hash';
118+
developer.log(' • Unique index name truncated: $indexName (${key.columns.join(', ')})', name: 'Migration');
119+
} else {
120+
developer.log(' • Unique index: $indexName (${key.columns.join(', ')})', name: 'Migration');
121+
}
122+
scripts.add(
123+
'CREATE UNIQUE INDEX $indexName ON ${table.name} (${key.columns.join(', ')});');
124+
}
125+
104126
return scripts;
105127
}
106128

declarative_sqlite/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: declarative_sqlite
22
description: A dart package for declaratively creating SQLite tables and automatically migrating them.
3-
version: 1.2.0
3+
version: 1.3.0
44
repository: https://github.com/graknol/declarative_sqlite
55

66
environment:
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import 'package:declarative_sqlite/declarative_sqlite.dart';
2+
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
3+
import 'package:test/test.dart';
4+
5+
void main() {
6+
sqfliteFfiInit();
7+
8+
late DeclarativeDatabase database;
9+
10+
setUp(() async {
11+
sqfliteFfiInit();
12+
13+
final schema = SchemaBuilder()
14+
.table('users', (table) {
15+
table.guid('id').notNull('default-id');
16+
table.text('name').notNull('Default Name');
17+
table.text('email').notNull('[email protected]');
18+
table.key(['id']).primary();
19+
table.key(['email']).unique(); // This will cause constraint violations
20+
})
21+
.build();
22+
23+
database = await DeclarativeDatabase.open(
24+
':memory:',
25+
databaseFactory: databaseFactoryFfi,
26+
schema: schema,
27+
fileRepository: FilesystemFileRepository('temp_test'),
28+
);
29+
});
30+
31+
tearDown(() async {
32+
await database.close();
33+
});
34+
35+
group('Bulk Load Constraint Violation Handling', () {
36+
test('throwException strategy throws on constraint violation', () async {
37+
// Insert initial data
38+
await database.insert('users', {
39+
'id': 'user-1',
40+
'name': 'John Doe',
41+
'email': '[email protected]',
42+
});
43+
44+
// Try to bulk load data with duplicate email (should throw)
45+
expect(() async {
46+
await database.bulkLoad(
47+
'users',
48+
[
49+
{
50+
'system_id': 'unique-server-user-1', // Different system_id to force INSERT
51+
'id': 'user-2',
52+
'name': 'Jane Doe',
53+
'email': '[email protected]', // Duplicate email
54+
}
55+
],
56+
onConstraintViolation: ConstraintViolationStrategy.throwException,
57+
);
58+
}, throwsException);
59+
});
60+
61+
test('skip strategy silently skips constraint violations', () async {
62+
// Insert initial data
63+
await database.insert('users', {
64+
'id': 'user-1',
65+
'name': 'John Doe',
66+
'email': '[email protected]',
67+
});
68+
69+
// Bulk load data with duplicate email (should skip silently)
70+
await database.bulkLoad(
71+
'users',
72+
[
73+
{
74+
'system_id': 'unique-server-user-2', // Different system_id to force INSERT
75+
'id': 'user-2',
76+
'name': 'Jane Doe',
77+
'email': '[email protected]', // Duplicate email - should be skipped
78+
},
79+
{
80+
'system_id': 'unique-server-user-3', // Different system_id to force INSERT
81+
'id': 'user-3',
82+
'name': 'Bob Smith',
83+
'email': '[email protected]', // Valid email - should work
84+
}
85+
],
86+
onConstraintViolation: ConstraintViolationStrategy.skip,
87+
);
88+
89+
// Verify that only the valid row was inserted
90+
final users = await database.queryMaps((q) => q.from('users'));
91+
expect(users.length, equals(2)); // Original + 1 valid new row
92+
93+
final emails = users.map((u) => u['email']).toList();
94+
expect(emails, contains('[email protected]'));
95+
expect(emails, contains('[email protected]'));
96+
expect(emails, isNot(contains('[email protected]'))); // Jane should be skipped
97+
});
98+
99+
test('default behavior is to throw exceptions', () async {
100+
// Insert initial data
101+
await database.insert('users', {
102+
'id': 'user-1',
103+
'name': 'John Doe',
104+
'email': '[email protected]',
105+
});
106+
107+
// Try to bulk load data with duplicate email (should throw by default)
108+
expect(() async {
109+
await database.bulkLoad('users', [
110+
{
111+
'system_id': 'unique-server-user-4', // Different system_id to force INSERT
112+
'id': 'user-2',
113+
'name': 'Jane Doe',
114+
'email': '[email protected]', // Duplicate email
115+
}
116+
]);
117+
}, throwsException);
118+
});
119+
});
120+
}

declarative_sqlite_flutter/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
## 1.3.0
2+
3+
### Updates
4+
- Updated to use declarative_sqlite ^1.3.0
5+
- Enhanced compatibility with new constraint violation handling features
6+
- Improved data safety when working with server synchronization and constraint violations in Flutter widgets
7+
- Fixed the `recreateDatabase` parameter not getting passed from the `DatabaseProvider` widget to the `DeclarativeDatabase.open(...)` call
8+
19
## 1.2.0
210

311
### Updates

declarative_sqlite_flutter/lib/src/database_provider.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ class _DatabaseProviderState extends State<DatabaseProvider> {
164164
'files',
165165
),
166166
),
167+
recreateDatabase: widget.recreateDatabase,
167168
);
168169
setState(() {
169170
_database = db;

0 commit comments

Comments
 (0)