dzql 0.5.33 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.sample +28 -0
- package/compose.yml +28 -0
- package/dist/client/index.ts +1 -0
- package/dist/client/stores/useMyProfileStore.ts +114 -0
- package/dist/client/stores/useOrgDashboardStore.ts +131 -0
- package/dist/client/stores/useVenueDetailStore.ts +117 -0
- package/dist/client/ws.ts +716 -0
- package/dist/db/migrations/000_core.sql +92 -0
- package/dist/db/migrations/20251229T212912022Z_schema.sql +3020 -0
- package/dist/db/migrations/20251229T212912022Z_subscribables.sql +371 -0
- package/dist/runtime/manifest.json +1562 -0
- package/docs/README.md +293 -36
- package/docs/feature-requests/applyPatch-bug-report.md +85 -0
- package/docs/feature-requests/connection-ready-profile.md +57 -0
- package/docs/feature-requests/hidden-bug-report.md +111 -0
- package/docs/feature-requests/hidden-fields-subscribables.md +34 -0
- package/docs/feature-requests/subscribable-param-key-bug.md +38 -0
- package/docs/feature-requests/todo.md +146 -0
- package/docs/for_ai.md +641 -0
- package/docs/project-setup.md +432 -0
- package/examples/blog.ts +50 -0
- package/examples/invalid.ts +18 -0
- package/examples/venues.js +485 -0
- package/package.json +23 -60
- package/src/cli/codegen/client.ts +99 -0
- package/src/cli/codegen/manifest.ts +95 -0
- package/src/cli/codegen/pinia.ts +174 -0
- package/src/cli/codegen/realtime.ts +58 -0
- package/src/cli/codegen/sql.ts +698 -0
- package/src/cli/codegen/subscribable_sql.ts +547 -0
- package/src/cli/codegen/subscribable_store.ts +184 -0
- package/src/cli/codegen/types.ts +142 -0
- package/src/cli/compiler/analyzer.ts +52 -0
- package/src/cli/compiler/graph_rules.ts +251 -0
- package/src/cli/compiler/ir.ts +233 -0
- package/src/cli/compiler/loader.ts +132 -0
- package/src/cli/compiler/permissions.ts +227 -0
- package/src/cli/index.ts +164 -0
- package/src/client/index.ts +1 -0
- package/src/client/ws.ts +286 -0
- package/src/create/.env.example +8 -0
- package/src/create/README.md +101 -0
- package/src/create/compose.yml +14 -0
- package/src/create/domain.ts +153 -0
- package/src/create/package.json +24 -0
- package/src/create/server.ts +18 -0
- package/src/create/setup.sh +11 -0
- package/src/create/tsconfig.json +15 -0
- package/src/runtime/auth.ts +39 -0
- package/src/runtime/db.ts +33 -0
- package/src/runtime/errors.ts +51 -0
- package/src/runtime/index.ts +98 -0
- package/src/runtime/js_functions.ts +63 -0
- package/src/runtime/manifest_loader.ts +29 -0
- package/src/runtime/namespace.ts +483 -0
- package/src/runtime/server.ts +87 -0
- package/src/runtime/ws.ts +197 -0
- package/src/shared/ir.ts +197 -0
- package/tests/client.test.ts +38 -0
- package/tests/codegen.test.ts +71 -0
- package/tests/compiler.test.ts +45 -0
- package/tests/graph_rules.test.ts +173 -0
- package/tests/integration/db.test.ts +174 -0
- package/tests/integration/e2e.test.ts +65 -0
- package/tests/integration/features.test.ts +922 -0
- package/tests/integration/full_stack.test.ts +262 -0
- package/tests/integration/setup.ts +45 -0
- package/tests/ir.test.ts +32 -0
- package/tests/namespace.test.ts +395 -0
- package/tests/permissions.test.ts +55 -0
- package/tests/pinia.test.ts +48 -0
- package/tests/realtime.test.ts +22 -0
- package/tests/runtime.test.ts +80 -0
- package/tests/subscribable_gen.test.ts +72 -0
- package/tests/subscribable_reactivity.test.ts +258 -0
- package/tests/venues_gen.test.ts +25 -0
- package/tsconfig.json +20 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/README.md +0 -90
- package/bin/cli.js +0 -727
- package/docs/compiler/ADVANCED_FILTERS.md +0 -183
- package/docs/compiler/CODING_STANDARDS.md +0 -415
- package/docs/compiler/COMPARISON.md +0 -673
- package/docs/compiler/QUICKSTART.md +0 -326
- package/docs/compiler/README.md +0 -134
- package/docs/examples/README.md +0 -38
- package/docs/examples/blog.sql +0 -160
- package/docs/examples/venue-detail-simple.sql +0 -8
- package/docs/examples/venue-detail-subscribable.sql +0 -45
- package/docs/for-ai/claude-guide.md +0 -1210
- package/docs/getting-started/quickstart.md +0 -125
- package/docs/getting-started/subscriptions-quick-start.md +0 -203
- package/docs/getting-started/tutorial.md +0 -1104
- package/docs/guides/atomic-updates.md +0 -299
- package/docs/guides/client-stores.md +0 -730
- package/docs/guides/composite-primary-keys.md +0 -158
- package/docs/guides/custom-functions.md +0 -362
- package/docs/guides/drop-semantics.md +0 -554
- package/docs/guides/field-defaults.md +0 -240
- package/docs/guides/interpreter-vs-compiler.md +0 -237
- package/docs/guides/many-to-many.md +0 -929
- package/docs/guides/subscriptions.md +0 -537
- package/docs/reference/api.md +0 -1373
- package/docs/reference/client.md +0 -224
- package/src/client/stores/index.js +0 -8
- package/src/client/stores/useAppStore.js +0 -285
- package/src/client/stores/useWsStore.js +0 -289
- package/src/client/ws.js +0 -762
- package/src/compiler/cli/compile-example.js +0 -33
- package/src/compiler/cli/compile-subscribable.js +0 -43
- package/src/compiler/cli/debug-compile.js +0 -44
- package/src/compiler/cli/debug-parse.js +0 -26
- package/src/compiler/cli/debug-path-parser.js +0 -18
- package/src/compiler/cli/debug-subscribable-parser.js +0 -21
- package/src/compiler/cli/index.js +0 -174
- package/src/compiler/codegen/auth-codegen.js +0 -153
- package/src/compiler/codegen/drop-semantics-codegen.js +0 -553
- package/src/compiler/codegen/graph-rules-codegen.js +0 -450
- package/src/compiler/codegen/notification-codegen.js +0 -232
- package/src/compiler/codegen/operation-codegen.js +0 -1382
- package/src/compiler/codegen/permission-codegen.js +0 -318
- package/src/compiler/codegen/subscribable-codegen.js +0 -827
- package/src/compiler/compiler.js +0 -371
- package/src/compiler/index.js +0 -11
- package/src/compiler/parser/entity-parser.js +0 -440
- package/src/compiler/parser/path-parser.js +0 -290
- package/src/compiler/parser/subscribable-parser.js +0 -244
- package/src/database/dzql-core.sql +0 -161
- package/src/database/migrations/001_schema.sql +0 -60
- package/src/database/migrations/002_functions.sql +0 -890
- package/src/database/migrations/003_operations.sql +0 -1135
- package/src/database/migrations/004_search.sql +0 -581
- package/src/database/migrations/005_entities.sql +0 -730
- package/src/database/migrations/006_auth.sql +0 -94
- package/src/database/migrations/007_events.sql +0 -133
- package/src/database/migrations/008_hello.sql +0 -18
- package/src/database/migrations/008a_meta.sql +0 -172
- package/src/database/migrations/009_subscriptions.sql +0 -240
- package/src/database/migrations/010_atomic_updates.sql +0 -157
- package/src/database/migrations/010_fix_m2m_events.sql +0 -94
- package/src/index.js +0 -40
- package/src/server/api.js +0 -9
- package/src/server/db.js +0 -442
- package/src/server/index.js +0 -317
- package/src/server/logger.js +0 -259
- package/src/server/mcp.js +0 -594
- package/src/server/meta-route.js +0 -251
- package/src/server/namespace.js +0 -292
- package/src/server/subscriptions.js +0 -351
- package/src/server/ws.js +0 -573
|
@@ -1,299 +0,0 @@
|
|
|
1
|
-
# Atomic Updates for Subscribables
|
|
2
|
-
|
|
3
|
-
Atomic updates enable efficient real-time synchronization by sending only the changes (insert/update/delete) to subscribed clients, instead of re-querying and sending the entire document on every change.
|
|
4
|
-
|
|
5
|
-
## Overview
|
|
6
|
-
|
|
7
|
-
### Problem Solved
|
|
8
|
-
|
|
9
|
-
Previously, when any data changed that affected a subscription, the server would:
|
|
10
|
-
1. Re-query the entire document using `get_<subscribable>()`
|
|
11
|
-
2. Send the complete document to the client
|
|
12
|
-
3. Client replaces its entire local state
|
|
13
|
-
|
|
14
|
-
This approach has several problems:
|
|
15
|
-
- **Network inefficiency**: Sends full product catalogue when one task template duration changes
|
|
16
|
-
- **Database load**: Re-executes complex queries on every tiny change
|
|
17
|
-
- **Client state loss**: Replaces entire local state, losing UI state (scroll position, expanded rows, etc.)
|
|
18
|
-
|
|
19
|
-
### Solution: Atomic Updates
|
|
20
|
-
|
|
21
|
-
With atomic updates, the server:
|
|
22
|
-
1. Forwards the raw event (table, operation, primary key, data) directly to clients
|
|
23
|
-
2. Client applies the patch to their local copy of the document
|
|
24
|
-
3. Only changed data traverses the network
|
|
25
|
-
|
|
26
|
-
Benefits:
|
|
27
|
-
- **Efficient**: O(change size) instead of O(document size) per update
|
|
28
|
-
- **Preserved state**: Client UI state remains intact
|
|
29
|
-
- **Reduced database load**: No re-querying on every change
|
|
30
|
-
|
|
31
|
-
## How It Works
|
|
32
|
-
|
|
33
|
-
### 1. Subscribe: Initial Data + Schema
|
|
34
|
-
|
|
35
|
-
When a client subscribes, they receive:
|
|
36
|
-
- The full initial document (unchanged)
|
|
37
|
-
- A **schema** that maps table names to document paths
|
|
38
|
-
|
|
39
|
-
```javascript
|
|
40
|
-
const { data, schema, unsubscribe } = await ws.api.subscribe_venue_detail(
|
|
41
|
-
{ venue_id: 1 },
|
|
42
|
-
(updated) => console.log('Updated:', updated)
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
// schema = {
|
|
46
|
-
// root: 'venues',
|
|
47
|
-
// paths: {
|
|
48
|
-
// 'venues': '.', // Root entity
|
|
49
|
-
// 'organisations': 'org', // FK expansion
|
|
50
|
-
// 'sites': 'sites', // Child collection
|
|
51
|
-
// 'packages': 'packages' // Child collection
|
|
52
|
-
// }
|
|
53
|
-
// }
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
### 2. On Changes: Atomic Events
|
|
57
|
-
|
|
58
|
-
When data changes, instead of re-querying, the server sends a `subscription:event` message:
|
|
59
|
-
|
|
60
|
-
```json
|
|
61
|
-
{
|
|
62
|
-
"jsonrpc": "2.0",
|
|
63
|
-
"method": "subscription:event",
|
|
64
|
-
"params": {
|
|
65
|
-
"subscription_id": "550e8400-e29b-41d4-a716-446655440000",
|
|
66
|
-
"subscribable": "venue_detail",
|
|
67
|
-
"event": {
|
|
68
|
-
"table": "sites",
|
|
69
|
-
"op": "update",
|
|
70
|
-
"pk": { "id": 5 },
|
|
71
|
-
"data": { "id": 5, "name": "Updated Site Name", "venue_id": 1 },
|
|
72
|
-
"before": { "id": 5, "name": "Old Name", "venue_id": 1 }
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
### 3. Client: Apply Patch
|
|
79
|
-
|
|
80
|
-
The client uses the schema to locate where the change belongs in the document and applies it:
|
|
81
|
-
|
|
82
|
-
- **insert**: Adds new item to the appropriate array
|
|
83
|
-
- **update**: Finds item by primary key and merges changes
|
|
84
|
-
- **delete**: Finds item by primary key and removes it
|
|
85
|
-
|
|
86
|
-
The callback receives the updated local document, preserving any UI state.
|
|
87
|
-
|
|
88
|
-
## Scope Tables
|
|
89
|
-
|
|
90
|
-
Each subscribable tracks which tables are "in scope" - tables that can affect the document. This enables an optimization: events from tables not in scope are immediately skipped, avoiding unnecessary `_affected_documents()` calls.
|
|
91
|
-
|
|
92
|
-
### Interpreted Mode (Runtime)
|
|
93
|
-
|
|
94
|
-
In interpreted mode, scope tables are stored in the `dzql.subscribables` table:
|
|
95
|
-
|
|
96
|
-
```sql
|
|
97
|
-
SELECT scope_tables FROM dzql.subscribables WHERE name = 'venue_detail';
|
|
98
|
-
-- Returns: ['venues', 'organisations', 'sites', 'packages']
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
Scope tables are automatically extracted when registering a subscribable:
|
|
102
|
-
|
|
103
|
-
```sql
|
|
104
|
-
SELECT dzql.register_subscribable(
|
|
105
|
-
'venue_detail',
|
|
106
|
-
'{"subscribe": [...]}'::jsonb,
|
|
107
|
-
'{"venue_id": "int"}'::jsonb,
|
|
108
|
-
'venues', -- root_entity -> scope includes 'venues'
|
|
109
|
-
'{
|
|
110
|
-
"org": "organisations", -- scope includes 'organisations'
|
|
111
|
-
"sites": {"entity": "sites", ...} -- scope includes 'sites'
|
|
112
|
-
}'::jsonb
|
|
113
|
-
);
|
|
114
|
-
|
|
115
|
-
-- scope_tables automatically set to: ['venues', 'organisations', 'sites']
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
### Compiled Mode (Static SQL)
|
|
119
|
-
|
|
120
|
-
In compiled mode, scope tables are embedded directly in the `get_<name>()` function return value. No `dzql.subscribables` table is required.
|
|
121
|
-
|
|
122
|
-
The compiled function returns:
|
|
123
|
-
```json
|
|
124
|
-
{
|
|
125
|
-
"data": { ... },
|
|
126
|
-
"schema": {
|
|
127
|
-
"root": "organisations",
|
|
128
|
-
"paths": {
|
|
129
|
-
"organisations": ".",
|
|
130
|
-
"products": "products"
|
|
131
|
-
},
|
|
132
|
-
"scopeTables": ["organisations", "products"]
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
When a client subscribes:
|
|
138
|
-
1. Server calls `get_<name>(params, user_id)`
|
|
139
|
-
2. Server extracts the embedded schema (including `scopeTables`)
|
|
140
|
-
3. Server caches the metadata for event filtering
|
|
141
|
-
4. Client receives `data` and `schema` for client-side patching
|
|
142
|
-
|
|
143
|
-
**Important**: The scope tables are only cached after the first subscribe call. This means:
|
|
144
|
-
- The `dzql.subscribables` table is not needed for compiled deployments
|
|
145
|
-
- Event filtering works correctly once at least one client has subscribed
|
|
146
|
-
- If no clients have subscribed, events will still be processed (the scope check is skipped when cache is empty)
|
|
147
|
-
|
|
148
|
-
## Path Mapping
|
|
149
|
-
|
|
150
|
-
The path mapping tells the client where each table's data lives in the document structure:
|
|
151
|
-
|
|
152
|
-
| Table | Path | Meaning |
|
|
153
|
-
|-------|------|---------|
|
|
154
|
-
| `venues` | `.` | Root level |
|
|
155
|
-
| `organisations` | `org` | `document.org` |
|
|
156
|
-
| `sites` | `sites` | `document.sites[]` |
|
|
157
|
-
| `allocations` | `packages.allocations` | `document.packages[].allocations[]` |
|
|
158
|
-
|
|
159
|
-
### Nested Relations
|
|
160
|
-
|
|
161
|
-
For nested relations, paths are dot-separated:
|
|
162
|
-
|
|
163
|
-
```javascript
|
|
164
|
-
relations = {
|
|
165
|
-
packages: {
|
|
166
|
-
entity: 'packages',
|
|
167
|
-
filter: 'venue_id=$venue_id',
|
|
168
|
-
include: {
|
|
169
|
-
allocations: 'allocations'
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Results in paths:
|
|
175
|
-
// 'packages' -> 'packages'
|
|
176
|
-
// 'allocations' -> 'packages.allocations'
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
## Client-Side Patching
|
|
180
|
-
|
|
181
|
-
The `WebSocketManager` automatically handles `subscription:event` messages:
|
|
182
|
-
|
|
183
|
-
```javascript
|
|
184
|
-
// Internally, when subscription:event is received:
|
|
185
|
-
applyAtomicUpdate(sub, event) {
|
|
186
|
-
const { table, op, pk, data } = event;
|
|
187
|
-
const path = sub.schema.paths[table];
|
|
188
|
-
|
|
189
|
-
if (path === '.') {
|
|
190
|
-
// Root entity update
|
|
191
|
-
Object.assign(sub.localData[sub.schema.root], data);
|
|
192
|
-
} else {
|
|
193
|
-
// Relation update
|
|
194
|
-
const arr = getArrayAtPath(sub.localData, path);
|
|
195
|
-
if (op === 'insert') arr.push(data);
|
|
196
|
-
if (op === 'update') {
|
|
197
|
-
const idx = arr.findIndex(item => pkMatch(item, pk));
|
|
198
|
-
if (idx !== -1) Object.assign(arr[idx], data);
|
|
199
|
-
}
|
|
200
|
-
if (op === 'delete') {
|
|
201
|
-
const idx = arr.findIndex(item => pkMatch(item, pk));
|
|
202
|
-
if (idx !== -1) arr.splice(idx, 1);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Trigger callback with patched document
|
|
207
|
-
sub.callback(sub.localData);
|
|
208
|
-
}
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
## Composite Primary Keys
|
|
212
|
-
|
|
213
|
-
Atomic updates support composite primary keys:
|
|
214
|
-
|
|
215
|
-
```json
|
|
216
|
-
{
|
|
217
|
-
"pk": { "product_id": 1, "part_id": 2 }
|
|
218
|
-
}
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
The client matches all key fields when finding items to update or delete.
|
|
222
|
-
|
|
223
|
-
## Migration from Full Re-queries
|
|
224
|
-
|
|
225
|
-
If you have existing subscribables, atomic updates are enabled automatically when:
|
|
226
|
-
1. The `scope_tables` column is populated (happens on `register_subscribable`)
|
|
227
|
-
2. The client receives the `schema` in the subscribe response
|
|
228
|
-
|
|
229
|
-
No code changes required - the system is backward compatible.
|
|
230
|
-
|
|
231
|
-
## Debugging
|
|
232
|
-
|
|
233
|
-
### Check Schema (Compiled Mode)
|
|
234
|
-
|
|
235
|
-
Subscribe and log the full schema including scopeTables:
|
|
236
|
-
|
|
237
|
-
```javascript
|
|
238
|
-
const { data, schema } = await ws.api.subscribe_product_catalogue(
|
|
239
|
-
{ organisation_id: 1 },
|
|
240
|
-
() => {}
|
|
241
|
-
);
|
|
242
|
-
console.log('Schema:', schema);
|
|
243
|
-
// Expected: { root: 'organisations', paths: {...}, scopeTables: [...] }
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
If `schema.root` is null or `schema.scopeTables` is missing, ensure you're using dzql >= 0.5.10 and have recompiled your subscribable functions.
|
|
247
|
-
|
|
248
|
-
### Check Scope Tables (Interpreted Mode)
|
|
249
|
-
|
|
250
|
-
```sql
|
|
251
|
-
SELECT name, scope_tables
|
|
252
|
-
FROM dzql.subscribables
|
|
253
|
-
WHERE name = 'your_subscribable';
|
|
254
|
-
```
|
|
255
|
-
|
|
256
|
-
### Verify Path Mapping
|
|
257
|
-
|
|
258
|
-
```javascript
|
|
259
|
-
const { schema } = await ws.api.subscribe_venue_detail(
|
|
260
|
-
{ venue_id: 1 },
|
|
261
|
-
() => {}
|
|
262
|
-
);
|
|
263
|
-
console.log('Path mapping:', schema.paths);
|
|
264
|
-
// Should map each table to its document path
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
### Server Logs
|
|
268
|
-
|
|
269
|
-
Enable debug logging to see atomic events:
|
|
270
|
-
|
|
271
|
-
```bash
|
|
272
|
-
LOG_CATEGORIES="notify:debug" bun run server
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
You should see:
|
|
276
|
-
```
|
|
277
|
-
Cached compiled metadata for product_catalogue: { scopeTables: ['organisations', 'products'] }
|
|
278
|
-
product_catalogue: 1 param set(s) affected by products:update
|
|
279
|
-
Sent atomic event to subscription abc123... (products:update)
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
### Common Issues
|
|
283
|
-
|
|
284
|
-
1. **`schema.root` is null**: Update to dzql >= 0.5.10 and recompile your subscribables
|
|
285
|
-
2. **No updates pushed**: Check that scopeTables includes the changed table
|
|
286
|
-
3. **Metadata not cached**: Ensure at least one client has subscribed before events occur
|
|
287
|
-
|
|
288
|
-
## Limitations
|
|
289
|
-
|
|
290
|
-
1. **Deeply nested updates**: For very deep nesting (3+ levels), paths become complex. Consider flattening your subscribable structure.
|
|
291
|
-
|
|
292
|
-
2. **Cascading deletes**: When a parent is deleted, the client may receive the parent delete before child deletes. The client handles missing parents gracefully.
|
|
293
|
-
|
|
294
|
-
3. **Permission changes**: If a user loses access mid-subscription, they may receive one final event before the subscription is terminated.
|
|
295
|
-
|
|
296
|
-
## See Also
|
|
297
|
-
|
|
298
|
-
- [Subscriptions Guide](./subscriptions.md) - Full subscribable documentation
|
|
299
|
-
- [Getting Started with Subscriptions](../getting-started/subscriptions-quick-start.md)
|