From 938e375a9a1724288d74f9d61026a2c23afb979a Mon Sep 17 00:00:00 2001 From: Kenny Daniel Date: Fri, 29 May 2026 16:11:29 -0700 Subject: [PATCH] Simplify SQL error messages --- src/core/query/sql.js | 11 ++++----- test/core/query-sql-error.test.js | 37 +++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 7 deletions(-) create mode 100644 test/core/query-sql-error.test.js diff --git a/src/core/query/sql.js b/src/core/query/sql.js index fcd99f3..6a4ae11 100644 --- a/src/core/query/sql.js +++ b/src/core/query/sql.js @@ -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) diff --git a/test/core/query-sql-error.test.js b/test/core/query-sql-error.test.js new file mode 100644 index 0000000..28cd311 --- /dev/null +++ b/test/core/query-sql-error.test.js @@ -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/) +})