dzql 0.10.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.
- package/README.md +6 -6
- package/package.json +1 -1
- package/src/compile/cascades.ts +6 -6
- package/src/compile/columns.ts +7 -8
- package/src/compile/holders.ts +5 -11
- package/src/compile/holds.ts +4 -5
- package/src/compile/ledger.ts +7 -8
- package/src/compile/lookups.ts +118 -0
- package/src/compile/opens.ts +6 -0
- package/src/compile/resolve.ts +4 -4
- package/src/compile/root.ts +4 -4
- package/src/compile/rows.ts +18 -17
- package/src/compile/scope.ts +5 -4
- package/src/compile/told.ts +5 -4
- package/src/postgres/codegen.ts +2 -1
- package/src/postgres/listener.ts +50 -35
- package/src/postgres/pglite.ts +4 -1
- package/src/sql/001a-delta-meta.sql +52 -19
- package/src/sql/001b-delta-scope.sql +18 -477
- package/src/sql/001c-delta-read.sql +14 -291
- package/src/sql/001d-delta-write.sql +64 -379
- package/src/sql/001e-delta-ops.sql +4 -62
- package/src/sql/001g-delta-ledger.sql +6 -29
package/README.md
CHANGED
|
@@ -20,12 +20,12 @@ import { openPglite } from "dzql/pglite"; // Postgres in this process
|
|
|
20
20
|
import { jwtAuth } from "dzql/auth-jwt";
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
Its tests run delta's own cases (`@blueshed/delta/testing`) against it.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
fan-out
|
|
27
|
-
|
|
28
|
-
Workshop's `docs/postgres-split-plan.md`.
|
|
23
|
+
Its tests run delta's own cases (`@blueshed/delta/testing`) against it. `generateSql` writes the
|
|
24
|
+
SQL for each of your documents and collections, compiled from their definitions (opening a
|
|
25
|
+
document, reading its name, who holds a row, a write's scope and rows, what each document is told,
|
|
26
|
+
the fan-out, the ledger's reads): nothing interprets a definition at run time. Apply it after the
|
|
27
|
+
framework, every time, one app per database (`docs/delta/postgres.md`, "Compiled SQL"). The plan
|
|
28
|
+
is in Workshop's `docs/postgres-split-plan.md`.
|
|
29
29
|
|
|
30
30
|
## The whole-stack dzql (retiring)
|
|
31
31
|
|
package/package.json
CHANGED
package/src/compile/cascades.ts
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* - `dzql_hanging_rows__<slug>(id)`: the rows that hang from it, and on down,
|
|
14
14
|
* parent first.
|
|
15
15
|
* And `_delta_cascade_remove`, `_delta_cascade_rows` and `_delta_hanging_rows`,
|
|
16
|
-
* sending each collection to its own
|
|
17
|
-
*
|
|
16
|
+
* sending each collection to its own; a collection there is not has nothing
|
|
17
|
+
* to take. The order is the one the framework's had: child collections by key, then
|
|
18
18
|
* the collections that cascade on it by key and key column, each in the
|
|
19
19
|
* database's byte order (`COLLATE "C"`), and each one's rows by id.
|
|
20
20
|
*/
|
|
@@ -118,15 +118,15 @@ END;
|
|
|
118
118
|
$$ LANGUAGE plpgsql STABLE;`;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
/** A dispatcher: each collection of `schema` to its own
|
|
121
|
+
/** A dispatcher: each collection of `schema` to its own; any other has nothing to take. */
|
|
122
122
|
function dispatch(schema: Schema, name: string, what: string, volatility: string): string {
|
|
123
123
|
const colls = Object.keys(schema.tables);
|
|
124
124
|
const body = colls.length
|
|
125
125
|
? ` CASE p_collection_key
|
|
126
126
|
${colls.map((c) => ` WHEN ${lit(c)} THEN RETURN ${fn(what, c)}(p_id);`).join("\n")}
|
|
127
|
-
ELSE RETURN
|
|
127
|
+
ELSE RETURN '[]'::jsonb; -- a collection there is not: nothing to take, nothing hangs from it
|
|
128
128
|
END CASE;`
|
|
129
|
-
: ` RETURN
|
|
129
|
+
: ` RETURN '[]'::jsonb;`;
|
|
130
130
|
return `CREATE OR REPLACE FUNCTION ${name}(p_collection_key TEXT, p_id BIGINT)
|
|
131
131
|
RETURNS JSONB AS $$
|
|
132
132
|
BEGIN
|
|
@@ -135,7 +135,7 @@ END;
|
|
|
135
135
|
$$ LANGUAGE plpgsql${volatility};`;
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
/** The cascades and hanging rows of every collection of `schema`, and
|
|
138
|
+
/** The cascades and hanging rows of every collection of `schema`, and their dispatch. */
|
|
139
139
|
export function compileCascades(schema: Schema): string {
|
|
140
140
|
assertSlugs(schema);
|
|
141
141
|
const colls = Object.keys(schema.tables);
|
package/src/compile/columns.ts
CHANGED
|
@@ -10,19 +10,18 @@
|
|
|
10
10
|
*
|
|
11
11
|
* - `dzql_columns__<slug>(collection, columns_def)` for each document: the
|
|
12
12
|
* columns it writes, the ones it names (`columns`) where it names some,
|
|
13
|
-
* from the list written in. `_delta_write_columns` sends each
|
|
14
|
-
*
|
|
15
|
-
* (`_delta_write_columns_framework`).
|
|
13
|
+
* from the list written in. `_delta_write_columns` sends each document to
|
|
14
|
+
* its own, and refuses a name of none.
|
|
16
15
|
* - `_delta_assert_fields` itself: for each collection, each set of columns
|
|
17
16
|
* the definitions give it (all of them, and each document's own), checked
|
|
18
17
|
* by written statements -- the fields there are, the parent key, each
|
|
19
18
|
* column's type -- with the framework's messages, put together in the
|
|
20
19
|
* framework's order (`ORDER BY` in SQL, so the database's collation orders
|
|
21
20
|
* both alike). Columns of another set (a walk's, another version's) are
|
|
22
|
-
* checked by the
|
|
21
|
+
* checked by the columns given (`_delta_assert_fields_declared`).
|
|
23
22
|
*/
|
|
24
23
|
import type { ColumnDef, DocDef, ResolvedTable, Schema } from "@blueshed/delta/backend";
|
|
25
|
-
import { lit, named, need } from "./opens";
|
|
24
|
+
import { lit, named, need, noDoc } from "./opens";
|
|
26
25
|
|
|
27
26
|
/** A document's columns function: `dzql_columns__<slug>`. */
|
|
28
27
|
export const columnsFunction = (prefix: string): string => named("columns", prefix);
|
|
@@ -105,7 +104,7 @@ RETURNS JSONB AS $$
|
|
|
105
104
|
BEGIN
|
|
106
105
|
CASE p_def.prefix
|
|
107
106
|
${docs.map((d) => ` WHEN ${lit(d.prefix)} THEN RETURN ${columnsFunction(d.prefix)}(p_coll, p_columns_def);`).join("\n")}
|
|
108
|
-
ELSE
|
|
107
|
+
ELSE ${noDoc("p_def.prefix")}
|
|
109
108
|
END CASE;
|
|
110
109
|
END;
|
|
111
110
|
$$ LANGUAGE plpgsql STABLE;`;
|
|
@@ -136,8 +135,8 @@ BEGIN
|
|
|
136
135
|
${branches.join("\n")}
|
|
137
136
|
ELSE NULL;
|
|
138
137
|
END CASE;
|
|
139
|
-
-- another set of columns (a walk's, another version's): the
|
|
140
|
-
PERFORM
|
|
138
|
+
-- another set of columns (a walk's, another version's): by the columns given
|
|
139
|
+
PERFORM _delta_assert_fields_declared(p_collection, p_columns, p_parent_fk, p_value);
|
|
141
140
|
END;
|
|
142
141
|
$$ LANGUAGE plpgsql IMMUTABLE;`;
|
|
143
142
|
return [...docs.map((d) => columnsOf(schema, d)), dispatch, checks, ""].join("\n\n");
|
package/src/compile/holders.ts
CHANGED
|
@@ -10,10 +10,9 @@
|
|
|
10
10
|
* row hangs from, its chain written as lookups, where the document is one row
|
|
11
11
|
* of an unscoped definition; else every open name of its prefix), and the
|
|
12
12
|
* writer's; each name its own prefix owns and whose document holds the row
|
|
13
|
-
* (`dzql_holds__<slug>`).
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* was applied with and pg_temp last, as the framework's.
|
|
13
|
+
* (`dzql_holds__<slug>`). There is no other document: the framework that asked
|
|
14
|
+
* the rest retired (todo #26). SECURITY DEFINER, with the search path it was
|
|
15
|
+
* applied with and pg_temp last, as the framework's was.
|
|
17
16
|
*/
|
|
18
17
|
import type { DocDef, Schema } from "@blueshed/delta/backend";
|
|
19
18
|
import { chainRoot, holdsFunction } from "./holds";
|
|
@@ -39,13 +38,12 @@ ${all ? "" : ` v_rid := ${coll === doc.root ? "(p_row->>'id')::BIGINT" : ch
|
|
|
39
38
|
END LOOP;`;
|
|
40
39
|
}
|
|
41
40
|
|
|
42
|
-
/** The SQL that answers who holds a row: `_delta_holders`, each of `docs` asked by its own lookups
|
|
41
|
+
/** The SQL that answers who holds a row: `_delta_holders`, each of `docs` asked by its own lookups. */
|
|
43
42
|
export function compileHolders(schema: Schema, docs: DocDef[]): string {
|
|
44
43
|
const branches = Object.keys(schema.tables).flatMap((coll) => {
|
|
45
44
|
const holding = docs.filter((d) => d.root === coll || d.include.includes(coll));
|
|
46
45
|
return holding.length ? [` WHEN ${lit(coll)} THEN\n${holding.map((d) => askOf(schema, d, coll)).join("\n")}`] : [];
|
|
47
46
|
});
|
|
48
|
-
const asked = `ARRAY[${docs.map((d) => lit(d.prefix)).join(", ")}]::text[]`;
|
|
49
47
|
return `CREATE OR REPLACE FUNCTION _delta_holders(p_coll TEXT, p_row JSONB, p_writer TEXT)
|
|
50
48
|
RETURNS TEXT[] AS $$
|
|
51
49
|
DECLARE
|
|
@@ -58,11 +56,7 @@ BEGIN
|
|
|
58
56
|
upper(current_setting('transaction_isolation')) USING ERRCODE = '0A000';
|
|
59
57
|
END IF;
|
|
60
58
|
IF p_row IS NULL THEN RETURN v_names; END IF;
|
|
61
|
-
${branches.length ? ` CASE p_coll\n${branches.join("\n")}\n ELSE NULL;\n END CASE;\n` : ""}
|
|
62
|
-
IF EXISTS (SELECT 1 FROM _delta_docs WHERE (root_collection = p_coll OR p_coll = ANY(include)) AND NOT (prefix = ANY(${asked}))) THEN
|
|
63
|
-
v_names := v_names || _delta_holders_framework(p_coll, p_row, p_writer, ${asked});
|
|
64
|
-
END IF;
|
|
65
|
-
RETURN v_names;
|
|
59
|
+
${branches.length ? ` CASE p_coll\n${branches.join("\n")}\n ELSE NULL;\n END CASE;\n` : ""} RETURN v_names;
|
|
66
60
|
END;
|
|
67
61
|
$$ LANGUAGE plpgsql VOLATILE SECURITY DEFINER;
|
|
68
62
|
DO $$ BEGIN
|
package/src/compile/holds.ts
CHANGED
|
@@ -10,12 +10,11 @@
|
|
|
10
10
|
* (`scopeOf`, a name its columns cannot take holding nothing), a temporal row
|
|
11
11
|
* not told to a document named with a time past, and an included row's chain
|
|
12
12
|
* of parents up to the root, as written lookups. And `_delta_doc_holds`
|
|
13
|
-
* itself, which sends each
|
|
14
|
-
*
|
|
15
|
-
* (`_delta_doc_holds_framework`).
|
|
13
|
+
* itself, which sends each document to its function and refuses a name of none (`no doc def for`: the framework that read the rest
|
|
14
|
+
* retired, todo #26).
|
|
16
15
|
*/
|
|
17
16
|
import { q, type DocDef, type ResolvedTable, type Schema } from "@blueshed/delta/backend";
|
|
18
|
-
import { lit, named, need, scopeOf
|
|
17
|
+
import { lit, named, need, noDoc, scopeOf, currentOnly } from "./opens";
|
|
19
18
|
|
|
20
19
|
/** A document's holds function: `dzql_holds__<slug>`. */
|
|
21
20
|
export const holdsFunction = (prefix: string): string => named("holds", prefix);
|
|
@@ -83,7 +82,7 @@ RETURNS BOOLEAN AS $$
|
|
|
83
82
|
BEGIN
|
|
84
83
|
CASE p_def.prefix
|
|
85
84
|
${docs.map((d) => ` WHEN ${lit(d.prefix)} THEN RETURN ${holdsFunction(d.prefix)}(p_doc_name, p_coll, p_row);`).join("\n")}
|
|
86
|
-
ELSE
|
|
85
|
+
ELSE ${noDoc("p_doc_name")}
|
|
87
86
|
END CASE;
|
|
88
87
|
END;
|
|
89
88
|
$$ LANGUAGE plpgsql STABLE;`;
|
package/src/compile/ledger.ts
CHANGED
|
@@ -8,17 +8,16 @@
|
|
|
8
8
|
* - `dzql_open_whole__<slug>(name)` for each document: its open with every
|
|
9
9
|
* column of every row, whatever columns it names (`readOf`, whole), and
|
|
10
10
|
* `_delta_open` itself, sending each compiled document to its own open, shown
|
|
11
|
-
* (`dzql_open__<slug>`) or whole, and
|
|
12
|
-
* (`_delta_open_framework`);
|
|
11
|
+
* (`dzql_open__<slug>`) or whole, and refusing a name of none;
|
|
13
12
|
* - `_delta_same(collection, field, a, b)`: two values of a field the same, a
|
|
14
13
|
* time by the instant it names. The field's type is the writing process's
|
|
15
14
|
* (`delta.declared`), else the schema's, written in for each collection;
|
|
16
|
-
*
|
|
15
|
+
* a collection there is not, the same as JSON;
|
|
17
16
|
* - `_delta_row_standing(collection, id)`: a row as it stands, as `{row}`, for
|
|
18
|
-
* every collection of the schema;
|
|
17
|
+
* every collection of the schema; NULL for one there is not.
|
|
19
18
|
*/
|
|
20
19
|
import type { DocDef, Schema } from "@blueshed/delta/backend";
|
|
21
|
-
import { lit, openFunction, openWholeFunction, readOf, standingRows } from "./opens";
|
|
20
|
+
import { lit, noDoc, openFunction, openWholeFunction, readOf, standingRows } from "./opens";
|
|
22
21
|
|
|
23
22
|
/** The SQL the ledger reads by, for `schema` and each of `docs`. */
|
|
24
23
|
export function compileLedger(schema: Schema, docs: DocDef[]): string {
|
|
@@ -31,7 +30,7 @@ DECLARE
|
|
|
31
30
|
BEGIN
|
|
32
31
|
CASE v_def.prefix
|
|
33
32
|
${docs.map((d) => ` WHEN ${lit(d.prefix)} THEN RETURN CASE WHEN p_whole THEN ${openWholeFunction(d.prefix)}(p_doc_name) ELSE ${openFunction(d.prefix)}(p_doc_name) END;`).join("\n")}
|
|
34
|
-
ELSE
|
|
33
|
+
ELSE ${noDoc("p_doc_name")}
|
|
35
34
|
END CASE;
|
|
36
35
|
END;
|
|
37
36
|
$$ LANGUAGE plpgsql;`;
|
|
@@ -51,7 +50,7 @@ ${colls.map(([coll, t]) => {
|
|
|
51
50
|
v_declared := NULLIF(current_setting('delta.declared', true), '')::jsonb -> ${lit(coll)};
|
|
52
51
|
v_type := CASE WHEN v_declared IS NOT NULL THEN v_declared -> p_field ->> 'type'${times.length ? ` WHEN p_field IN (${times.map(lit).join(", ")}) THEN 'timestamptz'` : ""} END;`;
|
|
53
52
|
}).join("\n")}
|
|
54
|
-
ELSE
|
|
53
|
+
ELSE NULL; -- a collection there is not: its values the same as JSON
|
|
55
54
|
END CASE;
|
|
56
55
|
IF v_type = 'timestamptz' THEN RETURN (a #>> '{}')::timestamptz = (b #>> '{}')::timestamptz; END IF;
|
|
57
56
|
END IF;
|
|
@@ -64,7 +63,7 @@ RETURNS JSONB AS $$
|
|
|
64
63
|
BEGIN
|
|
65
64
|
CASE p_coll
|
|
66
65
|
${colls.map(([coll, t]) => ` WHEN ${lit(coll)} THEN RETURN jsonb_build_object('row', (SELECT _delta_strip_temporal(to_jsonb(t.*)) FROM ${standingRows(t)} t WHERE t.id = p_id));`).join("\n")}
|
|
67
|
-
ELSE RETURN
|
|
66
|
+
ELSE RETURN NULL; -- a collection there is not
|
|
68
67
|
END CASE;
|
|
69
68
|
END;
|
|
70
69
|
$$ LANGUAGE plpgsql STABLE;`;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compiled lookups: a collection's definition, its table's serial, and a
|
|
3
|
+
* document read as it stood. What the write, the reads and the ledger still
|
|
4
|
+
* looked up at run time -- `_delta_collections` for a collection's table,
|
|
5
|
+
* parent and columns, a table's sequence by its name, the framework's read as
|
|
6
|
+
* it stood -- written from the definitions.
|
|
7
|
+
*
|
|
8
|
+
* `compileLookups(schema, docs)` writes:
|
|
9
|
+
* - `_delta_collection(key)`: each collection's definition, as generateSql
|
|
10
|
+
* records it in `_delta_collections`, written in; NULL fields for a key of
|
|
11
|
+
* none;
|
|
12
|
+
* - `_delta_serial_new(table, id)` and `_delta_serial_past(table, id)`: each
|
|
13
|
+
* table's sequence named, read or moved as the framework's do (as the
|
|
14
|
+
* functions' owner, for a caller with the table's privilege); for a table
|
|
15
|
+
* there is not, no id is new and nothing moves;
|
|
16
|
+
* - `_delta_find_doc(name)`: the document a name is of, by the longest prefix
|
|
17
|
+
* it starts with, as generateSql records it in `_delta_docs`, written in;
|
|
18
|
+
* NULL for a name of none;
|
|
19
|
+
* - `delta_open_at(name, at)`: each compiled document to its own read as it
|
|
20
|
+
* stood (`dzql_open_at__<slug>`), refusing a name of none.
|
|
21
|
+
*
|
|
22
|
+
* Each answers what the framework's did (recorded: tests/compile/lookups.test.ts).
|
|
23
|
+
*/
|
|
24
|
+
import { q, type DocDef, type Schema } from "@blueshed/delta/backend";
|
|
25
|
+
import { findCascadeOn } from "../postgres/sql";
|
|
26
|
+
import { lit, noDoc, openAtFunction } from "./opens";
|
|
27
|
+
|
|
28
|
+
/** The caller, as the framework's serials ask: the role set, else the session's. */
|
|
29
|
+
const CALLER = "COALESCE(NULLIF(current_setting('role'), 'none'), session_user)";
|
|
30
|
+
|
|
31
|
+
/** The search path a definer function runs with: the schemas applied in, and pg_temp last (as the framework's serials). */
|
|
32
|
+
const searchPath = (signature: string) => `DO $$
|
|
33
|
+
DECLARE
|
|
34
|
+
v_path TEXT := COALESCE((SELECT string_agg(quote_ident(s), ', ') FROM unnest(current_schemas(false)) AS s), 'public') || ', pg_temp';
|
|
35
|
+
BEGIN
|
|
36
|
+
EXECUTE format('ALTER FUNCTION ${signature} SET search_path = %s', v_path);
|
|
37
|
+
END $$;`;
|
|
38
|
+
|
|
39
|
+
export function compileLookups(schema: Schema, docs: DocDef[]): string {
|
|
40
|
+
const tables = Object.entries(schema.tables);
|
|
41
|
+
|
|
42
|
+
const collection = `CREATE OR REPLACE FUNCTION _delta_collection(p_key TEXT)
|
|
43
|
+
RETURNS _delta_collections AS $$
|
|
44
|
+
BEGIN
|
|
45
|
+
CASE p_key
|
|
46
|
+
${tables.map(([key, t]) => {
|
|
47
|
+
const row = {
|
|
48
|
+
collection_key: key,
|
|
49
|
+
table_name: t.name,
|
|
50
|
+
columns_def: t.columns,
|
|
51
|
+
parent_collection: t.parent?.collection ?? null,
|
|
52
|
+
parent_fk: t.parent?.fkColumn ?? null,
|
|
53
|
+
temporal: t.temporal,
|
|
54
|
+
cascade_on: findCascadeOn(key, schema),
|
|
55
|
+
};
|
|
56
|
+
return ` WHEN ${lit(key)} THEN RETURN jsonb_populate_record(null::_delta_collections, ${lit(JSON.stringify(row))}::jsonb);`;
|
|
57
|
+
}).join("\n")}
|
|
58
|
+
ELSE RETURN NULL; -- a collection there is not
|
|
59
|
+
END CASE;
|
|
60
|
+
END;
|
|
61
|
+
$$ LANGUAGE plpgsql STABLE;`;
|
|
62
|
+
|
|
63
|
+
const serialNew = `CREATE OR REPLACE FUNCTION _delta_serial_new(p_table TEXT, p_id BIGINT)
|
|
64
|
+
RETURNS BOOLEAN AS $$
|
|
65
|
+
DECLARE
|
|
66
|
+
v_new BOOLEAN;
|
|
67
|
+
BEGIN
|
|
68
|
+
CASE p_table
|
|
69
|
+
${tables.map(([, t]) => ` WHEN ${lit(t.name)} THEN
|
|
70
|
+
IF NOT has_table_privilege(${CALLER}, p_table, 'SELECT') THEN RETURN FALSE; END IF;
|
|
71
|
+
SELECT p_id >= last_value + is_called::int INTO v_new FROM ${q(`seq_${t.name}`)};
|
|
72
|
+
RETURN v_new;`).join("\n")}
|
|
73
|
+
ELSE RETURN FALSE; -- a table there is not: no id is new
|
|
74
|
+
END CASE;
|
|
75
|
+
END;
|
|
76
|
+
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
|
77
|
+
${searchPath("_delta_serial_new(text, bigint)")}`;
|
|
78
|
+
|
|
79
|
+
const serialPast = `CREATE OR REPLACE FUNCTION _delta_serial_past(p_table TEXT, p_id BIGINT)
|
|
80
|
+
RETURNS void AS $$
|
|
81
|
+
BEGIN
|
|
82
|
+
CASE p_table
|
|
83
|
+
${tables.map(([, t]) => ` WHEN ${lit(t.name)} THEN
|
|
84
|
+
IF p_id > 9007199254740991 OR NOT has_table_privilege(${CALLER}, p_table, 'INSERT') THEN RETURN; END IF;
|
|
85
|
+
PERFORM setval(${lit(`seq_${t.name}`)}, p_id) FROM ${q(`seq_${t.name}`)} WHERE p_id >= last_value + is_called::int;`).join("\n")}
|
|
86
|
+
ELSE NULL; -- a table there is not: nothing to move
|
|
87
|
+
END CASE;
|
|
88
|
+
END;
|
|
89
|
+
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
|
90
|
+
${searchPath("_delta_serial_past(text, bigint)")}`;
|
|
91
|
+
|
|
92
|
+
// longest first, as the framework's ORDER BY length(prefix) DESC: a name is of the longest prefix it starts with
|
|
93
|
+
const byLength = [...docs].sort((a, b) => b.prefix.length - a.prefix.length || (a.prefix < b.prefix ? -1 : 1));
|
|
94
|
+
const findDoc = `CREATE OR REPLACE FUNCTION _delta_find_doc(p_doc_name TEXT)
|
|
95
|
+
RETURNS _delta_docs AS $$
|
|
96
|
+
BEGIN
|
|
97
|
+
${byLength.map((d) => {
|
|
98
|
+
const row = { prefix: d.prefix, root_collection: d.root, include: d.include, scope: d.scope, implied: !!d.implied, held: d.columns ?? {} };
|
|
99
|
+
return ` IF starts_with(p_doc_name, ${lit(d.prefix)}) THEN RETURN jsonb_populate_record(null::_delta_docs, ${lit(JSON.stringify(row))}::jsonb); END IF;`;
|
|
100
|
+
}).join("\n")}
|
|
101
|
+
RETURN NULL; -- a name no document holds
|
|
102
|
+
END;
|
|
103
|
+
$$ LANGUAGE plpgsql STABLE;`;
|
|
104
|
+
|
|
105
|
+
const openAt = `CREATE OR REPLACE FUNCTION delta_open_at(p_doc_name TEXT, p_at TIMESTAMPTZ)
|
|
106
|
+
RETURNS JSONB AS $$
|
|
107
|
+
DECLARE
|
|
108
|
+
v_def _delta_docs := _delta_find_doc(p_doc_name);
|
|
109
|
+
BEGIN
|
|
110
|
+
CASE v_def.prefix
|
|
111
|
+
${docs.map((d) => ` WHEN ${lit(d.prefix)} THEN RETURN ${openAtFunction(d.prefix)}(p_doc_name, p_at);`).join("\n")}
|
|
112
|
+
ELSE ${noDoc("p_doc_name")}
|
|
113
|
+
END CASE;
|
|
114
|
+
END;
|
|
115
|
+
$$ LANGUAGE plpgsql STABLE;`;
|
|
116
|
+
|
|
117
|
+
return [collection, findDoc, serialNew, serialPast, openAt, ""].join("\n\n");
|
|
118
|
+
}
|
package/src/compile/opens.ts
CHANGED
|
@@ -27,6 +27,12 @@ import { columnSqlType } from "../postgres/sql";
|
|
|
27
27
|
/** A string as an SQL literal. */
|
|
28
28
|
export const lit = (s: string): string => `'${s.replace(/'/g, "''")}'`;
|
|
29
29
|
|
|
30
|
+
/** A name no document compiled here holds: there is none (the framework that read the rest retired, todo #26). */
|
|
31
|
+
export const noDoc = (name: string): string => `RAISE EXCEPTION 'no doc def for: %', ${name} USING ERRCODE = 'P0001';`;
|
|
32
|
+
|
|
33
|
+
/** A collection none was compiled for: the write asks only its own, and delta_apply refuses any other first. */
|
|
34
|
+
export const noCollection = (key: string): string => `RAISE EXCEPTION 'unknown collection: %', ${key} USING ERRCODE = '22023';`;
|
|
35
|
+
|
|
30
36
|
/** A compiled function's name: `dzql_<kind>__` and the document's prefix (or a collection's key), lower case, every other character `_`. */
|
|
31
37
|
export const named = (kind: string, name: string): string =>
|
|
32
38
|
`dzql_${kind}__${name.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "")}`;
|
package/src/compile/resolve.ts
CHANGED
|
@@ -10,11 +10,11 @@
|
|
|
10
10
|
* bindings), `mode` and `at`; a value its column cannot take refused with the
|
|
11
11
|
* same message (`scopeOf`), raised, or with `refuse` false answered as the
|
|
12
12
|
* scope that holds nothing. And `_delta_resolve_scope` itself, which sends each
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* document to its own and refuses a name of none (`no doc def for`: the framework that read the rest
|
|
14
|
+
* retired, todo #26).
|
|
15
15
|
*/
|
|
16
16
|
import type { DocDef, Schema } from "@blueshed/delta/backend";
|
|
17
|
-
import { lit, named, need, scopeOf } from "./opens";
|
|
17
|
+
import { lit, named, need, noDoc, scopeOf } from "./opens";
|
|
18
18
|
|
|
19
19
|
/** A document's scope function: `dzql_scope__<slug>`. */
|
|
20
20
|
export const resolveFunction = (prefix: string): string => named("scope", prefix);
|
|
@@ -67,7 +67,7 @@ RETURNS JSONB AS $$
|
|
|
67
67
|
BEGIN
|
|
68
68
|
CASE p_def.prefix
|
|
69
69
|
${docs.map((d) => ` WHEN ${lit(d.prefix)} THEN RETURN ${resolveFunction(d.prefix)}(p_doc_name, p_refuse);`).join("\n")}
|
|
70
|
-
ELSE
|
|
70
|
+
ELSE ${noDoc("p_doc_name")}
|
|
71
71
|
END CASE;
|
|
72
72
|
END;
|
|
73
73
|
$$ LANGUAGE plpgsql STABLE;`;
|
package/src/compile/root.ts
CHANGED
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
* (`scopeOf`, refused with the same message), and the root's current rows
|
|
10
10
|
* asked with its conditions written as typed SQL, where the framework runs
|
|
11
11
|
* the resolver's conditions as text. And `_delta_root_there` itself, which
|
|
12
|
-
* sends each
|
|
13
|
-
*
|
|
12
|
+
* sends each document to its own and refuses a name of none (`no doc def for`: the framework that read the rest
|
|
13
|
+
* retired, todo #26).
|
|
14
14
|
*/
|
|
15
15
|
import type { DocDef, Schema } from "@blueshed/delta/backend";
|
|
16
|
-
import { lit, named, need, scopeOf, standingRows } from "./opens";
|
|
16
|
+
import { lit, named, need, noDoc, scopeOf, standingRows } from "./opens";
|
|
17
17
|
|
|
18
18
|
/** A document's root check: `dzql_root_there__<slug>`. */
|
|
19
19
|
export const rootThereFunction = (prefix: string): string => named("root_there", prefix);
|
|
@@ -53,7 +53,7 @@ RETURNS BOOLEAN AS $$
|
|
|
53
53
|
BEGIN
|
|
54
54
|
CASE p_def.prefix
|
|
55
55
|
${docs.map((d) => ` WHEN ${lit(d.prefix)} THEN RETURN ${rootThereFunction(d.prefix)}(p_doc_name, p_id);`).join("\n")}
|
|
56
|
-
ELSE
|
|
56
|
+
ELSE ${noDoc("p_doc_name")}
|
|
57
57
|
END CASE;
|
|
58
58
|
END;
|
|
59
59
|
$$ LANGUAGE plpgsql STABLE;`;
|
package/src/compile/rows.ts
CHANGED
|
@@ -15,17 +15,18 @@
|
|
|
15
15
|
* back, so an add of its id takes its place;
|
|
16
16
|
* - `_delta_row_there`: is the row there now (a temporal one's open version)?
|
|
17
17
|
* - `_delta_row_read`: the row as it stands, without its validity;
|
|
18
|
-
* - `_delta_row_add`: an add's row stored.
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
18
|
+
* - `_delta_row_add`: an add's row stored. An INSERT names the columns the row
|
|
19
|
+
* gives (`_delta_insert_sql`), so one it leaves out takes its database
|
|
20
|
+
* default: where the row gives exactly every column the schema declares (its
|
|
21
|
+
* id, parent key, columns and validity), that INSERT is one statement,
|
|
22
|
+
* written here; a row with any other keys is stored by the columns it gives
|
|
23
|
+
* (`_delta_row_add_given`).
|
|
23
24
|
*
|
|
24
|
-
* Each answers what the framework's
|
|
25
|
-
* collection it
|
|
25
|
+
* Each answers what the framework's did. A collection none was compiled for is
|
|
26
|
+
* refused (`unknown collection`), and a row of it is not there.
|
|
26
27
|
*/
|
|
27
28
|
import { q, type Schema } from "@blueshed/delta/backend";
|
|
28
|
-
import { lit, standingRows } from "./opens";
|
|
29
|
+
import { lit, noCollection, standingRows } from "./opens";
|
|
29
30
|
|
|
30
31
|
/** A CASE over the collections, each branch from `branch`, any other collection to `fallback`. */
|
|
31
32
|
function dispatch(schema: Schema, branch: (coll: string, table: Schema["tables"][string]) => string | null, fallback: string): string {
|
|
@@ -36,7 +37,7 @@ function dispatch(schema: Schema, branch: (coll: string, table: Schema["tables"]
|
|
|
36
37
|
return branches.length ? ` CASE p_coll\n${branches.join("\n")}\n ELSE ${fallback}\n END CASE;` : ` ${fallback}`;
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
/** The row SQL of every collection of `schema`, and
|
|
40
|
+
/** The row SQL of every collection of `schema`, and its dispatch. */
|
|
40
41
|
export function compileRows(schema: Schema): string {
|
|
41
42
|
const lock = `CREATE OR REPLACE FUNCTION _delta_row_lock(p_coll TEXT, p_table TEXT, p_temporal BOOLEAN, p_id BIGINT)
|
|
42
43
|
RETURNS JSONB AS $$
|
|
@@ -44,7 +45,7 @@ DECLARE
|
|
|
44
45
|
v_row JSONB;
|
|
45
46
|
BEGIN
|
|
46
47
|
${dispatch(schema, (_, t) => `SELECT to_jsonb(t.*) INTO v_row FROM ${q(t.name)} t WHERE t.id = p_id${t.temporal ? " AND valid_to IS NULL" : ""} FOR UPDATE;
|
|
47
|
-
RETURN v_row;`, "
|
|
48
|
+
RETURN v_row;`, noCollection("p_coll"))}
|
|
48
49
|
END;
|
|
49
50
|
$$ LANGUAGE plpgsql;`;
|
|
50
51
|
|
|
@@ -61,7 +62,7 @@ ${dispatch(schema, (_, t) => (t.temporal
|
|
|
61
62
|
RETURN v_row;`
|
|
62
63
|
: `DELETE FROM ${q(t.name)} WHERE id = p_id;
|
|
63
64
|
INSERT INTO ${q(t.name)} AS t SELECT * FROM jsonb_populate_record(null::${q(t.name)}, p_row) RETURNING to_jsonb(t.*) INTO v_row;
|
|
64
|
-
RETURN v_row;`), "
|
|
65
|
+
RETURN v_row;`), noCollection("p_coll"))}
|
|
65
66
|
END;
|
|
66
67
|
$$ LANGUAGE plpgsql;`;
|
|
67
68
|
|
|
@@ -69,7 +70,7 @@ $$ LANGUAGE plpgsql;`;
|
|
|
69
70
|
const next = `CREATE OR REPLACE FUNCTION _delta_next_id(p_coll TEXT, p_table TEXT)
|
|
70
71
|
RETURNS BIGINT AS $$
|
|
71
72
|
BEGIN
|
|
72
|
-
${dispatch(schema, (_, t) => `RETURN nextval(${lit(`seq_${t.name}`)});`, "
|
|
73
|
+
${dispatch(schema, (_, t) => `RETURN nextval(${lit(`seq_${t.name}`)});`, noCollection("p_coll"))}
|
|
73
74
|
END;
|
|
74
75
|
$$ LANGUAGE plpgsql;`;
|
|
75
76
|
|
|
@@ -77,7 +78,7 @@ $$ LANGUAGE plpgsql;`;
|
|
|
77
78
|
RETURNS void AS $$
|
|
78
79
|
BEGIN
|
|
79
80
|
${dispatch(schema, (_, t) => (t.temporal ? `DELETE FROM ${q(t.name)} WHERE id = p_id AND valid_from = p_ts AND valid_to = p_ts;
|
|
80
|
-
RETURN;` : null), "
|
|
81
|
+
RETURN;` : null), noCollection("p_coll"))}
|
|
81
82
|
END;
|
|
82
83
|
$$ LANGUAGE plpgsql;`;
|
|
83
84
|
|
|
@@ -87,7 +88,7 @@ DECLARE
|
|
|
87
88
|
p_coll TEXT := p_collection_key;
|
|
88
89
|
BEGIN
|
|
89
90
|
${dispatch(schema, (_, t) => `RETURN p_id IS NOT NULL AND EXISTS (SELECT 1 FROM ${standingRows(t)} WHERE id = p_id);`,
|
|
90
|
-
"RETURN
|
|
91
|
+
"RETURN FALSE; -- a collection there is not: no row of it is there")}
|
|
91
92
|
END;
|
|
92
93
|
$$ LANGUAGE plpgsql STABLE;`;
|
|
93
94
|
|
|
@@ -97,7 +98,7 @@ DECLARE
|
|
|
97
98
|
v_row JSONB;
|
|
98
99
|
BEGIN
|
|
99
100
|
${dispatch(schema, (_, t) => `SELECT _delta_strip_temporal(to_jsonb(t.*)) INTO v_row FROM ${standingRows(t)} t WHERE t.id = p_id;
|
|
100
|
-
RETURN v_row;`, "
|
|
101
|
+
RETURN v_row;`, noCollection("p_coll"))}
|
|
101
102
|
END;
|
|
102
103
|
$$ LANGUAGE plpgsql STABLE;`;
|
|
103
104
|
|
|
@@ -113,8 +114,8 @@ ${dispatch(schema, (_, t) => {
|
|
|
113
114
|
INSERT INTO ${q(t.name)} AS t (${cols}) SELECT ${cols} FROM jsonb_populate_record(null::${q(t.name)}, p_row) RETURNING to_jsonb(t.*) INTO v_row;
|
|
114
115
|
RETURN v_row;
|
|
115
116
|
END IF;
|
|
116
|
-
RETURN
|
|
117
|
-
}, "
|
|
117
|
+
RETURN _delta_row_add_given(p_table, p_row); -- some other set of keys: the INSERT names those it gives`;
|
|
118
|
+
}, noCollection("p_coll"))}
|
|
118
119
|
END;
|
|
119
120
|
$$ LANGUAGE plpgsql;`;
|
|
120
121
|
|
package/src/compile/scope.ts
CHANGED
|
@@ -10,11 +10,12 @@
|
|
|
10
10
|
* its included collections whole and its root rows by the name's conditions;
|
|
11
11
|
* a single document holds a row whose parent chain, walked up through the
|
|
12
12
|
* tables as they stand, reaches its root, which meets the rest of its name's
|
|
13
|
-
* scope. And `_delta_row_in_scope` itself, which sends each
|
|
14
|
-
*
|
|
13
|
+
* scope. And `_delta_row_in_scope` itself, which sends each document to its
|
|
14
|
+
* own and refuses a name of none (`no doc def for`: the framework that read the rest
|
|
15
|
+
* retired, todo #26).
|
|
15
16
|
*/
|
|
16
17
|
import { q, type DocDef, type Schema } from "@blueshed/delta/backend";
|
|
17
|
-
import { lit, named, need, scopeOf
|
|
18
|
+
import { lit, named, need, noDoc, scopeOf, currentOnly } from "./opens";
|
|
18
19
|
|
|
19
20
|
/** A document's write-scope function: `dzql_in_scope__<slug>`. */
|
|
20
21
|
export const inScopeFunction = (prefix: string): string => named("in_scope", prefix);
|
|
@@ -79,7 +80,7 @@ RETURNS BOOLEAN AS $$
|
|
|
79
80
|
BEGIN
|
|
80
81
|
CASE p_def.prefix
|
|
81
82
|
${docs.map((d) => ` WHEN ${lit(d.prefix)} THEN RETURN ${inScopeFunction(d.prefix)}(p_doc_name, p_collection_key, p_id);`).join("\n")}
|
|
82
|
-
ELSE
|
|
83
|
+
ELSE ${noDoc("p_doc_name")}
|
|
83
84
|
END CASE;
|
|
84
85
|
END;
|
|
85
86
|
$$ LANGUAGE plpgsql STABLE;`;
|
package/src/compile/told.ts
CHANGED
|
@@ -8,11 +8,12 @@
|
|
|
8
8
|
* definition: whether the name makes it one row (`scopeOf`), its root, the
|
|
9
9
|
* columns it shows of each collection it names them for (`columns`, the row's
|
|
10
10
|
* id and parent key with them), and the empty root an implied one opens with
|
|
11
|
-
* when its root leaves. And `_delta_told` itself, which sends each
|
|
12
|
-
*
|
|
11
|
+
* when its root leaves. And `_delta_told` itself, which sends each document to
|
|
12
|
+
* its own and refuses a name of none (`no doc def for`: the framework that read the rest
|
|
13
|
+
* retired, todo #26).
|
|
13
14
|
*/
|
|
14
15
|
import type { DocDef, Schema } from "@blueshed/delta/backend";
|
|
15
|
-
import { lit, named, need, scopeOf } from "./opens";
|
|
16
|
+
import { lit, named, need, noDoc, scopeOf } from "./opens";
|
|
16
17
|
|
|
17
18
|
/** A document's telling function: `dzql_told__<slug>`. */
|
|
18
19
|
export const toldFunction = (prefix: string): string => named("told", prefix);
|
|
@@ -86,7 +87,7 @@ RETURNS JSONB AS $$
|
|
|
86
87
|
BEGIN
|
|
87
88
|
CASE p_def.prefix
|
|
88
89
|
${docs.map((d) => ` WHEN ${lit(d.prefix)} THEN RETURN ${toldFunction(d.prefix)}(p_target, p_rows);`).join("\n")}
|
|
89
|
-
ELSE
|
|
90
|
+
ELSE ${noDoc("p_target")}
|
|
90
91
|
END CASE;
|
|
91
92
|
END;
|
|
92
93
|
$$ LANGUAGE plpgsql STABLE;`;
|
package/src/postgres/codegen.ts
CHANGED
|
@@ -36,6 +36,7 @@ import { compileColumns } from "../compile/columns";
|
|
|
36
36
|
import { compileRows } from "../compile/rows";
|
|
37
37
|
import { compileCascades } from "../compile/cascades";
|
|
38
38
|
import { compileLedger } from "../compile/ledger";
|
|
39
|
+
import { compileLookups } from "../compile/lookups";
|
|
39
40
|
import { compileTold } from "../compile/told";
|
|
40
41
|
|
|
41
42
|
export interface GenerateSqlOptions {
|
|
@@ -180,6 +181,6 @@ export function generateSql(
|
|
|
180
181
|
lines.push("");
|
|
181
182
|
|
|
182
183
|
// each document opened by SQL compiled for it (dzql), which docTypeFromDef calls
|
|
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));
|
|
184
|
+
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), "", compileLookups(schema, docs));
|
|
184
185
|
return lines.join("\n");
|
|
185
186
|
}
|