@stacksjs/database 0.70.240 → 0.70.243
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/migrations.d.ts +30 -0
- package/dist/migrations.js +101 -8
- package/package.json +11 -11
package/dist/migrations.d.ts
CHANGED
|
@@ -42,6 +42,20 @@ export declare function prepareMigrationModelsDir(): { modelsDir: string, skip:
|
|
|
42
42
|
* applied while its first column had never been created.
|
|
43
43
|
*/
|
|
44
44
|
export declare function sqlStatementsOf(content: string): string[];
|
|
45
|
+
/**
|
|
46
|
+
* Make a Postgres `CREATE TYPE … AS ENUM` statement safe to run twice.
|
|
47
|
+
*
|
|
48
|
+
* `CREATE TYPE` has no `IF NOT EXISTS`, so a corpus containing one can only be
|
|
49
|
+
* applied to a database that has never seen it. Every other way a migration run
|
|
50
|
+
* can be interrupted - a partially recorded ledger, a schema restored from a
|
|
51
|
+
* dump, a database built before the ledger existed - leaves `buddy migrate`
|
|
52
|
+
* dead on the first enum it meets, with an error naming a type that is already
|
|
53
|
+
* exactly right.
|
|
54
|
+
*
|
|
55
|
+
* The guard is a `DO` block catching `duplicate_object`, which needs a
|
|
56
|
+
* dollar-quoted body - hence the splitter above having to understand one.
|
|
57
|
+
*/
|
|
58
|
+
export declare function guardPostgresEnumTypes(sql: string): string;
|
|
45
59
|
export declare function preprocessSqliteMigrations(): void;
|
|
46
60
|
/**
|
|
47
61
|
* Public bootstrap entry point.
|
|
@@ -131,6 +145,21 @@ export declare function generateMigrations(options?: GenerateMigrationsOptions):
|
|
|
131
145
|
*/
|
|
132
146
|
export declare function referencesUndefinedType(statement: string, dangling: string[]): boolean;
|
|
133
147
|
export declare function findDanglingTypeReferences(statements: string[]): string[];
|
|
148
|
+
/**
|
|
149
|
+
* Define the enum types an ALTER needs but nothing in the batch creates.
|
|
150
|
+
*
|
|
151
|
+
* Postgres enum columns are backed by a named type, and bun-query-builder names
|
|
152
|
+
* it `<table>_<column>_type` when it creates the table. A column that becomes an
|
|
153
|
+
* enum *later* gets an `ALTER … TYPE "<table>_<column>_type"` naming a type that
|
|
154
|
+
* was never created, because the `CREATE TYPE` only ever accompanied a
|
|
155
|
+
* `CREATE TABLE`.
|
|
156
|
+
*
|
|
157
|
+
* The values are right there in the plan - the model declared them - so the type
|
|
158
|
+
* can be created instead of the statement being thrown away. Throwing it away
|
|
159
|
+
* left the column as it was, so a model change quietly did not happen and the
|
|
160
|
+
* next diff proposed the same thing again forever.
|
|
161
|
+
*/
|
|
162
|
+
export declare function createMissingEnumTypes(dangling: string[], plan: MigrationPlanLike | undefined): { statements: string[], defined: string[] };
|
|
134
163
|
/**
|
|
135
164
|
* SQLite supports a nullable foreign key on `ADD COLUMN`, but not a later
|
|
136
165
|
* `ADD CONSTRAINT`. bun-query-builder emits the new relation column without
|
|
@@ -192,6 +221,7 @@ declare interface MigrationPlanColumn {
|
|
|
192
221
|
table?: string
|
|
193
222
|
column?: string
|
|
194
223
|
}
|
|
224
|
+
enumValues?: string[]
|
|
195
225
|
}
|
|
196
226
|
declare interface MigrationPlanTable {
|
|
197
227
|
table?: string
|
package/dist/migrations.js
CHANGED
|
@@ -96,9 +96,70 @@ export function prepareMigrationModelsDir() {
|
|
|
96
96
|
};
|
|
97
97
|
}
|
|
98
98
|
export function sqlStatementsOf(content) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
const statements = [];
|
|
100
|
+
let current = "", quote = null, dollarTag = null;
|
|
101
|
+
for (let i = 0;i < content.length; i++) {
|
|
102
|
+
const char = content[i];
|
|
103
|
+
if (dollarTag) {
|
|
104
|
+
current += char;
|
|
105
|
+
if (char === "$" && content.startsWith(dollarTag, i)) {
|
|
106
|
+
current += content.slice(i + 1, i + dollarTag.length);
|
|
107
|
+
i += dollarTag.length - 1;
|
|
108
|
+
dollarTag = null;
|
|
109
|
+
}
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (quote) {
|
|
113
|
+
current += char;
|
|
114
|
+
if (quote === "single" && char === "'" || quote === "double" && char === '"')
|
|
115
|
+
quote = null;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (char === "-" && content[i + 1] === "-") {
|
|
119
|
+
const newline = content.indexOf(`
|
|
120
|
+
`, i);
|
|
121
|
+
if (newline === -1)
|
|
122
|
+
break;
|
|
123
|
+
i = newline - 1;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const dollar = char === "$" ? /^\$[A-Za-z_]*\$/.exec(content.slice(i)) : null;
|
|
127
|
+
if (dollar) {
|
|
128
|
+
dollarTag = dollar[0];
|
|
129
|
+
current += dollarTag;
|
|
130
|
+
i += dollarTag.length - 1;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (char === "'") {
|
|
134
|
+
quote = "single";
|
|
135
|
+
current += char;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (char === '"') {
|
|
139
|
+
quote = "double";
|
|
140
|
+
current += char;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (char === ";") {
|
|
144
|
+
const trimmed = current.trim();
|
|
145
|
+
if (trimmed.length > 0)
|
|
146
|
+
statements.push(trimmed);
|
|
147
|
+
current = "";
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
current += char;
|
|
151
|
+
}
|
|
152
|
+
const trailing = current.trim();
|
|
153
|
+
if (trailing.length > 0)
|
|
154
|
+
statements.push(trailing);
|
|
155
|
+
return statements;
|
|
156
|
+
}
|
|
157
|
+
export function guardPostgresEnumTypes(sql) {
|
|
158
|
+
return sql.replace(/CREATE\s+TYPE\s+("?[\w.]+"?)\s+AS\s+ENUM\s*\(([^)]*)\)/gi, (match, name, members, offset, whole) => {
|
|
159
|
+
if (/\bBEGIN\s*$/i.test(whole.slice(Math.max(0, offset - 40), offset)))
|
|
160
|
+
return match;
|
|
161
|
+
return `DO $stacks$ BEGIN CREATE TYPE ${name} AS ENUM (${members}); EXCEPTION WHEN duplicate_object THEN null; END $stacks$`;
|
|
162
|
+
});
|
|
102
163
|
}
|
|
103
164
|
export function preprocessSqliteMigrations() {
|
|
104
165
|
const migrationsDir = join(process.cwd(), "database", "migrations");
|
|
@@ -484,9 +545,9 @@ function makeMigrationsIdempotent() {
|
|
|
484
545
|
} catch {
|
|
485
546
|
continue;
|
|
486
547
|
}
|
|
487
|
-
if (
|
|
548
|
+
if (!(/\bADD\s+(?:COLUMN|CONSTRAINT)\b/i.test(sql) || /\bCREATE\s+TYPE\b/i.test(sql)))
|
|
488
549
|
continue;
|
|
489
|
-
const next = idempotentSql(sql);
|
|
550
|
+
const next = guardPostgresEnumTypes(idempotentSql(sql));
|
|
490
551
|
if (next !== sql)
|
|
491
552
|
try {
|
|
492
553
|
writeFileSync(p, next);
|
|
@@ -751,9 +812,17 @@ export async function generateMigrations(options = {}) {
|
|
|
751
812
|
if (result.hasChanges && sqlStatements.length > 0) {
|
|
752
813
|
const dangling = findDanglingTypeReferences(sqlStatements);
|
|
753
814
|
if (dangling.length > 0) {
|
|
754
|
-
const
|
|
755
|
-
|
|
756
|
-
|
|
815
|
+
const created = createMissingEnumTypes(dangling, result.plan);
|
|
816
|
+
if (created.statements.length > 0) {
|
|
817
|
+
sqlStatements = [...created.statements, ...sqlStatements];
|
|
818
|
+
log.debug(`[migration] Created ${created.statements.length} enum type(s) an ALTER needed: ${created.defined.join(", ")}`);
|
|
819
|
+
}
|
|
820
|
+
const unresolved = dangling.filter((name) => !created.defined.includes(name));
|
|
821
|
+
if (unresolved.length > 0) {
|
|
822
|
+
const before = sqlStatements.length;
|
|
823
|
+
sqlStatements = sqlStatements.filter((statement) => !referencesUndefinedType(statement, unresolved));
|
|
824
|
+
log.warn(`[migration] Skipped ${before - sqlStatements.length} generated statement(s) referencing enum type(s) nothing creates and no model defines values for (${unresolved.slice(0, 3).join(", ")}${unresolved.length > 3 ? ", \u2026" : ""}).`);
|
|
825
|
+
}
|
|
757
826
|
}
|
|
758
827
|
}
|
|
759
828
|
if (result.hasChanges) {
|
|
@@ -787,6 +856,30 @@ export function findDanglingTypeReferences(statements) {
|
|
|
787
856
|
}
|
|
788
857
|
return [...referenced].filter((name) => !defined.has(name)).sort();
|
|
789
858
|
}
|
|
859
|
+
export function createMissingEnumTypes(dangling, plan) {
|
|
860
|
+
if (dangling.length === 0)
|
|
861
|
+
return { statements: [], defined: [] };
|
|
862
|
+
const values = new Map;
|
|
863
|
+
for (const table of plan?.tables ?? []) {
|
|
864
|
+
if (!table.table)
|
|
865
|
+
continue;
|
|
866
|
+
for (const column of table.columns ?? []) {
|
|
867
|
+
if (!column.name || !column.enumValues?.length)
|
|
868
|
+
continue;
|
|
869
|
+
values.set(`${table.table}_${column.name}_type`, column.enumValues);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
const statements = [], defined = [];
|
|
873
|
+
for (const name of dangling) {
|
|
874
|
+
const members = values.get(name);
|
|
875
|
+
if (!members?.length)
|
|
876
|
+
continue;
|
|
877
|
+
const literals = members.map((member) => `'${String(member).replaceAll("'", "''")}'`).join(", ");
|
|
878
|
+
statements.push(`${guardPostgresEnumTypes(`CREATE TYPE "${name}" AS ENUM (${literals})`)};`);
|
|
879
|
+
defined.push(name);
|
|
880
|
+
}
|
|
881
|
+
return { statements, defined };
|
|
882
|
+
}
|
|
790
883
|
export function inlineSqliteAddedColumnReferences(statements, plan) {
|
|
791
884
|
const references = new Map;
|
|
792
885
|
for (const table of plan?.tables ?? []) {
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/database",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.70.
|
|
5
|
+
"version": "0.70.243",
|
|
6
6
|
"description": "The Stacks database integration.",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"contributors": [
|
|
@@ -54,19 +54,19 @@
|
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"@stacksjs/ts-validation": "^0.5.0",
|
|
57
|
-
"bun-query-builder": "^0.2.
|
|
57
|
+
"bun-query-builder": "^0.2.3",
|
|
58
58
|
"dynamodb-tooling": "^0.3.2"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
|
-
"@stacksjs/cli": "0.70.
|
|
62
|
-
"@stacksjs/config": "0.70.
|
|
63
|
-
"@stacksjs/logging": "0.70.
|
|
64
|
-
"@stacksjs/router": "0.70.
|
|
61
|
+
"@stacksjs/cli": "0.70.243",
|
|
62
|
+
"@stacksjs/config": "0.70.243",
|
|
63
|
+
"@stacksjs/logging": "0.70.243",
|
|
64
|
+
"@stacksjs/router": "0.70.243",
|
|
65
65
|
"better-dx": "^0.2.17",
|
|
66
|
-
"@stacksjs/path": "0.70.
|
|
67
|
-
"@stacksjs/query-builder": "0.70.
|
|
68
|
-
"@stacksjs/storage": "0.70.
|
|
69
|
-
"@stacksjs/strings": "0.70.
|
|
70
|
-
"@stacksjs/utils": "0.70.
|
|
66
|
+
"@stacksjs/path": "0.70.243",
|
|
67
|
+
"@stacksjs/query-builder": "0.70.243",
|
|
68
|
+
"@stacksjs/storage": "0.70.243",
|
|
69
|
+
"@stacksjs/strings": "0.70.243",
|
|
70
|
+
"@stacksjs/utils": "0.70.243"
|
|
71
71
|
}
|
|
72
72
|
}
|