Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
1f464c5
feat(analyst): `sql_check predicate_effect` reports what each filter …
anandgupta42 Oct 12, 2026
52f0e5e
fix(analyst): `predicate_effect` reports unknown estimates, blank inp…
anandgupta42 Oct 12, 2026
25baadb
fix(analyst): `predicate_effect` accepts table-qualified predicates a…
anandgupta42 Oct 12, 2026
66236b5
fix(analyst): `predicate_effect` refuses backend-signalling functions…
anandgupta42 Oct 12, 2026
e9b84ed
Merge remote-tracking branch 'origin/feat/analyst-harness-dab-2026-10…
anandgupta42 Oct 12, 2026
3fa390c
fix(analyst): `predicate_effect` runs read-only and denies server-con…
anandgupta42 Oct 12, 2026
c566efc
fix(analyst): close the remaining write paths in `sql_check predicate…
anandgupta42 Oct 12, 2026
8e56751
fix(analyst): deny the advisory-unlock functions in sql_check predica…
anandgupta42 Oct 12, 2026
9fce7ba
merge: bring the harness base (feat/analyst-harness-dab-2026-10) into…
anandgupta42 Oct 13, 2026
4e0f5db
fix(analyst): refuse every dblink function and the WAL replay control…
anandgupta42 Oct 13, 2026
be91b03
fix(analyst): refuse the PostgreSQL WAL, backup, replication and serv…
anandgupta42 Oct 13, 2026
4a7c61e
fix(analyst): refuse every PostgreSQL replication function in read-on…
anandgupta42 Oct 13, 2026
2f824cf
fix(analyst): refuse the PostgreSQL statistics-reset functions in rea…
anandgupta42 Oct 13, 2026
c9cfe17
fix(analyst): refuse every PostgreSQL large-object write path in read…
anandgupta42 Oct 13, 2026
5a69583
fix(analyst): deny PostgreSQL control, large-object, dblink and check…
anandgupta42 Oct 13, 2026
9ae7d77
fix(analyst): DuckDB read-only guard uses txid_current, dynamic query…
anandgupta42 Oct 13, 2026
fa0f628
Merge remote-tracking branch 'origin/feat/analyst-harness-dab-2026-10…
anandgupta42 Oct 13, 2026
214e47e
fix(analyst): `sql_check` rethrows probe timeouts that lost local ses…
anandgupta42 Oct 13, 2026
0d41c01
Merge remote-tracking branch 'origin/feat/analyst-harness-dab-2026-10…
anandgupta42 Oct 13, 2026
b4953d4
Merge remote-tracking branch 'origin/feat/analyst-harness-dab-2026-10…
anandgupta42 Oct 13, 2026
d152ddd
fix(analyst): `predicate_effect` admits only built-in pure functions …
anandgupta42 Oct 14, 2026
5df7cfa
Merge remote-tracking branch 'origin/feat/analyst-harness-dab-2026-10…
anandgupta42 Oct 14, 2026
bc5c347
fix(analyst): `predicate_effect` resolves functions and operators aga…
anandgupta42 Oct 14, 2026
0ff8f08
Merge remote-tracking branch 'origin/feat/analyst-harness-dab-2026-10…
anandgupta42 Oct 14, 2026
7db53c1
fix(analyst): `predicate_effect` checks explicit cast targets against…
anandgupta42 Oct 14, 2026
902ac47
Merge remote-tracking branch 'origin/feat/analyst-harness-dab-2026-10…
anandgupta42 Oct 14, 2026
defa2ba
fix(analyst): `predicate_effect` authorizes the combined probe and ac…
anandgupta42 Oct 14, 2026
388d088
Merge remote-tracking branch 'origin/feat/analyst-harness-dab-2026-10…
anandgupta42 Oct 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/drivers/src/duckdb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,11 +166,12 @@ export async function connect(
params: any[] | undefined,
timeoutMs: number | undefined,
maxResultBytes?: number,
readOnly?: boolean,
onDispatch?: () => void,
): Promise<any[]> {
if (!shared) throw new Error("DuckDB connection not open")
try {
return await shared.engine.call<any[]>("all", { sql, params, connectionId, maxResultBytes }, timeoutMs, onDispatch)
return await shared.engine.call<any[]>("all", { sql, params, connectionId, maxResultBytes, readOnly }, timeoutMs, onDispatch)
} catch (err) {
throw err instanceof Error ? wrapDuckDBError(err) : err
}
Expand Down Expand Up @@ -321,6 +322,7 @@ export async function connect(
binds?.length ? binds : undefined,
resolveQueryTimeout(config, options).ms,
options?.maxResultBytes,
options?.readOnly,
options?.onDispatch,
)
// altimate_change end
Expand Down
35 changes: 33 additions & 2 deletions packages/drivers/src/local-engine-child-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,10 +222,41 @@ async function close() {
return null;
}

// Guard setup, result consumption and cleanup share one serialized request.
// Separate requests would let another caller run between setup and cleanup.
async function withReadOnly(p, execute) {
if (engine === "sqlite") {
const previous = sqliteDb.query("PRAGMA query_only").get().query_only;
sqliteDb.exec("PRAGMA query_only = ON");
try {
return await execute();
} finally {
sqliteDb.exec("PRAGMA query_only = " + (previous ? "ON" : "OFF"));
}
}
const control = (sql) => all({ sql, connectionId: p.connectionId });
// The legacy binding has no autocommit getter. Implicit transactions receive
// different IDs; an explicit transaction keeps the same ID across statements.
// Refuse before BEGIN: a nested BEGIN would abort the caller's transaction.
const first = await control("SELECT txid_current() AS id");
const second = await control("SELECT txid_current() AS id");
if (first[0].id === second[0].id) {
throw new Error("DuckDB read-only execution cannot run inside an active transaction; finish it before running the probe.");
}
await control("BEGIN TRANSACTION READ ONLY");
try {
return await execute();
} finally {
await control("ROLLBACK");
}
}

async function handle(req) {
if (req.op === "open") return open(req.payload);
if (req.op === "all") return all(req.payload);
if (req.op === "run") return run(req.payload);
if (req.op === "all" || req.op === "run") {
const execute = () => req.op === "all" ? all(req.payload) : run(req.payload);
return req.payload.readOnly ? withReadOnly(req.payload, execute) : execute();
}
if (req.op === "disconnect") return disconnect(req.payload);
if (req.op === "close") return close();
throw new Error("unknown op " + req.op);
Expand Down
8 changes: 5 additions & 3 deletions packages/drivers/src/postgres.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export async function connect(config: ConnectionConfig): Promise<Connector> {
pool = new Pool(poolConfig)
},

async execute(sql: string, limit?: number, _binds?: any[], options?: ExecuteOptions): Promise<ConnectorResult> {
async execute(sql: string, limit?: number, binds?: any[], options?: ExecuteOptions): Promise<ConnectorResult> {
const client = await acquireClient(pool, options?.signal)
// Tighter of the explicit statement_timeout and the common execution
// deadline: a configured limit stricter than the default is preserved.
Expand Down Expand Up @@ -160,7 +160,7 @@ export async function connect(config: ConnectionConfig): Promise<Connector> {
const consume = createResultByteBudget(options.maxResultBytes)
const Query = pg.default?.Query ?? pg.Query
return await new Promise<ConnectorResult>((resolve, reject) => {
const stream = new Query({ text: query, rowMode: "array" })
const stream = new Query({ text: query, values: binds, rowMode: "array" })
const rows: unknown[][] = []
let columns: string[] = []
let truncated = false
Expand Down Expand Up @@ -197,7 +197,9 @@ export async function connect(config: ConnectionConfig): Promise<Connector> {
})
}
// altimate_change end
const result = await client.query(query)
// altimate_change start — preserve bound catalog names and other query parameters
const result = await client.query(query, binds)
// altimate_change end
const columns = result.fields?.map((f: any) => f.name) ?? []
const truncated = effectiveLimit > 0 && result.rows.length > effectiveLimit
const rows = truncated
Expand Down
12 changes: 10 additions & 2 deletions packages/drivers/src/sqlite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,11 @@ export async function connect(config: ConnectionConfig): Promise<Connector> {

if (!isSelect) {
// Non-SELECT statements (INSERT, UPDATE, DELETE, CREATE, etc.)
const info = await engine.call<{ changes: number; lastInsertRowid: number | bigint }>("run", { sql }, timeoutMs)
const info = await engine.call<{ changes: number; lastInsertRowid: number | bigint }>(
"run",
{ sql, readOnly: options?.readOnly },
timeoutMs,
)
return {
columns: ["changes", "lastInsertRowid"],
rows: [[info.changes, info.lastInsertRowid]],
Expand All @@ -84,7 +88,11 @@ export async function connect(config: ConnectionConfig): Promise<Connector> {
}

// altimate_change start — forward the budget before the helper collects rows
const rows = await engine.call<any[]>("all", { sql: query, maxResultBytes: options?.maxResultBytes }, timeoutMs)
const rows = await engine.call<any[]>(
"all",
{ sql: query, maxResultBytes: options?.maxResultBytes, readOnly: options?.readOnly },
timeoutMs,
)
// altimate_change end
const columns = rows.length > 0 ? Object.keys(rows[0]) : []
const truncated = effectiveLimit > 0 && rows.length > effectiveLimit
Expand Down
2 changes: 1 addition & 1 deletion packages/drivers/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export interface ExecuteOptions {
maxResultBytes?: number
// altimate_change end
// altimate_change start — PostgreSQL source extraction uses a pinned, rolled-back transaction
/** PostgreSQL only: execute in a READ ONLY transaction and roll it back before releasing the client. */
/** PostgreSQL/DuckDB: rolled-back READ ONLY transaction; SQLite: scoped query_only pragma. */
readOnly?: boolean
// altimate_change end
/** PostgreSQL only: abandon pool acquisition and prevent SQL from starting after cancellation. */
Expand Down
59 changes: 59 additions & 0 deletions packages/drivers/test/postgres-readonly.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect, mock, test } from "bun:test"
import { EventEmitter } from "node:events"
import { connect } from "../src/postgres"
import { QueryTimeoutError } from "../src/types"

Expand Down Expand Up @@ -183,6 +184,64 @@ test("Postgres never starts user SQL when cancellation arrives during session se
}
})

for (const maxResultBytes of [undefined, 1024]) {
test(`Postgres forwards bound catalog arrays in ${maxResultBytes === undefined ? "regular" : "byte-limited"} read-only queries`, async () => {
const calls: { sql: string; values?: unknown[] }[] = []
const releases: unknown[] = []
class Query extends EventEmitter {
constructor(readonly config: { text: string; values?: unknown[]; rowMode: string }) {
super()
}
}
mock.module("pg", () => ({
default: {
Query,
Pool: class {
async connect() {
return {
query(query: string | Query, values?: unknown[]) {
const sql = typeof query === "string" ? query : query.config.text
calls.push({ sql, values: typeof query === "string" ? values : query.config.values })
const fields = [{ name: "proname" }]
if (query instanceof Query) {
queueMicrotask(() => {
query.emit("row", ["lower"], { fields })
query.emit("end", { fields })
})
return query
}
return Promise.resolve({ fields, rows: [{ proname: "lower" }] })
},
release(discard?: boolean) { releases.push(discard) },
}
}
async end() {}
},
},
}))
const connector = await connect({ type: "postgres", query_timeout_ms: 0 })
await connector.connect()
const sql = "SELECT proname FROM pg_catalog.pg_proc WHERE proname = ANY($1)"
const values = [["lower", "upper"]]
try {
const result = await connector.execute(sql, undefined, values, {
noLimit: true,
readOnly: true,
maxResultBytes,
})
expect(result.rows).toEqual([["lower"]])
expect(calls).toEqual([
{ sql: "BEGIN READ ONLY", values: undefined },
{ sql, values },
{ sql: "ROLLBACK", values: undefined },
])
expect(releases).toEqual([undefined])
} finally {
await connector.close()
}
})
}

async function timeoutDatabase() {
const queries: string[] = []
let statementTimeout = "1s"
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/altimate/prompts/analyst.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
You are altimate-code: a read-only analyst. Do not modify source data or files. When available, `sql_materialize` may write session-local `scratch` tables.

Inspect connections, schemas and actual rows before joining or parsing; key formats may differ across databases. Establish population, grain, denominator and assumptions. Every clause of the question must bite: a filter that removes nothing usually means the wrong column. When a phrase admits two readings (which population, denominator, transform or owner column), compute both, then choose by the question's own definitions and the data's shape; keep that reasoning out of the final message. Use `sql_check` for keys, fan-out, grain, nulls or extraction coverage when they affect the answer; trivial queries need no ritual. `sql_analyze` is optional.
Inspect connections, schemas and actual rows before joining or parsing; key formats may differ across databases. Establish population, grain, denominator and assumptions. Every clause of the question must bite: a filter that removes nothing usually means the wrong column. (`sql_check` `predicate_effect` reports what each filter removes; a filter that removes nothing at the right population is a reason to profile an alternative column, not to invent an exclusion). When a phrase admits two readings (which population, denominator, transform or owner column), compute both, then choose by the question's own definitions and the data's shape; keep that reasoning out of the final message. Use `sql_check` for keys, fan-out, grain, nulls or extraction coverage when they affect the answer; trivial queries need no ritual. `sql_analyze` is optional.

Derive large intermediate sets in-engine. For a cross-engine join, move the smaller side: a few hundred keys go into an IN list or VALUES against the big table; materialize into `scratch` only the side that is small after filtering, never a multi-million-row table when a filter or aggregate could shrink it first. Preserve requested weighting: group means weighted by contributing counts can be exact; unweighted averages change the weighting. For many similar tables, prefer catalog-driven SQL (information_schema, DuckDB query_table/UNION ALL BY NAME) after confirming dialect support.

Expand Down
Loading
Loading