@syntopica/db-quality 0.2.1 → 0.3.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/cli.js CHANGED
@@ -546,6 +546,52 @@ var postgrestSectionFrom = (raw) => {
546
546
  return { roots };
547
547
  };
548
548
 
549
+ // src/config/SQLITE_QUERY_MIN_ROWS.ts
550
+ var SQLITE_QUERY_MIN_ROWS = 1e4;
551
+
552
+ // src/config/sqliteQueriesSectionFrom.ts
553
+ var sqliteQueriesSectionFrom = (raw) => {
554
+ const paths = raw["paths"];
555
+ const database = raw["database"];
556
+ const minRows = raw["minRows"] ?? SQLITE_QUERY_MIN_ROWS;
557
+ expectConfig(
558
+ isStringList(paths),
559
+ "sqlite.queries.paths must be a list of strings"
560
+ );
561
+ expectConfig(
562
+ paths.length > 0,
563
+ "sqlite.queries.paths must name at least one directory"
564
+ );
565
+ expectConfig(
566
+ database === void 0 || typeof database === "string",
567
+ "sqlite.queries.database must be a string"
568
+ );
569
+ expectConfig(
570
+ isPositiveInteger(minRows),
571
+ "sqlite.queries.minRows must be a positive integer"
572
+ );
573
+ return {
574
+ paths,
575
+ ...database === void 0 ? {} : { database },
576
+ minRows
577
+ };
578
+ };
579
+
580
+ // src/config/sqliteSectionFrom.ts
581
+ var sqliteSectionFrom = (raw) => {
582
+ const files = raw["files"];
583
+ if (!isStringList(files))
584
+ throw new ConfigError("sqlite.files must be a list of strings");
585
+ const queries = raw["queries"];
586
+ if (queries === void 0) return { files };
587
+ if (typeof queries !== "object" || queries === null || Array.isArray(queries))
588
+ throw new ConfigError("sqlite.queries must be an object");
589
+ return {
590
+ files,
591
+ queries: sqliteQueriesSectionFrom(queries)
592
+ };
593
+ };
594
+
549
595
  // src/config/stackSectionsFrom.ts
