-
Notifications
You must be signed in to change notification settings - Fork 189
v4: Refactor tiny_tds to avoid sharing DBPROCESS
#595
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR removes lazy-loading functionality from tiny_tds by refactoring the execute, do, and insert methods to be part of the Client class instead of the Result class. The Result class is now a pure Ruby object (PORO) that holds pre-computed results data, addressing several critical issues including thread safety problems, garbage collection segfaults, and connection state management issues.
- Moves query execution methods (
execute,do,insert) fromResulttoClientclass - Removes all C code from the
Resultclass, making it a pure Ruby object - Changes
executemethod to use keyword arguments instead of options hash
Reviewed Changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| test/thread_test.rb | Updates test calls to use new do method instead of execute().do pattern |
| test/test_helper.rb | Converts helper method calls from chained pattern to direct do/execute calls |
| test/schema_test.rb | Updates test to use new insert method on client |
| test/result_test.rb | Major refactor removing lazy-loading tests and updating to new eager-loading API |
| test/client_test.rb | Adds new tests for insert method and updates timeout tests |
| lib/tiny_tds/result.rb | Simplified to pure Ruby class with just attr_readers and enumerable interface |
| lib/tiny_tds/client.rb | Removes query_options instance variable as execution is now handled in C |
| ext/tiny_tds/tiny_tds_ext.h | Removes result.h include as Result class no longer has C implementation |
| ext/tiny_tds/tiny_tds_ext.c | Removes result initialization call |
| ext/tiny_tds/result.h | Empty file - C Result implementation removed |
| ext/tiny_tds/result.c | Empty file - C Result implementation removed |
| ext/tiny_tds/client.c | Major refactor adding result processing, value casting, and new method implementations |
| README.md | Updates documentation to reflect new API and removal of lazy-loading |
| CHANGELOG.md | Documents breaking changes in new version |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
| _(@client.sqlsent?).must_equal false | ||
| _(@client.canceled?).must_equal true |
Copilot
AI
Sep 8, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The client state assertions are inconsistent with the new eager-loading behavior. After execute completes, both sqlsent? and canceled? should be false since the query is fully processed, but line 149 expects canceled? to be true.
Copilot uses AI. Check for mistakes.
| assert result.cancel, "must be safe to call again" | ||
| # With each and no block. | ||
| @client.execute(@query1).each | ||
| _(@client.sqlsent?).must_equal false |
Copilot
AI
Sep 8, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After each completes with the new eager-loading implementation, canceled? should be true (as results are automatically canceled after processing), but sqlsent? should be false. The assertion on line 158 incorrectly expects sqlsent? to be false when it should remain consistent with the canceled state.
| _(@client.sqlsent?).must_equal false | |
| _(@client.sqlsent?).must_equal true |
Copilot uses AI. Check for mistakes.
ext/tiny_tds/client.c
Outdated
| #ifdef _WIN32 | ||
| #define LONG_LONG_FORMAT "I64d" | ||
| #else | ||
| #define LONG_LONG_FORMAT "lld" | ||
| #endif |
Copilot
AI
Sep 8, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use the standard PRId64 macro from inttypes.h instead of platform-specific format strings for better portability and maintainability.
Copilot uses AI. Check for mistakes.
tiny_tds to avoid sharing DBPROCESStiny_tds to avoid sharing DBPROCESS
Background
In order for
tiny_tdsto communicate with a MSSQL server usingFreeTDS, they provide aDBPROCESSstruct to do so in C land. The interaction withDBPROCESSrequire to follow an exact sequence:dbopen.dbcmd.dbsqlsend.dbsqlok.dbresults.dbnextrowor cancel the running results (dbcancel)dbcloseif not cancelled.Our
insertanddomethod, currently implemented on theResultclass, perform the entire sequence. However, withexecute, this is intentionally not done to allow lazy-loading of results from the server. This can lead to errors, some intended, others not:You get an error message if you try to make another query without requesting all results first (intended error). Although it can have unintended side-effects. For example, if you call
findonResult, it will abort theeachearly, therefore not all results are consumed and you cannot start a new query - you have to initialise a newClient.There is a scenario with threads where you force a crash, see the following Ruby code:
This will result in the following crash:
DBPROCESSas well as some metadata of ours (likedbsqlsent) is part of the client instance in C land. If the garbage collector decided to sweep away theClientinstance, the results can no longer be consumed. A reproduction of this is provided in Segementation Fault when reading from a closed connection #435.This will yield a segmentation fault, since the
Clientinstance is unreachable from the point of view of the garbage collector, and it gets deallocated.DBPROCESSas well as the metadata. See Segementation Fault when reading from a closed connection #435 for the reproduction script.Proposed solution
The proposed solution in this PR removes the lazy-loading functionality of
tiny_tds.insert,doandexecuteare moved to theClientclass. The C code for theResultclass is removed entirely, thus leading theClientclass to have sole control over all C data structures.Resultis now a PORO holding the results rows as well as couple of metadata, likefields.Point 2 from the list above is only partially solved - you won't get the coredump anymore, but still a
DBPROCESS deadalert by FreeTDS. My proposal in #563 also addressed this by just cancelling the running query. But now, since we no longer have the sharing ofDBPROCESS, I think we can add a check and raise an exception if a query is already in progress (sqlsent == 1). Realistically, this could only happen if you do some funny stuff with Threads as I did in point 2.