Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
a3a7096
fix(analyst): report tool failures as failures, accept valid read-onl…
anandgupta42 Oct 12, 2026
0379b21
fix(tool): keep bash results ordinary, finish on exit, and drop marke…
anandgupta42 Oct 12, 2026
2ae497f
fix(drivers,tool): Mongo count keeps its filter, shorthand conflicts …
anandgupta42 Oct 12, 2026
9115eab
Merge remote-tracking branch 'origin/feat/analyst-harness-dab-2026-10…
anandgupta42 Oct 12, 2026
2bfd6bb
fix(analyst): make `sql_execute full_text_columns` tolerant of strict…
anandgupta42 Oct 13, 2026
425e6e2
fix(drivers): refuse the ambiguous Mongo `distinct` shorthand instead…
anandgupta42 Oct 13, 2026
965f97a
fix(analyst): keep `full_text_columns` output bounded and always reco…
anandgupta42 Oct 13, 2026
1453824
fix(analyst): bound spill memory per row and keep the routing notice …
anandgupta42 Oct 13, 2026
b2973c2
fix(read): bound the character window to the byte budget before slicing
anandgupta42 Oct 13, 2026
6b9533b
merge: bring the harness base (feat/analyst-harness-dab-2026-10) into…
anandgupta42 Oct 13, 2026
420275f
Merge remote-tracking branch 'origin/feat/analyst-harness-dab-2026-10…
anandgupta42 Oct 13, 2026
b8d4968
Merge remote-tracking branch 'origin/feat/analyst-harness-dab-2026-10…
anandgupta42 Oct 13, 2026
2e0fa65
Merge remote-tracking branch 'origin/feat/analyst-harness-dab-2026-10…
anandgupta42 Oct 13, 2026
1bd447f
fix(analyst): prioritized full-text cells use the shared cell rendering
anandgupta42 Oct 14, 2026
f0ba3a0
Merge remote-tracking branch 'origin/feat/analyst-harness-dab-2026-10…
anandgupta42 Oct 14, 2026
4acbfc6
Merge remote-tracking branch 'origin/feat/analyst-harness-dab-2026-10…
anandgupta42 Oct 14, 2026
2ec46a1
Merge remote-tracking branch 'origin/feat/analyst-harness-dab-2026-10…
anandgupta42 Oct 14, 2026
111946b
Merge remote-tracking branch 'origin/feat/analyst-harness-dab-2026-10…
anandgupta42 Oct 14, 2026
ef59c60
Merge remote-tracking branch 'origin/feat/analyst-harness-dab-2026-10…
anandgupta42 Oct 14, 2026
542bdf4
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
63 changes: 59 additions & 4 deletions packages/drivers/src/mongodb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ import type { ConnectionConfig, Connector, ConnectorResult, ExecuteOptions, Sche
import { QueryTimeoutError } from "./types"
import { resolveQueryTimeout } from "./query-timeout"
import { loadOptionalDriver } from "./resolve"
// altimate_change start — detect conflicting shorthand arguments
import { isDeepStrictEqual } from "node:util"
// altimate_change end
// altimate_change start — cap extraction before cursor results accumulate
import { createResultByteBudget } from "./result-byte-limit"
// altimate_change end
Expand Down Expand Up @@ -75,13 +78,59 @@ function validateMqlBounds(parsed: Record<string, unknown>): void {
}
}

