@smartive/graphql-magic 23.7.0-next.5 → 23.8.0-next.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -2
- package/dist/bin/gqm.cjs +142 -55
- package/dist/cjs/index.cjs +142 -55
- package/dist/esm/migrations/generate.d.ts +8 -3
- package/dist/esm/migrations/generate.js +147 -71
- package/dist/esm/migrations/generate.js.map +1 -1
- package/docs/docs/5-migrations.md +4 -0
- package/package.json +1 -1
- package/src/migrations/generate.ts +195 -73
- package/tests/unit/migration-constraints.spec.ts +300 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
## [23.
|
|
1
|
+
## [23.8.0-next.1](https://github.com/smartive/graphql-magic/compare/v23.7.0...v23.8.0-next.1) (2026-03-04)
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
* Exclude constraints and triggers ([12f5d85](https://github.com/smartive/graphql-magic/commit/12f5d85470bdf1107e023700d5a11012df69d098))
|
|
2
6
|
|
|
3
7
|
### Bug Fixes
|
|
4
8
|
|
|
5
|
-
* constraint normalization for empty migration when schema in sync ([
|
|
9
|
+
* constraint normalization for empty migration when schema in sync ([77a8283](https://github.com/smartive/graphql-magic/commit/77a828367abc78c07708556d7d751d780b785a9a))
|
|
10
|
+
* gen ([80bb12d](https://github.com/smartive/graphql-magic/commit/80bb12da6f7eb0aa231fb13c22019995f5811a6c))
|
|
11
|
+
* normalize identifier quoting in constraint comparison ([689136b](https://github.com/smartive/graphql-magic/commit/689136b1ef07874b24986c4596aae37acc92d74b))
|
|
12
|
+
* produce empty migration when schema is in sync ([e8e8e88](https://github.com/smartive/graphql-magic/commit/e8e8e8825a1d4ac833886f993a702c187364ea6f))
|
package/dist/bin/gqm.cjs
CHANGED
|
@@ -1317,7 +1317,6 @@ var MigrationGenerator = class _MigrationGenerator {
|
|
|
1317
1317
|
);
|
|
1318
1318
|
this.updateFields(model, existingFields, up, down);
|
|
1319
1319
|
if (model.constraints?.length) {
|
|
1320
|
-
const existingCheckMap = this.existingCheckConstraints[model.name];
|
|
1321
1320
|
const existingExcludeMap = this.existingExcludeConstraints[model.name];
|
|
1322
1321
|
const existingTriggerMap = this.existingConstraintTriggers[model.name];
|
|
1323
1322
|
for (let i = 0; i < model.constraints.length; i++) {
|
|
@@ -1326,23 +1325,31 @@ var MigrationGenerator = class _MigrationGenerator {
|
|
|
1326
1325
|
const constraintName = this.getConstraintName(model, entry, i);
|
|
1327
1326
|
if (entry.kind === "check") {
|
|
1328
1327
|
validateCheckConstraint(model, entry);
|
|
1329
|
-
const
|
|
1330
|
-
|
|
1331
|
-
if (existingExpression === void 0) {
|
|
1328
|
+
const existingConstraint = this.findExistingConstraint(table, entry, constraintName);
|
|
1329
|
+
if (!existingConstraint) {
|
|
1332
1330
|
up.push(() => {
|
|
1333
|
-
this.addCheckConstraint(table, constraintName,
|
|
1331
|
+
this.addCheckConstraint(table, constraintName, entry.expression, entry.deferrable);
|
|
1334
1332
|
});
|
|
1335
1333
|
down.push(() => {
|
|
1336
1334
|
this.dropCheckConstraint(table, constraintName);
|
|
1337
1335
|
});
|
|
1338
|
-
} else if (
|
|
1336
|
+
} else if (!await this.equalExpressions(
|
|
1337
|
+
table,
|
|
1338
|
+
existingConstraint.constraintName,
|
|
1339
|
+
existingConstraint.expression,
|
|
1340
|
+
entry.expression
|
|
1341
|
+
)) {
|
|
1339
1342
|
up.push(() => {
|
|
1340
|
-
this.dropCheckConstraint(table, constraintName);
|
|
1341
|
-
this.addCheckConstraint(table, constraintName,
|
|
1343
|
+
this.dropCheckConstraint(table, existingConstraint.constraintName);
|
|
1344
|
+
this.addCheckConstraint(table, constraintName, entry.expression, entry.deferrable);
|
|
1342
1345
|
});
|
|
1343
1346
|
down.push(() => {
|
|
1344
1347
|
this.dropCheckConstraint(table, constraintName);
|
|
1345
|
-
this.addCheckConstraint(
|
|
1348
|
+
this.addCheckConstraint(
|
|
1349
|
+
table,
|
|
1350
|
+
existingConstraint.constraintName,
|
|
1351
|
+
existingConstraint.expression
|
|
1352
|
+
);
|
|
1346
1353
|
});
|
|
1347
1354
|
}
|
|
1348
1355
|
} else if (entry.kind === "exclude") {
|
|
@@ -1814,64 +1821,144 @@ var MigrationGenerator = class _MigrationGenerator {
|
|
|
1814
1821
|
}
|
|
1815
1822
|
return result;
|
|
1816
1823
|
}
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1824
|
+
normalizeExcludeDef(def) {
|
|
1825
|
+
const s = def.replace(/\s+/g, " ").replace(/\s*\(\s*/g, "(").replace(/\s*\)\s*/g, ")").trim();
|
|
1826
|
+
return this.normalizeSqlIdentifiers(s);
|
|
1827
|
+
}
|
|
1828
|
+
normalizeTriggerDef(def) {
|
|
1829
|
+
const s = def.replace(/\s+/g, " ").replace(/\s*\(\s*/g, "(").replace(/\s*\)\s*/g, ")").replace(/\bON\s+[a-zA-Z_][a-zA-Z0-9_]*\./gi, "ON ").trim();
|
|
1830
|
+
return this.normalizeSqlIdentifiers(s);
|
|
1831
|
+
}
|
|
1832
|
+
normalizeCheckExpression(expr) {
|
|
1833
|
+
let normalized = expr.replace(/\s+/g, " ").trim();
|
|
1834
|
+
while (this.isWrappedByOuterParentheses(normalized)) {
|
|
1835
|
+
normalized = normalized.slice(1, -1).trim();
|
|
1836
|
+
}
|
|
1837
|
+
return normalized;
|
|
1838
|
+
}
|
|
1839
|
+
isWrappedByOuterParentheses(expr) {
|
|
1840
|
+
if (!expr.startsWith("(") || !expr.endsWith(")")) {
|
|
1841
|
+
return false;
|
|
1842
|
+
}
|
|
1843
|
+
let depth = 0;
|
|
1844
|
+
let inSingleQuote = false;
|
|
1845
|
+
for (let i = 0; i < expr.length; i++) {
|
|
1846
|
+
const char = expr[i];
|
|
1847
|
+
const next = expr[i + 1];
|
|
1848
|
+
if (char === "'") {
|
|
1849
|
+
if (inSingleQuote && next === "'") {
|
|
1850
|
+
i++;
|
|
1851
|
+
continue;
|
|
1826
1852
|
}
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1853
|
+
inSingleQuote = !inSingleQuote;
|
|
1854
|
+
continue;
|
|
1855
|
+
}
|
|
1856
|
+
if (inSingleQuote) {
|
|
1857
|
+
continue;
|
|
1858
|
+
}
|
|
1859
|
+
if (char === "(") {
|
|
1860
|
+
depth++;
|
|
1861
|
+
} else if (char === ")") {
|
|
1862
|
+
depth--;
|
|
1863
|
+
if (depth === 0 && i !== expr.length - 1) {
|
|
1864
|
+
return false;
|
|
1865
|
+
}
|
|
1866
|
+
if (depth < 0) {
|
|
1867
|
+
return false;
|
|
1830
1868
|
}
|
|
1831
1869
|
}
|
|
1832
|
-
|
|
1833
|
-
|
|
1870
|
+
}
|
|
1871
|
+
return depth === 0;
|
|
1872
|
+
}
|
|
1873
|
+
findExistingConstraint(table, entry, preferredConstraintName) {
|
|
1874
|
+
const existingMap = this.existingCheckConstraints[table];
|
|
1875
|
+
if (!existingMap) {
|
|
1876
|
+
return null;
|
|
1877
|
+
}
|
|
1878
|
+
const preferredExpression = existingMap.get(preferredConstraintName);
|
|
1879
|
+
if (preferredExpression !== void 0) {
|
|
1880
|
+
return {
|
|
1881
|
+
constraintName: preferredConstraintName,
|
|
1882
|
+
expression: preferredExpression
|
|
1883
|
+
};
|
|
1884
|
+
}
|
|
1885
|
+
const normalizedNewExpression = this.normalizeCheckExpression(entry.expression);
|
|
1886
|
+
const constraintPrefix = `${table}_${entry.name}_${entry.kind}_`;
|
|
1887
|
+
for (const [constraintName, expression] of existingMap.entries()) {
|
|
1888
|
+
if (!constraintName.startsWith(constraintPrefix)) {
|
|
1889
|
+
continue;
|
|
1890
|
+
}
|
|
1891
|
+
if (this.normalizeCheckExpression(expression) !== normalizedNewExpression) {
|
|
1892
|
+
continue;
|
|
1834
1893
|
}
|
|
1835
|
-
|
|
1894
|
+
return { constraintName, expression };
|
|
1836
1895
|
}
|
|
1837
|
-
return
|
|
1896
|
+
return null;
|
|
1838
1897
|
}
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1898
|
+
async equalExpressions(table, constraintName, existingExpression, newExpression) {
|
|
1899
|
+
try {
|
|
1900
|
+
const [canonicalExisting, canonicalNew] = await Promise.all([
|
|
1901
|
+
this.canonicalizeCheckExpressionWithPostgres(table, existingExpression),
|
|
1902
|
+
this.canonicalizeCheckExpressionWithPostgres(table, newExpression)
|
|
1903
|
+
]);
|
|
1904
|
+
return canonicalExisting === canonicalNew;
|
|
1905
|
+
} catch (error) {
|
|
1906
|
+
console.warn(
|
|
1907
|
+
`Failed to canonicalize check constraint "${constraintName}" on table "${table}". Treating it as changed.`,
|
|
1908
|
+
error
|
|
1909
|
+
);
|
|
1910
|
+
return false;
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
async canonicalizeCheckExpressionWithPostgres(table, expression) {
|
|
1914
|
+
const sourceTableIdentifier = table.split(".").map((part) => this.quoteIdentifier(part)).join(".");
|
|
1915
|
+
const uniqueSuffix = `${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
1916
|
+
const tableSlug = table.toLowerCase().replace(/[^a-z0-9_]/g, "_");
|
|
1917
|
+
const tempTableName = `gqm_tmp_check_${tableSlug}_${uniqueSuffix}`;
|
|
1918
|
+
const tempTableIdentifier = this.quoteIdentifier(tempTableName);
|
|
1919
|
+
const constraintName = `gqm_tmp_check_${uniqueSuffix}`;
|
|
1920
|
+
const constraintIdentifier = this.quoteIdentifier(constraintName);
|
|
1921
|
+
const trx = await this.knex.transaction();
|
|
1922
|
+
try {
|
|
1923
|
+
await trx.raw(`CREATE TEMP TABLE ${tempTableIdentifier} (LIKE ${sourceTableIdentifier}) ON COMMIT DROP`);
|
|
1924
|
+
await trx.raw(`ALTER TABLE ${tempTableIdentifier} ADD CONSTRAINT ${constraintIdentifier} CHECK (${expression})`);
|
|
1925
|
+
const result = await trx.raw(
|
|
1926
|
+
`SELECT pg_get_constraintdef(c.oid, true) AS constraint_definition
|
|
1927
|
+
FROM pg_constraint c
|
|
1928
|
+
JOIN pg_class t
|
|
1929
|
+
ON t.oid = c.conrelid
|
|
1930
|
+
WHERE t.relname = ?
|
|
1931
|
+
AND c.conname = ?
|
|
1932
|
+
ORDER BY c.oid DESC
|
|
1933
|
+
LIMIT 1`,
|
|
1934
|
+
[tempTableName, constraintName]
|
|
1935
|
+
);
|
|
1936
|
+
const rows = "rows" in result && Array.isArray(result.rows) ? result.rows : [];
|
|
1937
|
+
const definition = rows[0]?.constraint_definition;
|
|
1938
|
+
if (!definition) {
|
|
1939
|
+
throw new Error(`Could not read canonical check definition for expression: ${expression}`);
|
|
1849
1940
|
}
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1941
|
+
return this.normalizeCheckExpression(this.extractCheckExpressionFromDefinition(definition));
|
|
1942
|
+
} finally {
|
|
1943
|
+
try {
|
|
1944
|
+
await trx.rollback();
|
|
1945
|
+
} catch {
|
|
1854
1946
|
}
|
|
1855
1947
|
}
|
|
1856
|
-
parts.push(s.slice(start).trim());
|
|
1857
|
-
return parts.filter(Boolean);
|
|
1858
1948
|
}
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
const
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
s = s.replace(/\s*\(\s*/g, "(").replace(/\s*\)\s*/g, ")").replace(/\s+AND\s+/gi, " AND ").replace(/\s+OR\s+/gi, " OR ").trim();
|
|
1867
|
-
return this.normalizeSqlIdentifiers(s);
|
|
1949
|
+
extractCheckExpressionFromDefinition(definition) {
|
|
1950
|
+
const trimmed = definition.trim();
|
|
1951
|
+
const match = trimmed.match(/^CHECK\s*\(([\s\S]*)\)$/i);
|
|
1952
|
+
if (!match) {
|
|
1953
|
+
return trimmed;
|
|
1954
|
+
}
|
|
1955
|
+
return match[1];
|
|
1868
1956
|
}
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
return this.normalizeSqlIdentifiers(s);
|
|
1957
|
+
quoteIdentifier(identifier) {
|
|
1958
|
+
return `"${identifier.replace(/"/g, '""')}"`;
|
|
1872
1959
|
}
|
|
1873
|
-
|
|
1874
|
-
return
|
|
1960
|
+
quoteQualifiedIdentifier(identifier) {
|
|
1961
|
+
return identifier.split(".").map((part) => this.quoteIdentifier(part)).join(".");
|
|
1875
1962
|
}
|
|
1876
1963
|
/** Escape expression for embedding inside a template literal in generated code */
|
|
1877
1964
|
escapeExpressionForRaw(expr) {
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -3332,7 +3332,6 @@ var MigrationGenerator = class _MigrationGenerator {
|
|
|
3332
3332
|
);
|
|
3333
3333
|
this.updateFields(model, existingFields, up, down);
|
|
3334
3334
|
if (model.constraints?.length) {
|
|
3335
|
-
const existingCheckMap = this.existingCheckConstraints[model.name];
|
|
3336
3335
|
const existingExcludeMap = this.existingExcludeConstraints[model.name];
|
|
3337
3336
|
const existingTriggerMap = this.existingConstraintTriggers[model.name];
|
|
3338
3337
|
for (let i = 0; i < model.constraints.length; i++) {
|
|
@@ -3341,23 +3340,31 @@ var MigrationGenerator = class _MigrationGenerator {
|
|
|
3341
3340
|
const constraintName = this.getConstraintName(model, entry, i);
|
|
3342
3341
|
if (entry.kind === "check") {
|
|
3343
3342
|
validateCheckConstraint(model, entry);
|
|
3344
|
-
const
|
|
3345
|
-
|
|
3346
|
-
if (existingExpression === void 0) {
|
|
3343
|
+
const existingConstraint = this.findExistingConstraint(table, entry, constraintName);
|
|
3344
|
+
if (!existingConstraint) {
|
|
3347
3345
|
up.push(() => {
|
|
3348
|
-
this.addCheckConstraint(table, constraintName,
|
|
3346
|
+
this.addCheckConstraint(table, constraintName, entry.expression, entry.deferrable);
|
|
3349
3347
|
});
|
|
3350
3348
|
down.push(() => {
|
|
3351
3349
|
this.dropCheckConstraint(table, constraintName);
|
|
3352
3350
|
});
|
|
3353
|
-
} else if (
|
|
3351
|
+
} else if (!await this.equalExpressions(
|
|
3352
|
+
table,
|
|
3353
|
+
existingConstraint.constraintName,
|
|
3354
|
+
existingConstraint.expression,
|
|
3355
|
+
entry.expression
|
|
3356
|
+
)) {
|
|
3354
3357
|
up.push(() => {
|
|
3355
|
-
this.dropCheckConstraint(table, constraintName);
|
|
3356
|
-
this.addCheckConstraint(table, constraintName,
|
|
3358
|
+
this.dropCheckConstraint(table, existingConstraint.constraintName);
|
|
3359
|
+
this.addCheckConstraint(table, constraintName, entry.expression, entry.deferrable);
|
|
3357
3360
|
});
|
|
3358
3361
|
down.push(() => {
|
|
3359
3362
|
this.dropCheckConstraint(table, constraintName);
|
|
3360
|
-
this.addCheckConstraint(
|
|
3363
|
+
this.addCheckConstraint(
|
|
3364
|
+
table,
|
|
3365
|
+
existingConstraint.constraintName,
|
|
3366
|
+
existingConstraint.expression
|
|
3367
|
+
);
|
|
3361
3368
|
});
|
|
3362
3369
|
}
|
|
3363
3370
|
} else if (entry.kind === "exclude") {
|
|
@@ -3829,64 +3836,144 @@ var MigrationGenerator = class _MigrationGenerator {
|
|
|
3829
3836
|
}
|
|
3830
3837
|
return result;
|
|
3831
3838
|
}
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3839
|
+
normalizeExcludeDef(def) {
|
|
3840
|
+
const s = def.replace(/\s+/g, " ").replace(/\s*\(\s*/g, "(").replace(/\s*\)\s*/g, ")").trim();
|
|
3841
|
+
return this.normalizeSqlIdentifiers(s);
|
|
3842
|
+
}
|
|
3843
|
+
normalizeTriggerDef(def) {
|
|
3844
|
+
const s = def.replace(/\s+/g, " ").replace(/\s*\(\s*/g, "(").replace(/\s*\)\s*/g, ")").replace(/\bON\s+[a-zA-Z_][a-zA-Z0-9_]*\./gi, "ON ").trim();
|
|
3845
|
+
return this.normalizeSqlIdentifiers(s);
|
|
3846
|
+
}
|
|
3847
|
+
normalizeCheckExpression(expr) {
|
|
3848
|
+
let normalized = expr.replace(/\s+/g, " ").trim();
|
|
3849
|
+
while (this.isWrappedByOuterParentheses(normalized)) {
|
|
3850
|
+
normalized = normalized.slice(1, -1).trim();
|
|
3851
|
+
}
|
|
3852
|
+
return normalized;
|
|
3853
|
+
}
|
|
3854
|
+
isWrappedByOuterParentheses(expr) {
|
|
3855
|
+
if (!expr.startsWith("(") || !expr.endsWith(")")) {
|
|
3856
|
+
return false;
|
|
3857
|
+
}
|
|
3858
|
+
let depth = 0;
|
|
3859
|
+
let inSingleQuote = false;
|
|
3860
|
+
for (let i = 0; i < expr.length; i++) {
|
|
3861
|
+
const char = expr[i];
|
|
3862
|
+
const next = expr[i + 1];
|
|
3863
|
+
if (char === "'") {
|
|
3864
|
+
if (inSingleQuote && next === "'") {
|
|
3865
|
+
i++;
|
|
3866
|
+
continue;
|
|
3841
3867
|
}
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3868
|
+
inSingleQuote = !inSingleQuote;
|
|
3869
|
+
continue;
|
|
3870
|
+
}
|
|
3871
|
+
if (inSingleQuote) {
|
|
3872
|
+
continue;
|
|
3873
|
+
}
|
|
3874
|
+
if (char === "(") {
|
|
3875
|
+
depth++;
|
|
3876
|
+
} else if (char === ")") {
|
|
3877
|
+
depth--;
|
|
3878
|
+
if (depth === 0 && i !== expr.length - 1) {
|
|
3879
|
+
return false;
|
|
3880
|
+
}
|
|
3881
|
+
if (depth < 0) {
|
|
3882
|
+
return false;
|
|
3845
3883
|
}
|
|
3846
3884
|
}
|
|
3847
|
-
|
|
3848
|
-
|
|
3885
|
+
}
|
|
3886
|
+
return depth === 0;
|
|
3887
|
+
}
|
|
3888
|
+
findExistingConstraint(table, entry, preferredConstraintName) {
|
|
3889
|
+
const existingMap = this.existingCheckConstraints[table];
|
|
3890
|
+
if (!existingMap) {
|
|
3891
|
+
return null;
|
|
3892
|
+
}
|
|
3893
|
+
const preferredExpression = existingMap.get(preferredConstraintName);
|
|
3894
|
+
if (preferredExpression !== void 0) {
|
|
3895
|
+
return {
|
|
3896
|
+
constraintName: preferredConstraintName,
|
|
3897
|
+
expression: preferredExpression
|
|
3898
|
+
};
|
|
3899
|
+
}
|
|
3900
|
+
const normalizedNewExpression = this.normalizeCheckExpression(entry.expression);
|
|
3901
|
+
const constraintPrefix = `${table}_${entry.name}_${entry.kind}_`;
|
|
3902
|
+
for (const [constraintName, expression] of existingMap.entries()) {
|
|
3903
|
+
if (!constraintName.startsWith(constraintPrefix)) {
|
|
3904
|
+
continue;
|
|
3905
|
+
}
|
|
3906
|
+
if (this.normalizeCheckExpression(expression) !== normalizedNewExpression) {
|
|
3907
|
+
continue;
|
|
3849
3908
|
}
|
|
3850
|
-
|
|
3909
|
+
return { constraintName, expression };
|
|
3851
3910
|
}
|
|
3852
|
-
return
|
|
3911
|
+
return null;
|
|
3853
3912
|
}
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3913
|
+
async equalExpressions(table, constraintName, existingExpression, newExpression) {
|
|
3914
|
+
try {
|
|
3915
|
+
const [canonicalExisting, canonicalNew] = await Promise.all([
|
|
3916
|
+
this.canonicalizeCheckExpressionWithPostgres(table, existingExpression),
|
|
3917
|
+
this.canonicalizeCheckExpressionWithPostgres(table, newExpression)
|
|
3918
|
+
]);
|
|
3919
|
+
return canonicalExisting === canonicalNew;
|
|
3920
|
+
} catch (error) {
|
|
3921
|
+
console.warn(
|
|
3922
|
+
`Failed to canonicalize check constraint "${constraintName}" on table "${table}". Treating it as changed.`,
|
|
3923
|
+
error
|
|
3924
|
+
);
|
|
3925
|
+
return false;
|
|
3926
|
+
}
|
|
3927
|
+
}
|
|
3928
|
+
async canonicalizeCheckExpressionWithPostgres(table, expression) {
|
|
3929
|
+
const sourceTableIdentifier = table.split(".").map((part) => this.quoteIdentifier(part)).join(".");
|
|
3930
|
+
const uniqueSuffix = `${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
3931
|
+
const tableSlug = table.toLowerCase().replace(/[^a-z0-9_]/g, "_");
|
|
3932
|
+
const tempTableName = `gqm_tmp_check_${tableSlug}_${uniqueSuffix}`;
|
|
3933
|
+
const tempTableIdentifier = this.quoteIdentifier(tempTableName);
|
|
3934
|
+
const constraintName = `gqm_tmp_check_${uniqueSuffix}`;
|
|
3935
|
+
const constraintIdentifier = this.quoteIdentifier(constraintName);
|
|
3936
|
+
const trx = await this.knex.transaction();
|
|
3937
|
+
try {
|
|
3938
|
+
await trx.raw(`CREATE TEMP TABLE ${tempTableIdentifier} (LIKE ${sourceTableIdentifier}) ON COMMIT DROP`);
|
|
3939
|
+
await trx.raw(`ALTER TABLE ${tempTableIdentifier} ADD CONSTRAINT ${constraintIdentifier} CHECK (${expression})`);
|
|
3940
|
+
const result = await trx.raw(
|
|
3941
|
+
`SELECT pg_get_constraintdef(c.oid, true) AS constraint_definition
|
|
3942
|
+
FROM pg_constraint c
|
|
3943
|
+
JOIN pg_class t
|
|
3944
|
+
ON t.oid = c.conrelid
|
|
3945
|
+
WHERE t.relname = ?
|
|
3946
|
+
AND c.conname = ?
|
|
3947
|
+
ORDER BY c.oid DESC
|
|
3948
|
+
LIMIT 1`,
|
|
3949
|
+
[tempTableName, constraintName]
|
|
3950
|
+
);
|
|
3951
|
+
const rows = "rows" in result && Array.isArray(result.rows) ? result.rows : [];
|
|
3952
|
+
const definition = rows[0]?.constraint_definition;
|
|
3953
|
+
if (!definition) {
|
|
3954
|
+
throw new Error(`Could not read canonical check definition for expression: ${expression}`);
|
|
3864
3955
|
}
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3956
|
+
return this.normalizeCheckExpression(this.extractCheckExpressionFromDefinition(definition));
|
|
3957
|
+
} finally {
|
|
3958
|
+
try {
|
|
3959
|
+
await trx.rollback();
|
|
3960
|
+
} catch {
|
|
3869
3961
|
}
|
|
3870
3962
|
}
|
|
3871
|
-
parts.push(s.slice(start).trim());
|
|
3872
|
-
return parts.filter(Boolean);
|
|
3873
3963
|
}
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
const
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
s = s.replace(/\s*\(\s*/g, "(").replace(/\s*\)\s*/g, ")").replace(/\s+AND\s+/gi, " AND ").replace(/\s+OR\s+/gi, " OR ").trim();
|
|
3882
|
-
return this.normalizeSqlIdentifiers(s);
|
|
3964
|
+
extractCheckExpressionFromDefinition(definition) {
|
|
3965
|
+
const trimmed = definition.trim();
|
|
3966
|
+
const match = trimmed.match(/^CHECK\s*\(([\s\S]*)\)$/i);
|
|
3967
|
+
if (!match) {
|
|
3968
|
+
return trimmed;
|
|
3969
|
+
}
|
|
3970
|
+
return match[1];
|
|
3883
3971
|
}
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
return this.normalizeSqlIdentifiers(s);
|
|
3972
|
+
quoteIdentifier(identifier) {
|
|
3973
|
+
return `"${identifier.replace(/"/g, '""')}"`;
|
|
3887
3974
|
}
|
|
3888
|
-
|
|
3889
|
-
return
|
|
3975
|
+
quoteQualifiedIdentifier(identifier) {
|
|
3976
|
+
return identifier.split(".").map((part) => this.quoteIdentifier(part)).join(".");
|
|
3890
3977
|
}
|
|
3891
3978
|
/** Escape expression for embedding inside a template literal in generated code */
|
|
3892
3979
|
escapeExpressionForRaw(expr) {
|
|
@@ -38,11 +38,16 @@ export declare class MigrationGenerator {
|
|
|
38
38
|
private static readonly SQL_KEYWORDS;
|
|
39
39
|
private static readonly LITERAL_PLACEHOLDER;
|
|
40
40
|
private normalizeSqlIdentifiers;
|
|
41
|
-
private stripOuterParens;
|
|
42
|
-
private splitAtTopLevel;
|
|
43
|
-
private normalizeCheckExpression;
|
|
44
41
|
private normalizeExcludeDef;
|
|
45
42
|
private normalizeTriggerDef;
|
|
43
|
+
private normalizeCheckExpression;
|
|
44
|
+
private isWrappedByOuterParentheses;
|
|
45
|
+
private findExistingConstraint;
|
|
46
|
+
private equalExpressions;
|
|
47
|
+
private canonicalizeCheckExpressionWithPostgres;
|
|
48
|
+
private extractCheckExpressionFromDefinition;
|
|
49
|
+
private quoteIdentifier;
|
|
50
|
+
private quoteQualifiedIdentifier;
|
|
46
51
|
/** Escape expression for embedding inside a template literal in generated code */
|
|
47
52
|
private escapeExpressionForRaw;
|
|
48
53
|
private addCheckConstraint;
|