From 7c9e5086c7be24bd8e2841b55f374c9c56c8b550 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 23:10:40 +0000 Subject: [PATCH 1/3] fix(driver-sql): refuse a SQLite file that is not a database at connect, naming it SqlDriver.connect() swallowed SQLITE_NOTADB from its first PRAGMA and resolved over a file it could not read. It now refuses it as a SqliteDatabaseFileError, as SQLITE_CANTOPEN already was, and the step-down in service-datasource classifies the code as a file failure, so a development boot no longer hands the file to wasm SQLite, whose open moved it aside and wrote an empty database in its place. Claude-Session: https://claude.ai/code/session_01QK6pdbxKZiHdFApL8RYVh9 Co-authored-by: Claude --- ...driver-24960-sqlite-not-a-database.test.ts | 100 ++++++++++++++++++ packages/drivers/driver-sql/src/sql-driver.ts | 39 +++++-- .../driver-sql/src/sqlite-database-file.ts | 5 +- .../src/sqlite-driver-fallback.ts | 34 +++--- 4 files changed, 156 insertions(+), 22 deletions(-) create mode 100644 packages/drivers/driver-sql/src/sql-driver-24960-sqlite-not-a-database.test.ts diff --git a/packages/drivers/driver-sql/src/sql-driver-24960-sqlite-not-a-database.test.ts b/packages/drivers/driver-sql/src/sql-driver-24960-sqlite-not-a-database.test.ts new file mode 100644 index 00000000000..cbaaf3e1e59 --- /dev/null +++ b/packages/drivers/driver-sql/src/sql-driver-24960-sqlite-not-a-database.test.ts @@ -0,0 +1,100 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * A declared SQLite file whose bytes are not a SQLite database fails + * `connect()` at once, naming the file, and the file is left exactly as it was + * found (#24960). + * + * Before: the connection's first statement (`PRAGMA auto_vacuum`) answered + * `SQLITE_NOTADB`, which `connect()` logged as "Failed to set PRAGMA + * auto_vacuum=INCREMENTAL" and swallowed, so it resolved over a file it could + * not read. In production the first query failed later; on a development boot + * the step-down (`resolveSqliteDriver` in `@objectstack/service-datasource`) + * answered with wasm SQLite, whose open moved the operator's file aside to + * `.corrupt-` and wrote an empty database in its place, and + * the datasource read `connected`. `SQLITE_CANTOPEN` was already refused this + * way (#23696); `SQLITE_NOTADB` was left out. + * + * The refusal is the driver's own, so it holds whatever `NODE_ENV` says: both + * modes are pinned. + */ + +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { SqlDriver } from './sql-driver.js'; +import { SqliteDatabaseFileError } from './sqlite-database-file.js'; + +const PLAIN_TEXT = 'archive notes — this file holds plain text, not a SQLite database\n'; + +const drivers: SqlDriver[] = []; +function make(filename: string): SqlDriver { + const d = new SqlDriver({ client: 'better-sqlite3', connection: { filename }, useNullAsDefault: true }); + drivers.push(d); + return d; +} + +/** `connect()`'s rejection, or a failure saying it resolved. */ +async function refusalOf(driver: SqlDriver): Promise { + try { + await driver.connect(); + } catch (error) { + return error; + } + throw new Error('connect() resolved for a file that is not a SQLite database'); +} + +let dir: string; +beforeEach(() => { dir = mkdtempSync(join(tmpdir(), 'os-24960-')); }); +afterEach(async () => { + vi.unstubAllEnvs(); + await Promise.all(drivers.splice(0).map((d) => d.disconnect().catch(() => {}))); + rmSync(dir, { recursive: true, force: true }); +}); + +describe('SqlDriver.connect — a declared SQLite file that is not a database (#24960)', () => { + for (const nodeEnv of ['development', 'production'] as const) { + it(`refuses a plain-text file with SQLITE_NOTADB, naming it, and leaves its bytes untouched (NODE_ENV=${nodeEnv})`, async () => { + vi.stubEnv('NODE_ENV', nodeEnv); + const filename = join(dir, 'archive.db'); + writeFileSync(filename, PLAIN_TEXT); + const before = readFileSync(filename); + + const error = await refusalOf(make(filename)); + + expect(error).toBeInstanceOf(SqliteDatabaseFileError); + const e = error as SqliteDatabaseFileError; + expect(e.code).toBe('SQLITE_NOTADB'); + expect(e.filename).toBe(filename); + expect(e.message).toContain(filename); + expect(e.message).toContain((e.cause as Error).message); + // Nothing was written: the same bytes, and no sibling (journal, WAL, + // `.corrupt-*`) beside it. + expect(readFileSync(filename).equals(before)).toBe(true); + expect(readdirSync(dir)).toEqual(['archive.db']); + }); + } + + it('CONTROL: a real database file still connects and answers, and is not moved', async () => { + const filename = join(dir, 'real.db'); + const seed = make(filename); + await seed.connect(); + await seed.execute('CREATE TABLE t (a INTEGER)'); + await seed.execute('INSERT INTO t (a) VALUES (7)'); + await seed.disconnect(); + + const driver = make(filename); + await driver.connect(); + expect(await driver.execute('SELECT a FROM t')).toEqual([{ a: 7 }]); + expect(readdirSync(dir).filter((name) => name.includes('.corrupt-'))).toEqual([]); + }); + + it('CONTROL: an empty file is an empty SQLite database, and connects', async () => { + const filename = join(dir, 'empty.db'); + writeFileSync(filename, ''); + const driver = make(filename); + await driver.connect(); + expect(await driver.execute('SELECT 1 AS one')).toEqual([{ one: 1 }]); + }); +}); diff --git a/packages/drivers/driver-sql/src/sql-driver.ts b/packages/drivers/driver-sql/src/sql-driver.ts index b4393f25d41..b99ad91eeb7 100644 --- a/packages/drivers/driver-sql/src/sql-driver.ts +++ b/packages/drivers/driver-sql/src/sql-driver.ts @@ -7029,6 +7029,9 @@ export class SqlDriver implements IDataDriver { * @returns whether the connection answered. The read is the first statement * any connection runs, so `false` says the connection itself is not answering — * not merely that space hygiene is unavailable. + * @throws {SqliteDatabaseFileError} when that first statement's answer is a + * verdict on the file itself ({@link SQLITE_FILE_REFUSALS}): it cannot be + * opened, or it is not a SQLite database. */ private async applyAutoVacuumIncremental(): Promise { try { @@ -7052,15 +7055,24 @@ export class SqlDriver implements IDataDriver { // let the step-down message that follows explain the outcome. Any OTHER // PRAGMA failure keeps the full warning (with stack) as before. const code = (e as { code?: string } | null | undefined)?.code; - // The file itself cannot be opened or created (#23696): a directory that - // exists but refuses the file, a path SQLite cannot open. Every later - // statement would answer the same, so the connect fails here, naming the - // file — before, it logged and resolved, and the datasource counted as - // connected while it could not answer one query. - if (code === 'SQLITE_CANTOPEN') { + // The file itself is refused, and every later statement would answer the + // same, so the connect fails here, naming the file. Before, each of these + // logged and resolved, and the datasource counted as connected while it + // could not answer one query: + // • SQLITE_CANTOPEN (#23696) — a directory that exists but refuses the + // file, a path SQLite cannot open; + // • SQLITE_NOTADB (#24960) — the file exists and opens, but its bytes + // are not a SQLite database (plain text, a foreign format, a file + // encrypted by another engine). On a development boot the step-down + // used to answer it with wasm SQLite, whose open moved the + // operator's file aside and wrote an empty database in its place. + // The refusal is issued before anything is written: the file is left + // exactly as it was found. + const refusal = code === undefined ? undefined : SqlDriver.SQLITE_FILE_REFUSALS.get(code); + if (refusal !== undefined) { const declared = (this.config as any).connection; const filename = this.sqliteFilename() ?? String(typeof declared === 'string' ? declared : declared?.filename); - throw new SqliteDatabaseFileError(filename, 'cannot be opened or created', e); + throw new SqliteDatabaseFileError(filename, refusal, e); } const msg = e instanceof Error ? e.message : String(e); const isNativeLoadFailure = @@ -7079,6 +7091,19 @@ export class SqlDriver implements IDataDriver { } } + /** + * The SQLite codes the connection's first statement can answer that are a + * verdict on the FILE, each with the words {@link SqliteDatabaseFileError} + * names it with. Only a loaded addon answers with a SQLite code, so none of + * these is a native-addon load failure; and every rung of a step-down opens + * the same file, so no other engine can cure one (`resolveSqliteDriver` in + * `@objectstack/service-datasource` re-throws them). + */ + private static readonly SQLITE_FILE_REFUSALS: ReadonlyMap = new Map([ + ['SQLITE_CANTOPEN', 'cannot be opened or created'], + ['SQLITE_NOTADB', 'is not a SQLite database, and was left as it is'], + ]); + /** What `PRAGMA auto_vacuum` answers for INCREMENTAL (0 is NONE, 1 is FULL). */ private static readonly SQLITE_AUTO_VACUUM_INCREMENTAL = 2; diff --git a/packages/drivers/driver-sql/src/sqlite-database-file.ts b/packages/drivers/driver-sql/src/sqlite-database-file.ts index f61d8d5618e..b30378feb48 100644 --- a/packages/drivers/driver-sql/src/sqlite-database-file.ts +++ b/packages/drivers/driver-sql/src/sqlite-database-file.ts @@ -4,13 +4,14 @@ import { mkdir, stat } from 'node:fs/promises'; import { dirname, isAbsolute, resolve } from 'node:path'; /** - * A SQLite database file this driver cannot open or create (#23696). + * A SQLite database file this driver cannot open or create (#23696), or one + * whose bytes are not a SQLite database (#24960). * * Thrown by `SqlDriver.connect()`, so the boot's connect refusal (ADR-0062 D5, * `datasource '': connect failed — `) carries the path and the * operating system's own answer instead of waiting for a plugin start timeout. * `code` is the underlying error's (`ENOENT`, `EACCES`, `ENOTDIR`, - * `SQLITE_CANTOPEN`, …) and `cause` the error itself. + * `SQLITE_CANTOPEN`, `SQLITE_NOTADB`, …) and `cause` the error itself. */ export class SqliteDatabaseFileError extends Error { readonly code: string | undefined; diff --git a/packages/services/service-datasource/src/sqlite-driver-fallback.ts b/packages/services/service-datasource/src/sqlite-driver-fallback.ts index ec4203705ce..165d52e2d85 100644 --- a/packages/services/service-datasource/src/sqlite-driver-fallback.ts +++ b/packages/services/service-datasource/src/sqlite-driver-fallback.ts @@ -42,12 +42,16 @@ * never built (`bindings`' "Could not locate the bindings file", which has * no code), or a missing `better-sqlite3` package. Today's path, * unchanged. - * - **The database FILE cannot be opened or created — re-thrown, as in - * production.** A path under a regular file, a parent directory the driver - * cannot make, a permission refusal, a directory where the file should be. + * - **The database FILE cannot be opened or created, or is not a database — + * re-thrown, as in production.** A path under a regular file, a parent + * directory the driver cannot make, a permission refusal, a directory where + * the file should be, a file whose bytes are not a SQLite database. * `SqlDriver.connect()` names these with the operating system's code * (`EEXIST`, `ENOTDIR`, `ENOENT`, `EACCES`, …) or SQLite's - * `SQLITE_CANTOPEN`; see {@link isSqliteFileOpenFailure}. Every rung opens + * `SQLITE_CANTOPEN` / `SQLITE_NOTADB`; see {@link isSqliteFileOpenFailure}. + * A file that is not a database used to step down to wasm SQLite, whose + * open moved the operator's file aside and wrote an empty database in its + * place (#24960). Every rung opens * the same file, so no rung can cure this. Before, the step-down answered * in its place: the in-memory rung when wasm failed on the same path, or a * wasm driver whose connect resolved over a file it could not read. So a @@ -134,20 +138,24 @@ function isEphemeralFilename(filename: string): boolean { } /** - * Is this probe failure the database FILE failing to open or be created, as - * opposed to the native addon failing to load (#24950)? Judged on the thrown - * value's own `code` alone, never on its message. `SqlDriver.connect()` is what - * refuses such a file, with a `SqliteDatabaseFileError` whose `code` is the - * answer it got: + * Is this probe failure the database FILE failing to open or be created, or + * not being a database at all, as opposed to the native addon failing to load + * (#24950, #24960)? Judged on the thrown value's own `code` alone, never on its + * message. `SqlDriver.connect()` is what refuses such a file, with a + * `SqliteDatabaseFileError` whose `code` is the answer it got: * * - an operating-system error code (`EEXIST`, `ENOTDIR`, `ENOENT`, `EACCES`, * `EPERM`, `EROFS`, …), from making the file's directory. These codes are * `E` plus capital letters and digits, with no underscore; * - SQLite's `SQLITE_CANTOPEN`: the directory is there, and SQLite still - * cannot open the file. Only a LOADED addon can answer with a SQLite code. + * cannot open the file. Only a LOADED addon can answer with a SQLite code; + * - SQLite's `SQLITE_NOTADB`: the file opens, and its bytes are not a SQLite + * database. wasm SQLite reads the same bytes, and its open moves a file it + * cannot read aside and starts an empty database in its place, so stepping + * down would replace the operator's file rather than read it. * - * An addon that cannot load never carries either. It surfaces at the probe's - * `SELECT 1`, whose failure `SqlDriver.execute()` hands back as + * An addon that cannot load never carries any of these. It surfaces at the + * probe's `SELECT 1`, whose failure `SqlDriver.execute()` hands back as * `DATABASE_ERROR`, with the load error as its `cause`: Node's * `ERR_DLOPEN_FAILED`, `MODULE_NOT_FOUND`, or no code at all (`bindings`' * "Could not locate the bindings file"). A missing `better-sqlite3` package @@ -159,7 +167,7 @@ function isEphemeralFilename(filename: string): boolean { function isSqliteFileOpenFailure(err: unknown): boolean { const code = (err as { code?: unknown } | null | undefined)?.code; if (typeof code !== 'string') return false; - return /^E[A-Z0-9]+$/.test(code) || code === 'SQLITE_CANTOPEN'; + return /^E[A-Z0-9]+$/.test(code) || code === 'SQLITE_CANTOPEN' || code === 'SQLITE_NOTADB'; } /** Best-effort close of a probe driver that failed; its own error is not the answer. */ From c5b499cf034d884f79646f90fbb9ae911e3f3245 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 23:15:37 +0000 Subject: [PATCH 2/3] test(service-datasource): pin a declared SQLite file that is not a database as refused and untouched A development boot of a declared datasource on a plain-text file is refused with the driver's SQLITE_NOTADB refusal, steps down to nothing, warns nothing, and leaves the file's bytes and directory exactly as they were; production refuses at connect too. Adds the patch changeset for both packages. Claude-Session: https://claude.ai/code/session_01QK6pdbxKZiHdFApL8RYVh9 Co-authored-by: Claude --- .changeset/24960-sqlite-notadb-connect.md | 20 +++ ...ite-driver-fallback-not-a-database.test.ts | 126 ++++++++++++++++++ 2 files changed, 146 insertions(+) create mode 100644 .changeset/24960-sqlite-notadb-connect.md create mode 100644 packages/services/service-datasource/src/__tests__/sqlite-driver-fallback-not-a-database.test.ts diff --git a/.changeset/24960-sqlite-notadb-connect.md b/.changeset/24960-sqlite-notadb-connect.md new file mode 100644 index 00000000000..ef65f8d8e4e --- /dev/null +++ b/.changeset/24960-sqlite-notadb-connect.md @@ -0,0 +1,20 @@ +--- +'@objectstack/driver-sql': patch +'@objectstack/service-datasource': patch +--- + +fix(driver-sql): a SQLite file whose bytes are not a SQLite database fails `connect()` at once, naming the file, and a development boot no longer moves it aside and replaces it with an empty database (#24960) + +`SqlDriver.connect()` runs `PRAGMA auto_vacuum` as the connection's first statement. On a file that is not a SQLite database (plain text, another format, a file encrypted by another engine) SQLite answers `SQLITE_NOTADB`, which `connect()` logged as "Failed to set PRAGMA auto_vacuum=INCREMENTAL" and swallowed. It resolved over a file it could not read. In production the first query failed later. On a development boot, `resolveSqliteDriver` (`@objectstack/service-datasource`) answered the failed probe by stepping down to wasm SQLite, whose open renamed the file to `.corrupt-`, wrote an empty database in its place and read `connected`, under a warning that blamed the native addon. + +`connect()` now refuses such a file with a `SqliteDatabaseFileError` (`code: 'SQLITE_NOTADB'`, `filename`, a message naming the file), as it already did for `SQLITE_CANTOPEN`, in every mode and before anything is written. A declared datasource's boot prints `datasource '': connect failed — `. `resolveSqliteDriver` classifies `SQLITE_NOTADB` as a file failure and re-throws it instead of stepping down, so the operator's file is left exactly as it was found. A real database file, an empty file and an absent file connect as before, and a native addon that cannot load still steps down to wasm SQLite. + +Clause-②: no (narrowing) + + + +**BREAKING** (`SqlDriver.connect()`'s accept set narrows): a connect that resolved over a SQLite file that is not a database now rejects. It ships as `patch`: no public surface changes, and during the launch window breaking-ness is carried by this banner and the disposition above, not by the level. + +- **Before:** `connect()` resolved; in production the first query failed with `DATABASE_ERROR`, and on a development boot the file was renamed to `.corrupt-` and an empty database was written in its place. +- **Now:** `connect()` rejects with `SqliteDatabaseFileError`, `code: 'SQLITE_NOTADB'`, naming the file; the file is untouched. +- **Fix:** point the datasource at a SQLite database file, or move the file aside yourself (an absent file is created empty on the next connect). diff --git a/packages/services/service-datasource/src/__tests__/sqlite-driver-fallback-not-a-database.test.ts b/packages/services/service-datasource/src/__tests__/sqlite-driver-fallback-not-a-database.test.ts new file mode 100644 index 00000000000..f2921cf09c2 --- /dev/null +++ b/packages/services/service-datasource/src/__tests__/sqlite-driver-fallback-not-a-database.test.ts @@ -0,0 +1,126 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * A declared SQLite datasource whose file is not a SQLite database is refused + * at connect, naming the file, in development as in production. On a + * development boot the operator's file is neither renamed nor replaced, and + * nothing steps down to wasm SQLite or the in-memory driver (#24960). + * + * Before: `SqlDriver.connect()` swallowed the file's `SQLITE_NOTADB` and + * resolved, the probe's `SELECT 1` then failed as a `DATABASE_ERROR`, and in + * development `resolveSqliteDriver` stepped down to wasm SQLite. wasm's open + * moved the file aside to `.corrupt-` and wrote an empty + * database in its place, under the warning "native better-sqlite3 unavailable + * (ABI mismatch or not built)", which named the wrong cause; the datasource + * read `connected`. Measured through `os migrate apply --yes` on the card. + * + * Real `better-sqlite3`, real wasm SQLite and real files in a scratch + * directory, as in `./sqlite-driver-fallback-file-open-failure.test.ts`. + */ + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { resolveSqliteDriver, type ResolvedSqliteDriver } from '../sqlite-driver-fallback.js'; +import { DatasourceConnectionService } from '../datasource-connection-service.js'; +import { createDefaultDatasourceDriverFactory } from '../default-datasource-driver-factory.js'; + +const PLAIN_TEXT = 'archive notes — this file holds plain text, not a SQLite database\n'; + +let dir: string; +let filename: string; +let bytes: Buffer; +const opened: ResolvedSqliteDriver[] = []; + +beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), 'os-24960-')); + filename = join(dir, 'archive.db'); + writeFileSync(filename, PLAIN_TEXT); + bytes = readFileSync(filename); +}); + +afterEach(async () => { + vi.restoreAllMocks(); + vi.unstubAllEnvs(); + await Promise.all(opened.splice(0).map((r) => r.driver?.disconnect?.().catch(() => {}))); + rmSync(dir, { recursive: true, force: true }); +}); + +/** `promise`'s rejection; a failure naming the engine when it resolves instead. */ +async function rejectionOf(promise: Promise): Promise { + let resolved: ResolvedSqliteDriver; + try { + resolved = await promise; + } catch (error) { + return error; + } + opened.push(resolved); + throw new Error(`resolved to engine '${resolved.engine}' for a file that is not a SQLite database`); +} + +/** The driver's refusal of the file: its own error name, SQLite's code, and the file. */ +function expectNotADatabaseRefusal(error: any): void { + expect(error?.name).toBe('SqliteDatabaseFileError'); + expect(error?.code).toBe('SQLITE_NOTADB'); + expect(error?.filename).toBe(filename); + expect(String(error?.message)).toContain(filename); +} + +/** The operator's file is exactly as it was, and nothing was written beside it. */ +function expectFileUntouched(): void { + expect(readFileSync(filename).equals(bytes)).toBe(true); + expect(readdirSync(dir)).toEqual(['archive.db']); +} + +describe('resolveSqliteDriver in development: a file that is not a SQLite database (#24960)', () => { + it('rejects with the driver\'s refusal, never wasm or memory, and leaves the file as it was', async () => { + const warn = vi.fn(); + expectNotADatabaseRefusal(await rejectionOf(resolveSqliteDriver({ filename, dev: true, warn }))); + // No step-down notice: the "native better-sqlite3 unavailable" warning + // named the wrong cause, and no rung was tried. + expect(warn).not.toHaveBeenCalled(); + expectFileUntouched(); + }); +}); + +describe('resolveSqliteDriver in production: a file that is not a SQLite database (#24960)', () => { + it('returns native unprobed, and its connect() refuses the file', async () => { + const warn = vi.fn(); + const resolved = await resolveSqliteDriver({ filename, dev: false, warn }); + opened.push(resolved); + expect(resolved.engine).toBe('better-sqlite3'); + let refusal: unknown; + await resolved.driver.connect().catch((e: unknown) => { refusal = e; }); + expectNotADatabaseRefusal(refusal); + expect(warn).not.toHaveBeenCalled(); + expectFileUntouched(); + }); +}); + +describe('DatasourceConnectionService: a declared SQLite datasource on a file that is not a database (#24960)', () => { + for (const mode of ['development', 'production'] as const) { + it(`${mode}: reports a failed connection state naming the file, registers no driver, and leaves the file as it was`, async () => { + vi.stubEnv('NODE_ENV', mode); + const registered: string[] = []; + const engine = { + registerDriver: (driver: { name?: string }) => { registered.push(String(driver?.name)); }, + getDriverByName: (name: string) => (registered.includes(name) ? {} : undefined), + }; + const service = new DatasourceConnectionService({ + factory: () => createDefaultDatasourceDriverFactory({ dev: mode === 'development' }), + engine: () => engine as any, + }); + + await service.connectDeclared({ + datasources: [{ name: 'archive', driver: 'sqlite', config: { filename }, autoConnect: true }], + }); + + const state = service.getConnectionState('archive'); + expect(state?.availability).toBe('failed'); + expect(state?.reason).toContain(filename); + expect(registered).toEqual([]); + expectFileUntouched(); + }); + } +}); From 69f54e9130bebfc69d274ae74b10fb27e50a250f Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Oct 2026 00:57:33 +0000 Subject: [PATCH 3/3] fix(driver-sql): grade the SQLITE_NOTADB changeset minor, as a declared narrowing ships in the launch window MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The PR declares Clause-② as no (narrowing), which the Check Changeset level axis reads as a breaking change. A breaking change takes minor during the launch window, so both packages move from patch to minor and the banner sentence states that level and where the breaking-ness is carried. Claude-Session: https://claude.ai/code/session_01QK6pdbxKZiHdFApL8RYVh9 Co-authored-by: Claude --- .changeset/24960-sqlite-notadb-connect.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.changeset/24960-sqlite-notadb-connect.md b/.changeset/24960-sqlite-notadb-connect.md index ef65f8d8e4e..ebee61af4ff 100644 --- a/.changeset/24960-sqlite-notadb-connect.md +++ b/.changeset/24960-sqlite-notadb-connect.md @@ -1,6 +1,6 @@ --- -'@objectstack/driver-sql': patch -'@objectstack/service-datasource': patch +'@objectstack/driver-sql': minor +'@objectstack/service-datasource': minor --- fix(driver-sql): a SQLite file whose bytes are not a SQLite database fails `connect()` at once, naming the file, and a development boot no longer moves it aside and replaces it with an empty database (#24960) @@ -13,7 +13,7 @@ Clause-②: no (narrowing) -**BREAKING** (`SqlDriver.connect()`'s accept set narrows): a connect that resolved over a SQLite file that is not a database now rejects. It ships as `patch`: no public surface changes, and during the launch window breaking-ness is carried by this banner and the disposition above, not by the level. +**BREAKING** (`SqlDriver.connect()`'s accept set narrows): a connect that resolved over a SQLite file that is not a database now rejects. It ships as `minor` under the launch-window convention for accept-set narrowings: during the launch window a breaking change does not take `major`, so the level does not say it breaks, and breaking-ness is carried by this banner and the disposition above. - **Before:** `connect()` resolved; in production the first query failed with `DATABASE_ERROR`, and on a development boot the file was renamed to `.corrupt-` and an empty database was written in its place. - **Now:** `connect()` rejects with `SqliteDatabaseFileError`, `code: 'SQLITE_NOTADB'`, naming the file; the file is untouched.