/** Accept the native `{"<command>": "<collection>"}` shape alongside `{command, collection}`. */
// altimate_change start — accept collection-based read command shorthand
/** Accept native command documents and collection-based command arguments; validate bounds on every accepted shape. */
export function normalizeMqlCommand<T extends Record<string, unknown>>(parsed: T): T & MqlQuery {
if (!parsed || typeof parsed !== "object") return parsed as T & MqlQuery
if (typeof parsed["command"] === "string") {
validateMqlBounds(parsed)
return parsed as T & MqlQuery
if (parsed["command"] !== "count") {
validateMqlBounds(parsed)
return parsed as T & MqlQuery
}
const normalized: Record<string, unknown> = { ...parsed, command: "countDocuments" }
if (normalized["filter"] === undefined) normalized["filter"] = normalized["query"]
validateMqlBounds(normalized)
return normalized as T & MqlQuery
}
if (typeof parsed["collection"] === "string" && parsed["collection"]) {
const keys = Object.keys(NATIVE_COMMAND_KEYS).filter((key) => Object.hasOwn(parsed, key))
// Refuse ambiguous shapes rather than silently choosing a different command.
if (keys.length !== 1) return parsed as T & MqlQuery
const key = keys[0]!
const value = parsed[key]
const command = NATIVE_COMMAND_KEYS[key]!
const object = value !== null && typeof value === "object" && !Array.isArray(value)
const argument =
(command === "find" || command === "countDocuments") && object
? "filter"
: command === "aggregate" && Array.isArray(value)
? "pipeline"
: command === "distinct" && typeof value === "string"
? "field"
: undefined
if (argument) {
const explicitArguments =
command === "countDocuments" ? [argument, "query"] : command === "distinct" ? [argument, "key"] : [argument]
for (const explicit of explicitArguments) {
if (parsed[explicit] !== undefined && !isDeepStrictEqual(parsed[explicit], value)) {
if (command === "distinct") {
throw new Error(
`Ambiguous distinct form: 'distinct' value ${JSON.stringify(value)} conflicts with explicit '${explicit}' value ${JSON.stringify(parsed[explicit])}`,
)
}
throw new Error(`Ambiguous MQL command: '${key}' conflicts with explicit '${explicit}' argument`)
Comment thread
anandgupta42 marked this conversation as resolved.
}
Comment thread
anandgupta42 marked this conversation as resolved.
}
const { [key]: _, ...rest } = parsed
const normalized: Record<string, unknown> = { ...rest, command }
if (normalized[argument] === undefined) normalized[argument] = value
if (command === "distinct" && normalized["filter"] === undefined) normalized["filter"] = normalized["query"]
validateMqlBounds(normalized)
return normalized as T & MqlQuery
}
}
Comment thread
anandgupta42 marked this conversation as resolved.
// A collection-bearing document is shorthand, even when its arguments are invalid.
if (Object.hasOwn(parsed, "collection")) return parsed as T & MqlQuery
for (const [key, command] of Object.entries(NATIVE_COMMAND_KEYS)) {
const value = parsed[key]
if (typeof value !== "string" || !value) continue
Expand All @@ -101,6 +150,8 @@ export function normalizeMqlCommand<T extends Record<string, unknown>>(parsed: T
return parsed as T & MqlQuery
}

// altimate_change end

const MONGO_WRITE_STAGES = new Set(["$out", "$merge"])

/** Detect write stages in this pipeline, nested pipeline arrays, and $facet branches. */
Expand Down Expand Up @@ -407,7 +458,11 @@ export async function connect(config: ConnectionConfig): Promise<Connector> {
const parsed = normalizeMqlCommand(raw)

if (!parsed.command) {
throw new Error("MQL query must include a 'command' field")
// altimate_change start — explain all accepted MQL command shapes
throw new Error(
"MQL query must include a 'command' field ({command, collection}), use a native command document (e.g. {find: 'users'}), or use collection shorthand ({collection, find|aggregate|count|distinct})",
)
// altimate_change end
}

const db = resolveDb(parsed.database)
Expand Down
123 changes: 118 additions & 5 deletions packages/drivers/test/mongodb-unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { normalizeMqlCommand } from "../src/mongodb"
// Use the driver's BSON package directly so the MongoDB client mock cannot replace Binary.
const { Binary } = createRequire(import.meta.resolve("mongodb"))("bson")

async function fakeMongo(docs: Record<string, unknown>[] = []) {
// `docs` undefined keeps the fixed count of 11 that the count tests expect; an array drives find/aggregate/count results.
async function fakeMongo(docs?: Record<string, unknown>[]) {
const finds: unknown[] = []
const pipelines: Record<string, unknown>[][] = []
const aggregateOptions: unknown[] = []
Expand All @@ -21,16 +22,16 @@ async function fakeMongo(docs: Record<string, unknown>[] = []) {
collection: () => ({
find: (filter: unknown) => {
finds.push(filter)
return { limit: () => ({ toArray: async () => docs }) }
return { limit: () => ({ toArray: async () => docs ?? [] }) }
},
aggregate: (pipeline: Record<string, unknown>[], options: unknown) => {
pipelines.push(pipeline)
aggregateOptions.push(options)
return { toArray: async () => docs }
return { toArray: async () => docs ?? [] }
},
countDocuments: async (filter: unknown, options: unknown) => {
countDocuments: async (filter: Record<string, unknown>, options: unknown) => {
counts.push({ filter, options })
return 11
return docs?.filter((doc) => Object.entries(filter).every(([key, value]) => doc[key] === value)).length ?? 11
},
distinct: async (field: string, filter: unknown, options: unknown) => {
distincts.push({ field, filter, options })
Expand Down Expand Up @@ -277,6 +278,18 @@ describe("MongoDB aggregate write protection", () => {
})

describe("MongoDB native command filters", () => {
test("count command alias counts only documents matching the query filter", async () => {
const { connector, counts } = await fakeMongo([{ status: "A" }, { status: "B" }, { status: "A" }])
const result = await connector.execute(
JSON.stringify({ command: "count", collection: "orders", query: { status: "A" } }),
1,
undefined,
{ queryTimeoutMs: 0 },
)
expect(result.rows).toEqual([[2]])
expect(counts).toEqual([{ filter: { status: "A" }, options: {} }])
})

test("count forwards the native query filter", async () => {
const { connector, counts } = await fakeMongo()
await connector.execute(JSON.stringify({ count: "orders", query: { status: "A" } }), 1, undefined, { queryTimeoutMs: 0 })
Expand Down Expand Up @@ -310,3 +323,103 @@ describe("MongoDB native command filters", () => {
expect(counts).toEqual([{ filter: { status: "B" }, options: {} }])
})
})

describe("MongoDB collection command shorthand", () => {
test.each([
{ name: "key", aliases: { key: "status" } },
{ name: "field", aliases: { field: "status" } },
{ name: "field with an agreeing key", aliases: { key: "category", field: "status" } },
{ name: "key with an agreeing field", aliases: { key: "status", field: "category" } },
])("refuses ambiguous distinct form with conflicting $name and names both values", ({ aliases }) => {
expect(() => normalizeMqlCommand({ collection: "items", distinct: "category", ...aliases })).toThrow(
/ambiguous distinct form.*category.*status/i,
)
})

test("normalizes distinct-as-field shorthand without changing the collection", () => {
expect(normalizeMqlCommand({ collection: "items", distinct: "category" })).toEqual({
collection: "items",
command: "distinct",
field: "category",
})
})

test.each([
{ name: "key", aliases: { key: "category" } },
{ name: "field", aliases: { field: "category" } },
{ name: "key and field", aliases: { key: "category", field: "category" } },
])("accepts distinct shorthand with agreeing $name", ({ aliases }) => {
const normalized = normalizeMqlCommand({ collection: "items", distinct: "category", ...aliases })
expect(normalized).toMatchObject({ collection: "items", command: "distinct", field: "category" })
expect(normalized).not.toHaveProperty("distinct")
})

test.each([
{ collection: "items", find: "other" },
{ collection: "", distinct: "category", key: "category" },
{ collection: null, distinct: "category", key: "category" },
])("does not fall through to native parsing when collection is present: %j", (input) => {
expect(normalizeMqlCommand(input)).toEqual(input)
})

test.each([
{ name: "find and filter", input: { collection: "items", find: { status: "x" }, filter: { status: "y" } } },
{ name: "count and query", input: { collection: "items", count: { status: "x" }, query: { status: "y" } } },
{ name: "distinct and field", input: { collection: "items", distinct: "status", field: "category" } },
])("refuses conflicting $name arguments", ({ input }) => {
expect(() => normalizeMqlCommand(input)).toThrow(/ambiguous.*conflict/i)
})

test("accepts equivalent embedded and explicit filters regardless of key order", () => {
expect(normalizeMqlCommand({
collection: "items",
find: { status: "x", active: true },
filter: { active: true, status: "x" },
})).toEqual({
collection: "items",
command: "find",
filter: { active: true, status: "x" },
})
})

test("normalizes a collection with an embedded find filter and projection", () => {
const command = {
collection: "articles",
find: {},
projection: { _id: 0, article_id: 1, title: 1, description: 1 },
}
expect(normalizeMqlCommand(command)).toEqual({
collection: "articles",
command: "find",
filter: {},
projection: command.projection,
})
})

test.each([
[
{ collection: "items", aggregate: [{ $match: { active: true } }] },
{ collection: "items", command: "aggregate", pipeline: [{ $match: { active: true } }] },
],
[
{ collection: "items", count: { active: true } },
{ collection: "items", command: "countDocuments", filter: { active: true } },
],
[
{ collection: "items", distinct: "category", query: { active: true } },
{
collection: "items",
command: "distinct",
field: "category",
query: { active: true },
filter: { active: true },
},
],
[
{ command: "count", collection: "items", filter: {} },
{ command: "countDocuments", collection: "items", filter: {} },
],
])("normalizes supported collection command keys: %j", (input, expected) => {
expect(normalizeMqlCommand(input)).toEqual(expected)
})
})
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. 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. When a rule or policy decides the answer, read its full text (not a preview), enumerate each competing clause, and evaluate every condition as satisfied, violated or unknown against the row's own fields and dates before choosing. 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
66 changes: 61 additions & 5 deletions packages/opencode/src/altimate/tools/sql-classify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,67 @@ try {
// Only SELECT queries are known safe. "other" (SHOW, SET, USE, etc.) is ambiguous — prompt for permission.
const READ_CATEGORIES = new Set(["query"])

/** Adapt read/write-equivalent syntax for the bundled parser, never for execution.
* Only code tokens change; quoted content and the original statement boundaries
* remain authoritative when checking the parser's result. */
function parserSql(sql: string, dialect?: string): string {
const duckdb = dialect?.toLowerCase() === "duckdb"
if (!duckdb && dialect?.toLowerCase() !== "sqlite") return sql
const compatible = tokenizeSql(sql, dialect).tokens
.map(({ kind, text }) => {
if (kind !== "code") return text
// The parser supports LIKE negation but not GLOB negation. Both are predicates.
text = text.replace(/\bNOT\s+GLOB\b/gi, "NOT LIKE")
if (!duckdb) return text
// SQLite uses / for integer operands; // is a DuckDB operator. PRIOR is
// an ordinary DuckDB identifier, not the parser's hierarchical operator.
return text.replaceAll("//", "/ ").replace(/\bprior(?=\s*\.)/gi, '"prior"')
})
.join("")
if (!duckdb) return compatible
// The native parser recognizes these query bodies inside a derived table,
// but reports their standalone form as UNKNOWN. Parse every body in full.
const statements = statementsOf(compatible, dialect)
const adapted = statements.map((statement) =>
/^\s*(PIVOT|UNPIVOT|WITH)\b/i.test(statement) && /\b(PIVOT|UNPIVOT)\b/i.test(maskSql(tokenizeSql(statement, dialect).tokens))
? `SELECT * FROM (${statement})`
: statement,
)
return adapted.some((statement, index) => statement !== statements[index]) ? adapted.join("; ") : compatible
}

/** Mask only UNPIVOT output labels at their own query depth, retaining SELECT INTO. */
function maskUnpivotLabels(sql: string): string {
const heads: string[] = []
const unpivots = new Set<number>()
let depth = 0
let previous = ""
return sql.replace(/\(|\)|\bINTO\s+NAME\b|\b[A-Z_][A-Z_0-9]*\b/gi, (token) => {
const word = token.toUpperCase()
if (word === "(") depth++
else if (word === ")") {
delete heads[depth]
unpivots.delete(depth--)
} else if (word === "UNPIVOT" && (!heads[depth] || (heads[depth] === "WITH" && previous === ")"))) {
unpivots.add(depth)
heads[depth] = word
} else if (/^INTO\s+NAME$/.test(word) && unpivots.delete(depth)) {
return " ".repeat(token.length)
} else if (!heads[depth] || (heads[depth] === "WITH" && /^(SELECT|VALUES|TABLE|PIVOT|INSERT|UPDATE|DELETE|MERGE)$/.test(word))) {
heads[depth] = word
}
previous = word
return token
})
}

// The parser labels SELECT INTO and queries with modifying CTEs as "query".
// Quoted identifiers/literals and comments cannot introduce these write clauses.
function hasQueryWriteClause(sql: string, dialect?: string): boolean {
return /\b(INTO|INSERT|UPDATE|DELETE|MERGE)\b/i.test(maskSql(tokenizeSql(sql, dialect).tokens))
let masked = maskSql(tokenizeSql(sql, dialect).tokens)
// UNPIVOT's INTO NAME labels output columns; it does not create a table.
if (dialect?.toLowerCase() === "duckdb") masked = maskUnpivotLabels(masked)
return /\b(INTO|INSERT|UPDATE|DELETE|MERGE)\b/i.test(masked)
}

const READ_ONLY_OTHER_PATTERN = /^\s*(SHOW\b[^;]*|DESCRIBE\b[^;]*|DESC\b[^;]*)$/i
Expand Down Expand Up @@ -135,7 +192,7 @@ function classifyFallback(sql: string, dialect?: string): { queryType: "read" |
export function isSingleQuery(sql: string, dialect?: string): boolean {
if (!getStatementTypes || !tokenizeSql(sql, dialect).complete) return false
try {
const result = getStatementTypes(sql, dialect)
const result = getStatementTypes(parserSql(sql, dialect), dialect)
return (
statementsOf(sql, dialect).length === 1 &&
result?.statement_count === 1 &&
Expand Down Expand Up @@ -173,7 +230,7 @@ export function classifyAndCheck(sql: string, dialect?: string): { queryType: "r
if (!sql || typeof sql !== "string") return { queryType: "read", blocked: false }
if (!getStatementTypes) return classifyFallback(sql, dialect)
try {
const result = getStatementTypes(sql, dialect)
const result = getStatementTypes(parserSql(sql, dialect), dialect)
if (!Array.isArray(result?.statements) || result.statement_count !== result.statements.length) {
return classifyFallback(sql, dialect)
}
Expand All @@ -192,8 +249,7 @@ export function classifyAndCheck(sql: string, dialect?: string): { queryType: "r
}
}

// MongoDB commands are JSON, not SQL: the SQL classifier reads every one as a write, which denied
// 52 of 52 Mongo reads for a read-only agent in one evaluation run. Classify them by command.
// MongoDB commands are JSON, not SQL. Classify their command shape independently.
const MONGO_READ_COMMANDS = new Set(["find", "aggregate", "countDocuments", "distinct", "listIndexes", "ping"])
const MONGO_BLOCKED_COMMANDS = new Set(["dropDatabase", "dropCollection"])

Expand Down
Loading
Loading