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.
@@ -13,22 +13,6 @@
13
13
  -- is pinned below, pg_temp last.
14
14
  -- ---------------------------------------------------------------------------
15
15
 
16
- CREATE OR REPLACE FUNCTION _delta_serial_past(p_table TEXT, p_id BIGINT)
17
- RETURNS void LANGUAGE plpgsql SECURITY DEFINER AS $$
18
- BEGIN
19
- IF p_id > 9007199254740991 OR NOT EXISTS (SELECT 1 FROM _delta_collections WHERE table_name = p_table)
20
- OR NOT has_table_privilege(COALESCE(NULLIF(current_setting('role'), 'none'), session_user), p_table, 'INSERT') THEN
21
- RETURN;
22
- END IF;
23
- EXECUTE format('SELECT setval(%L, $1) FROM %I WHERE $1 >= last_value + is_called::int',
24
- 'seq_' || p_table, 'seq_' || p_table) USING p_id;
25
- END;
26
- $$;
27
- -- the search path it was applied with, and pg_temp last (as _delta_holders, 001b)
28
- DO $$ BEGIN
29
- EXECUTE format('ALTER FUNCTION _delta_serial_past(TEXT, BIGINT) SET search_path = %s, pg_temp', COALESCE(NULLIF(current_setting('search_path'), ''), 'public'));
30
- END $$;
31
-
32
16
  -- ---------------------------------------------------------------------------
33
17
  -- _delta_serial_new: whether an id is past every one a collection's serial has
34
18
  -- handed out (minted, or passed for an add that named it): what an add may
@@ -37,25 +21,15 @@ END $$;
37
21
  -- other every id is taken, so it learns nothing of the table's rows.
38
22
  -- ---------------------------------------------------------------------------
39
23
 
24
+ -- generateSql writes each table's sequence in (compileLookups): as the
25
+ -- functions' owner, with the path they were applied with and pg_temp last, so a
26
+ -- caller's temporary _delta_collections, or a temporary table named as one of
27
+ -- delta's, never stands in for the real one. Before it is applied there is no
28
+ -- table: no id is new, and nothing moves.
29
+ CREATE OR REPLACE FUNCTION _delta_serial_past(p_table TEXT, p_id BIGINT)
30
+ RETURNS void LANGUAGE plpgsql SECURITY DEFINER AS $$ BEGIN NULL; END $$;
40
31
  CREATE OR REPLACE FUNCTION _delta_serial_new(p_table TEXT, p_id BIGINT)
41
- RETURNS BOOLEAN LANGUAGE plpgsql SECURITY DEFINER AS $$
42
- DECLARE
43
- v_new BOOLEAN;
44
- BEGIN
45
- IF NOT EXISTS (SELECT 1 FROM _delta_collections WHERE table_name = p_table)
46
- OR NOT has_table_privilege(COALESCE(NULLIF(current_setting('role'), 'none'), session_user), p_table, 'SELECT') THEN
47
- RETURN FALSE;
48
- END IF;
49
- EXECUTE format('SELECT $1 >= last_value + is_called::int FROM %I', 'seq_' || p_table) INTO v_new USING p_id;
50
- RETURN v_new;
51
- END;
52
- $$;
53
-
54
- -- Both run as the framework's owner, so each looks its names up only in the
55
- -- schemas the framework is applied in, and pg_temp last: a caller's temporary
56
- -- _delta_collections, or a temporary table named as one of delta's, never
57
- -- stands in for the real one. (search_path FROM CURRENT took "$user", public,
58
- -- and pg_temp is searched first when it is not named.)
32
+ RETURNS BOOLEAN LANGUAGE sql SECURITY DEFINER AS $$ SELECT FALSE; $$;
59
33
  DO $$
60
34
  DECLARE
61
35
  v_path TEXT := COALESCE((SELECT string_agg(quote_ident(s), ', ') FROM unnest(current_schemas(false)) AS s), 'public') || ', pg_temp';
@@ -75,85 +49,10 @@ END $$;
75
49
  -- ---------------------------------------------------------------------------
76
50
 
77
51
  DROP FUNCTION IF EXISTS _delta_cascade_remove(TEXT, BIGINT, TEXT[]);
