dzql 0.7.0 → 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.
- package/README.md +16 -0
- package/package.json +11 -4
- package/src/postgres/auth-jwt-sql.ts +32 -0
- package/src/postgres/auth-jwt.ts +209 -0
- package/src/postgres/auth.ts +50 -0
- package/src/postgres/bootstrap.ts +55 -0
- package/src/postgres/carry.ts +40 -0
- package/src/postgres/codegen.ts +172 -0
- package/src/postgres/index.ts +58 -0
- package/src/postgres/listener.ts +706 -0
- package/src/postgres/migrate.ts +143 -0
- package/src/postgres/pglite.ts +142 -0
- package/src/postgres/registry.ts +253 -0
- package/src/postgres/schema.ts +171 -0
- package/src/postgres/sql.ts +42 -0
- package/src/sql/001a-delta-meta.sql +204 -0
- package/src/sql/001b-delta-scope.sql +619 -0
- package/src/sql/001c-delta-read.sql +506 -0
- package/src/sql/001d-delta-write.sql +1088 -0
- package/src/sql/001e-delta-ops.sql +142 -0
- package/src/sql/001f-delta-as.sql +41 -0
- package/src/sql/001g-delta-ledger.sql +564 -0
- 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; $$;
|