From 6cc7f7075d23a51e5061192b12567f26c3a80bc4 Mon Sep 17 00:00:00 2001 From: Emanuele Bardelli Date: Fri, 14 Nov 2025 07:02:25 -0800 Subject: [PATCH 1/3] Fix runtime --- packages/datasources/mssql/index.cjs | 49 +++++++++++++++--- packages/datasources/mssql/test/test.js | 68 ++++++++++++++++++++++++- 2 files changed, 107 insertions(+), 10 deletions(-) diff --git a/packages/datasources/mssql/index.cjs b/packages/datasources/mssql/index.cjs index 7360078080..fd56064495 100755 --- a/packages/datasources/mssql/index.cjs +++ b/packages/datasources/mssql/index.cjs @@ -144,18 +144,51 @@ const runQuery = async (queryString, database = {}, batchSize = 100000) => { const request = new mssql.Request(); request.stream = true; + + // Promise that resolves when recordset is emitted or rejects on request error + const recordsetPromise = new Promise((resolve, reject) => { + request.once('recordset', resolve); + request.once('error', reject); + }); + + // Start the streaming query request.query(queryString); - const columns = await new Promise((res) => request.once('recordset', res)); + try { + const columns = await recordsetPromise; - const stream = request.toReadableStream(); - const results = await asyncIterableToBatchedAsyncGenerator(stream, batchSize, { - closeConnection: () => pool.close() - }); - results.columnTypes = mapResultsToEvidenceColumnTypes(columns); - results.expectedRowCount = expected_row_count; + const stream = request.toReadableStream(); + + // Ensure any stream errors are handled to avoid unhandled 'error' events + stream.on('error', async (streamErr) => { + try { + await pool.close(); + } catch (_) { + // ignore close errors + } + // Nothing else to do here; the asyncIterableToBatchedAsyncGenerator + // consumer will observe the error as a rejection when iterating. + }); + + const results = await asyncIterableToBatchedAsyncGenerator(stream, batchSize, { + closeConnection: () => pool.close() + }); + results.columnTypes = mapResultsToEvidenceColumnTypes(columns); + results.expectedRowCount = expected_row_count; + + return results; + } catch (err) { + // Close pool when we hit errors from the request/stream and normalize the error + try { + await pool.close(); + } catch (_) {} - return results; + if (err && err.message) { + throw err.message.replace(/\n|\r/g, ' '); + } else { + throw ('' + err).replace(/\n|\r/g, ' '); + } + } } catch (err) { if (err.message) { throw err.message.replace(/\n|\r/g, ' '); diff --git a/packages/datasources/mssql/test/test.js b/packages/datasources/mssql/test/test.js index a5be8b17f3..5952074f86 100644 --- a/packages/datasources/mssql/test/test.js +++ b/packages/datasources/mssql/test/test.js @@ -1,9 +1,10 @@ import { test } from 'uvu'; import * as assert from 'uvu/assert'; -import runQuery from '../index.cjs'; -import { TypeFidelity, batchedAsyncGeneratorToArray } from '@evidence-dev/db-commons'; +import { createRequire } from 'module'; import 'dotenv/config'; +const require = createRequire(import.meta.url); + test('query runs', async () => { if (process.env.MSSQL_DATABASE) { try { @@ -99,4 +100,67 @@ test('query batches results properly', async () => { } }); +test('runQuery returns normalized error on SQL syntax error', async () => { + // Create a fake mssql module and inject before loading index.cjs + const fakeMssql = { + TYPES: {}, + connect: async () => ({ + request: () => ({ + query: async (q) => { + // If it's the COUNT(*) wrapper, return 0 + if (typeof q === 'string' && q.trim().toUpperCase().startsWith('SELECT COUNT(*)')) { + return { recordset: [{ expected_row_count: 0 }] }; + } + // otherwise, return empty + return { recordset: [] }; + } + }), + close: async () => {} + }), + Request: function () { + const EventEmitter = require('events'); + const r = new EventEmitter(); + r.stream = false; + r.query = function (q) { + // simulate an async error emitted by the request (syntax error) + process.nextTick(() => { + const err = new Error("Incorrect syntax near the keyword 'select'."); + err.code = 'EREQUEST'; + r.emit('error', err); + }); + }; + r.toReadableStream = function () { + const { Readable } = require('stream'); + // A readable that immediately errors when read + const s = new Readable({ objectMode: true, read() {} }); + process.nextTick(() => s.emit('error', new Error('stream error'))); + return s; + }; + return r; + } + }; + + const path = require('path'); + const mssqlModulePath = path.join(process.cwd(), 'node_modules', 'mssql', 'index.js'); + require.cache[mssqlModulePath] = { + id: mssqlModulePath, + filename: mssqlModulePath, + loaded: true, + exports: fakeMssql + }; + + const runQuery = require('../index.cjs'); + + try { + await runQuery('select * from', {}, 10); + assert.unreachable('Expected runQuery to throw'); + } catch (e) { + // The function normalizes errors to strings + assert.ok(typeof e === 'string' || e instanceof String || e.message, 'error should be string or have message'); + const msg = typeof e === 'string' ? e : e.message || String(e); + // Message should be non-empty and mention syntax/select or be an error code + assert.ok(msg && msg.length > 0, 'error message should be non-empty'); + } +}); + test.run(); From c0e427bfe6a07cfa8d7ceaf0fb1082486e166cde Mon Sep 17 00:00:00 2001 From: Emanuele Bardelli Date: Fri, 14 Nov 2025 11:32:08 -0800 Subject: [PATCH 2/3] Lint --- packages/datasources/mssql/test/test.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/datasources/mssql/test/test.js b/packages/datasources/mssql/test/test.js index 5952074f86..ff4d12a2cb 100644 --- a/packages/datasources/mssql/test/test.js +++ b/packages/datasources/mssql/test/test.js @@ -156,7 +156,10 @@ test('runQuery returns normalized error on SQL syntax error', async () => { assert.unreachable('Expected runQuery to throw'); } catch (e) { // The function normalizes errors to strings - assert.ok(typeof e === 'string' || e instanceof String || e.message, 'error should be string or have message'); + assert.ok( + typeof e === 'string' || e instanceof String || e.message, + 'error should be string or have message' + ); const msg = typeof e === 'string' ? e : e.message || String(e); // Message should be non-empty and mention syntax/select or be an error code assert.ok(msg && msg.length > 0, 'error message should be non-empty'); From 155ca56a98a9a595e00c3849a6d48fbc33f06186 Mon Sep 17 00:00:00 2001 From: Emanuele Bardelli Date: Fri, 14 Nov 2025 11:33:33 -0800 Subject: [PATCH 3/3] Add changeset --- .changeset/bright-trainers-tan.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/bright-trainers-tan.md diff --git a/.changeset/bright-trainers-tan.md b/.changeset/bright-trainers-tan.md new file mode 100644 index 0000000000..9f922aac65 --- /dev/null +++ b/.changeset/bright-trainers-tan.md @@ -0,0 +1,5 @@ +--- +'@evidence-dev/mssql': minor +--- + +Fix runtime error handling for MSSQL data source