78
- CREATE OR REPLACE FUNCTION _delta_cascade_remove(
79
- p_collection_key TEXT,
80
- p_id BIGINT
81
- ) RETURNS JSONB AS $$
82
- DECLARE
83
- v_coll RECORD;
84
- v_view TEXT;
85
- v_child RECORD;
86
- v_child_row RECORD;
87
- v_ref RECORD;
88
- v_ref_row RECORD;
89
- v_affected BIGINT;
90
- v_ops JSONB := '[]'::jsonb;
91
- BEGIN
92
- SELECT * INTO v_coll FROM _delta_collections
93
- WHERE collection_key = p_collection_key;
94
- IF NOT FOUND THEN RETURN v_ops; END IF;
95
-
96
- -- Close or delete the row itself
97
- IF v_coll.temporal THEN
98
- EXECUTE format(
99
- 'UPDATE %I SET valid_to = NOW() WHERE id = $1 AND valid_to IS NULL',
100
- v_coll.table_name
101
- ) USING p_id;
102
- ELSE
103
- EXECUTE format('DELETE FROM %I WHERE id = $1', v_coll.table_name)
104
- USING p_id;
105
- END IF;
106
-
107
- -- Only claim a removal that actually happened. The row may already be closed,
108
- -- or an RLS policy may have silently filtered the UPDATE/DELETE to zero rows —
109
- -- in which case emitting the op would tell every subscriber to drop a row that
110
- -- is still in the table, and cascading from it would compound the lie.
111
- GET DIAGNOSTICS v_affected = ROW_COUNT;
112
- IF v_affected = 0 THEN RETURN v_ops; END IF;
113
-
114
- v_ops := v_ops || jsonb_build_array(
115
- jsonb_build_object('op', 'remove', 'path', _delta_build_path(p_collection_key, p_id::text))
116
- );
117
-
118
- -- Cascade via parent relationship (children of this collection)
119
- FOR v_child IN
120
- SELECT * FROM _delta_collections
121
- WHERE parent_collection = p_collection_key
122
- ORDER BY collection_key COLLATE "C"
123
- LOOP
124
- v_view := _delta_source_view(v_child.table_name, v_child.temporal);
125
-
126
- FOR v_child_row IN
127
- EXECUTE format('SELECT id FROM %I WHERE %I = $1 ORDER BY id', v_view, v_child.parent_fk)
128
- USING p_id
129
- LOOP
130
- v_ops := v_ops || _delta_cascade_remove(v_child.collection_key, v_child_row.id);
131
- END LOOP;
132
- END LOOP;
133
-
134
- -- Cascade via cascadeOn (referencedBy)
135
- -- Find collections whose cascade_on references this collection
136
- FOR v_ref IN
137
- SELECT c.collection_key, c.table_name, c.temporal,
138
- elem->>'fk' AS fk_column
139
- FROM _delta_collections c,
140
- jsonb_array_elements(c.cascade_on) AS elem
141
- WHERE elem->>'collection' = p_collection_key
142
- ORDER BY c.collection_key COLLATE "C", elem->>'fk' COLLATE "C"
143
- LOOP
144
- v_view := _delta_source_view(v_ref.table_name, v_ref.temporal);
145
-
146
- FOR v_ref_row IN
147
- EXECUTE format('SELECT id FROM %I WHERE %I = $1 ORDER BY id', v_view, v_ref.fk_column)
148
- USING p_id
149
- LOOP
150
- v_ops := v_ops || _delta_cascade_remove(v_ref.collection_key, v_ref_row.id);
151
- END LOOP;
152
- END LOOP;
153
-
154
- RETURN v_ops;
155
- END;
156
- $$ LANGUAGE plpgsql;
52
+ -- generateSql writes one for each collection (compileCascades); a collection
53
+ -- there is not has nothing to take
54
+ CREATE OR REPLACE FUNCTION _delta_cascade_remove(p_collection_key TEXT, p_id BIGINT)
55
+ RETURNS JSONB LANGUAGE sql AS $$ SELECT '[]'::jsonb; $$;
157
56
 
158
57
  -- ---------------------------------------------------------------------------
159
58
  -- _delta_cascade_rows: the rows _delta_cascade_remove would take, read before
