@cruba/crubal-postgres 0.0.0-stage → 0.3.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 +126 -2
- package/dist/bin/crubal-pg.d.ts +2 -0
- package/dist/bin/crubal-pg.js +115 -0
- package/dist/bin/crubal-pg.js.map +1 -0
- package/dist/src/border.d.ts +122 -0
- package/dist/src/border.js +432 -0
- package/dist/src/border.js.map +1 -0
- package/dist/src/db.d.ts +19 -0
- package/dist/src/db.js +60 -0
- package/dist/src/db.js.map +1 -0
- package/dist/src/definitions.d.ts +11 -0
- package/dist/src/definitions.js +119 -0
- package/dist/src/definitions.js.map +1 -0
- package/dist/src/index.d.ts +9 -0
- package/dist/src/index.js +9 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/projector.d.ts +24 -0
- package/dist/src/projector.js +148 -0
- package/dist/src/projector.js.map +1 -0
- package/dist/src/schema.sql +273 -0
- package/dist/src/state.d.ts +16 -0
- package/dist/src/state.js +138 -0
- package/dist/src/state.js.map +1 -0
- package/dist/src/store.d.ts +269 -0
- package/dist/src/store.js +1003 -0
- package/dist/src/store.js.map +1 -0
- package/dist/src/working.d.ts +64 -0
- package/dist/src/working.js +312 -0
- package/dist/src/working.js.map +1 -0
- package/package.json +44 -4
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
// definitions.ts: what the engine runs with, built from the store (docs/design/publishing.md): the installed
|
|
3
|
+
// bundles in install order (extended before extending, because a dependency is installed first), then the
|
|
4
|
+
// owner's working package, every `define` write as the member now stands. The publish checks run on the
|
|
5
|
+
// result, so a definition that breaks the set is refused before it is written.
|
|
6
|
+
import { checkPermissions } from "@cruba/crubal";
|
|
7
|
+
import { emptyParsed, readBundleOnto } from "@cruba/crubal";
|
|
8
|
+
import { checkPackage } from "@cruba/crubal";
|
|
9
|
+
import { checkContracts } from "@cruba/crubal";
|
|
10
|
+
import { unitsExtensionText } from "@cruba/crubal";
|
|
11
|
+
import { extendUnits, resetUnits, unitsTableWith } from "@cruba/crubal";
|
|
12
|
+
import { checkRules } from "@cruba/crubal";
|
|
13
|
+
import { checkViews } from "@cruba/crubal";
|
|
14
|
+
/** The owner's working package as one bundle: the define writes grouped by member kind, in log order. */
|
|
15
|
+
/** The owner's units extension (11.9), from the last `units` define write. */
|
|
16
|
+
export function unitsExtensionOf(definitions) {
|
|
17
|
+
let measures = [];
|
|
18
|
+
for (const d of definitions)
|
|
19
|
+
if (d.units === "extension" && Array.isArray(d.measures))
|
|
20
|
+
measures = d.measures;
|
|
21
|
+
return measures;
|
|
22
|
+
}
|
|
23
|
+
export function workingBundle(owner, definitions) {
|
|
24
|
+
// The member kinds that carry another kind's word come first (a filter names its facet, a view its shape,
|
|
25
|
+
// a contract its kind), so each define is filed under what it is (G11).
|
|
26
|
+
const sections = { filter: "filters", view: "views", contract: "contracts", facet: "facets", class: "classes", rule: "rules", kind: "kinds", shape: "shapes" };
|
|
27
|
+
const b = { crubal: "2", package: "working", version: "working", owner, id: `${owner}/working` };
|
|
28
|
+
// In log order, by member key: a later define of the same member replaces the earlier, a retired one
|
|
29
|
+
// (8.5, 11.4) takes it out, so the working package holds each member once as it now stands.
|
|
30
|
+
const members = new Map();
|
|
31
|
+
for (const d of definitions) {
|
|
32
|
+
const member = Object.keys(sections).find((m) => typeof d[m] === "string");
|
|
33
|
+
if (member) {
|
|
34
|
+
const key = `${member}:${String(d[member])}`;
|
|
35
|
+
if (d.retired === true)
|
|
36
|
+
members.delete(key);
|
|
37
|
+
else
|
|
38
|
+
members.set(key, { section: sections[member], def: d });
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (typeof d.permissions === "string") {
|
|
42
|
+
if (d.retired === true)
|
|
43
|
+
delete b.permissions;
|
|
44
|
+
else
|
|
45
|
+
b.permissions = d;
|
|
46
|
+
}
|
|
47
|
+
if (d.ownership !== undefined)
|
|
48
|
+
b.ownership = d.ownership;
|
|
49
|
+
}
|
|
50
|
+
for (const { section, def } of members.values()) {
|
|
51
|
+
const list = b[section] ?? [];
|
|
52
|
+
list.push(def);
|
|
53
|
+
b[section] = list;
|
|
54
|
+
}
|
|
55
|
+
return b;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Parsed definitions shared across stores that hold the same packages and the same working package
|
|
59
|
+
* (brief 18 step 1: a host of a thousand deployments on one package set parses it once, not a thousand
|
|
60
|
+
* times, and keeps one copy). Keyed by the installed versions and a hash of the define writes; a trial
|
|
61
|
+
* (`extra`) is never cached. The object is read-only to its callers, as a store's own cache already was.
|
|
62
|
+
*/
|
|
63
|
+
const parsedShared = new Map();
|
|
64
|
+
const PARSED_SHARED_MAX = 64;
|
|
65
|
+
/** Build the definitions in force: installed bundles in order, then the working package. */
|
|
66
|
+
export async function definitionsOf(client, owner, extra = []) {
|
|
67
|
+
const installed = (await client.query("select json from installed where owner = $1 order by ord", [owner])).rows;
|
|
68
|
+
const defs = (await client.query("select json from definitions where owner = $1 order by ord", [owner])).rows;
|
|
69
|
+
const key = extra.length ? undefined : `${installed.map((r) => `${r.json.package}/${r.json.version}`).join(",")}|${createHash("sha256").update(JSON.stringify(defs.map((r) => r.json))).digest("hex")}`;
|
|
70
|
+
const shared = key ? parsedShared.get(key) : undefined;
|
|
71
|
+
if (shared) {
|
|
72
|
+
parsedShared.delete(key);
|
|
73
|
+
parsedShared.set(key, shared);
|
|
74
|
+
// The process units table follows the owner read last, for the facet-only readers (see below).
|
|
75
|
+
const measures = shared.units ?? [];
|
|
76
|
+
resetUnits();
|
|
77
|
+
if (measures.length)
|
|
78
|
+
extendUnits(unitsExtensionText(measures));
|
|
79
|
+
return shared;
|
|
80
|
+
}
|
|
81
|
+
const base = emptyParsed();
|
|
82
|
+
for (const row of installed) {
|
|
83
|
+
const w = row.json;
|
|
84
|
+
const { rows } = await client.query("select json from bundles where owner = $1 and package = $2 and version = $3", [owner, String(w.package), String(w.version)]);
|
|
85
|
+
const bundle = rows[0]?.json;
|
|
86
|
+
if (!bundle)
|
|
87
|
+
throw new Error(`${w.package} /${w.version} is installed but its bundle is missing`);
|
|
88
|
+
readBundleOnto(bundle, base);
|
|
89
|
+
}
|
|
90
|
+
// `extra`: define writes not yet committed, so a trial (store.define) is checked as the package would stand.
|
|
91
|
+
const working = [...defs.map((r) => r.json), ...extra];
|
|
92
|
+
if (working.length)
|
|
93
|
+
readBundleOnto(workingBundle(owner, working), base);
|
|
94
|
+
// The units table in force: the standard table plus this owner's extension (one owner per process for now).
|
|
95
|
+
const measures = unitsExtensionOf(working);
|
|
96
|
+
// The units table in force rides on the dictionary (11.9, per owner): the readers that hold a dictionary
|
|
97
|
+
// read it there. The process table is set too, for the few readers that hold a facet alone
|
|
98
|
+
// (checkScalar's unit question); that remaining path still sees the last owner read in this process.
|
|
99
|
+
base.dict.units = unitsTableWith(measures.length ? unitsExtensionText(measures) : undefined);
|
|
100
|
+
resetUnits();
|
|
101
|
+
if (measures.length)
|
|
102
|
+
extendUnits(unitsExtensionText(measures));
|
|
103
|
+
base.units = measures;
|
|
104
|
+
if (key) {
|
|
105
|
+
parsedShared.set(key, base);
|
|
106
|
+
while (parsedShared.size > PARSED_SHARED_MAX)
|
|
107
|
+
parsedShared.delete(parsedShared.keys().next().value);
|
|
108
|
+
}
|
|
109
|
+
return base;
|
|
110
|
+
}
|
|
111
|
+
/** Every publish check (11.5) over a set of definitions. Throws the first problem. */
|
|
112
|
+
export function checkAll(p) {
|
|
113
|
+
checkPackage(p.dict);
|
|
114
|
+
checkRules(p.rules, p.dict);
|
|
115
|
+
checkContracts(p.contracts, p.dict, "", new Set(p.views.views.keys()));
|
|
116
|
+
checkViews(p.views, p.dict);
|
|
117
|
+
checkPermissions(p.perms, p.dict, new Set(p.contracts.map((c) => c.name)), new Set(p.views.views.keys()));
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=definitions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { connect, migrate, testSchema, transaction, type Client, type Pool } from "./db.ts";
|
|
2
|
+
export { loadState, persist, wipeState } from "./state.ts";
|
|
3
|
+
export { PgStore } from "./store.ts";
|
|
4
|
+
export { entriesSince, markStale, publish, type Projecting } from "./projector.ts";
|
|
5
|
+
export { Needs, settle, WorkingMap, WorkingRecords, workingState } from "./working.ts";
|
|
6
|
+
export { type Adapter, PgBorder, type BorderOptions } from "./border.ts";
|
|
7
|
+
export type { StoreOptions } from "./store.ts";
|
|
8
|
+
export { borderOptions } from "./border.ts";
|
|
9
|
+
export { checkAll, definitionsOf, unitsExtensionOf, workingBundle } from "./definitions.ts";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { connect, migrate, testSchema, transaction } from "./db.js";
|
|
2
|
+
export { loadState, persist, wipeState } from "./state.js";
|
|
3
|
+
export { PgStore } from "./store.js";
|
|
4
|
+
export { entriesSince, markStale, publish } from "./projector.js";
|
|
5
|
+
export { Needs, settle, WorkingMap, WorkingRecords, workingState } from "./working.js";
|
|
6
|
+
export { PgBorder } from "./border.js";
|
|
7
|
+
export { borderOptions } from "./border.js";
|
|
8
|
+
export { checkAll, definitionsOf, unitsExtensionOf, workingBundle } from "./definitions.js";
|
|
9
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,WAAW,EAA0B,MAAM,SAAS,CAAC;AAC5F,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC3D,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AACrC,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,OAAO,EAAmB,MAAM,gBAAgB,CAAC;AACnF,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACvF,OAAO,EAAgB,QAAQ,EAAsB,MAAM,aAAa,CAAC;AAEzE,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type Permissions } from "@cruba/crubal";
|
|
2
|
+
import type { Dictionary } from "@cruba/crubal";
|
|
3
|
+
import type { State } from "@cruba/crubal";
|
|
4
|
+
import { type StreamEntry, type ViewSet } from "@cruba/crubal";
|
|
5
|
+
import type { Client } from "./db.ts";
|
|
6
|
+
/** What a store needs to project: the dictionary, the views, and the permissions for partitioned ones. */
|
|
7
|
+
export type Projecting = {
|
|
8
|
+
dict: Dictionary;
|
|
9
|
+
views: ViewSet;
|
|
10
|
+
perms?: Permissions;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Mark the rows an entry made stale (12.6): for every record it changed, in every view, the rows whose
|
|
14
|
+
* read set the change touched, worked out on the state as it was before the entry.
|
|
15
|
+
*/
|
|
16
|
+
export declare function markStale(client: Client, owner: string, p: Projecting, before: State, after: State, seq: number): Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* Publish one view for one audience: rebuild its stale rows (every row the first time, or after a new
|
|
19
|
+
* version), compare them with the projection as stored, append the stream entries, and clear the marks.
|
|
20
|
+
* Returns the entries published.
|
|
21
|
+
*/
|
|
22
|
+
export declare function publish(client: Client, owner: string, p: Projecting, name: string, audience?: string): Promise<StreamEntry[]>;
|
|
23
|
+
/** The stream entries of a view for an audience after `seq`, for an index resuming from its cursor. */
|
|
24
|
+
export declare function entriesSince(client: Client, owner: string, p: Projecting, name: string, seq: number, audience?: string, limit?: number): Promise<StreamEntry[]>;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// projector.ts: projections and the projection stream (spec 12.6, docs/design/store.md). A write marks the
|
|
2
|
+
// rows it makes stale inside its own transaction, a row per (view, record); publishing drains those marks
|
|
3
|
+
// outside any write transaction, rebuilds only the marked rows with the engine's Views, upserts the
|
|
4
|
+
// projection and appends one stream entry per row that changed or left, numbered per view and audience.
|
|
5
|
+
import { Access } from "@cruba/crubal";
|
|
6
|
+
import { serialize } from "@cruba/crubal";
|
|
7
|
+
import { Views } from "@cruba/crubal";
|
|
8
|
+
import { Needs, settle, workingState, loadWhole } from "./working.js";
|
|
9
|
+
/**
|
|
10
|
+
* Mark the rows an entry made stale (12.6): for every record it changed, in every view, the rows whose
|
|
11
|
+
* read set the change touched, worked out on the state as it was before the entry.
|
|
12
|
+
*/
|
|
13
|
+
export async function markStale(client, owner, p, before, after, seq) {
|
|
14
|
+
const changed = [];
|
|
15
|
+
const beforeRecords = new Map(Map.prototype.entries.call(before.records));
|
|
16
|
+
for (const [id, r] of Map.prototype.entries.call(after.records)) {
|
|
17
|
+
if (serialize((beforeRecords.get(id) ?? null)) !== serialize(r))
|
|
18
|
+
changed.push(r);
|
|
19
|
+
}
|
|
20
|
+
if (!changed.length)
|
|
21
|
+
return;
|
|
22
|
+
const { out } = await settle(client, owner, before, (s) => {
|
|
23
|
+
const views = new Views(p.dict, p.views, s.records);
|
|
24
|
+
const marks = [];
|
|
25
|
+
for (const name of p.views.views.keys())
|
|
26
|
+
for (const r of changed)
|
|
27
|
+
for (const id of views.staleAfter(name, r))
|
|
28
|
+
marks.push([name, id]);
|
|
29
|
+
return marks;
|
|
30
|
+
});
|
|
31
|
+
for (const [view, record] of out) {
|
|
32
|
+
await client.query("insert into stale (owner, view, record, since) values ($1, $2, $3, $4) on conflict (owner, view, record) do update set since = least(stale.since, excluded.since)", [owner, view, record, seq]);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Publish one view for one audience: rebuild its stale rows (every row the first time, or after a new
|
|
37
|
+
* version), compare them with the projection as stored, append the stream entries, and clear the marks.
|
|
38
|
+
* Returns the entries published.
|
|
39
|
+
*/
|
|
40
|
+
export async function publish(client, owner, p, name, audience) {
|
|
41
|
+
const v = p.views.views.get(name);
|
|
42
|
+
if (!v)
|
|
43
|
+
throw new Error(`no view ${name}`);
|
|
44
|
+
const aud = audience ?? v.show;
|
|
45
|
+
const version = v.version ?? "";
|
|
46
|
+
const projection = version ? `${name} /${version}` : name;
|
|
47
|
+
// What is marked and whether the view was ever published come first: a view with nothing stale costs
|
|
48
|
+
// two small queries, never a read of every stored row (ME: fifteen views, thousands of rows each, on
|
|
49
|
+
// every write). Stored rows are read in full only for a first or a reset publish; otherwise the marked ones.
|
|
50
|
+
// Published before, and under which versions: the marker row, or (stores published before 0.1.16 kept
|
|
51
|
+
// no marker) any stored row; an empty view published once is not published again whole.
|
|
52
|
+
const marker = (await client.query("select version from published where owner = $1 and view = $2 and audience = $3", [owner, name, aud])).rows;
|
|
53
|
+
const anyVersion = marker.length ? marker : (await client.query("select distinct version from projections where owner = $1 and view = $2 and audience = $3", [owner, name, aud])).rows;
|
|
54
|
+
const published = anyVersion.length > 0;
|
|
55
|
+
const reset = published && !anyVersion.some((r) => r.version === version);
|
|
56
|
+
const marked = (await client.query("select record from stale where owner = $1 and view = $2", [owner, name])).rows;
|
|
57
|
+
const full = !published || reset;
|
|
58
|
+
const only = full ? undefined : marked.map((r) => String(r.record));
|
|
59
|
+
if (!full && !only?.length)
|
|
60
|
+
return [];
|
|
61
|
+
const stored = (full
|
|
62
|
+
? await client.query("select record, row, buckets, sort, pin, seq from projections where owner = $1 and view = $2 and audience = $3 and version = $4 order by seq", [owner, name, aud, version])
|
|
63
|
+
: await client.query("select record, row, buckets, sort, pin, seq from projections where owner = $1 and view = $2 and audience = $3 and version = $4 and record = any($5) order by seq", [owner, name, aud, version, only])).rows;
|
|
64
|
+
// On a reset every row is announced again, whatever was stored: the index drops everything when the
|
|
65
|
+
// reset arrives and keeps only what follows it.
|
|
66
|
+
const last = new Map(reset ? [] : stored.map((r) => [String(r.record), serialize({ row: r.row, buckets: (r.buckets ?? {}), order: { sort: (r.sort ?? []), ...(r.pin ? { pin: Number(r.pin) } : {}) } })]));
|
|
67
|
+
// The rows, on a working set that loads what the view reads.
|
|
68
|
+
const needs = new Needs();
|
|
69
|
+
const base = workingState(needs);
|
|
70
|
+
await loadWhole(client, owner, base);
|
|
71
|
+
const { out: now } = await settle(client, owner, base, (s) => {
|
|
72
|
+
const views = new Views(p.dict, p.views, s.records);
|
|
73
|
+
const access = p.perms ? new Access(p.dict, p.perms, s.records, [...s.edges.values()], owner) : undefined;
|
|
74
|
+
if (access && p.views)
|
|
75
|
+
access.views = p.views;
|
|
76
|
+
const rows = new Map();
|
|
77
|
+
// Every row in one pass: a page at a time selected and counted the whole view once per page.
|
|
78
|
+
for (const row of views.project(name, { audience: aud, all: true, ...(access ? { access } : {}), ...(only ? { only } : {}) }).rows) {
|
|
79
|
+
const rec = s.records.get(String(row.record));
|
|
80
|
+
rows.set(String(row.record), { row, buckets: rec ? views.bucketsFor(v, rec) : {}, ...(rec ? views.sortOf(v, rec) : { sort: [] }) });
|
|
81
|
+
}
|
|
82
|
+
return rows;
|
|
83
|
+
});
|
|
84
|
+
const head = (await client.query("select coalesce(max(seq), 0) as seq from projection_stream where owner = $1 and view = $2 and audience = $3 and version = $4", [owner, name, aud, version])).rows[0];
|
|
85
|
+
// The stream continues from its head under this version: a fresh version starts at 1, and a version
|
|
86
|
+
// come back to (a package reinstalled, a later one retired) continues past what it published before,
|
|
87
|
+
// so an index resuming from its cursor for that version takes the reset and what follows.
|
|
88
|
+
let seq = Number(head.seq);
|
|
89
|
+
const entries = [];
|
|
90
|
+
const upserts = [];
|
|
91
|
+
if (reset)
|
|
92
|
+
await client.query("delete from projections where owner = $1 and view = $2 and audience = $3 and version <> $4", [owner, name, aud, version]);
|
|
93
|
+
for (const [id, { row, buckets, sort, pin }] of now) {
|
|
94
|
+
const s = serialize({ row, buckets, order: { sort, ...(pin ? { pin } : {}) } });
|
|
95
|
+
if (last.get(id) === s)
|
|
96
|
+
continue;
|
|
97
|
+
const e = { projection, audience: aud, seq: ++seq, record: id, row, ...(Object.keys(buckets).length ? { buckets } : {}), ...(sort.length ? { sort } : {}), ...(pin ? { pin } : {}) };
|
|
98
|
+
if (reset && !entries.length)
|
|
99
|
+
e.reset = true;
|
|
100
|
+
entries.push(e);
|
|
101
|
+
upserts.push({ id, row, buckets, sort, ...(pin ? { pin } : {}), seq });
|
|
102
|
+
}
|
|
103
|
+
// Rows that left: every stored row the rebuild didn't give back, among the records rebuilt.
|
|
104
|
+
const gone = full ? [...last.keys()].filter((id) => !now.has(id)) : (only ?? []).filter((id) => last.has(id) && !now.has(id));
|
|
105
|
+
for (const id of gone) {
|
|
106
|
+
const e = { projection, audience: aud, seq: ++seq, record: id, removed: true };
|
|
107
|
+
if (reset && !entries.length)
|
|
108
|
+
e.reset = true;
|
|
109
|
+
entries.push(e);
|
|
110
|
+
}
|
|
111
|
+
// The stream and the rows written in batches, one statement per 500 entries rather than two per row
|
|
112
|
+
// (ME: 9,437 units in one view; the round trips were a third of a full publish).
|
|
113
|
+
for (const batch of chunks(entries, 500)) {
|
|
114
|
+
await client.query("insert into projection_stream (owner, view, version, audience, seq, json) select $1, $2, $3, $4, (e ->> 'seq')::bigint, e from jsonb_array_elements($5::jsonb) as t(e)", [owner, name, version, aud, JSON.stringify(batch)]);
|
|
115
|
+
}
|
|
116
|
+
for (const batch of chunks(upserts, 500)) {
|
|
117
|
+
await client.query(`insert into projections (owner, view, version, audience, record, row, buckets, marks, ancestors, sort, pin, seq)
|
|
118
|
+
select $1, $2, $3, $4, e ->> 'id', e -> 'row', e -> 'buckets', e -> 'row' -> 'marks',
|
|
119
|
+
coalesce(array(select jsonb_array_elements_text(case when jsonb_typeof(e -> 'row' -> 'ancestors') = 'array' then e -> 'row' -> 'ancestors' else '[]'::jsonb end)), '{}'),
|
|
120
|
+
e -> 'sort', (e ->> 'pin')::integer, (e ->> 'seq')::bigint
|
|
121
|
+
from jsonb_array_elements($5::jsonb) as t(e)
|
|
122
|
+
on conflict (owner, view, version, audience, record) do update set row = excluded.row, buckets = excluded.buckets, marks = excluded.marks, ancestors = excluded.ancestors, sort = excluded.sort, pin = excluded.pin, seq = excluded.seq`, [owner, name, version, aud, JSON.stringify(batch)]);
|
|
123
|
+
}
|
|
124
|
+
if (gone.length)
|
|
125
|
+
await client.query("delete from projections where owner = $1 and view = $2 and version = $3 and audience = $4 and record = any($5)", [owner, name, version, aud, gone]);
|
|
126
|
+
if (reset && !entries.length) {
|
|
127
|
+
// A new version with nothing changed still starts its stream: the index must drop the old rows.
|
|
128
|
+
await client.query("delete from projection_stream where owner = $1 and view = $2 and audience = $3 and version <> $4", [owner, name, aud, version]);
|
|
129
|
+
}
|
|
130
|
+
await client.query("insert into published (owner, view, audience, version) values ($1, $2, $3, $4) on conflict (owner, view, audience) do update set version = excluded.version", [owner, name, aud, version]);
|
|
131
|
+
await client.query("delete from stale where owner = $1 and view = $2", [owner, name]);
|
|
132
|
+
return entries;
|
|
133
|
+
}
|
|
134
|
+
/** The stream entries of a view for an audience after `seq`, for an index resuming from its cursor. */
|
|
135
|
+
export async function entriesSince(client, owner, p, name, seq, audience, limit) {
|
|
136
|
+
const v = p.views.views.get(name);
|
|
137
|
+
if (!v)
|
|
138
|
+
throw new Error(`no view ${name}`);
|
|
139
|
+
const rows = (await client.query(`select json from projection_stream where owner = $1 and view = $2 and audience = $3 and version = $4 and seq > $5 order by seq${limit ? ` limit ${Math.max(1, Math.floor(limit))}` : ""}`, [owner, name, audience ?? v.show, v.version ?? "", seq])).rows;
|
|
140
|
+
return rows.map((r) => r.json);
|
|
141
|
+
}
|
|
142
|
+
function chunks(list, size) {
|
|
143
|
+
const out = [];
|
|
144
|
+
for (let i = 0; i < list.length; i += size)
|
|
145
|
+
out.push(list.slice(i, i + size));
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=projector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"projector.js","sourceRoot":"","sources":["../../src/projector.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
-- The persistent store's schema (docs/design/store.md). Every table is keyed by owner: a domain, or a
|
|
2
|
+
-- hosted person's domain:ID. The log is the truth; every other table is a cache that `rebuild` gives back
|
|
3
|
+
-- from the log alone. JSON columns hold the spec's JSON form unchanged. `ord` keeps insertion order, so a
|
|
4
|
+
-- state read back lists things in the order the log made them, as the memory store does.
|
|
5
|
+
|
|
6
|
+
create table if not exists schema_version (version text primary key, applied_at timestamptz not null default now());
|
|
7
|
+
|
|
8
|
+
create table if not exists chains (
|
|
9
|
+
owner text not null,
|
|
10
|
+
chain text not null,
|
|
11
|
+
head_seq bigint not null default 0,
|
|
12
|
+
head_hash text,
|
|
13
|
+
primary key (owner, chain)
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
-- Append-only. The one update allowed is redaction (8.5.2); the trigger below refuses every other one.
|
|
17
|
+
create table if not exists log (
|
|
18
|
+
owner text not null,
|
|
19
|
+
chain text not null,
|
|
20
|
+
seq bigint not null,
|
|
21
|
+
log_id text not null,
|
|
22
|
+
prev text,
|
|
23
|
+
hash text not null,
|
|
24
|
+
at text,
|
|
25
|
+
witnessed boolean not null default false,
|
|
26
|
+
entry jsonb not null,
|
|
27
|
+
primary key (owner, chain, seq),
|
|
28
|
+
unique (owner, log_id)
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
create table if not exists records (
|
|
32
|
+
owner text not null,
|
|
33
|
+
id text not null,
|
|
34
|
+
ord bigint generated always as identity,
|
|
35
|
+
kind text not null,
|
|
36
|
+
level text not null,
|
|
37
|
+
"of" text,
|
|
38
|
+
within text,
|
|
39
|
+
ancestors text[] not null default '{}',
|
|
40
|
+
status jsonb,
|
|
41
|
+
applied jsonb,
|
|
42
|
+
wears text[] not null default '{}',
|
|
43
|
+
created text,
|
|
44
|
+
updated text,
|
|
45
|
+
doc jsonb not null,
|
|
46
|
+
primary key (owner, id)
|
|
47
|
+
);
|
|
48
|
+
create index if not exists records_level on records (owner, level);
|
|
49
|
+
-- A level's or a kind's rows in log order with a limit, served from the index alone (the probe: a seq scan over a million rows otherwise).
|
|
50
|
+
create index if not exists records_level_ord on records (owner, level, ord);
|
|
51
|
+
create index if not exists records_kind_ord on records (owner, kind, ord);
|
|
52
|
+
create index if not exists records_of on records (owner, "of");
|
|
53
|
+
create index if not exists records_within on records (owner, within);
|
|
54
|
+
create index if not exists records_ancestors on records using gin (ancestors);
|
|
55
|
+
create index if not exists records_values on records using gin ((doc -> 'values'));
|
|
56
|
+
create index if not exists records_kind on records (owner, kind);
|
|
57
|
+
create index if not exists records_wears on records using gin (wears);
|
|
58
|
+
|
|
59
|
+
create table if not exists definitions (
|
|
60
|
+
owner text not null,
|
|
61
|
+
key text not null,
|
|
62
|
+
ord bigint generated always as identity,
|
|
63
|
+
json jsonb not null,
|
|
64
|
+
primary key (owner, key)
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
create table if not exists installed (
|
|
68
|
+
owner text not null,
|
|
69
|
+
ord bigint generated always as identity,
|
|
70
|
+
json jsonb not null,
|
|
71
|
+
primary key (owner, ord)
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
-- The bundle of every installed version, whole: the install write names it by hash, and definitions are
|
|
75
|
+
-- rebuilt from it (docs/design/publishing.md).
|
|
76
|
+
create table if not exists bundles (
|
|
77
|
+
owner text not null,
|
|
78
|
+
package text not null,
|
|
79
|
+
version text not null,
|
|
80
|
+
hash text not null,
|
|
81
|
+
json jsonb not null,
|
|
82
|
+
primary key (owner, package, version)
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
create table if not exists edges (
|
|
86
|
+
owner text not null,
|
|
87
|
+
edge text not null,
|
|
88
|
+
ord bigint generated always as identity,
|
|
89
|
+
json jsonb not null,
|
|
90
|
+
primary key (owner, edge)
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
create table if not exists bindings (
|
|
94
|
+
owner text not null,
|
|
95
|
+
key text not null,
|
|
96
|
+
ord bigint generated always as identity,
|
|
97
|
+
record text not null,
|
|
98
|
+
primary key (owner, key)
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
create table if not exists instructions (
|
|
102
|
+
owner text not null,
|
|
103
|
+
issuer text not null,
|
|
104
|
+
n integer not null,
|
|
105
|
+
ord bigint generated always as identity,
|
|
106
|
+
json jsonb not null,
|
|
107
|
+
primary key (owner, issuer, n)
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
create table if not exists review (
|
|
111
|
+
owner text not null,
|
|
112
|
+
ord bigint generated always as identity,
|
|
113
|
+
json jsonb not null,
|
|
114
|
+
primary key (owner, ord)
|
|
115
|
+
);
|
|
116
|
+
create index if not exists review_key on review (owner, (json ->> 'key'));
|
|
117
|
+
|
|
118
|
+
create table if not exists runs (
|
|
119
|
+
owner text not null,
|
|
120
|
+
run text not null,
|
|
121
|
+
ord bigint generated always as identity,
|
|
122
|
+
json jsonb not null,
|
|
123
|
+
primary key (owner, run)
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
create table if not exists observations (
|
|
127
|
+
owner text not null,
|
|
128
|
+
ord bigint generated always as identity,
|
|
129
|
+
json jsonb not null,
|
|
130
|
+
primary key (owner, ord)
|
|
131
|
+
);
|
|
132
|
+
create index if not exists observations_on on observations (owner, (json ->> 'on'));
|
|
133
|
+
create index if not exists observations_supports on observations (owner, (json ->> 'supports'));
|
|
134
|
+
|
|
135
|
+
create table if not exists reads (
|
|
136
|
+
owner text not null,
|
|
137
|
+
ord bigint generated always as identity,
|
|
138
|
+
json jsonb not null,
|
|
139
|
+
primary key (owner, ord)
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
-- Kept beside the log, never rebuilt from it (9.1): the bytes as received.
|
|
143
|
+
create table if not exists intake (
|
|
144
|
+
owner text not null,
|
|
145
|
+
intake text not null,
|
|
146
|
+
contract text,
|
|
147
|
+
received text,
|
|
148
|
+
"for" text,
|
|
149
|
+
raw bytea,
|
|
150
|
+
sha256 text,
|
|
151
|
+
type text,
|
|
152
|
+
json jsonb not null,
|
|
153
|
+
primary key (owner, intake)
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
-- A send's mapped-out payload, kept beside the log (docs/design/runs.md): the run write holds only its hash,
|
|
157
|
+
-- so the payload can be shown, replayed to the adapter on retry, and erased.
|
|
158
|
+
create table if not exists outtake (
|
|
159
|
+
owner text not null,
|
|
160
|
+
run text not null,
|
|
161
|
+
contract text not null,
|
|
162
|
+
record text,
|
|
163
|
+
key text not null,
|
|
164
|
+
raw bytea not null,
|
|
165
|
+
sha256 text not null,
|
|
166
|
+
primary key (owner, run)
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
create table if not exists cursors (
|
|
170
|
+
owner text not null,
|
|
171
|
+
contract text not null,
|
|
172
|
+
view text not null,
|
|
173
|
+
version text not null,
|
|
174
|
+
audience text not null,
|
|
175
|
+
seq bigint not null,
|
|
176
|
+
primary key (owner, contract, view, version, audience)
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
create table if not exists schedules (
|
|
180
|
+
owner text not null,
|
|
181
|
+
contract text not null,
|
|
182
|
+
last_tick text,
|
|
183
|
+
primary key (owner, contract)
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
create table if not exists projections (
|
|
187
|
+
owner text not null,
|
|
188
|
+
view text not null,
|
|
189
|
+
version text not null,
|
|
190
|
+
audience text not null,
|
|
191
|
+
record text not null,
|
|
192
|
+
row jsonb,
|
|
193
|
+
buckets jsonb,
|
|
194
|
+
marks jsonb,
|
|
195
|
+
ancestors text[] not null default '{}',
|
|
196
|
+
sort jsonb,
|
|
197
|
+
pin integer,
|
|
198
|
+
seq bigint not null,
|
|
199
|
+
primary key (owner, view, version, audience, record)
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
create table if not exists projection_stream (
|
|
203
|
+
owner text not null,
|
|
204
|
+
view text not null,
|
|
205
|
+
version text not null,
|
|
206
|
+
audience text not null,
|
|
207
|
+
seq bigint not null,
|
|
208
|
+
json jsonb not null,
|
|
209
|
+
primary key (owner, view, version, audience, seq)
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
-- A view published at least once for an audience, and under which version: a view with no rows is
|
|
213
|
+
-- otherwise indistinguishable from one never published, and would rebuild whole on every write.
|
|
214
|
+
create table if not exists published (
|
|
215
|
+
owner text not null,
|
|
216
|
+
view text not null,
|
|
217
|
+
audience text not null,
|
|
218
|
+
version text not null,
|
|
219
|
+
primary key (owner, view, audience)
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
create table if not exists stale (
|
|
223
|
+
owner text not null,
|
|
224
|
+
view text not null,
|
|
225
|
+
record text not null,
|
|
226
|
+
since bigint not null,
|
|
227
|
+
primary key (owner, view, record)
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
-- The log never changes except by redaction (8.5.2): a write replaced by its hash, or an entry reduced to
|
|
231
|
+
-- its hash. Anything else is refused here, whatever the caller.
|
|
232
|
+
create or replace function log_only_redaction() returns trigger language plpgsql as $$
|
|
233
|
+
declare
|
|
234
|
+
i integer;
|
|
235
|
+
old_writes jsonb := old.entry -> 'writes';
|
|
236
|
+
new_writes jsonb := new.entry -> 'writes';
|
|
237
|
+
begin
|
|
238
|
+
if tg_op = 'DELETE' then
|
|
239
|
+
raise exception 'the log is append-only: % cannot be deleted', old.log_id;
|
|
240
|
+
end if;
|
|
241
|
+
if new.owner <> old.owner or new.chain <> old.chain or new.seq <> old.seq or new.log_id <> old.log_id
|
|
242
|
+
or new.hash <> old.hash or coalesce(new.prev, '') <> coalesce(old.prev, '') then
|
|
243
|
+
raise exception 'the log is append-only: only a redaction may change %', old.log_id;
|
|
244
|
+
end if;
|
|
245
|
+
-- A whole entry reduced to its hash.
|
|
246
|
+
if new.entry ? 'redacted' then
|
|
247
|
+
if (new.entry - 'log' - 'prev' - 'chain' - 'sig' - 'redacted') <> '{}'::jsonb then
|
|
248
|
+
raise exception 'a redacted entry keeps only log, prev, chain, sig and redacted: %', old.log_id;
|
|
249
|
+
end if;
|
|
250
|
+
return new;
|
|
251
|
+
end if;
|
|
252
|
+
-- Write by write: every write is unchanged or replaced by {"redacted": "sha256:…"}.
|
|
253
|
+
if (new.entry - 'writes') <> (old.entry - 'writes') or jsonb_array_length(new_writes) <> jsonb_array_length(old_writes) then
|
|
254
|
+
raise exception 'the log is append-only: only a write may be redacted in %', old.log_id;
|
|
255
|
+
end if;
|
|
256
|
+
for i in 0 .. jsonb_array_length(old_writes) - 1 loop
|
|
257
|
+
if (new_writes -> i) <> (old_writes -> i) and not (((new_writes -> i) ? 'redacted') and ((new_writes -> i) - 'redacted') = '{}'::jsonb) then
|
|
258
|
+
raise exception 'the log is append-only: write % of % may only be redacted', i, old.log_id;
|
|
259
|
+
end if;
|
|
260
|
+
end loop;
|
|
261
|
+
return new;
|
|
262
|
+
end $$;
|
|
263
|
+
|
|
264
|
+
drop trigger if exists log_only_redaction on log;
|
|
265
|
+
create trigger log_only_redaction before update or delete on log for each row execute function log_only_redaction();
|
|
266
|
+
|
|
267
|
+
insert into schema_version (version) values ('2026-10-15') on conflict do nothing;
|
|
268
|
+
|
|
269
|
+
-- One order across an owner's chain and the chains of the people it hosts (14.1), for a rebuild.
|
|
270
|
+
alter table log add column if not exists ord bigint generated always as identity;
|
|
271
|
+
|
|
272
|
+
-- The platform index reads a caller's edges across owners (14.8, docs/design/host.md).
|
|
273
|
+
create index if not exists edges_who on edges ((json->>'who'));
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type State } from "@cruba/crubal";
|
|
2
|
+
import type { JsonObject, Record } from "@cruba/crubal";
|
|
3
|
+
import type { Client } from "./db.ts";
|
|
4
|
+
declare const isObj: (v: unknown) => v is JsonObject;
|
|
5
|
+
export type Row = {
|
|
6
|
+
[column: string]: unknown;
|
|
7
|
+
};
|
|
8
|
+
/** A record as the engine holds it, from its row. Absent members stay absent. */
|
|
9
|
+
export declare function recordOf(row: Row): Record;
|
|
10
|
+
/** Read an owner's whole state. Order is the order the log made things, by `ord`. */
|
|
11
|
+
export declare function loadState(client: Client, owner: string): Promise<State>;
|
|
12
|
+
/** Write what changed between two states of one owner: upserts, deletes, and appends to the lists. */
|
|
13
|
+
export declare function persist(client: Client, owner: string, before: State, after: State): Promise<void>;
|
|
14
|
+
/** Forget an owner's state tables (not its log), before a rebuild. */
|
|
15
|
+
export declare function wipeState(client: Client, owner: string): Promise<void>;
|
|
16
|
+
export { isObj };
|