Skip to content

Commit 4720e66

Browse files
committed
fix: address code review findings — readonly bug, transaction wrapping, tests
Fixes from 6-model consensus code review: 1. **CRITICAL** — SQLite driver `readonly` + `create: true` bug: gate `create` and WAL pragma on `!readonly` so readonly connections don't silently open read-write or crash on PRAGMA WAL. 2. **MAJOR** — Wrap `indexWarehouse` inserts in `db.transaction()` per-table to avoid per-statement disk fsyncs (~200x slowdown for large warehouses). 3. **MAJOR** — Fix no-op parent directory test (was creating dir before testing). Add 3 readonly connection tests (read existing, reject writes, refuse create). 4. **MINOR** — Extend `idx_columns_table` covering index to include `column_name` for `listColumns()` ORDER BY.
1 parent e63d963 commit 4720e66

3 files changed

Lines changed: 83 additions & 11 deletions

File tree

packages/drivers/src/sqlite.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,14 @@ export async function connect(config: ConnectionConfig): Promise<Connector> {
1212

1313
return {
1414
async connect() {
15+
const isReadonly = config.readonly === true
1516
db = new Database(dbPath, {
16-
readonly: config.readonly === true,
17-
create: true,
17+
readonly: isReadonly,
18+
create: !isReadonly,
1819
})
19-
db.exec("PRAGMA journal_mode = WAL")
20+
if (!isReadonly) {
21+
db.exec("PRAGMA journal_mode = WAL")
22+
}
2023
},
2124

2225
async execute(sql: string, limit?: number, _binds?: any[]): Promise<ConnectorResult> {

packages/opencode/src/altimate/native/schema/cache.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ CREATE INDEX IF NOT EXISTS idx_tables_search ON tables_cache(search_text);
6666
CREATE INDEX IF NOT EXISTS idx_columns_search ON columns_cache(search_text);
6767
CREATE INDEX IF NOT EXISTS idx_tables_warehouse ON tables_cache(warehouse);
6868
CREATE INDEX IF NOT EXISTS idx_columns_warehouse ON columns_cache(warehouse);
69-
CREATE INDEX IF NOT EXISTS idx_columns_table ON columns_cache(warehouse, schema_name, table_name);
69+
CREATE INDEX IF NOT EXISTS idx_columns_table ON columns_cache(warehouse, schema_name, table_name, column_name);
7070
`
7171

7272
// ---------------------------------------------------------------------------
@@ -177,6 +177,18 @@ export class SchemaCache {
177177
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
178178
)
179179

180+
// Batch inserts per-table inside a transaction to avoid per-statement disk fsyncs.
181+
// The async connector calls (listTables, describeTable) run outside the transaction;
182+
// only the synchronous SQLite inserts are wrapped.
183+
const insertTableBatch = this.db.transaction(
184+
(tableArgs: any[], columnArgsBatch: any[][]) => {
185+
insertTable.run(...tableArgs)
186+
for (const colArgs of columnArgsBatch) {
187+
insertColumn.run(...colArgs)
188+
}
189+
},
190+
)
191+
180192
for (const schemaName of schemas) {
181193
if (schemaName.toUpperCase() === "INFORMATION_SCHEMA") continue
182194
totalSchemas++
@@ -191,27 +203,32 @@ export class SchemaCache {
191203
for (const tableInfo of tables) {
192204
totalTables++
193205
const searchText = makeSearchText(databaseName, schemaName, tableInfo.name, tableInfo.type)
194-
insertTable.run(
195-
warehouseName, databaseName, schemaName, tableInfo.name, tableInfo.type, searchText,
196-
)
197206

198207
let columns: Array<{ name: string; data_type: string; nullable: boolean }> = []
199208
try {
200209
columns = await connector.describeTable(schemaName, tableInfo.name)
201210
} catch {
202-
continue
211+
// continue with empty columns
203212
}
204213

214+
// Build column insert args
215+
const columnArgsBatch: any[][] = []
205216
for (const col of columns) {
206217
totalColumns++
207218
const colSearch = makeSearchText(
208219
databaseName, schemaName, tableInfo.name, col.name, col.data_type,
209220
)
210-
insertColumn.run(
221+
columnArgsBatch.push([
211222
warehouseName, databaseName, schemaName, tableInfo.name,
212223
col.name, col.data_type, col.nullable ? 1 : 0, colSearch,
213-
)
224+
])
214225
}
226+
227+
// Insert table + all its columns in a single transaction
228+
insertTableBatch(
229+
[warehouseName, databaseName, schemaName, tableInfo.name, tableInfo.type, searchText],
230+
columnArgsBatch,
231+
)
215232
}
216233
}
217234

packages/opencode/test/altimate/schema-cache.test.ts

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -605,7 +605,7 @@ describe("file-based cache persistence", () => {
605605
cache2.close()
606606
})
607607

608-
test("creates parent directory if it doesn't exist", () => {
608+
test("opens existing DB file at a custom path", () => {
609609
const nestedPath = join(tmpDir, "deep", "nested", "dir", "cache.db")
610610
mkdirSync(join(tmpDir, "deep", "nested", "dir"), { recursive: true })
611611
const cache = SchemaCache.create(nestedPath)
@@ -680,3 +680,55 @@ describe("SQLite driver PRAGMA handling", () => {
680680
await connector.close()
681681
})
682682
})
683+
684+
// ---------------------------------------------------------------------------
685+
// 13. SQLite driver — readonly connection handling
686+
// ---------------------------------------------------------------------------
687+
688+
describe("SQLite driver readonly connections", () => {
689+
test("readonly connection can read existing database", async () => {
690+
const { connect } = await import("@altimateai/drivers/sqlite")
691+
const dbPath = join(tmpDir, "readonly-test.db")
692+
693+
// Create a database with data first
694+
const writer = await connect({ type: "sqlite", path: dbPath })
695+
await writer.connect()
696+
await writer.execute("CREATE TABLE items (id INTEGER, name TEXT)")
697+
await writer.execute("INSERT INTO items VALUES (1, 'test')")
698+
await writer.close()
699+
700+
// Open readonly and verify reads work
701+
const reader = await connect({ type: "sqlite", path: dbPath, readonly: true })
702+
await reader.connect()
703+
const result = await reader.execute("SELECT * FROM items")
704+
expect(result.rows).toEqual([[1, "test"]])
705+
await reader.close()
706+
})
707+
708+
test("readonly connection rejects writes", async () => {
709+
const { connect } = await import("@altimateai/drivers/sqlite")
710+
const dbPath = join(tmpDir, "readonly-write-test.db")
711+
712+
// Create a database first
713+
const writer = await connect({ type: "sqlite", path: dbPath })
714+
await writer.connect()
715+
await writer.execute("CREATE TABLE items (id INTEGER)")
716+
await writer.close()
717+
718+
// Open readonly and verify writes fail
719+
const reader = await connect({ type: "sqlite", path: dbPath, readonly: true })
720+
await reader.connect()
721+
expect(() => reader.execute("INSERT INTO items VALUES (1)")).toThrow()
722+
await reader.close()
723+
})
724+
725+
test("readonly connection does not create nonexistent file", async () => {
726+
const { connect } = await import("@altimateai/drivers/sqlite")
727+
const dbPath = join(tmpDir, "ghost-file.db")
728+
729+
const reader = await connect({ type: "sqlite", path: dbPath, readonly: true })
730+
// Should throw because the file doesn't exist and create=false
731+
expect(() => reader.connect()).toThrow()
732+
expect(existsSync(dbPath)).toBe(false)
733+
})
734+
})

0 commit comments

Comments
 (0)