@@ -162,52 +61,8 @@ $$ LANGUAGE plpgsql;
162
61
  -- ---------------------------------------------------------------------------
163
62
 
164
63
  DROP FUNCTION IF EXISTS _delta_cascade_rows(TEXT, BIGINT, TEXT[]);
165
- CREATE OR REPLACE FUNCTION _delta_cascade_rows(
166
- p_collection_key TEXT,
167
- p_id BIGINT
168
- ) RETURNS JSONB AS $$
169
- DECLARE
170
- v_coll RECORD;
171
- v_view TEXT;
172
- v_row JSONB;
173
- v_child RECORD;
174
- v_ref RECORD;
175
- v_id BIGINT;
176
- v_rows JSONB := '[]'::jsonb;
177
- BEGIN
178
- SELECT * INTO v_coll FROM _delta_collections WHERE collection_key = p_collection_key;
179
- IF NOT FOUND THEN RETURN v_rows; END IF;
180
- v_view := _delta_source_view(v_coll.table_name, v_coll.temporal);
181
- EXECUTE format('SELECT to_jsonb(t.*) FROM %I t WHERE t.id = $1', v_view) INTO v_row USING p_id;
182
- IF v_row IS NULL THEN RETURN v_rows; END IF;
183
- v_rows := jsonb_build_array(jsonb_build_object('coll', p_collection_key, 'id', p_id, 'row', _delta_strip_temporal(v_row)));
184
-
185
- FOR v_child IN
186
- SELECT * FROM _delta_collections
187
- WHERE parent_collection = p_collection_key
188
- ORDER BY collection_key COLLATE "C"
189
- LOOP
190
- FOR v_id IN EXECUTE format('SELECT id FROM %I WHERE %I = $1 ORDER BY id', _delta_source_view(v_child.table_name, v_child.temporal), v_child.parent_fk) USING p_id
191
- LOOP
192
- v_rows := v_rows || _delta_cascade_rows(v_child.collection_key, v_id);
193
- END LOOP;
194
- END LOOP;
195
-
196
- FOR v_ref IN
197
- SELECT c.collection_key, c.table_name, c.temporal, elem->>'fk' AS fk_column
198
- FROM _delta_collections c, jsonb_array_elements(c.cascade_on) AS elem
199
- WHERE elem->>'collection' = p_collection_key
200
- ORDER BY c.collection_key COLLATE "C", elem->>'fk' COLLATE "C"
201
- LOOP
202
- FOR v_id IN EXECUTE format('SELECT id FROM %I WHERE %I = $1 ORDER BY id', _delta_source_view(v_ref.table_name, v_ref.temporal), v_ref.fk_column) USING p_id
203
- LOOP
204
- v_rows := v_rows || _delta_cascade_rows(v_ref.collection_key, v_id);
205
- END LOOP;
206
- END LOOP;
207
-
208
- RETURN v_rows;
209
- END;
210
- $$ LANGUAGE plpgsql STABLE;
64
+ CREATE OR REPLACE FUNCTION _delta_cascade_rows(p_collection_key TEXT, p_id BIGINT)
65
+ RETURNS JSONB LANGUAGE sql STABLE AS $$ SELECT '[]'::jsonb; $$;
211
66
 
212
67
  -- ---------------------------------------------------------------------------
213
68
  -- _delta_hanging_rows: the rows that hang from a row -- its children, theirs,
@@ -217,26 +72,7 @@ $$ LANGUAGE plpgsql STABLE;
217
72
  -- ---------------------------------------------------------------------------
218
73
 
219
74
  CREATE OR REPLACE FUNCTION _delta_hanging_rows(p_collection_key TEXT, p_id BIGINT)
