@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
|
@@ -9,10 +9,12 @@
|
|
|
9
9
|
* Then each file as wrangler sends it, the file and the history row it appends, through wrangler's
|
|
10
10
|
* `splitSqlQuery`, in one batch. D1 resolves a function in a trigger body, a view or a generated
|
|
11
11
|
* column only when a statement uses it, so EXPLAIN then compiles each view, and each table's INSERT,
|
|
12
|
-
* UPDATE and DELETE, the file made or changed
|
|
12
|
+
* UPDATE and DELETE, the file made or changed; a view takes only the writes its INSTEAD OF triggers fire
|
|
13
|
+
* on (trigger-write.mjs). What D1 refuses comes back in D1's own words.
|
|
13
14
|
*/
|
|
14
15
|
import { readFileSync } from "node:fs"
|
|
15
16
|
import { pathToFileURL } from "node:url"
|
|
17
|
+
import { EVENTS, quote, triggerEvent, writeOf } from "./trigger-write.mjs"
|
|
16
18
|
|
|
17
19
|
/**
|
|
18
20
|
* @typedef {{ all(): Promise<{ results?: Array<Record<string, unknown>> | undefined }> }} D1Statement
|
|
@@ -27,9 +29,6 @@ import { pathToFileURL } from "node:url"
|
|
|
27
29
|
* @typedef {import("./d1-engine-child.d.mts").ProofResult} ProofResult
|
|
28
30
|
*/
|
|
29
31
|
|
|
30
|
-
/** @param {string} name */
|
|
31
|
-
const quote = (name) => `"${name.replaceAll('"', '""')}"`
|
|
32
|
-
|
|
33
32
|
/**
|
|
34
33
|
* D1's own words for a failure: the cause wrangler's client wraps, when there is one
|
|
35
34
|
* @param {unknown} error
|
|
@@ -87,7 +86,8 @@ async function isRowTable(db, table) {
|
|
|
87
86
|
* @returns {Promise<string[]>}
|
|
88
87
|
*/
|
|
89
88
|
async function probes(db, before) {
|
|
90
|
-
const
|
|
89
|
+
const after = [...(await master(db))]
|
|
90
|
+
const changed = after.filter(([key, object]) => before.get(key)?.sql !== object.sql)
|
|
91
91
|
const views = changed.filter(([, object]) => object.type === "view").map(([, object]) => object.table)
|
|
92
92
|
const tables = new Set(changed.filter(([, object]) => object.type === "trigger").map(([, object]) => object.table))
|
|
93
93
|
for (const [, object] of changed)
|
|
@@ -96,16 +96,32 @@ async function probes(db, before) {
|
|
|
96
96
|
const writes = []
|
|
97
97
|
for (const table of tables) {
|
|
98
98
|
const { results = [] } = await db.prepare(`PRAGMA table_xinfo(${quote(table)})`).all()
|
|
99
|
-
const columns = results.filter((row) => row["hidden"] === 0).map((row) =>
|
|
100
|
-
writes.push(
|
|
101
|
-
`EXPLAIN INSERT INTO ${quote(table)} DEFAULT VALUES`,
|
|
102
|
-
`EXPLAIN UPDATE ${quote(table)} SET ${columns.map((column) => `${column} = ${column}`).join(", ")}`,
|
|
103
|
-
`EXPLAIN DELETE FROM ${quote(table)}`,
|
|
104
|
-
)
|
|
99
|
+
const columns = results.filter((row) => row["hidden"] === 0).map((row) => String(row["name"]))
|
|
100
|
+
for (const event of eventsOf(after, table)) writes.push(`EXPLAIN ${writeOf(table, event, columns)}`)
|
|
105
101
|
}
|
|
106
102
|
return [...views.map((view) => `EXPLAIN SELECT * FROM ${quote(view)}`), ...writes]
|
|
107
103
|
}
|
|
108
104
|
|
|
105
|
+
/**
|
|
106
|
+
* `name` as SQLite compares names, ASCII letters folded alone: a trigger's tbl_name is spelled as its ON clause was
|
|
107
|
+
* @param {string} name
|
|
108
|
+
*/
|
|
109
|
+
const fold = (name) => name.replace(/[A-Z]/g, (letter) => letter.toLowerCase())
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The writes `table` takes in the schema `objects`: a table every one, a view each its triggers fire on
|
|
113
|
+
* @param {ReadonlyArray<[string, SchemaObject]>} objects
|
|
114
|
+
* @param {string} table
|
|
115
|
+
*/
|
|
116
|
+
function eventsOf(objects, table) {
|
|
117
|
+
const on = objects.filter(([, object]) => fold(object.table) === fold(table))
|
|
118
|
+
if (!on.some(([, object]) => object.type === "view")) return EVENTS
|
|
119
|
+
const fired = new Set(
|
|
120
|
+
on.filter(([, object]) => object.type === "trigger").map(([, object]) => triggerEvent(object.sql)),
|
|
121
|
+
)
|
|
122
|
+
return EVENTS.filter((event) => fired.has(event))
|
|
123
|
+
}
|
|
124
|
+
|
|
109
125
|
/**
|
|
110
126
|
* The scratch D1 a proof runs on: what it holds is replayed on a fresh one when workerd crashes,
|
|
111
127
|
* which a disconnect shows ("other side closed"), not a D1 error
|
|
@@ -15,7 +15,7 @@ import { executeAll } from "../drivers/d1.ts"
|
|
|
15
15
|
import { lockedOut } from "../drivers/d1-lock.ts"
|
|
16
16
|
import { assertLocalExists, type D1Target, localMissing, type ReadConfig, resolveBinding } from "./d1-binding.ts"
|
|
17
17
|
import { d1Files, refuseSwallowing } from "./d1-files.ts"
|
|
18
|
-
import {
|
|
18
|
+
import { refuseUnlike } from "./d1-wrangler-error.ts"
|
|
19
19
|
import { D1_HASH_INTENT_TABLE, D1_HASH_TABLE, DRIZZLE_HISTORY_TABLE } from "./d1-history-tables.ts"
|
|
20
20
|
import {
|
|
21
21
|
type Hashed,
|
|
@@ -37,8 +37,9 @@ import { orphanMessage } from "./history-match.ts"
|
|
|
37
37
|
import { generateMigrationHash, migrationHashes } from "./parse.ts"
|
|
38
38
|
import { proveOnD1 } from "./d1-engine.ts"
|
|
39
39
|
import { planApply } from "./plan-apply.ts"
|
|
40
|
-
import { runPeer } from "./process.ts"
|
|
40
|
+
import { type RunResult, runPeer } from "./process.ts"
|
|
41
41
|
import { Refused } from "./refused.ts"
|
|
42
|
+
import { whileStoppable } from "./stopped.ts"
|
|
42
43
|
|
|
43
44
|
/** The files `__drizzle_migrations` records that the migrations table does not */
|
|
44
45
|
function recordedByDrizzle(
|
|
@@ -129,27 +130,6 @@ async function appliedAfter(
|
|
|
129
130
|
}
|
|
130
131
|
}
|
|
131
132
|
|
|
132
|
-
/** Refuse a failed run, or one whose table gained (`added`) other than the `names` checked, naming a file never checked */
|
|
133
|
-
function refuseUnlike(
|
|
134
|
-
ran: { code: number; stderr: string; stdout: string },
|
|
135
|
-
{ added, locked, names, table }: { added: string[]; locked: boolean; names: string[]; table: string },
|
|
136
|
-
): void {
|
|
137
|
-
const unchecked = added.filter((name) => !names.includes(name))
|
|
138
|
-
const rest = names.filter((name) => !added.includes(name))
|
|
139
|
-
const also =
|
|
140
|
-
unchecked.length === 0
|
|
141
|
-
? ""
|
|
142
|
-
: `; wrangler also applied ${unchecked.join(", ")}, which comb never checked and holds no hash for: check each against git, then run comb migrate baseline <db> --hashes`
|
|
143
|
-
const now = { applied: added, pending: rest }
|
|
144
|
-
if (ran.code !== 0) throw new Refused(`wrangler d1 migrations apply failed: ${applyFailure(ran, locked)}${also}`, now)
|
|
145
|
-
if (unchecked.length > 0 || rest.length > 0) {
|
|
146
|
-
throw new Refused(
|
|
147
|
-
`wrangler recorded ${added.join(", ") || "nothing"} in ${table}, where comb checked ${names.join(", ")}; wrangler said: ${wranglerError(ran)}${also}`,
|
|
148
|
-
now,
|
|
149
|
-
)
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
133
|
type D1Apply = {
|
|
154
134
|
approve(migration: ParsedMigration): Promise<Approval>
|
|
155
135
|
/** only check: apply nothing */
|
|
@@ -158,6 +138,25 @@ type D1Apply = {
|
|
|
158
138
|
read: ReadConfig
|
|
159
139
|
}
|
|
160
140
|
|
|
141
|
+
/**
|
|
142
|
+
* wrangler's apply of `names`, each announced first. A stop signal ends its run, and the table then
|
|
143
|
+
* says which of them it recorded (stopped.ts)
|
|
144
|
+
*/
|
|
145
|
+
async function wranglerApply(
|
|
146
|
+
target: D1Target,
|
|
147
|
+
{ print, read }: D1Apply,
|
|
148
|
+
{ names, table }: { names: string[]; table: string },
|
|
149
|
+
): Promise<RunResult> {
|
|
150
|
+
for (const name of names) print(`Apply: ${name}`)
|
|
151
|
+
const found = async () => {
|
|
152
|
+
const recorded = await appliedAfter(target, read, { names, ok: false, table })
|
|
153
|
+
return { applied: names.filter((name) => recorded.has(name)), pending: names.filter((name) => !recorded.has(name)) }
|
|
154
|
+
}
|
|
155
|
+
return whileStoppable({ names, print, read: found }, () =>
|
|
156
|
+
runPeer(target.exec, target.wrangler)(["d1", "migrations", "apply", target.name, ...target.flags], { input: "" }),
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
161
160
|
/** Check the pending migrations and, unless only checking, apply them through wrangler; returns them */
|
|
162
161
|
async function applyD1(target: D1Target, options: D1Apply): Promise<string[]> {
|
|
163
162
|
const { applied, hasIntents, marks, settle, table } = await history(options.read, target, {
|
|
@@ -193,9 +192,7 @@ async function applyD1(target: D1Target, options: D1Apply): Promise<string[]> {
|
|
|
193
192
|
/* the intents go first: a lost record then fails before wrangler applies anything */
|
|
194
193
|
const hashed = pending.map(({ migration, name }) => ({ hash: migration.hash, marked: true, name }))
|
|
195
194
|
await target.conn.batch([...settleStatements(settle, hasIntents, marks), ...intentStatements(hashed)])
|
|
196
|
-
|
|
197
|
-
const apply = ["d1", "migrations", "apply", target.name, ...target.flags]
|
|
198
|
-
const ran = await runPeer(target.exec, target.wrangler)(apply, { input: "" })
|
|
195
|
+
const ran = await wranglerApply(target, options, { names, table })
|
|
199
196
|
/* asked before anything else runs, while the lock that failed the apply is most likely still held */
|
|
200
197
|
const locked = ran.code !== 0 && target.flags.includes("--local") && (await lockedOut(ran, target.held))
|
|
201
198
|
const after = await appliedAfter(target, options.read, { names, ok: ran.code === 0, table })
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* What a failed `wrangler d1 migrations apply` says, read for the refusal comb gives
|
|
2
|
+
* What a failed `wrangler d1 migrations apply` says, read for the refusal comb gives, and the refusal
|
|
3
|
+
* of a run whose table records other than the files comb checked.
|
|
3
4
|
*/
|
|
4
5
|
import { LOCKER } from "../drivers/d1-lock.ts"
|
|
6
|
+
import { Refused } from "./refused.ts"
|
|
5
7
|
|
|
6
8
|
type Ran = { stderr: string; stdout: string }
|
|
7
9
|
|
|
@@ -23,3 +25,24 @@ export function applyFailure(ran: Ran, locked: boolean): string {
|
|
|
23
25
|
? `the local D1 database was locked by ${LOCKER} while wrangler applied: let it finish and rerun`
|
|
24
26
|
: wranglerError(ran)
|
|
25
27
|
}
|
|
28
|
+
|
|
29
|
+
/** Refuse a failed run, or one whose table gained (`added`) other than the `names` checked, naming a file never checked */
|
|
30
|
+
export function refuseUnlike(
|
|
31
|
+
ran: { code: number; stderr: string; stdout: string },
|
|
32
|
+
{ added, locked, names, table }: { added: string[]; locked: boolean; names: string[]; table: string },
|
|
33
|
+
): void {
|
|
34
|
+
const unchecked = added.filter((name) => !names.includes(name))
|
|
35
|
+
const rest = names.filter((name) => !added.includes(name))
|
|
36
|
+
const also =
|
|
37
|
+
unchecked.length === 0
|
|
38
|
+
? ""
|
|
39
|
+
: `; wrangler also applied ${unchecked.join(", ")}, which comb never checked and holds no hash for: check each against git, then run comb migrate baseline <db> --hashes`
|
|
40
|
+
const now = { applied: added, pending: rest }
|
|
41
|
+
if (ran.code !== 0) throw new Refused(`wrangler d1 migrations apply failed: ${applyFailure(ran, locked)}${also}`, now)
|
|
42
|
+
if (unchecked.length > 0 || rest.length > 0) {
|
|
43
|
+
throw new Refused(
|
|
44
|
+
`wrangler recorded ${added.join(", ") || "nothing"} in ${table}, where comb checked ${names.join(", ")}; wrangler said: ${wranglerError(ran)}${also}`,
|
|
45
|
+
now,
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
* The replay's transactions and the reads around them (pg-replay.ts): a file's or comb's own
|
|
3
3
|
* statement's transaction, refused before its COMMIT when this backend's counts show it wrote past
|
|
4
4
|
* its database, and the server's shared catalogs read as committed around it and around
|
|
5
|
-
* every bare statement, a change there the file's when it is to
|
|
5
|
+
* every bare statement, a change there the file's when it is to the replay's own database, or to an
|
|
6
|
+
* object the file names beside a statement that can run code comb cannot read;
|
|
6
7
|
* a `txmode none` file's statement in a transaction of its own first, bare only when
|
|
7
8
|
* Postgres runs it outside a block alone.
|
|
8
9
|
*/
|
|
9
10
|
import { adopt, bareAgain, type Run } from "./pg-replay-roles.ts"
|
|
10
|
-
import {
|
|
11
|
+
import { type Hidden, runsHidden } from "./pg-replay-hidden.ts"
|
|
12
|
+
import { type Snapshot, snapshot, snapshotChanges, subscriptionsIn } from "./pg-server-snapshot.ts"
|
|
11
13
|
import {
|
|
12
14
|
type Change,
|
|
13
15
|
catalogsOf,
|
|
@@ -31,12 +33,13 @@ async function begin(run: Run): Promise<void> {
|
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
/**
|
|
34
|
-
* Where a replayed transaction or bare statement is: its file's name and the names it spells,
|
|
35
|
-
*
|
|
36
|
-
* adopts
|
|
36
|
+
* Where a replayed transaction or bare statement is: its file's name and the names it spells, what
|
|
37
|
+
* its statements so far can run that comb cannot read (`hidden`), the statement, if bare, and what a
|
|
38
|
+
* refusal stops; told each role the transaction adds that the replay adopts
|
|
37
39
|
*/
|
|
38
40
|
type At = {
|
|
39
41
|
adopted?: ((role: string) => void) | undefined
|
|
42
|
+
hidden: Hidden
|
|
40
43
|
name: string
|
|
41
44
|
names: ReadonlySet<string>
|
|
42
45
|
sent: string | undefined
|
|
@@ -46,52 +49,52 @@ type At = {
|
|
|
46
49
|
/* the words that name devUrl's role in a file, as namesIn folds them: CURRENT_USER and its kin */
|
|
47
50
|
const SESSION_ROLE = ["current_role", "current_user", "session_user", "user"]
|
|
48
51
|
|
|
52
|
+
/** The objects the file `names`, devUrl's role among them where it names that by keyword */
|
|
53
|
+
function spelled(run: Run, names: ReadonlySet<string>): Set<string> {
|
|
54
|
+
const its = new Set(names)
|
|
55
|
+
if (SESSION_ROLE.some((word) => names.has(word))) its.add(run.role)
|
|
56
|
+
return its
|
|
57
|
+
}
|
|
58
|
+
|
|
49
59
|
/**
|
|
50
|
-
* The server's shared catalogs read as committed against the last read: a change since to
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* the cleanup
|
|
60
|
+
* The server's shared catalogs read as committed against the last read: a change since to the
|
|
61
|
+
* replay's own database, or to what it holds, is the file's, as only this replay runs in it, and the
|
|
62
|
+
* cleanup drops it. A change to an object the file names is the file's when its statements so far
|
|
63
|
+
* can run code comb cannot read (pg-replay-hidden.ts), the one way a change past the database gets by
|
|
64
|
+
* the counts, and is refused, but no cleanup drops it: Postgres keeps no creator for a role or a
|
|
65
|
+
* database, and another session may have made it. A change to any other object, or beside statements
|
|
66
|
+
* that run only what their parse says, is another session's. `read` is that read when the caller took it
|
|
55
67
|
*/
|
|
56
68
|
async function window(run: Run, at: At, read?: Snapshot): Promise<void> {
|
|
57
|
-
|
|
69
|
+
const now = read ?? (await snapshot(run.admin))
|
|
70
|
+
strayed(run, { ...at, hid: await hides(run, at, now) }, now)
|
|
58
71
|
}
|
|
59
72
|
|
|
73
|
+
/* whether a change in `now` to an object the file names may be the file's, as its statements can run code comb cannot read */
|
|
74
|
+
const hides = (run: Run, { hidden, names }: At, now: Snapshot): boolean | Promise<boolean> =>
|
|
75
|
+
snapshotChanges(run.seen, now, spelled(run, names)).length > 0 && runsHidden(run.sql, hidden)
|
|
76
|
+
|
|
60
77
|
/**
|
|
61
78
|
* window's verdict on `now`, the server's shared catalogs as committed after the file or `sent`,
|
|
62
|
-
* against the last read
|
|
79
|
+
* against the last read, the objects the file names counted when it `hid` code comb cannot read;
|
|
80
|
+
* kept apart from window's await, as v8 miscounts a branch after an await
|
|
63
81
|
*/
|
|
64
|
-
function strayed(run: Run, { name, names, sent, stops }: At, now: Snapshot): void {
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
const { databases, roles, slots } = added(run.seen, now, its)
|
|
82
|
+
function strayed(run: Run, { hid, name, names, sent, stops }: At & { hid: boolean }, now: Snapshot): void {
|
|
83
|
+
const replays = new Set([run.database])
|
|
84
|
+
const changes = snapshotChanges(run.seen, now, hid ? new Set([...spelled(run, names), ...replays]) : replays)
|
|
85
|
+
const own = new Set(snapshotChanges(run.seen, now, replays))
|
|
69
86
|
const within = subscriptionsIn(run.seen, now, run.database)
|
|
70
87
|
run.seen = now
|
|
71
88
|
if (changes.length === 0) return
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
run.strays.slots.push(...slots)
|
|
75
|
-
const own = `database ${run.database} `
|
|
76
|
-
const strays = [
|
|
77
|
-
...roles.map((role) => `role ${role}`),
|
|
78
|
-
...databases.map((database) => `database ${database} unless a session is in it`),
|
|
79
|
-
...slots.map((slot) => `replication slot ${slot} unless a session holds it`),
|
|
80
|
-
]
|
|
81
|
-
const replays =
|
|
82
|
-
changes.some((change) => change.startsWith(own)) || within.length > 0
|
|
89
|
+
const dropped =
|
|
90
|
+
own.size > 0
|
|
83
91
|
? `; comb drops the database it replays in${within.length === 0 ? "" : `, and ${within.join(", ")} in it`}`
|
|
84
92
|
: ""
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
...slots.map((slot) => `replication slot ${slot} added`),
|
|
91
|
-
...within.map((subject) => `${subject} added`),
|
|
92
|
-
])
|
|
93
|
-
const kept = changes.filter((change) => !dropping.has(change) && !change.startsWith(own))
|
|
94
|
-
const left = kept.length === 0 ? "" : `; devUrl's server keeps ${kept.join(", ")}, which comb cannot undo`
|
|
93
|
+
const kept = changes.filter((change) => !own.has(change))
|
|
94
|
+
const left =
|
|
95
|
+
kept.length === 0
|
|
96
|
+
? ""
|
|
97
|
+
: `; comb leaves ${kept.join(", ")} on devUrl's server, as another session may have made ${kept.length === 1 ? "that change" : "those changes"}: undo there each that is the file's`
|
|
95
98
|
throw new ReplayFailed(
|
|
96
99
|
`${name}: devUrl's server changed past the database the replay runs in while ${sent === undefined ? "the file" : JSON.stringify(sent)} ran: ${changes.join(", ")}. A change made there, from inside a DO block, a function, EXECUTE or CALL, or over a second connection such as dblink's, apply would make on the target's server, so comb refuses the file; write the change as a statement of its own, which the replay leaves out and apply runs${dropped}${left}; ${stops}`,
|
|
97
100
|
)
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* which the next replay or apply sweeps. Postgres drops no database that holds a subscription or a
|
|
5
5
|
* logical slot, so both go first, and only from the replay databases it drops: a subscription from
|
|
6
6
|
* a session in its database, the only one that can name it. A drop that fails never decides the
|
|
7
|
-
* verdict, and is said, never left unsaid: what it left
|
|
8
|
-
*
|
|
7
|
+
* verdict, and is said, never left unsaid: what it left and dev's error, which a later sweep takes. The
|
|
8
|
+
* replay database is dropped WITH
|
|
9
9
|
* (FORCE), so a backend that came in after the cleanup ended the others cannot hold it.
|
|
10
10
|
*/
|
|
11
11
|
import { sqlIdentifier } from "../../codegen/utils/case.ts"
|
|
@@ -24,14 +24,11 @@ async function settle(step: () => Promise<unknown>): Promise<void> {
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
/**
|
|
28
|
-
type
|
|
29
|
-
|
|
30
|
-
/** How a drop that failed is said: what it `left`, with dev's error */
|
|
31
|
-
type Leave = (left: Left, error: unknown) => void
|
|
27
|
+
/** How a drop that failed is said: what it left on devUrl's server, a database, role or slot by name, with dev's error */
|
|
28
|
+
type Leave = (left: string, error: unknown) => void
|
|
32
29
|
|
|
33
30
|
/** `drop` run, a failure `leave`d: cleaning up never replaces the replay's verdict, and never leaves something unsaid */
|
|
34
|
-
async function dropOr(leave: Leave, left:
|
|
31
|
+
async function dropOr(leave: Leave, left: string, drop: () => Promise<unknown>): Promise<void> {
|
|
35
32
|
try {
|
|
36
33
|
await drop()
|
|
37
34
|
} catch (error) {
|
|
@@ -45,9 +42,9 @@ type Drops = { into: (database: string) => Sql; leave: Leave }
|
|
|
45
42
|
|
|
46
43
|
/** A `leave` that prints, `failed` naming devUrl in dev's error */
|
|
47
44
|
function leaving(print: (line: string) => void, failed: Parameters<typeof failureOf>[1]): Leave {
|
|
48
|
-
return (
|
|
45
|
+
return (left, error) =>
|
|
49
46
|
print(
|
|
50
|
-
`comb could not drop ${
|
|
47
|
+
`comb could not drop ${left}, which a replay left on devUrl's server: ${failureOf(error, failed)}; the next check or apply that reaches that server drops it`,
|
|
51
48
|
)
|
|
52
49
|
}
|
|
53
50
|
|
|
@@ -80,14 +77,13 @@ async function emptyDatabase(admin: Sql, database: string, { into, leave }: Drop
|
|
|
80
77
|
if (subscriptions.length > 0) {
|
|
81
78
|
const sql = into(database)
|
|
82
79
|
try {
|
|
83
|
-
for (const name of subscriptions)
|
|
84
|
-
await dropOr(leave, { swept: true, what: `subscription ${name}` }, () => dropSubscription(sql, name))
|
|
80
|
+
for (const name of subscriptions) await dropOr(leave, `subscription ${name}`, () => dropSubscription(sql, name))
|
|
85
81
|
} finally {
|
|
86
82
|
await settle(() => sql.end({ timeout: 0 }))
|
|
87
83
|
}
|
|
88
84
|
}
|
|
89
85
|
for (const slot of (holds?.["slots"] ?? []) as string[])
|
|
90
|
-
await dropOr(leave,
|
|
86
|
+
await dropOr(leave, `replication slot ${slot}`, () => dropSlot(admin, slot))
|
|
91
87
|
}
|
|
92
88
|
|
|
93
89
|
/**
|
|
@@ -132,11 +128,9 @@ async function sweep(admin: Sql, now: number, drops: Drops): Promise<void> {
|
|
|
132
128
|
await settle(() => emptyDatabase(admin, name, drops))
|
|
133
129
|
/* Postgres drops no template, which a committing DO can make the replay's database */
|
|
134
130
|
if (template) await settle(() => admin.unsafe(`ALTER DATABASE ${sqlIdentifier(name)} IS_TEMPLATE false`, []))
|
|
135
|
-
await dropOr(leave,
|
|
136
|
-
admin.unsafe(`DROP DATABASE IF EXISTS ${sqlIdentifier(name)}`, []),
|
|
137
|
-
)
|
|
131
|
+
await dropOr(leave, `database ${name}`, () => admin.unsafe(`DROP DATABASE IF EXISTS ${sqlIdentifier(name)}`, []))
|
|
138
132
|
}
|
|
139
|
-
for (const role of roles) await dropOr(leave,
|
|
133
|
+
for (const role of roles) await dropOr(leave, `role ${role}`, () => dropRole(admin, role))
|
|
140
134
|
}
|
|
141
135
|
|
|
142
136
|
/* why comb loads postgres.js in an apply */
|
|
@@ -196,10 +190,10 @@ async function sweepDev(
|
|
|
196
190
|
|
|
197
191
|
/**
|
|
198
192
|
* `run`'s cleanup, each drop that fails `leave`d: its connection closed, a query in flight with it,
|
|
199
|
-
* any backend still in its database ended, the
|
|
200
|
-
* the database
|
|
201
|
-
*
|
|
202
|
-
*
|
|
193
|
+
* any backend still in its database ended, the database's subscriptions and slots, which keep it
|
|
194
|
+
* from its drop, the database made no template and dropped, forcing out a backend that came in
|
|
195
|
+
* since, and each role it made or adopted. It drops nothing else a refused window saw, as Postgres
|
|
196
|
+
* keeps no creator for a role, a database or a slot, and another session may have made it
|
|
203
197
|
*/
|
|
204
198
|
async function cleanUp(run: Run, drops: Drops): Promise<void> {
|
|
205
199
|
const { leave } = drops
|
|
@@ -210,21 +204,12 @@ async function cleanUp(run: Run, drops: Drops): Promise<void> {
|
|
|
210
204
|
[run.database],
|
|
211
205
|
),
|
|
212
206
|
)
|
|
213
|
-
for (const slot of run.strays.slots)
|
|
214
|
-
await dropOr(leave, { swept: false, what: `replication slot ${slot}` }, () => dropSlot(run.admin, slot))
|
|
215
207
|
await settle(() => emptyDatabase(run.admin, run.database, drops))
|
|
216
208
|
await settle(() => run.admin.unsafe(`ALTER DATABASE ${run.database} IS_TEMPLATE false`, []))
|
|
217
|
-
await dropOr(leave,
|
|
209
|
+
await dropOr(leave, `database ${run.database}`, () =>
|
|
218
210
|
run.admin.unsafe(`DROP DATABASE IF EXISTS ${run.database} WITH (FORCE)`, []),
|
|
219
211
|
)
|
|
220
|
-
for (const role of run.created)
|
|
221
|
-
await dropOr(leave, { swept: true, what: `role ${role}` }, () => dropMade(run.admin, role))
|
|
222
|
-
for (const name of run.strays.databases)
|
|
223
|
-
await dropOr(leave, { swept: false, what: `database ${name}` }, () =>
|
|
224
|
-
run.admin.unsafe(`DROP DATABASE IF EXISTS ${sqlIdentifier(name)}`, []),
|
|
225
|
-
)
|
|
226
|
-
for (const role of run.strays.roles)
|
|
227
|
-
await dropOr(leave, { swept: false, what: `role ${role}` }, () => dropRole(run.admin, role))
|
|
212
|
+
for (const role of run.created) await dropOr(leave, `role ${role}`, () => dropMade(run.admin, role))
|
|
228
213
|
}
|
|
229
214
|
|
|
230
215
|
export { cleanUp, type Drops, dropMade, dropOr, dropRole, dropSlot, type Leave, leaving, settle, sweep, sweepDev }
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether the statements a replay's window covers can run code comb cannot read, the only way one
|
|
3
|
+
* makes a change past its database that this backend's counts miss (pg-replay-commit.ts): a DO
|
|
4
|
+
* block, a CALL, an EXECUTE, or a call of a function outside pg_catalog, dblink's among them. A
|
|
5
|
+
* statement whose tree holds none of those runs only what its parse says, and BEYOND and SERVER_CALLS
|
|
6
|
+
* (pg-outside-transaction.ts, pg-server-calls.ts) leave out every such statement that reaches past
|
|
7
|
+
* the database, so a change the server's read shows beside it is another session's. A function
|
|
8
|
+
* called by a bare name is pg_catalog's only where the replay's database holds no other of that name,
|
|
9
|
+
* as an overload in a schema on the search path can take the call, and the file makes none of it.
|
|
10
|
+
* Code Postgres runs for a statement on its own, a trigger, a default, an operator or a cast that
|
|
11
|
+
* calls a function an earlier file defines, is no call in the tree, so it is not read here; what it
|
|
12
|
+
* writes in the replay's transaction its counts still see.
|
|
13
|
+
*/
|
|
14
|
+
import type { Sql } from "./pg-replay-database.ts"
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* What the statements so far can run that comb cannot read: `code` when one surely can, the
|
|
18
|
+
* functions they call by a bare name (`calls`), which may be pg_catalog's, and those the file makes
|
|
19
|
+
* (`made`). A file's every later window counts what one ran, as a change a DO block makes in the
|
|
20
|
+
* file's own block commits at its COMMIT
|
|
21
|
+
*/
|
|
22
|
+
type Hidden = { calls: Set<string>; code: boolean; made: Set<string> }
|
|
23
|
+
|
|
24
|
+
/** Nothing run yet */
|
|
25
|
+
const nothingHidden = (): Hidden => ({ calls: new Set(), code: false, made: new Set() })
|
|
26
|
+
|
|
27
|
+
// lookup-table: the statements that run code the parse does not hold: a DO block's body, a procedure, a prepared statement
|
|
28
|
+
const RUNS_CODE: ReadonlySet<string> = /* @__PURE__ */ new Set(["CallStmt", "DoStmt", "ExecuteStmt"])
|
|
29
|
+
|
|
30
|
+
/** The name parts of a FuncCall's or a CREATE FUNCTION's `funcname`, each a String node, the name last */
|
|
31
|
+
const partsOf = (funcname: unknown): string[] =>
|
|
32
|
+
(funcname as Array<{ String: { sval: string } }>).map((part) => part.String.sval)
|
|
33
|
+
|
|
34
|
+
/** `tree`, a parse tree or a part of one, read `into` */
|
|
35
|
+
function readTree(tree: unknown, into: Hidden): void {
|
|
36
|
+
if (tree === null || typeof tree !== "object") return
|
|
37
|
+
for (const [key, value] of Object.entries(tree)) {
|
|
38
|
+
if (RUNS_CODE.has(key)) into.code = true
|
|
39
|
+
else if (key === "FuncCall" || key === "CreateFunctionStmt") {
|
|
40
|
+
const parts = partsOf((value as { funcname: unknown }).funcname)
|
|
41
|
+
/* a funcname holds its name at least */
|
|
42
|
+
const name = parts.at(-1) as string
|
|
43
|
+
if (key === "CreateFunctionStmt") into.made.add(name)
|
|
44
|
+
else if (parts.length === 1) into.calls.add(name)
|
|
45
|
+
else if (parts.length > 2 || parts[0] !== "pg_catalog") into.code = true
|
|
46
|
+
}
|
|
47
|
+
readTree(value, into)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A statement the replay runs, read `into`: one the parser cannot read may run anything */
|
|
52
|
+
function readStatement(tree: unknown, into: Hidden): void {
|
|
53
|
+
if (tree === undefined) into.code = true
|
|
54
|
+
else readTree(tree, into)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* of the functions a JSON array names, whether one is no pg_catalog function, or also one outside it */
|
|
58
|
+
const NOT_CATALOG = `SELECT EXISTS (
|
|
59
|
+
SELECT FROM jsonb_array_elements_text($1::text::jsonb) f(name)
|
|
60
|
+
WHERE NOT EXISTS (SELECT FROM pg_proc p WHERE p.proname = f.name AND p.pronamespace = 'pg_catalog'::regnamespace)
|
|
61
|
+
OR EXISTS (SELECT FROM pg_proc p WHERE p.proname = f.name AND p.pronamespace <> 'pg_catalog'::regnamespace)
|
|
62
|
+
) AS hidden`
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Whether what `hidden` holds can run code comb cannot read, a bare name resolved on `sql`, the
|
|
66
|
+
* replay's connection, which sees what its open transaction made
|
|
67
|
+
*/
|
|
68
|
+
async function runsHidden(sql: Sql, { calls, code, made }: Hidden): Promise<boolean> {
|
|
69
|
+
if (code || [...calls].some((name) => made.has(name))) return true
|
|
70
|
+
if (calls.size === 0) return false
|
|
71
|
+
const rows = await sql.unsafe(NOT_CATALOG, [JSON.stringify([...calls])])
|
|
72
|
+
return rows.some((row) => row["hidden"] === true)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export { type Hidden, nothingHidden, readStatement, runsHidden }
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import { sqlIdentifier } from "../../codegen/utils/case.ts"
|
|
19
19
|
import { codeOf, literal, type Sql, TAG } from "./pg-replay-database.ts"
|
|
20
20
|
import { addWrites, catalogsOf, counts, type Rows, subjectRows, type Writes, writesSince } from "./pg-server-writes.ts"
|
|
21
|
-
import type { Snapshot
|
|
21
|
+
import type { Snapshot } from "./pg-server-snapshot.ts"
|
|
22
22
|
import { flag } from "./pg-nodes.ts"
|
|
23
23
|
|
|
24
24
|
/**
|
|
@@ -26,8 +26,7 @@ import { flag } from "./pg-nodes.ts"
|
|
|
26
26
|
* that owns it, its role (`role`, which CURRENT_USER and its kin name in a file), whether that role
|
|
27
27
|
* is a superuser, which reads pg_authid, whether it has CREATEROLE (`createrole`), and the roles it made; the
|
|
28
28
|
* open transaction's own writes, comb's CREATE ROLEs', with the rows they left; the shared catalogs
|
|
29
|
-
* as last read committed (`seen`)
|
|
30
|
-
* added (`strays`), which the cleanup drops
|
|
29
|
+
* as last read committed (`seen`)
|
|
31
30
|
*/
|
|
32
31
|
type Run = {
|
|
33
32
|
admin: Sql
|
|
@@ -39,7 +38,6 @@ type Run = {
|
|
|
39
38
|
role: string
|
|
40
39
|
seen: Snapshot
|
|
41
40
|
sql: Sql
|
|
42
|
-
strays: Strays
|
|
43
41
|
/** the tablespaces it left out the CREATE of, which a statement then runs in pg_default instead */
|
|
44
42
|
tablespaces: Set<string>
|
|
45
43
|
superuser: boolean
|
|
@@ -3,11 +3,10 @@
|
|
|
3
3
|
* catalog query: Postgres, not a model of it, decides what a cascade, a rename or a
|
|
4
4
|
* move leaves, and the two sides compare as one read of two databases.
|
|
5
5
|
*
|
|
6
|
-
* The replay runs as Atlas applies: each file on a session of its own, as apply
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* made for it and dropped after (pg-replay-database.ts). One transaction for the whole history
|
|
6
|
+
* The replay runs as Atlas applies: each file on a session of its own, as apply gives each file an
|
|
7
|
+
* Atlas call, and in a transaction of its own, a `-- atlas:txmode none` file statement by statement,
|
|
8
|
+
* each in a transaction of its own first and bare, as Atlas sends it, where Postgres runs it only
|
|
9
|
+
* so, in a database made for it and dropped after (pg-replay-database.ts). One transaction for the whole history
|
|
11
10
|
* filled Postgres's lock table and left a seed's deferred trigger events pending, and devUrl's own
|
|
12
11
|
* database lent the history what it held. Committed, a statement whose scope is the server or
|
|
13
12
|
* another database would outlive the replay on devUrl's server, the target's own when they share
|
|
@@ -29,19 +28,19 @@
|
|
|
29
28
|
* Bare runs only a statement a block refuses (an outside-only class, a DO block or CALL that
|
|
30
29
|
* commits), one only a block runs (a LOCK, a SAVEPOINT, a cursor not held) and the file's own
|
|
31
30
|
* block, and a change one makes past its database has committed when the read sees it: the refusal
|
|
32
|
-
* names what devUrl's server keeps. The read is the server's, another session's work in it too,
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* file builds at run time, by concatenation, from a query,
|
|
39
|
-
* is none it spells, so comb
|
|
40
|
-
* pg_authid alone, so a
|
|
31
|
+
* names what devUrl's server keeps. The read is the server's, another session's work in it too, and
|
|
32
|
+
* Postgres keeps no creator for a role or a database, so a change there is the file's only beside a
|
|
33
|
+
* statement that can run code comb cannot read (pg-replay-hidden.ts), and only when it is to an
|
|
34
|
+
* object the file names: a name it spells, unquoted and folded as Postgres folds it, quoted and
|
|
35
|
+
* exact, or inside a string a DO body, dblink_exec or format() may run (namesIn in sql-code.ts).
|
|
36
|
+
* That change is refused, naming it, and left, as another session may have made it; any other is
|
|
37
|
+
* another session's and passes. A name the file builds at run time, by concatenation, from a query,
|
|
38
|
+
* or in a function another file defines, is none it spells, so comb does not refuse that change. A
|
|
39
|
+
* role's password reads from pg_authid alone, so a bare statement's is seen by a superuser's only.
|
|
41
40
|
*
|
|
42
|
-
* A SIGINT or a SIGTERM, a Ctrl-C or a cancelled CI job, skips a `finally`, so either runs the
|
|
43
|
-
*
|
|
44
|
-
*
|
|
41
|
+
* A SIGINT or a SIGTERM, a Ctrl-C or a cancelled CI job, skips a `finally`, so either runs the replay's
|
|
42
|
+
* cleanup first, once, and then ends the process as the signal would; a run killed past it is swept
|
|
43
|
+
* by the next replay or apply on the server.
|
|
45
44
|
*/
|
|
46
45
|
import { sqlIdentifier } from "../../codegen/utils/case.ts"
|
|
47
46
|
import { type PgTarget, refuseUrlProblems } from "./atlas-target.ts"
|
|
@@ -65,6 +64,7 @@ import { prepareSnapshot, snapshot } from "./pg-server-snapshot.ts"
|
|
|
65
64
|
import { refuseUncounted } from "./pg-server-writes.ts"
|
|
66
65
|
import { pgRead } from "./pg-split.ts"
|
|
67
66
|
import { namesIn } from "./sql-code.ts"
|
|
67
|
+
import { nothingHidden, readStatement } from "./pg-replay-hidden.ts"
|
|
68
68
|
|
|
69
69
|
/** A history file as baseline reads it */
|
|
70
70
|
type HistoryFile = { name: string; sql: string }
|
|
@@ -92,12 +92,14 @@ async function replayFile(run: Run, { name, sql: text }: HistoryFile, replay: Re
|
|
|
92
92
|
const inFile = !outsideTransaction(text)
|
|
93
93
|
const file = {
|
|
94
94
|
adopted: (role: string) => replay.adopted?.(name, role),
|
|
95
|
+
hidden: nothingHidden(),
|
|
95
96
|
name,
|
|
96
97
|
names: namesIn(text),
|
|
97
98
|
stops: replay.stops,
|
|
98
99
|
}
|
|
99
100
|
const statements = pgRead(text).statements.map(({ sent, tree }) => ({
|
|
100
101
|
sent,
|
|
102
|
+
tree,
|
|
101
103
|
...(tree === undefined ? { node: {}, type: "" } : (([type, node]) => ({ node, type }))(nodeOf(tree))),
|
|
102
104
|
}))
|
|
103
105
|
const locks = lockReader(
|
|
@@ -110,13 +112,14 @@ async function replayFile(run: Run, { name, sql: text }: HistoryFile, replay: Re
|
|
|
110
112
|
await begin(run)
|
|
111
113
|
await locks.began()
|
|
112
114
|
}
|
|
113
|
-
for (const [index, { node, sent, type }] of statements.entries()) {
|
|
115
|
+
for (const [index, { node, sent, tree, type }] of statements.entries()) {
|
|
114
116
|
const beyond = (await runsOnMade(run, type, node)) ? undefined : beyondDatabase(type, node)
|
|
115
117
|
if (beyond !== undefined) {
|
|
116
118
|
leaveOutTablespace(run.tablespaces, type, node)
|
|
117
119
|
replay.leftOut(name, sent, beyond)
|
|
118
120
|
continue
|
|
119
121
|
}
|
|
122
|
+
readStatement(tree, file.hidden)
|
|
120
123
|
await locks.next(index, type, node)
|
|
121
124
|
const send = () => runStatement(run, { name, node, sent, type }, replay)
|
|
122
125
|
if (inFile) await send()
|
|
@@ -206,7 +209,6 @@ async function opening(
|
|
|
206
209
|
owner,
|
|
207
210
|
seen: new Map(),
|
|
208
211
|
sql: open(),
|
|
209
|
-
strays: { databases: [], roles: [], slots: [] },
|
|
210
212
|
tablespaces: new Set(),
|
|
211
213
|
...(await devRoleOf(admin)),
|
|
212
214
|
}
|