@bockit/sequel 0.0.0-stage → 0.2.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.
Files changed (104) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +227 -2
  3. package/THIRD_PARTY_NOTICES.md +268 -0
  4. package/binaries.json +106 -0
  5. package/dist/binaries/cache.d.ts +6 -0
  6. package/dist/binaries/cache.js +36 -0
  7. package/dist/binaries/ensure-binary.d.ts +10 -0
  8. package/dist/binaries/ensure-binary.js +85 -0
  9. package/dist/binaries/manifest.d.ts +30 -0
  10. package/dist/binaries/manifest.js +112 -0
  11. package/dist/binaries/platform.d.ts +3 -0
  12. package/dist/binaries/platform.js +10 -0
  13. package/dist/binaries/unpack.d.ts +2 -0
  14. package/dist/binaries/unpack.js +35 -0
  15. package/dist/binaries/verify.d.ts +2 -0
  16. package/dist/binaries/verify.js +16 -0
  17. package/dist/check.d.ts +9 -0
  18. package/dist/check.js +178 -0
  19. package/dist/cli.d.ts +2 -0
  20. package/dist/cli.js +339 -0
  21. package/dist/config/define-config.d.ts +24 -0
  22. package/dist/config/define-config.js +4 -0
  23. package/dist/config/load-config.d.ts +6 -0
  24. package/dist/config/load-config.js +34 -0
  25. package/dist/config/resolve-config.d.ts +35 -0
  26. package/dist/config/resolve-config.js +159 -0
  27. package/dist/db/connection.d.ts +6 -0
  28. package/dist/db/connection.js +38 -0
  29. package/dist/db/shadow.d.ts +11 -0
  30. package/dist/db/shadow.js +68 -0
  31. package/dist/engine/client.d.ts +24 -0
  32. package/dist/engine/client.js +123 -0
  33. package/dist/engine/diff.d.ts +12 -0
  34. package/dist/engine/diff.js +51 -0
  35. package/dist/errors.d.ts +3 -0
  36. package/dist/errors.js +5 -0
  37. package/dist/extras/snapshot.d.ts +3 -0
  38. package/dist/extras/snapshot.js +99 -0
  39. package/dist/files.d.ts +1 -0
  40. package/dist/files.js +15 -0
  41. package/dist/generate/generate-modules.d.ts +17 -0
  42. package/dist/generate/generate-modules.js +97 -0
  43. package/dist/generate/modules.d.ts +16 -0
  44. package/dist/generate/modules.js +118 -0
  45. package/dist/generate/pg-dump.d.ts +8 -0
  46. package/dist/generate/pg-dump.js +66 -0
  47. package/dist/generate/plugin-wrapper.d.ts +3 -0
  48. package/dist/generate/plugin-wrapper.js +20 -0
  49. package/dist/generate/sqlc.d.ts +27 -0
  50. package/dist/generate/sqlc.js +113 -0
  51. package/dist/generate/watch.d.ts +16 -0
  52. package/dist/generate/watch.js +73 -0
  53. package/dist/generator/generate.d.ts +21 -0
  54. package/dist/generator/generate.js +440 -0
  55. package/dist/generator/keys.d.ts +4 -0
  56. package/dist/generator/keys.js +24 -0
  57. package/dist/generator/lexer.d.ts +12 -0
  58. package/dist/generator/lexer.js +210 -0
  59. package/dist/generator/main.d.ts +1 -0
  60. package/dist/generator/main.js +12 -0
  61. package/dist/generator/pg-array-helper.d.ts +2 -0
  62. package/dist/generator/pg-array-helper.js +53 -0
  63. package/dist/generator/plugin.d.ts +6 -0
  64. package/dist/generator/plugin.js +26 -0
  65. package/dist/generator/request.d.ts +57 -0
  66. package/dist/generator/request.js +96 -0
  67. package/dist/generator/select-list.d.ts +27 -0
  68. package/dist/generator/select-list.js +580 -0
  69. package/dist/generator/strings.d.ts +2 -0
  70. package/dist/generator/strings.js +34 -0
  71. package/dist/generator/types.d.ts +23 -0
  72. package/dist/generator/types.js +216 -0
  73. package/dist/index.d.ts +2 -0
  74. package/dist/index.js +2 -0
  75. package/dist/migrate/adopt-prisma.d.ts +23 -0
  76. package/dist/migrate/adopt-prisma.js +119 -0
  77. package/dist/migrate/deploy.d.ts +7 -0
  78. package/dist/migrate/deploy.js +23 -0
  79. package/dist/migrate/drift.d.ts +10 -0
  80. package/dist/migrate/drift.js +36 -0
  81. package/dist/migrate/files.d.ts +19 -0
  82. package/dist/migrate/files.js +97 -0
  83. package/dist/migrate/history.d.ts +20 -0
  84. package/dist/migrate/history.js +54 -0
  85. package/dist/migrate/lint.d.ts +21 -0
  86. package/dist/migrate/lint.js +153 -0
  87. package/dist/migrate/new.d.ts +13 -0
  88. package/dist/migrate/new.js +41 -0
  89. package/dist/migrate/runner.d.ts +28 -0
  90. package/dist/migrate/runner.js +124 -0
  91. package/dist/migrate/sql-text.d.ts +33 -0
  92. package/dist/migrate/sql-text.js +233 -0
  93. package/dist/schema/schema.d.ts +16 -0
  94. package/dist/schema/schema.js +111 -0
  95. package/dist/schema/unsupported.d.ts +3 -0
  96. package/dist/schema/unsupported.js +36 -0
  97. package/dist/schema/wasm.d.ts +8 -0
  98. package/dist/schema/wasm.js +32 -0
  99. package/package.json +50 -4
  100. package/vendor/prisma-schema-wasm/LICENSE +201 -0
  101. package/vendor/prisma-schema-wasm/README.md +32 -0
  102. package/vendor/prisma-schema-wasm/package.json +3 -0
  103. package/vendor/prisma-schema-wasm/prisma_schema_build.js +539 -0
  104. package/vendor/prisma-schema-wasm/prisma_schema_build_bg.wasm +0 -0