220
- RETURNS JSONB AS $$
221
- DECLARE
222
- v_child RECORD;
223
- v_part JSONB;
224
- v_rows JSONB := '[]'::jsonb;
225
- BEGIN
226
- -- a child collection at a time, in one aggregate: each row by id, the rows under it after it
227
- FOR v_child IN SELECT * FROM _delta_collections WHERE parent_collection = p_collection_key ORDER BY collection_key COLLATE "C" LOOP
228
- EXECUTE format(
229
- 'SELECT COALESCE(jsonb_agg(e ORDER BY t.id, n), ''[]''::jsonb) FROM %I t,
230
- jsonb_array_elements(jsonb_build_array(jsonb_build_object(''coll'', %L, ''id'', t.id, ''row'', _delta_strip_temporal(to_jsonb(t.*))))
231
- || _delta_hanging_rows(%L, t.id)) WITH ORDINALITY AS x(e, n)
232
- WHERE t.%I = $1',
233
- _delta_source_view(v_child.table_name, v_child.temporal), v_child.collection_key, v_child.collection_key, v_child.parent_fk
234
- ) INTO v_part USING p_id;
235
- v_rows := v_rows || v_part;
236
- END LOOP;
237
- RETURN v_rows;
238
- END;
239
- $$ LANGUAGE plpgsql STABLE;
75
+ RETURNS JSONB LANGUAGE sql STABLE AS $$ SELECT '[]'::jsonb; $$;
240
76
 
241
77
  -- ---------------------------------------------------------------------------
242
78
  -- _delta_tell: tell every document that holds a row a write changed what
@@ -268,6 +104,14 @@ $$ LANGUAGE plpgsql STABLE;
268
104
  -- two-argument form logs none.
269
105
  -- ---------------------------------------------------------------------------
270
106
 
107
+ -- _delta_told: what one document is told of a write -- `p_rows`, the write's
108
+ -- touches with their holders, as _delta_tell gathers them -- by its name
109
+ -- `p_target`: an add, a replace of each field that changed, a remove; its
110
+ -- root's arrival or leaving as a replace. generateSql sends each document to
111
+ -- its own (compileTold).
112
+ CREATE OR REPLACE FUNCTION _delta_told(p_def _delta_docs, p_target TEXT, p_rows JSONB)
113
+ RETURNS JSONB LANGUAGE plpgsql STABLE AS $$ BEGIN RAISE EXCEPTION 'no doc def for: %', p_target USING ERRCODE = 'P0001'; END $$;
114
+
271
115
  CREATE OR REPLACE FUNCTION _delta_tell(p_writer TEXT, p_touched JSONB, p_applied JSONB)
272
116
  RETURNS BIGINT AS $$
273
117
  DECLARE
@@ -277,12 +121,7 @@ DECLARE
277
121
  v_targets TEXT[] := ARRAY[]::text[];
278
122
  v_target TEXT;
279
123
  v_def _delta_docs;
280
- v_single BOOLEAN;
281
124
  v_told JSONB;
282
- v_was BOOLEAN;
283
- v_is BOOLEAN;
284
- v_root BOOLEAN;
285
- v_path TEXT;
286
125
  v_version BIGINT;
287
126
  v_next JSONB := '{}'::jsonb; -- a row's holders, as its next touch found them
288
127
  v_key TEXT;
@@ -309,29 +148,7 @@ BEGIN
309
148
  LOOP
310
149
  v_def := _delta_find_doc(v_target);
311
150
  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;
151
+ v_told := _delta_told(v_def, v_target, v_rows);
335
152
  IF v_target = p_writer THEN
336
153
  v_version := _delta_bump_and_notify(p_writer, v_told, CASE WHEN p_applied IS DISTINCT FROM v_told THEN p_applied END);
337
154
  ELSIF jsonb_array_length(v_told) > 0 THEN
@@ -353,19 +170,10 @@ RETURNS BIGINT LANGUAGE sql AS $$ SELECT _delta_tell(p_writer, p_touched, NULL);
353
170
  -- not yet written opens empty). `there` in sqlite.ts.
354
171
  -- ---------------------------------------------------------------------------
355
172
 
173
+ -- generateSql writes one for each collection (compileRows); a collection there
174
+ -- is not has no row there
356
175
  CREATE OR REPLACE FUNCTION _delta_row_there(p_collection_key TEXT, p_id BIGINT)
357
- RETURNS BOOLEAN AS $$
358
- DECLARE
359
- v_coll RECORD;
360
- v_exists BOOLEAN;
361
- BEGIN
362
- SELECT * INTO v_coll FROM _delta_collections WHERE collection_key = p_collection_key;
363
- IF NOT FOUND OR p_id IS NULL THEN RETURN FALSE; END IF;
364
- EXECUTE format('SELECT EXISTS (SELECT 1 FROM %I WHERE id = $1)', _delta_source_view(v_coll.table_name, v_coll.temporal))
365
- INTO v_exists USING p_id;
366
- RETURN v_exists;
367
- END;
368
- $$ LANGUAGE plpgsql STABLE;
176
+ RETURNS BOOLEAN LANGUAGE sql STABLE AS $$ SELECT FALSE; $$;
369
177
 
