@drhomet/mithaq 0.0.0-stage → 0.1.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.
Files changed (151) hide show
  1. package/LICENSE +22 -0
  2. package/LICENSES/payload.txt +22 -0
  3. package/LICENSES/ponytail.txt +21 -0
  4. package/dist/asas/actor.d.ts +96 -0
  5. package/dist/asas/actor.js +114 -0
  6. package/dist/asas/build.d.ts +231 -0
  7. package/dist/asas/build.js +787 -0
  8. package/dist/asas/cli/mithaq.d.ts +2 -0
  9. package/dist/asas/cli/mithaq.js +145 -0
  10. package/dist/asas/cli/upgrade.d.ts +20 -0
  11. package/dist/asas/cli/upgrade.js +147 -0
  12. package/dist/asas/http/fetch.d.ts +23 -0
  13. package/dist/asas/http/fetch.js +115 -0
  14. package/dist/asas/http/orpc.d.ts +43 -0
  15. package/dist/asas/http/orpc.js +135 -0
  16. package/dist/asas/runner/bulk.d.ts +20 -0
  17. package/dist/asas/runner/bulk.js +191 -0
  18. package/dist/asas/runner/errors.d.ts +60 -0
  19. package/dist/asas/runner/errors.js +59 -0
  20. package/dist/asas/runner/read.d.ts +68 -0
  21. package/dist/asas/runner/read.js +579 -0
  22. package/dist/asas/runner/runtime.d.ts +21 -0
  23. package/dist/asas/runner/runtime.js +8 -0
  24. package/dist/asas/runner/steps.d.ts +400 -0
  25. package/dist/asas/runner/steps.js +271 -0
  26. package/dist/asas/runner/tenant.d.ts +54 -0
  27. package/dist/asas/runner/tenant.js +213 -0
  28. package/dist/asas/runner/write.d.ts +444 -0
  29. package/dist/asas/runner/write.js +791 -0
  30. package/dist/auth/better-auth/access.d.ts +15 -0
  31. package/dist/auth/better-auth/access.js +71 -0
  32. package/dist/auth/better-auth/generate.d.ts +29 -0
  33. package/dist/auth/better-auth/generate.js +310 -0
  34. package/dist/auth/better-auth/index.d.ts +92 -0
  35. package/dist/auth/better-auth/index.js +81 -0
  36. package/dist/auth/better-auth/tenant.d.ts +18 -0
  37. package/dist/auth/better-auth/tenant.js +26 -0
  38. package/dist/auth/local/index.d.ts +285 -0
  39. package/dist/auth/local/index.js +736 -0
  40. package/dist/base/decimal.d.ts +9 -0
  41. package/dist/base/decimal.js +16 -0
  42. package/dist/base/fields.d.ts +123 -0
  43. package/dist/base/fields.js +51 -0
  44. package/dist/base/index.d.ts +46 -0
  45. package/dist/base/index.js +5 -0
  46. package/dist/base/kinds.d.ts +54 -0
  47. package/dist/base/kinds.js +78 -0
  48. package/dist/base/rules.d.ts +73 -0
  49. package/dist/base/rules.js +80 -0
  50. package/dist/drizzle/index.d.ts +438 -0
  51. package/dist/drizzle/index.js +832 -0
  52. package/dist/drizzle/kinds.d.ts +27 -0
  53. package/dist/drizzle/kinds.js +41 -0
  54. package/dist/drizzle/migrate.d.ts +133 -0
  55. package/dist/drizzle/migrate.js +1285 -0
  56. package/dist/drizzle/proxy/index.d.ts +1 -0
  57. package/dist/drizzle/proxy/index.js +1 -0
  58. package/dist/drizzle/proxy/node-postgres.d.ts +1 -0
  59. package/dist/drizzle/proxy/node-postgres.js +1 -0
  60. package/dist/drizzle/proxy/pg-core.d.ts +1 -0
  61. package/dist/drizzle/proxy/pg-core.js +1 -0
  62. package/dist/drizzle/rules.d.ts +336 -0
  63. package/dist/drizzle/rules.js +44 -0
  64. package/dist/index.d.ts +45 -0
  65. package/dist/index.js +36 -0
  66. package/dist/mithaq/define.d.ts +288 -0
  67. package/dist/mithaq/define.js +43 -0
  68. package/dist/mithaq/generate-messages.d.ts +38 -0
  69. package/dist/mithaq/generate-messages.js +234 -0
  70. package/dist/mithaq/language.d.ts +335 -0
  71. package/dist/mithaq/language.js +76 -0
  72. package/dist/mithaq/lock.d.ts +71 -0
  73. package/dist/mithaq/lock.js +329 -0
  74. package/dist/mithaq/messages.d.ts +55 -0
  75. package/dist/mithaq/messages.js +106 -0
  76. package/dist/mithaq/refusal.d.ts +6 -0
  77. package/dist/mithaq/refusal.js +6 -0
  78. package/dist/mithaq/resolve.d.ts +10 -0
  79. package/dist/mithaq/resolve.js +954 -0
  80. package/dist/mithaq/rows.d.ts +87 -0
  81. package/dist/mithaq/rows.js +174 -0
  82. package/dist/mithaq/rukn.d.ts +72 -0
  83. package/dist/mithaq/rukn.js +15 -0
  84. package/dist/mithaq/standard-schema.d.ts +38 -0
  85. package/dist/mithaq/standard-schema.js +2 -0
  86. package/dist/mithaq/types.d.ts +358 -0
  87. package/dist/mithaq/types.js +15 -0
  88. package/dist/mithaq/user.d.ts +19 -0
  89. package/dist/mithaq/user.js +16 -0
  90. package/dist/mithaq/where.d.ts +32 -0
  91. package/dist/mithaq/where.js +87 -0
  92. package/dist/rbac/index.d.ts +105 -0
  93. package/dist/rbac/index.js +182 -0
  94. package/dist/rbac/steps.d.ts +22 -0
  95. package/dist/rbac/steps.js +161 -0
  96. package/dist/rich-text/index.d.ts +27 -0
  97. package/dist/rich-text/index.js +58 -0
  98. package/dist/rich-text/rich-text.d.ts +159 -0
  99. package/dist/rich-text/rich-text.js +372 -0
  100. package/dist/screen/client.d.ts +153 -0
  101. package/dist/screen/client.js +68 -0
  102. package/dist/screen/generate.d.ts +14 -0
  103. package/dist/screen/generate.js +226 -0
  104. package/dist/screen/index.d.ts +479 -0
  105. package/dist/screen/index.js +645 -0
  106. package/dist/screen/react.d.ts +95 -0
  107. package/dist/screen/react.js +58 -0
  108. package/dist/screen/studio.d.ts +24 -0
  109. package/dist/screen/studio.js +68 -0
  110. package/dist/service/index.d.ts +63 -0
  111. package/dist/service/index.js +15 -0
  112. package/dist/storage/allowed-type.d.ts +5 -0
  113. package/dist/storage/allowed-type.js +7 -0
  114. package/dist/storage/index.d.ts +43 -0
  115. package/dist/storage/index.js +44 -0
  116. package/dist/storage/storage.d.ts +384 -0
  117. package/dist/storage/storage.js +405 -0
  118. package/dist/versions/index.d.ts +37 -0
  119. package/dist/versions/index.js +8 -0
  120. package/dist/versions/versions.d.ts +104 -0
  121. package/dist/versions/versions.js +160 -0
  122. package/dist/zod/index.d.ts +184 -0
  123. package/dist/zod/index.js +121 -0
  124. package/dist/zod/kinds.d.ts +34 -0
  125. package/dist/zod/kinds.js +49 -0
  126. package/dist/zod/rules.d.ts +117 -0
  127. package/dist/zod/rules.js +67 -0
  128. package/package.json +63 -4
  129. package/r/mithaq-cells.json +20 -0
  130. package/r/mithaq-controls.json +37 -0
  131. package/r/mithaq-directional-icon.json +15 -0
  132. package/r/mithaq-expected-updated-at.json +15 -0
  133. package/r/mithaq-field-shell.json +18 -0
  134. package/r/mithaq-filter-operators.json +15 -0
  135. package/r/mithaq-form.json +33 -0
  136. package/r/mithaq-list-columns.json +23 -0
  137. package/r/mithaq-list-filters.json +24 -0
  138. package/r/mithaq-list-pagination.json +24 -0
  139. package/r/mithaq-list-query.json +18 -0
  140. package/r/mithaq-list-selection.json +19 -0
  141. package/r/mithaq-list.json +33 -0
  142. package/r/mithaq-nav.json +24 -0
  143. package/r/mithaq-nodes.json +34 -0
  144. package/r/mithaq-relationship-cell.json +20 -0
  145. package/r/mithaq-relationship-control.json +20 -0
  146. package/r/mithaq-rich-text.json +40 -0
  147. package/r/mithaq-screens-host.json +96 -0
  148. package/r/mithaq-storage.json +44 -0
  149. package/r/mithaq-upload.json +29 -0
  150. package/r/registry.json +550 -0
  151. package/README.md +0 -3
