dzql 0.8.0 → 0.10.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.
@@ -75,7 +75,7 @@ END $$;
75
75
  -- ---------------------------------------------------------------------------
76
76
 
77
77
  DROP FUNCTION IF EXISTS _delta_cascade_remove(TEXT, BIGINT, TEXT[]);
78
- CREATE OR REPLACE FUNCTION _delta_cascade_remove(
78
+ CREATE OR REPLACE FUNCTION _delta_cascade_remove_framework(
79
79
  p_collection_key TEXT,
80
80
  p_id BIGINT
81
81
  ) RETURNS JSONB AS $$
@@ -155,6 +155,12 @@ BEGIN
155
155
  END;
156
156
  $$ LANGUAGE plpgsql;
157
157
 
158
+ -- the framework's, until dzql's compiler (`compileCascades`) writes one per
159
+ -- collection; the framework's own recursion goes through this name, so a
160
+ -- collection either way cascades into the other
161
+ CREATE OR REPLACE FUNCTION _delta_cascade_remove(p_collection_key TEXT, p_id BIGINT)
162
+ RETURNS JSONB LANGUAGE sql AS $$ SELECT _delta_cascade_remove_framework(p_collection_key, p_id); $$;
163
+
158
164
  -- ---------------------------------------------------------------------------
159
165
  -- _delta_cascade_rows: the rows _delta_cascade_remove would take, read before
160
166
  -- it takes them, in the order it emits their removes -- so who holds each can
@@ -162,7 +168,7 @@ $$ LANGUAGE plpgsql;
162
168
  -- ---------------------------------------------------------------------------
163
169
 
164
170
  DROP FUNCTION IF EXISTS _delta_cascade_rows(TEXT, BIGINT, TEXT[]);
165
- CREATE OR REPLACE FUNCTION _delta_cascade_rows(
171
+ CREATE OR REPLACE FUNCTION _delta_cascade_rows_framework(
166
172
  p_collection_key TEXT,
167
173
  p_id BIGINT
168
174
  ) RETURNS JSONB AS $$
@@ -209,6 +215,9 @@ BEGIN
209
215
  END;
210
216
  $$ LANGUAGE plpgsql STABLE;
211
217
 
218
+ CREATE OR REPLACE FUNCTION _delta_cascade_rows(p_collection_key TEXT, p_id BIGINT)
219
+ RETURNS JSONB LANGUAGE sql STABLE AS $$ SELECT _delta_cascade_rows_framework(p_collection_key, p_id); $$;
220
+
212
221
  -- ---------------------------------------------------------------------------
213
222
  -- _delta_hanging_rows: the rows that hang from a row -- its children, theirs,
214
223
  -- and on down, whichever documents include them -- parent first, each
@@ -216,7 +225,7 @@ $$ LANGUAGE plpgsql STABLE;
216
225
  -- hanging in sqlite.ts).
217
226
  -- ---------------------------------------------------------------------------
218
227
 
219
- CREATE OR REPLACE FUNCTION _delta_hanging_rows(p_collection_key TEXT, p_id BIGINT)
228
+ CREATE OR REPLACE FUNCTION _delta_hanging_rows_framework(p_collection_key TEXT, p_id BIGINT)
220
229
  RETURNS JSONB AS $$
221
230
  DECLARE
222
231
  v_child RECORD;
@@ -238,6 +247,9 @@ BEGIN
238
247
  END;
239
248
  $$ LANGUAGE plpgsql STABLE;
240
249
 
250
+ CREATE OR REPLACE FUNCTION _delta_hanging_rows(p_collection_key TEXT, p_id BIGINT)
251
+ RETURNS JSONB LANGUAGE sql STABLE AS $$ SELECT _delta_hanging_rows_framework(p_collection_key, p_id); $$;
252
+
241
253
  -- ---------------------------------------------------------------------------
242
254
  -- _delta_tell: tell every document that holds a row a write changed what
243
255
  -- changed for it (todo #28), and answer the writer's new version.
@@ -268,6 +280,53 @@ $$ LANGUAGE plpgsql STABLE;
268
280
  -- two-argument form logs none.
269
281
  -- ---------------------------------------------------------------------------
270
282
 
283
+ -- _delta_told: what one document is told of a write -- `p_rows`, the write's
284
+ -- touches with their holders, as _delta_tell gathers them -- by its name
285
+ -- `p_target`: an add, a replace of each field that changed, a remove; its
286
+ -- root's arrival or leaving as a replace. The framework's answer is
287
+ -- `_delta_told_framework`; dzql's compiler (`compileTold`) replaces
288
+ -- `_delta_told` with one sending each document it compiled to its own.
289
+
290
+ CREATE OR REPLACE FUNCTION _delta_told_framework(p_def _delta_docs, p_target TEXT, p_rows JSONB)
291
+ RETURNS JSONB AS $$
292
+ DECLARE
293
+ v_t JSONB;
294
+ v_single BOOLEAN;
295
+ v_told JSONB := '[]'::jsonb;
296
+ v_was BOOLEAN;
297
+ v_is BOOLEAN;
298
+ v_root BOOLEAN;
299
+ v_path TEXT;
300
+ BEGIN
301
+ v_single := (_delta_resolve_scope(p_def, p_target)->>'mode') = 'single';
302
+ FOR v_t IN SELECT jsonb_array_elements(p_rows) LOOP
303
+ v_was := (v_t->'before') ? p_target;
304
+ v_is := (v_t->'holders') ? p_target;
305
+ CONTINUE WHEN NOT v_was AND NOT v_is;
306
+ CONTINUE WHEN v_was AND v_is AND (v_t ? 'carried'); -- carried along, and held either way: unchanged for it
307
+ v_root := v_single AND (v_t->>'coll') = p_def.root_collection;
308
+ v_path := CASE WHEN v_root THEN _delta_build_path(v_t->>'coll') ELSE _delta_build_path(v_t->>'coll', v_t->>'id') END;
309
+ -- each row as this document shows it: the columns it holds (_delta_show, todo #70)
310
+ IF v_was AND v_is THEN -- stayed: what changed
311
+ v_told := v_told || COALESCE((SELECT jsonb_agg(jsonb_build_object('op', 'replace', 'path', v_path || _delta_build_path(f), 'value', v) ORDER BY f COLLATE "C")
312
+ FROM jsonb_each(_delta_show(p_def, v_t->>'coll', v_t->'after')) AS e(f, v) WHERE v IS DISTINCT FROM v_t->'was'->f), '[]'::jsonb);
313
+ ELSIF v_is THEN
314
+ v_told := v_told || jsonb_build_array(jsonb_build_object(
315
+ 'op', CASE WHEN v_root THEN 'replace' ELSE 'add' END, 'path', v_path, 'value', _delta_show(p_def, v_t->>'coll', v_t->'after')));
316
+ ELSIF v_root THEN -- gone: null, or the empty root an implied document then opens with
317
+ v_told := v_told || jsonb_build_array(jsonb_build_object('op', 'replace', 'path', v_path, 'value',
318
+ CASE WHEN COALESCE(p_def.implied, FALSE) THEN _delta_show(p_def, v_t->>'coll', _delta_implied_root(p_def, (v_t->>'id')::BIGINT)) ELSE 'null'::jsonb END));
319
+ ELSE
320
+ v_told := v_told || jsonb_build_array(jsonb_build_object('op', 'remove', 'path', v_path));
321
+ END IF;
322
+ END LOOP;
323
+ RETURN v_told;
324
+ END;
325
+ $$ LANGUAGE plpgsql STABLE;
326
+
327
+ CREATE OR REPLACE FUNCTION _delta_told(p_def _delta_docs, p_target TEXT, p_rows JSONB)
328
+ RETURNS JSONB LANGUAGE sql STABLE AS $$ SELECT _delta_told_framework(p_def, p_target, p_rows); $$;
329
+
271
330
  CREATE OR REPLACE FUNCTION _delta_tell(p_writer TEXT, p_touched JSONB, p_applied JSONB)
272
331
  RETURNS BIGINT AS $$
273
332
  DECLARE
@@ -277,12 +336,7 @@ DECLARE
277
336
  v_targets TEXT[] := ARRAY[]::text[];
278
337
  v_target TEXT;
279
338
  v_def _delta_docs;
280
- v_single BOOLEAN;
281
339
  v_told JSONB;
282
- v_was BOOLEAN;
283
- v_is BOOLEAN;
284
- v_root BOOLEAN;
285
- v_path TEXT;
286
340
  v_version BIGINT;
287
341
  v_next JSONB := '{}'::jsonb; -- a row's holders, as its next touch found them
288
342
  v_key TEXT;
@@ -309,29 +363,7 @@ BEGIN
309
363
  LOOP
310
364
  v_def := _delta_find_doc(v_target);
311
365
  CONTINUE WHEN v_def.prefix IS NULL;
312
- v_single := (_delta_resolve_scope(v_def, v_target)->>'mode') = 'single';
313
- v_told := '[]'::jsonb;
314
- FOR v_t IN SELECT jsonb_array_elements(v_rows) LOOP
315
- v_was := (v_t->'before') ? v_target;
316
- v_is := (v_t->'holders') ? v_target;
317
- CONTINUE WHEN NOT v_was AND NOT v_is;
318
- CONTINUE WHEN v_was AND v_is AND (v_t ? 'carried'); -- carried along, and held either way: unchanged for it
319
- v_root := v_single AND (v_t->>'coll') = v_def.root_collection;
320
- v_path := CASE WHEN v_root THEN _delta_build_path(v_t->>'coll') ELSE _delta_build_path(v_t->>'coll', v_t->>'id') END;
321
- -- each row as this document shows it: the columns it holds (_delta_show, todo #70)
322
- IF v_was AND v_is THEN -- stayed: what changed
323
- v_told := v_told || COALESCE((SELECT jsonb_agg(jsonb_build_object('op', 'replace', 'path', v_path || _delta_build_path(f), 'value', v) ORDER BY f COLLATE "C")
324
- FROM jsonb_each(_delta_show(v_def, v_t->>'coll', v_t->'after')) AS e(f, v) WHERE v IS DISTINCT FROM v_t->'was'->f), '[]'::jsonb);
325
- ELSIF v_is THEN
326
- v_told := v_told || jsonb_build_array(jsonb_build_object(
327
- 'op', CASE WHEN v_root THEN 'replace' ELSE 'add' END, 'path', v_path, 'value', _delta_show(v_def, v_t->>'coll', v_t->'after')));
328
- ELSIF v_root THEN -- gone: null, or the empty root an implied document then opens with
329
- v_told := v_told || jsonb_build_array(jsonb_build_object('op', 'replace', 'path', v_path, 'value',
330
- CASE WHEN COALESCE(v_def.implied, FALSE) THEN _delta_show(v_def, v_t->>'coll', _delta_implied_root(v_def, (v_t->>'id')::BIGINT)) ELSE 'null'::jsonb END));
331
- ELSE
332
- v_told := v_told || jsonb_build_array(jsonb_build_object('op', 'remove', 'path', v_path));
333
- END IF;
334
- END LOOP;
366
+ v_told := _delta_told(v_def, v_target, v_rows);
335
367
  IF v_target = p_writer THEN
336
368
  v_version := _delta_bump_and_notify(p_writer, v_told, CASE WHEN p_applied IS DISTINCT FROM v_told THEN p_applied END);
337
369
  ELSIF jsonb_array_length(v_told) > 0 THEN
@@ -353,7 +385,7 @@ RETURNS BIGINT LANGUAGE sql AS $$ SELECT _delta_tell(p_writer, p_touched, NULL);
353
385
  -- not yet written opens empty). `there` in sqlite.ts.
354
386
  -- ---------------------------------------------------------------------------
355
387
 
356
- CREATE OR REPLACE FUNCTION _delta_row_there(p_collection_key TEXT, p_id BIGINT)
388
+ CREATE OR REPLACE FUNCTION _delta_row_there_framework(p_collection_key TEXT, p_id BIGINT)
357
389
  RETURNS BOOLEAN AS $$
358
390
  DECLARE
359
391
  v_coll RECORD;
@@ -367,6 +399,10 @@ BEGIN
367
399
  END;
368
400
  $$ LANGUAGE plpgsql STABLE;
369
401
 
402
+ -- the framework's, until dzql's compiler (`compileRows`) writes one per collection
403
+ CREATE OR REPLACE FUNCTION _delta_row_there(p_collection_key TEXT, p_id BIGINT)
404
+ RETURNS BOOLEAN LANGUAGE sql STABLE AS $$ SELECT _delta_row_there_framework(p_collection_key, p_id); $$;
405
+
370
406
  -- ---------------------------------------------------------------------------
371
407
  -- _delta_assert_parent_held: a parent key written (in `p_value`, over the row
372
408
  -- as it is, `p_row`) moves the row, and only under a parent the document holds,
@@ -414,6 +450,136 @@ RETURNS JSONB AS $$
414
450
  ELSE COALESCE((SELECT jsonb_object_agg(k, v) FROM jsonb_each(p_columns) AS e(k, v) WHERE (p_def.held->p_coll) ? k), '{}'::jsonb) END;
415
451
  $$ LANGUAGE sql STABLE;
416
452
 
453
+ -- _delta_root_there: is a single document's root row `p_id` there now, and
454
+ -- does it meet the whole of its name's scope (a column beside its id), as
455
+ -- delta_open reads it? Asked before each op of a write (delta_apply). The
456
+ -- framework's runs the scope's conditions as text (`p_scope`, the resolver's
457
+ -- answer) over the root's current rows (`p_view`); dzql's compiler
458
+ -- (`compileRootThere`) replaces `_delta_root_there` with one sending each
459
+ -- document it compiled to its own.
460
+ CREATE OR REPLACE FUNCTION _delta_root_there_framework(p_def _delta_docs, p_doc_name TEXT, p_scope JSONB, p_view TEXT, p_id BIGINT)
461
+ RETURNS BOOLEAN AS $$
462
+ DECLARE
463
+ v_exists BOOLEAN;
464
+ BEGIN
465
+ EXECUTE format('SELECT EXISTS (SELECT 1 FROM %I t WHERE t.id = $1 AND %s)', p_view, p_scope->>'where') INTO v_exists USING p_id;
466
+ RETURN v_exists;
467
+ END;
468
+ $$ LANGUAGE plpgsql STABLE;
469
+
470
+ CREATE OR REPLACE FUNCTION _delta_root_there(p_def _delta_docs, p_doc_name TEXT, p_scope JSONB, p_view TEXT, p_id BIGINT)
471
+ RETURNS BOOLEAN LANGUAGE sql STABLE AS $$ SELECT _delta_root_there_framework(p_def, p_doc_name, p_scope, p_view, p_id); $$;
472
+
473
+ -- _delta_write_columns: the columns of `p_coll` a document writes -- as the
474
+ -- process writing declares them (_delta_cols), those the document holds
475
+ -- (_delta_held_columns). The framework's, until dzql's compiler
476
+ -- (`compileColumns`) replaces it with one sending each document it compiled to
477
+ -- its own.
478
+ CREATE OR REPLACE FUNCTION _delta_write_columns_framework(p_def _delta_docs, p_coll TEXT, p_columns_def JSONB)
479
+ RETURNS JSONB LANGUAGE sql STABLE AS $$ SELECT _delta_held_columns(p_def, p_coll, _delta_cols(p_coll, p_columns_def)); $$;
480
+
481
+ CREATE OR REPLACE FUNCTION _delta_write_columns(p_def _delta_docs, p_coll TEXT, p_columns_def JSONB)
482
+ RETURNS JSONB LANGUAGE sql STABLE AS $$ SELECT _delta_write_columns_framework(p_def, p_coll, p_columns_def); $$;
483
+
484
+ -- ---------------------------------------------------------------------------
485
+ -- A row's SQL, as delta_apply runs it: the framework's, built from the table's
486
+ -- name at run time; dzql's compiler (`compileRows`) replaces each with one
487
+ -- written for each collection it compiled, any other sent here.
488
+ -- _delta_row_lock: a row as it stands, locked for the write (NULL: not there)
489
+ -- _delta_row_put: a replace's row stored -- a temporal one a new version (one
490
+ -- this write made gives way to it; the standing one closes at `p_ts`), else
491
+ -- the row again -- answered as stored
492
+ -- _delta_next_id: an add's id, minted
493
+ -- _delta_row_clear_moment: a temporal version this write made and took back
494
+ -- (it never was outside the write), so an add of its id takes its place
495
+ -- (_delta_row_there, above, is an add's "is the id there now".)
496
+ -- ---------------------------------------------------------------------------
497
+
498
+ CREATE OR REPLACE FUNCTION _delta_row_lock_framework(p_coll TEXT, p_table TEXT, p_temporal BOOLEAN, p_id BIGINT)
499
+ RETURNS JSONB AS $$
500
+ DECLARE
501
+ v_row JSONB;
502
+ BEGIN
503
+ IF p_temporal THEN
504
+ EXECUTE format('SELECT to_jsonb(t.*) FROM %I t WHERE t.id = $1 AND valid_to IS NULL FOR UPDATE', p_table) INTO v_row USING p_id;
505
+ ELSE
506
+ EXECUTE format('SELECT to_jsonb(t.*) FROM %I t WHERE t.id = $1 FOR UPDATE', p_table) INTO v_row USING p_id;
507
+ END IF;
508
+ RETURN v_row;
509
+ END;
510
+ $$ LANGUAGE plpgsql;
511
+
512
+ CREATE OR REPLACE FUNCTION _delta_row_put_framework(p_coll TEXT, p_table TEXT, p_temporal BOOLEAN, p_id BIGINT, p_row JSONB, p_ts TIMESTAMPTZ)
513
+ RETURNS JSONB AS $$
514
+ DECLARE
515
+ v_row JSONB;
516
+ BEGIN
517
+ IF p_temporal THEN
518
+ EXECUTE format('DELETE FROM %I WHERE id = $1 AND valid_to IS NULL AND valid_from = $2', p_table) USING p_id, p_ts;
519
+ EXECUTE format('UPDATE %I SET valid_to = $2 WHERE id = $1 AND valid_to IS NULL', p_table) USING p_id, p_ts;
520
+ EXECUTE format('INSERT INTO %I AS t SELECT * FROM jsonb_populate_record(null::%I, $1) RETURNING to_jsonb(t.*)', p_table, p_table)
521
+ INTO v_row USING p_row || jsonb_build_object('valid_from', p_ts, 'valid_to', NULL);
522
+ ELSE
523
+ EXECUTE format('DELETE FROM %I WHERE id = $1', p_table) USING p_id;
524
+ EXECUTE format('INSERT INTO %I AS t SELECT * FROM jsonb_populate_record(null::%I, $1) RETURNING to_jsonb(t.*)', p_table, p_table)
525
+ INTO v_row USING p_row;
526
+ END IF;
527
+ RETURN v_row;
528
+ END;
529
+ $$ LANGUAGE plpgsql;
530
+
531
+ CREATE OR REPLACE FUNCTION _delta_next_id_framework(p_coll TEXT, p_table TEXT)
532
+ RETURNS BIGINT AS $$
533
+ DECLARE
534
+ v_id BIGINT;
535
+ BEGIN
536
+ EXECUTE format('SELECT nextval(%L)', 'seq_' || p_table) INTO v_id;
537
+ RETURN v_id;
538
+ END;
539
+ $$ LANGUAGE plpgsql;
540
+
541
+ CREATE OR REPLACE FUNCTION _delta_row_clear_moment_framework(p_coll TEXT, p_table TEXT, p_id BIGINT, p_ts TIMESTAMPTZ)
542
+ RETURNS void AS $$
543
+ BEGIN
544
+ EXECUTE format('DELETE FROM %I WHERE id = $1 AND valid_from = $2 AND valid_to = $2', p_table) USING p_id, p_ts;
545
+ END;
546
+ $$ LANGUAGE plpgsql;
547
+
548
+ -- _delta_row_add: an add's row stored, naming the columns the row gives (_delta_insert_sql), answered as stored
549
+ CREATE OR REPLACE FUNCTION _delta_row_add_framework(p_coll TEXT, p_table TEXT, p_row JSONB)
550
+ RETURNS JSONB AS $$
551
+ DECLARE
552
+ v_row JSONB;
553
+ BEGIN
554
+ EXECUTE _delta_insert_sql(p_table, p_row) || ' RETURNING to_jsonb(t.*)' INTO v_row USING p_row;
555
+ RETURN v_row;
556
+ END;
557
+ $$ LANGUAGE plpgsql;
558
+
559
+ -- _delta_row_read: a row as it stands, without its validity (NULL: not there)
560
+ CREATE OR REPLACE FUNCTION _delta_row_read_framework(p_coll TEXT, p_view TEXT, p_id BIGINT)
561
+ RETURNS JSONB AS $$
562
+ DECLARE
563
+ v_row JSONB;
564
+ BEGIN
565
+ EXECUTE format('SELECT _delta_strip_temporal(to_jsonb(t.*)) FROM %I t WHERE t.id = $1', p_view) INTO v_row USING p_id;
566
+ RETURN v_row;
567
+ END;
568
+ $$ LANGUAGE plpgsql STABLE;
569
+
570
+ CREATE OR REPLACE FUNCTION _delta_row_add(p_coll TEXT, p_table TEXT, p_row JSONB)
571
+ RETURNS JSONB LANGUAGE sql AS $$ SELECT _delta_row_add_framework(p_coll, p_table, p_row); $$;
572
+ CREATE OR REPLACE FUNCTION _delta_row_read(p_coll TEXT, p_view TEXT, p_id BIGINT)
573
+ RETURNS JSONB LANGUAGE sql STABLE AS $$ SELECT _delta_row_read_framework(p_coll, p_view, p_id); $$;
574
+ CREATE OR REPLACE FUNCTION _delta_row_lock(p_coll TEXT, p_table TEXT, p_temporal BOOLEAN, p_id BIGINT)
575
+ RETURNS JSONB LANGUAGE sql AS $$ SELECT _delta_row_lock_framework(p_coll, p_table, p_temporal, p_id); $$;
576
+ CREATE OR REPLACE FUNCTION _delta_row_put(p_coll TEXT, p_table TEXT, p_temporal BOOLEAN, p_id BIGINT, p_row JSONB, p_ts TIMESTAMPTZ)
577
+ RETURNS JSONB LANGUAGE sql AS $$ SELECT _delta_row_put_framework(p_coll, p_table, p_temporal, p_id, p_row, p_ts); $$;
578
+ CREATE OR REPLACE FUNCTION _delta_next_id(p_coll TEXT, p_table TEXT)
579
+ RETURNS BIGINT LANGUAGE sql AS $$ SELECT _delta_next_id_framework(p_coll, p_table); $$;
580
+ CREATE OR REPLACE FUNCTION _delta_row_clear_moment(p_coll TEXT, p_table TEXT, p_id BIGINT, p_ts TIMESTAMPTZ)
581
+ RETURNS void LANGUAGE sql AS $$ SELECT _delta_row_clear_moment_framework(p_coll, p_table, p_id, p_ts); $$;
582
+
417
583
  CREATE OR REPLACE FUNCTION _delta_whole(p_columns JSONB, p_value JSONB)
418
584
  RETURNS JSONB AS $$
419
585
  DECLARE
@@ -524,11 +690,11 @@ BEGIN
524
690
  -- documents, which showed it already (ensureImpliedRoot in sqlite.ts); an
525
691
  -- empty write makes nothing
526
692
  IF NOT v_is_list AND COALESCE(v_def.implied, FALSE) AND jsonb_array_length(p_ops) > 0 THEN
527
- EXECUTE format('SELECT EXISTS (SELECT 1 FROM %I WHERE id = $1)', v_root_view) INTO v_exists USING v_doc_id;
693
+ v_exists := _delta_row_there(v_def.root_collection, v_doc_id);
528
694
  PERFORM _delta_make_implied(v_def, v_doc_id, p_ops, v_ts);
529
695
  IF NOT v_exists THEN
530
696
  v_passed := jsonb_build_object(v_root_coll.table_name, v_doc_id);
531
- EXECUTE format('SELECT _delta_strip_temporal(to_jsonb(t.*)) FROM %I t WHERE t.id = $1', v_root_view) INTO v_row USING v_doc_id;
697
+ v_row := _delta_row_read(v_def.root_collection, v_root_view, v_doc_id);
532
698
  IF v_row IS NOT NULL THEN
533
699
  v_touched := jsonb_build_array(jsonb_build_object('coll', v_def.root_collection, 'id', v_doc_id, 'after', v_row, 'carried', TRUE,
534
700
  'before', COALESCE((SELECT jsonb_agg(n) FROM unnest(_delta_holders(v_def.root_collection, v_row, p_doc_name)) AS n
@@ -541,7 +707,7 @@ BEGIN
541
707
  -- An empty write is asked what each op is (below): a single document whose
542
708
  -- root is not there is not found, as on SQLite -- unless implied, or a walk.
543
709
  IF jsonb_array_length(p_ops) = 0 AND NOT v_is_list AND NOT COALESCE(p_walk, FALSE) AND NOT COALESCE(v_def.implied, FALSE) THEN
544
- EXECUTE format('SELECT EXISTS (SELECT 1 FROM %I t WHERE t.id = $1 AND %s)', v_root_view, v_scope->>'where') INTO v_exists USING v_doc_id;
710
+ v_exists := _delta_root_there(v_def, p_doc_name, v_scope, v_root_view, v_doc_id);
545
711
  IF NOT v_exists THEN
546
712
  RAISE EXCEPTION 'document not found: % (its root, %, is not there)',
547
713
  p_doc_name, _delta_build_path(v_def.root_collection, v_doc_id::text)
@@ -565,7 +731,7 @@ BEGIN
565
731
  -- (_delta_walk_plan), as SQLite's and ./json's are.
566
732
  IF NOT v_is_list AND NOT COALESCE(p_walk, FALSE) THEN -- a walk not said (null) is none
567
733
  -- there, and meeting the whole of its name's scope (a column beside its id), as delta_open reads it
568
- EXECUTE format('SELECT EXISTS (SELECT 1 FROM %I t WHERE t.id = $1 AND %s)', v_root_view, v_scope->>'where') INTO v_exists USING v_doc_id;
734
+ v_exists := _delta_root_there(v_def, p_doc_name, v_scope, v_root_view, v_doc_id);
569
735
  v_opened := COALESCE(v_opened, v_exists OR COALESCE(v_def.implied, FALSE));
570
736
  v_back := FALSE;
571
737
  IF NOT v_exists AND v_opened AND v_op->>'op' = 'add' THEN
@@ -594,7 +760,7 @@ BEGIN
594
760
  THEN COALESCE((SELECT jsonb_object_agg(c, '{"nullable": true}'::jsonb) FROM unnest(_delta_stored_columns(v_coll.table_name)) AS c
595
761
  WHERE c NOT IN ('id', 'valid_from', 'valid_to') AND c IS DISTINCT FROM v_coll.parent_fk), '{}'::jsonb)
596
762
  || _delta_cols(v_coll_key, v_coll.columns_def)
597
- ELSE _delta_held_columns(v_def, v_coll_key, _delta_cols(v_coll_key, v_coll.columns_def)) END;
763
+ ELSE _delta_write_columns(v_def, v_coll_key, v_coll.columns_def) END;
598
764
 
599
765
  -- Scope guard: an op may only target the doc's root or an included
600
766
  -- collection. Defence-in-depth — a client that opened doc A must not write
@@ -637,17 +803,7 @@ BEGIN
637
803
  v_blank := CASE WHEN array_length(v_parts, 1) = 1 THEN _delta_whole(v_cols, v_op->'value') ELSE '{}'::jsonb END;
638
804
 
639
805
  -- Read + lock
640
- IF v_coll.temporal THEN
641
- EXECUTE format(
642
- 'SELECT to_jsonb(t.*) FROM %I t WHERE t.id = $1 AND valid_to IS NULL FOR UPDATE',
643
- v_coll.table_name
644
- ) INTO v_row USING v_doc_id;
645
- ELSE
646
- EXECUTE format(
647
- 'SELECT to_jsonb(t.*) FROM %I t WHERE t.id = $1 FOR UPDATE',
648
- v_coll.table_name
649
- ) INTO v_row USING v_doc_id;
650
- END IF;
806
+ v_row := _delta_row_lock(v_coll_key, v_coll.table_name, v_coll.temporal, v_doc_id); -- read, and lock
651
807
 
652
808
  IF v_row IS NULL THEN
653
809
  RAISE EXCEPTION 'root row not found: %', v_def.root_collection USING ERRCODE = 'P0002';
@@ -660,34 +816,8 @@ BEGIN
660
816
  -- is the path's (it renumbered the row).
661
817
  v_new_row := v_row || v_blank || (v_op->'value') || jsonb_build_object('id', v_doc_id);
662
818
 
663
- IF v_coll.temporal THEN
664
- -- One version per write: a version this write made (its valid_from is the
665
- -- write's NOW()) gives way to the next, rather than closing at the same
666
- -- instant and colliding with it on (id, valid_from).
667
- EXECUTE format(
668
- 'DELETE FROM %I WHERE id = $1 AND valid_to IS NULL AND valid_from = $2',
669
- v_coll.table_name
670
- ) USING v_doc_id, v_ts;
671
- EXECUTE format(
672
- 'UPDATE %I SET valid_to = $2 WHERE id = $1 AND valid_to IS NULL',
673
- v_coll.table_name
674
- ) USING v_doc_id, v_ts;
675
-
676
- v_new_row := v_new_row || jsonb_build_object('valid_from', v_ts, 'valid_to', NULL);
677
-
678
- EXECUTE format(
679
- 'INSERT INTO %I AS t SELECT * FROM jsonb_populate_record(null::%I, $1) RETURNING to_jsonb(t.*)',
680
- v_coll.table_name, v_coll.table_name
681
- ) INTO v_new_row USING v_new_row;
682
-
683
- v_new_row := _delta_strip_temporal(v_new_row);
684
- ELSE
685
- EXECUTE format('DELETE FROM %I WHERE id = $1', v_coll.table_name) USING v_doc_id;
686
- EXECUTE format(
687
- 'INSERT INTO %I AS t SELECT * FROM jsonb_populate_record(null::%I, $1) RETURNING to_jsonb(t.*)',
688
- v_coll.table_name, v_coll.table_name
689
- ) INTO v_new_row USING v_new_row;
690
- END IF;
819
+ v_new_row := _delta_row_put(v_coll_key, v_coll.table_name, v_coll.temporal, v_doc_id, v_new_row, v_ts); -- the new version, or the row again
820
+ IF v_coll.temporal THEN v_new_row := _delta_strip_temporal(v_new_row); END IF;
691
821
 
692
822
  -- The root written through its document stays in its scope, as a list's
693
823
  -- rows do (below): one written out of the rest of its name's scope -- a
@@ -737,7 +867,7 @@ BEGIN
737
867
  v_id_text := v_parts[2];
738
868
  -- Auto-generate ID from sequence if path ends with '-'
739
869
  IF v_id_text = '-' THEN
740
- EXECUTE format('SELECT nextval(%L)', 'seq_' || v_coll.table_name) INTO v_id;
870
+ v_id := _delta_next_id(v_coll_key, v_coll.table_name);
741
871
  ELSE
742
872
  v_id := _delta_row_id(v_coll_key, v_id_text);
743
873
  END IF;
@@ -767,7 +897,7 @@ BEGIN
767
897
  v_exists := NOT _delta_serial_new(v_coll.table_name, v_id);
768
898
  END IF;
769
899
  IF NOT v_exists AND (v_id_text <> '-' OR v_coll.temporal) THEN
770
- EXECUTE format('SELECT EXISTS (SELECT 1 FROM %I WHERE id = $1)', v_view) INTO v_exists USING v_id;
900
+ v_exists := _delta_row_there(v_coll_key, v_id);
771
901
  END IF;
772
902
  IF v_exists THEN
773
903
  RAISE EXCEPTION 'id already taken: % -- add to % for a new id',
@@ -847,10 +977,7 @@ BEGIN
847
977
  -- valid_from and valid_to both the write's NOW()) never was outside it,
848
978
  -- so an add of the same id takes its place rather than colliding with it
849
979
  -- on (id, valid_from). SQLite's insertCollectionRow keeps the same rule.
850
- EXECUTE format(
851
- 'DELETE FROM %I WHERE id = $1 AND valid_from = $2 AND valid_to = $2',
852
- v_coll.table_name
853
- ) USING v_id, v_ts;
980
+ PERFORM _delta_row_clear_moment(v_coll_key, v_coll.table_name, v_id, v_ts);
854
981
  v_new_row := v_new_row || jsonb_build_object('valid_from', v_ts, 'valid_to', NULL);
855
982
  END IF;
856
983
 
@@ -858,7 +985,7 @@ BEGIN
858
985
  -- value its column casts (a scope's '1' into an integer, a date into a
859
986
  -- timestamptz) is told as a later open reads it, so the broadcast, the
860
987
  -- ledger's entry and undo's guard agree with the table.
861
- EXECUTE _delta_insert_sql(v_coll.table_name, v_new_row) || ' RETURNING to_jsonb(t.*)' INTO v_new_row USING v_new_row;
988
+ v_new_row := _delta_row_add(v_coll_key, v_coll.table_name, v_new_row);
862
989
 
863
990
  -- An id the add named moves the serial past it, so a later /- does not meet it.
864
991
  IF v_id_text <> '-' AND v_id > COALESCE((v_passed->>v_coll.table_name)::BIGINT, 0) THEN
@@ -955,17 +1082,7 @@ BEGIN
955
1082
  USING ERRCODE = 'P0002';
956
1083
  END IF;
957
1084
 
958
- IF v_coll.temporal THEN
959
- EXECUTE format(
960
- 'SELECT to_jsonb(t.*) FROM %I t WHERE t.id = $1 AND valid_to IS NULL FOR UPDATE',
961
- v_coll.table_name
962
- ) INTO v_row USING v_id;
963
- ELSE
964
- EXECUTE format(
965
- 'SELECT to_jsonb(t.*) FROM %I t WHERE t.id = $1 FOR UPDATE',
966
- v_coll.table_name
967
- ) INTO v_row USING v_id;
968
- END IF;
1085
+ v_row := _delta_row_lock(v_coll_key, v_coll.table_name, v_coll.temporal, v_id); -- read, and lock
969
1086
 
970
1087
  IF v_row IS NULL THEN
971
1088
  RAISE EXCEPTION 'row not found: %/%', v_coll_key, v_id USING ERRCODE = 'P0002';
@@ -1000,35 +1117,7 @@ BEGIN
1000
1117
  -- an add's is (it renumbered the row).
1001
1118
  v_new_row := v_row || v_blank || (v_op->'value') || jsonb_build_object('id', v_id);
1002
1119
 
1003
- IF v_coll.temporal THEN
1004
- -- One version per write: a version this write made (its valid_from is the
1005
- -- write's NOW()) gives way to the next, rather than closing at the same
1006
- -- instant and colliding with it on (id, valid_from).
1007
- EXECUTE format(
1008
- 'DELETE FROM %I WHERE id = $1 AND valid_to IS NULL AND valid_from = $2',
1009
- v_coll.table_name
1010
- ) USING v_id, v_ts;
1011
- EXECUTE format(
1012
- 'UPDATE %I SET valid_to = $2 WHERE id = $1 AND valid_to IS NULL',
1013
- v_coll.table_name
1014
- ) USING v_id, v_ts;
1015
-
1016
- v_new_row := v_new_row || jsonb_build_object('valid_from', v_ts, 'valid_to', NULL);
1017
-
1018
- EXECUTE format(
1019
- 'INSERT INTO %I AS t SELECT * FROM jsonb_populate_record(null::%I, $1) RETURNING to_jsonb(t.*)',
1020
- v_coll.table_name, v_coll.table_name
1021
- ) INTO v_new_row USING v_new_row;
1022
- ELSE
1023
- -- Non-temporal: UPDATE with merged row
1024
- EXECUTE format(
1025
- 'DELETE FROM %I WHERE id = $1', v_coll.table_name
1026
- ) USING v_id;
1027
- EXECUTE format(
1028
- 'INSERT INTO %I AS t SELECT * FROM jsonb_populate_record(null::%I, $1) RETURNING to_jsonb(t.*)',
1029
- v_coll.table_name, v_coll.table_name
1030
- ) INTO v_new_row USING v_new_row;
1031
- END IF;
1120
+ v_new_row := _delta_row_put(v_coll_key, v_coll.table_name, v_coll.temporal, v_id, v_new_row, v_ts); -- the new version, or the row again
1032
1121
 
1033
1122
  -- A list's root row written through the list stays in its scope, as an
1034
1123
  -- add through it is given its bindings: a field, a row or a merge that
@@ -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,7 +254,7 @@ $$;
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.
236
- CREATE OR REPLACE FUNCTION _delta_same(p_coll TEXT, p_field TEXT, a JSONB, b JSONB)
257
+ CREATE OR REPLACE FUNCTION _delta_same_framework(p_coll TEXT, p_field TEXT, a JSONB, b JSONB)
237
258
  RETURNS BOOLEAN LANGUAGE sql STABLE AS $$
238
259
  SELECT CASE
239
260
  WHEN jsonb_typeof(a) = 'string' AND jsonb_typeof(b) = 'string'
@@ -243,6 +264,31 @@ RETURNS BOOLEAN LANGUAGE sql STABLE AS $$
243
264
  END;
244
265
  $$;
245
266
 
267
+ -- the framework's, until dzql's compiler (`compileLedger`) writes each
268
+ -- collection's time fields in
269
+ CREATE OR REPLACE FUNCTION _delta_same(p_coll TEXT, p_field TEXT, a JSONB, b JSONB)
270
+ RETURNS BOOLEAN LANGUAGE sql STABLE AS $$ SELECT _delta_same_framework(p_coll, p_field, a, b); $$;
271
+
272
+ -- _delta_row_standing: row `p_id` of a collection as it stands, without its
273
+ -- validity, as {row} (row null: not there); NULL for a collection there is not.
274
+ -- What a walk guards a row beyond its document by.
275
+ CREATE OR REPLACE FUNCTION _delta_row_standing_framework(p_coll TEXT, p_id BIGINT)
276
+ RETURNS JSONB AS $$
277
+ DECLARE
278
+ v_coll RECORD;
279
+ v_row JSONB;
280
+ BEGIN
281
+ SELECT * INTO v_coll FROM _delta_collections WHERE collection_key = p_coll;
282
+ IF NOT FOUND THEN RETURN NULL; END IF;
283
+ 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))
284
+ INTO v_row USING p_id;
285
+ RETURN jsonb_build_object('row', v_row);
286
+ END;
287
+ $$ LANGUAGE plpgsql STABLE;
288
+
289
+ CREATE OR REPLACE FUNCTION _delta_row_standing(p_coll TEXT, p_id BIGINT)
290
+ RETURNS JSONB LANGUAGE sql STABLE AS $$ SELECT _delta_row_standing_framework(p_coll, p_id); $$;
291
+
246
292
  -- _delta_walk_key: a row's key in a walk -- the keyOf of planWalk in
247
293
  -- src/server/ledger.ts. A single document's root is one row at two paths,
248
294
  -- /root (a replace of it) and /root/id (its add, its remove): both are keyed
@@ -306,11 +352,9 @@ BEGIN
306
352
  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
353
  CONTINUE WHEN array_length(v_parts, 1) <> 2 OR v_parts[2] !~ '^[0-9]+$' OR v_parts[2]::numeric > 9007199254740991
308
354
  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));
355
+ v_row := _delta_row_standing(v_parts[1], v_parts[2]::bigint);
356
+ CONTINUE WHEN v_row IS NULL; -- a collection there is not
357
+ v_beyond := v_beyond || jsonb_build_array(jsonb_build_object('coll', v_parts[1], 'id', v_parts[2]::bigint, 'row', v_row->'row'));
314
358
  END LOOP;
315
359
  v_doc := _delta_beside(v_doc, v_def, v_beyond);
316
360
 
@@ -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 })`.