370
178
  -- ---------------------------------------------------------------------------
371
179
  -- _delta_assert_parent_held: a parent key written (in `p_value`, over the row
@@ -391,6 +199,63 @@ BEGIN
391
199
  END;
392
200
  $$ LANGUAGE plpgsql STABLE;
393
201
 
202
+ -- _delta_root_there: is a single document's root row `p_id` there now, and
203
+ -- does it meet the whole of its name's scope (a column beside its id), as
204
+ -- delta_open reads it? Asked before each op of a write (delta_apply).
205
+ -- generateSql sends each document to its own (compileRootThere).
206
+ CREATE OR REPLACE FUNCTION _delta_root_there(p_def _delta_docs, p_doc_name TEXT, p_scope JSONB, p_view TEXT, p_id BIGINT)
207
+ RETURNS BOOLEAN LANGUAGE plpgsql STABLE AS $$ BEGIN RAISE EXCEPTION 'no doc def for: %', p_doc_name USING ERRCODE = 'P0001'; END $$;
208
+
209
+ -- _delta_write_columns: the columns of `p_coll` a document writes -- as the
210
+ -- process writing declares them (_delta_cols), those the document holds
211
+ -- (`held`, todo #70), else all: a write through it names those alone
212
+ -- (_delta_assert_fields: 400 for another), and replacing a row whole sets those
213
+ -- alone. generateSql sends each document to its own (compileColumns).
214
+ CREATE OR REPLACE FUNCTION _delta_write_columns(p_def _delta_docs, p_coll TEXT, p_columns_def JSONB)
215
+ RETURNS JSONB LANGUAGE plpgsql STABLE AS $$ BEGIN RAISE EXCEPTION 'no doc def for: %', p_def.prefix USING ERRCODE = 'P0001'; END $$;
216
+
217
+ -- ---------------------------------------------------------------------------
218
+ -- A row's SQL, as delta_apply runs it: generateSql writes each for each
219
+ -- collection (compileRows).
220
+ -- _delta_row_lock: a row as it stands, locked for the write (NULL: not there)
221
+ -- _delta_row_put: a replace's row stored -- a temporal one a new version (one
222
+ -- this write made gives way to it; the standing one closes at `p_ts`), else
223
+ -- the row again -- answered as stored
224
+ -- _delta_next_id: an add's id, minted
225
+ -- _delta_row_clear_moment: a temporal version this write made and took back
226
+ -- (it never was outside the write), so an add of its id takes its place
227
+ -- _delta_row_add: an add's row stored, answered as stored
228
+ -- _delta_row_read: a row as it stands, without its validity (NULL: not there)
229
+ -- (_delta_row_there, above, is an add's "is the id there now".)
230
+ -- ---------------------------------------------------------------------------
231
+
232
+ CREATE OR REPLACE FUNCTION _delta_row_lock(p_coll TEXT, p_table TEXT, p_temporal BOOLEAN, p_id BIGINT)
233
+ RETURNS JSONB LANGUAGE plpgsql AS $$ BEGIN RAISE EXCEPTION 'unknown collection: %', p_coll USING ERRCODE = '22023'; END $$;
234
+ 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)
235
+ RETURNS JSONB LANGUAGE plpgsql AS $$ BEGIN RAISE EXCEPTION 'unknown collection: %', p_coll USING ERRCODE = '22023'; END $$;
236
+ CREATE OR REPLACE FUNCTION _delta_next_id(p_coll TEXT, p_table TEXT)
237
+ RETURNS BIGINT LANGUAGE plpgsql AS $$ BEGIN RAISE EXCEPTION 'unknown collection: %', p_coll USING ERRCODE = '22023'; END $$;
238
+ CREATE OR REPLACE FUNCTION _delta_row_clear_moment(p_coll TEXT, p_table TEXT, p_id BIGINT, p_ts TIMESTAMPTZ)
239
+ RETURNS void LANGUAGE plpgsql AS $$ BEGIN RAISE EXCEPTION 'unknown collection: %', p_coll USING ERRCODE = '22023'; END $$;
240
+ CREATE OR REPLACE FUNCTION _delta_row_add(p_coll TEXT, p_table TEXT, p_row JSONB)
241
+ RETURNS JSONB LANGUAGE plpgsql AS $$ BEGIN RAISE EXCEPTION 'unknown collection: %', p_coll USING ERRCODE = '22023'; END $$;
242
+ CREATE OR REPLACE FUNCTION _delta_row_read(p_coll TEXT, p_view TEXT, p_id BIGINT)
243
+ RETURNS JSONB LANGUAGE plpgsql STABLE AS $$ BEGIN RAISE EXCEPTION 'unknown collection: %', p_coll USING ERRCODE = '22023'; END $$;
244
+
245
+ -- _delta_row_add_given: a row stored by the columns it gives (_delta_insert_sql),
246
+ -- answered as stored. A compiled add whose row gives some other set of keys
247
+ -- than its collection's every column (a walk's row, as stored; a column a
248
+ -- migration added, left to its default) stores it so.
249
+ CREATE OR REPLACE FUNCTION _delta_row_add_given(p_table TEXT, p_row JSONB)
250
+ RETURNS JSONB AS $$
251
+ DECLARE
252
+ v_row JSONB;
253
+ BEGIN
254
+ EXECUTE _delta_insert_sql(p_table, p_row) || ' RETURNING to_jsonb(t.*)' INTO v_row USING p_row;
255
+ RETURN v_row;
256
+ END;
257
+ $$ LANGUAGE plpgsql;
258
+
394
259
  -- ---------------------------------------------------------------------------
