dzql 0.6.37 → 0.8.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.
Files changed (45) hide show
  1. package/README.md +16 -0
  2. package/docs/README.md +1 -1
  3. package/docs/for_ai.md +39 -9
  4. package/package.json +12 -5
  5. package/src/cli/codegen/notification.ts +3 -3
  6. package/src/cli/codegen/sql/core.ts +21 -5
  7. package/src/cli/codegen/sql/delete.ts +5 -7
  8. package/src/cli/codegen/sql/get.ts +9 -11
  9. package/src/cli/codegen/sql/index.ts +7 -4
  10. package/src/cli/codegen/sql/lookup.ts +4 -5
  11. package/src/cli/codegen/sql/save.ts +58 -39
  12. package/src/cli/codegen/sql/search.ts +44 -10
  13. package/src/cli/codegen/sql/utils.ts +16 -1
  14. package/src/cli/codegen/subscribable_sql.ts +116 -124
  15. package/src/cli/compile.ts +263 -0
  16. package/src/cli/compiler/path-validator.ts +115 -83
  17. package/src/cli/compiler/paths.ts +299 -379
  18. package/src/cli/compiler/permissions.ts +26 -26
  19. package/src/cli/index.ts +17 -219
  20. package/src/client/ws.ts +20 -10
  21. package/src/postgres/auth-jwt-sql.ts +32 -0
  22. package/src/postgres/auth-jwt.ts +209 -0
  23. package/src/postgres/auth.ts +50 -0
  24. package/src/postgres/bootstrap.ts +55 -0
  25. package/src/postgres/carry.ts +40 -0
  26. package/src/postgres/codegen.ts +172 -0
  27. package/src/postgres/index.ts +58 -0
  28. package/src/postgres/listener.ts +706 -0
  29. package/src/postgres/migrate.ts +143 -0
  30. package/src/postgres/pglite.ts +142 -0
  31. package/src/postgres/registry.ts +253 -0
  32. package/src/postgres/schema.ts +171 -0
  33. package/src/postgres/sql.ts +42 -0
  34. package/src/runtime/auth.ts +33 -2
  35. package/src/runtime/index.ts +47 -6
  36. package/src/runtime/manifest_loader.ts +10 -0
  37. package/src/runtime/projection.ts +19 -0
  38. package/src/sql/001a-delta-meta.sql +204 -0
  39. package/src/sql/001b-delta-scope.sql +619 -0
  40. package/src/sql/001c-delta-read.sql +506 -0
  41. package/src/sql/001d-delta-write.sql +1088 -0
  42. package/src/sql/001e-delta-ops.sql +142 -0
  43. package/src/sql/001f-delta-as.sql +41 -0
  44. package/src/sql/001g-delta-ledger.sql +564 -0
  45. package/src/sql/auth-jwt.sql +62 -0