@@ -0,0 +1,1285 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ import { execFile } from 'node:child_process';
10
+ import { existsSync, readFileSync } from 'node:fs';
11
+ import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promises';
12
+ import { tmpdir } from 'node:os';
13
+ import { join, resolve as resolvePath } from 'node:path';
14
+ import { fileURLToPath, pathToFileURL } from 'node:url';
15
+ import { getTableName, sql } from 'drizzle-orm';
16
+ import { readMigrationFiles } from 'drizzle-orm/migrator';
17
+ import { getMigrationsToRun } from 'drizzle-orm/migrator.utils';
18
+ import { drizzle } from 'drizzle-orm/node-postgres';
19
+ import { migrate as applyMigrations } from 'drizzle-orm/node-postgres/migrator';
20
+ import { Refusal } from '../mithaq/refusal.js';
21
+ import { createHistory, devPushMarker, historyLock, historyTable, joinTableOf, manyFields, migrationsTable, recordMigration, refuseStrangers, schemaOf } from './index.js';
22
+ // `bin.cjs` is not in drizzle-kit's `exports`, so it is resolved as the root entry's
23
+ // sibling; resolved from this file, so a consumer gets its own `mithaq`'s copy.
24
+ export const binary = fileURLToPath(new URL('bin.cjs', import.meta.resolve('drizzle-kit')));
25
+ const configName = 'drizzle.config.ts';
26
+ const schemaName = 'schema.ts';
27
+ const hintsName = 'mithaq.hints.json';
28
+ const identifier = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
29
+ // The first line of both written files, so a reader knows what regenerates them.
30
+ export const header = '// Written by `mithaq migrate`. Commit it.';
31
+ // No `defineConfig`: the loader resolves a config's imports from its own directory,
32
+ // which may not reach a workspace-root `drizzle-kit`. Paths come from `import.meta.url`,
33
+ // since drizzle-kit resolves a relative `out` against the working directory.
34
+ const configSource = (asasFrom) => `${header}
35
+ import { fileURLToPath } from 'node:url'
36
+ import { asas } from '${asasFrom}'
37
+
38
+ export default {
39
+ dialect: 'postgresql',
40
+ schema: fileURLToPath(new URL('${schemaName}', import.meta.url)),
41
+ out: fileURLToPath(new URL('.', import.meta.url)),
42
+ dbCredentials: { url: asas.targets.drizzle.connectionString },
43
+ migrations: { schema: '${migrationsTable.schema}', table: '${migrationsTable.table}' },
44
+ }
45
+ `;
46
+ // Every table named literally: drizzle-kit reads named exports alone, and ESM has no
47
+ // dynamic named exports.
48
+ const schemaSource = ({ tables, schemas, enums, asasFrom }) => {
49
+ for (const table of tables) {
50
+ if (identifier.test(table))
51
+ continue;
52
+ throw new Refusal(`Kayan "${table}" cannot be a named export of ${schemaName}, and drizzle-kit reads named exports alone, so nothing would migrate its table. Rename the kayan.`);
53
+ }
54
+ return `${header}
55
+ // Every table the language built, named one per line. drizzle-kit reads named
56
+ // exports alone: an object export contributes no table and reports no changes
57
+ // forever, and a partial export still emits foreign keys to the tables it left
58
+ // out. \`mithaq migrate\` rewrites this file when a kayan is added or removed.
59
+ import { asas } from '${asasFrom}'
60
+
61
+ export const {
62
+ ${tables.map((table) => ` ${table},`).join('\n')}
63
+ } = asas.targets.drizzle.tables
64
+ ${schemas.length ? `
65
+ // And every schema those tables are in but \`public\`, because drizzle-kit
66
+ // reads a schema only from an export and otherwise writes no CREATE SCHEMA
67
+ // for it, and once one is written, a DROP SCHEMA.
68
+ export const {
69
+ ${schemas.map((schema) => ` ${schema}: ${schema}Schema,`).join('\n')}
70
+ } = asas.targets.drizzle.schemas
71
+ ` : ''}${enums.length ? `
72
+ // And every enum their columns use, in its own schema, because drizzle-kit
73
+ // reads an enum only from an export and otherwise writes no CREATE TYPE for it.
74
+ export const {
75
+ ${enums.map(([key, name]) => ` ${JSON.stringify(key)}: ${name},`).join('\n')}
76
+ } = asas.targets.drizzle.enums
77
+ ` : ''}`;
78
+ };
79
+ // An enum in a schema no table is in is refused: no migration would create that schema.
80
+ const enumExports = ({ enums, schemas, taken }) => {
81
+ const names = new Set(taken);
82
+ return Object.entries(enums).map(([key, one]) => {
83
+ const schema = one.schema ?? 'public';
84
+ if (!schemas.includes(schema)) {
85
+ throw new Refusal(`A column uses the enum "${key}", in a schema no kayan is built in, so no migration would create that schema. Build the enum in the schemaName of a kayan.`);
86
+ }
87
+ const name = `${schema}_${one.enumName}Enum`;
88
+ if (!identifier.test(name) || names.has(name)) {
89
+ throw new Refusal(`The enum "${key}" cannot be exported from ${schemaName} as ${JSON.stringify(name)}, which is ${names.has(name) ? 'already a name the module exports' : 'not a JavaScript name'}, and drizzle-kit reads an enum only from a named export. Rename the enum.`);
90
+ }
91
+ names.add(name);
92
+ return [key, name];
93
+ });
94
+ };
95
+ // Every statement type not named here is refused. `drop_check`, `drop_enum` and
96
+ // `drop_sequence` lose no row but are asked about by `guardDroppedBy`.
97
+ const safe = (type) => /^(create|add|rename|move)_/.test(type) ||
98
+ [
99
+ 'alter_check',
100
+ 'alter_column',
101
+ 'alter_enum',
102
+ 'alter_policy',
103
+ 'alter_rls',
104
+ 'alter_role',
105
+ 'alter_sequence',
106
+ 'alter_unique',
107
+ 'drop_fk',
108
+ 'drop_index',
109
+ 'drop_policy',
110
+ 'drop_role',
111
+ 'drop_unique',
112
+ 'recreate_enum',
113
+ 'recreate_fk',
114
+ 'recreate_index',
115
+ 'recreate_policy',
116
+ ].includes(type);
117
+ // A statement passed through unread would be a drop nothing counted rows for.
118
+ // An identifier that is not a non-empty string would read as nothing to lose.
119
+ export const droppedBy = (statement) => {
120
+ // Destructuring a missing container would be a `TypeError` where every other unreadable statement is a sentence.
121
+ const held = (value) => value && typeof value === 'object' ? value : {};
122
+ const named = (kind, container, ...keys) => {
123
+ const entity = keys.map((key) => held(container)[key]);
124
+ if (entity.some((part) => typeof part !== 'string' || part === '')) {
125
+ throw new Refusal(`drizzle-kit reported a ${statement.type} naming ${JSON.stringify(entity)}, and the data-loss probe cannot ask a database whether that holds rows. Report it with the change that produced it.`);
126
+ }
127
+ return { kind, entity: entity };
128
+ };
129
+ switch (statement.type) {
130
+ case 'drop_table':
131
+ return named('table', statement.table, 'schema', 'name');
132
+ case 'drop_column':
133
+ return named('column', statement.column, 'schema', 'table', 'name');
134
+ // mithaq never drops a schema. A renamed schema would carry another tool's tables with
135
+ // it, so it is refused.
136
+ case 'drop_schema':
137
+ return undefined;
138
+ case 'rename_schema': {
139
+ const { from, to } = statement;
140
+ throw new Refusal(`drizzle-kit renames the schema ${String(from?.name)} to ${String(to?.name)}, which would carry every object in ${String(from?.name)} with it, another tool's among them, and mithaq never renames a schema. Reply \`create\` for the schema and \`rename\` for each of the language's tables in ${hintsName}, which moves those tables alone and keeps ${String(from?.name)}.`);
141
+ }
142
+ case 'drop_pk': {
143
+ // drizzle's own default: `statement.pk.schema ?? "public"`.
144
+ const pk = held(statement.pk);
145
+ return named('primary_key', { ...pk, schema: pk.schema ?? 'public' }, 'schema', 'table', 'name');
146
+ }
147
+ case 'drop_view': {
148
+ // A plain view stores no rows of its own.
149
+ const view = held(statement.view);
150
+ return view.materialized ? named('view', view, 'schema', 'name') : undefined;
151
+ }
152
+ case 'recreate_column':
153
+ return named('column', held(statement.diff).$right, 'schema', 'table', 'name');
154
+ }
155
+ if (safe(statement.type))
156
+ return undefined;
157
+ throw new Refusal(`\`mithaq migrate\` has not measured whether a ${statement.type} statement loses data, and it will not write a migration carrying one nothing counted rows for. Report it with the change that produced it.`);
158
+ };
159
+ // drizzle-kit writes a `public` table unqualified, so it runs in `current_schema()`;
160
+ // asking `"public"."t"` would read another table whenever `search_path` differs.
161
+ const tableAt = (schema, table) => schema === 'public' ? sql.identifier(table) : sql `${sql.identifier(schema)}.${sql.identifier(table)}`;
162
+ // `SET DATA TYPE … USING` to `numeric(p, s)` or `integer` rounds a value with no error.
163
+ const roundedBy = (statement) => {
164
+ if (statement.type !== 'alter_column')
165
+ return undefined;
166
+ const { diff, to } = statement;
167
+ const target = diff?.type?.to;
168
+ const sized = typeof target === 'string' ? /^numeric\((\d+),\s*(\d+)\)$/.exec(target) : null;
169
+ const type = sized ? `numeric(${sized[1]},${sized[2]})` : target === 'integer' ? 'integer' : undefined;
170
+ if (!type || typeof to?.table !== 'string' || typeof to.name !== 'string')
171
+ return undefined;
172
+ return { schema: typeof to.schema === 'string' && to.schema ? to.schema : 'public', table: to.table, column: to.name, type };
173
+ };
174
+ // drizzle renames and moves first and drops or narrows after, so the rows are asked
175
+ // about under their name before the run; under the new name they would read as empty.
176
+ const storedAs = (at, statements) => {
177
+ const renamed = statements;
178
+ const column = renamed.find((one) => {
179
+ const to = one.to;
180
+ return one.type === 'rename_column' && to?.name === at.column && to.table === at.table;
181
+ })?.from;
182
+ let { schema, table } = at;
183
+ for (const one of [...renamed].reverse()) {
184
+ if (one.type === 'move_table' && one.to === schema && one.name === table && typeof one.from === 'string')
185
+ schema = one.from;
186
+ if (one.type === 'rename_table' && (one.schema ?? 'public') === schema && one.to === table && typeof one.from === 'string')
187
+ table = one.from;
188
+ }
189
+ return { ...at, schema, table, column: typeof column?.name === 'string' ? column.name : at.column };
190
+ };
191
+ // A value that would round, overflow (`22003`) or read as no number (`22P02`) is counted,
192
+ // since the migration would round or stop on it.
193
+ const roundedRows = async (db, { schema, table, column, type }) => {
194
+ const cell = sql.identifier(column);
195
+ try {
196
+ const { rows } = await db.execute(sql `select count(*)::int as changed from ${tableAt(schema, table)} where ${cell} is not null and ${cell}::${sql.raw(type)} <> ${cell}::numeric`);
197
+ return rows[0].changed;
198
+ }
199
+ catch (error) {
200
+ const code = error.cause?.code;
201
+ // Every folder is applied before a write counts, so a missing table or column was
202
+ // lost outside the migrations and holds nothing to round.
203
+ if (code === '42P01' || code === '42703' || code === '42846')
204
+ return 0;
205
+ if (code === '22003')
206
+ return 'overflow';
207
+ if (code === '22P02')
208
+ return 'unread';
209
+ throw error;
210
+ }
211
+ };
212
+ // Postgres refuses NOT NULL over a row holding no value, so those rows are counted
213
+ // before anything is written.
214
+ const nulledBy = (statement) => {
215
+ const { type, diff, to, column, isCompositePK } = statement;
216
+ const at = type === 'alter_column' && diff?.notNull?.to === true && !diff.identity?.to ? to
217
+ : type === 'add_column' && column?.notNull === true && !column.default && !column.identity && !column.generated && !isCompositePK &&
218
+ // drizzle-kit's own test (`bin.cjs` `return /^(?:serial|bigserial|smallserial)$/i.test(type);`).
219
+ !/^(?:serial|bigserial|smallserial)$/i.test(String(column.type)) ? column
220
+ : undefined;
221
+ if (typeof at?.table !== 'string' || typeof at.name !== 'string')
222
+ return undefined;
223
+ return { schema: typeof at.schema === 'string' && at.schema ? at.schema : 'public', table: at.table, column: at.name, added: type === 'add_column' };
224
+ };
225
+ const nulledRows = async (db, { schema, table, column, added }) => {
226
+ try {
227
+ const { rows } = await db.execute(sql `select count(*)::int as nulled from ${tableAt(schema, table)}${added ? sql `` : sql ` where ${sql.identifier(column)} is null`}`);
228
+ return rows[0].nulled;
229
+ }
230
+ catch (error) {
231
+ const code = error.cause?.code;
232
+ if (code === '42P01' || code === '42703')
233
+ return 0;
234
+ throw error;
235
+ }
236
+ };
237
+ // Postgres refuses a primary key over two rows sharing it.
238
+ const keyedBy = (statement) => {
239
+ if (statement.type !== 'add_pk')
240
+ return undefined;
241
+ const { schema, table, columns } = statement.pk ?? {};
242
+ if (typeof table !== 'string' || !Array.isArray(columns) || !columns.length || !columns.every((column) => typeof column === 'string'))
243
+ return undefined;
244
+ return { schema: typeof schema === 'string' && schema ? schema : 'public', table, columns: columns };
245
+ };
246
+ const duplicatedRows = async (db, { schema, table, columns }) => {
247
+ try {
248
+ const { rows } = await db.execute(sql `select coalesce(sum(held), 0)::int as duplicated from (select count(*) as held from ${tableAt(schema, table)} group by ${sql.join(columns.map((column) => sql.identifier(column)), sql `, `)} having count(*) > 1) as shared`);
249
+ return rows[0].duplicated;
250
+ }
251
+ catch (error) {
252
+ const code = error.cause?.code;
253
+ if (code === '42P01' || code === '42703')
254
+ return 0;
255
+ throw error;
256
+ }
257
+ };
258
+ /** An SQL identifier, quoted as Postgres reads one. */
259
+ const quotedName = (name) => `"${name.replaceAll('"', '""')}"`;
260
+ /** An SQL string literal. */
261
+ const quotedText = (value) => `'${value.replaceAll("'", "''")}'`;
262
+ /** Words in a sentence: `a`, `a and b`, `a, b and c`. */
263
+ const listed = (values) => values.join(', ').replace(/, ([^,]*)$/, ' and $1');
264
+ /** A table as a migration names it: bare in `public`, as drizzle-kit writes one. */
265
+ const tableNamed = (schema, table) => (schema === 'public' ? quotedName(table) : `${quotedName(schema)}.${quotedName(table)}`);
266
+ // A Payload `<table>_rels` is copied into the kayan's join tables and dropped. Anything
267
+ // the copy cannot carry is refused, since the drop would lose it.
268
+ const payloadRels = async ({ db, kayan, tables, dropped }) => {
269
+ const own = new Set(Object.values(tables).map((table) => `${schemaOf(table)}.${getTableName(table)}`));
270
+ const found = [];
271
+ for (const one of kayan) {
272
+ if (one.targets?.[one.store])
273
+ continue;
274
+ const schema = one.schemaName ?? 'public';
275
+ const rels = `${one.table}_rels`;
276
+ if (own.has(`${schema}.${rels}`) || (dropped && !dropped({ schema, table: rels })))
277
+ continue;
278
+ const { rows } = await db.execute(sql `select column_name from information_schema.columns
279
+ where table_schema = ${schema === 'public' ? sql `current_schema()` : schema} and table_name = ${rels}`);
280
+ const columns = rows.map((row) => row.column_name);
281
+ if (!columns.includes('path') || !columns.includes('parent_id') || !columns.includes('order'))
282
+ continue;
283
+ // Payload's own foreign key from `parent_id`, which a project's own table does not carry.
284
+ const { rows: [parentKey] } = await db.execute(sql `select count(*)::int as held from pg_catalog.pg_constraint
285
+ join pg_catalog.pg_attribute on attrelid = conrelid and attnum = conkey[1]
286
+ where contype = 'f' and cardinality(conkey) = 1 and attname = 'parent_id' and conname like '%\_fk'
287
+ and conrelid = to_regclass(${tableNamed(schema, rels)}) and confrelid = to_regclass(${tableNamed(schema, one.table)})`);
288
+ if (!parentKey.held) {
289
+ // A migration reaches this only for tables its snapshot already drops.
290
+ if (!dropped)
291
+ console.log(`${schema}.${rels} is left alone because it lacks Payload's parent key, a foreign key from parent_id to ${schema}.${one.table} whose name ends in _fk.`);
292
+ continue;
293
+ }
294
+ const at = `${schema}.${rels}`;
295
+ const from = tableAt(schema, rels);
296
+ const targets = columns.filter((column) => column.endsWith('_id') && column !== 'parent_id');
297
+ const strange = targets.filter((column) => !own.has(`${schema}.${column.slice(0, -'_id'.length)}`));
298
+ if (strange.length) {
299
+ throw new Refusal(`${at} has the ${strange.length === 1 ? 'column' : 'columns'} ${strange.join(', ')}, pointing at ${strange.length === 1 ? 'a collection' : 'collections'} no kayan of this language builds in ${schema}, so the baseline would hold a foreign key to a table it never creates. Nothing was written. Port ${strange.length === 1 ? 'that collection' : 'those collections'} as kayan first, or drop the ${strange.length === 1 ? 'column' : 'columns'} once no row uses ${strange.length === 1 ? 'it' : 'them'}.`);
300
+ }
301
+ const copies = manyFields(one).flatMap((field) => {
302
+ const column = `${field.target.table}_id`;
303
+ return targets.includes(column) ? [{ field: field.name, join: joinTableOf(one, field), column }] : [];
304
+ });
305
+ const paths = (await db.execute(sql `select distinct path from ${from} order by path`)).rows.map((row) => row.path);
306
+ const unknown = paths.filter((path) => !copies.some((copy) => copy.field === path));
307
+ if (unknown.length) {
308
+ throw new Refusal(`${at} holds rows under the ${unknown.length === 1 ? 'path' : 'paths'} ${listed(unknown.map((path) => JSON.stringify(path)))}, which ${unknown.length === 1 ? 'names' : 'name'} no hasMany field of kayan "${one.name}", so the first migration after a baseline would drop them. Nothing was written. Each path of a Payload \`_rels\` table is copied into the join table of the hasMany field it names, and a field nested in a group, an array or a block is none.`);
309
+ }
310
+ if (!copies.length)
311
+ continue;
312
+ const rowsOf = (count) => (count === 1 ? '1 row' : `${count} rows`);
313
+ for (const { field, column } of copies) {
314
+ const elsewhere = targets.filter((other) => other !== column);
315
+ const { rows: [counted] } = await db.execute(sql `select
316
+ count(*) filter (where ${elsewhere.length ? sql.join(elsewhere.map((other) => sql `${sql.identifier(other)} is not null`), sql ` or `) : sql `false`})::int as elsewhere,
317
+ ${columns.includes('locale') ? sql `count(*) filter (where "locale" is not null)::int` : sql `0`} as localized,
318
+ count(*) filter (where ${sql.identifier(column)} is null or "order" is null)::int as empty,
319
+ (select coalesce(sum(held), 0)::int from (select count(*) as held from ${from} where path = ${field} group by parent_id, "order" having count(*) > 1) as shared) as shared
320
+ from ${from} where path = ${field}`);
321
+ const { elsewhere: other, localized, empty, shared } = counted;
322
+ const target = column.slice(0, -'_id'.length);
323
+ const refused = other ? `${rowsOf(other)} under "${field}" that ${other === 1 ? 'points' : 'point'} at a collection other than ${target}, a polymorphic relationship, and a mithaq join table points at one kayan`
324
+ : localized ? `${rowsOf(localized)} under "${field}" with a locale, a localized relationship, and a mithaq join table holds one set for every locale`
325
+ : empty ? `${rowsOf(empty)} under "${field}" with no ${column} or no order, and a join table's columns are NOT NULL`
326
+ : shared ? `${rowsOf(shared)} under "${field}" whose (parent_id, order) another row also holds, and a join table is keyed on the pair`
327
+ : undefined;
328
+ if (refused)
329
+ throw new Refusal(`${at} holds ${refused}. Nothing was written. Fix or delete each such row, and run the command again.`);
330
+ }
331
+ found.push({ schema, rels, kayan: one.name, copies, targets, locale: columns.includes('locale') });
332
+ }
333
+ return found;
334
+ };
335
+ // `drizzle-kit migrate` applies a migration in one transaction, so a failure leaves every
336
+ // table as it was. A row the copy would not carry stops it first.
337
+ const copyOf = ({ found: { schema, rels, kayan, copies, targets, locale }, drop }) => {
338
+ const at = (table) => tableNamed(schema, table);
339
+ const carried = copies.map(({ field, column }) => `("path" = ${quotedText(field)} AND ${[`${quotedName(column)} IS NOT NULL`, ...targets.filter((other) => other !== column).map((other) => `${quotedName(other)} IS NULL`)].join(' AND ')})`);
340
+ const kept = `(${carried.join(' OR ') || 'FALSE'})${locale ? ' AND "locale" IS NULL' : ''}`;
341
+ return [
342
+ `-- Payload's relationship rows, copied into each hasMany field's join table, and then ${rels} dropped
343
+ --. A row no field of ${kayan} carries stops the migration first.
344
+ DO $$ DECLARE lost integer; BEGIN
345
+ SELECT count(*) INTO lost FROM ${at(rels)} WHERE NOT (${kept});
346
+ IF lost > 0 THEN
347
+ RAISE EXCEPTION '${rels} holds % rows that no hasMany field of ${kayan} carries, under another path, another collection or a locale, and dropping it would lose them. Nothing was applied.', lost;
348
+ END IF;
349
+ END $$;`,
350
+ ...copies.map(({ field, join, column }) => `INSERT INTO ${at(join)} ("parent_id", "target_id", "order") SELECT "parent_id", ${quotedName(column)}, "order" FROM ${at(rels)} WHERE "path" = ${quotedText(field)};`),
351
+ drop,
352
+ ];
353
+ };
354
+ // A `DROP TYPE` fails while a table outside the language still uses the enum.
355
+ const enumUsedBy = async ({ db, schema, name, mine }) => {
356
+ const type = tableNamed(schema, name);
357
+ const { rows } = await db.execute(sql `select distinct nspname || '.' || relname as at from pg_catalog.pg_attribute
358
+ join pg_catalog.pg_class on pg_class.oid = attrelid
359
+ join pg_catalog.pg_namespace on pg_namespace.oid = relnamespace
360
+ where attnum > 0 and not attisdropped and relkind in ('r', 'p')
361
+ and atttypid in (to_regtype(${type}), (select typarray from pg_catalog.pg_type where oid = to_regtype(${type})))`);
362
+ return rows.map((row) => row.at).filter((at) => !mine(at)).sort();
363
+ };
364
+ // The diff is against the last snapshot, so drizzle-kit would create an enum another tool
365
+ // already made, and later alter another tool's type.
366
+ const heldEnum = async (db, statement) => {
367
+ if (statement.type !== 'create_enum')
368
+ return undefined;
369
+ const { schema, name } = statement.enum ?? {};
370
+ if (typeof schema !== 'string' || typeof name !== 'string')
371
+ return undefined;
372
+ const { rows } = await db.execute(sql `select array_agg(enumlabel::text order by enumsortorder) as labels from pg_catalog.pg_enum
373
+ join pg_catalog.pg_type on pg_type.oid = enumtypid
374
+ join pg_catalog.pg_namespace on pg_namespace.oid = typnamespace
375
+ where nspname = ${schema === 'public' ? sql `current_schema()` : schema} and typname = ${name}`);
376
+ const [found] = rows;
377
+ return found?.labels ? { schema, name, labels: found.labels } : undefined;
378
+ };
379
+ // A recreated enum casts each column back; a value whose label is gone stops the whole
380
+ // migration, so those values are counted first.
381
+ const relabeledRows = async (db, statement) => {
382
+ if (statement.type !== 'recreate_enum')
383
+ return [];
384
+ const { to, columns } = statement;
385
+ const values = Array.isArray(to?.values) ? to.values.filter((value) => typeof value === 'string') : [];
386
+ const kept = sql `array[${sql.join(values.map((value) => sql `${value}`), sql `, `)}]::text[]`;
387
+ const lost = [];
388
+ for (const { schema, table, name, dimensions } of columns ?? []) {
389
+ if (typeof table !== 'string' || typeof name !== 'string')
390
+ continue;
391
+ const at = typeof schema === 'string' && schema ? schema : 'public';
392
+ const cell = sql.identifier(name);
393
+ // An array is cast element by element; a null element casts to null.
394
+ const gone = dimensions
395
+ ? sql `exists (select 1 from unnest(${cell}::text[]) as element where element is not null and not (element = any(${kept})))`
396
+ : sql `not (${cell}::text = any(${kept}))`;
397
+ try {
398
+ const { rows } = await db.execute(sql `select count(*)::int as lost from ${tableAt(at, table)} where ${gone}`);
399
+ const count = rows[0].lost;
400
+ if (count)
401
+ lost.push({ at: `${at}.${table}.${name}`, count });
402
+ }
403
+ catch (error) {
404
+ const code = error.cause?.code;
405
+ if (code !== '42P01' && code !== '42703')
406
+ throw error;
407
+ }
408
+ }
409
+ return lost;
410
+ };
411
+ const guardReply = 'confirm_guard_loss';
412
+ // None of these loses a row, but each is a guard on the data no base kind builds, or a
413
+ // counter another tool may draw from, so each needs an explicit reply.
414
+ export const guardDroppedBy = (statement) => {
415
+ const held = statement;
416
+ const [kind, container, keys] = statement.type === 'drop_check' ? ['check', held.check, ['schema', 'table', 'name']]
417
+ : statement.type === 'drop_enum' ? ['enum', held.enum, ['schema', 'name']]
418
+ : statement.type === 'drop_sequence' ? ['sequence', held.sequence, ['schema', 'name']]
419
+ : statement.type === 'alter_column' && held.wasEnum === true && held.isEnum !== true ? ['column', held.diff?.$right, ['schema', 'table', 'name']]
420
+ : [undefined, undefined, []];
421
+ if (!kind)
422
+ return undefined;
423
+ const entity = keys.map((key) => container?.[key]);
424
+ if (entity.some((part) => typeof part !== 'string' || part === '')) {
425
+ throw new Refusal(`drizzle-kit reported a ${statement.type} naming ${JSON.stringify(entity)}, and \`mithaq migrate\` cannot name it in a reply. Report it with the change that produced it.`);
426
+ }
427
+ return { kind, entity: entity };
428
+ };
429
+ // `generate` never connects, so it writes `DROP COLUMN` over data unasked; the count is
430
+ // taken here. Any failure but `42P01` or `42703` is thrown: no answer is not "empty".
431
+ const holdsData = async (db, { kind, entity }, statements) => {
432
+ const [named, title, part] = entity;
433
+ // A materialized view is no table a rename or a move names.
434
+ const { schema, table, column } = kind === 'view' ? { schema: named, table: title, column: part } : storedAs({ schema: named, table: title, column: part }, statements);
435
+ const filter = kind === 'column' ? sql `where ${sql.identifier(column)} is not null` : sql ``;
436
+ try {
437
+ const { rows } = await db.execute(sql `select 1 from ${tableAt(schema, table)} ${filter} limit 1`);
438
+ return rows.length > 0;
439
+ }
440
+ catch (error) {
441
+ const code = error.cause?.code;
442
+ if (code === '42P01' || code === '42703')
443
+ return false;
444
+ throw error;
445
+ }
446
+ };
447
+ /** A Postgres regular expression true of a value JavaScript's `trim` leaves empty; `btrim` strips spaces alone. */
448
+ const blank = String.raw `^[\t\n\v\f\r \u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]*$`;
449
+ // Only predicates SQL can judge exactly as the schema does: no `pattern`, no transform.
450
+ // Length is JavaScript's UTF-16 count, `char_length` plus one per astral character.
451
+ const refusals = (field, column) => {
452
+ const rules = (field.rules ?? {});
453
+ const transform = ['trim', 'uppercase', 'lowercase'].find((rule) => rules[rule]);
454
+ const values = field.options?.map((option) => option.value) ?? [];
455
+ const length = sql `(char_length(${column}) + regexp_count(${column}, '[\\U00010000-\\U0010FFFF]'))`;
456
+ const count = sql `cardinality(${column})`;
457
+ const listed = sql.join(values.map((value) => sql `${value}`), sql `, `);
458
+ const judged = {
459
+ // Under `trim` a value that trims to empty is refused too.
460
+ required: rules.required === true &&
461
+ (field.kind !== 'text' ? sql `${column} is null`
462
+ : rules.trim === true ? sql `(${column} is null or ${column} ~ ${blank})`
463
+ : sql `(${column} is null or ${column} = '')`),
464
+ minLength: typeof rules.minLength === 'number' && sql `${length} < ${rules.minLength}`,
465
+ maxLength: typeof rules.maxLength === 'number' && sql `${length} > ${rules.maxLength}`,
466
+ min: rules.min !== undefined && sql `${column} < ${rules.min}`,
467
+ max: rules.max !== undefined && sql `${column} > ${rules.max}`,
468
+ // An empty list passes the floor unless the field is required.
469
+ minRows: typeof rules.minRows === 'number' &&
470
+ (rules.required === true ? sql `${count} < ${rules.minRows}` : sql `${count} between 1 and ${rules.minRows - 1}`),
471
+ maxRows: typeof rules.maxRows === 'number' && sql `${count} > ${rules.maxRows}`,
472
+ options: values.length > 0 &&
473
+ (field.kind === 'textArray' ? sql `not (${column} <@ array[${listed}]::text[])` : sql `${column} not in (${listed})`),
474
+ };
475
+ const counted = [];
476
+ const notCounted = rules.pattern ? ['pattern'] : [];
477
+ for (const [rule, test] of Object.entries(judged)) {
478
+ if (!test)
479
+ continue;
480
+ if (transform && ['minLength', 'maxLength', 'options'].includes(rule))
481
+ notCounted.push(`${rule} (${transform})`);
482
+ else
483
+ counted.push([rule, test]);
484
+ }
485
+ return { counted, notCounted };
486
+ };
487
+ // Counts only, so no value leaves the database, in a read-only transaction under a
488
+ // `statement_timeout`; a table that times out is listed and the run goes on.
489
+ const refusedRows = async (db, kayanList) => {
490
+ const given = process.env.MITHAQ_COUNT_TIMEOUT_MS ?? '';
491
+ const timeout = /^[1-9]\d*$/.test(given) ? given : '5000';
492
+ // `'public'` means what no key means, so both read `current_schema()`.
493
+ const schemaIn = ({ schemaName }) => (schemaName === 'public' ? undefined : schemaName);
494
+ const named = [...new Set(kayanList.flatMap((kayan) => schemaIn(kayan) ?? []))];
495
+ const { rows } = await db.execute(sql `select table_schema = current_schema() as current, table_schema, table_name, column_name from information_schema.columns
496
+ where table_schema = current_schema()${named.length ? sql ` or table_schema in (${sql.join(named.map((name) => sql `${name}`), sql `, `)})` : sql ``}`);
497
+ const held = new Set(rows.flatMap((row) => [
498
+ `${row.table_schema}.${row.table_name}.${row.column_name}`,
499
+ ...(row.current ? [`.${row.table_name}.${row.column_name}`] : []),
500
+ ]));
501
+ const notCounted = [];
502
+ for (const kayan of kayanList) {
503
+ // A table the project built by hand has columns this command cannot name.
504
+ if (kayan.targets?.[kayan.store])
505
+ continue;
506
+ const checks = [];
507
+ for (const field of Object.values(kayan.fields)) {
508
+ if (field.hasMany || field.kind === 'join' || field.targets?.[kayan.store])
509
+ continue;
510
+ const columns = field.rules?.localized ? field.locales.map((locale) => `${field.column}_${locale}`) : [field.column];
511
+ const stored = columns.filter((column) => held.has(`${schemaIn(kayan) ?? ''}.${kayan.table}.${column}`));
512
+ if (!stored.length)
513
+ continue;
514
+ // A row breaks a localized field's rule when any of its locales does.
515
+ const byRule = new Map();
516
+ for (const column of stored) {
517
+ const { counted, notCounted: left } = refusals(field, sql.identifier(column));
518
+ for (const [rule, test] of counted)
519
+ byRule.set(rule, [...(byRule.get(rule) ?? []), test]);
520
+ if (column === stored[0])
521
+ notCounted.push(...left.map((rule) => `${kayan.name}.${field.name} ${rule}`));
522
+ }
523
+ for (const [rule, tests] of byRule)
524
+ checks.push({ field: field.name, rule, test: sql.join(tests, sql ` or `) });
525
+ }
526
+ if (!checks.length)
527
+ continue;
528
+ const query = sql `select ${sql.join(checks.map((check, index) => sql `count(*) filter (where ${check.test}) as ${sql.identifier(`c${index}`)}`), sql `, `)} from ${tableAt(schemaIn(kayan) ?? 'public', kayan.table)}`;
529
+ let counted;
530
+ try {
531
+ // `set_config` with `true` is `SET LOCAL`, which takes no parameter.
532
+ counted = await db.transaction(async (tx) => {
533
+ await tx.execute(sql `select set_config('statement_timeout', ${timeout}, true)`);
534
+ return tx.execute(query);
535
+ }, { accessMode: 'read only' });
536
+ }
537
+ catch (error) {
538
+ // `57014` is postgres's query_canceled, which the timeout raises.
539
+ const code = error.cause?.code ?? error.code;
540
+ if (code !== '57014')
541
+ throw error;
542
+ notCounted.push(`${kayan.name} (timed out after ${timeout} ms)`);
543
+ continue;
544
+ }
545
+ const [answer] = counted.rows;
546
+ const byField = new Map();
547
+ checks.forEach((check, index) => {
548
+ const count = Number(answer?.[`c${index}`] ?? 0);
549
+ if (!count)
550
+ return;
551
+ const parts = byField.get(check.field) ?? [];
552
+ parts.push(parts.length
553
+ ? `${count} ${count === 1 ? 'breaks' : 'break'} ${check.rule}`
554
+ : `${count} stored ${count === 1 ? 'row breaks' : 'rows break'} ${check.rule}`);
555
+ byField.set(check.field, parts);
556
+ });
557
+ for (const [field, parts] of byField) {
558
+ console.log(`${kayan.name}.${field}: ${parts.join(', ')}. Each is refused on its next save until it complies.`);
559
+ }
560
+ }
561
+ if (notCounted.length) {
562
+ console.log(`Not counted, since SQL cannot judge them the way the schema does: ${notCounted.join(', ')}.`);
563
+ }
564
+ };
565
+ // Pushed tables are nobody's migration: the first migration over them would create
566
+ // every table again. `42P01` covers a missing `drizzle` schema as well as a missing table.
567
+ const pushed = async (db) => {
568
+ try {
569
+ const { rows } = await db.execute(sql `select bool_or(name = ${devPushMarker}) as marker, bool_or(name is distinct from ${devPushMarker}) as others from ${historyTable}`);
570
+ // An empty table answers both `null`, which is neither.
571
+ const [found] = rows;
572
+ return { marker: found?.marker === true, others: found?.others === true };
573
+ }
574
+ catch (error) {
575
+ if (error.cause?.code === '42P01')
576
+ return { marker: false, others: false };
577
+ throw error;
578
+ }
579
+ };
580
+ // Folder rows provably this project's (name and hash match a folder) are copied from
581
+ // drizzle's default table on every run, or drizzle would run a stranded migration twice.
582
+ // Nothing in the default table is changed; an ambiguous hash stops the run.
583
+ const moveHistory = async ({ db, directory, check }) => {
584
+ const codeOf = (error) => error.cause?.code;
585
+ // `undefined` while mithaq's table is not there, which no query may name.
586
+ let empty;
587
+ try {
588
+ empty = !(await db.execute(sql `select 1 from ${historyTable} limit 1`)).rows.length;
589
+ }
590
+ catch (error) {
591
+ if (codeOf(error) !== '42P01')
592
+ throw error;
593
+ }
594
+ let own = [];
595
+ try {
596
+ own = readMigrationFiles({ migrationsFolder: fileURLToPath(directory) });
597
+ }
598
+ catch (error) {
599
+ if (error.code !== 'ENOENT')
600
+ throw error;
601
+ }
602
+ const movable = empty === false ? own : [{ name: devPushMarker, hash: devPushMarker }, ...own];
603
+ if (!movable.length)
604
+ return;
605
+ const pairs = sql.join(movable.map(({ name, hash }) => sql `(${name}, ${hash})`), sql `, `);
606
+ const names = sql.join(own.map(({ name }) => sql `${name}`), sql `, `);
607
+ const missing = empty === undefined ? sql `` : sql `and not exists (select 1 from ${historyTable} as own where own.name = old.name)`;
608
+ const mine = sql `(old.name, old.hash) in (${pairs}) ${missing}`;
609
+ let rows;
610
+ let other;
611
+ try {
612
+ rows = (await db.execute(sql `select old.name from drizzle.__drizzle_migrations as old where ${mine}`)).rows;
613
+ other = own.length
614
+ ? (await db.execute(sql `select distinct old.name from drizzle.__drizzle_migrations as old
615
+ where old.name in (${names}) ${missing} and not exists (
616
+ select 1 from drizzle.__drizzle_migrations as same where same.name = old.name and (same.name, same.hash) in (${pairs}))`)).rows.map((row) => row.name)
617
+ : [];
618
+ }
619
+ catch (error) {
620
+ if (codeOf(error) === '42P01' || codeOf(error) === '42703')
621
+ return;
622
+ throw error;
623
+ }
624
+ const history = `${migrationsTable.schema}.${migrationsTable.table}`;
625
+ if (other.length) {
626
+ console.error(`drizzle.__drizzle_migrations holds ${other.join(', ')} under a folder's name and with another hash, so this run cannot prove the row is this project's and moves nothing into ${history}. The checkout's migration.sql differs from the one applied, by its line endings or its text, and drizzle matches a migration by name alone, so \`drizzle-kit migrate\` through a config naming ${history} would run it again. Compare that folder with the applied migration, then either move the row by hand or restore the folder's bytes to the ones applied, and run \`mithaq migrate\` again.`);
627
+ return check ? 1 : 2;
628
+ }
629
+ if (!rows.length)
630
+ return undefined;
631
+ const count = `${rows.length} ${rows.length === 1 ? 'row' : 'rows'}`;
632
+ if (check) {
633
+ console.log(`${count} of this project's migration history ${rows.length === 1 ? 'sits' : 'sit'} in drizzle.__drizzle_migrations and not in ${history}, where mithaq keeps it. Run \`mithaq migrate\` against this database, before the next \`drizzle-kit migrate\` through a config naming ${history}.`);
634
+ return rows.map((row) => row.name);
635
+ }
636
+ await createHistory(db);
637
+ const moved = await db.execute(sql `insert into ${historyTable} (hash, created_at, name, applied_at)
638
+ select hash, created_at, name, applied_at from drizzle.__drizzle_migrations as old
639
+ where (old.name, old.hash) in (${pairs}) and not exists (select 1 from ${historyTable} as own where own.name = old.name)
640
+ order by old.id returning name`);
641
+ console.log(`Copied ${moved.rows.length} ${moved.rows.length === 1 ? 'row' : 'rows'} of this project's history from drizzle.__drizzle_migrations into ${history}, where mithaq keeps it now. The rows in drizzle.__drizzle_migrations are left as they were.`);
642
+ };
643
+ // drizzle's own rule, which matches a folder to a row by `name` alone.
644
+ const unapplied = async ({ db, directory }) => {
645
+ let localMigrations = [];
646
+ try {
647
+ localMigrations = readMigrationFiles({ migrationsFolder: fileURLToPath(directory) });
648
+ }
649
+ catch (error) {
650
+ if (error.code !== 'ENOENT')
651
+ throw error;
652
+ }
653
+ if (!localMigrations.length)
654
+ return [];
655
+ let dbMigrations = [];
656
+ try {
657
+ dbMigrations = (await db.execute(sql `select id, hash, created_at, name from ${historyTable}`)).rows;
658
+ }
659
+ catch (error) {
660
+ if (error.cause?.code !== '42P01')
661
+ throw error;
662
+ }
663
+ return getMigrationsToRun({ localMigrations, dbMigrations });
664
+ };
665
+ // One transaction for every pending folder, so a failure applies none, under an advisory
666
+ // lock so a second instance booting at once applies nothing.
667
+ export const bootMigrations = async ({ db, prodMigrations }) => {
668
+ if (!prodMigrations) {
669
+ throw new Refusal("A production connect() needs prodMigrations on drizzleAdapter, which says what a boot does with a migration this database has not applied: { migrationsFolder, pending: 'run' } applies every pending one before serving, and { migrationsFolder, pending: 'refuse' } refuses to start while one is pending. Nothing was checked or applied.");
670
+ }
671
+ const { migrationsFolder, pending } = prodMigrations;
672
+ // A folder without the config is not the project's, and would read as nothing pending.
673
+ if (!existsSync(join(migrationsFolder, configName))) {
674
+ throw new Refusal(`prodMigrations names ${migrationsFolder} as the migrations folder, and under ${process.cwd()} it holds no ${configName}, which every folder \`mithaq migrate\` writes holds, so this boot cannot tell which migrations this database has applied. Ship the folder with the deploy, or name where it is. Nothing was checked or applied.`);
675
+ }
676
+ const directory = pathToFileURL(`${resolvePath(migrationsFolder)}/`);
677
+ const history = `${migrationsTable.schema}.${migrationsTable.table}`;
678
+ // Read from mithaq's table alone, an unmoved history says nothing was applied.
679
+ if ((await moveHistory({ db, directory, check: true })) !== undefined) {
680
+ throw new Refusal(`This database's migration history is in drizzle.__drizzle_migrations, not in ${history}, where mithaq reads it, so a production boot cannot tell which migrations it has applied. Run \`mithaq migrate\` against this database once, which moves the history, and start again. Nothing was applied.`);
681
+ }
682
+ // A pushed database's history names no migration, so every folder would read as pending.
683
+ if ((await pushed(db)).marker) {
684
+ throw new Refusal(`This database carries mithaq's dev-push marker, the row named ${devPushMarker} in ${history}: a development connect() pushed its schema rather than migrating it, so its history says nothing about its tables, and a production boot neither checks nor applies migrations over it. Point DATABASE_URL at a database built from the migrations, or run \`mithaq migrate --baseline\` against this one to record what the push built as its baseline. Nothing was applied.`);
685
+ }
686
+ if (pending === 'run') {
687
+ // drizzle's migrator reads the history before its own transaction, so two boots would
688
+ // both apply a folder; the lock holds until this transaction ends.
689
+ await db.transaction(async (tx) => {
690
+ await tx.execute(historyLock);
691
+ await applyMigrations(tx, { migrationsFolder, migrationsSchema: migrationsTable.schema, migrationsTable: migrationsTable.table });
692
+ });
693
+ return;
694
+ }
695
+ const behind = await unapplied({ db, directory });
696
+ if (!behind.length)
697
+ return;
698
+ const names = behind.map((migration) => migration.name);
699
+ throw new Refusal(`This database has not applied ${names.join(', ')}. prodMigrations refuses a production boot while a migration is pending, so nothing was served and nothing was written. Apply ${names.length === 1 ? 'it' : 'them'} with the installed binary, \`drizzle-kit migrate --config ${join(migrationsFolder, configName)}\`, and start again.`);
700
+ };
701
+ // stdout alone is parsed: stderr carries import-time warnings, never the envelope.
702
+ const kit = (cwd, argv) => new Promise((done, fail) => {
703
+ // Node's 1 MB default `maxBuffer` kills a child past it, and the envelope grows with the
704
+ // migration. Without an `error` listener a child that cannot spawn hangs the command.
705
+ const child = execFile(process.execPath, [binary, ...argv], { cwd, maxBuffer: 64 * 1024 * 1024 });
706
+ let stdout = '';
707
+ let stderr = '';
708
+ // Decoded by the stream, so a multibyte character split across chunks stays whole.
709
+ child.stdout?.setEncoding('utf8');
710
+ child.stderr?.setEncoding('utf8');
711
+ child.stdout?.on('data', (chunk) => {
712
+ stdout += chunk;
713
+ });
714
+ child.stderr?.on('data', (chunk) => {
715
+ stderr += chunk;
716
+ });
717
+ child.on('error', fail);
718
+ // The signal too, since a child killed from outside closes with no code.
719
+ child.on('close', (code, signal) => done({ code, signal, stdout, stderr }));
720
+ });
721
+ const envelopeOf = async (cwd, argv) => {
722
+ const { code, signal, stdout, stderr } = await kit(cwd, argv);
723
+ try {
724
+ return JSON.parse(stdout.trim());
725
+ }
726
+ catch {
727
+ throw new Refusal(`\`drizzle-kit ${argv[0]}\` ${signal ? `was killed by ${signal}` : `exited ${code}`} without printing an envelope. It said:\n${stderr.trim() || stdout.trim()}`);
728
+ }
729
+ };
730
+ /** What drizzle-kit writes between two statements, and what its migrator splits on. */
731
+ const breakpoint = '--> statement-breakpoint';
732
+ // `CREATE SCHEMA` becomes `IF NOT EXISTS` and `DROP SCHEMA` a comment, so another tool's
733
+ // schema is never harmed. An unexpected count of either leaves the file unrewritten.
734
+ const keptSchemas = ({ text, expected }) => {
735
+ let created = 0;
736
+ let dropped = 0;
737
+ const rewritten = text
738
+ .replace(/^CREATE SCHEMA ("[^"]+");$/gm, (_line, name) => {
739
+ created += 1;
740
+ return `CREATE SCHEMA IF NOT EXISTS ${name};`;
741
+ })
742
+ .replace(/^DROP SCHEMA ("[^"]+");$/gm, (_line, name) => {
743
+ dropped += 1;
744
+ return `-- mithaq never drops a schema: ${name} is kept.`;
745
+ });
746
+ return created === expected.created && dropped === expected.dropped ? rewritten : undefined;
747
+ };
748
+ // Pull writes its SQL commented out, which `drizzle-kit migrate` cannot apply; only the
749
+ // measured shape is uncommented. A pulled table the kayan do not name would be dropped next.
750
+ export const baselineOf = ({ envelope, read, tables }) => {
751
+ if (envelope.status === 'error')
752
+ throw new Refusal(said(envelope.error));
753
+ const stamp = /(\d{14})_[^/\\]+[/\\]migration\.sql$/.exec(envelope.migrationPath ?? '')?.[1];
754
+ const lines = stamp ? read(envelope.migrationPath).split('\n') : [];
755
+ const statements = lines.slice(3, -1).join('\n');
756
+ if (lines.length < 4 || !lines[0].startsWith('-- ') || !lines[1].startsWith('-- ') || lines[2] !== '/*' ||
757
+ lines.at(-1) !== '*/' || /\/\*|\*\//.test(statements)) {
758
+ throw new Refusal('drizzle-kit pull wrote a migration.sql in a shape `--baseline` has not measured, which is two comment lines and then the statements inside one `/*` and `*/`, so it cannot be uncommented exactly. Nothing was written. Report it with the drizzle-kit version that wrote it.');
759
+ }
760
+ // The pull brings every standalone sequence and enum in `public`, which the next migration
761
+ // would drop; each one no pulled column uses is left out.
762
+ const parsed = JSON.parse(read(envelope.snapshotPath));
763
+ const ddl = parsed.ddl ?? [];
764
+ const used = new Set(ddl.filter((entry) => entry.entityType === 'columns').map((column) => `${column.typeSchema ?? 'public'}.${column.type}`));
765
+ const left = ddl.filter((entry) => entry.entityType === 'sequences' || (entry.entityType === 'enums' && !used.has(`${entry.schema}.${entry.name}`)));
766
+ let chunks = statements.split(breakpoint);
767
+ for (const entry of left) {
768
+ const word = entry.entityType === 'enums' ? 'TYPE' : 'SEQUENCE';
769
+ const heads = [`CREATE ${word} ${quotedName(entry.schema ?? '')}.${quotedName(entry.name ?? '')} `];
770
+ if (entry.schema === 'public')
771
+ heads.push(`CREATE ${word} ${quotedName(entry.name ?? '')} `);
772
+ const matched = chunks.filter((chunk) => heads.some((head) => chunk.trimStart().startsWith(head)));
773
+ if (matched.length !== 1) {
774
+ throw new Refusal(`drizzle-kit pull returned the ${entry.entityType === 'enums' ? 'enum' : 'sequence'} ${entry.schema}.${entry.name}, which no kayan column uses, and \`--baseline\` cannot find the one statement that creates it to leave out. Nothing was written. Report it with the drizzle-kit version that pulled it.`);
775
+ }
776
+ chunks = chunks.filter((chunk) => chunk !== matched[0]);
777
+ }
778
+ const pulledSql = left.length ? chunks.join(breakpoint).trimStart() : statements;
779
+ const kept = keptSchemas({ text: pulledSql, expected: { created: ddl.filter((entry) => entry.entityType === 'schemas' && entry.name !== 'public').length, dropped: 0 } });
780
+ if (kept === undefined) {
781
+ throw new Refusal('drizzle-kit pull wrote a migration.sql whose CREATE SCHEMA lines are not one for each schema its snapshot holds, so `--baseline` cannot make each one keep the schema. Nothing was written. Report it with the drizzle-kit version that wrote it.');
782
+ }
783
+ const snapshot = left.length
784
+ ? JSON.stringify({ ...parsed, ddl: ddl.filter((entry) => !left.includes(entry)) }, null, 2)
785
+ : read(envelope.snapshotPath);
786
+ const views = ddl.filter((entry) => entry.entityType === 'views');
787
+ if (views.length) {
788
+ throw new Refusal(`drizzle-kit pull returned the ${views.map((view) => `${view.materialized ? 'materialized view' : 'view'} ${view.schema}.${view.name}`).join(', ')}, named like a table of this language, and no kayan builds a view, so the first migration after a baseline would drop it. Rename the kayan's table, or move the view. Nothing was written.`);
789
+ }
790
+ const pulled = ddl.filter((entry) => entry.entityType === 'tables');
791
+ const outside = pulled.filter((table) => !tables.includes(`${table.schema}.${table.name}`));
792
+ if (outside.length) {
793
+ throw new Refusal(`drizzle-kit pull returned ${outside.map((table) => `${table.schema}.${table.name}`).join(', ')}, which no kayan of this language names, and the first migration after a baseline would drop it. \`--baseline\` adopts the language's own tables and never another. Nothing was written. Report it with the drizzle-kit version that pulled it.`);
794
+ }
795
+ if (!pulled.length) {
796
+ throw new Refusal(`This database holds none of the ${tables.length} tables this language builds, so there is nothing to adopt. Run \`mithaq migrate\`.`);
797
+ }
798
+ return { folder: `${stamp}_baseline`, sql: kept, snapshot };
799
+ };
800
+ // No DDL runs and no row is read or written. `pull --init` is not used: it inserts its
801
+ // row before any check here could refuse. The folder is removed if the row fails.
802
+ const baseline = async ({ cwd, directory, configPath, db, tables, kayan }) => {
803
+ // Before the pull, which would otherwise bring in another tool's table of a shared name.
804
+ await refuseStrangers({ db, tables, by: 'baseline' });
805
+ // Pulled with the join tables, so the migration after the baseline copies its rows.
806
+ const adopted = await payloadRels({ db, kayan, tables });
807
+ const built = Object.values(tables);
808
+ const tableNames = [...built.map((table) => getTableName(table)), ...adopted.map(({ rels }) => rels)];
809
+ // Every schema the language uses, `public` only when some table is there.
810
+ const schemaFilter = [...new Set(built.map(schemaOf))];
811
+ const scratch = await mkdtemp(join(tmpdir(), 'mithaq-baseline-'));
812
+ try {
813
+ const config = join(scratch, configName);
814
+ await writeFile(config, `import base from ${JSON.stringify(configPath)}\nexport default { ...base, out: ${JSON.stringify(join(scratch, 'out'))}, schemaFilter: ${JSON.stringify(schemaFilter)}, tablesFilter: ${JSON.stringify(tableNames)} }\n`);
815
+ const pulled = (await envelopeOf(cwd, ['pull', '--output', 'json', '--config', config]));
816
+ const pairs = [...built.map((table) => `${schemaOf(table)}.${getTableName(table)}`), ...adopted.map(({ schema, rels }) => `${schema}.${rels}`)];
817
+ const { folder, sql: statements, snapshot } = baselineOf({ envelope: pulled, read: (path) => readFileSync(path, 'utf8'), tables: pairs });
818
+ const written = new URL(`${folder}/`, directory);
819
+ await mkdir(written);
820
+ try {
821
+ await writeFile(new URL('migration.sql', written), statements);
822
+ await writeFile(new URL('snapshot.json', written), snapshot);
823
+ // The hash and time drizzle's migrator computes for the folder, which it matches the row against.
824
+ const migration = readMigrationFiles({ migrationsFolder: fileURLToPath(directory) }).find((one) => one.name === folder);
825
+ await recordMigration({ db, hash: migration.hash, createdAt: migration.folderMillis, name: migration.name });
826
+ }
827
+ catch (error) {
828
+ await rm(written, { recursive: true, force: true });
829
+ throw error;
830
+ }
831
+ console.log(`Recorded ${folder} as this database's baseline. Run \`mithaq migrate --check\` to see what the kayan change.`);
832
+ for (const { schema, rels, copies } of adopted) {
833
+ console.log(`${schema}.${rels} holds Payload's relationship rows for ${listed(copies.map(({ field }) => field))}, which the next \`mithaq migrate\` copies into ${listed(copies.map(({ join }) => join))} before it drops ${schema}.${rels}.`);
834
+ }
835
+ return 0;
836
+ }
837
+ finally {
838
+ await rm(scratch, { recursive: true, force: true });
839
+ }
840
+ };
841
+ // An `ok` listing no statements is refused: an answer that did not come is not "nothing".
842
+ export const statementsOf = (envelope) => {
843
+ if (envelope.status === 'no_changes')
844
+ return [];
845
+ if (envelope.status === 'ok' && envelope.statements?.length)
846
+ return envelope.statements;
847
+ throw new Refusal(`\`drizzle-kit generate --explain\` answered ${envelope.status} without listing its statements, so the data-loss guard has nothing to count rows for and this run writes nothing.`);
848
+ };
849
+ // Never `issues`: a rejected hints file comes back as a 9 KB union tree on one line.
850
+ const said = (error) => `drizzle-kit refused: ${error.code}${error.source ? ` (source: ${error.source})` : ''}`;
851
+ const unresolvedLine = (item) => `${item.kind} ${item.entity.join('.')} ${item.reason ?? item.type}`;
852
+ const answered = (hint) => JSON.stringify(['confirm_data_loss', hint.kind, hint.entity]);
853
+ // Reached only for a file this command wrote, so a failure here is that file's own.
854
+ const namesOf = async (schemaFile) => {
855
+ try {
856
+ return Object.keys((await import(__rewriteRelativeImportExtension(schemaFile.href))))
857
+ .filter((name) => name !== 'default')
858
+ .sort();
859
+ }
860
+ catch {
861
+ return undefined;
862
+ }
863
+ };
864
+ const shortOf = ({ configHeld, exported, tables, schemas, enums, directory, }) => {
865
+ if (configHeld === undefined) {
866
+ return `${configName} under ${directory.href} is not what a run would write: it is not there. Run \`mithaq migrate\`.`;
867
+ }
868
+ if (exported === undefined) {
869
+ return `${schemaName} under ${directory.href} is not what a run would write: it is not there, or it does not load. Run \`mithaq migrate\`.`;
870
+ }
871
+ const missing = [...tables, ...schemas, ...enums].filter((table) => !exported.includes(table)).sort();
872
+ return `${schemaName} under ${directory.href} is not what a run would write: it exports ${exported.length} ${exported.length === 1 ? 'name' : 'names'} and the language built ${tables.length} ${tables.length === 1 ? 'table' : 'tables'}${schemas.length ? ` in ${schemas.length} ${schemas.length === 1 ? 'schema' : 'schemas'} besides public` : ''}${enums.length ? ` using ${enums.length} ${enums.length === 1 ? 'enum' : 'enums'}` : ''}${missing.length ? `, missing ${missing.join(', ')}` : ''}. drizzle-kit reads named exports alone, so a run would report no changes over the tables it cannot see. Run \`mithaq migrate\`.`;
873
+ };
874
+ // A check writes nothing and answers 0 for no changes, 1 for a pending migration or a
875
+ // failure, and 2 for a decision only the project can make.
876
+ export const run = async ({ target, flags, }) => {
877
+ const check = flags.includes('--check');
878
+ const adopt = flags.includes('--baseline');
879
+ // The slug becomes part of the migration folder's path, so only a plain name passes.
880
+ const at = flags.indexOf('--name');
881
+ const name = at === -1 ? undefined : flags[at + 1];
882
+ if (at !== -1 && !/^[a-z0-9_]{1,64}$/.test(name ?? '')) {
883
+ throw new Refusal(`\`--name\` takes a slug of lowercase letters, digits and underscores, 1 to 64 characters, such as \`mithaq migrate --name center_feedback\`, and was handed ${name === undefined ? 'nothing' : JSON.stringify(name)}. Nothing was written.`);
884
+ }
885
+ if (adopt && at !== -1) {
886
+ throw new Refusal('`--baseline` names its folder `_baseline` after the timestamp, so `--name` has nothing to name. Run `mithaq migrate --baseline` alone. Nothing was written.');
887
+ }
888
+ if (adopt && check) {
889
+ throw new Refusal('`--baseline` records a baseline and `--check` writes nothing, so the two do not run together. Run `mithaq migrate --baseline`, and then `mithaq migrate --check` to see what the kayan change.');
890
+ }
891
+ const project = pathToFileURL(`${process.cwd()}/`);
892
+ const { generate } = (await import(__rewriteRelativeImportExtension(new URL('src/mithaq.ts', project).href)));
893
+ const directory = new URL(generate?.migrations ?? 'src/migrations/', project);
894
+ // The config drizzle-kit applies through and the probe that counts rows use one string,
895
+ // so a drop is never counted on one database and applied to another. A check needs none.
896
+ const { connectionString } = target;
897
+ const ready = typeof connectionString !== 'string' || connectionString === '';
898
+ if (ready && !check) {
899
+ throw new Refusal('This language built its drizzle target from a ready client, and `mithaq migrate` needs the connection string itself: drizzle-kit reads a config, and a config cannot take a live client. Build the target with `connectionString`, or run `drizzle-kit` against a config this project writes.');
900
+ }
901
+ // A pool of its own, so the probe reads the database the config names.
902
+ const own = ready ? undefined : drizzle(connectionString);
903
+ const db = (own ?? target.db);
904
+ const tables = Object.keys(target.tables);
905
+ const schemas = Object.keys(target.schemas);
906
+ try {
907
+ // Never the answer: a count that could not be taken is said on stderr.
908
+ await refusedRows(db, target.kayan).catch((error) => console.error(`The stored rows the rules would refuse could not be counted: ${error.message}`));
909
+ return await migrate({ project, directory, db, connectionString: ready ? undefined : connectionString, tables, built: target.tables, schemas, enums: target.enums, check, adopt, kayan: target.kayan, name });
910
+ }
911
+ finally {
912
+ // In a `finally`: a refusal is a throw, and an escaped one would leave the pool open.
913
+ try {
914
+ await own?.$client.end();
915
+ }
916
+ catch {
917
+ // A pool already ended, which a refusal thrown mid-probe can leave.
918
+ }
919
+ }
920
+ };
921
+ const migrate = async ({ project, directory, db, connectionString, tables, built, schemas, enums, check, adopt, kayan, name, }) => {
922
+ // Before anything is written: every answer below assumes the tables are the migrations'.
923
+ // A check is refused only for a marker beside a migration's row; a baseline replaces a lone marker.
924
+ const stopped = await moveHistory({ db, directory, check });
925
+ if (typeof stopped === 'number')
926
+ return stopped;
927
+ const history = `${migrationsTable.schema}.${migrationsTable.table}`;
928
+ // Rows still in drizzle's default table are read as moved, so a check does not tell a
929
+ // pushed database to apply folders the push already built.
930
+ const found = await pushed(db);
931
+ const marker = found.marker || !!stopped?.includes(devPushMarker);
932
+ const others = found.others || !!stopped?.some((name) => name !== devPushMarker);
933
+ if (adopt && others) {
934
+ throw new Refusal(`${history} already holds migration rows, so this database already has a history and a baseline would sit beside it. \`--baseline\` adopts a database whose history is empty or holds only the dev-push marker.`);
935
+ }
936
+ if (marker && !adopt && (!check || others)) {
937
+ console.error(`\`connect()\` pushed this schema rather than migrating it, so this database carries mithaq's dev-push marker and its tables belong to no migration. A migration written now would recreate every one of them. Run \`mithaq migrate --baseline\` to record what the push built as this database's baseline, which replaces the marker. Where ${history} holds migration rows beside the marker, a baseline is refused; delete the row named ${devPushMarker} there instead.`);
938
+ return 2;
939
+ }
940
+ if (marker && !adopt) {
941
+ const markedIn = found.marker ? history : 'drizzle.__drizzle_migrations';
942
+ console.log(`This database carries mithaq's dev-push marker, the row named ${devPushMarker} in ${markedIn}, so \`connect()\` pushed its schema rather than migrating it, and this run skipped the check of the migration files against the kayan. Run \`mithaq migrate --check\` against a database built from the migrations with \`drizzle-kit migrate --config ${relativeSpecifier(project, new URL(configName, directory)).replace(/^\.\//, '')}\`, such as a fresh one in CI, to check them.`);
943
+ return 0;
944
+ }
945
+ const configFile = new URL(configName, directory);
946
+ const schemaFile = new URL(schemaName, directory);
947
+ // Computed, so a project that points `generate.migrations` elsewhere still imports its one build.
948
+ const asasFrom = relativeSpecifier(new URL('.', schemaFile), new URL('src/asas.ts', project));
949
+ const exportedSchemas = schemas.filter((schema) => schema !== 'public');
950
+ const schemaExports = exportedSchemas.map((schema) => `${schema}Schema`);
951
+ const enumNames = enumExports({ enums, schemas, taken: [...tables, ...schemaExports] });
952
+ const wanted = schemaSource({ tables, schemas: exportedSchemas, enums: enumNames, asasFrom });
953
+ const exports = [...tables, ...schemaExports, ...enumNames.map(([, name]) => name)];
954
+ const held = await readFile(schemaFile, 'utf8').catch(() => undefined);
955
+ const configHeld = await readFile(configFile, 'utf8').catch(() => undefined);
956
+ if (held !== undefined && !held.startsWith(header)) {
957
+ throw new Refusal(`${schemaName} under ${directory.href} was not written by \`mithaq migrate\` and regenerating would overwrite it. Move it or delete it.`);
958
+ }
959
+ // Compared by the exports the loader sees: a module short a name diffs only what it names,
960
+ // and one exporting an object diffs nothing and passes forever.
961
+ const exported = held === undefined ? undefined : await namesOf(schemaFile);
962
+ const short = exported === undefined || exported.join(',') !== [...exports].sort().join(',');
963
+ if (check) {
964
+ // A check writes nothing, not even a directory.
965
+ if (configHeld === undefined || short) {
966
+ console.error(shortOf({ configHeld, exported, tables, schemas: schemaExports, enums: enumNames.map(([, name]) => name), directory }));
967
+ return 1;
968
+ }
969
+ }
970
+ else {
971
+ await mkdir(directory, { recursive: true });
972
+ // The config is written once and is then the project's; the schema module is derived and rewritten.
973
+ if (configHeld === undefined)
974
+ await writeFile(configFile, configSource(asasFrom));
975
+ if (short)
976
+ await writeFile(schemaFile, wanted);
977
+ }
978
+ const config = (await import(__rewriteRelativeImportExtension(configFile.href))).default;
979
+ if (typeof config?.schema !== 'string' || resolvePath(config.schema) !== fileURLToPath(schemaFile)) {
980
+ throw new Refusal(`${configName} names a schema file \`mithaq migrate\` did not write. The tables are values \`buildAsas\` builds, so the config has to point \`schema\` at ${schemaName} beside it.`);
981
+ }
982
+ // A config naming another directory is a run whose snapshot the next run never diffs against.
983
+ if (typeof config.out !== 'string' || resolvePath(config.out) !== resolvePath(fileURLToPath(directory))) {
984
+ throw new Refusal(`${configName} names an out directory \`mithaq migrate\` did not write. The migrations belong beside it, under ${directory.href}, which is where the next run reads the snapshot from.`);
985
+ }
986
+ // A config not naming mithaq's history table would record into drizzle's default one.
987
+ const named = config.migrations;
988
+ if (named?.schema !== migrationsTable.schema || named.table !== migrationsTable.table) {
989
+ throw new Refusal(`${configName} applies migrations through drizzle's default history table, and mithaq keeps its own in ${history}, so a database another tool migrates is never read as this project's. Delete ${configName} and run \`mithaq migrate\` again to have it written naming that table. A run copies this project's rows into it on the database it reaches; run it once against every other database this project migrates before the first \`drizzle-kit migrate\` through the new config.`);
990
+ }
991
+ // Or the migration would be applied somewhere the data-loss probe never asked about.
992
+ if (connectionString !== undefined && config.dbCredentials?.url !== connectionString) {
993
+ throw new Refusal(`${configName} names a database other than the one this language connects to, and the data-loss guard counts rows through the language. Delete ${configName} and run \`mithaq migrate\` again to have it written from \`asas.targets.drizzle.connectionString\`.`);
994
+ }
995
+ for (const entry of await readdir(directory, { withFileTypes: true })) {
996
+ if (!entry.isDirectory())
997
+ continue;
998
+ const inside = await readdir(new URL(`${entry.name}/`, directory));
999
+ if (adopt && inside.includes('migration.sql')) {
1000
+ throw new Refusal(`${entry.name} under ${directory.href} is a migration, so this project already has a history and a baseline would sit beside it. \`--baseline\` adopts a database for a project with no migration yet.`);
1001
+ }
1002
+ if (inside.includes('migration.sql') && !inside.includes('snapshot.json')) {
1003
+ throw new Refusal(`${entry.name} holds a migration.sql with no snapshot.json beside it, and the snapshot is what the next migration is diffed against. Restore it, or remove the folder.`);
1004
+ }
1005
+ }
1006
+ // A write is refused until every folder is applied: a migration written but not applied
1007
+ // keeps its renamed rows under the old name, and the probe would read them as empty.
1008
+ const behind = adopt ? [] : (await unapplied({ db, directory })).filter((migration) => !stopped?.includes(migration.name));
1009
+ if (behind.length) {
1010
+ const names = behind.map((migration) => migration.name);
1011
+ const config = relativeSpecifier(project, configFile).replace(/^\.\//, '');
1012
+ const them = names.length === 1 ? 'it' : 'them';
1013
+ console.error(check
1014
+ ? `This database has not applied ${names.join(', ')}. This check compares the migration files with the kayan, and a database behind the files is not the schema they describe. Apply ${them} with the installed binary, \`drizzle-kit migrate --config ${config}\`, and run \`mithaq migrate --check\` again.`
1015
+ : `This database has not applied ${names.join(', ')}. The next migration is written from where ${names.length === 1 ? 'that one leaves' : 'those leave'} the schema, so a drop after a rename in ${names.length === 1 ? 'it' : 'one of them'} would be probed under the new name, which holds no rows yet, and pass unasked. Apply ${them} first, with the installed binary, \`drizzle-kit migrate --config ${config}\`, and run \`mithaq migrate\` again. No migration was written.`);
1016
+ console.error(`If a migration's SQL already ran by hand, record it instead of applying it, with its line below. Recording a migration that did not really run makes mithaq and drizzle-kit treat its changes as present, so record only one whose SQL already ran. mithaq never runs these.`);
1017
+ for (const { hash, folderMillis, name } of behind) {
1018
+ console.error(`insert into ${history} (hash, created_at, name) values (${quotedText(hash)}, ${folderMillis}, ${quotedText(name)});`);
1019
+ }
1020
+ return check ? 1 : 2;
1021
+ }
1022
+ const cwd = fileURLToPath(project);
1023
+ const configPath = fileURLToPath(configFile);
1024
+ if (adopt)
1025
+ return baseline({ cwd, directory, configPath, db, tables: built, kayan });
1026
+ const hintsFile = new URL(hintsName, project);
1027
+ const hintsText = await readFile(hintsFile, 'utf8').catch(() => undefined);
1028
+ let hints = [];
1029
+ if (hintsText !== undefined) {
1030
+ try {
1031
+ hints = JSON.parse(hintsText);
1032
+ }
1033
+ catch {
1034
+ throw new Refusal(`${hintsName} is not a hints file: it does not parse as JSON.`);
1035
+ }
1036
+ if (!Array.isArray(hints))
1037
+ throw new Refusal(`${hintsName} is not a hints file: it is not an array of hints.`);
1038
+ // `null` is an object to `typeof`, so it would reach a property read as a crash.
1039
+ if (hints.some((hint) => typeof hint !== 'object' || hint === null || Array.isArray(hint))) {
1040
+ throw new Refusal(`${hintsName} is not a hints file: one of its entries is not an object.`);
1041
+ }
1042
+ }
1043
+ // An adopted `_rels` in the last snapshot is dropped once its rows are copied; each table
1044
+ // this migration creates is answered `create`, since a join table is never `_rels` renamed.
1045
+ const last = (await readdir(directory)).filter((name) => /^\d{14}_/.test(name)).sort().at(-1);
1046
+ const ddl = last
1047
+ ? JSON.parse(await readFile(new URL(`${last}/snapshot.json`, directory), 'utf8')).ddl ?? []
1048
+ : [];
1049
+ const snapshotted = ddl.filter((entry) => entry.entityType === 'tables').map((entry) => `${entry.schema}.${entry.name}`);
1050
+ const builtPairs = Object.values(built).map((table) => [schemaOf(table), getTableName(table)]);
1051
+ const strangers = snapshotted.filter((pair) => !builtPairs.some(([schema, name]) => `${schema}.${name}` === pair));
1052
+ // Payload names its foreign key `_fk` where drizzle and Postgres write `_fkey`, which
1053
+ // tells its table from a project's own of that name.
1054
+ const ddlOf = (pair) => ddl.filter((entry) => `${entry.schema}.${entry.table}` === pair);
1055
+ const payloadShaped = strangers.filter((pair) => {
1056
+ const [schema, name] = [pair.slice(0, pair.indexOf('.')), pair.slice(pair.indexOf('.') + 1)];
1057
+ return name.endsWith('_rels') &&
1058
+ ['path', 'parent_id', 'order'].every((column) => ddlOf(pair).some((entry) => entry.entityType === 'columns' && entry.name === column)) &&
1059
+ ddlOf(pair).some((entry) => entry.entityType === 'fks' && JSON.stringify(entry.columns) === '["parent_id"]' &&
1060
+ entry.schemaTo === schema && entry.tableTo === name.slice(0, -'_rels'.length) && entry.name?.endsWith('_fk'));
1061
+ });
1062
+ const adopted = strangers.length ? await payloadRels({ db, kayan, tables: built, dropped: ({ schema, table }) => strangers.includes(`${schema}.${table}`) }) : [];
1063
+ const copied = new Set(adopted.map(({ schema, rels }) => `${schema}.${rels}`));
1064
+ const answeredAlready = (entity) => hints.some((hint) => hint.kind === 'table' && JSON.stringify(hint.entity ?? hint.to) === JSON.stringify(entity));
1065
+ const creates = adopted.length && strangers.every((pair) => copied.has(pair))
1066
+ ? builtPairs
1067
+ .filter(([schema, name]) => !snapshotted.includes(`${schema}.${name}`) && !answeredAlready([schema, name]))
1068
+ .map(([schema, name]) => ({ type: 'create', kind: 'table', entity: [schema, name] }))
1069
+ : [];
1070
+ // Without the replies drizzle's strict hints schema has no shape for. A file, because
1071
+ // a command line is capped (32 767 characters on Windows).
1072
+ const generating = async (argv) => {
1073
+ if (hintsText === undefined && !creates.length)
1074
+ return envelopeOf(cwd, argv);
1075
+ const scratch = await mkdtemp(join(tmpdir(), 'mithaq-hints-'));
1076
+ try {
1077
+ const file = join(scratch, hintsName);
1078
+ await writeFile(file, JSON.stringify([...hints.filter((hint) => hint.type !== guardReply), ...creates]));
1079
+ return await envelopeOf(cwd, [...argv, '--hints-file', file]);
1080
+ }
1081
+ finally {
1082
+ await rm(scratch, { recursive: true, force: true });
1083
+ }
1084
+ };
1085
+ // `--explain` writes nothing in every status measured, and hands a write the statements
1086
+ // the guard counts rows for.
1087
+ const explained = await generating(['generate', '--explain', '--output', 'json', '--config', configPath]);
1088
+ if (explained.status === 'error' || explained.status === 'missing_hints')
1089
+ return answerOf(explained);
1090
+ const upToDate = explained.status === 'no_changes';
1091
+ const statements = statementsOf(explained);
1092
+ if (check) {
1093
+ if (upToDate) {
1094
+ console.log('The database schema is up to date.');
1095
+ return 0;
1096
+ }
1097
+ // `generate --explain` answers a pending change `ok` at exit 0, as a clean run does.
1098
+ console.error(`A migration is pending: ${statements.map((statement) => statement.type).join(', ')}. Run \`mithaq migrate\`.`);
1099
+ return 1;
1100
+ }
1101
+ const losing = [];
1102
+ const guards = [];
1103
+ // An enum another tool's table still uses is never dropped.
1104
+ const dropping = new Set(statements.flatMap((statement) => {
1105
+ const table = statement.table;
1106
+ return statement.type === 'drop_table' ? [`${String(table?.schema)}.${String(table?.name)}`] : [];
1107
+ }));
1108
+ const mine = (pair) => dropping.has(pair) || builtPairs.some(([schema, name]) => `${schema}.${name}` === pair);
1109
+ const keptEnums = [];
1110
+ for (const statement of statements) {
1111
+ const held = await heldEnum(db, statement);
1112
+ if (held) {
1113
+ const { schema, name, labels } = held;
1114
+ const wanted = (statement.enum?.values ?? []).join(', ');
1115
+ const typed = `"${schema}"."${name}"`;
1116
+ throw new Refusal(`The enum ${typed} is already in the database with the labels ${labels.join(', ')}, and no migration of this project made it, so it is another tool's. ${labels.join(', ') === wanted ? 'The migration would create it again and stop on it' : `Giving it the labels ${wanted} would alter it`}. Give the column's enum another name or, if it is this language's own, left behind by a dropped table, drop the type with DROP TYPE ${typed}. Nothing was written.`);
1117
+ }
1118
+ for (const { at, count } of await relabeledRows(db, statement)) {
1119
+ throw new Refusal(`${at} holds ${count === 1 ? '1 value' : `${count} values`} whose label the enum no longer has, and the migration recreates the enum without it and would stop on ${count === 1 ? 'it' : 'them'} with Postgres's invalid input value for enum. Change each one to a label the enum keeps, or keep the label, and run \`mithaq migrate\` again.`);
1120
+ }
1121
+ // A guard first: `droppedBy` would refuse `drop_check` and `drop_enum` as unmeasured.
1122
+ const guard = guardDroppedBy(statement);
1123
+ const [schema, name] = guard?.kind === 'enum' ? guard.entity : [];
1124
+ const users = schema && name ? await enumUsedBy({ db, schema, name, mine }) : [];
1125
+ if (users.length)
1126
+ keptEnums.push({ schema: schema, name: name, users });
1127
+ else if (guard)
1128
+ guards.push(guard);
1129
+ else {
1130
+ const dropped = droppedBy(statement);
1131
+ if (dropped && !(dropped.kind === 'table' && copied.has(dropped.entity.join('.'))) && (await holdsData(db, dropped, statements)))
1132
+ losing.push(dropped);
1133
+ }
1134
+ // No hint answers a value rounded in place, so the run is refused until none would change.
1135
+ const narrowed = roundedBy(statement);
1136
+ const rounded = narrowed && storedAs(narrowed, statements);
1137
+ const changed = rounded ? await roundedRows(db, rounded) : 0;
1138
+ if (rounded && changed) {
1139
+ const { schema, table, column, type } = rounded;
1140
+ const at = `${schema}.${table}.${column}`;
1141
+ throw new Refusal(changed === 'unread'
1142
+ ? `${at} holds text that reads as no ${type === 'integer' ? 'integer' : 'number'}, and the migration would stop on it with Postgres's invalid input syntax. Fix or clear each such value, and run \`mithaq migrate\` again.`
1143
+ : changed === 'overflow'
1144
+ ? `${at} holds a value too wide for ${type}, and the migration would stop on it with Postgres's ${type === 'integer' ? 'integer out of range' : 'numeric field overflow'}. Move or shorten each such value yourself, or keep the wider precision, and run \`mithaq migrate\` again.`
1145
+ : `${at} holds ${changed === 1 ? '1 value' : `${changed} values`} that ${type} would change, and Postgres rounds a value in place to fit, with no error. Round or move each one yourself, as \`update ${schema === 'public' ? '' : `"${schema}".`}"${table}" set "${column}" = round("${column}"::numeric, <scale>)\` does, or keep the wider type, and run \`mithaq migrate\` again.`);
1146
+ }
1147
+ // The migration would fail on the row when applied, and no hint fills a row.
1148
+ const notNull = nulledBy(statement);
1149
+ const nulled = notNull && storedAs(notNull, statements);
1150
+ const empty = nulled ? await nulledRows(db, nulled) : 0;
1151
+ if (nulled && empty) {
1152
+ const { schema, table, column, added } = nulled;
1153
+ const at = `${schema}.${table}.${column}`;
1154
+ const rows = empty === 1 ? '1 row' : `${empty} rows`;
1155
+ throw new Refusal(added
1156
+ ? `${at} would be added NOT NULL with no default over ${rows}, and Postgres would stop on them with "column contains null values". Give the field a literal \`defaultValue\`, or add it without \`required\`, fill each row, and make it required in a later run.`
1157
+ : `${at} holds ${rows} with no value, and SET NOT NULL would stop on them with Postgres's "column contains null values". Fill each one yourself, as \`update ${schema === 'public' ? '' : `"${schema}".`}"${table}" set "${column}" = <value> where "${column}" is null\` does, or leave the field without \`required\`, and run \`mithaq migrate\` again.`);
1158
+ }
1159
+ // No hint renumbers a row.
1160
+ const key = keyedBy(statement);
1161
+ const stored = key && key.columns.map((column) => storedAs({ schema: key.schema, table: key.table, column }, statements));
1162
+ const shared = stored ? await duplicatedRows(db, { schema: stored[0].schema, table: stored[0].table, columns: stored.map(({ column }) => column) }) : 0;
1163
+ if (stored && shared) {
1164
+ const { schema, table } = stored[0];
1165
+ throw new Refusal(`${schema}.${table} holds ${shared} rows whose (${stored.map(({ column }) => column).join(', ')}) another row also holds, and ADD PRIMARY KEY would stop on them with Postgres's "could not create unique index". Delete or renumber each one yourself, and run \`mithaq migrate\` again.`);
1166
+ }
1167
+ }
1168
+ const replies = new Set(hints.filter((hint) => hint.type === 'confirm_data_loss').map(({ kind, entity }) => answered({ kind, entity })));
1169
+ const guardReplies = new Set(hints.filter((hint) => hint.type === guardReply).map((hint) => JSON.stringify([hint.kind, hint.entity])));
1170
+ const unanswered = losing.filter((hint) => !replies.has(answered(hint)));
1171
+ const unguarded = guards.filter((guard) => !guardReplies.has(JSON.stringify([guard.kind, guard.entity])));
1172
+ if (unanswered.length) {
1173
+ for (const { kind, entity } of unanswered) {
1174
+ console.error(JSON.stringify({ type: 'confirm_data_loss', kind, entity, reason: 'non_empty' }));
1175
+ }
1176
+ console.error(`Each line above would lose data. Write the ones this project means down in ${hintsName}, without \`reason\`, and run \`mithaq migrate\` again.`);
1177
+ }
1178
+ if (unguarded.length) {
1179
+ for (const { kind, entity } of unguarded)
1180
+ console.error(JSON.stringify({ type: guardReply, kind, entity }));
1181
+ console.error(`Each line above drops a check, an enum, a column's enum type or a sequence that the database keeps and no kayan builds. Write the ones this project means down in ${hintsName} as ${guardReply} replies, and run \`mithaq migrate\` again.`);
1182
+ }
1183
+ if (unanswered.length || unguarded.length)
1184
+ return 2;
1185
+ // drizzle accepts a hint nothing asked for in silence, so an unused entry is reported.
1186
+ const wantedReplies = new Set(losing.map(answered));
1187
+ const wantedGuards = new Set(guards.map((guard) => JSON.stringify([guard.kind, guard.entity])));
1188
+ for (const hint of hints) {
1189
+ if (hint.type === guardReply && !wantedGuards.has(JSON.stringify([hint.kind, hint.entity]))) {
1190
+ console.log(`${JSON.stringify(hint)} in ${hintsName} answers no drop this run found.`);
1191
+ }
1192
+ if (hint.type !== 'confirm_data_loss')
1193
+ continue;
1194
+ if (wantedReplies.has(answered(hint)))
1195
+ continue;
1196
+ console.log(`${JSON.stringify(hint)} in ${hintsName} answers no drop this run found.`);
1197
+ }
1198
+ if (upToDate) {
1199
+ console.log('The database schema is up to date.');
1200
+ return 0;
1201
+ }
1202
+ const written = await generating(['generate', '--output', 'json', '--config', configPath, ...(name ? ['--name', name] : [])]);
1203
+ if (written.status === 'error' || written.status === 'missing_hints')
1204
+ return answerOf(written);
1205
+ if (written.status === 'no_changes') {
1206
+ console.log('The database schema is up to date.');
1207
+ return 0;
1208
+ }
1209
+ // Only the file this run wrote: a committed migration is never touched.
1210
+ const path = written.migration_path ?? '';
1211
+ const folder = new URL('.', pathToFileURL(path));
1212
+ const count = (type) => statements.filter((statement) => statement.type === type).length;
1213
+ const text = /[/\\]migration\.sql$/.test(path) && new URL('..', folder).href === directory.href ? await readFile(path, 'utf8') : undefined;
1214
+ // Not provably this run's, so named for the project to remove rather than removed.
1215
+ if (text === undefined) {
1216
+ throw new Refusal(`drizzle-kit reported writing ${path ? JSON.stringify(path) : 'a migration with no path'}, which is not a migration.sql in a folder under ${directory.href}, so \`mithaq migrate\` cannot make its schema lines keep the schema and has not touched it. Delete ${path ? `${JSON.stringify(path)} and the folder it is in` : 'the folder drizzle-kit wrote'} before the next run, and report it with the drizzle-kit version that wrote it.`);
1217
+ }
1218
+ const kept = keptSchemas({ text, expected: { created: count('create_schema'), dropped: count('drop_schema') } });
1219
+ if (kept === undefined) {
1220
+ await rm(folder, { recursive: true, force: true });
1221
+ throw new Refusal(`drizzle-kit wrote ${path} in a shape \`mithaq migrate\` has not measured: its CREATE SCHEMA and DROP SCHEMA lines are not one for each create_schema and drop_schema it explained, so they cannot each be made to keep the schema. Nothing was written. Report it with the drizzle-kit version that wrote it.`);
1222
+ }
1223
+ // drizzle's own drop goes last, after the copy, once every join table exists.
1224
+ let final = kept;
1225
+ for (const found of adopted) {
1226
+ const chunks = final.split(breakpoint).map((chunk) => chunk.trim());
1227
+ const drop = `DROP TABLE ${tableNamed(found.schema, found.rels)};`;
1228
+ if (chunks.filter((chunk) => chunk === drop).length !== 1) {
1229
+ await rm(folder, { recursive: true, force: true });
1230
+ throw new Refusal(`drizzle-kit wrote ${path} without the one \`${drop}\` it explained, so \`mithaq migrate\` cannot put the copy of ${found.schema}.${found.rels} before it. Nothing was written. Report it with the drizzle-kit version that wrote it.`);
1231
+ }
1232
+ final = [...chunks.filter((chunk) => chunk !== drop), ...copyOf({ found, drop })].join(`${breakpoint}\n`);
1233
+ }
1234
+ for (const { schema, name, users } of keptEnums) {
1235
+ const drop = `DROP TYPE ${tableNamed(schema, name)};`;
1236
+ const chunks = final.split(breakpoint).map((chunk) => chunk.trim());
1237
+ if (chunks.filter((chunk) => chunk === drop).length !== 1) {
1238
+ await rm(folder, { recursive: true, force: true });
1239
+ throw new Refusal(`drizzle-kit wrote ${path} without the one \`${drop}\` it explained, so \`mithaq migrate\` cannot leave out the drop of ${schema}.${name}, which ${listed(users)} still ${users.length === 1 ? 'uses' : 'use'}. Nothing was written. Report it with the drizzle-kit version that wrote it.`);
1240
+ }
1241
+ final = chunks.filter((chunk) => chunk !== drop).join(`${breakpoint}\n`);
1242
+ console.log(`The enum ${schema}.${name} is kept: ${listed(users)} still ${users.length === 1 ? 'uses' : 'use'} it, and no kayan does.`);
1243
+ }
1244
+ // A `_rels` no field carries any more may hold rows where it is applied, so its drop
1245
+ // is refused there, counted.
1246
+ for (const pair of payloadShaped.filter((one) => !copied.has(one))) {
1247
+ const [schema, rels] = [pair.slice(0, pair.indexOf('.')), pair.slice(pair.indexOf('.') + 1)];
1248
+ const drop = `DROP TABLE ${tableNamed(schema, rels)};`;
1249
+ final = final.split(breakpoint).map((chunk) => chunk.trim()).flatMap((chunk) => chunk === drop
1250
+ ? [`-- ${rels} is Payload's relationship table, which no hasMany field carries now, so it is dropped only
1251
+ -- while it holds no row.
1252
+ DO $$ DECLARE lost integer; BEGIN
1253
+ SELECT count(*) INTO lost FROM ${tableNamed(schema, rels)};
1254
+ IF lost > 0 THEN
1255
+ RAISE EXCEPTION '${rels} holds % rows that no hasMany field carries, and dropping it would lose them. Nothing was applied.', lost;
1256
+ END IF;
1257
+ END $$;`, chunk]
1258
+ : [chunk]).join(`${breakpoint}\n`);
1259
+ }
1260
+ if (final !== text)
1261
+ await writeFile(path, final);
1262
+ console.log(`Wrote ${written.migration_path}.`);
1263
+ return 0;
1264
+ };
1265
+ // The two statuses both runs answer alike; `ok` and `no_changes` are the caller's.
1266
+ const answerOf = (envelope) => {
1267
+ if (envelope.status === 'error') {
1268
+ console.error(said(envelope.error));
1269
+ return 1;
1270
+ }
1271
+ for (const item of envelope.unresolved)
1272
+ console.error(unresolvedLine(item));
1273
+ console.error(`Write the reply down in ${hintsName} and run \`mithaq migrate\` again.`);
1274
+ return 2;
1275
+ };
1276
+ // A bare specifier is a package name. Built from URL paths, since a module specifier is posix everywhere.
1277
+ const relativeSpecifier = (from, to) => {
1278
+ const here = from.pathname.split('/').filter(Boolean);
1279
+ const there = to.pathname.split('/').filter(Boolean);
1280
+ let shared = 0;
1281
+ while (shared < here.length && shared < there.length && here[shared] === there[shared])
1282
+ shared += 1;
1283
+ const up = '../'.repeat(here.length - shared);
1284
+ return `${up || './'}${there.slice(shared).join('/')}`;
1285
+ };