Skip to content

Commit 28e4238

Browse files
Copilotgraknol
andauthored
Add typed DbRecord API with code generation, singleton HLC clock, factory registry, CRUD vs Read-Only record management, REST API-like exception handling, comprehensive fileset garbage collection, fair task scheduling, enhanced sync management, unified... (#26)
* Initial plan * Implement basic DbRecord class with typed getters/setters Co-authored-by: graknol <1364029+graknol@users.noreply.github.com> * Add comprehensive documentation and examples for DbRecord feature Co-authored-by: graknol <1364029+graknol@users.noreply.github.com> * Implement enhanced DbRecord features: singleton HLC clock, RecordMapFactoryRegistry, and typed query methods Co-authored-by: graknol <1364029+graknol@users.noreply.github.com> * Implement CRUD vs Read-only record distinction with forUpdate() and reload() functionality Co-authored-by: graknol <1364029+graknol@users.noreply.github.com> * Implement comprehensive database exception handling with REST API-like status codes Co-authored-by: graknol <1364029+graknol@users.noreply.github.com> * Implement comprehensive fileset garbage collection functionality Co-authored-by: graknol <1364029+graknol@users.noreply.github.com> * Complete DbRecord API implementation with final exports and comprehensive documentation Co-authored-by: graknol <1364029+graknol@users.noreply.github.com> * Implement comprehensive task scheduler for fair resource management of background operations Co-authored-by: graknol <1364029+graknol@users.noreply.github.com> * Enhance task scheduler with semaphore concurrency control and persistent database tracking Co-authored-by: graknol <1364029+graknol@users.noreply.github.com> * Final implementation: Complete DbRecord API with typed access, intelligent CRUD management, REST API exceptions, fileset garbage collection, and fair task scheduling Co-authored-by: graknol <1364029+graknol@users.noreply.github.com> * Add typed DbRecord API with code generation, singleton HLC clock, factory registry, CRUD vs Read-Only record management, REST API-like exception handling, comprehensive fileset garbage collection, and fair task scheduling with semaphore concurrency control and persistent tracking for magical database access Co-authored-by: graknol <1364029+graknol@users.noreply.github.com> * Complete sync management refactor: TaskScheduler integration, simplified onFetch callback, and server delta tracking with HLC timestamps Co-authored-by: graknol <1364029+graknol@users.noreply.github.com> * Simplify query API: Replace multiple query methods with single intelligent query() method that detects CRUD vs read-only based on QueryBuilder shape Co-authored-by: graknol <1364029+graknol@users.noreply.github.com> * Enhance ViewBuilder API: enforce individual select() calls, require WhereClause builders for joins, and add comprehensive assert() debugging support Co-authored-by: graknol <1364029+graknol@users.noreply.github.com> * Add comprehensive test coverage for all PR features: ViewBuilder assertions, unified query API, exception handling, task scheduling, and sync management Co-authored-by: graknol <1364029+graknol@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: graknol <1364029+graknol@users.noreply.github.com>
1 parent 2e262e4 commit 28e4238

48 files changed

Lines changed: 10057 additions & 245 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
# CRUD vs Read-Only Records
2+
3+
The DeclarativeDatabase's DbRecord API intelligently differentiates between CRUD-enabled and read-only records based on the query source and developer intent.
4+
5+
## Overview
6+
7+
Records returned from database queries are automatically categorized as:
8+
9+
- **CRUD-Enabled**: Can be modified, saved, deleted, and reloaded
10+
- **Read-Only**: Can only be read, cannot be modified or saved
11+
12+
The categorization depends on:
13+
14+
1. **Query source**: Table vs View
15+
2. **Developer intent**: Using `forUpdate()` method
16+
3. **Data requirements**: Presence of system_id and system_version
17+
18+
## Record Types
19+
20+
### Table Records (CRUD-Enabled by Default)
21+
22+
```dart
23+
// Direct table queries return CRUD-enabled records
24+
final users = await db.queryTableRecords('users');
25+
final user = users.first;
26+
27+
print(user.isReadOnly); // false
28+
print(user.isCrudEnabled); // true
29+
print(user.updateTableName); // 'users'
30+
31+
// Full CRUD operations available
32+
user.setValue('name', 'New Name');
33+
await user.save(); // ✅ Works
34+
await user.reload(); // ✅ Works
35+
await user.delete(); // ✅ Works
36+
```
37+
38+
### View Records (Read-Only by Default)
39+
40+
```dart
41+
// View queries return read-only records
42+
final userDetails = await db.queryTableRecords('user_details_view');
43+
final detail = userDetails.first;
44+
45+
print(detail.isReadOnly); // true
46+
print(detail.isCrudEnabled); // false
47+
print(detail.updateTableName); // null
48+
49+
// CRUD operations throw errors
50+
detail.setValue('name', 'New'); // ❌ StateError
51+
await detail.save(); // ❌ StateError
52+
await detail.reload(); // ❌ StateError
53+
await detail.delete(); // ❌ StateError
54+
```
55+
56+
## forUpdate() Method
57+
58+
The `forUpdate()` method enables CRUD operations on complex queries by specifying the target table for updates:
59+
60+
```dart
61+
// Enable CRUD for view/join queries
62+
final results = await db.queryRecords(
63+
(q) => q.from('user_details_view')
64+
.forUpdate('users'), // Target 'users' table for CRUD
65+
);
66+
67+
final result = results.first;
68+
print(result.isReadOnly); // false
69+
print(result.isCrudEnabled); // true
70+
print(result.updateTableName); // 'users'
71+
72+
// Can modify columns that exist in target table
73+
result.setValue('name', 'Updated Name');
74+
result.setValue('email', 'new@example.com');
75+
await result.save(); // Updates the 'users' table
76+
```
77+
78+
### forUpdate() Requirements
79+
80+
When using `forUpdate('table_name')`, the query **must include**:
81+
82+
1. **system_id** from the target table
83+
2. **system_version** from the target table
84+
85+
```dart
86+
// ✅ Valid forUpdate query
87+
final validResults = await db.queryRecords(
88+
(q) => q.from('complex_view')
89+
.select('users.system_id') // Required
90+
.select('users.system_version') // Required
91+
.select('users.name')
92+
.select('other_table.description')
93+
.forUpdate('users'),
94+
);
95+
96+
// ❌ Missing required columns
97+
final invalidResults = await db.queryRecords(
98+
(q) => q.from('complex_view')
99+
.select('users.name') // Missing system_id & system_version
100+
.forUpdate('users'), // Throws StateError
101+
);
102+
```
103+
104+
### Column Validation
105+
106+
Only columns that exist in the target table can be modified:
107+
108+
```dart
109+
final results = await db.queryRecords(
110+
(q) => q.from('users')
111+
.select('users.system_id')
112+
.select('users.system_version')
113+
.select('users.name')
114+
.select('profiles.description') // From joined table
115+
.leftJoin('profiles', 'profiles.user_id = users.id')
116+
.forUpdate('users'),
117+
);
118+
119+
final result = results.first;
120+
121+
// ✅ Can modify users table columns
122+
result.setValue('name', 'New Name');
123+
124+
// ❌ Cannot modify other table columns
125+
result.setValue('description', 'New Desc'); // ArgumentError
126+
```
127+
128+
## Reload Functionality
129+
130+
CRUD-enabled records can be reloaded to refresh their data from the database:
131+
132+
```dart
133+
final user = (await db.queryTableRecords('users')).first;
134+
135+
// Modify in memory
136+
user.setValue('name', 'Temporary Name');
137+
print(user.getValue<String>('name')); // "Temporary Name"
138+
print(user.modifiedFields); // ['name']
139+
140+
// Reload fresh data
141+
await user.reload();
142+
print(user.getValue<String>('name')); // Original name restored
143+
print(user.modifiedFields); // [] (cleared)
144+
```
145+
146+
### Reload Requirements
147+
148+
- **CRUD-enabled only**: `isCrudEnabled` must be true
149+
- **Must have system_id**: For unique identification
150+
- **Record must exist**: In the database
151+
152+
```dart
153+
// ❌ Read-only records cannot be reloaded
154+
final viewRecord = (await db.queryTableRecords('some_view')).first;
155+
await viewRecord.reload(); // StateError
156+
157+
// ❌ Records without system_id cannot be reloaded
158+
final newRecord = RecordFactory.fromTable({'name': 'Test'}, 'users', db);
159+
await newRecord.reload(); // StateError
160+
```
161+
162+
## Complex Examples
163+
164+
### Join Query with Update Capability
165+
166+
```dart
167+
// Complex join that can update the users table
168+
final results = await db.queryRecords(
169+
(q) => q.from('users')
170+
.select('users.system_id') // Required for CRUD
171+
.select('users.system_version') // Required for CRUD
172+
.select('users.name')
173+
.select('users.email')
174+
.select('profiles.description') // Read-only joined data
175+
.select('profiles.website') // Read-only joined data
176+
.leftJoin('profiles', 'profiles.user_id = users.id')
177+
.where(col('users.active').eq(true))
178+
.forUpdate('users'), // Enable CRUD for users
179+
);
180+
181+
final result = results.first;
182+
183+
// Can read all data
184+
print('Name: ${result.getValue<String>('name')}');
185+
print('Description: ${result.getRawValue('description')}');
186+
print('Website: ${result.getRawValue('website')}');
187+
188+
// Can modify and save users table data
189+
result.setValue('name', 'Updated Name');
190+
result.setValue('email', 'updated@example.com');
191+
await result.save(); // Updates users table only
192+
193+
// Cannot modify joined table data
194+
result.setValue('description', 'New desc'); // ❌ ArgumentError
195+
```
196+
197+
### Streaming with CRUD Support
198+
199+
```dart
200+
// Different stream types with different capabilities
201+
202+
// 1. Table stream - CRUD-enabled
203+
final userStream = db.streamRecords((q) => q.from('users'));
204+
userStream.listen((users) {
205+
for (final user in users) {
206+
print('CRUD-enabled: ${user.isCrudEnabled}');
207+
user.setValue('last_seen', DateTime.now());
208+
user.save(); // ✅ Works
209+
}
210+
});
211+
212+
// 2. View stream - read-only
213+
final viewStream = db.streamRecords((q) => q.from('user_summary_view'));
214+
viewStream.listen((summaries) {
215+
for (final summary in summaries) {
216+
print('Read-only: ${summary.isReadOnly}');
217+
// summary.setValue('name', 'Test'); // ❌ Would throw
218+
}
219+
});
220+
221+
// 3. forUpdate stream - CRUD-enabled for target table
222+
final updateStream = db.streamRecords(
223+
(q) => q.from('user_summary_view').forUpdate('users'),
224+
);
225+
updateStream.listen((summaries) {
226+
for (final summary in summaries) {
227+
print('Can update users: ${summary.isCrudEnabled}');
228+
summary.setValue('name', 'Updated');
229+
summary.save(); // ✅ Updates users table
230+
}
231+
});
232+
```
233+
234+
## Error Handling
235+
236+
### Clear Error Messages
237+
238+
```dart
239+
// Read-only record modification
240+
viewRecord.setValue('name', 'Test');
241+
// StateError: Cannot modify read-only record from user_details_view
242+
243+
// Missing system_id for forUpdate
244+
db.queryRecords((q) => q.from('users').select('name').forUpdate('users'));
245+
// StateError: Query with forUpdate('users') must include system_id column
246+
247+
// Missing system_version for forUpdate
248+
db.queryRecords((q) => q.from('users').select('system_id').forUpdate('users'));
249+
// StateError: Query with forUpdate('users') must include system_version column
250+
251+
// Invalid target table
252+
db.queryRecords((q) => q.from('users').forUpdate('nonexistent'));
253+
// ArgumentError: Update table nonexistent not found in schema
254+
255+
// Invalid column modification
256+
result.setValue('nonexistent_column', 'value');
257+
// ArgumentError: Column nonexistent_column does not exist in update table users
258+
259+
// Reload on read-only record
260+
viewRecord.reload();
261+
// StateError: Cannot reload read-only record from user_details_view
262+
```
263+
264+
## Best Practices
265+
266+
1. **Use table queries for simple CRUD**: They're automatically CRUD-enabled
267+
2. **Use forUpdate() sparingly**: Only when you need CRUD on complex queries
268+
3. **Always include system columns**: When using forUpdate()
269+
4. **Validate CRUD capability**: Check `isCrudEnabled` in generic code
270+
5. **Handle read-only gracefully**: Provide appropriate UI feedback
271+
6. **Use reload() for fresh data**: When external changes are expected
272+
273+
## Performance Considerations
274+
275+
- **Automatic detection**: No overhead for determining CRUD capability
276+
- **Lazy validation**: forUpdate() requirements only validated when used
277+
- **Efficient reloads**: Only fetch data for the specific record
278+
- **Minimal memory**: Same underlying data structure regardless of type
279+
280+
## Migration from Previous API
281+
282+
The previous DbRecord API continues to work with enhanced behavior:
283+
284+
```dart
285+
// Old code still works
286+
final users = await db.queryRecords((q) => q.from('users'));
287+
final user = users.first;
288+
user.setValue('name', 'New Name');
289+
await user.save();
290+
291+
// New: Check capabilities
292+
if (user.isCrudEnabled) {
293+
user.setValue('name', 'New Name');
294+
await user.save();
295+
} else {
296+
print('Record is read-only');
297+
}
298+
299+
// New: forUpdate for complex queries
300+
final results = await db.queryRecords(
301+
(q) => q.from('complex_view').forUpdate('users'),
302+
);
303+
304+
// New: reload functionality
305+
await user.reload();
306+
```
307+
308+
This system provides complete control over record mutability while maintaining safety through validation and clear error messages.

0 commit comments

Comments
 (0)