dzql 0.9.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.
- package/README.md +29 -13
- package/package.json +2 -2
- package/src/compile/cascades.ts +151 -0
- package/src/compile/columns.ts +144 -0
- package/src/compile/holders.ts +72 -0
- package/src/compile/holds.ts +91 -0
- package/src/compile/ledger.ts +73 -0
- package/src/compile/opens.ts +63 -23
- package/src/compile/resolve.ts +75 -0
- package/src/compile/root.ts +61 -0
- package/src/compile/rows.ts +122 -0
- package/src/compile/scope.ts +87 -0
- package/src/compile/told.ts +94 -0
- package/src/postgres/auth-jwt-sql.ts +1 -1
- package/src/postgres/auth-jwt.ts +2 -2
- package/src/postgres/bootstrap.ts +2 -2
- package/src/postgres/codegen.ts +12 -2
- package/src/postgres/index.ts +2 -2
- package/src/postgres/pglite.ts +1 -1
- package/src/sql/001a-delta-meta.sql +8 -1
- package/src/sql/001b-delta-scope.sql +32 -5
- package/src/sql/001c-delta-read.sql +10 -11
- package/src/sql/001d-delta-write.sql +212 -123
- package/src/sql/001g-delta-ledger.sql +67 -23
- package/src/sql/auth-jwt.sql +1 -1
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* These are split out of `auth-jwt.ts` so the `delta init` CLI (which only
|
|
5
5
|
* needs the file path) can run in projects that haven't installed jose.
|
|
6
|
-
* Consumers of the
|
|
6
|
+
* Consumers of the `dzql/auth-jwt` subpath still get these via
|
|
7
7
|
* re-export from `auth-jwt.ts`.
|
|
8
8
|
*/
|
|
9
9
|
import type { Pool } from "pg";
|
package/src/postgres/auth-jwt.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Auth-JWT — reference `DeltaAuth` implementation using JWTs (jose) and
|
|
3
3
|
* Postgres stored functions for credential verification.
|
|
4
4
|
*
|
|
5
|
-
* Ship this as an opt-in subpath:
|
|
5
|
+
* Ship this as an opt-in subpath: `dzql/auth-jwt`. Users who want
|
|
6
6
|
* a different auth scheme (sessions, OAuth, magic links, none) implement
|
|
7
7
|
* their own `DeltaAuth` instead.
|
|
8
8
|
*
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*
|
|
14
14
|
* Both are overridable via the `loginSql` / `registerSql` options.
|
|
15
15
|
*
|
|
16
|
-
* import { jwtAuth } from "
|
|
16
|
+
* import { jwtAuth } from "dzql/auth-jwt";
|
|
17
17
|
*
|
|
18
18
|
* const auth = jwtAuth({
|
|
19
19
|
* pool,
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* consumer's `init_db/` directory.
|
|
5
5
|
*
|
|
6
6
|
* import { Pool } from "pg";
|
|
7
|
-
* import { applyFramework } from "
|
|
7
|
+
* import { applyFramework } from "dzql/postgres";
|
|
8
8
|
*
|
|
9
9
|
* const pool = new Pool({ connectionString: process.env.PG_URL });
|
|
10
10
|
* await applyFramework(pool);
|
|
@@ -23,7 +23,7 @@ const FRAMEWORK_SQL_DIR = join(import.meta.dir, "..", "sql");
|
|
|
23
23
|
|
|
24
24
|
/** Absolute paths to the framework SQL files, in apply-order.
|
|
25
25
|
* Filters to `001*-*.sql` — siblings like `auth-jwt.sql` are applied
|
|
26
|
-
* separately (see
|
|
26
|
+
* separately (see `dzql/auth-jwt` → `applyAuthJwtSchema`). */
|
|
27
27
|
export function frameworkSqlFiles(): string[] {
|
|
28
28
|
return readdirSync(FRAMEWORK_SQL_DIR)
|
|
29
29
|
.filter((f) => /^001[a-z]?-.+\.sql$/.test(f))
|
package/src/postgres/codegen.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*
|
|
15
15
|
* Drive it from your project's `types.ts`:
|
|
16
16
|
*
|
|
17
|
-
* import { generateSql } from "
|
|
17
|
+
* import { generateSql } from "dzql/postgres";
|
|
18
18
|
* import { schema, docs } from "./types";
|
|
19
19
|
* await Bun.write("init_db/003-tables.sql", generateSql(schema, docs));
|
|
20
20
|
*
|
|
@@ -27,6 +27,16 @@ import { lit, columnSqlType, sqlDefault, findCascadeOn } from "./sql";
|
|
|
27
27
|
import { defaultForType, q } from "@blueshed/delta/backend";
|
|
28
28
|
import { assertScopeKeys } from "@blueshed/delta/backend";
|
|
29
29
|
import { compileOpens } from "../compile/opens";
|
|
30
|
+
import { compileHolds } from "../compile/holds";
|
|
31
|
+
import { compileInScope } from "../compile/scope";
|
|
32
|
+
import { compileHolders } from "../compile/holders";
|
|
33
|
+
import { compileResolve } from "../compile/resolve";
|
|
34
|
+
import { compileRootThere } from "../compile/root";
|
|
35
|
+
import { compileColumns } from "../compile/columns";
|
|
36
|
+
import { compileRows } from "../compile/rows";
|
|
37
|
+
import { compileCascades } from "../compile/cascades";
|
|
38
|
+
import { compileLedger } from "../compile/ledger";
|
|
39
|
+
import { compileTold } from "../compile/told";
|
|
30
40
|
|
|
31
41
|
export interface GenerateSqlOptions {
|
|
32
42
|
/** Header comment included at the top of the file. */
|
|
@@ -170,6 +180,6 @@ export function generateSql(
|
|
|
170
180
|
lines.push("");
|
|
171
181
|
|
|
172
182
|
// each document opened by SQL compiled for it (dzql), which docTypeFromDef calls
|
|
173
|
-
lines.push("", compileOpens(schema, docs));
|
|
183
|
+
lines.push("", compileOpens(schema, docs), "", compileHolds(schema, docs), "", compileInScope(schema, docs), "", compileTold(schema, docs), "", compileHolders(schema, docs), "", compileResolve(schema, docs), "", compileRootThere(schema, docs), "", compileColumns(schema, docs), "", compileRows(schema), "", compileCascades(schema), "", compileLedger(schema, docs));
|
|
174
184
|
return lines.join("\n");
|
|
175
185
|
}
|
package/src/postgres/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* dzql/postgres — delta's Postgres backend, compiled from its definitions.
|
|
3
3
|
*
|
|
4
4
|
* Schema + stored functions + LISTEN dispatch. Tables are generated from
|
|
5
5
|
* a TypeScript schema definition; all mutations go through stored functions
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* import {
|
|
10
10
|
* defineSchema, defineDoc,
|
|
11
11
|
* createDocListener, docTypeFromDef,
|
|
12
|
-
* } from "
|
|
12
|
+
* } from "dzql/postgres";
|
|
13
13
|
*
|
|
14
14
|
* The stored functions are `src/sql/001*.sql`: `applyFramework(pool)` applies
|
|
15
15
|
* them, or `bunx @blueshed/delta init <dir>` copies them for a migration tool
|
package/src/postgres/pglite.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* The same stored functions, the same listener, the same documents as a
|
|
5
5
|
* Postgres server, on PGlite (Postgres compiled to WASM): no database to run,
|
|
6
6
|
* the data in a directory (or in memory). `openPglite()` answers the slice of
|
|
7
|
-
* `pg`'s Pool delta uses, so everything in
|
|
7
|
+
* `pg`'s Pool delta uses, so everything in `dzql/postgres` runs on
|
|
8
8
|
* it unchanged:
|
|
9
9
|
*
|
|
10
10
|
* const pool = await openPglite("./data"); // or openPglite() in memory
|
|
@@ -136,7 +136,7 @@ $$;
|
|
|
136
136
|
-- It used to be merged into the broadcast row, acked, and dropped (or cast) by
|
|
137
137
|
-- jsonb_populate_record: stored otherwise than told. id, the parent key and
|
|
138
138
|
-- the temporal columns are the row's own.
|
|
139
|
-
CREATE OR REPLACE FUNCTION
|
|
139
|
+
CREATE OR REPLACE FUNCTION _delta_assert_fields_framework(
|
|
140
140
|
p_collection TEXT, p_columns JSONB, p_parent_fk TEXT, p_value JSONB
|
|
141
141
|
) RETURNS void LANGUAGE plpgsql IMMUTABLE AS $$
|
|
142
142
|
DECLARE
|
|
@@ -178,6 +178,13 @@ BEGIN
|
|
|
178
178
|
END;
|
|
179
179
|
$$;
|
|
180
180
|
|
|
181
|
+
-- The checks every write asks: the framework's, until dzql's compiler
|
|
182
|
+
-- (`compileColumns`) replaces this with one that checks each column set it
|
|
183
|
+
-- compiled by its own statements, and any other by the framework's.
|
|
184
|
+
CREATE OR REPLACE FUNCTION _delta_assert_fields(
|
|
185
|
+
p_collection TEXT, p_columns JSONB, p_parent_fk TEXT, p_value JSONB
|
|
186
|
+
) RETURNS void LANGUAGE sql IMMUTABLE AS $$ SELECT _delta_assert_fields_framework(p_collection, p_columns, p_parent_fk, p_value); $$;
|
|
187
|
+
|
|
181
188
|
-- Pick the source relation for a collection.
|
|
182
189
|
-- temporal + no timestamp → current_<table> view
|
|
183
190
|
-- temporal + timestamp → base <table> (caller filters with _delta_temporal_where)
|
|
@@ -51,7 +51,7 @@ $$ LANGUAGE sql STABLE;
|
|
|
51
51
|
-- how the fan-out reads a name opened before a rule refused it (_delta_doc_holds).
|
|
52
52
|
-- ---------------------------------------------------------------------------
|
|
53
53
|
|
|
54
|
-
CREATE OR REPLACE FUNCTION
|
|
54
|
+
CREATE OR REPLACE FUNCTION _delta_resolve_scope_framework(p_def _delta_docs, p_doc_name TEXT, p_refuse BOOLEAN)
|
|
55
55
|
RETURNS JSONB AS $$
|
|
56
56
|
DECLARE
|
|
57
57
|
v_doc_id TEXT;
|
|
@@ -285,6 +285,12 @@ BEGIN
|
|
|
285
285
|
END;
|
|
286
286
|
$$ LANGUAGE plpgsql STABLE;
|
|
287
287
|
|
|
288
|
+
-- The resolver every caller asks: the framework's, until dzql's compiler
|
|
289
|
+
-- (`compileResolve`) replaces it with one sending each document it compiled
|
|
290
|
+
-- to its own.
|
|
291
|
+
CREATE OR REPLACE FUNCTION _delta_resolve_scope(p_def _delta_docs, p_doc_name TEXT, p_refuse BOOLEAN)
|
|
292
|
+
RETURNS JSONB LANGUAGE sql STABLE AS $$ SELECT _delta_resolve_scope_framework(p_def, p_doc_name, p_refuse); $$;
|
|
293
|
+
|
|
288
294
|
-- The resolver as every caller but the fan-out asks it: a value refused is raised.
|
|
289
295
|
CREATE OR REPLACE FUNCTION _delta_resolve_scope(p_def _delta_docs, p_doc_name TEXT)
|
|
290
296
|
RETURNS JSONB LANGUAGE sql STABLE AS $$
|
|
@@ -323,7 +329,7 @@ $$;
|
|
|
323
329
|
-- so the gate can't be used to probe for ids.
|
|
324
330
|
-- ---------------------------------------------------------------------------
|
|
325
331
|
|
|
326
|
-
CREATE OR REPLACE FUNCTION
|
|
332
|
+
CREATE OR REPLACE FUNCTION _delta_row_in_scope_framework(
|
|
327
333
|
p_def _delta_docs,
|
|
328
334
|
p_doc_name TEXT,
|
|
329
335
|
p_collection_key TEXT,
|
|
@@ -413,6 +419,12 @@ BEGIN
|
|
|
413
419
|
END;
|
|
414
420
|
$$ LANGUAGE plpgsql STABLE;
|
|
415
421
|
|
|
422
|
+
-- _delta_row_in_scope: as the framework reads a definition (above), until dzql's generateSql
|
|
423
|
+
-- compiles the app's documents: it then sends each to its own dzql_in_scope__<prefix>, and any
|
|
424
|
+
-- other (registered by hand, or by another generateSql) here.
|
|
425
|
+
CREATE OR REPLACE FUNCTION _delta_row_in_scope(p_def _delta_docs, p_doc_name TEXT, p_collection_key TEXT, p_id BIGINT)
|
|
426
|
+
RETURNS BOOLEAN LANGUAGE sql STABLE AS $$ SELECT _delta_row_in_scope_framework(p_def, p_doc_name, p_collection_key, p_id); $$;
|
|
427
|
+
|
|
416
428
|
-- ---------------------------------------------------------------------------
|
|
417
429
|
-- Who holds a row: the fan-out (todo #28)
|
|
418
430
|
--
|
|
@@ -459,7 +471,7 @@ $$ LANGUAGE plpgsql STABLE;
|
|
|
459
471
|
-- _delta_doc_holds: does the document `p_doc_name` (of `p_def`) hold `p_row`
|
|
460
472
|
-- of `p_coll`? Judged on the row's values, so a row as it was before a write
|
|
461
473
|
-- is judged as well as one as it is after -- the same rule delta_open reads by.
|
|
462
|
-
CREATE OR REPLACE FUNCTION
|
|
474
|
+
CREATE OR REPLACE FUNCTION _delta_doc_holds_framework(p_def _delta_docs, p_doc_name TEXT, p_coll TEXT, p_row JSONB)
|
|
463
475
|
RETURNS BOOLEAN AS $$
|
|
464
476
|
DECLARE
|
|
465
477
|
v_scope JSONB;
|
|
@@ -503,6 +515,12 @@ BEGIN
|
|
|
503
515
|
END;
|
|
504
516
|
$$ LANGUAGE plpgsql STABLE;
|
|
505
517
|
|
|
518
|
+
-- _delta_doc_holds: as the framework reads a definition (above), until dzql's generateSql
|
|
519
|
+
-- compiles the app's documents: it then sends each to its own dzql_holds__<prefix>, and any
|
|
520
|
+
-- other (registered by hand, or by another generateSql) here.
|
|
521
|
+
CREATE OR REPLACE FUNCTION _delta_doc_holds(p_def _delta_docs, p_doc_name TEXT, p_coll TEXT, p_row JSONB)
|
|
522
|
+
RETURNS BOOLEAN LANGUAGE sql STABLE AS $$ SELECT _delta_doc_holds_framework(p_def, p_doc_name, p_coll, p_row); $$;
|
|
523
|
+
|
|
506
524
|
-- _delta_holders: the documents that hold `p_row` of `p_coll` -- every one
|
|
507
525
|
-- open now (a name in _delta_held, todo #73), and the writer's own `p_writer`.
|
|
508
526
|
-- A single-mode document of an unscoped definition is found by its name,
|
|
@@ -527,7 +545,7 @@ $$ LANGUAGE plpgsql STABLE;
|
|
|
527
545
|
-- that applied the framework, owning the tables without FORCE ROW LEVEL
|
|
528
546
|
-- SECURITY, or with BYPASSRLS; applied as the app's own role, it sees what
|
|
529
547
|
-- the writer sees, as before.
|
|
530
|
-
CREATE OR REPLACE FUNCTION
|
|
548
|
+
CREATE OR REPLACE FUNCTION _delta_holders_framework(p_coll TEXT, p_row JSONB, p_writer TEXT, p_skip TEXT[])
|
|
531
549
|
RETURNS TEXT[] AS $$
|
|
532
550
|
DECLARE
|
|
533
551
|
v_def _delta_docs;
|
|
@@ -547,7 +565,7 @@ BEGIN
|
|
|
547
565
|
|
|
548
566
|
FOR v_def IN
|
|
549
567
|
SELECT * FROM _delta_docs
|
|
550
|
-
WHERE root_collection = p_coll OR p_coll = ANY(include)
|
|
568
|
+
WHERE (root_collection = p_coll OR p_coll = ANY(include)) AND NOT (prefix = ANY(p_skip))
|
|
551
569
|
LOOP
|
|
552
570
|
v_all := v_def.scope <> '{}'::jsonb
|
|
553
571
|
OR (p_coll IS DISTINCT FROM v_def.root_collection AND v_coll.parent_collection IS NULL);
|
|
@@ -572,6 +590,15 @@ END;
|
|
|
572
590
|
$$ LANGUAGE plpgsql VOLATILE SECURITY DEFINER;
|
|
573
591
|
-- the search path it was applied with, and pg_temp last: a caller's temporary
|
|
574
592
|
-- tables never stand in for delta's own (pg_temp is searched first unless named)
|
|
593
|
+
DO $$ BEGIN
|
|
594
|
+
EXECUTE format('ALTER FUNCTION _delta_holders_framework(TEXT, JSONB, TEXT, TEXT[]) SET search_path = %s, pg_temp', COALESCE(NULLIF(current_setting('search_path'), ''), 'public'));
|
|
595
|
+
END $$;
|
|
596
|
+
|
|
597
|
+
-- `p_skip`: the prefixes asked already, by the compiled `_delta_holders`
|
|
598
|
+
-- (dzql's `compileHolders`), which replaces this one: each document it
|
|
599
|
+
-- compiled asked by its own lookups, the rest by the framework's.
|
|
600
|
+
CREATE OR REPLACE FUNCTION _delta_holders(p_coll TEXT, p_row JSONB, p_writer TEXT)
|
|
601
|
+
RETURNS TEXT[] LANGUAGE sql VOLATILE SECURITY DEFINER AS $$ SELECT _delta_holders_framework(p_coll, p_row, p_writer, ARRAY[]::text[]); $$;
|
|
575
602
|
DO $$ BEGIN
|
|
576
603
|
EXECUTE format('ALTER FUNCTION _delta_holders(TEXT, JSONB, TEXT) SET search_path = %s, pg_temp', COALESCE(NULLIF(current_setting('search_path'), ''), 'public'));
|
|
577
604
|
END $$;
|
|
@@ -212,8 +212,7 @@ BEGIN
|
|
|
212
212
|
END IF;
|
|
213
213
|
SELECT * INTO v_coll FROM _delta_collections WHERE collection_key = p_def.root_collection;
|
|
214
214
|
IF NOT FOUND THEN RETURN; END IF;
|
|
215
|
-
|
|
216
|
-
INTO v_exists USING p_id;
|
|
215
|
+
v_exists := _delta_row_there(p_def.root_collection, p_id);
|
|
217
216
|
IF v_exists THEN RETURN; END IF;
|
|
218
217
|
-- Not there, as the writer reads it: made under the add rule (todo #71) -- an
|
|
219
218
|
-- id the serial has handed out is taken, whether a row RLS hides holds it or
|
|
@@ -225,7 +224,7 @@ BEGIN
|
|
|
225
224
|
END IF;
|
|
226
225
|
v_row := _delta_implied_root(p_def, p_id);
|
|
227
226
|
IF v_coll.temporal THEN v_row := v_row || jsonb_build_object('valid_from', p_ts, 'valid_to', NULL); END IF;
|
|
228
|
-
|
|
227
|
+
PERFORM _delta_row_add(p_def.root_collection, v_coll.table_name, v_row);
|
|
229
228
|
END;
|
|
230
229
|
$$ LANGUAGE plpgsql;
|
|
231
230
|
|
|
@@ -235,17 +234,12 @@ DECLARE
|
|
|
235
234
|
v_doc JSONB := COALESCE(_delta_open(p_doc_name, TRUE), '{}'::jsonb);
|
|
236
235
|
v_def _delta_docs := _delta_find_doc(p_doc_name);
|
|
237
236
|
v_scope JSONB;
|
|
238
|
-
v_coll RECORD;
|
|
239
|
-
v_exists BOOLEAN;
|
|
240
237
|
BEGIN
|
|
241
238
|
IF NOT COALESCE(v_def.implied, FALSE) THEN RETURN v_doc; END IF;
|
|
242
239
|
v_scope := _delta_resolve_scope(v_def, p_doc_name);
|
|
243
240
|
IF (v_scope->>'mode') <> 'single' THEN RETURN v_doc; END IF;
|
|
244
|
-
|
|
245
|
-
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;
|
|
241
|
+
-- (its root collection is there: _delta_open raised otherwise)
|
|
242
|
+
IF _delta_row_there(v_def.root_collection, (v_scope->'values'->>'id')::BIGINT) THEN RETURN v_doc; END IF;
|
|
249
243
|
RETURN v_doc || jsonb_build_object(v_def.root_collection, 'null'::jsonb);
|
|
250
244
|
END;
|
|
251
245
|
$$ LANGUAGE plpgsql;
|
|
@@ -351,7 +345,7 @@ $$ LANGUAGE plpgsql STABLE;
|
|
|
351
345
|
-- whole: what the ledger's inverse and a walk's guard read.
|
|
352
346
|
-- ---------------------------------------------------------------------------
|
|
353
347
|
|
|
354
|
-
CREATE OR REPLACE FUNCTION
|
|
348
|
+
CREATE OR REPLACE FUNCTION _delta_open_framework(p_doc_name TEXT, p_whole BOOLEAN)
|
|
355
349
|
RETURNS JSONB AS $$
|
|
356
350
|
DECLARE
|
|
357
351
|
v_def _delta_docs;
|
|
@@ -445,6 +439,11 @@ BEGIN
|
|
|
445
439
|
END;
|
|
446
440
|
$$ LANGUAGE plpgsql;
|
|
447
441
|
|
|
442
|
+
-- the framework's, until dzql's compiler (`compileLedger`) sends each document
|
|
443
|
+
-- it compiled to its own open, shown or whole
|
|
444
|
+
CREATE OR REPLACE FUNCTION _delta_open(p_doc_name TEXT, p_whole BOOLEAN)
|
|
445
|
+
RETURNS JSONB LANGUAGE sql AS $$ SELECT _delta_open_framework(p_doc_name, p_whole); $$;
|
|
446
|
+
|
|
448
447
|
CREATE OR REPLACE FUNCTION delta_open(p_doc_name TEXT)
|
|
449
448
|
RETURNS JSONB LANGUAGE sql AS $$ SELECT _delta_open(p_doc_name, FALSE); $$;
|
|
450
449
|
|