@stacksjs/database 0.70.241 → 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.
@@ -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.
@@ -96,9 +96,70 @@ export function prepareMigrationModelsDir() {
96
96
  };
97
97
  }
98
98
  export function sqlStatementsOf(content) {
99
- return content.split(`
100
- `).map((line) => line.replace(/--.*$/, "")).join(`
101
- `).split(";").map((s) => s.trim()).filter((s) => s.length > 0);
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 (!/\bADD\s+(?:COLUMN|CONSTRAINT)\b/i.test(sql))
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);
@@ -814,7 +875,7 @@ export function createMissingEnumTypes(dangling, plan) {
814
875
  if (!members?.length)
815
876
  continue;
816
877
  const literals = members.map((member) => `'${String(member).replaceAll("'", "''")}'`).join(", ");
817
- statements.push(`DO $$ BEGIN CREATE TYPE "${name}" AS ENUM (${literals}); EXCEPTION WHEN duplicate_object THEN null; END $$;`);
878
+ statements.push(`${guardPostgresEnumTypes(`CREATE TYPE "${name}" AS ENUM (${literals})`)};`);
818
879
  defined.push(name);
819
880
  }
820
881
  return { statements, defined };
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.241",
5
+ "version": "0.70.243",
6
6
  "description": "The Stacks database integration.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -58,15 +58,15 @@
58
58
  "dynamodb-tooling": "^0.3.2"
59
59
  },
60
60
  "devDependencies": {
61
- "@stacksjs/cli": "0.70.241",
62
- "@stacksjs/config": "0.70.241",
63
- "@stacksjs/logging": "0.70.241",
64
- "@stacksjs/router": "0.70.241",
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.241",
67
- "@stacksjs/query-builder": "0.70.241",
68
- "@stacksjs/storage": "0.70.241",
69
- "@stacksjs/strings": "0.70.241",
70
- "@stacksjs/utils": "0.70.241"
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
  }