@syntopica/db-quality 0.2.0 → 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
  };
@@ -2761,14 +3158,17 @@ var findSqliteFiles = (root) => {
2761
3158
  return found.sort();
2762
3159
  };
2763
3160
 
3161
+ // src/config/hasPostgrestSources.ts
3162
+ var hasPostgrestSources = (root, name) => sourceFilesUnder(root, [name]).length > 0;
3163
+
2764
3164
  // src/config/hasSupabaseJsDependency.ts
2765
- import { existsSync as existsSync15, readFileSync as readFileSync10 } from "fs";
2766
- import { join as join21 } from "path";
3165
+ import { existsSync as existsSync16, readFileSync as readFileSync11 } from "fs";
3166
+ import { join as join24 } from "path";
2767
3167
  var hasSupabaseJsDependency = (root) => {
2768
- const manifestPath = join21(root, "package.json");
2769
- if (!existsSync15(manifestPath)) return false;
3168
+ const manifestPath = join24(root, "package.json");
3169
+ if (!existsSync16(manifestPath)) return false;
2770
3170
  const manifest = JSON.parse(
2771
- readFileSync10(manifestPath, "utf8")
3171
+ readFileSync11(manifestPath, "utf8")
2772
3172
  );
2773
3173
  const dependencies = { ...manifest.dependencies, ...manifest.devDependencies };
2774
3174
  return "@supabase/supabase-js" in dependencies;
@@ -2778,17 +3178,17 @@ var hasSupabaseJsDependency = (root) => {
2778
3178
  var detectStacks = (root) => {
2779
3179
  const sqliteFiles = findSqliteFiles(root);
2780
3180
  const hasDrizzle = ["ts", "js", "mjs"].some(
2781
- (ext) => existsSync16(join22(root, `drizzle.config.${ext}`))
3181
+ (ext) => existsSync17(join25(root, `drizzle.config.${ext}`))
2782
3182
  );
2783
3183
  const drizzleRoots = ["src", "server", "app", "lib", "db"].filter(
2784
- (name) => isDirectory(join22(root, name))
3184
+ (name) => isDirectory(join25(root, name))
2785
3185
  );
2786
3186
  const postgrestRoots = ["src", "app", "supabase/functions"].filter(
2787
- (name) => isDirectory(join22(root, name))
3187
+ (name) => isDirectory(join25(root, name)) && hasPostgrestSources(root, name)
2788
3188
  );
2789
3189
  return {
2790
- ...isDirectory(join22(root, "supabase/migrations")) ? { supabase: { migrations: "supabase/migrations" } } : {},
2791
- ...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" } } : {},
2792
3192
  ...hasDrizzle ? { drizzle: { roots: drizzleRoots, objectNames: ["db", "tx"] } } : {},
2793
3193
  ...sqliteFiles.length > 0 ? { sqlite: { files: sqliteFiles } } : {},
2794
3194
  ...hasSupabaseJsDependency(root) && postgrestRoots.length > 0 ? { postgrest: { roots: postgrestRoots } } : {}
@@ -2816,14 +3216,14 @@ var upgradedConfigDocument = (raw, detected) => {
2816
3216
 
2817
3217
  // src/init/planConfigFile.ts
2818
3218
  var planConfigFile = (root, force) => {
2819
- const path = join23(root, CONFIG_FILENAME);
3219
+ const path = join26(root, CONFIG_FILENAME);
2820
3220
  const freshContent = `${JSON.stringify(
2821
3221
  { schemaVersion: 2, ...detectStacks(root), perf: PERF_INIT_SECTION },
2822
3222
  null,
2823
3223
  2
2824
3224
  )}
2825
3225
  `;
2826
- if (!existsSync17(path)) {
3226
+ if (!existsSync18(path)) {
2827
3227
  return {
2828
3228
  path: CONFIG_FILENAME,
2829
3229
  disposition: "create",
@@ -2832,7 +3232,7 @@ var planConfigFile = (root, force) => {
2832
3232
  };
2833
3233
  }
2834
3234
  try {
2835
- const raw = JSON.parse(readFileSync11(path, "utf8"));
3235
+ const raw = JSON.parse(readFileSync12(path, "utf8"));
2836
3236
  const config = configFromDocument(raw);
2837
3237
  if (config.schemaVersion === 1) {
2838
3238
  return {
@@ -2863,23 +3263,23 @@ var planConfigFile = (root, force) => {
2863
3263
  };
2864
3264
 
2865
3265
  // src/init/planPackageScript.ts
2866
- import { existsSync as existsSync18, readFileSync as readFileSync12 } from "fs";
2867
- import { join as join24 } from "path";
3266
+ import { existsSync as existsSync19, readFileSync as readFileSync13 } from "fs";
3267
+ import { join as join27 } from "path";
2868
3268
 
2869
3269
  // src/init/PACKAGE_MANIFEST.ts
2870
3270
  var PACKAGE_MANIFEST = "package.json";
2871
3271
 
2872
3272
  // src/init/planPackageScript.ts
2873
3273
  var planPackageScript = (root, force) => {
2874
- const path = join24(root, PACKAGE_MANIFEST);
2875
- if (!existsSync18(path)) {
3274
+ const path = join27(root, PACKAGE_MANIFEST);
3275
+ if (!existsSync19(path)) {
2876
3276
  return {
2877
3277
  path: PACKAGE_MANIFEST,
2878
3278
  disposition: "unchanged",
2879
3279
  detail: "no package.json"
2880
3280
  };
2881
3281
  }
2882
- const manifest = JSON.parse(readFileSync12(path, "utf8"));
3282
+ const manifest = JSON.parse(readFileSync13(path, "utf8"));
2883
3283
  const current = manifest.scripts?.["db:gate"];
2884
3284
  if (current === "codeality-db gate") {
2885
3285
  return {
@@ -2907,8 +3307,8 @@ var planPackageScript = (root, force) => {
2907
3307
  };
2908
3308
 
2909
3309
  // src/init/planWorkflow.ts
2910
- import { existsSync as existsSync19, readFileSync as readFileSync13 } from "fs";
2911
- import { join as join25 } from "path";
3310
+ import { existsSync as existsSync20, readFileSync as readFileSync14 } from "fs";
3311
+ import { join as join28 } from "path";
2912
3312
 
2913
3313
  // src/init/sha256Of.ts
2914
3314
  import { createHash as createHash2 } from "crypto";
@@ -2921,33 +3321,33 @@ var SHIPPED_WORKFLOW_HASHES = {
2921
3321
 
2922
3322
  // src/init/planWorkflow.ts
2923
3323
  var planWorkflow = (root, force) => {
2924
- const relative4 = ".github/workflows/db-quality.yml";
2925
- const content = readFileSync13(assetPath("db-quality.yml"), "utf8");
2926
- const path = join25(root, relative4);
2927
- 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)) {
2928
3328
  const detail = "runs codeality-db gate on push and pull request";
2929
- return { path: relative4, disposition: "create", detail, content };
3329
+ return { path: relative5, disposition: "create", detail, content };
2930
3330
  }
2931
- const existing = readFileSync13(path, "utf8");
3331
+ const existing = readFileSync14(path, "utf8");
2932
3332
  if (existing === content) {
2933
- return { path: relative4, disposition: "unchanged", detail: "identical" };
3333
+ return { path: relative5, disposition: "unchanged", detail: "identical" };
2934
3334
  }
2935
3335
  const shippedVersion = SHIPPED_WORKFLOW_HASHES[sha256Of(existing)];
2936
3336
  if (shippedVersion !== void 0) {
2937
3337
  return {
2938
- path: relative4,
3338
+ path: relative5,
2939
3339
  disposition: "merge",
2940
3340
  detail: `upgraded from the ${shippedVersion} workflow`,
2941
3341
  content
2942
3342
  };
2943
3343
  }
2944
3344
  return force ? {
2945
- path: relative4,
3345
+ path: relative5,
2946
3346
  disposition: "merge",
2947
3347
  detail: "replaced (--force)",
2948
3348
  content
2949
3349
  } : {
2950
- path: relative4,
3350
+ path: relative5,
2951
3351
  disposition: "conflict",
2952
3352
  detail: "differs from the shipped workflow"
2953
3353
  };
@@ -3088,9 +3488,9 @@ var renderBenchJson = (result, recorded) => JSON.stringify(
3088
3488
 
3089
3489
  // src/bench/writeBenchRecord.ts
3090
3490
  import { renameSync as renameSync2, writeFileSync as writeFileSync4 } from "fs";
3091
- import { join as join26 } from "path";
3491
+ import { join as join29 } from "path";
3092
3492
  var writeBenchRecord = (root, record) => {
3093
- const path = join26(root, BENCH_RECORD_FILENAME);
3493
+ const path = join29(root, BENCH_RECORD_FILENAME);
3094
3494
  writeFileSync4(`${path}.tmp`, `${JSON.stringify(record, null, 2)}
3095
3495
  `);
3096
3496
  renameSync2(`${path}.tmp`, path);
@@ -3193,9 +3593,9 @@ var perfDiffAction = ({
3193
3593
 
3194
3594
  // src/perf/writePerfSnapshot.ts
3195
3595
  import { renameSync as renameSync3, writeFileSync as writeFileSync5 } from "fs";
3196
- import { join as join27 } from "path";
3596
+ import { join as join30 } from "path";
3197
3597
  var writePerfSnapshot = (root, snapshot) => {
3198
- const path = join27(root, PERF_SNAPSHOT_FILENAME);
3598
+ const path = join30(root, PERF_SNAPSHOT_FILENAME);
3199
3599
  writeFileSync5(`${path}.tmp`, `${JSON.stringify(snapshot, null, 2)}
3200
3600
  `);
3201
3601
  renameSync3(`${path}.tmp`, path);
@@ -3327,7 +3727,7 @@ var runCli = (argv2, io) => {
3327
3727
 
3328
3728
  // src/tools/spawnRunner.ts
3329
3729
  import { spawnSync } from "child_process";
3330
- import { delimiter, join as join28 } from "path";
3730
+ import { delimiter, join as join31 } from "path";
3331
3731
  import { env as processEnv } from "process";
3332
3732
 
3333
3733
  // src/tools/packageBinPath.ts
@@ -3337,7 +3737,7 @@ var packageBinPath = () => fileURLToPath2(new URL("../node_modules/.bin", import
3337
3737
  // src/tools/spawnRunner.ts
3338
3738
  var spawnRunner = (command, args, options) => {
3339
3739
  const path = options.systemPathOnly ? processEnv["PATH"] ?? "" : [
3340
- join28(options.cwd, "node_modules/.bin"),
3740
+ join31(options.cwd, "node_modules/.bin"),
3341
3741
  packageBinPath(),
3342
3742
  processEnv["PATH"] ?? ""
3343
3743
  ].join(delimiter);