Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
1 change: 1 addition & 0 deletions src/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3176,6 +3176,7 @@ class Connection extends EventEmitter {
request.rowCount! = 0;
request.rows! = [];
request.rst! = [];
request.error = undefined;

const onCancel = () => {
payloadStream.unpipe(message);
Expand Down
81 changes: 81 additions & 0 deletions test/integration/prepare-execute-statements-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,87 @@ describe('Prepare Execute Statement', function() {
});
});

it('should not persist error state between executions of prepared statement (GH#1712)', function(done) {
const config = getConfig();

const connection = new Connection(config);
if (process.env.TEDIOUS_DEBUG) {
connection.on('debug', console.log);
}

// Prepare a statement that can cause a divide by zero error depending on the parameter
const request = new Request('select 1 / @divisor as result', function() {});
request.addParameter('divisor', TYPES.Int);

let executionCount = 0;
const results: (number | null)[] = [];
const errors: (Error | undefined)[] = [];

request.on('row', function(columns) {
results.push(columns[0].value);
});

request.on('prepared', function() {
assert.ok(request.handle);

// First execution: should succeed with divisor = 1
request.once('requestCompleted', function() {
executionCount++;
errors.push(request.error);

// Second execution: should fail with divisor = 0 (divide by zero)
request.once('requestCompleted', function() {
executionCount++;
errors.push(request.error);

// Third execution: should succeed with divisor = 2
// Before the fix, this would report the error from the second execution
request.once('requestCompleted', function() {
executionCount++;
errors.push(request.error);

// Unprepare and close
connection.unprepare(request);
connection.close();
Copy link
Copy Markdown
Collaborator

@chdh chdh Dec 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling close() without waiting for the unprepare() to complete cancels the unprepare() immediatelly so that is has no effect. While unprepare is not necessary before a close anyway, the test case is not entirely correct in this form and it's a bad example for the users.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@claude Can you fix this?

Copy link
Copy Markdown

@claude claude Bot Dec 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude finished @arthurschreiber's task in 1m 27s —— View job


Fixed test to wait for unprepare() before close()

I've fixed the issue where close() was called without waiting for unprepare() to complete.

Todo List:

  • Read the test file to understand current implementation
  • Fix the test to properly chain unprepare() and close()
  • Commit and push the changes

Changes made:

  • Modified the test at test/integration/prepare-execute-statements-test.ts:199-202 to listen for the requestCompleted event before calling close()
  • This ensures unprepare() completes before the connection is closed
  • Prevents the unprepare operation from being cancelled prematurely

The fix follows the same event-driven pattern used throughout the test file, where operations are chained using requestCompleted event handlers. The test now properly waits for unprepare to finish before closing, making it a correct example for users.

Commit: ff14d7d

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@chdh Claude fixed this, but looking at how the prepare/unprepare API is supposed to be used, it makes very little sense to me. 😟

Looks like the callback to new Request is called when the request is unprepared? But how is the user supposed to understand the difference between a request being unprepared and a request being executed?

This seems to be a major design flaw in the current API - prepare, execute, unprepare, execSqlBatch, execSql all should take their own callbacks, and new Request should not even have a callback set.

Also I guess there should be a PreparedRequest object that's returned by prepare and that can be passed into execute and unprepare.

Or do I misunderstand the current API? Do you think the "future" API I'm laying out above is not going in the right direction?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be a major design flaw in the current API - prepare, execute, unprepare, execSqlBatch, execSql all should take their own callbacks, and new Request should not even have a callback set.

Probably execSql, execute and execSqlBatch should actually return a Response object which can be iterated over / events will be emitted on.

Copy link
Copy Markdown
Collaborator

@chdh chdh Dec 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like the callback to new Request is called when the request is unprepared? But how is the user supposed to understand the difference between a request being unprepared and a request being executed?

This seems to be a major design flaw in the current API - prepare, execute, unprepare, execSqlBatch, execSql all should take their own callbacks, and new Request should not even have a callback set.

Yes, I had exactly this problem when I wrote my promise-based wrapper. I solved it by routing the request completion callback through a function pointer in aPreparedStatement object.

export interface PreparedStatement {
   _dbConnection:                      DbConnection;
   _request:                           Tedious.Request;
   _requestCompletionCallback?:        (err: Error, rowCount: number) => void;
   parmDefs:                           DbParmDef[];
   isPrepared:                         boolean;
   isDisposed:                         boolean;
}

});

connection.execute(request, { divisor: 2 });
});

connection.execute(request, { divisor: 0 });
});

connection.execute(request, { divisor: 1 });
});

connection.connect(function(err) {
if (err) {
return done(err);
}

connection.prepare(request);
});

connection.on('end', function() {
// Verify the behavior
assert.strictEqual(executionCount, 3, 'Should have completed 3 executions');

// First execution succeeded
assert.isUndefined(errors[0], 'First execution should have no error');
assert.strictEqual(results[0], 1, 'First execution should return 1');

// Second execution failed with divide by zero
assert.isDefined(errors[1], 'Second execution should have an error');
assert.include(errors[1]!.message, 'Divide by zero', 'Error should be divide by zero');

// Third execution succeeded - this is the key assertion for GH#1712
assert.isUndefined(errors[2], 'Third execution should have no error (error state should be cleared)');
assert.strictEqual(results[1], 0, 'Third execution should return 0 (1/2 truncated to int)');

done();
});
});

it('should test unprepare', function(done) {
const config = getConfig();
const request = new Request('select 3', function(err) {
Expand Down
Loading