@sqg/sqg 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.mjs +100 -19
- package/dist/sqg.mjs +102 -21
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -2744,6 +2744,21 @@ var EnumType = class {
|
|
|
2744
2744
|
return `ENUM(${this.values.map((v) => `'${v}'`).join(", ")})`;
|
|
2745
2745
|
}
|
|
2746
2746
|
};
|
|
2747
|
+
/**
|
|
2748
|
+
* Append a placeholder (`?`, `$1`, `$name`, or an inlined source) to the parts
|
|
2749
|
+
* rendered so far, keeping it separated from the preceding SQL text.
|
|
2750
|
+
*
|
|
2751
|
+
* Without the separator, `LIMIT ${n}` would render as `LIMIT$1` — which
|
|
2752
|
+
* Postgres lexes as the identifier `limit$1` and rejects. The whitespace is
|
|
2753
|
+
* added to the parts themselves so every rendering path (the joined `sql`
|
|
2754
|
+
* string and `sqlParts`, used by the template-literal renderers) agrees.
|
|
2755
|
+
*/
|
|
2756
|
+
function pushPlaceholder(parts, placeholder) {
|
|
2757
|
+
const lastIndex = parts.length - 1;
|
|
2758
|
+
const last = parts[lastIndex];
|
|
2759
|
+
if (typeof last === "string" && /[A-Za-z0-9_$]$/.test(last)) parts[lastIndex] = `${last} `;
|
|
2760
|
+
parts.push(placeholder);
|
|
2761
|
+
}
|
|
2747
2762
|
var SQLQuery = class {
|
|
2748
2763
|
columns;
|
|
2749
2764
|
allColumns;
|
|
@@ -2926,23 +2941,13 @@ function parseSQLQueries(filePath, extraVariables) {
|
|
|
2926
2941
|
return this.sqlParts.filter((part) => typeof part !== "string" && !part.name.startsWith("sources_"));
|
|
2927
2942
|
}
|
|
2928
2943
|
toSqlWithAnonymousPlaceholders() {
|
|
2929
|
-
let sql = "";
|
|
2930
2944
|
const sqlParts = [];
|
|
2931
|
-
for (const part of this.sqlParts) if (typeof part === "string")
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
} else {
|
|
2935
|
-
if (sql.length > 0) {
|
|
2936
|
-
const last = sql[sql.length - 1];
|
|
2937
|
-
if (last !== " " && last !== "=" && last !== ">" && last !== "<") sql += " ";
|
|
2938
|
-
}
|
|
2939
|
-
sql += "?";
|
|
2940
|
-
if (part.name.startsWith("sources_")) sqlParts.push(part);
|
|
2941
|
-
else sqlParts.push("?");
|
|
2942
|
-
}
|
|
2945
|
+
for (const part of this.sqlParts) if (typeof part === "string") sqlParts.push(part);
|
|
2946
|
+
else if (part.name.startsWith("sources_")) pushPlaceholder(sqlParts, part);
|
|
2947
|
+
else pushPlaceholder(sqlParts, "?");
|
|
2943
2948
|
return {
|
|
2944
2949
|
parameters: this.parameters(),
|
|
2945
|
-
sql,
|
|
2950
|
+
sql: sqlParts.map((part) => typeof part === "string" ? part : "?").join(""),
|
|
2946
2951
|
sqlParts
|
|
2947
2952
|
};
|
|
2948
2953
|
}
|
|
@@ -2953,7 +2958,7 @@ function parseSQLQueries(filePath, extraVariables) {
|
|
|
2953
2958
|
else {
|
|
2954
2959
|
const varName = part.name;
|
|
2955
2960
|
const value = part.value;
|
|
2956
|
-
if (varName.startsWith("sources_")) sqlParts
|
|
2961
|
+
if (varName.startsWith("sources_")) pushPlaceholder(sqlParts, part);
|
|
2957
2962
|
else {
|
|
2958
2963
|
let pos = parameters.findIndex((p) => p.name === varName);
|
|
2959
2964
|
if (pos < 0) {
|
|
@@ -2963,7 +2968,7 @@ function parseSQLQueries(filePath, extraVariables) {
|
|
|
2963
2968
|
});
|
|
2964
2969
|
pos = parameters.length;
|
|
2965
2970
|
} else pos = pos + 1;
|
|
2966
|
-
sqlParts
|
|
2971
|
+
pushPlaceholder(sqlParts, `$${pos}`);
|
|
2967
2972
|
}
|
|
2968
2973
|
}
|
|
2969
2974
|
return {
|
|
@@ -2975,8 +2980,8 @@ function parseSQLQueries(filePath, extraVariables) {
|
|
|
2975
2980
|
toSqlWithNamedPlaceholders() {
|
|
2976
2981
|
const sqlParts = [];
|
|
2977
2982
|
for (const part of this.sqlParts) if (typeof part === "string") sqlParts.push(part);
|
|
2978
|
-
else if (part.name.startsWith("sources_")) sqlParts
|
|
2979
|
-
else sqlParts
|
|
2983
|
+
else if (part.name.startsWith("sources_")) pushPlaceholder(sqlParts, part);
|
|
2984
|
+
else pushPlaceholder(sqlParts, `$${part.name}`);
|
|
2980
2985
|
return {
|
|
2981
2986
|
parameters: this.parameters(),
|
|
2982
2987
|
sqlParts,
|
|
@@ -2999,8 +3004,9 @@ function parseSQLQueries(filePath, extraVariables) {
|
|
|
2999
3004
|
if (!varRef.startsWith("${") || !varRef.endsWith("}")) throw SqgError.inQuery(`Invalid variable reference: ${varRef}`, "SQL_PARSE_ERROR", name, filePath, { suggestion: "Variables should be in the format ${varName}" });
|
|
3000
3005
|
const varName = varRef.replace("${", "").replace("}", "");
|
|
3001
3006
|
const value = getVariable(varName);
|
|
3002
|
-
if (
|
|
3007
|
+
if (from >= 0 && child.from > from) sql.appendSql(content.slice(from, child.from));
|
|
3003
3008
|
from = child.to;
|
|
3009
|
+
to = child.to;
|
|
3004
3010
|
sql.appendVariable(varName, value);
|
|
3005
3011
|
} else {
|
|
3006
3012
|
if (from < 0) from = child.from;
|
|
@@ -3051,6 +3057,29 @@ function parseSQLQueries(filePath, extraVariables) {
|
|
|
3051
3057
|
}
|
|
3052
3058
|
//#endregion
|
|
3053
3059
|
//#region src/db/types.ts
|
|
3060
|
+
/**
|
|
3061
|
+
* Whether an error is an integrity constraint violation (foreign key, unique,
|
|
3062
|
+
* check, not-null) rather than a problem with the statement itself.
|
|
3063
|
+
*/
|
|
3064
|
+
function isConstraintViolation(error) {
|
|
3065
|
+
const message = error?.message ?? "";
|
|
3066
|
+
const code = String(error?.code ?? "");
|
|
3067
|
+
return code.startsWith("SQLITE_CONSTRAINT") || /^23/.test(code) || /^Constraint Error/i.test(message) || /violates .* constraint|constraint failed/i.test(message);
|
|
3068
|
+
}
|
|
3069
|
+
/**
|
|
3070
|
+
* Type introspection executes every EXEC statement against whatever data the
|
|
3071
|
+
* MIGRATE and TESTDATA blocks left behind, so a statement can fail purely
|
|
3072
|
+
* because a constraint has nothing to reference — a NOT NULL foreign key can
|
|
3073
|
+
* never be satisfied when no fixture created the parent row.
|
|
3074
|
+
*
|
|
3075
|
+
* Running an EXEC contributes nothing to the generated code (the prepared
|
|
3076
|
+
* statement already supplied the parameter types), so such a failure is
|
|
3077
|
+
* reported as a warning instead of aborting generation. Statements whose
|
|
3078
|
+
* result shape SQG needs (QUERY, including INSERT ... RETURNING) still fail.
|
|
3079
|
+
*/
|
|
3080
|
+
function warnConstraintViolation(query, error) {
|
|
3081
|
+
consola.warn(`Skipped executing '${query.id}' in ${query.filename} during type introspection: ${error.message}\n Introspection runs each statement against the TESTDATA fixtures only, so constraints such as foreign keys may not be satisfiable. Code generation is unaffected — add a TESTDATA block with the referenced rows to silence this warning.`);
|
|
3082
|
+
}
|
|
3054
3083
|
async function initializeDatabase(queries, execQueries, reporter) {
|
|
3055
3084
|
const baselineQueries = queries.filter((q) => q.isBaseline && !q.sourceTarget);
|
|
3056
3085
|
for (const query of baselineQueries) try {
|
|
@@ -3198,6 +3227,10 @@ const duckdb = new class {
|
|
|
3198
3227
|
}
|
|
3199
3228
|
return await stmt.run();
|
|
3200
3229
|
} catch (error) {
|
|
3230
|
+
if (!query.isQuery && isConstraintViolation(error)) {
|
|
3231
|
+
warnConstraintViolation(query, error);
|
|
3232
|
+
return;
|
|
3233
|
+
}
|
|
3201
3234
|
consola.error(`Failed to execute query '${query.id}':`, error);
|
|
3202
3235
|
throw error;
|
|
3203
3236
|
}
|
|
@@ -3238,6 +3271,38 @@ const tempDatabaseName = "sqg-db-temp";
|
|
|
3238
3271
|
const typeIdToName = /* @__PURE__ */ new Map();
|
|
3239
3272
|
for (const [name, id] of Object.entries(types.builtins)) typeIdToName.set(Number(id), name);
|
|
3240
3273
|
/**
|
|
3274
|
+
* Turn off referential integrity enforcement for the introspection connection.
|
|
3275
|
+
*
|
|
3276
|
+
* QUERY and EXEC statements are introspected against whatever data MIGRATE and
|
|
3277
|
+
* TESTDATA left behind, each in its own transaction, so a NOT NULL foreign key
|
|
3278
|
+
* can never be satisfied by a row an earlier statement inserted.
|
|
3279
|
+
* `session_replication_role = replica` disables the triggers enforcing it, for
|
|
3280
|
+
* this connection only, and every statement introspected here is rolled back.
|
|
3281
|
+
*
|
|
3282
|
+
* This happens after the database is initialized, so a foreign key violation in
|
|
3283
|
+
* a MIGRATE or TESTDATA block is still reported — those describe real data.
|
|
3284
|
+
*
|
|
3285
|
+
* Setting it needs superuser (or the SET privilege), so this is best effort:
|
|
3286
|
+
* without it, constraint violations on EXEC statements are still tolerated,
|
|
3287
|
+
* only INSERT ... RETURNING queries would fail.
|
|
3288
|
+
*/
|
|
3289
|
+
async function relaxConstraints(db, inTransaction) {
|
|
3290
|
+
try {
|
|
3291
|
+
if (inTransaction) {
|
|
3292
|
+
await db.query("SAVEPOINT sqg_relax_constraints");
|
|
3293
|
+
try {
|
|
3294
|
+
await db.query("SET LOCAL session_replication_role = 'replica'");
|
|
3295
|
+
} catch (e) {
|
|
3296
|
+
await db.query("ROLLBACK TO SAVEPOINT sqg_relax_constraints");
|
|
3297
|
+
throw e;
|
|
3298
|
+
}
|
|
3299
|
+
await db.query("RELEASE SAVEPOINT sqg_relax_constraints");
|
|
3300
|
+
} else await db.query("SET session_replication_role = 'replica'");
|
|
3301
|
+
} catch (e) {
|
|
3302
|
+
consola.debug(`Could not disable constraint enforcement for introspection: ${e.message}`);
|
|
3303
|
+
}
|
|
3304
|
+
}
|
|
3305
|
+
/**
|
|
3241
3306
|
* External database mode: connects directly to the user's database.
|
|
3242
3307
|
* Uses a single transaction for the entire session and rolls back on close,
|
|
3243
3308
|
* so the database is never modified. Individual queries use savepoints.
|
|
@@ -3264,6 +3329,9 @@ var ExternalDbMode = class {
|
|
|
3264
3329
|
await db.query("ROLLBACK TO SAVEPOINT sqg_query");
|
|
3265
3330
|
}
|
|
3266
3331
|
}
|
|
3332
|
+
relaxConstraints(db) {
|
|
3333
|
+
return relaxConstraints(db, true);
|
|
3334
|
+
}
|
|
3267
3335
|
async close(db) {
|
|
3268
3336
|
await db.query("ROLLBACK");
|
|
3269
3337
|
await db.end();
|
|
@@ -3313,6 +3381,9 @@ var TempDbMode = class {
|
|
|
3313
3381
|
await db.query("ROLLBACK");
|
|
3314
3382
|
}
|
|
3315
3383
|
}
|
|
3384
|
+
relaxConstraints(db) {
|
|
3385
|
+
return relaxConstraints(db, false);
|
|
3386
|
+
}
|
|
3316
3387
|
async close(db) {
|
|
3317
3388
|
await db.end();
|
|
3318
3389
|
await this.dbInitial.query(`DROP DATABASE IF EXISTS "${tempDatabaseName}"`);
|
|
@@ -3408,6 +3479,7 @@ const postgres = new class {
|
|
|
3408
3479
|
async executeQueries(queries, reporter) {
|
|
3409
3480
|
const db = this.db;
|
|
3410
3481
|
if (!db) throw new DatabaseError("PostgreSQL database not initialized", "postgres", "This is an internal error. Check that migrations completed successfully.");
|
|
3482
|
+
await this.mode.relaxConstraints(db);
|
|
3411
3483
|
try {
|
|
3412
3484
|
const executableQueries = queries.filter((q) => !q.skipGenerateFunction);
|
|
3413
3485
|
for (const query of executableQueries) {
|
|
@@ -3474,6 +3546,10 @@ const postgres = new class {
|
|
|
3474
3546
|
}
|
|
3475
3547
|
return result;
|
|
3476
3548
|
} catch (error) {
|
|
3549
|
+
if (!query.isQuery && isConstraintViolation(error)) {
|
|
3550
|
+
warnConstraintViolation(query, error);
|
|
3551
|
+
return;
|
|
3552
|
+
}
|
|
3477
3553
|
consola.error(`Failed to execute query '${query.id}':`, error);
|
|
3478
3554
|
throw error;
|
|
3479
3555
|
}
|
|
@@ -3526,6 +3602,7 @@ const sqlite = new class {
|
|
|
3526
3602
|
executeQueries(queries, reporter) {
|
|
3527
3603
|
const db = this.db;
|
|
3528
3604
|
if (!db) throw new DatabaseError("SQLite database not initialized", "sqlite", "This is an internal error. Migrations may have failed silently.");
|
|
3605
|
+
db.pragma("foreign_keys = OFF");
|
|
3529
3606
|
try {
|
|
3530
3607
|
const executableQueries = queries.filter((q) => !q.skipGenerateFunction);
|
|
3531
3608
|
for (const query of executableQueries) {
|
|
@@ -3626,6 +3703,10 @@ const sqlite = new class {
|
|
|
3626
3703
|
}
|
|
3627
3704
|
return stmt.run(...params);
|
|
3628
3705
|
} catch (error) {
|
|
3706
|
+
if (!query.isQuery && isConstraintViolation(error)) {
|
|
3707
|
+
warnConstraintViolation(query, error);
|
|
3708
|
+
return;
|
|
3709
|
+
}
|
|
3629
3710
|
consola.error(`Failed to execute query '${query.id}' in ${query.filename}:\n ${statement.sql} \n ${statement.parameters.map((p) => p.value).join(", ")}`, error);
|
|
3630
3711
|
throw error;
|
|
3631
3712
|
}
|
package/dist/sqg.mjs
CHANGED
|
@@ -787,6 +787,21 @@ var EnumType = class {
|
|
|
787
787
|
return `ENUM(${this.values.map((v) => `'${v}'`).join(", ")})`;
|
|
788
788
|
}
|
|
789
789
|
};
|
|
790
|
+
/**
|
|
791
|
+
* Append a placeholder (`?`, `$1`, `$name`, or an inlined source) to the parts
|
|
792
|
+
* rendered so far, keeping it separated from the preceding SQL text.
|
|
793
|
+
*
|
|
794
|
+
* Without the separator, `LIMIT ${n}` would render as `LIMIT$1` — which
|
|
795
|
+
* Postgres lexes as the identifier `limit$1` and rejects. The whitespace is
|
|
796
|
+
* added to the parts themselves so every rendering path (the joined `sql`
|
|
797
|
+
* string and `sqlParts`, used by the template-literal renderers) agrees.
|
|
798
|
+
*/
|
|
799
|
+
function pushPlaceholder(parts, placeholder) {
|
|
800
|
+
const lastIndex = parts.length - 1;
|
|
801
|
+
const last = parts[lastIndex];
|
|
802
|
+
if (typeof last === "string" && /[A-Za-z0-9_$]$/.test(last)) parts[lastIndex] = `${last} `;
|
|
803
|
+
parts.push(placeholder);
|
|
804
|
+
}
|
|
790
805
|
var SQLQuery = class {
|
|
791
806
|
columns;
|
|
792
807
|
allColumns;
|
|
@@ -969,23 +984,13 @@ function parseSQLQueries(filePath, extraVariables) {
|
|
|
969
984
|
return this.sqlParts.filter((part) => typeof part !== "string" && !part.name.startsWith("sources_"));
|
|
970
985
|
}
|
|
971
986
|
toSqlWithAnonymousPlaceholders() {
|
|
972
|
-
let sql = "";
|
|
973
987
|
const sqlParts = [];
|
|
974
|
-
for (const part of this.sqlParts) if (typeof part === "string")
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
} else {
|
|
978
|
-
if (sql.length > 0) {
|
|
979
|
-
const last = sql[sql.length - 1];
|
|
980
|
-
if (last !== " " && last !== "=" && last !== ">" && last !== "<") sql += " ";
|
|
981
|
-
}
|
|
982
|
-
sql += "?";
|
|
983
|
-
if (part.name.startsWith("sources_")) sqlParts.push(part);
|
|
984
|
-
else sqlParts.push("?");
|
|
985
|
-
}
|
|
988
|
+
for (const part of this.sqlParts) if (typeof part === "string") sqlParts.push(part);
|
|
989
|
+
else if (part.name.startsWith("sources_")) pushPlaceholder(sqlParts, part);
|
|
990
|
+
else pushPlaceholder(sqlParts, "?");
|
|
986
991
|
return {
|
|
987
992
|
parameters: this.parameters(),
|
|
988
|
-
sql,
|
|
993
|
+
sql: sqlParts.map((part) => typeof part === "string" ? part : "?").join(""),
|
|
989
994
|
sqlParts
|
|
990
995
|
};
|
|
991
996
|
}
|
|
@@ -996,7 +1001,7 @@ function parseSQLQueries(filePath, extraVariables) {
|
|
|
996
1001
|
else {
|
|
997
1002
|
const varName = part.name;
|
|
998
1003
|
const value = part.value;
|
|
999
|
-
if (varName.startsWith("sources_")) sqlParts
|
|
1004
|
+
if (varName.startsWith("sources_")) pushPlaceholder(sqlParts, part);
|
|
1000
1005
|
else {
|
|
1001
1006
|
let pos = parameters.findIndex((p) => p.name === varName);
|
|
1002
1007
|
if (pos < 0) {
|
|
@@ -1006,7 +1011,7 @@ function parseSQLQueries(filePath, extraVariables) {
|
|
|
1006
1011
|
});
|
|
1007
1012
|
pos = parameters.length;
|
|
1008
1013
|
} else pos = pos + 1;
|
|
1009
|
-
sqlParts
|
|
1014
|
+
pushPlaceholder(sqlParts, `$${pos}`);
|
|
1010
1015
|
}
|
|
1011
1016
|
}
|
|
1012
1017
|
return {
|
|
@@ -1018,8 +1023,8 @@ function parseSQLQueries(filePath, extraVariables) {
|
|
|
1018
1023
|
toSqlWithNamedPlaceholders() {
|
|
1019
1024
|
const sqlParts = [];
|
|
1020
1025
|
for (const part of this.sqlParts) if (typeof part === "string") sqlParts.push(part);
|
|
1021
|
-
else if (part.name.startsWith("sources_")) sqlParts
|
|
1022
|
-
else sqlParts
|
|
1026
|
+
else if (part.name.startsWith("sources_")) pushPlaceholder(sqlParts, part);
|
|
1027
|
+
else pushPlaceholder(sqlParts, `$${part.name}`);
|
|
1023
1028
|
return {
|
|
1024
1029
|
parameters: this.parameters(),
|
|
1025
1030
|
sqlParts,
|
|
@@ -1042,8 +1047,9 @@ function parseSQLQueries(filePath, extraVariables) {
|
|
|
1042
1047
|
if (!varRef.startsWith("${") || !varRef.endsWith("}")) throw SqgError.inQuery(`Invalid variable reference: ${varRef}`, "SQL_PARSE_ERROR", name, filePath, { suggestion: "Variables should be in the format ${varName}" });
|
|
1043
1048
|
const varName = varRef.replace("${", "").replace("}", "");
|
|
1044
1049
|
const value = getVariable(varName);
|
|
1045
|
-
if (
|
|
1050
|
+
if (from >= 0 && child.from > from) sql.appendSql(content.slice(from, child.from));
|
|
1046
1051
|
from = child.to;
|
|
1052
|
+
to = child.to;
|
|
1047
1053
|
sql.appendVariable(varName, value);
|
|
1048
1054
|
} else {
|
|
1049
1055
|
if (from < 0) from = child.from;
|
|
@@ -1094,6 +1100,29 @@ function parseSQLQueries(filePath, extraVariables) {
|
|
|
1094
1100
|
}
|
|
1095
1101
|
//#endregion
|
|
1096
1102
|
//#region src/db/types.ts
|
|
1103
|
+
/**
|
|
1104
|
+
* Whether an error is an integrity constraint violation (foreign key, unique,
|
|
1105
|
+
* check, not-null) rather than a problem with the statement itself.
|
|
1106
|
+
*/
|
|
1107
|
+
function isConstraintViolation(error) {
|
|
1108
|
+
const message = error?.message ?? "";
|
|
1109
|
+
const code = String(error?.code ?? "");
|
|
1110
|
+
return code.startsWith("SQLITE_CONSTRAINT") || /^23/.test(code) || /^Constraint Error/i.test(message) || /violates .* constraint|constraint failed/i.test(message);
|
|
1111
|
+
}
|
|
1112
|
+
/**
|
|
1113
|
+
* Type introspection executes every EXEC statement against whatever data the
|
|
1114
|
+
* MIGRATE and TESTDATA blocks left behind, so a statement can fail purely
|
|
1115
|
+
* because a constraint has nothing to reference — a NOT NULL foreign key can
|
|
1116
|
+
* never be satisfied when no fixture created the parent row.
|
|
1117
|
+
*
|
|
1118
|
+
* Running an EXEC contributes nothing to the generated code (the prepared
|
|
1119
|
+
* statement already supplied the parameter types), so such a failure is
|
|
1120
|
+
* reported as a warning instead of aborting generation. Statements whose
|
|
1121
|
+
* result shape SQG needs (QUERY, including INSERT ... RETURNING) still fail.
|
|
1122
|
+
*/
|
|
1123
|
+
function warnConstraintViolation(query, error) {
|
|
1124
|
+
consola.warn(`Skipped executing '${query.id}' in ${query.filename} during type introspection: ${error.message}\n Introspection runs each statement against the TESTDATA fixtures only, so constraints such as foreign keys may not be satisfiable. Code generation is unaffected — add a TESTDATA block with the referenced rows to silence this warning.`);
|
|
1125
|
+
}
|
|
1097
1126
|
async function initializeDatabase(queries, execQueries, reporter) {
|
|
1098
1127
|
const baselineQueries = queries.filter((q) => q.isBaseline && !q.sourceTarget);
|
|
1099
1128
|
for (const query of baselineQueries) try {
|
|
@@ -1241,6 +1270,10 @@ const duckdb = new class {
|
|
|
1241
1270
|
}
|
|
1242
1271
|
return await stmt.run();
|
|
1243
1272
|
} catch (error) {
|
|
1273
|
+
if (!query.isQuery && isConstraintViolation(error)) {
|
|
1274
|
+
warnConstraintViolation(query, error);
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1244
1277
|
consola.error(`Failed to execute query '${query.id}':`, error);
|
|
1245
1278
|
throw error;
|
|
1246
1279
|
}
|
|
@@ -1281,6 +1314,38 @@ const tempDatabaseName = "sqg-db-temp";
|
|
|
1281
1314
|
const typeIdToName = /* @__PURE__ */ new Map();
|
|
1282
1315
|
for (const [name, id] of Object.entries(types.builtins)) typeIdToName.set(Number(id), name);
|
|
1283
1316
|
/**
|
|
1317
|
+
* Turn off referential integrity enforcement for the introspection connection.
|
|
1318
|
+
*
|
|
1319
|
+
* QUERY and EXEC statements are introspected against whatever data MIGRATE and
|
|
1320
|
+
* TESTDATA left behind, each in its own transaction, so a NOT NULL foreign key
|
|
1321
|
+
* can never be satisfied by a row an earlier statement inserted.
|
|
1322
|
+
* `session_replication_role = replica` disables the triggers enforcing it, for
|
|
1323
|
+
* this connection only, and every statement introspected here is rolled back.
|
|
1324
|
+
*
|
|
1325
|
+
* This happens after the database is initialized, so a foreign key violation in
|
|
1326
|
+
* a MIGRATE or TESTDATA block is still reported — those describe real data.
|
|
1327
|
+
*
|
|
1328
|
+
* Setting it needs superuser (or the SET privilege), so this is best effort:
|
|
1329
|
+
* without it, constraint violations on EXEC statements are still tolerated,
|
|
1330
|
+
* only INSERT ... RETURNING queries would fail.
|
|
1331
|
+
*/
|
|
1332
|
+
async function relaxConstraints(db, inTransaction) {
|
|
1333
|
+
try {
|
|
1334
|
+
if (inTransaction) {
|
|
1335
|
+
await db.query("SAVEPOINT sqg_relax_constraints");
|
|
1336
|
+
try {
|
|
1337
|
+
await db.query("SET LOCAL session_replication_role = 'replica'");
|
|
1338
|
+
} catch (e) {
|
|
1339
|
+
await db.query("ROLLBACK TO SAVEPOINT sqg_relax_constraints");
|
|
1340
|
+
throw e;
|
|
1341
|
+
}
|
|
1342
|
+
await db.query("RELEASE SAVEPOINT sqg_relax_constraints");
|
|
1343
|
+
} else await db.query("SET session_replication_role = 'replica'");
|
|
1344
|
+
} catch (e) {
|
|
1345
|
+
consola.debug(`Could not disable constraint enforcement for introspection: ${e.message}`);
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
/**
|
|
1284
1349
|
* External database mode: connects directly to the user's database.
|
|
1285
1350
|
* Uses a single transaction for the entire session and rolls back on close,
|
|
1286
1351
|
* so the database is never modified. Individual queries use savepoints.
|
|
@@ -1307,6 +1372,9 @@ var ExternalDbMode = class {
|
|
|
1307
1372
|
await db.query("ROLLBACK TO SAVEPOINT sqg_query");
|
|
1308
1373
|
}
|
|
1309
1374
|
}
|
|
1375
|
+
relaxConstraints(db) {
|
|
1376
|
+
return relaxConstraints(db, true);
|
|
1377
|
+
}
|
|
1310
1378
|
async close(db) {
|
|
1311
1379
|
await db.query("ROLLBACK");
|
|
1312
1380
|
await db.end();
|
|
@@ -1356,6 +1424,9 @@ var TempDbMode = class {
|
|
|
1356
1424
|
await db.query("ROLLBACK");
|
|
1357
1425
|
}
|
|
1358
1426
|
}
|
|
1427
|
+
relaxConstraints(db) {
|
|
1428
|
+
return relaxConstraints(db, false);
|
|
1429
|
+
}
|
|
1359
1430
|
async close(db) {
|
|
1360
1431
|
await db.end();
|
|
1361
1432
|
await this.dbInitial.query(`DROP DATABASE IF EXISTS "${tempDatabaseName}"`);
|
|
@@ -1451,6 +1522,7 @@ const postgres = new class {
|
|
|
1451
1522
|
async executeQueries(queries, reporter) {
|
|
1452
1523
|
const db = this.db;
|
|
1453
1524
|
if (!db) throw new DatabaseError("PostgreSQL database not initialized", "postgres", "This is an internal error. Check that migrations completed successfully.");
|
|
1525
|
+
await this.mode.relaxConstraints(db);
|
|
1454
1526
|
try {
|
|
1455
1527
|
const executableQueries = queries.filter((q) => !q.skipGenerateFunction);
|
|
1456
1528
|
for (const query of executableQueries) {
|
|
@@ -1517,6 +1589,10 @@ const postgres = new class {
|
|
|
1517
1589
|
}
|
|
1518
1590
|
return result;
|
|
1519
1591
|
} catch (error) {
|
|
1592
|
+
if (!query.isQuery && isConstraintViolation(error)) {
|
|
1593
|
+
warnConstraintViolation(query, error);
|
|
1594
|
+
return;
|
|
1595
|
+
}
|
|
1520
1596
|
consola.error(`Failed to execute query '${query.id}':`, error);
|
|
1521
1597
|
throw error;
|
|
1522
1598
|
}
|
|
@@ -1569,6 +1645,7 @@ const sqlite = new class {
|
|
|
1569
1645
|
executeQueries(queries, reporter) {
|
|
1570
1646
|
const db = this.db;
|
|
1571
1647
|
if (!db) throw new DatabaseError("SQLite database not initialized", "sqlite", "This is an internal error. Migrations may have failed silently.");
|
|
1648
|
+
db.pragma("foreign_keys = OFF");
|
|
1572
1649
|
try {
|
|
1573
1650
|
const executableQueries = queries.filter((q) => !q.skipGenerateFunction);
|
|
1574
1651
|
for (const query of executableQueries) {
|
|
@@ -1669,6 +1746,10 @@ const sqlite = new class {
|
|
|
1669
1746
|
}
|
|
1670
1747
|
return stmt.run(...params);
|
|
1671
1748
|
} catch (error) {
|
|
1749
|
+
if (!query.isQuery && isConstraintViolation(error)) {
|
|
1750
|
+
warnConstraintViolation(query, error);
|
|
1751
|
+
return;
|
|
1752
|
+
}
|
|
1672
1753
|
consola.error(`Failed to execute query '${query.id}' in ${query.filename}:\n ${statement.sql} \n ${statement.parameters.map((p) => p.value).join(", ")}`, error);
|
|
1673
1754
|
throw error;
|
|
1674
1755
|
}
|
|
@@ -3964,7 +4045,7 @@ async function processProject(projectPath, ui) {
|
|
|
3964
4045
|
//#region src/mcp-server.ts
|
|
3965
4046
|
const server = new Server({
|
|
3966
4047
|
name: "sqg-mcp",
|
|
3967
|
-
version: process.env.npm_package_version ?? "0.
|
|
4048
|
+
version: process.env.npm_package_version ?? "0.23.0"
|
|
3968
4049
|
}, { capabilities: {
|
|
3969
4050
|
tools: {},
|
|
3970
4051
|
resources: {}
|
|
@@ -4435,7 +4516,7 @@ function formatMs(ms) {
|
|
|
4435
4516
|
}
|
|
4436
4517
|
//#endregion
|
|
4437
4518
|
//#region src/sqg.ts
|
|
4438
|
-
const version = process.env.npm_package_version ?? "0.
|
|
4519
|
+
const version = process.env.npm_package_version ?? "0.23.0";
|
|
4439
4520
|
updateNotifier({ pkg: {
|
|
4440
4521
|
name: "@sqg/sqg",
|
|
4441
4522
|
version
|