550
596
  var stackSectionsFrom = (raw) => {
551
597
  const stacks = {};
@@ -566,12 +612,7 @@ var stackSectionsFrom = (raw) => {
566
612
  const drizzle = configSection(raw, "drizzle");
567
613
  if (drizzle) stacks.drizzle = drizzleSectionFrom(drizzle);
568
614
  const sqlite = configSection(raw, "sqlite");
569
- if (sqlite) {
570
- const files = sqlite["files"];
571
- if (!isStringList(files))
572
- throw new ConfigError("sqlite.files must be a list of strings");
573
- stacks.sqlite = { files };
574
- }
615
+ if (sqlite) stacks.sqlite = sqliteSectionFrom(sqlite);
575
616
  const postgrest = configSection(raw, "postgrest");
576
617
  if (postgrest) stacks.postgrest = postgrestSectionFrom(postgrest);
577
618
  return stacks;
@@ -653,8 +694,8 @@ var auditCommand = (argv2, io) => {
653
694
  };
654
695
 
655
696
  // src/commands/baselineCommand.ts
656
- import { existsSync as existsSync6 } from "fs";
657
- import { join as join9 } from "path";
697
+ import { existsSync as existsSync7 } from "fs";
698
+ import { join as join12 } from "path";
658
699
 
659
700
  // src/baseline/BASELINE_FILENAME.ts
660
701
  var BASELINE_FILENAME = ".codeality-db-baseline.json";
@@ -855,7 +896,7 @@ var sqliteFinding = (input, disabled) => {
855
896
  code: input.code,
856
897
  severity: input.severity,
857
898
  path: input.file,
858
- line: 0,
899
+ line: input.line ?? 0,
859
900
  message: input.message,
860
901
  subject: input.subject
861
902
  };
@@ -962,9 +1003,434 @@ var runSqliteChecks = (runner, root, files, disabled) => files.flatMap((file) =>
962
1003
  ...primaryKeyFindings(runner, root, file, disabled)
963
1004
  ]);
964
1005
 
1006
+ // src/adapters/sqlite/probeQueryDatabase.ts
1007
+ import { existsSync as existsSync5 } from "fs";
1008
+
1009
+ // src/adapters/sqlite/attemptSqlite.ts
1010
+ var attemptSqlite = (call) => {
1011
+ try {
1012
+ return call();
1013
+ } catch (error) {
1014
+ if (error instanceof ToolMissingError) throw error;
1015
+ return void 0;
1016
+ }
1017
+ };
1018
+
1019
+ // src/adapters/sqlite/resolveQueryDatabase.ts
1020
+ import { homedir } from "os";
1021
+ import { join as join6, resolve } from "path";
1022
+ var resolveQueryDatabase = (root, database) => database.startsWith("~/") ? join6(homedir(), database.slice(2)) : resolve(root, database);
1023
+
1024
+ // src/adapters/sqlite/userTables.ts
1025
+ var userTables = (runner, root, database) => new Map(
1026
+ sqliteRows(
1027
+ runner,
1028
+ root,
1029
+ database,
1030
+ "select name from sqlite_master where type = 'table'"
1031
+ ).map((row) => [row.name.toLowerCase(), row.name])
1032
+ );
1033
+
1034
+ // src/adapters/sqlite/probeQueryDatabase.ts
1035
+ var probeQueryDatabase = (runner, root, database) => {
1036
+ const path = resolveQueryDatabase(root, database);
1037
+ if (!existsSync5(path)) return { path, unavailable: "does not exist" };
1038
+ const tables = attemptSqlite(() => userTables(runner, root, path));
1039
+ return tables ? { path, tables } : { path, unavailable: "is not a readable SQLite database" };
1040
+ };
1041
+
1042
+ // src/adapters/sqlite/fullScanMessage.ts
1043
+ var fullScanMessage = (scan, rows, sorts) => {
1044
+ const what = scan.kind === "table" ? "full table scan" : "full index scan";
1045
+ const sorted = sorts ? ", then USE TEMP B-TREE FOR ORDER BY" : "";
1046
+ const hint = scan.kind === "table" ? "give the filter an index it can use" : "the whole index is read; filter on its leading column or change the join order";
1047
+ return `${what} of ${scan.table} (${String(rows)} rows) in the plan the application prepares: ${scan.detail}${sorted}; ${hint}`;
1048
+ };
1049
+
1050
+ // src/adapters/sqlite/planDetails.ts
1051
+ var planDetails = (output) => {
1052
+ const text = output.trim();
1053
+ if (text.startsWith("["))
1054
+ return JSON.parse(text).map((row) => row.detail);
1055
+ return text.split("\n").map((line) => line.replace(/^[\s|`-]*/, "").trim()).filter((line) => line !== "" && line !== "QUERY PLAN");
1056
+ };
1057
+
1058
+ // src/adapters/sqlite/queryPlan.ts
1059
+ var queryPlan = (runner, root, database, statement) => planDetails(
1060
+ sqliteQuery(runner, root, database, `EXPLAIN QUERY PLAN ${statement}`)
1061
+ );
1062
+
1063
+ // src/adapters/sqlite/cteNames.ts
1064
+ var cteNames = (text) => new Set(
1065
+ [
1066
+ ...text.matchAll(
1067
+ /(?:\bwith\s+(?:recursive\s+)?|,\s*)"?(?<name>\w+)"?(?:\s*\([^()]*\))?\s+as\s*(?:not\s+)?(?:materialized\s*)?\(/gi
1068
+ )
1069
+ ].map((match) => (match.groups?.["name"] ?? "").toLowerCase())
1070
+ );
1071
+
1072
+ // src/adapters/sqlite/scanOfPlanRow.ts
1073
+ var scanOfPlanRow = (detail) => {
1074
+ const match = /^SCAN (?:TABLE )?(?<name>[^\s(]\S*)(?: AS \S+)?(?<rest>(?: .*)?)$/.exec(
1075
+ detail
1076
+ );
1077
+ const name = match?.groups?.["name"];
1078
+ const rest = match?.groups?.["rest"] ?? "";
1079
+ if (name === void 0 || detail === "SCAN CONSTANT ROW") return void 0;
1080
+ if (rest.includes("VIRTUAL TABLE")) return void 0;
1081
+ return {
1082
+ name,
1083
+ kind: /\bUSING (?:COVERING )?INDEX\b/.test(rest) ? "index" : "table",
1084
+ detail
1085
+ };
1086
+ };
1087
+
1088
+ // src/adapters/sqlite/ALIAS_KEYWORDS.ts
1089
+ var ALIAS_KEYWORDS = /* @__PURE__ */ new Set([
1090
+ "and",
1091
+ "as",
1092
+ "cross",
1093
+ "default",
1094
+ "except",
1095
+ "from",
1096
+ "full",
1097
+ "group",
1098
+ "having",
1099
+ "indexed",
1100
+ "inner",
1101
+ "intersect",
1102
+ "join",
1103
+ "left",
1104
+ "limit",
1105
+ "natural",
1106
+ "not",
1107
+ "offset",
1108
+ "on",
1109
+ "or",
1110
+ "order",
1111
+ "outer",
1112
+ "returning",
1113
+ "right",
1114
+ "select",
1115
+ "set",
1116
+ "union",
1117
+ "using",
1118
+ "values",
1119
+ "where",
1120
+ "window"
1121
+ ]);
1122
+
1123
+ // src/adapters/sqlite/statementTableAliases.ts
1124
+ var statementTableAliases = (text, tables) => {
1125
+ const aliases = /* @__PURE__ */ new Map();
1126
+ const pattern = /(?<=\b(?:from|join|update|into)\s+|,\s*)"?(?<table>\w+)"?(?:\s+as)?\s+"?(?<alias>\w+)"?/gi;
1127
+ for (const match of text.matchAll(pattern)) {
1128
+ const table = tables.get(match.groups?.["table"]?.toLowerCase() ?? "");
1129
+ const alias = match.groups?.["alias"]?.toLowerCase() ?? "";
1130
+ if (table !== void 0 && !ALIAS_KEYWORDS.has(alias))
1131
+ aliases.set(alias, table);
1132
+ }
1133
+ return aliases;
1134
+ };
1135
+
1136
+ // src/adapters/sqlite/scannedTables.ts
1137
+ var scannedTables = (statement, details, tables) => {
1138
+ const ctes = cteNames(statement);
1139
+ const aliases = statementTableAliases(statement, tables);
1140
+ return details.flatMap((detail) => {
1141
+ const scan = scanOfPlanRow(detail);
1142
+ if (!scan) return [];
1143
+ const name = scan.name.toLowerCase();
1144
+ if (ctes.has(name)) return [];
1145
+ const table = aliases.get(name) ?? tables.get(name);
1146
+ return table === void 0 ? [] : [{ table, kind: scan.kind, detail: scan.detail }];
1147
+ });
1148
+ };
1149
+
1150
+ // src/adapters/sqlite/tableRowCount.ts
1151
+ var tableRowCount = (runner, root, database, table) => sqliteRows(
1152
+ runner,
1153
+ root,
1154
+ database,
1155
+ `select count(*) as n from "${table.replaceAll('"', '""')}"`
1156
+ )[0]?.n ?? 0;
1157
+
1158
+ // src/adapters/sqlite/queryPlanFindings.ts
1159
+ var queryPlanFindings = (context, files) => {
1160
+ if (isDisabled("BDB406", context.disabled)) return [];
1161
+ const { runner, root, database } = context;
1162
+ const counts = /* @__PURE__ */ new Map();
1163
+ const rowsOf = (table) => {
1164
+ const known = counts.get(table);
1165
+ if (known !== void 0) return known;
1166
+ const rows = attemptSqlite(() => tableRowCount(runner, root, database, table)) ?? 0;
1167
+ counts.set(table, rows);
1168
+ return rows;
1169
+ };
1170
+ return files.flatMap((file) => {
1171
+ const reported = /* @__PURE__ */ new Set();
1172
+ return file.statements.flatMap((statement) => {
1173
+ const details = attemptSqlite(
1174
+ () => queryPlan(runner, root, database, statement.text)
1175
+ );
1176
+ if (!details) return [];
1177
+ const sorts = details.includes("USE TEMP B-TREE FOR ORDER BY");
1178
+ return scannedTables(statement.text, details, context.tables).flatMap(
1179
+ (scan) => {
1180
+ const key = `${scan.table}:${scan.kind}`;
1181
+ const rows = rowsOf(scan.table);
1182
+ if (reported.has(key) || rows < context.minRows) return [];
1183
+ reported.add(key);
1184
+ return sqliteFinding(
1185
+ {
1186
+ code: "BDB406",
1187
+ severity: "warn",
1188
+ file: file.path,
1189
+ line: statement.line,
1190
+ subject: scan.table,
1191
+ message: fullScanMessage(scan, rows, sorts),
1192
+ context: key
1193
+ },
1194
+ context.disabled
1195
+ );
1196
+ }
1197
+ );
1198
+ });
1199
+ });
1200
+ };
1201
+
1202
+ // src/adapters/sqlite/readQueryFiles.ts
1203
+ import { readFileSync as readFileSync4 } from "fs";
1204
+ import { join as join8 } from "path";
1205
+
1206
+ // src/adapters/sqlite/isQueryStatement.ts
1207
+ var isQueryStatement = (statement) => /^(?:select|with|update|delete|insert|replace)\b/i.test(statement.text);
1208
+
1209
+ // src/adapters/sqlite/queryFilesUnder.ts
1210
+ import { readdirSync } from "fs";
1211
+ import { join as join7, relative as relative2 } from "path";
1212
+
1213
+ // src/config/isDirectory.ts
1214
+ import { existsSync as existsSync6, statSync } from "fs";
1215
+ var isDirectory = (path) => existsSync6(path) && statSync(path).isDirectory();
1216
+
1217
+ // src/postgrest/SKIPPED_SOURCE_DIRS.ts
1218
+ var SKIPPED_SOURCE_DIRS = /* @__PURE__ */ new Set([
1219
+ "node_modules",
1220
+ "dist",
1221
+ "build",
1222
+ ".next",
1223
+ "coverage",
1224
+ "__tests__",
1225
+ "__mocks__"
1226
+ ]);
1227
+
1228
+ // src/adapters/sqlite/queryFilesUnder.ts
1229
+ var queryFilesUnder = (root, paths) => {
1230
+ for (const base of paths) {
1231
+ if (!isDirectory(join7(root, base)))
1232
+ throw new ConfigError(
1233
+ `sqlite.queries.paths: "${base}" is not a directory`
1234
+ );
1235
+ }
1236
+ const found = /* @__PURE__ */ new Set();
1237
+ const walk = (dir) => {
1238
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
1239
+ if (entry.name.startsWith(".")) continue;
1240
+ const path = join7(dir, entry.name);
1241
+ if (entry.isDirectory()) {
1242
+ if (!SKIPPED_SOURCE_DIRS.has(entry.name)) walk(path);
1243
+ } else if (entry.isFile() && entry.name.endsWith(".sql")) {
1244
+ found.add(relative2(root, path).replaceAll("\\", "/"));
1245
+ }
1246
+ }
1247
+ };
1248
+ for (const base of paths) walk(join7(root, base));
1249
+ return [...found].sort();
1250
+ };
1251
+
1252
+ // src/sql/quotedSpanEnd.ts
1253
+ var quotedSpanEnd = (sql, index) => {
1254
+ const dollar = /^\$[a-z_]*\$/i.exec(sql.slice(index, index + 64));
1255
+ if (dollar) {
1256
+ const end2 = sql.indexOf(dollar[0], index + dollar[0].length);
1257
+ return end2 === -1 ? sql.length : end2 + dollar[0].length;
1258
+ }
1259
+ const quote = sql[index];
1260
+ if (quote !== "'" && quote !== '"') return void 0;
1261
+ const end = sql.indexOf(quote, index + 1);
1262
+ return end === -1 ? sql.length : end + 1;
1263
+ };
1264
+
1265
+ // src/sql/stripSqlComments.ts
1266
+ var stripSqlComments = (sql) => {
1267
+ let out = "";
1268
+ let index = 0;
1269
+ while (index < sql.length) {
1270
+ const quoted = quotedSpanEnd(sql, index);
1271
+ if (quoted !== void 0) {
1272
+ out += sql.slice(index, quoted);
1273
+ index = quoted;
1274
+ } else if (sql.startsWith("--", index)) {
1275
+ const end = sql.indexOf("\n", index);
1276
+ index = end === -1 ? sql.length : end;
1277
+ } else if (sql.startsWith("/*", index)) {
1278
+ const end = sql.indexOf("*/", index + 2);
1279
+ const stop = end === -1 ? sql.length : end + 2;
1280
+ out += sql.slice(index, stop).replaceAll(/[^\n]/g, "");
1281
+ index = stop;
1282
+ } else {
1283
+ out += sql.charAt(index);
1284
+ index += 1;
1285
+ }
1286
+ }
1287
+ return out;
1288
+ };
1289
+
1290
+ // src/sql/splitSqlStatements.ts
1291
+ var splitSqlStatements = (sql) => {
1292
+ const text = stripSqlComments(sql);
1293
+ const statements = [];
1294
+ let start = 0;
1295
+ const flush = (end) => {
1296
+ const raw = text.slice(start, end);
1297
+ const leading = raw.length - raw.trimStart().length;
1298
+ const body = raw.trim();
1299
+ if (body) {
1300
+ const line = 1 + (text.slice(0, start + leading).match(/\n/g)?.length ?? 0);
1301
+ statements.push({ text: body, line });
1302
+ }
1303
+ start = end + 1;
1304
+ };
1305
+ let index = 0;
1306
+ while (index < text.length) {
1307
+ const quoted = quotedSpanEnd(text, index);
1308
+ if (quoted !== void 0) index = quoted;
1309
+ else {
1310
+ if (text[index] === ";") flush(index);
1311
+ index += 1;
1312
+ }
1313
+ }
1314
+ flush(text.length);
1315
+ return statements;
1316
+ };
1317
+
1318
+ // src/adapters/sqlite/readQueryFiles.ts
1319
+ var readQueryFiles = (root, paths) => queryFilesUnder(root, paths).map((path) => ({
1320
+ path,
1321
+ statements: splitSqlStatements(
1322
+ readFileSync4(join8(root, path), "utf8")
1323
+ ).filter(isQueryStatement)
1324
+ }));
1325
+
1326
+ // src/adapters/sqlite/statementAtMatch.ts
1327
+ var statementAtMatch = (statement, index) => ({
1328
+ text: statement.text,
1329
+ line: statement.line + (statement.text.slice(0, index).match(/\n/g)?.length ?? 0)
1330
+ });
1331
+
965
1332
  // src/model/normalizeSqlText.ts
966
1333
  var normalizeSqlText = (text) => text.toLowerCase().replaceAll(/\s+/g, " ").trim();
967
1334
 
1335
+ // src/rules/makeSqlFinding.ts
1336
+ var makeSqlFinding = (rule, { file, statement }, subject, message) => {
1337
+ const partial = {
1338
+ code: rule.code,
1339
+ severity: rule.severity,
1340
+ path: file.path,
1341
+ line: statement.line,
1342
+ message,
1343
+ subject
1344
+ };
1345
+ return {
1346
+ ...partial,
1347
+ fingerprint: fingerprintFinding(partial, normalizeSqlText(statement.text))
1348
+ };
1349
+ };
1350
+
1351
+ // src/adapters/sqlite/commaListMembership.ts
1352
+ var commaListMembership = {
1353
+ code: "BDB405",
1354
+ name: "comma-list-membership",
1355
+ severity: "warn",
1356
+ // instr(',' || ?1 || ',', ',' || col || ','): membership in a comma-joined
1357
+ // list, which no index can answer.
1358
+ run: (set) => set.flatMap(
1359
+ (file) => file.statements.flatMap((statement) => {
1360
+ const match = /\binstr\s*\(\s*','\s*\|\|\s*(?<param>\?\d*|[:@$][a-z_]\w*)\s*\|\|\s*','/i.exec(
1361
+ statement.text
1362
+ );
1363
+ if (!match) return [];
1364
+ const param = match.groups?.["param"] ?? "";
1365
+ return [
1366
+ makeSqlFinding(
1367
+ commaListMembership,
1368
+ { file, statement: statementAtMatch(statement, match.index) },
1369
+ param,
1370
+ `instr() over a comma-joined ${param} cannot use an index; bind a JSON array and filter with col IN (SELECT value FROM json_each(${param}))`
1371
+ )
1372
+ ];
1373
+ })
1374
+ )
1375
+ };
1376
+
1377
+ // src/adapters/sqlite/optionalParameterGuard.ts
1378
+ var optionalParameterGuard = {
1379
+ code: "BDB404",
1380
+ name: "optional-parameter-guard",
1381
+ severity: "warn",
1382
+ // `?1 IS NULL OR col = ?1` and `col = ?1 OR ?1 IS NULL`, for every parameter
1383
+ // spelling SQLite binds: ?, ?NNN, :name, @name and $name.
1384
+ run: (set) => set.flatMap(
1385
+ (file) => file.statements.flatMap((statement) => {
1386
+ const match = /(?<param>\?\d*|[:@$][a-z_]\w*)\s+is\s+null\s+or\b|\bor\s+(?<trailing>\?\d*|[:@$][a-z_]\w*)\s+is\s+null\b/i.exec(
1387
+ statement.text
1388
+ );
1389
+ if (!match) return [];
1390
+ const param = match.groups?.["param"] ?? match.groups?.["trailing"] ?? "";
1391
+ return [
1392
+ makeSqlFinding(
1393
+ optionalParameterGuard,
1394
+ { file, statement: statementAtMatch(statement, match.index) },
1395
+ param,
1396
+ `"${param} IS NULL OR" hides the index from the planner: SQLite picks the plan at prepare time, before the value is bound; split the query per case, or bind a non-null sentinel or a json_each list`
1397
+ )
1398
+ ];
1399
+ })
1400
+ )
1401
+ };
1402
+
1403
+ // src/adapters/sqlite/SQLITE_QUERY_RULES.ts
1404
+ var SQLITE_QUERY_RULES = [
1405
+ optionalParameterGuard,
1406
+ commaListMembership
1407
+ ];
1408
+
1409
+ // src/adapters/sqlite/runSqliteQueryChecks.ts
1410
+ var runSqliteQueryChecks = (runner, root, queries, disabled) => {
1411
+ const files = readQueryFiles(root, queries.paths);
1412
+ const findings = SQLITE_QUERY_RULES.filter(
1413
+ (rule) => !isDisabled(rule.code, disabled)
1414
+ ).flatMap((rule) => rule.run(files));
1415
+ if (queries.database === void 0) return findings;
1416
+ const probe = probeQueryDatabase(runner, root, queries.database);
1417
+ if ("unavailable" in probe) return findings;
1418
+ return [
1419
+ ...findings,
1420
+ ...queryPlanFindings(
1421
+ {
1422
+ runner,
1423
+ root,
1424
+ database: probe.path,
1425
+ tables: probe.tables,
1426
+ minRows: queries.minRows,
1427
+ disabled
1428
+ },
1429
+ files
1430
+ )
1431
+ ];
1432
+ };
1433
+
968
1434
  // src/sql/statementAtLine.ts
969
1435
  var statementAtLine = (file, line) => [...file.statements].reverse().find((statement) => statement.line <= line);
970
1436
 
@@ -1027,8 +1493,8 @@ var runSquawk = (runner, root, set, disabled) => {
1027
1493
  };
1028
1494
 
1029
1495
  // src/postgrest/runPostgrestRules.ts
1030
- import { readFileSync as readFileSync4 } from "fs";
1031
- import { join as join7 } from "path";
1496
+ import { readFileSync as readFileSync5 } from "fs";
1497
+ import { join as join10 } from "path";
1032
1498
 
1033
1499
  // src/postgrest/ITERATION_METHODS.ts
1034
1500
  var ITERATION_METHODS = /* @__PURE__ */ new Set([
@@ -1307,45 +1773,28 @@ var POSTGREST_RULES = [
1307
1773
  ];
1308
1774
 
1309
1775
  // src/postgrest/sourceFilesUnder.ts
1310
- import { readdirSync } from "fs";
1311
- import { join as join6, relative as relative2 } from "path";
1312
-
1313
- // src/config/isDirectory.ts
1314
- import { existsSync as existsSync5, statSync } from "fs";
1315
- var isDirectory = (path) => existsSync5(path) && statSync(path).isDirectory();
1316
-
1317
- // src/postgrest/SKIPPED_SOURCE_DIRS.ts
1318
- var SKIPPED_SOURCE_DIRS = /* @__PURE__ */ new Set([
1319
- "node_modules",
1320
- "dist",
1321
- "build",
1322
- ".next",
1323
- "coverage",
1324
- "__tests__",
1325
- "__mocks__"
1326
- ]);
1327
-
1328
- // src/postgrest/sourceFilesUnder.ts
1776
+ import { readdirSync as readdirSync2 } from "fs";
1777
+ import { join as join9, relative as relative3 } from "path";
1329
1778
  var sourceFilesUnder = (root, roots) => {
1330
1779
  for (const base of roots) {
1331
- if (!isDirectory(join6(root, base)))
1780
+ if (!isDirectory(join9(root, base)))
1332
1781
  throw new ConfigError(`postgrest.roots: "${base}" is not a directory`);
1333
1782
  }
1334
1783
  const files = [];
1335
1784
  const walk = (dir) => {
1336
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
1785
+ for (const entry of readdirSync2(dir, { withFileTypes: true })) {
1337
1786
  if (entry.isDirectory()) {
1338
1787
  if (!SKIPPED_SOURCE_DIRS.has(entry.name) && !entry.name.startsWith("."))
1339
- walk(join6(dir, entry.name));
1788
+ walk(join9(dir, entry.name));
1340
1789
  continue;
1341
1790
  }
1342
1791
  const name = entry.name;
1343
1792
  if (!/\.tsx?$/.test(name) || name.endsWith(".d.ts") || /\.(?:test|spec)\.tsx?$/.test(name))
1344
1793
  continue;
1345
- files.push(relative2(root, join6(dir, name)));
1794
+ files.push(relative3(root, join9(dir, name)));
1346
1795
  }
1347
1796
  };
1348
- for (const base of roots) walk(join6(root, base));
1797
+ for (const base of roots) walk(join9(root, base));
1349
1798
  return files.sort();
1350
1799
  };
1351
1800
 
@@ -1355,12 +1804,12 @@ var runPostgrestRules = (root, roots, knowledge, disabled) => {
1355
1804
  (rule) => !isDisabled(rule.code, disabled)
1356
1805
  );
1357
1806
  const context = { knowledge };
1358
- const compiler = loadTypeScript([join7(root, "package.json"), import.meta.url]);
1807
+ const compiler = loadTypeScript([join10(root, "package.json"), import.meta.url]);
1359
1808
  return sourceFilesUnder(root, roots).flatMap(
1360
1809
  (path) => collectPostgrestChains(
1361
1810
  compiler,
1362
1811
  path,
1363
- readFileSync4(join7(root, path), "utf8")
1812
+ readFileSync5(join10(root, path), "utf8")
1364
1813
  )
1365
1814
  ).flatMap((chain) => rules.map((rule) => rule.run(chain, context))).filter((finding) => finding !== void 0).sort(compareFindings);
1366
1815
  };
@@ -1491,22 +1940,6 @@ var indexedColumns = (set) => {
1491
1940
  return knowledge;
1492
1941
  };
1493
1942
 
1494
- // src/rules/makeSqlFinding.ts
1495
- var makeSqlFinding = (rule, { file, statement }, subject, message) => {
1496
- const partial = {
1497
- code: rule.code,
1498
- severity: rule.severity,
1499
- path: file.path,
1500
- line: statement.line,
1501
- message,
1502
- subject
1503
- };
1504
- return {
1505
- ...partial,
1506
- fingerprint: fingerprintFinding(partial, normalizeSqlText(statement.text))
1507
- };
1508
- };
1509
-
1510
1943
  // src/rules/policyTable.ts
1511
1944
  var policyTable = (statement) => {
1512
1945
  const match = /^create policy .+? on ((?:"[^"]+"|[\w$]+)(?:\.(?:"[^"]+"|[\w$]+))?)/.exec(
@@ -1647,80 +2080,12 @@ var SQL_RULES = [
1647
2080
  var runSqlRules = (set, disabled) => SQL_RULES.filter((rule) => !isDisabled(rule.code, disabled)).flatMap((rule) => rule.run(set)).sort(compareFindings);
1648
2081
 
1649
2082
  // src/sql/readMigrationSet.ts
1650
- import { readdirSync as readdirSync2, readFileSync as readFileSync5 } from "fs";
1651
- import { join as join8 } from "path";
1652
-
1653
- // src/sql/quotedSpanEnd.ts
1654
- var quotedSpanEnd = (sql, index) => {
1655
- const dollar = /^\$[a-z_]*\$/i.exec(sql.slice(index, index + 64));
1656
- if (dollar) {
1657
- const end2 = sql.indexOf(dollar[0], index + dollar[0].length);
1658
- return end2 === -1 ? sql.length : end2 + dollar[0].length;
1659
- }
1660
- const quote = sql[index];
1661
- if (quote !== "'" && quote !== '"') return void 0;
1662
- const end = sql.indexOf(quote, index + 1);
1663
- return end === -1 ? sql.length : end + 1;
1664
- };
1665
-
1666
- // src/sql/stripSqlComments.ts
1667
- var stripSqlComments = (sql) => {
1668
- let out = "";
1669
- let index = 0;
1670
- while (index < sql.length) {
1671
- const quoted = quotedSpanEnd(sql, index);
1672
- if (quoted !== void 0) {
1673
- out += sql.slice(index, quoted);
1674
- index = quoted;
1675
- } else if (sql.startsWith("--", index)) {
1676
- const end = sql.indexOf("\n", index);
1677
- index = end === -1 ? sql.length : end;
1678
- } else if (sql.startsWith("/*", index)) {
1679
- const end = sql.indexOf("*/", index + 2);
1680
- const stop = end === -1 ? sql.length : end + 2;
1681
- out += sql.slice(index, stop).replaceAll(/[^\n]/g, "");
1682
- index = stop;
1683
- } else {
1684
- out += sql.charAt(index);
1685
- index += 1;
1686
- }
1687
- }
1688
- return out;
1689
- };
1690
-
1691
- // src/sql/splitSqlStatements.ts
1692
- var splitSqlStatements = (sql) => {
1693
- const text = stripSqlComments(sql);
1694
- const statements = [];
1695
- let start = 0;
1696
- const flush = (end) => {
1697
- const raw = text.slice(start, end);
1698
- const leading = raw.length - raw.trimStart().length;
1699
- const body = raw.trim();
1700
- if (body) {
1701
- const line = 1 + (text.slice(0, start + leading).match(/\n/g)?.length ?? 0);
1702
- statements.push({ text: body, line });
1703
- }
1704
- start = end + 1;
1705
- };
1706
- let index = 0;
1707
- while (index < text.length) {
1708
- const quoted = quotedSpanEnd(text, index);
1709
- if (quoted !== void 0) index = quoted;
1710
- else {
1711
- if (text[index] === ";") flush(index);
1712
- index += 1;
1713
- }
1714
- }
1715
- flush(text.length);
1716
- return statements;
1717
- };
1718
-
1719
- // src/sql/readMigrationSet.ts
1720
- var readMigrationSet = (root, migrationsDir) => readdirSync2(join8(root, migrationsDir)).filter((name) => name.endsWith(".sql")).sort().map((name) => ({
2083
+ import { readdirSync as readdirSync3, readFileSync as readFileSync6 } from "fs";
2084
+ import { join as join11 } from "path";
2085
+ var readMigrationSet = (root, migrationsDir) => readdirSync3(join11(root, migrationsDir)).filter((name) => name.endsWith(".sql")).sort().map((name) => ({
1721
2086
  path: `${migrationsDir.replaceAll("\\", "/")}/${name}`,
1722
2087
  statements: splitSqlStatements(
1723
- readFileSync5(join8(root, migrationsDir, name), "utf8")
2088
+ readFileSync6(join11(root, migrationsDir, name), "utf8")
1724
2089
  )
1725
2090
  }));
1726
2091
 
@@ -1747,6 +2112,15 @@ var runCheck = ({ root, config, runner }) => {
1747
2112
  findings.push(
1748
2113
  ...runSqliteChecks(runner, root, config.sqlite.files, config.disable)
1749
2114
  );
2115
+ if (config.sqlite.queries)
2116
+ findings.push(
2117
+ ...runSqliteQueryChecks(
2118
+ runner,
2119
+ root,
2120
+ config.sqlite.queries,
2121
+ config.disable
2122
+ )
2123
+ );
1750
2124
  }
1751
2125
  if (config.postgrest) {
1752
2126
  findings.push(
@@ -1761,6 +2135,20 @@ var runCheck = ({ root, config, runner }) => {
1761
2135
  return findings.sort(compareFindings);
1762
2136
  };
1763
2137
 
2138
+ // src/check/sqliteQueriesNotice.ts
2139
+ var sqliteQueriesNotice = (runner, root, config) => {
2140
+ const database = config.sqlite?.queries?.database;
2141
+ if (database === void 0 || isDisabled("BDB406", config.disable))
2142
+ return void 0;
2143
+ try {
2144
+ const probe = probeQueryDatabase(runner, root, database);
2145
+ return "unavailable" in probe ? `sqlite.queries.database ${probe.path} ${probe.unavailable}; the query-plan check BDB406 was skipped` : void 0;
2146
+ } catch (error) {
2147
+ if (error instanceof ToolMissingError) return void 0;
2148
+ throw error;
2149
+ }
2150
+ };
2151
+
1764
2152
  // src/packageVersion.ts
1765
2153
  import { createRequire as createRequire2 } from "module";
1766
2154
  var PACKAGE_VERSION = createRequire2(import.meta.url)("../package.json").version;
@@ -1772,7 +2160,7 @@ var baselineCommand = (argv2, io) => {
1772
2160
  "check-stale": { type: "boolean" }
1773
2161
  });
1774
2162
  const action = baselineActionFrom(positionals);
1775
- if (action === "create" && existsSync6(join9(io.root, BASELINE_FILENAME))) {
2163
+ if (action === "create" && existsSync7(join12(io.root, BASELINE_FILENAME))) {
1776
2164
  throw new ConfigError(
1777
2165
  `${BASELINE_FILENAME} exists; use "baseline update" to rewrite it`
1778
2166
  );
@@ -1780,6 +2168,9 @@ var baselineCommand = (argv2, io) => {
1780
2168
  const config = readConfig(io.root);
1781
2169
  const notice = legacyConfigNotice(config);
1782
2170
  if (notice) io.stderr(`${notice}
2171
+ `);
2172
+ const skipped = sqliteQueriesNotice(io.runner, io.root, config);
2173
+ if (skipped) io.stderr(`${skipped}
1783
2174
  `);
1784
2175
  const findings = runCheck({
1785
2176
  root: io.root,
@@ -1816,6 +2207,9 @@ var checkCommand = (argv2, io) => {
1816
2207
  const config = readConfig(io.root);
1817
2208
  const notice = legacyConfigNotice(config);
1818
2209
  if (notice) io.stderr(`${notice}
2210
+ `);
2211
+ const skipped = sqliteQueriesNotice(io.runner, io.root, config);
2212
+ if (skipped) io.stderr(`${skipped}
1819
2213
  `);
1820
2214
  const findings = runCheck({
1821
2215
  root: io.root,
@@ -1842,24 +2236,24 @@ var gateExitCode = (results) => {
1842
2236
  };
1843
2237
 
1844
2238
  // src/gate/gateStages.ts
1845
- import { existsSync as existsSync11 } from "fs";
1846
- import { join as join15 } from "path";
2239
+ import { existsSync as existsSync12 } from "fs";
2240
+ import { join as join18 } from "path";
1847
2241
 
1848
2242
  // src/gate/perfStage.ts
1849
- import { existsSync as existsSync10 } from "fs";
1850
- import { join as join14 } from "path";
2243
+ import { existsSync as existsSync11 } from "fs";
2244
+ import { join as join17 } from "path";
1851
2245
 
1852
2246
  // src/bench/BENCH_RECORD_FILENAME.ts
1853
2247
  var BENCH_RECORD_FILENAME = ".codeality-db-bench.json";
1854
2248
 
1855
2249
  // src/bench/readBenchRecord.ts
1856
- import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
1857
- import { join as join10 } from "path";
2250
+ import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
2251
+ import { join as join13 } from "path";
1858
2252
  var readBenchRecord = (root) => {
1859
- const path = join10(root, BENCH_RECORD_FILENAME);
1860
- if (!existsSync7(path)) return void 0;
2253
+ const path = join13(root, BENCH_RECORD_FILENAME);
2254
+ if (!existsSync8(path)) return void 0;
1861
2255
  const document = JSON.parse(
1862
- readFileSync6(path, "utf8")
2256
+ readFileSync7(path, "utf8")
1863
2257
  );
1864
2258
  if (document.schemaVersion !== 1)
1865
2259
  throw new ConfigError(
@@ -1926,18 +2320,18 @@ var benchFindings = (input, recorded, current, judgement) => {
1926
2320
  };
1927
2321
 
1928
2322
  // src/bench/readBenchQueries.ts
1929
- import { readdirSync as readdirSync3, readFileSync as readFileSync7 } from "fs";
1930
- import { join as join11 } from "path";
2323
+ import { readdirSync as readdirSync4, readFileSync as readFileSync8 } from "fs";
2324
+ import { join as join14 } from "path";
1931
2325
 
1932
2326
  // src/bench/RUNS_HEADER.ts
1933
2327
  var RUNS_HEADER = /^--\s*runs:\s*(\S+)/;
1934
2328
 
1935
2329
  // src/bench/readBenchQueries.ts
1936
2330
  var readBenchQueries = (root, benchDir, defaultRuns) => {
1937
- const dir = join11(root, benchDir);
2331
+ const dir = join14(root, benchDir);
1938
2332
  if (!isDirectory(dir)) throw new ConfigError(`${benchDir} is not a directory`);
1939
- return readdirSync3(dir).filter((name) => name.endsWith(".sql")).sort().map((name) => {
1940
- const raw = readFileSync7(join11(dir, name), "utf8");
2333
+ return readdirSync4(dir).filter((name) => name.endsWith(".sql")).sort().map((name) => {
2334
+ const raw = readFileSync8(join14(dir, name), "utf8");
1941
2335
  const match = RUNS_HEADER.exec(raw);
1942
2336
  const runs = match?.[1] ? Number(match[1]) : defaultRuns;
1943
2337
  if (!isPositiveInteger(runs))
@@ -2336,22 +2730,22 @@ var diffSnapshots = (previous, current, perf, disabled) => {
2336
2730
  var QUERY_TIMEOUT_MS = 3e4;
2337
2731
 
2338
2732
  // src/perf/readPerfSnapshot.ts
2339
- import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
2340
- import { join as join12 } from "path";
2733
+ import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
2734
+ import { join as join15 } from "path";
2341
2735
 
2342
2736
  // src/perf/PERF_SNAPSHOT_FILENAME.ts
2343
2737
  var PERF_SNAPSHOT_FILENAME = ".codeality-db-perf.json";
2344
2738
 
2345
2739
  // src/perf/readPerfSnapshot.ts
2346
2740
  var readPerfSnapshot = (root) => {
2347
- const path = join12(root, PERF_SNAPSHOT_FILENAME);
2348
- if (!existsSync8(path)) {
2741
+ const path = join15(root, PERF_SNAPSHOT_FILENAME);
2742
+ if (!existsSync9(path)) {
2349
2743
  throw new ConfigError(
2350
2744
  `${PERF_SNAPSHOT_FILENAME} not found; run "codeality-db perf snapshot"`
2351
2745
  );
2352
2746
  }
2353
2747
  const document = JSON.parse(
2354
- readFileSync8(path, "utf8")
2748
+ readFileSync9(path, "utf8")
2355
2749
  );
2356
2750
  if (document.schemaVersion !== 1) {
2357
2751
  throw new ConfigError(
@@ -2472,8 +2866,8 @@ var perfDiffFindings = ({ root, config, runner }, target) => {
2472
2866
  };
2473
2867
 
2474
2868
  // src/postgres/resolvePostgresTarget.ts
2475
- import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
2476
- import { join as join13 } from "path";
2869
+ import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
2870
+ import { join as join16 } from "path";
2477
2871
  import { env } from "process";
2478
2872
 
2479
2873
  // src/postgres/DB_PASSWORD_ENV.ts
@@ -2530,12 +2924,12 @@ var targetFromUrl = (raw, source, fallbackPassword) => {
2530
2924
  var resolvePostgresTarget = (root, flags) => {
2531
2925
  const explicit = flags["db-url"];
2532
2926
  if (explicit !== void 0) return targetFromUrl(explicit, "--db-url");
2533
- const poolerPath = join13(root, POOLER_URL_FILE);
2927
+ const poolerPath = join16(root, POOLER_URL_FILE);
2534
2928
  const password = env[DB_PASSWORD_ENV];
2535
- if (!existsSync9(poolerPath) || password === void 0 || password === "")
2929
+ if (!existsSync10(poolerPath) || password === void 0 || password === "")
2536
2930
  return void 0;
2537
2931
  return targetFromUrl(
2538
- readFileSync9(poolerPath, "utf8").trim(),
2932
+ readFileSync10(poolerPath, "utf8").trim(),
2539
2933
  POOLER_URL_FILE,
2540
2934
  password
2541
2935
  );
@@ -2551,8 +2945,8 @@ var perfStage = (context) => ({
2551
2945
  return {
2552
2946
  skipped: "no linked project with SUPABASE_DB_PASSWORD"
2553
2947
  };
2554
- const hasSnapshot = existsSync10(join14(context.root, PERF_SNAPSHOT_FILENAME));
2555
- const hasRecord = existsSync10(join14(context.root, BENCH_RECORD_FILENAME));
2948
+ const hasSnapshot = existsSync11(join17(context.root, PERF_SNAPSHOT_FILENAME));
2949
+ const hasRecord = existsSync11(join17(context.root, BENCH_RECORD_FILENAME));
2556
2950
  if (!hasSnapshot && !hasRecord)
2557
2951
  return {
2558
2952
  skipped: 'no perf snapshot and no bench record; run "perf snapshot" or "perf bench --record"'
@@ -2566,8 +2960,8 @@ var perfStage = (context) => ({
2566
2960
 
2567
2961
  // src/gate/gateStages.ts
2568
2962
  var gateStages = (context) => {
2569
- const hasBaseline = existsSync11(join15(context.root, BASELINE_FILENAME));
2570
- const linked = existsSync11(join15(context.root, "supabase/.temp/project-ref"));
2963
+ const hasBaseline = existsSync12(join18(context.root, BASELINE_FILENAME));
2964
+ const linked = existsSync12(join18(context.root, "supabase/.temp/project-ref"));
2571
2965
  return [
2572
2966
  hasBaseline ? {
2573
2967
  name: "baseline-check",
@@ -2621,6 +3015,9 @@ var gateCommand = (argv2, io) => {
2621
3015
  const config = readConfig(io.root);
2622
3016
  const notice = legacyConfigNotice(config);
2623
3017
  if (notice) io.stderr(`${notice}
3018
+ `);
3019
+ const skipped = sqliteQueriesNotice(io.runner, io.root, config);
3020
+ if (skipped) io.stderr(`${skipped}
2624
3021
  `);
2625
3022
  const results = runGate(
2626
3023
  gateStages({
@@ -2639,12 +3036,12 @@ var gateCommand = (argv2, io) => {
2639
3036
  };
2640
3037
 
2641
3038
  // src/init/adoptionPhase.ts
2642
- import { existsSync as existsSync12 } from "fs";
2643
- import { join as join16 } from "path";
3039
+ import { existsSync as existsSync13 } from "fs";
3040
+ import { join as join19 } from "path";
2644
3041
  var adoptionPhase = (root, config) => {
2645
3042
  if (config.schemaVersion !== 2) return 0;
2646
- const hasSnapshot = existsSync12(join16(root, PERF_SNAPSHOT_FILENAME));
2647
- const hasBench = existsSync12(join16(root, BENCH_RECORD_FILENAME));
3043
+ const hasSnapshot = existsSync13(join19(root, PERF_SNAPSHOT_FILENAME));
3044
+ const hasBench = existsSync13(join19(root, BENCH_RECORD_FILENAME));
2648
3045
  if (config.perf.inGate && (hasSnapshot || hasBench)) return 4;
2649
3046
  if (hasBench) return 3;
2650
3047
  if (hasSnapshot) return 2;
@@ -2652,12 +3049,12 @@ var adoptionPhase = (root, config) => {
2652
3049
  };
2653
3050
 
2654
3051
  // src/init/baselineStep.ts
2655
- import { existsSync as existsSync13 } from "fs";
2656
- import { join as join17 } from "path";
3052
+ import { existsSync as existsSync14 } from "fs";
3053
+ import { join as join20 } from "path";
2657
3054
  var baselineStep = (context) => {
2658
3055
  const findings = runCheck(context);
2659
3056
  if (findings.length === 0) return "covered";
2660
- if (!existsSync13(join17(context.root, BASELINE_FILENAME))) return "create";
3057
+ if (!existsSync14(join20(context.root, BASELINE_FILENAME))) return "create";
2661
3058
  const classified = classifyFindings(findings, readBaseline(context.root));
2662
3059
  return classified.new.length > 0 ? "update" : "covered";
2663
3060
  };
@@ -2677,19 +3074,19 @@ var adoptionStanding = (context) => {
2677
3074
 
2678
3075
  // src/init/applyInit.ts
2679
3076
  import { mkdirSync, writeFileSync as writeFileSync3 } from "fs";
2680
- import { dirname, join as join18 } from "path";
3077
+ import { dirname, join as join21 } from "path";
2681
3078
  var applyInit = (root, plan) => {
2682
3079
  for (const file of plan) {
2683
3080
  const writes = file.disposition === "create" || file.disposition === "merge";
2684
3081
  if (!writes || file.content === void 0) continue;
2685
- mkdirSync(dirname(join18(root, file.path)), { recursive: true });
2686
- writeFileSync3(join18(root, file.path), file.content);
3082
+ mkdirSync(dirname(join21(root, file.path)), { recursive: true });
3083
+ writeFileSync3(join21(root, file.path), file.content);
2687
3084
  }
2688
3085
  };
2689
3086
 
2690
3087
  // src/init/planBenchReadme.ts
2691
- import { existsSync as existsSync14 } from "fs";
2692
- import { join as join19 } from "path";
3088
+ import { existsSync as existsSync15 } from "fs";
3089
+ import { join as join22 } from "path";
2693
3090
 
2694
3091
  // src/init/BENCH_README.ts
2695
3092
  var BENCH_README = `# Bench queries
@@ -2721,12 +3118,12 @@ var planBenchReadme = (root) => {
2721
3118
  return PERF_DEFAULTS.benchDir;
2722
3119
  }
2723
3120
  })();
2724
- const relative4 = `${benchDir}/README.md`;
2725
- if (existsSync14(join19(root, relative4))) {
2726
- return { path: relative4, disposition: "unchanged", detail: "present" };
3121
+ const relative5 = `${benchDir}/README.md`;
3122
+ if (existsSync15(join22(root, relative5))) {
3123
+ return { path: relative5, disposition: "unchanged", detail: "present" };
2727
3124
  }
2728
3125
  return {
2729
- path: relative4,
3126
+ path: relative5,
2730
3127
  disposition: "create",
2731
3128
  detail: "explains the bench convention",
2732
3129
  content: BENCH_README
@@ -2734,26 +3131,26 @@ var planBenchReadme = (root) => {
2734
3131
  };
2735
3132
 
2736
3133
  // src/init/planConfigFile.ts
2737
- import { existsSync as existsSync17, readFileSync as readFileSync11 } from "fs";
2738
- import { join as join23 } from "path";
3134
+ import { existsSync as existsSync18, readFileSync as readFileSync12 } from "fs";
3135
+ import { join as join26 } from "path";
2739
3136
 
2740
3137
  // src/config/detectStacks.ts
2741
- import { existsSync as existsSync16 } from "fs";
2742
- import { join as join22 } from "path";
3138
+ import { existsSync as existsSync17 } from "fs";
3139
+ import { join as join25 } from "path";
2743
3140
 
2744
3141
  // src/config/findSqliteFiles.ts
2745
- import { readdirSync as readdirSync4 } from "fs";
2746
- import { join as join20, relative as relative3 } from "path";
3142
+ import { readdirSync as readdirSync5 } from "fs";
3143
+ import { join as join23, relative as relative4 } from "path";
2747
3144
  var findSqliteFiles = (root) => {
2748
3145
  const skipped = /* @__PURE__ */ new Set(["node_modules", "dist", "coverage"]);
2749
3146
  const found = [];
2750
3147
  const walk = (directory, depth) => {
2751
- for (const entry of readdirSync4(directory, { withFileTypes: true })) {
3148
+ for (const entry of readdirSync5(directory, { withFileTypes: true })) {
2752
3149
  if (skipped.has(entry.name) || entry.name.startsWith(".")) continue;
2753
- const path = join20(directory, entry.name);
3150
+ const path = join23(directory, entry.name);
2754
3151
  if (entry.isDirectory() && depth < 2) walk(path, depth + 1);
2755
3152
  else if (entry.isFile() && /\.(?:db|sqlite|sqlite3)$/.test(entry.name)) {
2756
- found.push(relative3(root, path).replaceAll("\\", "/"));
3153
+ found.push(relative4(root, path).replaceAll("\\", "/"));
2757
3154
  }
2758
3155
  }
2759
3156
  };
@@ -2765,13 +3162,13 @@ var findSqliteFiles = (root) => {
2765
3162
  var hasPostgrestSources = (root, name) => sourceFilesUnder(root, [name]).length > 0;
2766
3163
 
2767
3164
  // src/config/hasSupabaseJsDependency.ts
2768
- import { existsSync as existsSync15, readFileSync as readFileSync10 } from "fs";
2769
- import { join as join21 } from "path";
3165
+ import { existsSync as existsSync16, readFileSync as readFileSync11 } from "fs";
3166
+ import { join as join24 } from "path";
2770
3167
  var hasSupabaseJsDependency = (root) => {
2771
- const manifestPath = join21(root, "package.json");
2772
- if (!existsSync15(manifestPath)) return false;
3168
+ const manifestPath = join24(root, "package.json");
3169
+ if (!existsSync16(manifestPath)) return false;
2773
3170
  const manifest = JSON.parse(
2774
- readFileSync10(manifestPath, "utf8")
3171
+ readFileSync11(manifestPath, "utf8")
2775
3172
  );
2776
3173
  const dependencies = { ...manifest.dependencies, ...manifest.devDependencies };
2777
3174
  return "@supabase/supabase-js" in dependencies;
@@ -2781,17 +3178,17 @@ var hasSupabaseJsDependency = (root) => {
2781
3178
  var detectStacks = (root) => {
2782
3179
  const sqliteFiles = findSqliteFiles(root);
2783
3180
  const hasDrizzle = ["ts", "js", "mjs"].some(
2784
- (ext) => existsSync16(join22(root, `drizzle.config.${ext}`))
3181
+ (ext) => existsSync17(join25(root, `drizzle.config.${ext}`))
2785
3182
  );
2786
3183
  const drizzleRoots = ["src", "server", "app", "lib", "db"].filter(
2787
- (name) => isDirectory(join22(root, name))
3184
+ (name) => isDirectory(join25(root, name))
2788
3185
  );
2789
3186
  const postgrestRoots = ["src", "app", "supabase/functions"].filter(
2790
- (name) => isDirectory(join22(root, name)) && hasPostgrestSources(root, name)
3187
+ (name) => isDirectory(join25(root, name)) && hasPostgrestSources(root, name)
2791
3188
  );
2792
3189
  return {
2793
- ...isDirectory(join22(root, "supabase/migrations")) ? { supabase: { migrations: "supabase/migrations" } } : {},
2794
- ...existsSync16(join22(root, "prisma/schema.prisma")) ? { prisma: { schema: "prisma/schema.prisma" } } : {},
3190
+ ...isDirectory(join25(root, "supabase/migrations")) ? { supabase: { migrations: "supabase/migrations" } } : {},
3191
+ ...existsSync17(join25(root, "prisma/schema.prisma")) ? { prisma: { schema: "prisma/schema.prisma" } } : {},
2795
3192
  ...hasDrizzle ? { drizzle: { roots: drizzleRoots, objectNames: ["db", "tx"] } } : {},
2796
3193
  ...sqliteFiles.length > 0 ? { sqlite: { files: sqliteFiles } } : {},
2797
3194
  ...hasSupabaseJsDependency(root) && postgrestRoots.length > 0 ? { postgrest: { roots: postgrestRoots } } : {}
@@ -2819,14 +3216,14 @@ var upgradedConfigDocument = (raw, detected) => {
2819
3216
 
2820
3217
  // src/init/planConfigFile.ts
2821
3218
  var planConfigFile = (root, force) => {
2822
- const path = join23(root, CONFIG_FILENAME);
3219
+ const path = join26(root, CONFIG_FILENAME);
2823
3220
  const freshContent = `${JSON.stringify(
2824
3221
  { schemaVersion: 2, ...detectStacks(root), perf: PERF_INIT_SECTION },
2825
3222
  null,
2826
3223
  2
2827
3224
  )}
2828
3225
  `;
2829
- if (!existsSync17(path)) {
3226
+ if (!existsSync18(path)) {
2830
3227
  return {
2831
3228
  path: CONFIG_FILENAME,
2832
3229
  disposition: "create",
@@ -2835,7 +3232,7 @@ var planConfigFile = (root, force) => {
2835
3232
  };
2836
3233
  }
2837
3234
  try {
2838
- const raw = JSON.parse(readFileSync11(path, "utf8"));
3235
+ const raw = JSON.parse(readFileSync12(path, "utf8"));
2839
3236
  const config = configFromDocument(raw);
2840
3237
  if (config.schemaVersion === 1) {
2841
3238
  return {
@@ -2866,23 +3263,23 @@ var planConfigFile = (root, force) => {
2866
3263
  };
2867
3264
 
2868
3265
  // src/init/planPackageScript.ts
2869
- import { existsSync as existsSync18, readFileSync as readFileSync12 } from "fs";
2870
- import { join as join24 } from "path";
3266
+ import { existsSync as existsSync19, readFileSync as readFileSync13 } from "fs";
3267
+ import { join as join27 } from "path";
2871
3268
 
2872
3269
  // src/init/PACKAGE_MANIFEST.ts
2873
3270
  var PACKAGE_MANIFEST = "package.json";
2874
3271
 
2875
3272
  // src/init/planPackageScript.ts
2876
3273
  var planPackageScript = (root, force) => {
2877
- const path = join24(root, PACKAGE_MANIFEST);
2878
- if (!existsSync18(path)) {
3274
+ const path = join27(root, PACKAGE_MANIFEST);
3275
+ if (!existsSync19(path)) {
2879
3276
  return {
2880
3277
  path: PACKAGE_MANIFEST,
2881
3278
  disposition: "unchanged",
2882
3279
  detail: "no package.json"
2883
3280
  };
2884
3281
  }
2885
- const manifest = JSON.parse(readFileSync12(path, "utf8"));
3282
+ const manifest = JSON.parse(readFileSync13(path, "utf8"));
2886
3283
  const current = manifest.scripts?.["db:gate"];
2887
3284
  if (current === "codeality-db gate") {
2888
3285
  return {
@@ -2910,8 +3307,8 @@ var planPackageScript = (root, force) => {
2910
3307
  };
2911
3308
 
2912
3309
  // src/init/planWorkflow.ts
2913
- import { existsSync as existsSync19, readFileSync as readFileSync13 } from "fs";
2914
- import { join as join25 } from "path";
3310
+ import { existsSync as existsSync20, readFileSync as readFileSync14 } from "fs";
3311
+ import { join as join28 } from "path";
2915
3312
 
2916
3313
  // src/init/sha256Of.ts
2917
3314
  import { createHash as createHash2 } from "crypto";
@@ -2924,33 +3321,33 @@ var SHIPPED_WORKFLOW_HASHES = {
2924
3321
 
2925
3322
  // src/init/planWorkflow.ts
2926
3323
  var planWorkflow = (root, force) => {
2927
- const relative4 = ".github/workflows/db-quality.yml";
2928
- const content = readFileSync13(assetPath("db-quality.yml"), "utf8");
2929
- const path = join25(root, relative4);
2930
- if (!existsSync19(path)) {
3324
+ const relative5 = ".github/workflows/db-quality.yml";
3325
+ const content = readFileSync14(assetPath("db-quality.yml"), "utf8");
3326
+ const path = join28(root, relative5);
3327
+ if (!existsSync20(path)) {
2931
3328
  const detail = "runs codeality-db gate on push and pull request";
2932
- return { path: relative4, disposition: "create", detail, content };
3329
+ return { path: relative5, disposition: "create", detail, content };
2933
3330
  }
2934
- const existing = readFileSync13(path, "utf8");
3331
+ const existing = readFileSync14(path, "utf8");
2935
3332
  if (existing === content) {
2936
- return { path: relative4, disposition: "unchanged", detail: "identical" };
3333
+ return { path: relative5, disposition: "unchanged", detail: "identical" };
2937
3334
  }
2938
3335
  const shippedVersion = SHIPPED_WORKFLOW_HASHES[sha256Of(existing)];
2939
3336
  if (shippedVersion !== void 0) {
2940
3337
  return {
2941
- path: relative4,
3338
+ path: relative5,
2942
3339
  disposition: "merge",
2943
3340
  detail: `upgraded from the ${shippedVersion} workflow`,
2944
3341
  content
2945
3342
  };
2946
3343
  }
2947
3344
  return force ? {
2948
- path: relative4,
3345
+ path: relative5,
2949
3346
  disposition: "merge",
2950
3347
  detail: "replaced (--force)",
2951
3348
  content
2952
3349
  } : {
2953
- path: relative4,
3350
+ path: relative5,
2954
3351
  disposition: "conflict",
2955
3352
  detail: "differs from the shipped workflow"
2956
3353
  };
@@ -3091,9 +3488,9 @@ var renderBenchJson = (result, recorded) => JSON.stringify(
3091
3488
 
3092
3489
  // src/bench/writeBenchRecord.ts
3093
3490
  import { renameSync as renameSync2, writeFileSync as writeFileSync4 } from "fs";
3094
- import { join as join26 } from "path";
3491
+ import { join as join29 } from "path";
3095
3492
  var writeBenchRecord = (root, record) => {
3096
- const path = join26(root, BENCH_RECORD_FILENAME);
3493
+ const path = join29(root, BENCH_RECORD_FILENAME);
3097
3494
  writeFileSync4(`${path}.tmp`, `${JSON.stringify(record, null, 2)}
3098
3495
  `);
3099
3496
  renameSync2(`${path}.tmp`, path);
@@ -3196,9 +3593,9 @@ var perfDiffAction = ({
3196
3593
 
3197
3594
  // src/perf/writePerfSnapshot.ts
3198
3595
  import { renameSync as renameSync3, writeFileSync as writeFileSync5 } from "fs";
3199
- import { join as join27 } from "path";
3596
+ import { join as join30 } from "path";
3200
3597
  var writePerfSnapshot = (root, snapshot) => {
3201
- const path = join27(root, PERF_SNAPSHOT_FILENAME);
3598
+ const path = join30(root, PERF_SNAPSHOT_FILENAME);
3202
3599
  writeFileSync5(`${path}.tmp`, `${JSON.stringify(snapshot, null, 2)}
3203
3600
  `);
3204
3601
  renameSync3(`${path}.tmp`, path);
@@ -3330,7 +3727,7 @@ var runCli = (argv2, io) => {
3330
3727
 
3331
3728
  // src/tools/spawnRunner.ts
3332
3729
  import { spawnSync } from "child_process";
3333
- import { delimiter, join as join28 } from "path";
3730
+ import { delimiter, join as join31 } from "path";
3334
3731
  import { env as processEnv } from "process";
3335
3732
 
3336
3733
  // src/tools/packageBinPath.ts
@@ -3340,7 +3737,7 @@ var packageBinPath = () => fileURLToPath2(new URL("../node_modules/.bin", import
3340
3737
  // src/tools/spawnRunner.ts
3341
3738
  var spawnRunner = (command, args, options) => {
3342
3739
  const path = options.systemPathOnly ? processEnv["PATH"] ?? "" : [
3343
- join28(options.cwd, "node_modules/.bin"),
3740
+ join31(options.cwd, "node_modules/.bin"),
3344
3741
  packageBinPath(),
3345
3742
  processEnv["PATH"] ?? ""
3346
3743
  ].join(delimiter);