Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 4 additions & 7 deletions src/core/query/sql.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,10 @@ export async function executeQuerySql(args) {
try {
const trimmed = query.trim()
if (trimmed.length === 0) throw new Error('SQL query is required')
let statement
try {
statement = parseSql({ query: trimmed })
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
throw new Error(`SQL must be a single read-only SELECT statement: ${message}`)
}
// squirreling only parses read-only SELECTs, so its own error message
// already points at the real problem (syntax error, unknown function,
// non-SELECT statement). Surface it verbatim rather than wrapping it.
const statement = parseSql({ query: trimmed })

const tableNames = uniqueStrings(extractTables(statement))
span.setAttribute('table_count', tableNames.length)
Expand Down
37 changes: 37 additions & 0 deletions test/core/query-sql-error.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// @ts-check

import test from 'node:test'
import assert from 'node:assert/strict'

import { executeQuerySql } from '../../src/core/query/sql.js'

/** Minimal registry/storage stubs — parse failures fire before either is used. */
const registry = { getDataset: () => null, listDatasets: () => [] }
const storage = { cacheRoot: '/tmp/hypaware-test', pendingInfo: async () => ({ pending: false }) }

/** @param {string} query */
async function runExpectError(query) {
try {
await executeQuerySql({ query, registry: /** @type {any} */ (registry), storage: /** @type {any} */ (storage) })
} catch (err) {
return err instanceof Error ? err.message : String(err)
}
throw new Error(`expected ${JSON.stringify(query)} to throw`)
}

test('parse errors surface the squirreling message verbatim, unwrapped', async () => {
const message = await runExpectError('SELECT foo(1)')
assert.match(message, /Unknown function "foo"/)
assert.doesNotMatch(message, /single read-only SELECT/)
})

test('non-SELECT statements surface the parser message without extra framing', async () => {
const message = await runExpectError('INSERT INTO t VALUES (1)')
assert.match(message, /Expected SELECT but found "INSERT"/)
assert.doesNotMatch(message, /single read-only SELECT/)
})

test('empty SQL is reported as required', async () => {
const message = await runExpectError(' ')
assert.match(message, /SQL query is required/)
})