@@ -0,0 +1,216 @@
1
+ import { singleQuoted } from './strings.js';
2
+ const INTERVAL = '{ years?: number; months?: number; days?: number; hours?: number; minutes?: number; seconds?: number; milliseconds?: number }';
3
+ const POINT = '{ x: number; y: number }';
4
+ const CIRCLE = '{ x: number; y: number; radius: number }';
5
+ const text = (rowArray = ['string']) => ({
6
+ row: ['string'],
7
+ rowArray,
8
+ param: ['string'],
9
+ });
10
+ const number = { row: ['number'], rowArray: ['number'], param: ['number'] };
11
+ const date = { row: ['Date'], rowArray: ['Date'], param: ['Date', 'string'] };
12
+ const json = { row: ['unknown'], rowArray: ['unknown'], param: ['unknown'], json: true };
13
+ const unknown = { row: ['unknown'], rowArray: 'unknown', param: ['unknown'] };
14
+ const BUILTINS = [
15
+ [['int2', 'smallint', 'smallserial', 'serial2'], number],
16
+ [['int4', 'integer', 'int', 'serial', 'serial4'], number],
17
+ [
18
+ ['int8', 'bigint', 'bigserial', 'serial8'],
19
+ { row: ['string'], rowArray: ['string'], param: ['bigint', 'number', 'string'], bigint: true },
20
+ ],
21
+ [['numeric', 'decimal'], { row: ['string'], rowArray: ['number'], param: ['number', 'string'] }],
22
+ [['float4', 'real'], number],
23
+ [['float8', 'double precision', 'float'], number],
24
+ [['money'], text()],
25
+ [['text'], text()],
26
+ [['varchar', 'character varying'], text()],
27
+ [['bpchar', 'character', 'char'], text()],
28
+ [['name'], text('parse')],
29
+ [['uuid'], text()],
30
+ [['bool', 'boolean'], { row: ['boolean'], rowArray: ['boolean'], param: ['boolean'] }],
31
+ [['bytea'], { row: ['Buffer'], rowArray: ['Buffer'], param: ['Buffer'] }],
32
+ [['date'], date],
33
+ [['time', 'time without time zone'], text()],
34
+ [['timetz', 'time with time zone'], text()],
35
+ [['timestamp', 'timestamp without time zone'], date],
36
+ [['timestamptz', 'timestamp with time zone'], date],
37
+ [['interval'], { row: [INTERVAL], rowArray: [INTERVAL], param: ['string'] }],
38
+ [['json'], json],
39
+ [['jsonb'], json],
40
+ [['jsonpath'], text('parse')],
41
+ [['inet', 'cidr', 'macaddr'], text()],
42
+ [['macaddr8'], text('parse')],
43
+ [['point'], { row: [POINT], rowArray: [POINT], param: ['string'] }],
44
+ [['circle'], { row: [CIRCLE], rowArray: 'unknown', param: ['string'] }],
45
+ [['line', 'lseg', 'path', 'polygon'], text('parse')],
46
+ [['box'], text('unknown')],
47
+ [['tsvector', 'tsquery'], text('parse')],
48
+ [['xml'], text('parse')],
49
+ [['bit', 'varbit', 'bit varying'], text('parse')],
50
+ [['numrange'], text()],
51
+ [['int4range', 'int8range', 'tsrange', 'tstzrange', 'daterange'], text('parse')],
52
+ [
53
+ [
54
+ 'int4multirange',
55
+ 'int8multirange',
56
+ 'nummultirange',
57
+ 'tsmultirange',
58
+ 'tstzmultirange',
59
+ 'datemultirange',
60
+ ],
61
+ text('parse'),
62
+ ],
63
+ [['oid'], number],
64
+ [['regproc'], text()],
65
+ [
66
+ [
67
+ 'regclass',
68
+ 'regtype',
69
+ 'regprocedure',
70
+ 'regoper',
71
+ 'regoperator',
72
+ 'regconfig',
73
+ 'regdictionary',
74
+ 'regnamespace',
75
+ 'regrole',
76
+ 'regcollation',
77
+ ],
78
+ text('parse'),
79
+ ],
80
+ [['xid', 'xid8', 'cid', 'tid', 'pg_lsn', 'txid_snapshot', 'pg_snapshot'], text('parse')],
81
+ [['void'], { row: ['null'], rowArray: 'unknown', param: ['unknown'] }],
82
+ ];
83
+ const BUILTIN_BY_NAME = new Map(BUILTINS.flatMap(([names, builtin]) => names.map((name) => [name, builtin])));
84
+ const CITEXT = text('parse');
85
+ // R44: splits, unquotes and strips a type name, and counts its array dimensions.
86
+ export function normaliseType(column) {
87
+ let schema = column.type?.schema ?? '';
88
+ let name = column.type?.name ?? '';
89
+ if (schema === '') {
90
+ const dot = dotOutsideQuotes(name);
91
+ if (dot !== -1) {
92
+ schema = name.slice(0, dot);
93
+ name = name.slice(dot + 1);
94
+ }
95
+ }
96
+ schema = unquote(schema);
97
+ name = unquote(name);
98
+ name = name
99
+ .replace(/\(\s*\d+\s*(,\s*\d+\s*)?\)/g, ' ')
100
+ .replace(/ +/g, ' ')
101
+ .trim();
102
+ if (name.startsWith('interval '))
103
+ name = 'interval';
104
+ let count = 0;
105
+ while (name.endsWith('[]')) {
106
+ name = name.slice(0, -2).trimEnd();
107
+ count++;
108
+ }
109
+ const dims = Math.max(count, column.array_dims, column.is_array ? 1 : 0);
110
+ return { schema, name, dims };
111
+ }
112
+ function dotOutsideQuotes(name) {
113
+ let quoted = false;
114
+ for (let i = 0; i < name.length; i++) {
115
+ const char = name[i];
116
+ if (char === '"')
117
+ quoted = !quoted;
118
+ else if (char === '.' && !quoted)
119
+ return i;
120
+ }
121
+ return -1;
122
+ }
123
+ function unquote(part) {
124
+ if (part.length >= 2 && part.startsWith('"') && part.endsWith('"')) {
125
+ return part.slice(1, -1).replaceAll('""', '"');
126
+ }
127
+ return part;
128
+ }
129
+ // R45: built-ins, then citext in any schema, then the catalog's enums, else unknown.
130
+ export function resolveType(column, catalog) {
131
+ const { schema, name, dims } = normaliseType(column);
132
+ if (schema === '' || schema === 'pg_catalog') {
133
+ const builtin = BUILTIN_BY_NAME.get(name);
134
+ if (builtin !== undefined)
135
+ return { base: builtin, dims };
136
+ }
137
+ if (name === 'citext')
138
+ return { base: CITEXT, dims };
139
+ const vals = findEnum(catalog, schema, name);
140
+ if (vals !== undefined) {
141
+ // R47: string literals, in catalog order, escaped as R53 says.
142
+ const union = vals.map((val) => singleQuoted(val));
143
+ // An empty enum is `unknown` in a row, because `row[i]` can't be assigned to `never`,
144
+ // and `never` as a parameter.
145
+ if (union.length === 0)
146
+ return { base: { row: ['unknown'], rowArray: 'unknown', param: [] }, dims };
147
+ return { base: { row: union, rowArray: 'parse', param: union }, dims };
148
+ }
149
+ return { base: unknown, dims };
150
+ }
151
+ function findEnum(catalog, schema, name) {
152
+ if (catalog === null)
153
+ return undefined;
154
+ const inSchema = (schemaName) => catalog.schemas.find((s) => s.name === schemaName)?.enums.find((e) => e.name === name)?.vals;
155
+ if (schema !== '')
156
+ return inSchema(schema);
157
+ const fromDefault = inSchema(catalog.default_schema);
158
+ if (fromDefault !== undefined)
159
+ return fromDefault;
160
+ const found = catalog.schemas.flatMap((s) => s.enums.filter((e) => e.name === name));
161
+ return found.length === 1 ? found[0]?.vals : undefined;
162
+ }
163
+ export function isJson(type) {
164
+ return type.base.json === true;
165
+ }
166
+ // Whether a row column of this type is built with `parsePgArray` (✱).
167
+ export function needsParse(type) {
168
+ return type.dims > 0 && type.base.rowArray === 'parse';
169
+ }
170
+ // A row column's TypeScript type (R46, R48). `elementsNotNull` is R64's or `@notnull`'s
171
+ // narrowing of an array's elements.
172
+ export function rowType(type, nullable, elementsNotNull, bigint) {
173
+ const { base, dims } = type;
174
+ if (dims === 0) {
175
+ const members = base.bigint === true ? [bigint] : base.row;
176
+ return render(members, nullable);
177
+ }
178
+ if (base.rowArray === 'unknown')
179
+ return 'unknown';
180
+ // ✱ elements come from `parsePgArray` as strings, or as an enum's values.
181
+ const element = base.rowArray === 'parse' ? base.row : base.bigint === true ? [bigint] : base.rowArray;
182
+ const isUnknown = element.length === 1 && element[0] === 'unknown';
183
+ const elementMembers = isUnknown || elementsNotNull ? element : withNull(element);
184
+ return render([arrayOf(elementMembers, dims)], nullable);
185
+ }
186
+ // A parameter's TypeScript type (R33, R46, R48, R49).
187
+ export function paramType(type, nullable) {
188
+ const { base, dims } = type;
189
+ if (dims === 0)
190
+ return render(base.param, nullable);
191
+ // An unknown element makes the array unknown; json elements are known, and stringified (R34).
192
+ if (base.param.length === 1 && base.param[0] === 'unknown' && base.json !== true)
193
+ return 'unknown';
194
+ return render([arrayOf(base.param, dims)], nullable);
195
+ }
196
+ function withNull(members) {
197
+ return members.includes('null') ? members : [...members, 'null'];
198
+ }
199
+ function arrayOf(members, dims) {
200
+ const element = members.length === 0
201
+ ? 'never'
202
+ : members.length === 1
203
+ ? members[0]
204
+ : `(${members.join(' | ')})`;
205
+ return element + '[]'.repeat(dims);
206
+ }
207
+ // Joins union members, with `null` last when nullable. `unknown` absorbs `null`, and an
208
+ // empty union is `never`.
209
+ function render(members, nullable) {
210
+ if (members.length === 1 && members[0] === 'unknown')
211
+ return 'unknown';
212
+ const all = nullable ? withNull(members) : members;
213
+ if (all.length === 0)
214
+ return 'never';
215
+ return all.join(' | ');
216
+ }
@@ -0,0 +1,2 @@
1
+ export { defineConfig } from './config/define-config.ts';
2
+ export type { BigintMode, SequelConfig } from './config/define-config.ts';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ // The only part of sequel an app imports, and only from its config file.
2
+ export { defineConfig } from './config/define-config.js';
@@ -0,0 +1,23 @@
1
+ import { type Migration } from './files.ts';
2
+ export declare const PRISMA_LOCK_FILE = "migration_lock.toml";
3
+ export type FileAdoption = {
4
+ renamed: {
5
+ from: string;
6
+ to: string;
7
+ }[];
8
+ lockRemoved: boolean;
9
+ };
10
+ export declare function adoptPrismaFiles(folder: string, root: string): Promise<FileAdoption>;
11
+ export type HistoryAdoption = {
12
+ status: 'adopted';
13
+ rows: number;
14
+ } | {
15
+ status: 'already-adopted';
16
+ } | {
17
+ status: 'no-prisma-history';
18
+ };
19
+ export declare function adoptPrismaHistory(options: {
20
+ url: string;
21
+ migrations: Migration[];
22
+ lockWaitMs?: number;
23
+ }): Promise<HistoryAdoption>;
@@ -0,0 +1,119 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { readdir, rename, rm, rmdir } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { databaseName, withClient } from '../db/connection.js';
5
+ import { SequelError } from '../errors.js';
6
+ import { compare } from './files.js';
7
+ import { acquireLock, createHistoryTable, historyState, recordMigration, releaseLock, } from './history.js';
8
+ const PRISMA_FOLDER = /^\d{14}_[a-z0-9_]+$/;
9
+ export const PRISMA_LOCK_FILE = 'migration_lock.toml';
10
+ // TECH-SPEC §8, the files: each `<ts>_<name>/migration.sql` becomes `<ts>_<name>.sql` in the
11
+ // same parent folder, with its contents untouched, and Prisma's lock file goes. Every move is
12
+ // checked before any is made.
13
+ export async function adoptPrismaFiles(folder, root) {
14
+ let entries;
15
+ try {
16
+ entries = await readdir(folder, { recursive: true, withFileTypes: true });
17
+ }
18
+ catch (error) {
19
+ if (error.code === 'ENOENT')
20
+ return { renamed: [], lockRemoved: false };
21
+ throw error;
22
+ }
23
+ const shown = (file) => path.relative(root, file);
24
+ const moves = [];
25
+ const problems = [];
26
+ const targets = new Set();
27
+ for (const entry of entries) {
28
+ if (!entry.isFile() || entry.name !== 'migration.sql')
29
+ continue;
30
+ const from = path.join(entry.parentPath, entry.name);
31
+ const dir = entry.parentPath;
32
+ const name = path.basename(dir);
33
+ if (!PRISMA_FOLDER.test(name)) {
34
+ problems.push(`${shown(from)} is in a folder not named \`<YYYYMMDDHHMMSS>_<name>\`, with a lower-case name, so it has no migration identity. Rename the folder to Prisma's form, then run this again.`);
35
+ continue;
36
+ }
37
+ const to = path.join(path.dirname(dir), `${name}.sql`);
38
+ if (existsSync(to) || targets.has(to)) {
39
+ problems.push(`${shown(from)} would become ${shown(to)}, which already exists. Remove one.`);
40
+ continue;
41
+ }
42
+ targets.add(to);
43
+ moves.push({ from, to });
44
+ }
45
+ if (problems.length > 0) {
46
+ throw new SequelError(`Refusing to rename Prisma's migrations, so nothing changed:\n${problems.join('\n')}`);
47
+ }
48
+ moves.sort((a, b) => compare(a.from, b.from));
49
+ for (const move of moves) {
50
+ await rename(move.from, move.to);
51
+ // Prisma's folder held only the file; anything else stays, with its folder.
52
+ await rmdir(path.dirname(move.from)).catch(() => { });
53
+ }
54
+ const lock = path.join(folder, PRISMA_LOCK_FILE);
55
+ const lockRemoved = existsSync(lock);
56
+ if (lockRemoved)
57
+ await rm(lock);
58
+ return { renamed: moves, lockRemoved };
59
+ }
60
+ // TECH-SPEC §8, the history: under the runner's lock, in one transaction that also creates
61
+ // `sequel_migrations`, copy every applied Prisma row once its checksum matches its file.
62
+ // `_prisma_migrations` is only read.
63
+ export async function adoptPrismaHistory(options) {
64
+ const database = databaseName(options.url);
65
+ return withClient(options.url, async (client) => {
66
+ client.on('error', () => { });
67
+ await acquireLock(client, database, options.lockWaitMs);
68
+ try {
69
+ const state = await historyState(client, database);
70
+ if (state.hasHistory)
71
+ return { status: 'already-adopted' };
72
+ if (!state.hasPrismaHistory)
73
+ return { status: 'no-prisma-history' };
74
+ const { rows } = await client.query(`SELECT migration_name, checksum, finished_at, rolled_back_at
75
+ FROM _prisma_migrations ORDER BY migration_name COLLATE "C", started_at`);
76
+ const files = new Map(options.migrations.map((m) => [m.identity, m]));
77
+ const problems = [];
78
+ const adopted = new Map();
79
+ for (const row of rows) {
80
+ const name = row.migration_name;
81
+ if (row.rolled_back_at !== null)
82
+ continue;
83
+ if (row.finished_at === null) {
84
+ problems.push(`\`${name}\` was started by Prisma but never finished, and never rolled back: a Prisma deploy failed. Resolve it with Prisma first (\`prisma migrate resolve --applied\` or \`--rolled-back\`), then run this again.`);
85
+ continue;
86
+ }
87
+ if (adopted.has(name))
88
+ continue;
89
+ const file = files.get(name);
90
+ if (file === undefined) {
91
+ problems.push(`\`${name}\` is applied, but no migration file has that name. Restore it from version control.`);
92
+ }
93
+ else if (file.sha256 !== row.checksum) {
94
+ problems.push(`${file.shown} has changed since Prisma applied it (sha256 ${row.checksum} then, ${file.sha256} now). Restore the file from version control.`);
95
+ }
96
+ adopted.set(name, row);
97
+ }
98
+ if (problems.length > 0) {
99
+ throw new SequelError(`Refusing to adopt the Prisma history of ${database}, so nothing changed:\n${problems.map((p) => ` - ${p}`).join('\n')}`);
100
+ }
101
+ await client.query('BEGIN');
102
+ try {
103
+ await createHistoryTable(client);
104
+ for (const row of adopted.values()) {
105
+ await recordMigration(client, row.migration_name, row.checksum, row.finished_at);
106
+ }
107
+ await client.query('COMMIT');
108
+ }
109
+ catch (error) {
110
+ await client.query('ROLLBACK').catch(() => { });
111
+ throw error;
112
+ }
113
+ return { status: 'adopted', rows: adopted.size };
114
+ }
115
+ finally {
116
+ await releaseLock(client).catch(() => { });
117
+ }
118
+ });
119
+ }
@@ -0,0 +1,7 @@
1
+ import type { ResolvedConfig } from '../config/resolve-config.ts';
2
+ import { type DeployResult } from './runner.ts';
3
+ export declare function deployTo(config: ResolvedConfig, url: string, options: {
4
+ allowOutOfOrder?: boolean;
5
+ log: (message: string) => void;
6
+ quietWhenCurrent?: boolean;
7
+ }): Promise<DeployResult>;
@@ -0,0 +1,23 @@
1
+ import { databaseName } from '../db/connection.js';
2
+ import { findMigrations } from './files.js';
3
+ import { deployMigrations } from './runner.js';
4
+ // Deploys the app's migrations to one of its databases, under the runner's rules, and reports
5
+ // what happened through `log`.
6
+ export async function deployTo(config, url, options) {
7
+ const migrations = await findMigrations(config.migrations, { root: config.root });
8
+ const result = await deployMigrations({
9
+ url,
10
+ migrations,
11
+ timeouts: config.timeouts,
12
+ allowOutOfOrder: options.allowOutOfOrder ?? false,
13
+ log: options.log,
14
+ });
15
+ const database = databaseName(url);
16
+ for (const name of result.newerApplied) {
17
+ options.log(`\`${name}\` is applied to ${database} but has no file, and sorts after every file: it is from a newer build, as after a rollback. Continuing.`);
18
+ }
19
+ if (result.applied.length === 0 && !options.quietWhenCurrent) {
20
+ options.log(`No pending migrations: ${database} is up to date.`);
21
+ }
22
+ return result;
23
+ }
@@ -0,0 +1,10 @@
1
+ import type { ResolvedConfig } from '../config/resolve-config.ts';
2
+ import { type DiffResult } from '../engine/diff.ts';
3
+ import type { SchemaFile } from '../schema/schema.ts';
4
+ import type { Migration } from './files.ts';
5
+ export declare function diffMigrationsToSchema(options: {
6
+ config: ResolvedConfig;
7
+ migrations: Migration[];
8
+ files: SchemaFile[];
9
+ engineBinary?: string;
10
+ }): Promise<DiffResult>;
@@ -0,0 +1,36 @@
1
+ import { ensureBinary } from '../binaries/ensure-binary.js';
2
+ import { shadowOptions, withShadowDatabase } from '../db/shadow.js';
3
+ import { startSchemaEngine } from '../engine/client.js';
4
+ import { diffToSchema } from '../engine/diff.js';
5
+ import { SequelError } from '../errors.js';
6
+ import { deployMigrations } from './runner.js';
7
+ // TECH-SPEC §8: the runner replays the migrations into a shadow database, exactly as `deploy`
8
+ // applies them, then the engine diffs that database against the schema folder. A difference is
9
+ // drift for `check`, and the next migration for `migrate new`. It never touches the target.
10
+ export async function diffMigrationsToSchema(options) {
11
+ const { config } = options;
12
+ const engineBinary = options.engineBinary ?? (await ensureBinary('schema-engine'));
13
+ return withShadowDatabase(shadowOptions(config), async (shadowUrl) => {
14
+ let historySchema;
15
+ try {
16
+ ;
17
+ ({ historySchema } = await deployMigrations({
18
+ url: shadowUrl,
19
+ migrations: options.migrations,
20
+ timeouts: config.timeouts,
21
+ }));
22
+ }
23
+ catch (error) {
24
+ if (!(error instanceof SequelError))
25
+ throw error;
26
+ throw new SequelError(`Replaying the migrations into a shadow database failed:\n${error.message}`);
27
+ }
28
+ const engine = startSchemaEngine(engineBinary, config.schema);
29
+ try {
30
+ return await diffToSchema(engine, { url: shadowUrl, historySchema }, options.files);
31
+ }
32
+ finally {
33
+ await engine.close();
34
+ }
35
+ });
36
+ }
@@ -0,0 +1,19 @@
1
+ import { type ScannedSql } from './sql-text.ts';
2
+ export type Migration = {
3
+ identity: string;
4
+ file: string;
5
+ shown: string;
6
+ text: string;
7
+ sha256: string;
8
+ noTransaction: boolean;
9
+ scanned: ScannedSql;
10
+ };
11
+ export declare const MIGRATION_FILE: RegExp;
12
+ export declare const NAME: RegExp;
13
+ export declare function migrationIdentity(now: Date, name: string): string;
14
+ export declare function sha256(bytes: Buffer | string): string;
15
+ export declare function findMigrations(folder: string, options?: {
16
+ root?: string;
17
+ }): Promise<Migration[]>;
18
+ export declare function readMigrationFile(file: string, root: string): Promise<Migration>;
19
+ export declare function compare(a: string, b: string): number;
@@ -0,0 +1,97 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readdir, readFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { SequelError } from '../errors.js';
5
+ import { noTransactionMarker, scanSql } from './sql-text.js';
6
+ export const MIGRATION_FILE = /^(\d{14})_([a-z0-9_]+)\.sql$/;
7
+ export const NAME = /^[a-z0-9_]+$/;
8
+ // `<YYYYMMDDHHMMSS>_<name>`, with the time in UTC.
9
+ export function migrationIdentity(now, name) {
10
+ if (!NAME.test(name)) {
11
+ throw new SequelError(`The migration name \`${name}\` must be lower-case letters, digits and underscores, such as \`add_accounts\`.`);
12
+ }
13
+ const stamp = now.toISOString().replace(/[-:T]/g, '').slice(0, 14);
14
+ return `${stamp}_${name}`;
15
+ }
16
+ export function sha256(bytes) {
17
+ return createHash('sha256').update(bytes).digest('hex');
18
+ }
19
+ // TECH-SPEC §8: every `.sql` file under the folder, at any depth, ordered by identity. The path
20
+ // plays no part in order or identity. A missing folder holds no migrations.
21
+ export async function findMigrations(folder, options = {}) {
22
+ const root = options.root ?? process.cwd();
23
+ const shown = (file) => path.relative(root, file);
24
+ const files = await sqlFilesUnder(folder);
25
+ const problems = [];
26
+ const byIdentity = new Map();
27
+ for (const file of files) {
28
+ const base = path.basename(file);
29
+ if (base === 'migration.sql') {
30
+ problems.push(`${shown(file)} is in Prisma's layout. Run \`sequel migrate adopt-prisma\` to rename Prisma's migrations to \`<timestamp>_<name>.sql\` files.`);
31
+ continue;
32
+ }
33
+ if (!MIGRATION_FILE.test(base)) {
34
+ problems.push(`${shown(file)} is not named as a migration. Every .sql file under ${shown(folder) || '.'} must be named \`<YYYYMMDDHHMMSS>_<name>.sql\`, with a lower-case name; rename it, or move it out of the migrations folder.`);
35
+ continue;
36
+ }
37
+ const identity = base.slice(0, -'.sql'.length);
38
+ const existing = byIdentity.get(identity);
39
+ if (existing !== undefined) {
40
+ problems.push(`${shown(existing.file)} and ${shown(file)} are the same migration, \`${identity}\`. Delete one, or rename it with a new timestamp.`);
41
+ continue;
42
+ }
43
+ const bytes = await readFile(file);
44
+ const text = bytes.toString('utf8');
45
+ const scanned = scanSql(text);
46
+ byIdentity.set(identity, {
47
+ identity,
48
+ file,
49
+ shown: shown(file),
50
+ text,
51
+ sha256: sha256(bytes),
52
+ noTransaction: noTransactionMarker(scanned).noTransaction,
53
+ scanned,
54
+ });
55
+ }
56
+ if (problems.length > 0)
57
+ throw new SequelError(problems.join('\n'));
58
+ return [...byIdentity.values()].sort((a, b) => compare(a.identity, b.identity));
59
+ }
60
+ // Reads one file as a migration, for `migrate lint` on files named on the command line.
61
+ export async function readMigrationFile(file, root) {
62
+ const bytes = await readFile(file);
63
+ const text = bytes.toString('utf8');
64
+ const scanned = scanSql(text);
65
+ return {
66
+ identity: path.basename(file).replace(/\.sql$/, ''),
67
+ file,
68
+ shown: path.relative(root, file),
69
+ text,
70
+ sha256: sha256(bytes),
71
+ noTransaction: noTransactionMarker(scanned).noTransaction,
72
+ scanned,
73
+ };
74
+ }
75
+ // Identities compare by code point, so order never depends on the locale.
76
+ export function compare(a, b) {
77
+ return a < b ? -1 : a > b ? 1 : 0;
78
+ }
79
+ async function sqlFilesUnder(folder) {
80
+ let entries;
81
+ try {
82
+ entries = await readdir(folder, { recursive: true, withFileTypes: true });
83
+ }
84
+ catch (error) {
85
+ if (error.code === 'ENOENT')
86
+ return [];
87
+ throw error;
88
+ }
89
+ return entries
90
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.sql'))
91
+ .map((entry) => path.join(entry.parentPath, entry.name))
92
+ .filter((file) => !path
93
+ .relative(folder, file)
94
+ .split(path.sep)
95
+ .some((part) => part.startsWith('.')))
96
+ .sort(compare);
97
+ }
@@ -0,0 +1,20 @@
1
+ import type pg from 'pg';
2
+ export declare const HISTORY_TABLE = "sequel_migrations";
3
+ export declare const LOCK_KEY = "126879532410220";
4
+ export declare const LOCK_WAIT_MS: number;
5
+ export type HistoryRow = {
6
+ name: string;
7
+ sha256: string;
8
+ appliedAt: Date;
9
+ };
10
+ export type HistoryState = {
11
+ schema: string;
12
+ hasHistory: boolean;
13
+ hasPrismaHistory: boolean;
14
+ };
15
+ export declare function acquireLock(client: pg.Client, database: string, waitMs?: number): Promise<void>;
16
+ export declare function releaseLock(client: pg.Client): Promise<void>;
17
+ export declare function historyState(client: pg.Client, database: string): Promise<HistoryState>;
18
+ export declare function createHistoryTable(client: pg.Client): Promise<void>;
19
+ export declare function readHistory(client: pg.Client): Promise<HistoryRow[]>;
20
+ export declare function recordMigration(client: pg.Client, name: string, sha256: string, appliedAt?: Date): Promise<void>;
@@ -0,0 +1,54 @@
1
+ import { SequelError } from '../errors.js';
2
+ // sequel's migration history: one row per applied file, in the connection's default schema.
3
+ export const HISTORY_TABLE = 'sequel_migrations';
4
+ // The advisory lock every sequel run that writes the history takes: "sequel" as a number.
5
+ // It blocks only other sequel runs; application queries never take it.
6
+ export const LOCK_KEY = '126879532410220';
7
+ // How long a run waits for another run's lock before failing.
8
+ export const LOCK_WAIT_MS = 10 * 60_000;
9
+ // Takes the session-level advisory lock, waiting up to `waitMs`. Postgres applies
10
+ // `lock_timeout` to advisory locks, so the wait is bounded without polling.
11
+ export async function acquireLock(client, database, waitMs = LOCK_WAIT_MS) {
12
+ await client.query(`SET lock_timeout = '${Math.trunc(waitMs)}ms'`);
13
+ try {
14
+ await client.query('SELECT pg_advisory_lock($1::bigint)', [LOCK_KEY]);
15
+ }
16
+ catch (error) {
17
+ if (error.code === '55P03') {
18
+ throw new SequelError(`Another sequel run has held the migration lock on ${database} for ${Math.round(waitMs / 1000)} s. Wait for it to finish, then run this again.`);
19
+ }
20
+ throw error;
21
+ }
22
+ finally {
23
+ await client.query('RESET lock_timeout');
24
+ }
25
+ }
26
+ export async function releaseLock(client) {
27
+ await client.query('SELECT pg_advisory_unlock($1::bigint)', [LOCK_KEY]);
28
+ }
29
+ // Which histories the database has. Both names resolve through the search path, as an
30
+ // unqualified `CREATE TABLE` would place them.
31
+ export async function historyState(client, database) {
32
+ const { rows } = await client.query(`SELECT current_schema() AS schema,
33
+ to_regclass('${HISTORY_TABLE}') IS NOT NULL AS history,
34
+ to_regclass('_prisma_migrations') IS NOT NULL AS prisma`);
35
+ const row = rows[0];
36
+ if (row?.schema == null) {
37
+ throw new SequelError(`${database} has no schema on its search path to hold ${HISTORY_TABLE}. Create the schema, such as \`public\`, or fix the role's search_path.`);
38
+ }
39
+ return { schema: row.schema, hasHistory: row.history, hasPrismaHistory: row.prisma };
40
+ }
41
+ export async function createHistoryTable(client) {
42
+ await client.query(`CREATE TABLE IF NOT EXISTS ${HISTORY_TABLE} (
43
+ name text PRIMARY KEY,
44
+ sha256 text NOT NULL,
45
+ applied_at timestamptz NOT NULL DEFAULT now()
46
+ )`);
47
+ }
48
+ export async function readHistory(client) {
49
+ const { rows } = await client.query(`SELECT name, sha256, applied_at FROM ${HISTORY_TABLE} ORDER BY name COLLATE "C"`);
50
+ return rows.map((row) => ({ name: row.name, sha256: row.sha256, appliedAt: row.applied_at }));
51
+ }
52
+ export async function recordMigration(client, name, sha256, appliedAt) {
53
+ await client.query(`INSERT INTO ${HISTORY_TABLE} (name, sha256, applied_at) VALUES ($1, $2, coalesce($3, now()))`, [name, sha256, appliedAt ?? null]);
54
+ }
@@ -0,0 +1,21 @@
1
+ import { type Migration } from './files.ts';
2
+ import type { UnsupportedColumns } from '../schema/unsupported.ts';
3
+ export type LintFinding = {
4
+ shown: string;
5
+ line: number;
6
+ column: number;
7
+ rule: string;
8
+ message: string;
9
+ };
10
+ export declare const DEFAULT_SQUAWK_CONFIG = "pg_version = \"18.0\"\nexcluded_rules = [\n \"prefer-bigint-over-int\",\n \"prefer-identity\",\n \"require-enum-value-ordering\",\n \"require-lock-timeout\",\n \"require-statement-timeout\",\n]\n";
11
+ export declare const SQUAWK_CONFIG_FILE = ".squawk.toml";
12
+ export declare function migrationsToLint(migrations: Migration[], cutover: string | undefined): Migration[];
13
+ export declare function lintMigrations(options: {
14
+ squawk: string;
15
+ migrations: Migration[];
16
+ root: string;
17
+ unsupported?: UnsupportedColumns;
18
+ }): Promise<LintFinding[]>;
19
+ export declare function formatFinding(finding: LintFinding): string;
20
+ export declare function ownRules(migration: Migration, unsupported?: UnsupportedColumns): LintFinding[];
21
+ export declare function lintFailureMessage(findings: LintFinding[]): string;