@@ -0,0 +1,506 @@
1
+ -- =========================================================================
2
+ -- Delta-doc: version tracking, collection loading, delta_open
3
+ -- =========================================================================
4
+
5
+ -- ---------------------------------------------------------------------------
6
+ -- _delta_bump_and_notify: bump version, log ops, fire NOTIFY — one place.
7
+ -- `p_applied` is the write as applied, for custom documents (the log's
8
+ -- `applied`, 001a); the two-argument form logs none, and is heard as told.
9
+ -- Nothing to tell anyone (no ops, none applied): no version, as on SQLite --
10
+ -- the version it is at.
11
+ -- ---------------------------------------------------------------------------
12
+
13
+ CREATE OR REPLACE FUNCTION _delta_bump_and_notify(p_doc TEXT, p_ops JSONB, p_applied JSONB)
14
+ RETURNS BIGINT AS $$
15
+ DECLARE v_version BIGINT;
16
+ BEGIN
17
+ IF jsonb_array_length(p_ops) = 0 AND COALESCE(jsonb_array_length(p_applied), 0) = 0 THEN
18
+ SELECT version INTO v_version FROM _delta_versions WHERE doc_name = p_doc;
19
+ RETURN COALESCE(v_version, 0);
20
+ END IF;
21
+
22
+ INSERT INTO _delta_versions (doc_name, version) VALUES (p_doc, 1)
23
+ ON CONFLICT (doc_name) DO UPDATE SET version = _delta_versions.version + 1
24
+ RETURNING version INTO v_version;
25
+
26
+ INSERT INTO _delta_ops_log (doc_name, version, ops, applied)
27
+ VALUES (p_doc, v_version, p_ops, p_applied);
28
+
29
+ PERFORM pg_notify('delta_changes',
30
+ json_build_object('doc', p_doc, 'v', v_version)::text
31
+ );
32
+
33
+ RETURN v_version;
34
+ END;
35
+ $$ LANGUAGE plpgsql;
36
+
37
+ CREATE OR REPLACE FUNCTION _delta_bump_and_notify(p_doc TEXT, p_ops JSONB)
38
+ RETURNS BIGINT LANGUAGE sql AS $$ SELECT _delta_bump_and_notify(p_doc, p_ops, NULL); $$;
39
+
40
+ -- ---------------------------------------------------------------------------
41
+ -- _delta_load_collection: recursively load a collection's rows as a JSONB map.
42
+ -- When p_at is NULL, loads current state. When set, loads at that point in time.
43
+ -- ---------------------------------------------------------------------------
44
+
45
+ CREATE OR REPLACE FUNCTION _delta_load_collection(
46
+ p_collection_key TEXT,
47
+ p_root_collection TEXT,
48
+ p_root_id BIGINT,
49
+ p_at TIMESTAMPTZ DEFAULT NULL
50
+ ) RETURNS JSONB AS $$
51
+ DECLARE
52
+ v_coll RECORD;
53
+ v_source TEXT;
54
+ v_strip BOOLEAN;
55
+ v_where TEXT;
56
+ v_result JSONB;
57
+ v_parent_map JSONB;
58
+ v_parent_ids BIGINT[];
59
+ BEGIN
60
+ SELECT * INTO v_coll FROM _delta_collections
61
+ WHERE collection_key = p_collection_key;
62
+ IF NOT FOUND THEN RETURN '{}'::jsonb; END IF;
63
+
64
+ v_source := _delta_source_view(v_coll.table_name, v_coll.temporal, p_at);
65
+ v_strip := v_coll.temporal;
66
+ v_where := CASE WHEN v_coll.temporal AND p_at IS NOT NULL
67
+ THEN _delta_temporal_where(p_at)
68
+ ELSE 'TRUE' END;
69
+
70
+ -- No parent
71
+ IF v_coll.parent_collection IS NULL THEN
72
+ IF v_strip THEN
73
+ EXECUTE format(
74
+ 'SELECT COALESCE(jsonb_object_agg(t.id, _delta_strip_temporal(to_jsonb(t.*))), ''{}''::jsonb) FROM %I t WHERE %s',
75
+ v_source, v_where
76
+ ) INTO v_result;
77
+ ELSE
78
+ EXECUTE format(
79
+ 'SELECT COALESCE(jsonb_object_agg(t.id, to_jsonb(t.*)), ''{}''::jsonb) FROM %I t',
80
+ v_source
81
+ ) INTO v_result;
82
+ END IF;
83
+ RETURN v_result;
84
+ END IF;
85
+
86
+ -- Direct child of root
87
+ IF v_coll.parent_collection = p_root_collection THEN
88
+ IF v_strip THEN
89
+ EXECUTE format(
90
+ 'SELECT COALESCE(jsonb_object_agg(t.id, _delta_strip_temporal(to_jsonb(t.*))), ''{}''::jsonb) FROM %I t WHERE t.%I = $1 AND %s',
91
+ v_source, v_coll.parent_fk, v_where
92
+ ) INTO v_result USING p_root_id;
93
+ ELSE
94
+ EXECUTE format(
95
+ 'SELECT COALESCE(jsonb_object_agg(t.id, to_jsonb(t.*)), ''{}''::jsonb) FROM %I t WHERE t.%I = $1',
96
+ v_source, v_coll.parent_fk
97
+ ) INTO v_result USING p_root_id;
98
+ END IF;
99
+ RETURN v_result;
100
+ END IF;
101
+
102
+ -- Grandchild+: load parent rows recursively, then filter by their IDs. The
103
+ -- parent is walked whether or not the document includes it; one with no
104
+ -- parent of its own ends the chain short of the root, so nothing under it is
105
+ -- in the document -- as _delta_chain_root and _delta_row_in_scope judge it,
106
+ -- and the SQLite backend reads it (todo #32).
107
+ IF NOT EXISTS (SELECT 1 FROM _delta_collections
108
+ WHERE collection_key = v_coll.parent_collection
109
+ AND parent_collection IS NOT NULL) THEN
110
+ RETURN '{}'::jsonb;
111
+ END IF;
112
+ v_parent_map := _delta_load_collection(
113
+ v_coll.parent_collection, p_root_collection, p_root_id, p_at
114
+ );
115
+ SELECT array_agg(k::BIGINT) INTO v_parent_ids
116
+ FROM jsonb_object_keys(v_parent_map) AS k;
117
+
118
+ IF v_parent_ids IS NULL THEN RETURN '{}'::jsonb; END IF;
119
+
120
+ IF v_strip THEN
121
+ EXECUTE format(
122
+ 'SELECT COALESCE(jsonb_object_agg(t.id, _delta_strip_temporal(to_jsonb(t.*))), ''{}''::jsonb) FROM %I t WHERE t.%I = ANY($1) AND %s',
123
+ v_source, v_coll.parent_fk, v_where
124
+ ) INTO v_result USING v_parent_ids;
125
+ ELSE
126
+ EXECUTE format(
127
+ 'SELECT COALESCE(jsonb_object_agg(t.id, to_jsonb(t.*)), ''{}''::jsonb) FROM %I t WHERE t.%I = ANY($1)',
128
+ v_source, v_coll.parent_fk
129
+ ) INTO v_result USING v_parent_ids;
130
+ END IF;
131
+
132
+ RETURN v_result;
133
+ END;
134
+ $$ LANGUAGE plpgsql STABLE;
135
+
136
+ -- ---------------------------------------------------------------------------
137
+ -- _delta_load_collection_all: load every row of a collection as a JSONB map.
138
+ -- Used by list-mode docs' `include` traversal (no FK filter — list mode has
139
+ -- no single root id to filter against, so "include" means "load it all").
140
+ -- ---------------------------------------------------------------------------
141
+
142
+ CREATE OR REPLACE FUNCTION _delta_load_collection_all(
143
+ p_collection_key TEXT,
144
+ p_at TIMESTAMPTZ DEFAULT NULL
145
+ ) RETURNS JSONB AS $$
146
+ DECLARE
147
+ v_coll RECORD;
148
+ v_source TEXT;
149
+ v_result JSONB;
150
+ BEGIN
151
+ SELECT * INTO v_coll FROM _delta_collections
152
+ WHERE collection_key = p_collection_key;
153
+ IF NOT FOUND THEN RETURN '{}'::jsonb; END IF;
154
+
155
+ v_source := _delta_source_view(v_coll.table_name, v_coll.temporal, p_at);
156
+
157
+ IF v_coll.temporal THEN
158
+ EXECUTE format(
159
+ 'SELECT COALESCE(jsonb_object_agg(t.id, _delta_strip_temporal(to_jsonb(t.*))), ''{}''::jsonb) FROM %I t WHERE %s',
160
+ v_source,
161
+ CASE WHEN p_at IS NOT NULL THEN _delta_temporal_where(p_at) ELSE 'TRUE' END
162
+ ) INTO v_result;
163
+ ELSE
164
+ EXECUTE format(
165
+ 'SELECT COALESCE(jsonb_object_agg(t.id, to_jsonb(t.*)), ''{}''::jsonb) FROM %I t',
166
+ v_source
167
+ ) INTO v_result;
168
+ END IF;
169
+
170
+ RETURN v_result;
171
+ END;
172
+ $$ LANGUAGE plpgsql STABLE;
173
+
174
+ -- ---------------------------------------------------------------------------
175
+ -- Implied documents (todo #34): there before their root row is, as on SQLite.
176
+ --
177
+ -- _delta_implied_root: the root row an implied document opens with, and its
178
+ -- first write makes -- its id, and each column's default (or null where it
179
+ -- may be null; else "", 0 or false by its type).
180
+ -- _delta_make_implied: make it, in the write's own transaction, unless it is
181
+ -- there or the write adds it itself (an undo of its removal).
182
+ -- _delta_open_held: the document as a walk guards it -- an implied one whose
183
+ -- root row is not there holds no root.
184
+ -- ---------------------------------------------------------------------------
185
+
186
+ CREATE OR REPLACE FUNCTION _delta_implied_root(p_def _delta_docs, p_id BIGINT)
187
+ RETURNS JSONB LANGUAGE sql STABLE AS $$
188
+ SELECT jsonb_build_object('id', p_id) || COALESCE((
189
+ SELECT jsonb_object_agg(k, CASE
190
+ WHEN COALESCE(d->'default', 'null'::jsonb) <> 'null'::jsonb THEN d->'default'
191
+ WHEN (d->>'nullable')::boolean THEN 'null'::jsonb
192
+ WHEN d->>'type' = 'text' THEN '""'::jsonb
193
+ WHEN d->>'type' IN ('integer', 'real') THEN '0'::jsonb
194
+ WHEN d->>'type' = 'boolean' THEN 'false'::jsonb
195
+ ELSE 'null'::jsonb END)
196
+ FROM _delta_collections c, jsonb_each(_delta_cols(c.collection_key, c.columns_def)) AS x(k, d)
197
+ WHERE c.collection_key = p_def.root_collection), '{}'::jsonb);
198
+ $$;
199
+
200
+ CREATE OR REPLACE FUNCTION _delta_make_implied(p_def _delta_docs, p_id BIGINT, p_ops JSONB, p_ts TIMESTAMPTZ)
201
+ RETURNS void AS $$
202
+ DECLARE
203
+ v_coll RECORD;
204
+ v_exists BOOLEAN;
205
+ v_row JSONB;
206
+ BEGIN
207
+ IF NOT COALESCE(p_def.implied, FALSE) OR p_id IS NULL THEN RETURN; END IF;
208
+ IF EXISTS (SELECT 1 FROM jsonb_array_elements(p_ops) o
209
+ WHERE o->>'op' = 'add' AND o->>'path' ~ '^/[^/]*/[^/]*$'
210
+ AND (_delta_split_path(o->>'path'))[1] = p_def.root_collection) THEN
211
+ RETURN;
212
+ END IF;
213
+ SELECT * INTO v_coll FROM _delta_collections WHERE collection_key = p_def.root_collection;
214
+ IF NOT FOUND THEN RETURN; END IF;
215
+ EXECUTE format('SELECT EXISTS (SELECT 1 FROM %I WHERE id = $1)', _delta_source_view(v_coll.table_name, v_coll.temporal))
216
+ INTO v_exists USING p_id;
217
+ IF v_exists THEN RETURN; END IF;
218
+ -- Not there, as the writer reads it: made under the add rule (todo #71) -- an
219
+ -- id the serial has handed out is taken, whether a row RLS hides holds it or
220
+ -- none does. delta_apply passes the serial past the root once it is done.
221
+ IF NOT _delta_serial_new(v_coll.table_name, p_id) THEN
222
+ RAISE EXCEPTION 'id already taken: % -- add to % for a new id',
223
+ _delta_build_path(p_def.root_collection, p_id::text), _delta_build_path(p_def.root_collection, '-')
224
+ USING ERRCODE = '23505';
225
+ END IF;
226
+ v_row := _delta_implied_root(p_def, p_id);
227
+ IF v_coll.temporal THEN v_row := v_row || jsonb_build_object('valid_from', p_ts, 'valid_to', NULL); END IF;
228
+ EXECUTE _delta_insert_sql(v_coll.table_name, v_row) USING v_row;
229
+ END;
230
+ $$ LANGUAGE plpgsql;
231
+
232
+ CREATE OR REPLACE FUNCTION _delta_open_held(p_doc_name TEXT)
233
+ RETURNS JSONB AS $$
234
+ DECLARE
235
+ v_doc JSONB := COALESCE(_delta_open(p_doc_name, TRUE), '{}'::jsonb);
236
+ v_def _delta_docs := _delta_find_doc(p_doc_name);
237
+ v_scope JSONB;
238
+ v_coll RECORD;
239
+ v_exists BOOLEAN;
240
+ BEGIN
241
+ IF NOT COALESCE(v_def.implied, FALSE) THEN RETURN v_doc; END IF;
242
+ v_scope := _delta_resolve_scope(v_def, p_doc_name);
243
+ IF (v_scope->>'mode') <> 'single' THEN RETURN v_doc; END IF;
244
+ SELECT * INTO v_coll FROM _delta_collections WHERE collection_key = v_def.root_collection;
245
+ IF NOT FOUND THEN RETURN v_doc; END IF;
246
+ EXECUTE format('SELECT EXISTS (SELECT 1 FROM %I WHERE id = $1)', _delta_source_view(v_coll.table_name, v_coll.temporal))
247
+ INTO v_exists USING (v_scope->'values'->>'id')::BIGINT;
248
+ IF v_exists THEN RETURN v_doc; END IF;
249
+ RETURN v_doc || jsonb_build_object(v_def.root_collection, 'null'::jsonb);
250
+ END;
251
+ $$ LANGUAGE plpgsql;
252
+
253
+ -- ---------------------------------------------------------------------------
254
+ -- _delta_read_list: a list document -- the root rows `p_where` admits, and
255
+ -- each included collection in full (list mode has no single root id to filter
256
+ -- against, so "include" means "load it all") -- as they stand (p_at NULL), or
257
+ -- a temporal table's as they stood at p_at. delta_open's and delta_open_at's.
258
+ -- ---------------------------------------------------------------------------
259
+
260
+ CREATE OR REPLACE FUNCTION _delta_read_list(p_def _delta_docs, p_where TEXT, p_at TIMESTAMPTZ)
261
+ RETURNS JSONB AS $$
262
+ DECLARE
263
+ v_root RECORD;
264
+ v_rows JSONB;
265
+ v_result JSONB;
266
+ v_coll_key TEXT;
267
+ BEGIN
268
+ SELECT * INTO v_root FROM _delta_collections WHERE collection_key = p_def.root_collection;
269
+ EXECUTE format(
270
+ 'SELECT COALESCE(jsonb_object_agg(t.id, %s), ''{}''::jsonb) FROM %I t WHERE %s AND %s',
271
+ CASE WHEN v_root.temporal THEN '_delta_strip_temporal(to_jsonb(t.*))' ELSE 'to_jsonb(t.*)' END,
272
+ _delta_source_view(v_root.table_name, v_root.temporal, p_at),
273
+ CASE WHEN v_root.temporal AND p_at IS NOT NULL THEN _delta_temporal_where(p_at) ELSE 'TRUE' END,
274
+ p_where
275
+ ) INTO v_rows;
276
+ v_result := jsonb_build_object(p_def.root_collection, v_rows);
277
+ FOREACH v_coll_key IN ARRAY p_def.include
278
+ LOOP
279
+ v_result := v_result || jsonb_build_object(v_coll_key, _delta_load_collection_all(v_coll_key, p_at));
280
+ END LOOP;
281
+ RETURN v_result;
282
+ END;
283
+ $$ LANGUAGE plpgsql STABLE;
284
+
285
+ -- ---------------------------------------------------------------------------
286
+ -- _delta_show: a row of `p_coll` as a document shows it -- where it names that
287
+ -- collection's columns (`held`, todo #70), those, the row's id and its parent
288
+ -- key; else whole. _delta_show_doc: each row of a document so (`p_single`: its
289
+ -- root is one row, not a map). showRow / showDoc in schema.ts.
290
+ -- ---------------------------------------------------------------------------
291
+
292
+ CREATE OR REPLACE FUNCTION _delta_show(p_def _delta_docs, p_coll TEXT, p_row JSONB)
293
+ RETURNS JSONB AS $$
294
+ SELECT CASE WHEN NOT COALESCE(p_def.held, '{}'::jsonb) ? p_coll OR jsonb_typeof(p_row) IS DISTINCT FROM 'object' THEN p_row
295
+ ELSE (SELECT COALESCE(jsonb_object_agg(f, v), '{}'::jsonb) FROM jsonb_each(p_row) AS e(f, v)
296
+ WHERE f = 'id' OR (p_def.held->p_coll) ? f
297
+ OR f = (SELECT parent_fk FROM _delta_collections WHERE collection_key = p_coll)) END;
298
+ $$ LANGUAGE sql STABLE;
299
+
300
+ CREATE OR REPLACE FUNCTION _delta_show_doc(p_def _delta_docs, p_doc JSONB, p_single BOOLEAN)
301
+ RETURNS JSONB AS $$
302
+ DECLARE
303
+ v_out JSONB := p_doc;
304
+ v_coll TEXT;
305
+ BEGIN
306
+ FOR v_coll IN SELECT jsonb_object_keys(COALESCE(p_def.held, '{}'::jsonb)) LOOP
307
+ CONTINUE WHEN jsonb_typeof(p_doc->v_coll) IS DISTINCT FROM 'object';
308
+ v_out := v_out || jsonb_build_object(v_coll, CASE WHEN p_single AND v_coll = p_def.root_collection THEN _delta_show(p_def, v_coll, p_doc->v_coll)
309
+ ELSE (SELECT COALESCE(jsonb_object_agg(id, _delta_show(p_def, v_coll, r)), '{}'::jsonb) FROM jsonb_each(p_doc->v_coll) AS e(id, r)) END);
310
+ END LOOP;
311
+ RETURN v_out;
312
+ END;
313
+ $$ LANGUAGE plpgsql STABLE;
314
+
315
+ -- _delta_show_ops: ops as the document `p_doc_name` shows them -- what its
316
+ -- writer is answered (a write's ops and inverse, a walk's, its history): a
317
+ -- row's value, its columns (_delta_show); a field it does not hold
318
+ -- (/<coll>/<id>/<field>, or a single root's /<root>/<field>), no op. The
319
+ -- ledger keeps them whole. showOps in schema.ts.
320
+
321
+ CREATE OR REPLACE FUNCTION _delta_show_ops(p_doc_name TEXT, p_ops JSONB)
322
+ RETURNS JSONB AS $$
323
+ DECLARE
324
+ v_def _delta_docs := _delta_find_doc(p_doc_name);
325
+ v_out JSONB := '[]'::jsonb;
326
+ v_op JSONB;
327
+ v_parts TEXT[];
328
+ v_coll RECORD;
329
+ v_field TEXT;
330
+ BEGIN
331
+ IF v_def.prefix IS NULL OR COALESCE(v_def.held, '{}'::jsonb) = '{}'::jsonb OR jsonb_typeof(p_ops) <> 'array' THEN RETURN p_ops; END IF;
332
+ FOR v_op IN SELECT jsonb_array_elements(p_ops) LOOP
333
+ v_parts := _delta_split_path(v_op->>'path');
334
+ SELECT * INTO v_coll FROM _delta_collections WHERE collection_key = v_parts[1];
335
+ v_field := CASE WHEN array_length(v_parts, 1) >= 3 THEN v_parts[3]
336
+ WHEN array_length(v_parts, 1) = 2 AND (_delta_cols(v_coll.collection_key, v_coll.columns_def) ? v_parts[2] OR v_parts[2] = v_coll.parent_fk) THEN v_parts[2] END;
337
+ IF v_field IS NOT NULL THEN
338
+ CONTINUE WHEN (v_def.held ? v_parts[1]) AND NOT ((v_def.held->v_parts[1]) ? v_field OR v_field = 'id' OR v_field = v_coll.parent_fk);
339
+ ELSIF v_op ? 'value' THEN
340
+ v_op := jsonb_set(v_op, '{value}', _delta_show(v_def, v_parts[1], v_op->'value'));
341
+ END IF;
342
+ v_out := v_out || jsonb_build_array(v_op);
343
+ END LOOP;
344
+ RETURN v_out;
345
+ END;
346
+ $$ LANGUAGE plpgsql STABLE;
347
+
348
+ -- ---------------------------------------------------------------------------
349
+ -- delta_open: load a doc from relational tables, return JSONB + version -- its
350
+ -- rows as it shows them (_delta_show_doc). _delta_open(name, TRUE) reads them
351
+ -- whole: what the ledger's inverse and a walk's guard read.
352
+ -- ---------------------------------------------------------------------------
353
+
354
+ CREATE OR REPLACE FUNCTION _delta_open(p_doc_name TEXT, p_whole BOOLEAN)
355
+ RETURNS JSONB AS $$
356
+ DECLARE
357
+ v_def _delta_docs;
358
+ v_resolved JSONB;
359
+ v_where TEXT;
360
+ v_mode TEXT;
361
+ v_at TIMESTAMPTZ;
362
+ v_doc_id BIGINT;
363
+ v_root_coll RECORD;
364
+ v_view TEXT;
365
+ v_root_row JSONB;
366
+ v_result JSONB;
367
+ v_coll_key TEXT;
368
+ v_version BIGINT;
369
+ BEGIN
370
+ v_def := _delta_find_doc(p_doc_name);
371
+ IF v_def.prefix IS NULL THEN
372
+ -- Fail-fast: caller asked for a doc whose prefix isn't registered.
373
+ -- NULL-as-config-error used to mask typos and missing migrations.
374
+ RAISE EXCEPTION 'no doc def for: %', p_doc_name
375
+ USING ERRCODE = 'P0001';
376
+ END IF;
377
+
378
+ v_resolved := _delta_resolve_scope(v_def, p_doc_name);
379
+ v_where := v_resolved->>'where';
380
+ v_mode := v_resolved->>'mode';
381
+ IF v_resolved->>'at' IS NOT NULL THEN
382
+ v_at := (v_resolved->>'at')::TIMESTAMPTZ;
383
+ END IF;
384
+
385
+ SELECT * INTO v_root_coll FROM _delta_collections
386
+ WHERE collection_key = v_def.root_collection;
387
+ IF NOT FOUND THEN
388
+ RAISE EXCEPTION 'doc "%" references unknown root collection: %',
389
+ p_doc_name, v_def.root_collection
390
+ USING ERRCODE = 'P0001';
391
+ END IF;
392
+
393
+ -- A time in the name (`at:`): the whole document as it stood then, its root
394
+ -- as its included collections, as SQLite reads it.
395
+ v_view := _delta_source_view(v_root_coll.table_name, v_root_coll.temporal, v_at);
396
+ IF v_root_coll.temporal AND v_at IS NOT NULL THEN
397
+ v_where := v_where || ' AND ' || _delta_temporal_where(v_at);
398
+ END IF;
399
+
400
+ IF v_mode = 'list' THEN
401
+ v_result := _delta_read_list(v_def, v_resolved->>'where', v_at);
402
+ ELSE
403
+ -- Single mode: one root row + included collections
404
+ v_doc_id := (v_resolved->'values'->>'id')::BIGINT;
405
+
406
+ -- the row its name names, where it meets the rest of the name's scope too
407
+ -- (fo-guest:<id>:<email>: that id with that email), as SQLite reads it
408
+ EXECUTE format('SELECT to_jsonb(t.*) FROM %I t WHERE t.id = $1 AND %s', v_view, v_where)
409
+ INTO v_root_row USING v_doc_id;
410
+
411
+ IF v_root_row IS NULL AND NOT COALESCE(v_def.implied, FALSE) THEN RETURN NULL; END IF;
412
+
413
+ IF v_root_row IS NULL THEN
414
+ -- Implied: there before its root row is -- the root its first write will
415
+ -- make, each included collection empty, as on SQLite. No row is made.
416
+ v_result := jsonb_build_object(v_def.root_collection, _delta_implied_root(v_def, v_doc_id));
417
+ FOREACH v_coll_key IN ARRAY v_def.include
418
+ LOOP
419
+ v_result := v_result || jsonb_build_object(v_coll_key, '{}'::jsonb);
420
+ END LOOP;
421
+ ELSE
422
+ IF v_root_coll.temporal THEN
423
+ v_root_row := _delta_strip_temporal(v_root_row);
424
+ END IF;
425
+
426
+ v_result := jsonb_build_object(v_def.root_collection, v_root_row);
427
+
428
+ FOREACH v_coll_key IN ARRAY v_def.include
429
+ LOOP
430
+ v_result := v_result || jsonb_build_object(
431
+ v_coll_key,
432
+ _delta_load_collection(v_coll_key, v_def.root_collection, v_doc_id, v_at)
433
+ );
434
+ END LOOP;
435
+ END IF;
436
+ END IF;
437
+
438
+ -- Track version
439
+ INSERT INTO _delta_versions (doc_name, version) VALUES (p_doc_name, 0)
440
+ ON CONFLICT (doc_name) DO NOTHING;
441
+ SELECT version INTO v_version FROM _delta_versions WHERE doc_name = p_doc_name;
442
+
443
+ IF NOT p_whole THEN v_result := _delta_show_doc(v_def, v_result, v_mode <> 'list'); END IF;
444
+ RETURN v_result || jsonb_build_object('_version', v_version);
445
+ END;
446
+ $$ LANGUAGE plpgsql;
447
+
448
+ CREATE OR REPLACE FUNCTION delta_open(p_doc_name TEXT)
449
+ RETURNS JSONB LANGUAGE sql AS $$ SELECT _delta_open(p_doc_name, FALSE); $$;
450
+
451
+ -- ---------------------------------------------------------------------------
452
+ -- The documents open now (_delta_held, todo #73): each listener holds the
453
+ -- names it has subscribers for, so a write's fan-out asks only those.
454
+ -- ---------------------------------------------------------------------------
455
+
456
+ -- _delta_clear_dead_holders: clear the names of listeners that are gone --
457
+ -- those whose connection no longer holds their lock (_delta_holder_start).
458
+ CREATE OR REPLACE FUNCTION _delta_clear_dead_holders() RETURNS void LANGUAGE sql AS $$
459
+ DELETE FROM _delta_held WHERE holder IN (
460
+ SELECT holder FROM _delta_held
461
+ EXCEPT
462
+ SELECT objid::bigint::integer FROM pg_locks
463
+ WHERE locktype = 'advisory' AND granted AND objsubid = 2
464
+ AND database = (SELECT oid FROM pg_database WHERE datname = current_database())
465
+ AND classid = hashtext('_delta_held')::oid);
466
+ $$;
467
+
468
+ -- _delta_holder_start: a listener starts, on the connection it keeps, as a new
469
+ -- holder (`p_holder` NULL) or again after its connection was lost. It takes
470
+ -- its lock, clears the names of listeners that are gone, and answers its id.
471
+ -- Coming back, the listener asks which of its names are still held: one
472
+ -- cleared while it was away may have missed a write.
473
+ DROP FUNCTION IF EXISTS _delta_holder_start(INTEGER); -- its first form answered `kept` too
474
+ DROP FUNCTION IF EXISTS _delta_holder_live(INTEGER);
475
+ CREATE OR REPLACE FUNCTION _delta_holder_start(p_holder INTEGER) RETURNS INTEGER AS $$
476
+ DECLARE v_holder INTEGER := COALESCE(p_holder, nextval('_delta_holder_ids')::INTEGER);
477
+ BEGIN
478
+ PERFORM pg_advisory_lock_shared(hashtext('_delta_held'), v_holder); -- shared: a lost connection may not have ended yet
479
+ PERFORM _delta_clear_dead_holders();
480
+ RETURN v_holder;
481
+ END;
482
+ $$ LANGUAGE plpgsql;
483
+
484
+ -- _delta_holder_stop: a listener lets go of every name it holds, and its lock.
485
+ CREATE OR REPLACE FUNCTION _delta_holder_stop(p_holder INTEGER) RETURNS void AS $$
486
+ BEGIN
487
+ DELETE FROM _delta_held WHERE holder = p_holder;
488
+ PERFORM pg_advisory_unlock_shared(hashtext('_delta_held'), p_holder);
489
+ END;
490
+ $$ LANGUAGE plpgsql;
491
+
492
+ -- _delta_hold: listener `p_holder` has `p_doc` open, from before it reads it.
493
+ -- It waits for the writes running now that may tell a document of the name's
494
+ -- prefix (each holds that prefix's lock, _delta_holders): they asked who holds
495
+ -- what without this name, so the read that follows must see what they wrote.
496
+ -- A write that asks after this commits sees the name.
497
+ CREATE OR REPLACE FUNCTION _delta_hold(p_holder INTEGER, p_doc TEXT) RETURNS void AS $$
498
+ BEGIN
499
+ INSERT INTO _delta_held (doc_name, holder) VALUES (p_doc, p_holder) ON CONFLICT DO NOTHING;
500
+ PERFORM pg_advisory_xact_lock(hashtext('_delta_hold'), hashtext(COALESCE((_delta_find_doc(p_doc)).prefix, '')));
501
+ END;
502
+ $$ LANGUAGE plpgsql;
503
+
504
+ -- _delta_let_go: listener `p_holder` has `p_doc` open no longer.
505
+ CREATE OR REPLACE FUNCTION _delta_let_go(p_holder INTEGER, p_doc TEXT) RETURNS void
506
+ LANGUAGE sql AS $$ DELETE FROM _delta_held WHERE doc_name = p_doc AND holder = p_holder; $$;