@mason-data/cli 0.7.0 → 0.9.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 +101 -73
- package/dist/src/api/error.js +3 -0
- package/dist/src/api/postgres/catalog-worker.js +49 -2
- package/dist/src/api/postgres/catalog.js +368 -25
- package/dist/src/api/postgres/encode.js +3 -1
- package/dist/src/api/postgres/explain.js +3 -2
- package/dist/src/api/postgres/listener.js +11 -3
- package/dist/src/api/postgres/protocol.js +39 -23
- package/dist/src/api/postgres/proxy.js +283 -0
- package/dist/src/api/postgres/schema.js +6 -0
- package/dist/src/api/postgres/server.js +1617 -245
- package/dist/src/api/postgres/translate.js +127 -10
- package/dist/src/api/request-log.js +18 -5
- package/dist/src/cdc/poll.js +5 -5
- package/dist/src/cli/background.js +47 -25
- package/dist/src/cli/bridge-stdin.js +101 -0
- package/dist/src/cli/cancel.js +75 -0
- package/dist/src/cli/command.js +170 -11
- package/dist/src/cli/commands.js +804 -209
- package/dist/src/cli/control.js +59 -0
- package/dist/src/cli/credentials.js +182 -0
- package/dist/src/cli/demo.js +20 -11
- package/dist/src/cli/deploy.js +574 -0
- package/dist/src/cli/dev.js +491 -130
- package/dist/src/cli/doctor.js +131 -38
- package/dist/src/cli/generated-files.js +143 -0
- package/dist/src/cli/guides.js +175 -36
- package/dist/src/cli/hosted.js +850 -0
- package/dist/src/cli/init.js +162 -65
- package/dist/src/cli/login.js +289 -0
- package/dist/src/cli/machine.js +13 -12
- package/dist/src/cli/main.js +11 -5
- package/dist/src/cli/mason.cjs +17 -0
- package/dist/src/cli/mcp.js +31 -22
- package/dist/src/cli/metrics.js +79 -0
- package/dist/src/cli/output-gate.js +176 -0
- package/dist/src/cli/page-share.js +96 -0
- package/dist/src/cli/pg-tls.js +14 -4
- package/dist/src/cli/pg-url.js +4 -0
- package/dist/src/cli/pin.js +19 -14
- package/dist/src/cli/pool-size.cjs +85 -0
- package/dist/src/cli/ports.js +2 -0
- package/dist/src/cli/preflight.js +138 -46
- package/dist/src/cli/project.js +142 -4
- package/dist/src/cli/readiness.js +76 -6
- package/dist/src/cli/registry.js +65 -38
- package/dist/src/cli/replica-hosted.js +665 -0
- package/dist/src/cli/replica.js +76 -0
- package/dist/src/cli/replication-checks.js +7 -5
- package/dist/src/cli/replication-mode.js +105 -26
- package/dist/src/cli/request-filter.js +134 -0
- package/dist/src/cli/roles.js +585 -0
- package/dist/src/cli/run-server.js +637 -48
- package/dist/src/cli/runs.js +1 -1
- package/dist/src/cli/security.js +67 -18
- package/dist/src/cli/serve.js +204 -46
- package/dist/src/cli/share-run.js +329 -0
- package/dist/src/cli/share.js +286 -0
- package/dist/src/cli/source-edit.js +75 -17
- package/dist/src/cli/source-role.js +344 -0
- package/dist/src/cli/sources.js +306 -47
- package/dist/src/cli/style.js +2 -1
- package/dist/src/cli/supervise.js +27 -24
- package/dist/src/cli/sync.js +55 -25
- package/dist/src/cli/token-hosted.js +337 -0
- package/dist/src/cli/tunnel.js +200 -0
- package/dist/src/cli/ui-ssh.js +460 -0
- package/dist/src/cli/ui.js +124 -17
- package/dist/src/cli/usage.js +128 -0
- package/dist/src/cli/user-hosted.js +299 -0
- package/dist/src/cli/user.js +573 -65
- package/dist/src/cli/version.js +9 -7
- package/dist/src/demo/seed.js +29 -0
- package/dist/src/duck/analyze.js +36 -20
- package/dist/src/duck/cancel.js +10 -1
- package/dist/src/duck/collation.js +266 -0
- package/dist/src/duck/constant-key.js +94 -0
- package/dist/src/duck/distinct-key.js +283 -26
- package/dist/src/duck/events.js +165 -0
- package/dist/src/duck/executor-worker.js +20 -11
- package/dist/src/duck/executor.js +47 -33
- package/dist/src/duck/explain.js +60 -46
- package/dist/src/duck/extensions.js +160 -0
- package/dist/src/duck/functions.js +461 -63
- package/dist/src/duck/gaps.js +48 -21
- package/dist/src/duck/generated-plan.js +1070 -0
- package/dist/src/duck/generated.js +906 -0
- package/dist/src/duck/grants.js +176 -0
- package/dist/src/duck/held-types.js +64 -0
- package/dist/src/duck/history.js +127 -0
- package/dist/src/duck/jsonb-order.js +9 -12
- package/dist/src/duck/kept-route.js +6 -5
- package/dist/src/duck/kept.js +15 -13
- package/dist/src/duck/late.js +1469 -40
- package/dist/src/duck/libc-bound.js +341 -0
- package/dist/src/duck/libc-order.js +892 -126
- package/dist/src/duck/pg-eval-worker.js +7 -1
- package/dist/src/duck/pg-eval.js +6 -4
- package/dist/src/duck/pgtree.js +774 -23
- package/dist/src/duck/plan.js +369 -47
- package/dist/src/duck/publication.js +20 -6
- package/dist/src/duck/requests.js +269 -93
- package/dist/src/duck/resolved.js +117 -2
- package/dist/src/duck/rewrite.js +4584 -580
- package/dist/src/duck/row-filter.js +655 -0
- package/dist/src/duck/statement.js +19 -0
- package/dist/src/duck/store.js +439 -177
- package/dist/src/duck/stream.js +19 -10
- package/dist/src/duck/sync.js +1411 -368
- package/dist/src/duck/token.js +4 -1
- package/dist/src/duck/types.js +69 -12
- package/dist/src/duck/usage.js +326 -0
- package/dist/src/duck/users.js +69 -15
- package/dist/src/duck/verified.js +161 -1
- package/dist/src/duck/views.js +5 -5
- package/dist/src/lib/database-name.js +62 -0
- package/dist/src/lib/database-url.js +55 -10
- package/dist/src/lib/dotenv.js +17 -4
- package/dist/src/lib/instant.js +50 -0
- package/dist/src/lib/log-buffer.js +77 -0
- package/dist/src/lib/log-read.js +196 -0
- package/dist/src/lib/pg-numeric.js +390 -49
- package/dist/src/lib/project-version.js +23 -23
- package/dist/src/lib/secret-mask.js +142 -0
- package/dist/src/lib/table-sync.js +282 -0
- package/dist/src/web/app.css +706 -66
- package/dist/src/web/app.js +331 -409
- package/dist/src/web/charts.js +229 -0
- package/dist/src/web/index.html +82 -18
- package/dist/src/web/pages/queries.js +192 -0
- package/dist/src/web/pages/status.js +611 -0
- package/dist/src/web/pages/syncs.js +155 -0
- package/dist/src/web/pages/tables.js +439 -0
- package/dist/src/web/pages/usage.js +397 -0
- package/dist/src/web/pages/users.js +219 -0
- package/dist/src/web/shared.js +584 -0
- package/dist/src/web/vendor/README.md +25 -0
- package/dist/src/web/vendor/geist-1.7.2/Geist-Variable.woff2 +0 -0
- package/dist/src/web/vendor/geist-1.7.2/GeistMono-Variable.woff2 +0 -0
- package/dist/src/web/vendor/geist-1.7.2/LICENSE.txt +92 -0
- package/dist/src/web/vendor/lucide-1.56.0/LICENSE.txt +43 -0
- package/dist/src/web/vendor/lucide-1.56.0/chart-column.mjs +29 -0
- package/dist/src/web/vendor/lucide-1.56.0/layout-dashboard.mjs +29 -0
- package/dist/src/web/vendor/lucide-1.56.0/logs.mjs +34 -0
- package/dist/src/web/vendor/lucide-1.56.0/menu.mjs +28 -0
- package/dist/src/web/vendor/lucide-1.56.0/square-terminal.mjs +28 -0
- package/dist/src/web/vendor/lucide-1.56.0/table.mjs +29 -0
- package/dist/src/web/vendor/lucide-1.56.0/users.mjs +29 -0
- package/dist/src/web/vendor/lucide-1.56.0/x.mjs +27 -0
- package/dist/src/web/vendor/uplot-1.6.32/LICENSE.txt +21 -0
- package/dist/src/web/vendor/uplot-1.6.32/uPlot.esm.js +6140 -0
- package/dist/src/web/vendor/uplot-1.6.32/uPlot.min.css +1 -0
- package/dist/src/web/vendor/zilla-slab-5.3.0/LICENSE.txt +93 -0
- package/dist/src/web/vendor/zilla-slab-5.3.0/zilla-slab-latin-700-normal.woff2 +0 -0
- package/package.json +12 -4
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ROLES: WHAT A USER MAY READ, HELD ON THE ANALYSED TREE (GitHub issue 30, stage 1: table and column grants per role).
|
|
3
|
+
* A role is an entry of the project's roles file (`mason/roles.yml`, src/cli/roles.ts): for each copied table, every
|
|
4
|
+
* column (`{}`), `columns:` or `exclude:`, the sources file's own words — or `tables: all`, every copied table and column.
|
|
5
|
+
* A `mason user` carries a role (src/duck/users.ts `MasonUser.role`); a user with none is held to the file's
|
|
6
|
+
* `default:` role, and reads nothing when the file names none (stage 1b: the FILE decides, never the code); with no
|
|
7
|
+
* roles file at all, every user reads every copied table. A token (no user) reads every copied table.
|
|
8
|
+
*
|
|
9
|
+
* ENFORCED ON THE TREE POSTGRES ANALYSED, NEVER ON SQL TEXT: Postgres puts one `RTEPERMISSIONINFO` per relation in the
|
|
10
|
+
* query tree it analyses (src/duck/analyze.ts), with `selectedCols` as its own bitmap of the columns the statement
|
|
11
|
+
* reads — a column read inside a CTE, a subquery, a join's USING, a window, a WHERE or a CASE is in it; `SELECT *` lists
|
|
12
|
+
* every column; `count(*)` reads none; a whole-row reference (`SELECT t FROM t`) is the bit for attnum 0, which means
|
|
13
|
+
* every column. This is the information Postgres's own executor checks privileges with (ExecCheckPermissions), so an
|
|
14
|
+
* alias, a subquery or a view cannot get around it: a view is expanded by Mason itself before the check
|
|
15
|
+
* (src/duck/views.ts), so the check sees the tables it reads — stricter than Postgres, where a grant on the view
|
|
16
|
+
* suffices, and said so in docs/POSTGRES-API.md.
|
|
17
|
+
*
|
|
18
|
+
* THE REFUSAL IS POSTGRES'S: 42501 `permission denied for table orders`, the words Postgres 18 answers whether the
|
|
19
|
+
* table or one of its columns is missing from the grants (its executor names the table only), so a tool handles it as
|
|
20
|
+
* it handles Postgres's. The DETAIL names the role and the first hidden column, the HINT what the role may read. The
|
|
21
|
+
* rules are Postgres's too: `count(*)` over a table is answered when the role has any column of it; `SELECT *` over a
|
|
22
|
+
* table the role has only part of is refused, never narrowed (a narrowed `*` would change the result's columns); a
|
|
23
|
+
* system column (xmin, ctid) needs the whole table.
|
|
24
|
+
*/
|
|
25
|
+
import { StatementError } from "./statement.js";
|
|
26
|
+
import { catalogRows } from "./analyze.js";
|
|
27
|
+
/** no roles file: every session reads every copied table, as before; one object, so the listener sees no change */
|
|
28
|
+
export const NO_ROLES = { file: null, grants: new Map(), problem: null, default: null };
|
|
29
|
+
/**
|
|
30
|
+
* THE CATALOG'S ROLE OF A USER THE FILE HOLDS TO NOTHING (no role of its own, no `default:`): a Postgres role with no
|
|
31
|
+
* grant, so its catalog requests answer as Postgres answers a login with none — information_schema lists none of the
|
|
32
|
+
* copied tables or views, every `has_*_privilege` is false, `set_config` is revoked (while any role exists; a session's
|
|
33
|
+
* own `set_config` never reaches the catalog, src/api/postgres/translate.ts setConfig).
|
|
34
|
+
* Named outside the file's namespace: `ROLE_NAME_RE` (src/cli/roles.ts) forbids a leading underscore, so no role of the
|
|
35
|
+
* file can ever be it.
|
|
36
|
+
*/
|
|
37
|
+
export const NO_ROLE_CATALOG = "__mason_no_role";
|
|
38
|
+
/** The roles the catalog Postgres holds (src/api/postgres/catalog.ts catalogRoles): the file's, and — whenever a roles
|
|
39
|
+
* file exists — `NO_ROLE_CATALOG`, a role with no table, for a user the file holds to nothing. */
|
|
40
|
+
export function catalogRolesOf(live) {
|
|
41
|
+
if (live.file === null)
|
|
42
|
+
return live.grants;
|
|
43
|
+
return new Map([...live.grants, [NO_ROLE_CATALOG, { name: NO_ROLE_CATALOG, file: live.file, tables: new Map(), all: false }]]);
|
|
44
|
+
}
|
|
45
|
+
/** `FirstLowInvalidHeapAttributeNumber` is -7: a column's bit in `selectedCols` is its attnum + 7 */
|
|
46
|
+
const ATTNUM_OFFSET = 7;
|
|
47
|
+
/** ACL_SELECT in `requiredPerms` */
|
|
48
|
+
const ACL_SELECT = 2;
|
|
49
|
+
const isNode = (v) => v !== null && v !== undefined && typeof v === "object" && !Array.isArray(v);
|
|
50
|
+
/** Every RTEPERMISSIONINFO of the tree that asks SELECT, with its bitmap read as attnums. */
|
|
51
|
+
export function tableReads(tree) {
|
|
52
|
+
const out = [];
|
|
53
|
+
const visit = (v) => {
|
|
54
|
+
if (Array.isArray(v)) {
|
|
55
|
+
for (const x of v)
|
|
56
|
+
visit(x);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (!isNode(v))
|
|
60
|
+
return;
|
|
61
|
+
if (v._node === "RTEPERMISSIONINFO") {
|
|
62
|
+
const perms = Number(v.requiredPerms ?? 0);
|
|
63
|
+
const relid = Number(v.relid ?? 0);
|
|
64
|
+
if ((perms & ACL_SELECT) !== 0 && relid > 0) {
|
|
65
|
+
// a bitmapset prints as `(b 8 9)`: the `b` first, then each member
|
|
66
|
+
const bits = Array.isArray(v.selectedCols) ? v.selectedCols.filter((x) => typeof x === "string" && /^\d+$/.test(x)).map(Number) : [];
|
|
67
|
+
const read = { relid, attnums: [], wholeRow: false, system: false };
|
|
68
|
+
for (const b of bits) {
|
|
69
|
+
const attnum = b - ATTNUM_OFFSET;
|
|
70
|
+
if (attnum === 0)
|
|
71
|
+
read.wholeRow = true;
|
|
72
|
+
else if (attnum < 0)
|
|
73
|
+
read.system = true;
|
|
74
|
+
else
|
|
75
|
+
read.attnums.push(attnum);
|
|
76
|
+
}
|
|
77
|
+
out.push(read);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
for (const [k, x] of Object.entries(v))
|
|
81
|
+
if (k !== "_node")
|
|
82
|
+
visit(x);
|
|
83
|
+
};
|
|
84
|
+
visit(tree);
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The reads named through the catalog Postgres's own pg_class and pg_attribute (dropped columns left out, as Postgres
|
|
89
|
+
* leaves them): the copied tables only (relkind r) — a view is never here, since Mason expands it before the plan is
|
|
90
|
+
* made (src/duck/views.ts), and a catalog relation is not a copy.
|
|
91
|
+
*/
|
|
92
|
+
export async function resolveReads(catalog, reads) {
|
|
93
|
+
if (reads.length === 0)
|
|
94
|
+
return [];
|
|
95
|
+
const relids = [...new Set(reads.map((r) => Math.trunc(r.relid)))];
|
|
96
|
+
const rows = await catalogRows(catalog, `SELECT c.oid::text, n.nspname, c.relname, a.attnum::text, a.attname FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace ` +
|
|
97
|
+
`LEFT JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum > 0 AND NOT a.attisdropped WHERE c.oid IN (${relids.join(", ")}) AND c.relkind = 'r' ` +
|
|
98
|
+
// the copied tables alone: pg_class and the rest of pg_catalog are relkind 'r' too, and PUBLIC's on Postgres (a review finding of #35)
|
|
99
|
+
`AND n.nspname NOT IN ('pg_catalog', 'information_schema') ORDER BY c.oid, a.attnum`);
|
|
100
|
+
const tables = new Map();
|
|
101
|
+
for (const [oid, schema, name, attnum, attname] of rows) {
|
|
102
|
+
const t = tables.get(Number(oid)) ?? { table: `${schema}.${name}`, columns: new Map() };
|
|
103
|
+
if (attnum !== null && attnum !== undefined && attname !== null && attname !== undefined)
|
|
104
|
+
t.columns.set(Number(attnum), attname);
|
|
105
|
+
tables.set(Number(oid), t);
|
|
106
|
+
}
|
|
107
|
+
const out = [];
|
|
108
|
+
for (const r of reads) {
|
|
109
|
+
const t = tables.get(r.relid);
|
|
110
|
+
if (t === undefined)
|
|
111
|
+
continue;
|
|
112
|
+
const all = [...t.columns.entries()].sort((a, b) => a[0] - b[0]).map(([, n]) => n);
|
|
113
|
+
const named = r.wholeRow ? all : [...r.attnums].sort((a, b) => a - b).map((n) => t.columns.get(n)).filter((n) => n !== undefined);
|
|
114
|
+
out.push({ table: t.table, columns: named, all, system: r.system });
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
/** a table as a tool writes it: its name alone in public, else schema.table (the sources file's own spelling) */
|
|
119
|
+
const shown = (table) => table.startsWith("public.") ? table.slice("public.".length) : table;
|
|
120
|
+
/** The 42501 of a read the role does not cover, Postgres's words first; null when the role covers every read. */
|
|
121
|
+
export function grantRefusal(reads, role) {
|
|
122
|
+
if (role.noRole !== undefined)
|
|
123
|
+
return reads.length === 0 ? null : noRoleRefusal(reads[0].table, role.file, role.noRole);
|
|
124
|
+
const tablesOf = () => {
|
|
125
|
+
if (role.all)
|
|
126
|
+
return "every synced table (tables: all)";
|
|
127
|
+
const names = [...role.tables.keys()].map(shown);
|
|
128
|
+
return names.length ? names.join(", ") : "none";
|
|
129
|
+
};
|
|
130
|
+
for (const r of reads) {
|
|
131
|
+
const g = role.tables.get(r.table);
|
|
132
|
+
const denied = (detail, hint) => new StatementError({ code: "42501", message: `permission denied for table ${r.table.replace(/^[^.]+\./, "")}`, detail, hint });
|
|
133
|
+
if (g === undefined) {
|
|
134
|
+
return denied(`the role ${role.name} has no grant on ${shown(r.table)}`, `the role ${role.name} reads: ${tablesOf()} (${role.file} names what each role reads; Mason gap roles)`);
|
|
135
|
+
}
|
|
136
|
+
if (g.all)
|
|
137
|
+
continue;
|
|
138
|
+
const may = `the role ${role.name} reads these columns of ${shown(r.table)}: ${g.columns.join(", ")} (${role.file} names what each role reads; Mason gap roles)`;
|
|
139
|
+
if (r.system)
|
|
140
|
+
return denied(`the role ${role.name} may read columns of ${shown(r.table)}, and a system column needs the whole table`, may);
|
|
141
|
+
const hidden = r.columns.find((c) => !g.columns.includes(c));
|
|
142
|
+
if (hidden !== undefined)
|
|
143
|
+
return denied(`the role ${role.name} may not read column ${hidden} of ${shown(r.table)}`, may);
|
|
144
|
+
// no column read (count(*)): any column of the table is enough, as Postgres rules it, and a grant names at least one
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* THE 42501 OF A USER WITH NO ROLE UNDER A FILE THAT NAMES NO DEFAULT (stage 1b): Postgres's message for the first
|
|
150
|
+
* table read, the reason in the DETAIL, and both ways out as the exact lines in the HINT — the file's `default:`, or
|
|
151
|
+
* the user made again with a role. `file`: the roles file as messages name it.
|
|
152
|
+
*/
|
|
153
|
+
export function noRoleRefusal(table, file, who) {
|
|
154
|
+
return new StatementError({ code: "42501", message: `permission denied for table ${table.replace(/^[^.]+\./, "")}`,
|
|
155
|
+
detail: `the user "${who.user}" has no role and ${file} names no default`, hint: noRoleWayOut(who.user, file, who.example) });
|
|
156
|
+
}
|
|
157
|
+
/** The two ways out for a user with no role under a file with no default, as exact lines: ONE wording, for the
|
|
158
|
+
* refusal's HINT, `mason start`, `mason user list` and `mason user add`. */
|
|
159
|
+
export const noRoleWayOut = (user, file, example) => `write default: ${example} at the top of ${file}, or mason user remove ${user} and mason user add ${user} --role ${example}`;
|
|
160
|
+
/** A role of the file to name in a way out: one of `tables: all` where there is one (the role today's access is), else
|
|
161
|
+
* the first, or a placeholder where it names none. */
|
|
162
|
+
export const exampleRole = (live) => [...live.grants.values()].find((g) => g.all)?.name ?? [...live.grants.keys()][0] ?? "<a role>";
|
|
163
|
+
/**
|
|
164
|
+
* The statement's table reads held to the role: the tree's reads named through the catalog, then refused where the role
|
|
165
|
+
* does not cover one. Called by the plan (src/duck/plan.ts) after Postgres's analysis and the views' expansion, before
|
|
166
|
+
* the rewrite — so Postgres's own refusal (an unknown column) comes first, and a role's before a Mason gap.
|
|
167
|
+
*/
|
|
168
|
+
export async function enforceGrants(catalog, tree, role) {
|
|
169
|
+
const reads = await resolveReads(catalog, tableReads(tree));
|
|
170
|
+
if (role !== null) {
|
|
171
|
+
const refused = grantRefusal(reads, role);
|
|
172
|
+
if (refused !== null)
|
|
173
|
+
throw refused;
|
|
174
|
+
}
|
|
175
|
+
return reads;
|
|
176
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHAT THE COPY HOLDS FOR A POSTGRES TYPE (issues 126 and 129): the one list that says so, read by the copy
|
|
3
|
+
* (src/duck/sync.ts: what it reads and declares), the catalog (the column declared as the source spells it, always), the
|
|
4
|
+
* wire (src/duck/types.ts: the source's OID) and the rewrite (src/duck/rewrite.ts: a literal, a sort).
|
|
5
|
+
*
|
|
6
|
+
* THE CLASS: a type the copy holds as another type. The catalog said the wire's type, and the wire said what DuckDB
|
|
7
|
+
* happened to read (`timetz` as text over DuckDB's TIMETZ, `point` as text over a STRUCT, `box` not copied at all), so a
|
|
8
|
+
* tool believing the catalog sent text functions, and they failed in the engine's words.
|
|
9
|
+
*
|
|
10
|
+
* THE DECISION: the catalog reports the source's type, whatever the copy holds, so Postgres's own analysis decides every
|
|
11
|
+
* statement; and every type is one of two kinds:
|
|
12
|
+
* - `same` (`NATIVE`): DuckDB holds a value of the type itself, whose order and text are Postgres's;
|
|
13
|
+
* - `text`: DuckDB holds Postgres's text of the value (`c::text`, an array as `c::text[]`). That is anything `NATIVE`
|
|
14
|
+
* does not name, a type added to Postgres or made by a user included: nothing falls through to what DuckDB reads.
|
|
15
|
+
* What can be answered over a text is answered (the value, `::text`, `=` and GROUP BY where equal values have one text); an
|
|
16
|
+
* order is not the text's, so a sort is refused by name (`held-type-compare`), and every operator, aggregate and cast
|
|
17
|
+
* stays refused until the oracle has verified it (src/duck/verified.ts).
|
|
18
|
+
*
|
|
19
|
+
* `tests/duck-held-types.test.ts` holds this list to pg_type: a type of Postgres's named neither way fails it.
|
|
20
|
+
*/
|
|
21
|
+
/** the types DuckDB holds as themselves, as `format_type` spells them with their typmod left off */
|
|
22
|
+
const NATIVE = new Set(["smallint", "integer", "bigint", "real", "double precision", "numeric", "text", "character varying", "character", "name", '"char"',
|
|
23
|
+
"boolean", "uuid", "date", "time without time zone", "time with time zone", "timestamp without time zone", "timestamp with time zone", "interval",
|
|
24
|
+
"json", "jsonb", "bytea", "oid",
|
|
25
|
+
// held as their text from the first: an inet's output is the rewrite's and src/duck/types.ts's, a citext's its bytes
|
|
26
|
+
"inet", "citext"]);
|
|
27
|
+
const held = (oid, array, size, textEqual = true, literal = true) => ({ oid, array, size, textEqual, literal });
|
|
28
|
+
/** every built-in type a column can have that `NATIVE` does not name, by its pg_type.typname */
|
|
29
|
+
export const HELD = {
|
|
30
|
+
point: held(600, 1017, 16, false), lseg: held(601, 1018, 32, false), path: held(602, 1019, -1, false), box: held(603, 1020, 32, false), polygon: held(604, 1027, -1, false),
|
|
31
|
+
line: held(628, 629, 24, false), circle: held(718, 719, 24, false), money: held(790, 791, 8, true, false), macaddr: held(829, 1040, 6), macaddr8: held(774, 775, 8), cidr: held(650, 651, -1),
|
|
32
|
+
bit: held(1560, 1561, -1), varbit: held(1562, 1563, -1), tsvector: held(3614, 3643, -1), tsquery: held(3615, 3645, -1, false), xml: held(142, 143, -1, false),
|
|
33
|
+
jsonpath: held(4072, 4073, -1, false), tid: held(27, 1010, 6), xid: held(28, 1011, 4), cid: held(29, 1012, 4), xid8: held(5069, 271, 8), pg_lsn: held(3220, 3221, 8),
|
|
34
|
+
regclass: held(2205, 2210, 4), regtype: held(2206, 2211, 4), regproc: held(24, 1008, 4), regprocedure: held(2202, 2207, 4), regoper: held(2203, 2208, 4), regoperator: held(2204, 2209, 4),
|
|
35
|
+
regconfig: held(3734, 3735, 4), regdictionary: held(3769, 3770, 4), regnamespace: held(4089, 4090, 4), regrole: held(4096, 4097, 4), regcollation: held(4191, 4192, 4),
|
|
36
|
+
int4range: held(3904, 3905, -1), int8range: held(3926, 3927, -1), daterange: held(3912, 3913, -1), tsrange: held(3908, 3909, -1), tstzrange: held(3910, 3911, -1, false),
|
|
37
|
+
numrange: held(3906, 3907, -1, false), int4multirange: held(4451, 6150, -1), int8multirange: held(4536, 6157, -1), datemultirange: held(4535, 6155, -1), tsmultirange: held(4533, 6152, -1),
|
|
38
|
+
tstzmultirange: held(4534, 6153, -1, false), nummultirange: held(4532, 6151, -1, false), int2vector: held(22, 1006, -1), oidvector: held(30, 1013, -1), aclitem: held(1033, 1034, 16),
|
|
39
|
+
refcursor: held(1790, 2201, -1), pg_snapshot: held(5038, 5039, -1, false), txid_snapshot: held(2970, 2949, -1, false),
|
|
40
|
+
};
|
|
41
|
+
/** `oid`'s array: DuckDB reads no array of it, so the copy holds it as a list of its digits */
|
|
42
|
+
export const OID_ARRAY = 1028;
|
|
43
|
+
const HELD_OIDS = new Map(Object.values(HELD).map((h) => [h.oid, h]));
|
|
44
|
+
const HELD_ARRAYS = new Map([...Object.values(HELD).map((h) => [h.array, h]), [OID_ARRAY, held(26, OID_ARRAY, -1)]]);
|
|
45
|
+
/** a built-in type held as its text, by its OID or its array's */
|
|
46
|
+
export const heldType = (oid) => oid === null ? undefined : HELD_OIDS.get(oid) ?? HELD_ARRAYS.get(oid);
|
|
47
|
+
/** a held type's name, for a refusal */
|
|
48
|
+
export const heldName = (oid) => Object.entries(HELD).find(([, h]) => h.oid === oid || h.array === oid)?.[0] ?? (oid === 18 ? '"char"' : oid === OID_ARRAY ? "oid[]" : String(oid));
|
|
49
|
+
/** an array of a type held as its text: a list of each element's text */
|
|
50
|
+
export const isHeldArray = (oid) => oid !== null && HELD_ARRAYS.has(oid);
|
|
51
|
+
/** a time written in the session's zone, which the copy's text cannot be: written again where the statement executes */
|
|
52
|
+
export const ZONED_HELD = new Map([[3910, "tstzrange"], [4534, "tstzmultirange"]]);
|
|
53
|
+
/** what `format_type` writes, its typmod and `[]` left off (`time(3) with time zone`, `bit(8)`, `numeric(12,2)[]`) */
|
|
54
|
+
const baseOf = (spelled) => spelled.replace(/\[\]$/, "").replace(/\(\d+(,\d+)?\)/, "").replace(/\s+/g, " ").trim().replace(/^interval .*/, "interval");
|
|
55
|
+
/** whether DuckDB holds a value of this type as itself */
|
|
56
|
+
export const isNative = (spelled) => NATIVE.has(baseOf(spelled));
|
|
57
|
+
/** How the copy holds a column of the source's type `spelled` (`typeKind`: pg_type.typtype of the type or its element).
|
|
58
|
+
* An enum is DuckDB's ENUM; any other type a database made is its text. */
|
|
59
|
+
export function holdOf(spelled, typeKind) {
|
|
60
|
+
const array = spelled.endsWith("[]");
|
|
61
|
+
if (typeKind === "e" || (isNative(spelled) && !(array && baseOf(spelled) === "oid")))
|
|
62
|
+
return "same";
|
|
63
|
+
return array ? "text[]" : "text";
|
|
64
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { MASON_SCHEMA } from "./store.js";
|
|
2
|
+
/** How many days every history table keeps (the request log, the sync's events, the usage samples, the copies'
|
|
3
|
+
* sizes) unless MASON_HISTORY_DAYS says. */
|
|
4
|
+
export const DEFAULT_HISTORY_DAYS = 7;
|
|
5
|
+
/** The instant `days` days before `now` (ms since the epoch), ISO 8601 in UTC: what a history table's trim deletes
|
|
6
|
+
* before. `now` is the module's own clock, the one that stamps its rows, never the engine's `now()`: a module reads
|
|
7
|
+
* ONE clock, or a clock it is given (a test's, a run's) and the engine's disagree and the trim deletes what it keeps. */
|
|
8
|
+
export function cutoff(now, days) { return new Date(now - days * 86_400_000).toISOString(); }
|
|
9
|
+
const KEPT = "the request log, the sync's events, the usage samples and the synced tables' sizes";
|
|
10
|
+
/** The days MASON_HISTORY_DAYS keeps, or what is wrong with the text. Unset or empty: the default. */
|
|
11
|
+
export function historyDays(text) {
|
|
12
|
+
if (text === undefined || text.trim() === "")
|
|
13
|
+
return DEFAULT_HISTORY_DAYS;
|
|
14
|
+
const days = Number(text);
|
|
15
|
+
if (!Number.isInteger(days) || days < 1) {
|
|
16
|
+
return { message: `MASON_HISTORY_DAYS=${JSON.stringify(text)} is not a number of days`,
|
|
17
|
+
why: `it is how many days Mason keeps its history (${KEPT}): a whole number, 1 or more`,
|
|
18
|
+
fix: `write MASON_HISTORY_DAYS=${DEFAULT_HISTORY_DAYS} (the default, as when it is unset) or another whole number of days` };
|
|
19
|
+
}
|
|
20
|
+
return days;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* THE HISTORY'S DAYS, AS THE ENVIRONMENT SETS THEM — read by this one function at every entrance (`mason serve`'s
|
|
24
|
+
* settings report, `mason start` before it spawns, and the run before anything opens), so a wrong setting is refused
|
|
25
|
+
* before the copy is touched, in the Failure shape. `MASON_REQUEST_LOG_DAYS` SET IS REFUSED, never run on the default
|
|
26
|
+
* (CLAUDE.md: a renamed setting refuses the start, naming the line to write): it kept the request log alone once, and
|
|
27
|
+
* the days kept are every history table's now, so it is MASON_HISTORY_DAYS. Then MASON_HISTORY_DAYS must be a number
|
|
28
|
+
* of days (`historyDays`).
|
|
29
|
+
*/
|
|
30
|
+
export function historyDaysSetting(env) {
|
|
31
|
+
const v = env.MASON_REQUEST_LOG_DAYS;
|
|
32
|
+
if (v !== undefined && v !== "") {
|
|
33
|
+
return { message: `MASON_REQUEST_LOG_DAYS=${v} is no longer read`,
|
|
34
|
+
why: `it was renamed MASON_HISTORY_DAYS, since the days it keeps are every history's (${KEPT})`,
|
|
35
|
+
fix: `write MASON_HISTORY_DAYS=${v.trim()} instead, then unset MASON_REQUEST_LOG_DAYS` };
|
|
36
|
+
}
|
|
37
|
+
return historyDays(env.MASON_HISTORY_DAYS);
|
|
38
|
+
}
|
|
39
|
+
/** what is wrong, as one line of `mason serve`'s settings report */
|
|
40
|
+
export function historyDaysWrongText(w) {
|
|
41
|
+
return `${w.message}: ${w.why} — ${w.fix}`;
|
|
42
|
+
}
|
|
43
|
+
/** how often the rows past the retention are deleted (and once at open) */
|
|
44
|
+
export const TRIM_EVERY_MS = 60 * 60 * 1000;
|
|
45
|
+
/** A history table's connections and its hourly trim. */
|
|
46
|
+
export class History {
|
|
47
|
+
writer;
|
|
48
|
+
reader;
|
|
49
|
+
trimTimer = null;
|
|
50
|
+
constructor(writer, reader) { this.writer = writer; this.reader = reader; }
|
|
51
|
+
/** The writer connected, `_mason` and each of `ddl` run on it in order, then the reader connected. */
|
|
52
|
+
static async open(store, ddl) {
|
|
53
|
+
const writer = await store.instance.connect();
|
|
54
|
+
await writer.run(`CREATE SCHEMA IF NOT EXISTS ${MASON_SCHEMA}`);
|
|
55
|
+
for (const d of ddl)
|
|
56
|
+
await writer.run(d);
|
|
57
|
+
const reader = await store.instance.connect();
|
|
58
|
+
return new History(writer, reader);
|
|
59
|
+
}
|
|
60
|
+
/** `trim` now (its failure is the open's), then every TRIM_EVERY_MS, an hourly one that fails said by `failed`. */
|
|
61
|
+
async keepTrimmed(trim, failed) {
|
|
62
|
+
await trim();
|
|
63
|
+
this.trimTimer = setInterval(() => { trim().catch((e) => failed(e)); }, TRIM_EVERY_MS);
|
|
64
|
+
this.trimTimer.unref();
|
|
65
|
+
}
|
|
66
|
+
/** No more hourly trims (the first step of a close). */
|
|
67
|
+
stopTrimming() {
|
|
68
|
+
if (this.trimTimer !== null) {
|
|
69
|
+
clearInterval(this.trimTimer);
|
|
70
|
+
this.trimTimer = null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** Both connections closed (the last step of a close). */
|
|
74
|
+
closeConnections() {
|
|
75
|
+
this.writer.closeSync();
|
|
76
|
+
this.reader.closeSync();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** A streak of failures, said once: `failed` says the first of a streak and counts what each cost; `succeeded` says,
|
|
80
|
+
* once, what the streak cost, and ends it. */
|
|
81
|
+
export class Streak {
|
|
82
|
+
lost = 0;
|
|
83
|
+
failed(cost, sayFirst) {
|
|
84
|
+
if (this.lost === 0)
|
|
85
|
+
sayFirst();
|
|
86
|
+
this.lost += cost;
|
|
87
|
+
}
|
|
88
|
+
succeeded(sayEnd) {
|
|
89
|
+
if (this.lost > 0) {
|
|
90
|
+
sayEnd(this.lost);
|
|
91
|
+
this.lost = 0;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** Rows written in batches: `push` one; a batch is written by `write` once `rows` are waiting or after `ms`, one batch
|
|
96
|
+
* at a time. `write` must not throw: a failure is its own to say (a Streak). */
|
|
97
|
+
export class Batches {
|
|
98
|
+
buffer = [];
|
|
99
|
+
timer = null;
|
|
100
|
+
/** the write in flight: one at a time, so batches commit in the order they were taken */
|
|
101
|
+
flushing = Promise.resolve();
|
|
102
|
+
rows;
|
|
103
|
+
ms;
|
|
104
|
+
write;
|
|
105
|
+
constructor(o) { this.rows = o.rows; this.ms = o.ms; this.write = o.write; }
|
|
106
|
+
push(row) {
|
|
107
|
+
this.buffer.push(row);
|
|
108
|
+
if (this.buffer.length >= this.rows) {
|
|
109
|
+
void this.flush();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
this.timer ??= setTimeout(() => { void this.flush(); }, this.ms);
|
|
113
|
+
}
|
|
114
|
+
/** Writes what is waiting, after any write in flight. Resolves whether it was written or not (`write` says a failure). */
|
|
115
|
+
flush() {
|
|
116
|
+
if (this.timer !== null) {
|
|
117
|
+
clearTimeout(this.timer);
|
|
118
|
+
this.timer = null;
|
|
119
|
+
}
|
|
120
|
+
const rows = this.buffer;
|
|
121
|
+
this.buffer = [];
|
|
122
|
+
if (rows.length === 0)
|
|
123
|
+
return this.flushing;
|
|
124
|
+
this.flushing = this.flushing.then(() => this.write(rows));
|
|
125
|
+
return this.flushing;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -30,14 +30,14 @@
|
|
|
30
30
|
* under ICU, ICU's sort key (DuckDB's `icu_sort_key`, which the rewrite hands in) and the bytes' hex; under a libc
|
|
31
31
|
* locale the measured key (src/duck/libc-order.ts `libcKey`) and the bytes' hex, as the rewrite sorts text
|
|
32
32
|
* (src/duck/rewrite.ts `libcKey`).
|
|
33
|
-
* A string's collation key is computed by DuckDB's ICU for the ICU
|
|
33
|
+
* A string's collation key is computed by DuckDB's ICU for the ICU kind, which a JavaScript function cannot
|
|
34
34
|
* call: the rewrite passes the document's strings' ICU keys as a list (`jsonbStrings` lists the strings, in the order
|
|
35
35
|
* this walk meets them; `icuKeys` the list DuckDB made of them). Held to Postgres by tests/duck-jsonb-order.test.ts
|
|
36
36
|
* (PGlite's order) and the oracle (tests/integration/duck-oracle.itest.ts, every collation it is run under).
|
|
37
37
|
*/
|
|
38
38
|
import { Buffer } from "node:buffer";
|
|
39
39
|
import { parseJson, storedPairs } from "./jsonb.js";
|
|
40
|
-
import {
|
|
40
|
+
import { libcKey } from "./libc-order.js";
|
|
41
41
|
const TAG = { null: 0x10, str: 0x20, num: 0x30, bool: 0x40, arr: 0x50, obj: 0x60 };
|
|
42
42
|
const BIAS = 0x80000000;
|
|
43
43
|
/** every string of the value — each object's keys and every string value — in the order the key's walk meets them */
|
|
@@ -68,7 +68,7 @@ function eachString(v, out) {
|
|
|
68
68
|
export const JSONB_KEY_FUNCTIONS = {
|
|
69
69
|
bytes: { key: "__mason_jsonb_key", strings: null },
|
|
70
70
|
icu: { key: "__mason_jsonb_key_icu", strings: "__mason_jsonb_strings" },
|
|
71
|
-
libc: { key: "__mason_jsonb_key_libc"
|
|
71
|
+
libc: { key: "__mason_jsonb_key_libc" },
|
|
72
72
|
};
|
|
73
73
|
export const STORED_SUFFIX = "_stored";
|
|
74
74
|
/**
|
|
@@ -86,14 +86,11 @@ export function jsonbStrings(text) {
|
|
|
86
86
|
/** whether a jsonb's strings hold a character a libc order does not cover: what its key refuses by name
|
|
87
87
|
* (`libc-uncovered`, src/duck/libc-order.ts `libcKey`), told from any other failure of the key (src/duck/kept.ts) */
|
|
88
88
|
export function libcUncovered(order, text) {
|
|
89
|
-
return jsonbStrings(text).some((s) => {
|
|
89
|
+
return jsonbStrings(text).some((s) => { if (order.unread)
|
|
90
|
+
return true; for (const ch of s)
|
|
90
91
|
if (order.uncovered[ch.codePointAt(0)])
|
|
91
92
|
return true; return false; });
|
|
92
93
|
}
|
|
93
|
-
/** The strings ICU's accent and case levels read for a libc key of each (src/duck/libc-order.ts `libcKept`). */
|
|
94
|
-
export function jsonbLibcKept(order, text) {
|
|
95
|
-
return jsonbStrings(text).map((s) => libcKept(order, s));
|
|
96
|
-
}
|
|
97
94
|
const u32 = (n) => [(n >>> 24) & 0xff, (n >>> 16) & 0xff, (n >>> 8) & 0xff, n & 0xff];
|
|
98
95
|
/**
|
|
99
96
|
* A JSON number's text as its key: zero, or its sign, exponent and digits (the value's, exactly: no rounding through a
|
|
@@ -120,15 +117,15 @@ export function jsonbKey(text, c) {
|
|
|
120
117
|
const out = [];
|
|
121
118
|
let at = 0;
|
|
122
119
|
const icuKey = () => {
|
|
123
|
-
if (c.kind
|
|
124
|
-
throw new Error("
|
|
120
|
+
if (c.kind !== "icu")
|
|
121
|
+
throw new Error("only ICU's key is handed in");
|
|
125
122
|
const k = c.icuKeys[at++];
|
|
126
123
|
if (k === undefined)
|
|
127
124
|
throw new Error(`the jsonb holds more strings than the ${c.icuKeys.length} ICU keys handed in: a Mason bug (the rewrite's list and this walk disagree)`);
|
|
128
125
|
return k;
|
|
129
126
|
};
|
|
130
127
|
const str = (s) => {
|
|
131
|
-
const key = c.kind === "bytes" ? s : c.kind === "icu" ? `${icuKey()} ${hex(s)}` : `${libcKey(c.order, s
|
|
128
|
+
const key = c.kind === "bytes" ? s : c.kind === "icu" ? `${icuKey()} ${hex(s)}` : `${libcKey(c.order, s)} ${hex(s)}`;
|
|
132
129
|
for (const b of Buffer.from(key, "utf8"))
|
|
133
130
|
out.push(b);
|
|
134
131
|
out.push(0x00);
|
|
@@ -169,7 +166,7 @@ export function jsonbKey(text, c) {
|
|
|
169
166
|
if (root.t !== "arr" && root.t !== "obj")
|
|
170
167
|
out.push(TAG.arr, ...u32(1), 0x00);
|
|
171
168
|
value(root);
|
|
172
|
-
if (c.kind
|
|
169
|
+
if (c.kind === "icu" && at !== c.icuKeys.length)
|
|
173
170
|
throw new Error(`the jsonb holds ${at} strings, ${c.icuKeys.length} ICU keys were handed in: a Mason bug (the rewrite's list and this walk disagree)`);
|
|
174
171
|
return Buffer.from(out);
|
|
175
172
|
}
|
|
@@ -149,14 +149,14 @@ export function pathText(r) {
|
|
|
149
149
|
* as a value: the exact read raises at it, by the column alone. A jsonb's key under a libc collation is marked where
|
|
150
150
|
* its strings hold a character the measured order does not cover, the gap the statement's own key refuses by name
|
|
151
151
|
* (`libc-uncovered`); any other mark is a Mason bug. Once counted (a minute at most), the CASE computes the row from its
|
|
152
|
-
* document, and raises the function's own error
|
|
152
|
+
* document, and raises the function's own error or answers. Raised by `MARKED_FUNCTIONS`, never
|
|
153
153
|
* DuckDB's `error()`, which is volatile (a window's ORDER BY refuses it: the eager oracle's `rank() OVER (ORDER BY j)`).
|
|
154
154
|
*/
|
|
155
155
|
export function markedError(r, libc) {
|
|
156
156
|
if (isOrderRead(r) && libc) {
|
|
157
|
-
return { code: "0A000", message: "a jsonb compared or sorted here holds a string Mason cannot order as the database's collation does
|
|
158
|
-
"
|
|
159
|
-
"in a minute, the
|
|
157
|
+
return { code: "0A000", message: "a jsonb compared or sorted here holds a string Mason cannot order as the database's collation does, " +
|
|
158
|
+
"in a row written since its stored keys were counted; asked again " +
|
|
159
|
+
"in a minute, the statement's own key answers or refuses it (Mason gap libc-uncovered)" };
|
|
160
160
|
}
|
|
161
161
|
return { code: "XX000", message: `a stored value of ${pathText(r)} could not be computed when its row was written (a Mason bug: report it, with mason logs); ` +
|
|
162
162
|
"asked again in a minute, it is computed from the row's document" };
|
|
@@ -224,7 +224,8 @@ export function exposes(tree) {
|
|
|
224
224
|
return false;
|
|
225
225
|
const k = kindOf(v);
|
|
226
226
|
const b = k === null ? v : bodyOf(v);
|
|
227
|
-
|
|
227
|
+
// (an arm of a set operation is a SelectStmt with no wrapper of its own: k is null)
|
|
228
|
+
if ((k === "SelectStmt" || k === null) && Array.isArray(b.targetList) && directTables(b.fromClause).length > 0
|
|
228
229
|
&& b.targetList.some((t) => kindOf(t) === "ResTarget" && star(bodyOf(t).val)))
|
|
229
230
|
return true;
|
|
230
231
|
if (k === "JoinExpr" && b.isNatural === true && directTables([b.larg, b.rarg]).length > 0)
|
package/dist/src/duck/kept.js
CHANGED
|
@@ -2,8 +2,8 @@ import { DUCKDB_VERSION, MASON_SCHEMA } from "./store.js";
|
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { canonicalRead, EQUAL_STEPS, HIDDEN_PREFIX, HIDDEN_RE, hiddenName, isEqualRead, isKeyRead, isOrderRead, KEY_NULL_HEX, KEY_RECOMPUTE_HEX, knownSteps, ORDER_STEPS, pathKey, pathText, readOfKey, VISIBLE_CATALOG } from "./kept-route.js";
|
|
4
4
|
import { JSONB_KEY_FUNCTIONS, KEY_VERSION, libcUncovered, STORED_SUFFIX } from "./jsonb-order.js";
|
|
5
|
+
import { TURN_FUNCTION } from "./functions.js";
|
|
5
6
|
import { encodeLibcOrder } from "./libc-order.js";
|
|
6
|
-
import { icuNameOf } from "./rewrite.js";
|
|
7
7
|
/** a path read by this many statements in the request log is stored (a larger table's also needs `KEEP_FILL_SHARE`) */
|
|
8
8
|
export const KEEP_AFTER_STATEMENTS = 3;
|
|
9
9
|
/** a path of a SMALL table (`SMALL_FILL_SECONDS`) read by one statement that took this long (ms, the request log's
|
|
@@ -89,7 +89,7 @@ export function storeJsonPathsOf(env) {
|
|
|
89
89
|
if (v === "off")
|
|
90
90
|
return false;
|
|
91
91
|
throw new Error(`${STORE_JSON_PATHS_SETTING}=${JSON.stringify(env[STORE_JSON_PATHS_SETTING])} is not a setting: on (the default) stores the JSON paths statements keep reading ` +
|
|
92
|
-
"as columns of the
|
|
92
|
+
"as columns of the synced table, off stores none (and drops those stored, at this start)");
|
|
93
93
|
}
|
|
94
94
|
const ident = (name) => `"${name.replaceAll('"', '""')}"`;
|
|
95
95
|
const literal = (s) => `'${s.replaceAll("'", "''")}'`;
|
|
@@ -97,7 +97,7 @@ const tableOf = (source) => { const at = source.indexOf("."); return `${ident(so
|
|
|
97
97
|
/** the value a stored column takes from its JSON column's `value` (a column reference or an expression): the path's
|
|
98
98
|
* `->` answer, never NULL (a NULL document's is `KEPT_NULL` too: the column's NULL is a row not stored yet) */
|
|
99
99
|
export function keptValueSql(p, value) {
|
|
100
|
-
return `coalesce(__mason_json_kept(NULL, ${value}, ${literal(p.steps)}, ${p.either}, ${p.canonical}, false), ${NULL_SQL})`;
|
|
100
|
+
return `coalesce(__mason_json_kept(${TURN_FUNCTION}(NULL, ${value}), ${value}, ${literal(p.steps)}, ${p.either}, ${p.canonical}, false), ${NULL_SQL})`;
|
|
101
101
|
}
|
|
102
102
|
/**
|
|
103
103
|
* A JSONB'S STORED KEY, from its column's `value` (src/duck/kept-route.ts `ORDER_STEPS`): the key the statement would
|
|
@@ -106,9 +106,9 @@ export function keptValueSql(p, value) {
|
|
|
106
106
|
* `KEY_RECOMPUTE_HEX` where the key raised, which its read computes (and raises) again.
|
|
107
107
|
*/
|
|
108
108
|
export function orderKeySql(c, value) {
|
|
109
|
-
const call = (name, ...args) => `${name}${STORED_SUFFIX}(NULL, ${args.join(", ")})`;
|
|
110
|
-
const key = c.kind
|
|
111
|
-
: call(JSONB_KEY_FUNCTIONS
|
|
109
|
+
const call = (name, ...args) => `${name}${STORED_SUFFIX}(${TURN_FUNCTION}(NULL, ${value}), ${args.join(", ")})`;
|
|
110
|
+
const key = c.kind !== "icu" ? call(JSONB_KEY_FUNCTIONS[c.kind].key, value)
|
|
111
|
+
: call(JSONB_KEY_FUNCTIONS.icu.key, value, `list_transform(${call(JSONB_KEY_FUNCTIONS.icu.strings, value)}, __mason_s -> icu_sort_key(__mason_s, ${literal(c.name)}))`);
|
|
112
112
|
return `CASE WHEN (${value}) IS NULL THEN unhex('${KEY_NULL_HEX}') ELSE coalesce(${key}, unhex('${KEY_RECOMPUTE_HEX}')) END`;
|
|
113
113
|
}
|
|
114
114
|
/**
|
|
@@ -124,7 +124,7 @@ export function orderId(c, libc, versions = `k${KEY_VERSION}:${DUCKDB_VERSION}`)
|
|
|
124
124
|
return `icu:${c.name}:${versions}`;
|
|
125
125
|
if (libc === undefined)
|
|
126
126
|
throw new Error("a libc collation's order was not read (Mason bug: report it)");
|
|
127
|
-
return `libc:${
|
|
127
|
+
return `libc:${createHash("sha256").update(encodeLibcOrder(libc)).digest("hex").slice(0, 16)}:${versions}`;
|
|
128
128
|
}
|
|
129
129
|
/**
|
|
130
130
|
* THE C KEY'S ID, stored under a libc collation for equality (`EQUAL_STEPS:<id>`, src/duck/kept-route.ts): the C
|
|
@@ -381,7 +381,7 @@ export class KeptJsonPaths {
|
|
|
381
381
|
for (const p of this.paths.values()) {
|
|
382
382
|
if (p.state === "dropped" || p.state === "waiting" || named.has(`${p.source}\u0000${p.name}`))
|
|
383
383
|
continue;
|
|
384
|
-
await this.drop(w, p, "its column is not in the
|
|
384
|
+
await this.drop(w, p, "its column is not in the synced table");
|
|
385
385
|
}
|
|
386
386
|
// a jsonb's stored key is its collation's, told after this (`orderBy`): until then a write leaves it unset and no
|
|
387
387
|
// plan reads it, so a ready one is filled again where unset first — the backfill's sweep alone (its ranges are done)
|
|
@@ -482,7 +482,7 @@ export class KeptJsonPaths {
|
|
|
482
482
|
async swapping(c, source, columns) {
|
|
483
483
|
this.gone = [];
|
|
484
484
|
for (const p of this.physical(source).filter((x) => !columns.includes(x.column))) {
|
|
485
|
-
const why = `${p.column} is no longer a column of the
|
|
485
|
+
const why = `${p.column} is no longer a column of the synced table ${source}`;
|
|
486
486
|
await c.run(`UPDATE ${PATHS_TABLE} SET state = 'dropped', why = ${literal(why)} WHERE name = ${literal(p.name)}`);
|
|
487
487
|
this.gone.push({ p, why });
|
|
488
488
|
}
|
|
@@ -662,7 +662,7 @@ export class KeptJsonPaths {
|
|
|
662
662
|
await this.check(p);
|
|
663
663
|
}
|
|
664
664
|
catch (e) {
|
|
665
|
-
this.host.log(`stored JSON paths: the review failed, and runs again in ${Math.round(this.reviewMs / 1000)} s (the
|
|
665
|
+
this.host.log(`stored JSON paths: the review failed, and runs again in ${Math.round(this.reviewMs / 1000)} s (the synced tables serve as they were): ${e instanceof Error ? e.message : String(e)}`);
|
|
666
666
|
}
|
|
667
667
|
finally {
|
|
668
668
|
this.reviewing = null;
|
|
@@ -726,10 +726,10 @@ export class KeptJsonPaths {
|
|
|
726
726
|
return false;
|
|
727
727
|
const name = hiddenName(read);
|
|
728
728
|
const col = cols.find((c) => c.name === read.column);
|
|
729
|
-
const why = col === undefined ? `${read.column} is not a column of the
|
|
729
|
+
const why = col === undefined ? `${read.column} is not a column of the synced table`
|
|
730
730
|
: !/^jsonb?$/.test(col.type) ? `${read.column} is a ${col.type}, not a json or jsonb`
|
|
731
731
|
: isKeyRead(read) && col.type !== "jsonb" ? `${read.column} is a ${col.type}, which has no order or equality (a jsonb's is stored)`
|
|
732
|
-
: cols.some((c) => c.name.toLowerCase() === name) ? `the
|
|
732
|
+
: cols.some((c) => c.name.toLowerCase() === name) ? `the synced table has a column named ${name} of its own` : null;
|
|
733
733
|
if (why === null)
|
|
734
734
|
return true;
|
|
735
735
|
const key = pathKey(read);
|
|
@@ -879,7 +879,9 @@ export class KeptJsonPaths {
|
|
|
879
879
|
const of = read < marked ? ` (of the first ${read.toLocaleString("en-US")} read, of ${marked.toLocaleString("en-US")})` : "";
|
|
880
880
|
const said = [];
|
|
881
881
|
if (uncovered > 0)
|
|
882
|
-
said.push(
|
|
882
|
+
said.push(libc?.unread === true
|
|
883
|
+
? `the database's collation is one Mason could not read, so a sort of ${uncovered.toLocaleString("en-US")} row(s) is refused by name (Mason gap libc-locale-unread)`
|
|
884
|
+
: `${uncovered.toLocaleString("en-US")} row(s) hold a character the database's libc order is not measured for, so a sort of them is refused by name (Mason gap libc-uncovered)`);
|
|
883
885
|
if (read > uncovered)
|
|
884
886
|
said.push(`${(read - uncovered).toLocaleString("en-US")} row(s) its function could not read are computed from their document when read (a Mason bug: report it, with mason logs)`);
|
|
885
887
|
return said.join("; ") + of;
|