dzql 0.9.0 → 0.11.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.
@@ -27,71 +27,13 @@ RETURNS TABLE(version BIGINT, ops JSONB, applied JSONB) AS $$
27
27
  $$ LANGUAGE sql STABLE;
28
28
 
29
29
  -- ---------------------------------------------------------------------------
30
- -- delta_open_at: time-travel — load a doc as it existed at a point in time
30
+ -- delta_open_at: time-travel -- a document as it stood at a point in time.
31
+ -- generateSql sends each document to its own read as it stood
32
+ -- (compileLookups, dzql_open_at__<slug>); before it is applied there is none.
31
33
  -- ---------------------------------------------------------------------------
32
34
 
33
35
  CREATE OR REPLACE FUNCTION delta_open_at(p_doc_name TEXT, p_at TIMESTAMPTZ)
34
- RETURNS JSONB AS $$
35
- DECLARE
36
- v_def _delta_docs;
37
- v_resolved JSONB;
38
- v_mode TEXT;
39
- v_doc_id BIGINT;
40
- v_root_coll RECORD;
41
- v_root_row JSONB;
42
- v_result JSONB;
43
- v_coll_key TEXT;
44
- BEGIN
45
- v_def := _delta_find_doc(p_doc_name);
46
- IF v_def.prefix IS NULL THEN
47
- RAISE EXCEPTION 'no doc def for: %', p_doc_name
48
- USING ERRCODE = 'P0001';
49
- END IF;
50
-
51
- v_resolved := _delta_resolve_scope(v_def, p_doc_name);
52
- v_mode := v_resolved->>'mode';
53
-
54
- SELECT * INTO v_root_coll FROM _delta_collections
55
- WHERE collection_key = v_def.root_collection;
56
- IF NOT FOUND THEN
57
- RAISE EXCEPTION 'doc "%" references unknown root collection: %',
58
- p_doc_name, v_def.root_collection
59
- USING ERRCODE = 'P0001';
60
- END IF;
61
-
62
- -- List mode (todo #74): as delta_open reads a list, at p_at -- each row as it shows (todo #70).
63
- IF v_mode = 'list' THEN RETURN _delta_show_doc(v_def, _delta_read_list(v_def, v_resolved->>'where', p_at), FALSE); END IF;
64
-
65
- v_doc_id := (v_resolved->'values'->>'id')::BIGINT;
66
-
67
- -- the root as it stood, where it met the whole of its name's scope then
68
- -- (a column beside its id), as delta_open reads it now
69
- IF v_root_coll.temporal THEN
70
- EXECUTE format(
71
- 'SELECT to_jsonb(t.*) FROM %I t WHERE t.id = $1 AND %s AND %s',
72
- v_root_coll.table_name, _delta_temporal_where(p_at), v_resolved->>'where'
73
- ) INTO v_root_row USING v_doc_id;
74
- v_root_row := _delta_strip_temporal(v_root_row);
75
- ELSE
76
- EXECUTE format('SELECT to_jsonb(t.*) FROM %I t WHERE t.id = $1 AND %s', v_root_coll.table_name, v_resolved->>'where')
77
- INTO v_root_row USING v_doc_id;
78
- END IF;
79
-
80
- IF v_root_row IS NULL THEN RETURN NULL; END IF;
81
-
82
- v_result := jsonb_build_object(v_def.root_collection, v_root_row);
83
-
84
- FOREACH v_coll_key IN ARRAY v_def.include
85
- LOOP
86
- v_result := v_result || jsonb_build_object(
87
- v_coll_key,
88
- _delta_load_collection(v_coll_key, v_def.root_collection, v_doc_id, p_at)
89
- );
90
- END LOOP;
91
-
92
- RETURN _delta_show_doc(v_def, v_result, TRUE);
93
- END;
94
- $$ LANGUAGE plpgsql STABLE;
36
+ RETURNS JSONB LANGUAGE plpgsql STABLE AS $$ BEGIN RAISE EXCEPTION 'no doc def for: %', p_doc_name USING ERRCODE = 'P0001'; END $$;
95
37
 
96
38
  -- ---------------------------------------------------------------------------
97
39
  -- delta_prune_ops: clean up old log entries -- and the names nobody holds
@@ -29,6 +29,8 @@ CREATE TABLE IF NOT EXISTS _delta_ledger (
29
29
  CREATE INDEX IF NOT EXISTS idx_delta_ledger_doc ON _delta_ledger (doc_name, version);
30
30
  CREATE INDEX IF NOT EXISTS idx_delta_ledger_cursor ON _delta_ledger (cursor);
31
31
  CREATE INDEX IF NOT EXISTS idx_delta_ledger_undoes ON _delta_ledger (undoes);
32
+ -- a cursor's entries newest first: where an undo or a redo looks for its tip (_delta_ledger_tip)
33
+ CREATE INDEX IF NOT EXISTS idx_delta_ledger_cursor_id ON _delta_ledger (cursor, id);
32
34
 
33
35
  -- ---------------------------------------------------------------------------
34
36
  -- _delta_row_at: the row a document holds at a row's path, split -- the twin
@@ -204,25 +206,44 @@ $$ LANGUAGE plpgsql;
204
206
  -- what could be redone; an entry not undoable (a fact) starts no chain.
205
207
  -- ---------------------------------------------------------------------------
206
208
 
209
+ -- _delta_ledger_tip: the entry an undo (p_back) or a redo of a cursor takes --
210
+ -- the newest tip of the cursor's chains that goes the way asked: back, a tip at
211
+ -- an even depth (a write, or a redo); forward, one at an odd depth (an undo that
212
+ -- changed something) newer than the cursor's last write. Found newest first,
213
+ -- among the cursor's own entries, so it costs what was written since the
214
+ -- answer, not the cursor's whole history (todo #28): every entry of a chain
215
+ -- carries its write's cursor, as delta_walk records it (an entry recorded by
216
+ -- hand as walking another cursor's chain is not one of this cursor's tips).
207
217
  CREATE OR REPLACE FUNCTION _delta_ledger_tip(p_cursor TEXT, p_back BOOLEAN)
208
218
  RETURNS _delta_ledger AS $$
209
- WITH RECURSIVE chain(id, depth) AS (
210
- SELECT id, 0 FROM _delta_ledger WHERE cursor = p_cursor AND undoes IS NULL AND undoable
211
- UNION ALL
212
- SELECT l.id, c.depth + 1 FROM _delta_ledger l JOIN chain c ON l.undoes = c.id
213
- ),
214
- tips AS (
215
- SELECT c.id, c.depth FROM chain c
216
- WHERE NOT EXISTS (SELECT 1 FROM _delta_ledger w WHERE w.undoes = c.id)
217
- )
218
- SELECT l.* FROM _delta_ledger l JOIN tips t ON t.id = l.id
219
- WHERE CASE WHEN p_back THEN t.depth % 2 = 0
220
- ELSE t.depth % 2 = 1 AND l.undoable -- a walk recorded as changing nothing is never redone
221
- AND l.id > COALESCE((SELECT MAX(id) FROM _delta_ledger WHERE cursor = p_cursor AND undoes IS NULL AND undoable), 0)
222
- END
223
- ORDER BY l.id DESC
224
- LIMIT 1;
225
- $$ LANGUAGE sql STABLE;
219
+ DECLARE
220
+ v_entry _delta_ledger;
221
+ v_up _delta_ledger;
222
+ v_depth INT;
223
+ BEGIN
224
+ -- a step at a time, each the next older entry (LIMIT 1 on the cursor's index): it reads as far as it goes
225
+ SELECT * INTO v_entry FROM _delta_ledger WHERE cursor = p_cursor ORDER BY id DESC LIMIT 1;
226
+ WHILE v_entry.id IS NOT NULL LOOP
227
+ -- the cursor's last write, met newest first: a redo is of nothing older (a fresh write ends what could be redone)
228
+ IF NOT p_back AND v_entry.undoes IS NULL AND v_entry.undoable THEN RETURN NULL; END IF;
229
+ IF NOT EXISTS (SELECT 1 FROM _delta_ledger w WHERE w.undoes = v_entry.id) THEN -- a tip: nothing has walked it
230
+ -- its depth along its chain, up to the write that began it
231
+ v_depth := 0;
232
+ v_up := v_entry;
233
+ WHILE v_up.undoes IS NOT NULL LOOP
234
+ SELECT * INTO v_up FROM _delta_ledger WHERE id = v_up.undoes;
235
+ v_depth := v_depth + 1;
236
+ END LOOP;
237
+ IF v_up.cursor IS NOT DISTINCT FROM p_cursor AND v_up.undoable THEN -- a chain this cursor's undoable write began
238
+ IF p_back AND v_depth % 2 = 0 THEN RETURN v_entry; END IF;
239
+ IF NOT p_back AND v_depth % 2 = 1 AND v_entry.undoable THEN RETURN v_entry; END IF; -- a walk recorded as changing nothing is never redone
240
+ END IF;
241
+ END IF;
242
+ SELECT * INTO v_entry FROM _delta_ledger WHERE cursor = p_cursor AND id < v_entry.id ORDER BY id DESC LIMIT 1;
243
+ END LOOP;
244
+ RETURN NULL;
245
+ END;
246
+ $$ LANGUAGE plpgsql STABLE;
226
247
 
227
248
  -- JSON values the same, with JSON null and a missing value the same.
228
249
  CREATE OR REPLACE FUNCTION _delta_same(a JSONB, b JSONB)
@@ -233,15 +254,17 @@ $$;
233
254
  -- A field of a collection's row the same in two values: a timestamptz by the
234
255
  -- instant it names, whatever zone its text was written in (a ledger's entry
235
256
  -- keeps the session's TimeZone of its write), anything else as _delta_same.
257
+ -- generateSql writes each collection's time fields in (compileLedger); before
258
+ -- it is applied there is no collection, and values are the same as JSON.
236
259
  CREATE OR REPLACE FUNCTION _delta_same(p_coll TEXT, p_field TEXT, a JSONB, b JSONB)
237
- RETURNS BOOLEAN LANGUAGE sql STABLE AS $$
238
- SELECT CASE
239
- WHEN jsonb_typeof(a) = 'string' AND jsonb_typeof(b) = 'string'
240
- AND (SELECT _delta_cols(collection_key, columns_def)->p_field->>'type' FROM _delta_collections WHERE collection_key = p_coll) = 'timestamptz'
241
- THEN (a #>> '{}')::timestamptz = (b #>> '{}')::timestamptz
242
- ELSE _delta_same(a, b)
243
- END;
244
- $$;
260
+ RETURNS BOOLEAN LANGUAGE sql STABLE AS $$ SELECT _delta_same(a, b); $$;
261
+
262
+ -- _delta_row_standing: row `p_id` of a collection as it stands, without its
263
+ -- validity, as {row} (row null: not there); NULL for a collection there is not.
264
+ -- What a walk guards a row beyond its document by. generateSql writes one for
265
+ -- each collection (compileLedger).
266
+ CREATE OR REPLACE FUNCTION _delta_row_standing(p_coll TEXT, p_id BIGINT)
267
+ RETURNS JSONB LANGUAGE sql STABLE AS $$ SELECT NULL::jsonb; $$;
245
268
 
246
269
  -- _delta_walk_key: a row's key in a walk -- the keyOf of planWalk in
247
270
  -- src/server/ledger.ts. A single document's root is one row at two paths,
@@ -306,11 +329,9 @@ BEGIN
306
329
  FOR v_parts IN SELECT DISTINCT _delta_split_path(o->>'path') FROM jsonb_array_elements(p_entry.ops || p_entry.inverse) AS o LOOP
307
330
  CONTINUE WHEN array_length(v_parts, 1) <> 2 OR v_parts[2] !~ '^[0-9]+$' OR v_parts[2]::numeric > 9007199254740991
308
331
  OR v_parts[1] = v_def.root_collection OR v_parts[1] = ANY(COALESCE(v_def.include, ARRAY[]::text[]));
309
- SELECT * INTO v_coll FROM _delta_collections WHERE collection_key = v_parts[1];
310
- CONTINUE WHEN NOT FOUND;
311
- EXECUTE format('SELECT _delta_strip_temporal(to_jsonb(t.*)) FROM %I t WHERE t.id = $1', _delta_source_view(v_coll.table_name, v_coll.temporal))
312
- INTO v_row USING v_parts[2]::bigint;
313
- v_beyond := v_beyond || jsonb_build_array(jsonb_build_object('coll', v_parts[1], 'id', v_parts[2]::bigint, 'row', v_row));
332
+ v_row := _delta_row_standing(v_parts[1], v_parts[2]::bigint);
333
+ CONTINUE WHEN v_row IS NULL; -- a collection there is not
334
+ v_beyond := v_beyond || jsonb_build_array(jsonb_build_object('coll', v_parts[1], 'id', v_parts[2]::bigint, 'row', v_row->'row'));
314
335
  END LOOP;
315
336
  v_doc := _delta_beside(v_doc, v_def, v_beyond);
316
337
 
@@ -2,7 +2,7 @@
2
2
  -- auth-jwt — reference users schema + login / register stored functions.
3
3
  -- =========================================================================
4
4
  --
5
- -- Apply this alongside the @blueshed/delta/auth-jwt module. Consumers who
5
+ -- Apply this alongside the dzql/auth-jwt module. Consumers who
6
6
  -- want a different identity schema (sessions, OAuth, custom tables) can
7
7
  -- substitute their own `login` / `register` functions and pass the SQL
8
8
  -- strings via `jwtAuth({ loginSql, registerSql })`.