@workerstack/database 0.21.0 → 0.22.1
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 +243 -40
- package/docs/meta-contract.md +55 -1
- package/package.json +3 -7
- package/src/cli/args.ts +4 -2
- package/src/cli/codegen.ts +2 -2
- package/src/cli/comb-config.ts +8 -3
- package/src/cli/file-path.ts +25 -0
- package/src/cli/migrate-apply.ts +5 -21
- package/src/cli/migrate-d1.ts +5 -1
- package/src/cli/migrate-json.ts +48 -0
- package/src/cli/migrate.ts +26 -4
- package/src/codegen/analyzer-types.ts +6 -1
- package/src/codegen/annotations.ts +66 -9
- package/src/codegen/format-constraints.ts +114 -0
- package/src/codegen/generators/dtos/entity-file.ts +2 -1
- package/src/codegen/generators/dtos/example-meta.ts +22 -2
- package/src/codegen/generators/dtos/field-schema.ts +31 -8
- package/src/codegen/generators/dtos/ip-patterns.ts +25 -11
- package/src/codegen/generators/dtos/number-range.ts +22 -3
- package/src/codegen/generators/dtos/schema-aliases.ts +5 -3
- package/src/codegen/generators/dtos/text-format.ts +63 -0
- package/src/codegen/generators/dtos/text-schema.ts +49 -2
- package/src/codegen/generators/manifest.ts +9 -1
- package/src/codegen/runtime/read-comb.ts +71 -3
- package/src/codegen/runtime/read-tables.ts +31 -19
- package/src/codegen/runtime/snapshot-types.ts +3 -1
- package/src/codegen/runtime/to-meta.ts +6 -3
- package/src/formats/catalog.ts +255 -0
- package/src/formats/iana-time-zone.ts +117 -0
- package/src/formats/iso3166-1.ts +78 -0
- package/src/formats/iso4217.ts +42 -0
- package/src/formats/iso639-1.ts +41 -0
- package/src/formats/uts35.ts +143 -0
- package/src/index.ts +5 -0
- package/src/meta/column.ts +22 -9
- package/src/migrate/core/apply.ts +63 -22
- package/src/migrate/core/atlas-apply-runs.ts +15 -26
- package/src/migrate/core/cleanup.ts +22 -10
- package/src/migrate/core/d1-engine-child.mjs +27 -11
- package/src/migrate/core/d1-history.ts +23 -26
- package/src/migrate/core/d1-wrangler-error.ts +24 -1
- package/src/migrate/core/pg-replay-commit.ts +41 -38
- package/src/migrate/core/pg-replay-drops.ts +17 -32
- package/src/migrate/core/pg-replay-hidden.ts +75 -0
- package/src/migrate/core/pg-replay-roles.ts +2 -4
- package/src/migrate/core/pg-replay.ts +21 -19
- package/src/migrate/core/pg-server-snapshot.ts +4 -24
- package/src/migrate/core/proof-readers.ts +80 -0
- package/src/migrate/core/proof-run.ts +19 -5
- package/src/migrate/core/proof-scope.ts +27 -14
- package/src/migrate/core/sql-code.ts +36 -5
- package/src/migrate/core/sql-scan.ts +7 -1
- package/src/migrate/core/sqlite-keywords.ts +70 -0
- package/src/migrate/core/stopped.ts +70 -0
- package/src/migrate/core/trigger-write.d.mts +7 -0
- package/src/migrate/core/trigger-write.mjs +42 -0
- package/src/pg/columns.ts +6 -4
- package/src/query/cursor-mint.ts +11 -18
- package/src/query/cursor-refusals.ts +22 -11
- package/src/query/descriptions.ts +4 -9
- package/src/query/filter-parse.ts +80 -22
- package/src/query/filter-validate.ts +10 -13
- package/src/query/filter.ts +40 -18
- package/src/query/list-extras.ts +7 -10
- package/src/query/list-schema.ts +0 -2
- package/src/query/merged.ts +3 -2
- package/src/query/pagination.ts +30 -6
- package/src/query/rounded-key.ts +5 -8
- package/src/query/schema-params.ts +12 -5
- package/src/query/select-validate.ts +7 -4
- package/src/query/sqlite/cursor-fit.ts +10 -2
- package/src/query/trusted.ts +7 -2
- package/src/sqlite/columns.ts +6 -4
- package/src/sqlite/constraints.ts +120 -6
- package/types/cli/args.d.ts.map +1 -1
- package/types/cli/comb-config.d.ts +5 -2
- package/types/cli/comb-config.d.ts.map +1 -1
- package/types/cli/file-path.d.ts +11 -0
- package/types/cli/file-path.d.ts.map +1 -0
- package/types/cli/migrate-apply.d.ts +1 -0
- package/types/cli/migrate-apply.d.ts.map +1 -1
- package/types/cli/migrate-d1.d.ts.map +1 -1
- package/types/cli/migrate-json.d.ts +4 -0
- package/types/cli/migrate-json.d.ts.map +1 -0
- package/types/cli/migrate.d.ts.map +1 -1
- package/types/codegen/analyzer-types.d.ts +6 -1
- package/types/codegen/analyzer-types.d.ts.map +1 -1
- package/types/codegen/annotations.d.ts +12 -7
- package/types/codegen/annotations.d.ts.map +1 -1
- package/types/codegen/format-constraints.d.ts +22 -0
- package/types/codegen/format-constraints.d.ts.map +1 -0
- package/types/codegen/generators/dtos/entity-file.d.ts.map +1 -1
- package/types/codegen/generators/dtos/example-meta.d.ts.map +1 -1
- package/types/codegen/generators/dtos/field-schema.d.ts.map +1 -1
- package/types/codegen/generators/dtos/ip-patterns.d.ts +7 -0
- package/types/codegen/generators/dtos/ip-patterns.d.ts.map +1 -1
- package/types/codegen/generators/dtos/number-range.d.ts +8 -0
- package/types/codegen/generators/dtos/number-range.d.ts.map +1 -1
- package/types/codegen/generators/dtos/schema-aliases.d.ts +1 -1
- package/types/codegen/generators/dtos/schema-aliases.d.ts.map +1 -1
- package/types/codegen/generators/dtos/text-format.d.ts +31 -0
- package/types/codegen/generators/dtos/text-format.d.ts.map +1 -0
- package/types/codegen/generators/dtos/text-schema.d.ts +8 -1
- package/types/codegen/generators/dtos/text-schema.d.ts.map +1 -1
- package/types/codegen/generators/manifest.d.ts +5 -0
- package/types/codegen/generators/manifest.d.ts.map +1 -1
- package/types/codegen/runtime/read-comb.d.ts +17 -3
- package/types/codegen/runtime/read-comb.d.ts.map +1 -1
- package/types/codegen/runtime/read-tables.d.ts.map +1 -1
- package/types/codegen/runtime/snapshot-types.d.ts +3 -1
- package/types/codegen/runtime/snapshot-types.d.ts.map +1 -1
- package/types/codegen/runtime/to-meta.d.ts.map +1 -1
- package/types/formats/catalog.d.ts +51 -0
- package/types/formats/catalog.d.ts.map +1 -0
- package/types/formats/iana-time-zone.d.ts +7 -0
- package/types/formats/iana-time-zone.d.ts.map +1 -0
- package/types/formats/iso3166-1.d.ts +12 -0
- package/types/formats/iso3166-1.d.ts.map +1 -0
- package/types/formats/iso4217.d.ts +7 -0
- package/types/formats/iso4217.d.ts.map +1 -0
- package/types/formats/iso639-1.d.ts +6 -0
- package/types/formats/iso639-1.d.ts.map +1 -0
- package/types/formats/uts35.d.ts +13 -0
- package/types/formats/uts35.d.ts.map +1 -0
- package/types/index.d.ts +5 -0
- package/types/index.d.ts.map +1 -1
- package/types/meta/column.d.ts +7 -1
- package/types/meta/column.d.ts.map +1 -1
- package/types/migrate/core/apply.d.ts +3 -2
- package/types/migrate/core/apply.d.ts.map +1 -1
- package/types/migrate/core/atlas-apply-runs.d.ts +6 -0
- package/types/migrate/core/atlas-apply-runs.d.ts.map +1 -1
- package/types/migrate/core/cleanup.d.ts +16 -4
- package/types/migrate/core/cleanup.d.ts.map +1 -1
- package/types/migrate/core/d1-history.d.ts.map +1 -1
- package/types/migrate/core/d1-wrangler-error.d.ts +11 -0
- package/types/migrate/core/d1-wrangler-error.d.ts.map +1 -1
- package/types/migrate/core/pg-replay-commit.d.ts +14 -9
- package/types/migrate/core/pg-replay-commit.d.ts.map +1 -1
- package/types/migrate/core/pg-replay-drops.d.ts +7 -12
- package/types/migrate/core/pg-replay-drops.d.ts.map +1 -1
- package/types/migrate/core/pg-replay-hidden.d.ts +36 -0
- package/types/migrate/core/pg-replay-hidden.d.ts.map +1 -0
- package/types/migrate/core/pg-replay-roles.d.ts +2 -4
- package/types/migrate/core/pg-replay-roles.d.ts.map +1 -1
- package/types/migrate/core/pg-replay.d.ts.map +1 -1
- package/types/migrate/core/pg-server-snapshot.d.ts +4 -11
- package/types/migrate/core/pg-server-snapshot.d.ts.map +1 -1
- package/types/migrate/core/proof-readers.d.ts +19 -0
- package/types/migrate/core/proof-readers.d.ts.map +1 -0
- package/types/migrate/core/proof-run.d.ts.map +1 -1
- package/types/migrate/core/proof-scope.d.ts +15 -7
- package/types/migrate/core/proof-scope.d.ts.map +1 -1
- package/types/migrate/core/sql-code.d.ts +7 -3
- package/types/migrate/core/sql-code.d.ts.map +1 -1
- package/types/migrate/core/sql-scan.d.ts.map +1 -1
- package/types/migrate/core/sqlite-keywords.d.ts +11 -0
- package/types/migrate/core/sqlite-keywords.d.ts.map +1 -0
- package/types/migrate/core/stopped.d.ts +23 -0
- package/types/migrate/core/stopped.d.ts.map +1 -0
- package/types/pg/columns.d.ts +3 -3
- package/types/pg/columns.d.ts.map +1 -1
- package/types/query/cursor-mint.d.ts.map +1 -1
- package/types/query/cursor-refusals.d.ts +17 -1
- package/types/query/cursor-refusals.d.ts.map +1 -1
- package/types/query/descriptions.d.ts +1 -1
- package/types/query/descriptions.d.ts.map +1 -1
- package/types/query/filter-parse.d.ts +15 -4
- package/types/query/filter-parse.d.ts.map +1 -1
- package/types/query/filter-validate.d.ts +1 -1
- package/types/query/filter-validate.d.ts.map +1 -1
- package/types/query/filter.d.ts +9 -1
- package/types/query/filter.d.ts.map +1 -1
- package/types/query/list-extras.d.ts.map +1 -1
- package/types/query/list-schema.d.ts.map +1 -1
- package/types/query/merged.d.ts.map +1 -1
- package/types/query/pagination.d.ts +13 -1
- package/types/query/pagination.d.ts.map +1 -1
- package/types/query/rounded-key.d.ts +2 -11
- package/types/query/rounded-key.d.ts.map +1 -1
- package/types/query/schema-params.d.ts.map +1 -1
- package/types/query/select-validate.d.ts.map +1 -1
- package/types/query/sqlite/cursor-fit.d.ts.map +1 -1
- package/types/query/trusted.d.ts +1 -1
- package/types/query/trusted.d.ts.map +1 -1
- package/types/sqlite/columns.d.ts +3 -3
- package/types/sqlite/columns.d.ts.map +1 -1
- package/types/sqlite/constraints.d.ts +117 -5
- package/types/sqlite/constraints.d.ts.map +1 -1
- package/src/sqlite/patterns.ts +0 -19
- package/types/sqlite/patterns.d.ts +0 -19
- package/types/sqlite/patterns.d.ts.map +0 -1
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* The server's shared catalogs read as committed, in one query: what a bare
|
|
3
3
|
* statement changed, which no transaction counts, and what a second connection committed, which this
|
|
4
4
|
* backend's counts never see (pg-server-writes.ts). The read is the server's, another session's work
|
|
5
|
-
* in it too, so each row carries the names of the objects it is about
|
|
6
|
-
*
|
|
5
|
+
* in it too, so each row carries the names of the objects it is about: a change is a file's when it
|
|
6
|
+
* is to the replay's own database, or to a name the file spells (namesIn in sql-code.ts) beside a
|
|
7
|
+
* statement that can run code comb cannot read (pg-replay-hidden.ts). A replay's own rows are left out, but
|
|
7
8
|
* for the fields of its own database's row that keep the cleanup from dropping it, which only that
|
|
8
9
|
* replay changes. Beside the catalogs, the server's replication slots, which no catalog
|
|
9
10
|
* holds.
|
|
@@ -142,27 +143,6 @@ function snapshotChanges(before: Snapshot, after: Snapshot, names?: ReadonlySet<
|
|
|
142
143
|
return changes
|
|
143
144
|
}
|
|
144
145
|
|
|
145
|
-
/** What the cleanup drops, by the noun its subject starts with */
|
|
146
|
-
type Strays = { databases: string[]; roles: string[]; slots: string[] }
|
|
147
|
-
|
|
148
|
-
// lookup-table: the nouns of what a file adds that the cleanup drops, by where it keeps them
|
|
149
|
-
const DROPPED: ReadonlyArray<[keyof Strays, string]> = [
|
|
150
|
-
["databases", "database "],
|
|
151
|
-
["roles", "role "],
|
|
152
|
-
["slots", "replication slot "],
|
|
153
|
-
]
|
|
154
|
-
|
|
155
|
-
/** The roles, databases and replication slots named in `names` that `after` holds and `before` does not */
|
|
156
|
-
function added(before: Snapshot, after: Snapshot, names: ReadonlySet<string>): Strays {
|
|
157
|
-
const out: Strays = { databases: [], roles: [], slots: [] }
|
|
158
|
-
for (const subject of after.keys()) {
|
|
159
|
-
if (before.has(subject) || !about(subject, { after, before }, names)) continue
|
|
160
|
-
const [where, noun] = DROPPED.find(([, prefix]) => subject.startsWith(prefix)) ?? []
|
|
161
|
-
if (where !== undefined) out[where].push(subject.slice(noun?.length))
|
|
162
|
-
}
|
|
163
|
-
return out
|
|
164
|
-
}
|
|
165
|
-
|
|
166
146
|
/** The subscriptions `after` holds in `database` and `before` does not, by subject: its drop takes them */
|
|
167
147
|
const subscriptionsIn = (before: Snapshot, after: Snapshot, database: string): string[] =>
|
|
168
148
|
[...after]
|
|
@@ -171,4 +151,4 @@ const subscriptionsIn = (before: Snapshot, after: Snapshot, database: string): s
|
|
|
171
151
|
)
|
|
172
152
|
.map(([subject]) => subject)
|
|
173
153
|
|
|
174
|
-
export {
|
|
154
|
+
export { prepareSnapshot, type Snapshot, snapshot, snapshotChanges, subscriptionsIn }
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a file leaves unable to run: SQLite's DROP TABLE and DROP VIEW check no view or trigger that names what they
|
|
3
|
+
* drop, and SQLite, D1 and libsql resolve those names only when a statement runs. So a file can apply and leave every
|
|
4
|
+
* read of a view, or every write a trigger fires on, failing. After a file applies to the proof's copy, each view and
|
|
5
|
+
* trigger that survives it and names an object the file dropped or changed is compiled: a view's read, a trigger's
|
|
6
|
+
* own write on its table or view (trigger-write.mjs). SQLite resolves a name only by name, and the copy holds every
|
|
7
|
+
* reader that names a table a file reaches (proof-scope.ts), so this finds every reader the file breaks.
|
|
8
|
+
*/
|
|
9
|
+
import type { LocalDb } from "../drivers/local-db.ts"
|
|
10
|
+
import type { LiveObject } from "./proof-copy.ts"
|
|
11
|
+
import { referencesOf } from "./proof-scope.ts"
|
|
12
|
+
import { asciiFold } from "./sql-code.ts"
|
|
13
|
+
import { quote, triggerEvent, writeOf } from "./trigger-write.mjs"
|
|
14
|
+
|
|
15
|
+
/** The copy's schema, each object by type and folded name */
|
|
16
|
+
function schemaOf(db: LocalDb): Map<string, LiveObject> {
|
|
17
|
+
const rows = db.all("SELECT type, name, sql, tbl_name FROM main.sqlite_master WHERE sql IS NOT NULL")
|
|
18
|
+
return new Map(
|
|
19
|
+
rows.map((row) => {
|
|
20
|
+
const object = {
|
|
21
|
+
name: String(row["name"]),
|
|
22
|
+
sql: String(row["sql"]),
|
|
23
|
+
table: String(row["tbl_name"]),
|
|
24
|
+
type: String(row["type"]),
|
|
25
|
+
}
|
|
26
|
+
return [`${object.type} ${asciiFold(object.name)}`, object]
|
|
27
|
+
}),
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** SQLite's error compiling `sql`, or undefined */
|
|
32
|
+
function compileError(db: LocalDb, sql: string): string | undefined {
|
|
33
|
+
try {
|
|
34
|
+
db.all(`EXPLAIN ${sql}`)
|
|
35
|
+
return undefined
|
|
36
|
+
} catch (error) {
|
|
37
|
+
// reason: what SQLite refuses to compile is the finding
|
|
38
|
+
return (error as Error).message
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Why `reader`, a view or a trigger, fails on `db`, or undefined */
|
|
43
|
+
function brokenRead(db: LocalDb, reader: LiveObject): string | undefined {
|
|
44
|
+
if (reader.type === "view") {
|
|
45
|
+
const error = compileError(db, `SELECT * FROM ${quote(reader.name)}`)
|
|
46
|
+
return error === undefined ? undefined : `view ${reader.name} cannot be read: ${error}`
|
|
47
|
+
}
|
|
48
|
+
const event = triggerEvent(reader.sql)
|
|
49
|
+
const columns = db
|
|
50
|
+
.all(`PRAGMA main.table_xinfo(${quote(reader.table)})`)
|
|
51
|
+
.filter((row) => row["hidden"] === 0)
|
|
52
|
+
.map((row) => String(row["name"]))
|
|
53
|
+
const error = compileError(db, writeOf(reader.table, event, columns))
|
|
54
|
+
return error === undefined
|
|
55
|
+
? undefined
|
|
56
|
+
: `every ${event} on ${reader.table} fails, as a trigger it fires cannot compile: ${error}`
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Throws when a file that turned the copy's schema `before` into `db`'s leaves a view or trigger unable to run,
|
|
61
|
+
* naming it and SQLite's error: no approval overrides, since every read or write through it would fail
|
|
62
|
+
*/
|
|
63
|
+
function refuseBrokenReaders(db: LocalDb, before: ReadonlyMap<string, LiveObject>): void {
|
|
64
|
+
const after = schemaOf(db)
|
|
65
|
+
const touched = new Set(
|
|
66
|
+
[...before]
|
|
67
|
+
.filter(([key, object]) => after.get(key)?.sql !== object.sql)
|
|
68
|
+
.map(([, object]) => asciiFold(object.name)),
|
|
69
|
+
)
|
|
70
|
+
if (touched.size === 0) return
|
|
71
|
+
for (const reader of after.values()) {
|
|
72
|
+
if (reader.type !== "view" && reader.type !== "trigger") continue
|
|
73
|
+
if (![...referencesOf(reader)].some((name) => touched.has(name))) continue
|
|
74
|
+
const broken = brokenRead(db, reader)
|
|
75
|
+
if (broken !== undefined)
|
|
76
|
+
throw new Error(`after this file, ${broken}; drop or change it in this file, or keep what it uses`)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export { refuseBrokenReaders, schemaOf }
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
* Seeded rows cannot meet every predicate a file holds, so what each statement writes is read from
|
|
13
13
|
* SQLite itself as it runs (proof-engine.ts): an update of a column that was there before, a
|
|
14
14
|
* delete, a trigger fired or a temp shadow is unsafe whichever rows it meets, and a backfill of the
|
|
15
|
-
* file's own columns is cautious. A file the copy cannot run is unproven, and unproven is unsafe.
|
|
15
|
+
* file's own columns is cautious. A file the copy cannot run is unproven, and unproven is unsafe. A
|
|
16
|
+
* file that leaves a view or trigger unable to run is refused outright (proof-readers.ts).
|
|
16
17
|
*/
|
|
17
18
|
import { isMirror, mirrorFindings, mirrorRows } from "./proof-mirror.ts"
|
|
18
19
|
import { type LocalDb, openScratch, type ScratchEngine } from "../drivers/local-db.ts"
|
|
@@ -21,6 +22,7 @@ import { notEngineTables } from "./engine-tables.ts"
|
|
|
21
22
|
import { compare, type Shape } from "./proof-compare.ts"
|
|
22
23
|
import { type Copy, copyOf, buildCopy, creatable, jsonColumns, type LiveObject, liveObjects } from "./proof-copy.ts"
|
|
23
24
|
import { applyToCopy, engineFindings } from "./proof-engine.ts"
|
|
25
|
+
import { refuseBrokenReaders, schemaOf } from "./proof-readers.ts"
|
|
24
26
|
import { quote } from "./proof-seed.ts"
|
|
25
27
|
import { observable } from "./proof-scope.ts"
|
|
26
28
|
import { asciiFold } from "./sql-code.ts"
|
|
@@ -174,7 +176,10 @@ function snapshotAfter(db: Copy["db"]): Map<string, Shape> | string {
|
|
|
174
176
|
}
|
|
175
177
|
}
|
|
176
178
|
|
|
177
|
-
/**
|
|
179
|
+
/**
|
|
180
|
+
* The proof of file `name`, which applied as `run`, from the copy's tables, mirrored rows and schema before it and its
|
|
181
|
+
* tables after; throws, naming the file, when it leaves a view or trigger unable to run, which no approval overrides
|
|
182
|
+
*/
|
|
178
183
|
function appliedProof(
|
|
179
184
|
copy: Copy,
|
|
180
185
|
run: ReturnType<typeof applyToCopy>,
|
|
@@ -182,8 +187,17 @@ function appliedProof(
|
|
|
182
187
|
after,
|
|
183
188
|
before,
|
|
184
189
|
mirrors,
|
|
185
|
-
|
|
190
|
+
name,
|
|
191
|
+
schema,
|
|
192
|
+
}: {
|
|
193
|
+
after: Map<string, Shape>
|
|
194
|
+
before: Map<string, Shape>
|
|
195
|
+
mirrors: ReturnType<typeof mirrorRows>
|
|
196
|
+
name: string
|
|
197
|
+
schema: ReturnType<typeof schemaOf>
|
|
198
|
+
},
|
|
186
199
|
): Proof {
|
|
200
|
+
named(name, () => refuseBrokenReaders(copy.db, schema))
|
|
187
201
|
const found = compare(copy.db, new Map([...before].filter(([table]) => !copy.unproven.has(table))), after)
|
|
188
202
|
const steps = engineFindings(run.steps, run.start)
|
|
189
203
|
return {
|
|
@@ -228,7 +242,7 @@ async function provePending(
|
|
|
228
242
|
proofs.set(name, failedProof(copy, failed))
|
|
229
243
|
continue
|
|
230
244
|
}
|
|
231
|
-
let [before, mirrors] = [snapshot(copy.db), mirrorRows(copy.db)]
|
|
245
|
+
let [before, mirrors, schema] = [snapshot(copy.db), mirrorRows(copy.db), schemaOf(copy.db)]
|
|
232
246
|
let run = named(name, () => applyToCopy(copy.db, sql))
|
|
233
247
|
const retry = run.error === undefined ? undefined : await reseeded(objects, applied, making)
|
|
234
248
|
if (retry !== undefined) {
|
|
@@ -263,7 +277,7 @@ async function provePending(
|
|
|
263
277
|
failed = `an earlier pending file, ${name}, left a table SQLite cannot read, so this one is unproven`
|
|
264
278
|
continue
|
|
265
279
|
}
|
|
266
|
-
proofs.set(name, appliedProof(copy, run, { after, before, mirrors }))
|
|
280
|
+
proofs.set(name, appliedProof(copy, run, { after, before, mirrors, name, schema }))
|
|
267
281
|
}
|
|
268
282
|
} finally {
|
|
269
283
|
copy.db.close()
|
|
@@ -3,15 +3,17 @@
|
|
|
3
3
|
* can observe, created, and the tables whose rows they can observe, seeded, so the copy refuses what the whole
|
|
4
4
|
* schema would and costs what the files reach.
|
|
5
5
|
*
|
|
6
|
-
* - An object's references are every name its text spells (sql-code's lexer,
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* - An object's references are every name its text spells where SQLite's grammar takes a name (sql-code's lexer,
|
|
7
|
+
* compared folded, never as a substring: the `TABLE` of `CREATE TABLE` is no name) and, for a virtual table, each
|
|
8
|
+
* word, string and quoted name among its module's arguments, whole. A table's FK parents are the other tables it
|
|
9
|
+
* references; a reader is a view, a trigger or a virtual table, and reads what it references.
|
|
9
10
|
* - N: each live object whose name a file spells, and the history table; S is N's tables, which a file creates,
|
|
10
11
|
* alters, drops, renames or writes.
|
|
11
12
|
* - Rows go to S, each table a reader reached from S references (to a fixpoint), D (their FK descendants: a cascade
|
|
12
13
|
* reaches them) and A (the FK ancestors of all those: their keys must be met).
|
|
13
14
|
* - The copy creates N, the seeded tables, and each reader that references an S table (SQLite checks views and
|
|
14
|
-
* triggers on DROP COLUMN and RENAME COLUMN, and a rename rewrites them)
|
|
15
|
+
* triggers on DROP COLUMN and RENAME COLUMN, and a rename rewrites them) or a view of N (a file that drops the
|
|
16
|
+
* view leaves the reader unable to run, which the proof compiles it to see). Then to a fixpoint each index and trigger
|
|
15
17
|
* on a created table or view (a view's INSTEAD OF triggers carry out a write through it), each table and view a
|
|
16
18
|
* created reader references, and each created table's FK ancestors, which a write to it compiles checks against.
|
|
17
19
|
* - Reflection reads the schema itself: a name with SQLite's reserved `sqlite_` prefix (its catalogs, sqlite_sequence,
|
|
@@ -66,24 +68,31 @@ const nameOf = ({ kind, text }: Token): string =>
|
|
|
66
68
|
asciiFold(kind === "literal" ? text.slice(1, -1).replaceAll("''", "'") : unquote(text))
|
|
67
69
|
|
|
68
70
|
/**
|
|
69
|
-
* Virtual table `object`'s module, the name after USING however it is written,
|
|
70
|
-
* the module's arguments, which the module may read as a table's name (fts5vocab, content=)
|
|
71
|
+
* Virtual table `object`'s module, the name after USING however it is written, each string and quoted name among
|
|
72
|
+
* the module's arguments, which the module may read as a table's name (fts5vocab, content=), and each bare word there
|
|
71
73
|
*/
|
|
72
|
-
function virtualOf(object: LiveObject): { args: string[]; module: string } | undefined {
|
|
74
|
+
function virtualOf(object: LiveObject): { args: string[]; module: string; words: string[] } | undefined {
|
|
73
75
|
if (!isVirtual(object)) return undefined
|
|
74
76
|
const tokens = tokensOf(object.sql)
|
|
75
77
|
const at = tokens.findIndex(({ kind, text }) => kind === "code" && /^using$/i.test(text)) + 1
|
|
76
|
-
const
|
|
77
|
-
return {
|
|
78
|
+
const inside = tokens[at + 1]?.text === "(" ? tokens.slice(at + 2) : []
|
|
79
|
+
return {
|
|
80
|
+
args: inside.filter(({ kind }) => kind !== "code").map(nameOf),
|
|
81
|
+
module: nameOf(tokens[at] as Token),
|
|
82
|
+
words: inside.filter(({ kind, text }) => kind === "code" && /^[A-Za-z_\u0080-\uffff]/.test(text)).map(nameOf),
|
|
83
|
+
}
|
|
78
84
|
}
|
|
79
85
|
|
|
80
86
|
/** The folded names of the shadow tables virtual table `object`'s module makes */
|
|
81
87
|
const shadowsOf = (object: LiveObject): string[] =>
|
|
82
88
|
(SHADOWS[virtualOf(object)?.module as string] ?? []).map((suffix) => asciiFold(`${object.name}_${suffix}`))
|
|
83
89
|
|
|
84
|
-
/**
|
|
90
|
+
/**
|
|
91
|
+
* Every name `object` references, folded (see the module's comment). A virtual table's module reads its own arguments,
|
|
92
|
+
* not SQL, so each bare word among them may name a table, `group` in `content=group` among them
|
|
93
|
+
*/
|
|
85
94
|
const referencesOf = (object: LiveObject): Set<string> =>
|
|
86
|
-
new Set([...sqliteNames(object.sql), ...(virtualOf(object)?.args ?? [])])
|
|
95
|
+
new Set([...sqliteNames(object.sql), ...(virtualOf(object)?.args ?? []), ...(virtualOf(object)?.words ?? [])])
|
|
87
96
|
|
|
88
97
|
/* a name SQLite reserves for its own tables, or a pragma's table-valued function: either reads the schema itself */
|
|
89
98
|
const REFLECTION = /^(?:sqlite_|pragma_)/
|
|
@@ -216,9 +225,13 @@ function observable(
|
|
|
216
225
|
const statements = files.flatMap(({ sql }) => splitSqlStatements(sql))
|
|
217
226
|
const checks = objects.filter((object) => object.type === "view" || object.type === "trigger")
|
|
218
227
|
/* a rename rewrites the text of each view and trigger naming an S table, and D1 then compiles the writes to its
|
|
219
|
-
table; a virtual table reading one reads the rows a file writes */
|
|
228
|
+
table; a virtual table reading one reads the rows a file writes; a drop of a view of N breaks each reading it */
|
|
229
|
+
const reached = new Set([
|
|
230
|
+
...s,
|
|
231
|
+
...n.filter((object) => object.type === "view").map((object) => asciiFold(object.name)),
|
|
232
|
+
])
|
|
220
233
|
const rewritten = objects.filter(
|
|
221
|
-
(object) => isReader(object) && [...(graph.names.get(object) as Set<string>)].some((name) =>
|
|
234
|
+
(object) => isReader(object) && [...(graph.names.get(object) as Set<string>)].some((name) => reached.has(name)),
|
|
222
235
|
)
|
|
223
236
|
/* a write to a created table compiles its keys' checks, which need each parent table */
|
|
224
237
|
let made = forward(graph, [...n, ...objectsOf(graph, seeded), ...rewritten])
|
|
@@ -240,4 +253,4 @@ function observable(
|
|
|
240
253
|
return { objects: kept, seeded }
|
|
241
254
|
}
|
|
242
255
|
|
|
243
|
-
export { isVirtual, observable, type Scope, shadowsOf, virtualOf }
|
|
256
|
+
export { isVirtual, observable, referencesOf, type Scope, shadowsOf, virtualOf }
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* an action list splits only at its top-level commas.
|
|
5
5
|
*/
|
|
6
6
|
import type { Dialect } from "../types.ts"
|
|
7
|
-
import { bodyText,
|
|
7
|
+
import { bodyText, segments } from "../../sql-lexer.ts"
|
|
8
|
+
import { NEVER_NAMES } from "./sqlite-keywords.ts"
|
|
8
9
|
|
|
9
10
|
/** One name part: double-quoted, backtick or bracket quoted, or bare */
|
|
10
11
|
const IDENT: string = `(?:"(?:[^"]|"")*"|\`[^\`]*\`|\\[[^\\]]*\\]|[A-Za-z_\\u0080-\\uffff][\\w$\\u0080-\\uffff]*)`
|
|
@@ -100,13 +101,43 @@ function pragmaOf(code: string): Pragma | undefined {
|
|
|
100
101
|
const qualifiedKey = (name: string): string =>
|
|
101
102
|
[...name.matchAll(PARTS)].map((match) => partKey(match[0], "sqlite")).join(".")
|
|
102
103
|
|
|
104
|
+
/* the words and punctuation of SQLite code: a bare word, or one character that breaks a run of words */
|
|
105
|
+
const CODE_TOKEN = /[A-Za-z_\u0080-\uffff][\w$\u0080-\uffff]*|\S/g
|
|
106
|
+
|
|
103
107
|
/**
|
|
104
|
-
* Every name SQLite code can spell, as SQLite resolves one: each
|
|
105
|
-
*
|
|
106
|
-
*
|
|
108
|
+
* Every name SQLite code can spell, as SQLite resolves one: each quoted name unquoted, each word of a string, which
|
|
109
|
+
* SQLite reads as a name where one stands (`FROM 'post'`) and a module as its option's (`content='post'`), and each
|
|
110
|
+
* bare word that can be one, all folded by asciiFold; never a word of a comment. A bare keyword is no name
|
|
111
|
+
* where SQLite's grammar takes none: `TABLE`, `CREATE`, `NULL` and the rest of NEVER_NAMES whenever it is bare, and
|
|
112
|
+
* `KEY` after `PRIMARY` or `FOREIGN`, which no name can follow. So a table named `table` is named by `"table"` and
|
|
113
|
+
* not by the `CREATE TABLE` of every file. Any other bare word may be a name and is read as one: a name read
|
|
114
|
+
* where there is none only widens what a proof copies, a name missed would narrow it
|
|
107
115
|
*/
|
|
108
116
|
function sqliteNames(sql: string): Set<string> {
|
|
109
|
-
|
|
117
|
+
const names = new Set<string>()
|
|
118
|
+
const quoted = (text: string) => {
|
|
119
|
+
for (const match of text.matchAll(PARTS)) names.add(partKey(match[0], "sqlite"))
|
|
120
|
+
}
|
|
121
|
+
let before: string | undefined
|
|
122
|
+
for (const { kind, text } of segments(sql, "sqlite")) {
|
|
123
|
+
if (kind === "comment") continue
|
|
124
|
+
if (kind !== "code") {
|
|
125
|
+
quoted(text)
|
|
126
|
+
before = undefined
|
|
127
|
+
continue
|
|
128
|
+
}
|
|
129
|
+
for (const [token] of text.matchAll(CODE_TOKEN)) {
|
|
130
|
+
if (!/^[A-Za-z_\u0080-\uffff]/.test(token)) {
|
|
131
|
+
before = undefined
|
|
132
|
+
continue
|
|
133
|
+
}
|
|
134
|
+
const word = asciiFold(token)
|
|
135
|
+
const keyword = NEVER_NAMES.has(word) || (word === "key" && (before === "primary" || before === "foreign"))
|
|
136
|
+
if (!keyword) names.add(word)
|
|
137
|
+
before = word
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return names
|
|
110
141
|
}
|
|
111
142
|
|
|
112
143
|
/* a bare word in Postgres code, which may be a name */
|
|
@@ -67,9 +67,15 @@ function readCode(reading: Reading, text: string): string[] {
|
|
|
67
67
|
return done
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
/* SQLITE_SPACE's whitespace at either end of a statement */
|
|
71
|
+
const SQLITE_EDGE_SPACE = /^[\t\n\f\r ]+|[\t\n\f\r ]+$/g
|
|
72
|
+
|
|
70
73
|
/** The statement read so far, reset for the next; empty when it held no code */
|
|
71
74
|
function finish(reading: Reading): string {
|
|
72
|
-
const statement =
|
|
75
|
+
const statement =
|
|
76
|
+
reading.code.replace(/;/g, "").replace(SQLITE_EDGE_SPACE, "") === ""
|
|
77
|
+
? ""
|
|
78
|
+
: reading.text.replace(SQLITE_EDGE_SPACE, "")
|
|
73
79
|
reading.code = ""
|
|
74
80
|
reading.state = 0
|
|
75
81
|
reading.text = ""
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The words SQLite reads only as keywords when they stand bare in code: no table, column, index, view,
|
|
3
|
+
* trigger or alias can be named by one unless it is quoted. The other keywords (`key`, `end`, `action`, `left`,
|
|
4
|
+
* `if`) SQLite takes as a name where its grammar wants one, so a bare one may be a name and is read as one.
|
|
5
|
+
* The list is the engine's: tests/sqlite-keywords.test.ts holds every word here to SQLite's refusal as each kind of
|
|
6
|
+
* name, in node:sqlite's SQLite and libsql's. `nothing` and `returning` are left out, as an SQLite before 3.24 and
|
|
7
|
+
* 3.35 reads them as plain names.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// lookup-table: the keywords no bare name can be, lower case, by the engine's own refusal
|
|
11
|
+
const NEVER_NAMES: ReadonlySet<string> = /* @__PURE__ */ new Set([
|
|
12
|
+
"add",
|
|
13
|
+
"all",
|
|
14
|
+
"alter",
|
|
15
|
+
"and",
|
|
16
|
+
"as",
|
|
17
|
+
"autoincrement",
|
|
18
|
+
"between",
|
|
19
|
+
"case",
|
|
20
|
+
"check",
|
|
21
|
+
"collate",
|
|
22
|
+
"commit",
|
|
23
|
+
"constraint",
|
|
24
|
+
"create",
|
|
25
|
+
"default",
|
|
26
|
+
"deferrable",
|
|
27
|
+
"delete",
|
|
28
|
+
"distinct",
|
|
29
|
+
"drop",
|
|
30
|
+
"else",
|
|
31
|
+
"escape",
|
|
32
|
+
"except",
|
|
33
|
+
"exists",
|
|
34
|
+
"foreign",
|
|
35
|
+
"from",
|
|
36
|
+
"group",
|
|
37
|
+
"having",
|
|
38
|
+
"in",
|
|
39
|
+
"index",
|
|
40
|
+
"insert",
|
|
41
|
+
"intersect",
|
|
42
|
+
"into",
|
|
43
|
+
"is",
|
|
44
|
+
"isnull",
|
|
45
|
+
"join",
|
|
46
|
+
"limit",
|
|
47
|
+
"not",
|
|
48
|
+
"notnull",
|
|
49
|
+
"null",
|
|
50
|
+
"on",
|
|
51
|
+
"or",
|
|
52
|
+
"order",
|
|
53
|
+
"primary",
|
|
54
|
+
"references",
|
|
55
|
+
"select",
|
|
56
|
+
"set",
|
|
57
|
+
"table",
|
|
58
|
+
"then",
|
|
59
|
+
"to",
|
|
60
|
+
"transaction",
|
|
61
|
+
"union",
|
|
62
|
+
"unique",
|
|
63
|
+
"update",
|
|
64
|
+
"using",
|
|
65
|
+
"values",
|
|
66
|
+
"when",
|
|
67
|
+
"where",
|
|
68
|
+
])
|
|
69
|
+
|
|
70
|
+
export { NEVER_NAMES }
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one report of a run a signal stopped: which files applied and which did not, as an
|
|
3
|
+
* object both of a command's outputs render, the human line and the `--json` object. The loop that
|
|
4
|
+
* applies files holds a step for the length of its work; when a signal comes, the step reads what
|
|
5
|
+
* committed once the work in flight has ended, prints the line and returns the report, which the
|
|
6
|
+
* command's own step, held before the loop's and so run after it, reads and prints as JSON. A stop
|
|
7
|
+
* parks the loop itself (cleanup.ts pastStop), so a run it stopped never throws its own report.
|
|
8
|
+
*/
|
|
9
|
+
import { hold } from "./cleanup.ts"
|
|
10
|
+
import { Refused } from "./refused.ts"
|
|
11
|
+
|
|
12
|
+
/** The files a stop found applied by the run, and left pending */
|
|
13
|
+
type Found = { applied: string[]; pending: string[] }
|
|
14
|
+
|
|
15
|
+
/** A run a signal stopped: the signal, and as a Refused the files it applied, left, or may have applied */
|
|
16
|
+
export class Stopped extends Refused {
|
|
17
|
+
readonly signal: NodeJS.Signals
|
|
18
|
+
|
|
19
|
+
constructor(signal: NodeJS.Signals, message: string, files: ConstructorParameters<typeof Refused>[1]) {
|
|
20
|
+
super(message, files)
|
|
21
|
+
this.name = "Stopped"
|
|
22
|
+
this.signal = signal
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const list = (names: readonly string[]) => names.join(", ") || "none"
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The report of a run `signal` stopped, whose approved files are `names`: what `read` found, or,
|
|
30
|
+
* when it cannot read, every one of `names` as unknown, since any of them may have committed
|
|
31
|
+
*/
|
|
32
|
+
async function reportOf(
|
|
33
|
+
signal: NodeJS.Signals,
|
|
34
|
+
{ names, read }: { names: readonly string[]; read: () => Promise<Found> },
|
|
35
|
+
): Promise<Stopped> {
|
|
36
|
+
try {
|
|
37
|
+
const { applied, pending } = await read()
|
|
38
|
+
return new Stopped(signal, `Stopped by ${signal}: applied ${list(applied)}; not applied ${list(pending)}`, {
|
|
39
|
+
applied,
|
|
40
|
+
pending,
|
|
41
|
+
})
|
|
42
|
+
} catch (error) {
|
|
43
|
+
return new Stopped(
|
|
44
|
+
signal,
|
|
45
|
+
`Stopped by ${signal}: comb could not read what was applied: ${(error as Error).message}`,
|
|
46
|
+
{ pending: [], unknown: names },
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* `work`, with a step held while it runs that, on a signal, says on `print` which of `names` the run
|
|
53
|
+
* applied and which it did not, as `read` finds them once the work in flight has ended (cleanup.ts
|
|
54
|
+
* runs a child's own step before this one), and returns the report. The process then ends by the signal.
|
|
55
|
+
*/
|
|
56
|
+
export async function whileStoppable<T>(
|
|
57
|
+
{ names, print, read }: { names: readonly string[]; print: (line: string) => void; read: () => Promise<Found> },
|
|
58
|
+
work: () => Promise<T>,
|
|
59
|
+
): Promise<T> {
|
|
60
|
+
const release = hold(async (signal) => {
|
|
61
|
+
const stopped = await reportOf(signal, { names, read })
|
|
62
|
+
print(stopped.message)
|
|
63
|
+
return stopped
|
|
64
|
+
})
|
|
65
|
+
try {
|
|
66
|
+
return await work()
|
|
67
|
+
} finally {
|
|
68
|
+
release()
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** A write a table takes */
|
|
2
|
+
export type Event = "DELETE" | "INSERT" | "UPDATE"
|
|
3
|
+
|
|
4
|
+
export const EVENTS: readonly ["INSERT", "UPDATE", "DELETE"]
|
|
5
|
+
export function quote(name: string): string
|
|
6
|
+
export function triggerEvent(sql: string): Event
|
|
7
|
+
export function writeOf(table: string, event: Event, columns: readonly string[]): string
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The write that compiles a trigger: SQLite compiles a trigger's body into the program of each statement of its
|
|
3
|
+
* event on its table or view, so `EXPLAIN` of that statement resolves every name the body uses and every function
|
|
4
|
+
* it calls. A view takes a write only for an event an INSTEAD OF trigger on it fires on; for any other event SQLite
|
|
5
|
+
* answers "cannot modify ... because it is a view", and no program exists to fail. Plain JavaScript, shared by the
|
|
6
|
+
* D1 proof's child (d1-engine-child.mjs), which node runs untransformed, and the SQLite proof (proof-readers.ts).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** The writes a table takes, in the order the proofs compile them */
|
|
10
|
+
export const EVENTS = /** @type {const} */ (["INSERT", "UPDATE", "DELETE"])
|
|
11
|
+
|
|
12
|
+
/** @param {string} name */
|
|
13
|
+
export const quote = (name) => `"${name.replaceAll('"', '""')}"`
|
|
14
|
+
|
|
15
|
+
/* what in SQL text may spell any word: a comment, a string, a quoted name */
|
|
16
|
+
const HIDDEN = /--[^\n]*|\/\*[\s\S]*?(?:\*\/|$)|'(?:[^']|'')*'|"(?:[^"]|"")*"|`(?:[^`]|``)*`|\[[^\]]*\]/g
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The event of trigger text `sql`, as sqlite_master holds it: the first bare INSERT, UPDATE or DELETE outside a
|
|
20
|
+
* comment, a string and a quoted name. CREATE TRIGGER's grammar puts the event before ON and the body, and no bare
|
|
21
|
+
* name before it can be one of those keywords
|
|
22
|
+
* @param {string} sql
|
|
23
|
+
* @returns {"DELETE" | "INSERT" | "UPDATE"}
|
|
24
|
+
*/
|
|
25
|
+
export function triggerEvent(sql) {
|
|
26
|
+
const event = /** @type {RegExpExecArray} */ (/\b(DELETE|INSERT|UPDATE)\b/i.exec(sql.replace(HIDDEN, " ")))
|
|
27
|
+
return /** @type {"DELETE" | "INSERT" | "UPDATE"} */ (/** @type {string} */ (event[1]).toUpperCase())
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The statement of `event` on `table` that compiles each trigger it fires and each generated column it computes:
|
|
32
|
+
* an insert of defaults, an update assigning each of `columns` (the stored and virtual ones, by name) to itself, a
|
|
33
|
+
* delete
|
|
34
|
+
* @param {string} table
|
|
35
|
+
* @param {"DELETE" | "INSERT" | "UPDATE"} event
|
|
36
|
+
* @param {readonly string[]} columns
|
|
37
|
+
*/
|
|
38
|
+
export function writeOf(table, event, columns) {
|
|
39
|
+
if (event === "INSERT") return `INSERT INTO ${quote(table)} DEFAULT VALUES`
|
|
40
|
+
if (event === "DELETE") return `DELETE FROM ${quote(table)}`
|
|
41
|
+
return `UPDATE ${quote(table)} SET ${columns.map((column) => `${quote(column)} = ${quote(column)}`).join(", ")}`
|
|
42
|
+
}
|
package/src/pg/columns.ts
CHANGED
|
@@ -31,6 +31,8 @@ import type {
|
|
|
31
31
|
EnumOptions,
|
|
32
32
|
IntegerConstraints,
|
|
33
33
|
JsonConstraints,
|
|
34
|
+
RealConstraints,
|
|
35
|
+
RefConstraints,
|
|
34
36
|
TextConstraints,
|
|
35
37
|
TimestampConstraints,
|
|
36
38
|
} from "../sqlite/constraints.ts"
|
|
@@ -100,9 +102,9 @@ export type PgColumns = {
|
|
|
100
102
|
_constraints?: JsonConstraints,
|
|
101
103
|
) => Set$Type<PgJsonbBuilder, z.infer<TSchema>>
|
|
102
104
|
/** a floating-point number, stored as double precision */
|
|
103
|
-
real: (name: string, _constraints?:
|
|
105
|
+
real: (name: string, _constraints?: RealConstraints) => PgDoublePrecisionBuilder
|
|
104
106
|
/** a text foreign key; chain `.references()` to name the column it points at */
|
|
105
|
-
ref: (name: string, _constraints?:
|
|
107
|
+
ref: (name: string, _constraints?: RefConstraints) => Text
|
|
106
108
|
/** the auto-incrementing 4-byte `serial` primary key `id`, filled by the database */
|
|
107
109
|
serialId: () => SetIsPrimaryKey<PgSerialBuilder>
|
|
108
110
|
/**
|
|
@@ -158,11 +160,11 @@ export const c: PgColumns = {
|
|
|
158
160
|
withCombColumn(jsonb(name).$type<z.infer<TSchema>>(), { constraints, kind: "json", schema }),
|
|
159
161
|
|
|
160
162
|
/* Floating point (double precision) */
|
|
161
|
-
real: (name: string, constraints?:
|
|
163
|
+
real: (name: string, constraints?: RealConstraints) =>
|
|
162
164
|
withCombColumn(doublePrecision(name), { constraints, kind: "real" }),
|
|
163
165
|
|
|
164
166
|
/* Foreign key reference - use with .references() */
|
|
165
|
-
ref: (name: string, constraints?:
|
|
167
|
+
ref: (name: string, constraints?: RefConstraints) => withCombColumn(text(name), { constraints, kind: "ref" }),
|
|
166
168
|
|
|
167
169
|
/* Auto-increment integer primary key */
|
|
168
170
|
serialId: () => withCombColumn(serial("id").primaryKey(), { kind: "serialId" }),
|
package/src/query/cursor-mint.ts
CHANGED
|
@@ -3,13 +3,12 @@
|
|
|
3
3
|
* MAX_CURSOR_LENGTH, a value too long for it carried by reference (cursor-ref.ts) on a table's
|
|
4
4
|
* list, and refused on a derived list, which no row holds to read a reference back from.
|
|
5
5
|
*/
|
|
6
|
-
import { CombError } from "../error.ts"
|
|
7
|
-
import { combErrorKeys } from "../types.ts"
|
|
8
6
|
import { cursorBytes, CursorRef, MAX_CURSOR_LENGTH } from "./cursor.ts"
|
|
9
7
|
import { readsApart } from "./cursor-reads.ts"
|
|
10
8
|
import { type CursorOptions, seal, sealedLength } from "./cursor-seal.ts"
|
|
11
9
|
import { roundedKey } from "./rounded-key.ts"
|
|
12
10
|
import type { SortField } from "./types.ts"
|
|
11
|
+
import { UnmintableRow } from "./cursor-refusals.ts"
|
|
13
12
|
|
|
14
13
|
/**
|
|
15
14
|
* The open's column check a builder hands the mint: the index of the first value, the tiebreak id last,
|
|
@@ -37,7 +36,7 @@ function longestFitting(record: Record<string, unknown>, field: string, fits: (r
|
|
|
37
36
|
}
|
|
38
37
|
|
|
39
38
|
/** The overflow, as paginate reports it on the page that met it: a 500 naming the field and the cap */
|
|
40
|
-
function tooLong(fields: string, length: number, kind: KeysetKind | "unread"):
|
|
39
|
+
function tooLong(fields: string, length: number, kind: KeysetKind | "unread"): UnmintableRow {
|
|
41
40
|
const reasons = {
|
|
42
41
|
derived: "a derived list reads no row to page by reference, so sort it by a key of bounded length",
|
|
43
42
|
merged:
|
|
@@ -47,11 +46,9 @@ function tooLong(fields: string, length: number, kind: KeysetKind | "unread"): C
|
|
|
47
46
|
"its long key is a column not of the anchor table, or an expression over one, so no reference reads its value back; sort by a key of bounded length, or by a column of the anchor table",
|
|
48
47
|
}
|
|
49
48
|
const reason = reasons[kind]
|
|
50
|
-
return new
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
status: "internal_server_error",
|
|
54
|
-
})
|
|
49
|
+
return new UnmintableRow(
|
|
50
|
+
`a row's ${fields} is too long to page this list by: its cursor would be ${length} characters, past ${MAX_CURSOR_LENGTH}; ${reason}`,
|
|
51
|
+
)
|
|
55
52
|
}
|
|
56
53
|
|
|
57
54
|
/**
|
|
@@ -159,11 +156,9 @@ function refuseInvalidDate(
|
|
|
159
156
|
({ field }) => at.asRead(field) && row[field] instanceof Date && Number.isNaN((row[field] as Date).getTime()),
|
|
160
157
|
)
|
|
161
158
|
if (unheld === undefined) return
|
|
162
|
-
throw new
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
status: "internal_server_error",
|
|
166
|
-
})
|
|
159
|
+
throw new UnmintableRow(
|
|
160
|
+
`the sort by ${unheld.field} reads a date no JS Date holds (a Postgres 'infinity' read in date mode, or a SQLite timestamp holding ±Infinity), which this list has no row to read back by, ${at.derived ? "a derived list having none" : "its key being no column of the anchor table"}; select it in string mode or sort by another column`,
|
|
161
|
+
)
|
|
167
162
|
}
|
|
168
163
|
|
|
169
164
|
/** A 500 naming the first value of `record` its column does not hold (ColumnCheck), the cursor the next page would refuse */
|
|
@@ -179,11 +174,9 @@ function refuseUnfit(
|
|
|
179
174
|
const index = options.holds(values, (i) => options.carried?.has(sort[i]?.field ?? "") ?? false)
|
|
180
175
|
if (index === -1) return
|
|
181
176
|
const named = sort[index] === undefined ? `the tiebreak id ${idField}` : `the sort by ${sort[index].field}`
|
|
182
|
-
throw new
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
status: "internal_server_error",
|
|
186
|
-
})
|
|
177
|
+
throw new UnmintableRow(
|
|
178
|
+
`${named} reads a value its column's mode does not hold (a JSON-mode key's, or a SQLite column's of another storage class than its mode reads), which no cursor of this list resumes after; sort by another column`,
|
|
179
|
+
)
|
|
187
180
|
}
|
|
188
181
|
|
|
189
182
|
export { type ColumnCheck, createCursor, fitCursor, fittedCursor, type KeysetKind }
|