395
260
  -- _delta_whole: a replace of a row itself (`/<coll>/<id>`, a single
396
261
  -- document's `/<root>`) is the row, as RFC 6902 says: each column
@@ -402,18 +267,6 @@ $$ LANGUAGE plpgsql STABLE;
402
267
  -- and asked with the value's fields, before whether the row is there (todo
403
268
  -- #3; replaceRow in sqlite.ts).
404
269
  -- ---------------------------------------------------------------------------
405
-
406
- -- _delta_held_columns: the columns of `p_coll` (`p_columns`, its columns_def)
407
- -- a document holds -- those it names (`held`, todo #70), else all. A write
408
- -- through it names those alone (_delta_assert_fields: 400 for another, as for
409
- -- a column there is not), and replacing a row whole sets those alone.
410
-
411
- CREATE OR REPLACE FUNCTION _delta_held_columns(p_def _delta_docs, p_coll TEXT, p_columns JSONB)
412
- RETURNS JSONB AS $$
413
- SELECT CASE WHEN NOT COALESCE(p_def.held, '{}'::jsonb) ? p_coll THEN p_columns
414
- 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
- $$ LANGUAGE sql STABLE;
416
-
417
270
  CREATE OR REPLACE FUNCTION _delta_whole(p_columns JSONB, p_value JSONB)
418
271
  RETURNS JSONB AS $$
419
272
  DECLARE
@@ -456,11 +309,11 @@ DECLARE
456
309
  v_scope JSONB;
457
310
  v_is_list BOOLEAN;
458
311
  v_doc_id BIGINT;
459
- v_root_coll RECORD;
312
+ v_root_coll _delta_collections;
460
313
  v_op JSONB;
461
314
  v_parts TEXT[];
462
315
  v_coll_key TEXT;
463
- v_coll RECORD;
316
+ v_coll _delta_collections;
464
317
  v_view TEXT;
465
318
  v_root_view TEXT;
466
319
  v_opened BOOLEAN; -- a single document's root was there as the write began
@@ -472,7 +325,7 @@ DECLARE
472
325
  v_row JSONB;
473
326
  v_new_row JSONB;
474
327
  v_blank JSONB; -- a replace of the row whole: each column as an add leaves it (_delta_whole); of a field, nothing
475
- v_cols JSONB; -- the op's collection's columns the document holds (_delta_held_columns); a walk's, all
328
+ v_cols JSONB; -- the op's collection's columns the document holds (_delta_write_columns); a walk's, all
476
329
  v_exists BOOLEAN;
477
330
  v_missing TEXT;
478
331
  v_ts TIMESTAMPTZ := NOW();
@@ -515,8 +368,7 @@ BEGIN
515
368
  v_doc_id := (v_scope->'values'->>'id')::BIGINT;
516
369
  END IF;
517
370
 
518
- SELECT * INTO v_root_coll FROM _delta_collections
519
- WHERE collection_key = v_def.root_collection;
371
+ v_root_coll := _delta_collection(v_def.root_collection);
520
372
  v_root_view := _delta_source_view(v_root_coll.table_name, v_root_coll.temporal);
521
373
 
522
374
  -- an implied document's first write makes its root row (todo #34), told as a
@@ -524,11 +376,11 @@ BEGIN
524
376
  -- documents, which showed it already (ensureImpliedRoot in sqlite.ts); an
525
377
  -- empty write makes nothing
526
378
  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;
379
+ v_exists := _delta_row_there(v_def.root_collection, v_doc_id);
528
380
  PERFORM _delta_make_implied(v_def, v_doc_id, p_ops, v_ts);
529
381
  IF NOT v_exists THEN
530
382
  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;
383
+ v_row := _delta_row_read(v_def.root_collection, v_root_view, v_doc_id);
532
384
  IF v_row IS NOT NULL THEN
533
385
  v_touched := jsonb_build_array(jsonb_build_object('coll', v_def.root_collection, 'id', v_doc_id, 'after', v_row, 'carried', TRUE,
534
386
  'before', COALESCE((SELECT jsonb_agg(n) FROM unnest(_delta_holders(v_def.root_collection, v_row, p_doc_name)) AS n
@@ -541,7 +393,7 @@ BEGIN
541
393
  -- An empty write is asked what each op is (below): a single document whose
542
394
  -- root is not there is not found, as on SQLite -- unless implied, or a walk.
543
395
  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;
396
+ v_exists := _delta_root_there(v_def, p_doc_name, v_scope, v_root_view, v_doc_id);
545
397
  IF NOT v_exists THEN
546
398
  RAISE EXCEPTION 'document not found: % (its root, %, is not there)',
547
399
  p_doc_name, _delta_build_path(v_def.root_collection, v_doc_id::text)
@@ -565,7 +417,7 @@ BEGIN
565
417
  -- (_delta_walk_plan), as SQLite's and ./json's are.
566
418
  IF NOT v_is_list AND NOT COALESCE(p_walk, FALSE) THEN -- a walk not said (null) is none
567
419
  -- 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;
420
+ v_exists := _delta_root_there(v_def, p_doc_name, v_scope, v_root_view, v_doc_id);
569
421
  v_opened := COALESCE(v_opened, v_exists OR COALESCE(v_def.implied, FALSE));
570
422
  v_back := FALSE;
571
423
  IF NOT v_exists AND v_opened AND v_op->>'op' = 'add' THEN
@@ -583,9 +435,8 @@ BEGIN
583
435
  v_parts := _delta_split_path(v_op->>'path');
584
436
  v_coll_key := v_parts[1];
585
437
 
586
- SELECT * INTO v_coll FROM _delta_collections
587
- WHERE collection_key = v_coll_key;
588
- IF NOT FOUND THEN
438
+ v_coll := _delta_collection(v_coll_key);
439
+ IF v_coll.collection_key IS NULL THEN
589
440
  RAISE EXCEPTION 'unknown collection: %', v_coll_key USING ERRCODE = '22023';
590
441
  END IF;
591
442
  -- a walk puts back what the ledger recorded, whole
@@ -594,7 +445,7 @@ BEGIN
594
445
  THEN COALESCE((SELECT jsonb_object_agg(c, '{"nullable": true}'::jsonb) FROM unnest(_delta_stored_columns(v_coll.table_name)) AS c
595
446
  WHERE c NOT IN ('id', 'valid_from', 'valid_to') AND c IS DISTINCT FROM v_coll.parent_fk), '{}'::jsonb)
596
447
  || _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;
448
+ ELSE _delta_write_columns(v_def, v_coll_key, v_coll.columns_def) END;
598
449
 
599
450
  -- Scope guard: an op may only target the doc's root or an included
600
451
  -- collection. Defence-in-depth — a client that opened doc A must not write
@@ -637,17 +488,7 @@ BEGIN
637
488
  v_blank := CASE WHEN array_length(v_parts, 1) = 1 THEN _delta_whole(v_cols, v_op->'value') ELSE '{}'::jsonb END;
638
489
 
639
490
  -- 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;
491
+ v_row := _delta_row_lock(v_coll_key, v_coll.table_name, v_coll.temporal, v_doc_id); -- read, and lock
651
492
 
652
493
  IF v_row IS NULL THEN
653
494
  RAISE EXCEPTION 'root row not found: %', v_def.root_collection USING ERRCODE = 'P0002';
@@ -660,34 +501,8 @@ BEGIN
660
501
  -- is the path's (it renumbered the row).
661
502
  v_new_row := v_row || v_blank || (v_op->'value') || jsonb_build_object('id', v_doc_id);
662
503
 
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;
504
+ 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
505
+ IF v_coll.temporal THEN v_new_row := _delta_strip_temporal(v_new_row); END IF;
691
506
 
692
507
  -- The root written through its document stays in its scope, as a list's
693
508
  -- rows do (below): one written out of the rest of its name's scope -- a
@@ -737,7 +552,7 @@ BEGIN
737
552
  v_id_text := v_parts[2];
738
553
  -- Auto-generate ID from sequence if path ends with '-'
739
554
  IF v_id_text = '-' THEN
740
- EXECUTE format('SELECT nextval(%L)', 'seq_' || v_coll.table_name) INTO v_id;
555
+ v_id := _delta_next_id(v_coll_key, v_coll.table_name);
741
556
  ELSE
742
557
  v_id := _delta_row_id(v_coll_key, v_id_text);
743
558
  END IF;
@@ -767,7 +582,7 @@ BEGIN
767
582
  v_exists := NOT _delta_serial_new(v_coll.table_name, v_id);
768
583
  END IF;
769
584
  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;
585
+ v_exists := _delta_row_there(v_coll_key, v_id);
771
586
  END IF;
772
587
  IF v_exists THEN
773
588
  RAISE EXCEPTION 'id already taken: % -- add to % for a new id',
@@ -847,10 +662,7 @@ BEGIN
847
662
  -- valid_from and valid_to both the write's NOW()) never was outside it,
848
663
  -- so an add of the same id takes its place rather than colliding with it
849
664
  -- 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;
665
+ PERFORM _delta_row_clear_moment(v_coll_key, v_coll.table_name, v_id, v_ts);
854
666
  v_new_row := v_new_row || jsonb_build_object('valid_from', v_ts, 'valid_to', NULL);
855
667
  END IF;
856
668
 
@@ -858,7 +670,7 @@ BEGIN
858
670
  -- value its column casts (a scope's '1' into an integer, a date into a
859
671
  -- timestamptz) is told as a later open reads it, so the broadcast, the
860
672
  -- 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;
673
+ v_new_row := _delta_row_add(v_coll_key, v_coll.table_name, v_new_row);
862
674
 
863
675
  -- An id the add named moves the serial past it, so a later /- does not meet it.
864
676
  IF v_id_text <> '-' AND v_id > COALESCE((v_passed->>v_coll.table_name)::BIGINT, 0) THEN
@@ -955,17 +767,7 @@ BEGIN
955
767
  USING ERRCODE = 'P0002';
956
768
  END IF;
957
769
 
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;
770
+ v_row := _delta_row_lock(v_coll_key, v_coll.table_name, v_coll.temporal, v_id); -- read, and lock
969
771
 
970
772
  IF v_row IS NULL THEN
971
773
  RAISE EXCEPTION 'row not found: %/%', v_coll_key, v_id USING ERRCODE = 'P0002';
@@ -1000,35 +802,7 @@ BEGIN
1000
802
  -- an add's is (it renumbered the row).
1001
803
  v_new_row := v_row || v_blank || (v_op->'value') || jsonb_build_object('id', v_id);
1002
804
 
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;
805
+ 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
806
 
1033
807
  -- A list's root row written through the list stays in its scope, as an
1034
808
  -- add through it is given its bindings: a field, a row or a merge that