107 lines · 3.7 KB · TypeScript
RawDownload
// Lint: reject migration SQL that nests a single function call too deeply.//// SQLite's lemon parser stack overflows at PREPARE time when an expression// nests too deep, and the limit is platform/build-dependent (historically the// macOS-built compiled CLI tripped around ~40 levels). The runtime is now// libSQL, which uses the same SQLite parser, so the structural risk persists;// this lint catches the class of bug regardless of which build runs the// migration. Cap is 20 (well above any legitimate nested-function call we have// today, well below the SQLite parser limit).import { describe, expect, it } from "@effect/vitest";import { readdirSync, readFileSync } from "node:fs";import { join } from "node:path"; const MIGRATIONS_FOLDER = join(import.meta.dirname, "../../drizzle");const MAX_NESTING = 20; // Walk the SQL char-by-char tracking the call-stack of nested function// names. A "function call" is any IDENT immediately followed by `(`.// Returns { fn, depth } for the deepest single-name nest found, or null.const findDeepestNest = (sql: string) => {  const stack: string[] = [];  let worst: { fn: string; depth: number; offset: number } | null = null;  let i = 0;  while (i < sql.length) {    const ch = sql[i];    // Skip line comments    if (ch === "-" && sql[i + 1] === "-") {      const eol = sql.indexOf("\n", i);      i = eol < 0 ? sql.length : eol + 1;      continue;    }    // Skip string literals (single- and double-quoted, backtick identifiers)    if (ch === "'" || ch === '"' || ch === "`") {      const quote = ch;      i++;      while (i < sql.length) {        if (sql[i] === quote) {          if (sql[i + 1] === quote) {            i += 2;            continue;          }          i++;          break;        }        i++;      }      continue;    }    if (ch === "(") {      // Walk back to find an IDENT immediately preceding this paren      let j = i - 1;      while (j >= 0 && /\s/u.test(sql[j])) j--;      let end = j;      while (j >= 0 && /[A-Za-z0-9_]/u.test(sql[j])) j--;      const name = sql.slice(j + 1, end + 1).toLowerCase();      stack.push(name || "(");      // Count consecutive same-name frames in the stack      let same = 0;      for (let k = stack.length - 1; k >= 0; k--) {        if (stack[k] === stack[stack.length - 1]) same++;        else break;      }      if (name && (worst === null || same > worst.depth)) {        worst = { fn: name, depth: same, offset: i };      }      i++;      continue;    }    if (ch === ")") {      stack.pop();      i++;      continue;    }    i++;  }  return worst;}; describe("drizzle migration SQL structural lint", () => {  const files = readdirSync(MIGRATIONS_FOLDER)    .filter((f) => f.endsWith(".sql"))    .sort();   for (const f of files) {    it(`${f} keeps nested-function depth under ${MAX_NESTING}`, () => {      const sql = readFileSync(join(MIGRATIONS_FOLDER, f), "utf-8");      const worst = findDeepestNest(sql);      const summary =        worst === null          ? { ok: true as const }          : {              ok: worst.depth < MAX_NESTING,              file: f,              line: sql.slice(0, worst.offset).split("\n").length,              fn: worst.fn,              depth: worst.depth,            };      // The expectation is `summary.ok === true`. The full `summary` object is      // matched (not just `.ok`) so the failure diff prints file/line/fn/depth      // — SQLite's lemon parser stack overflows on deeply nested expressions,      // so the diff is the breadcrumb that tells you which migration to      // refactor (precompute into a temp table à la 0008's __slug_norm, or      // split the expression into multiple shallow steps).      expect(summary).toEqual({ ...summary, ok: true });    });  }});