diff --git a/server/CLAUDE.md b/server/CLAUDE.md index aca926a78..6e4710867 100644 --- a/server/CLAUDE.md +++ b/server/CLAUDE.md @@ -35,7 +35,7 @@ Single test: `npx vitest run tests/unit/nest/weather.controller.test.ts`, or `np **Build emits even with type errors** (`scripts/build.mjs` catches `tsc` failures and warns). `npm run build` succeeding does **not** mean the code typechecks — run `npm run typecheck`. -**`lint:dialect`** (`scripts/dialect-lint.mjs`) reads every string and template literal under `src/` (comments do not count) and every `.insertId` read, and counts the SQLite-only spellings per file and rule: `INSERT OR IGNORE`/`REPLACE`, `datetime()`/`julianday()`, `strftime`, `json_extract` and its siblings, `GLOB`, `last_insert_rowid`, `AUTOINCREMENT`, a `PRAGMA` statement outside the `src/db/*.ts` lifecycle files and the migrations, and `||` concatenation in SQL. The counts are held to `scripts/dialect-baseline.json`; a new hit fails, and so does an entry above what its file holds now, so the change that removes one runs `npm run lint:dialect -- --update` (it never raises or adds an entry). `src/db/dialect/sql-functions.ts` is not scanned, and neither are the migrations up to `FROZEN_MIGRATIONS_THROUGH` in the script; a newer migration is held to the DML rules, while its DDL stays engine specific. +**`lint:dialect`** (`scripts/dialect-lint.mjs`) reads every string and template literal under `src/` (comments do not count) and every `.insertId` read, and counts the SQLite-only spellings per file and rule: `INSERT OR IGNORE`/`REPLACE`, `datetime()`/`julianday()`/`unixepoch()` (also as the bare name handed to `eb.fn`), `strftime`, `json_extract` and its siblings, `GLOB`, `last_insert_rowid`, `AUTOINCREMENT`, a `PRAGMA` statement outside the `src/db/*.ts` lifecycle files and the migrations, and `||` concatenation in SQL. The counts are held to `scripts/dialect-baseline.json`; a new hit fails, and so does an entry above what its file holds now, so the change that removes one runs `npm run lint:dialect -- --update` (it never raises or adds an entry). `src/db/dialect/sql-functions.ts` is not scanned, and neither are the migrations up to `FROZEN_MIGRATIONS_THROUGH` in the script; a newer migration is held to the DML rules, while its DDL stays engine specific. ## Architecture invariants diff --git a/server/scripts/dialect-lint.mjs b/server/scripts/dialect-lint.mjs index f12f072e5..75388ca8f 100644 --- a/server/scripts/dialect-lint.mjs +++ b/server/scripts/dialect-lint.mjs @@ -58,7 +58,9 @@ export const RULES = [ }, { name: 'datetime', - test: (text) => /\b(datetime|julianday|unixepoch)\s*\(/i.test(text), + // The call in SQL text, or the bare name the query builder takes in eb.fn('datetime', [...]). + test: (text) => + /\b(datetime|julianday|unixepoch)\s*\(/i.test(text) || /^(datetime|julianday|unixepoch)$/i.test(text.trim()), advice: 'use currentTimestamp/nowMinusDays/nowPlusSeconds and friends from sql-functions.ts', migrations: true, }, diff --git a/server/tests/unit/scripts/dialect-lint.test.ts b/server/tests/unit/scripts/dialect-lint.test.ts index 993ab548d..b024439fe 100644 --- a/server/tests/unit/scripts/dialect-lint.test.ts +++ b/server/tests/unit/scripts/dialect-lint.test.ts @@ -72,6 +72,7 @@ describe('dialect-lint.mjs', () => { ['insert-or', "void 'INSERT OR IGNORE INTO t (a) VALUES (?)';"], ['insert-or', 'void `INSERT OR REPLACE INTO t (a) VALUES (?)`;'], ['datetime', "void \"UPDATE t SET at = datetime('now')\";"], + ['datetime', "declare function fn(name: string, args: string[]): void;\nfn('julianday', ['now']);"], ['strftime', "void `SELECT strftime('%s', 'now')`;"], ['json', "void 'SELECT json_extract(settings, ?) FROM users';"], ['glob', "void 'SELECT id FROM t WHERE name GLOB ?';"],