fix(jdbc-driver): actually run connection queries on the query and stream paths - #11456
Conversation
…ream paths query() passed the array from prepareConnectionQueries() as queryPromised's options argument, which reads options.prepareConnectionQueries off it -- undefined on an array, so the replay loop ran zero times. stream() had no replay at all. The defect dates to the driver's original commit. MySQL accessed through this driver (CUBEJS_DB_TYPE=jdbc, not the native mysql driver) therefore never applied its SET time_zone = '+00:00', reading timestamps in the server's local timezone instead of UTC. Any driver config passing prepareConnectionQueries explicitly was silently ignored too. mysql is the only dbType in supported-drivers.ts with a non-empty list -- athena, sparksql and hive all ship [], and Databricks resolves to [] because its dbType is absent from SupportedDrivers -- so the behaviour change is confined to MySQL-over-JDBC and to explicit user config.
|
Claude finished @igorlukanin's task in 2m 41s —— View job Review complete — fix is correct; 2 medium, 2 low. No blockers.Full review
VerdictThe diagnosis is right and the fix is the minimal correct one. I independently confirmed the blast-radius analysis:
The behaviour-change call-out in the description is the important part of this PR — MySQL-over-JDBC starts returning UTC timestamps where it returned server-local ones. That belongs in the release notes, and arguably deserves a line in the driver docs too, since anyone who compensated downstream will see values shift silently. FindingsMedium — the replay is per-call, not per-connection (inline on These are connection queries: session state that survives for the life of the JDBC connection. Running them at each call site means every Medium — statement leak, now on the stream path too
Low — a stream test doesn't test what its name says (inline on
Low — The out-of-scope note about Things done well
What I could not verify
|
| // A streamed query runs on its own connection, so it needs the same | ||
| // connection queries the query path replays. | ||
| for (const connectionQuery of this.prepareConnectionQueries()) { | ||
| await this.executeStatement(conn, connectionQuery); | ||
| } |
There was a problem hiding this comment.
Worth considering whether the replay belongs in the pool's create factory instead of per query/stream.
These are connection queries — session state (SET time_zone) that survives for the life of the JDBC connection. Running them at the call site means every query() and every stream() on a pooled connection re-issues the same SET, so for MySQL-over-JDBC this fix doubles the round-trips on the query path. Doing it once in the pool factory (line 142-152, right after new Connection(...)) gives the same guarantee for one round-trip per physical connection, and would also make withConnection() and any future path correct for free rather than requiring each one to remember.
The counter-argument is that the pool factory can't distinguish "connection reused after a session-resetting failure" — but nothing in this driver resets sessions, and the current per-call design has been dead code since the driver's first commit, so there's no established behaviour to preserve.
Not a blocker for landing the correctness fix, but the per-call placement is a choice that's cheap now and expensive to revisit once it's live.
|
|
||
| await driver.stream('SELECT 1', [], { highWaterMark: 1 }); | ||
|
|
||
| expect(executed).toEqual([MYSQL_TIME_ZONE]); |
There was a problem hiding this comment.
This assertion doesn't actually test the "before opening the stream" part of the test name.
The primary query in stream() goes through conn.createStatement → statement.execute, not through the stubbed executeStatement, so executed can only ever contain connection queries. The assertion would pass identically if the replay loop were placed after await executeQuery(query).
Pushing a marker from the stream's own execution path into the same array makes the ordering claim real:
execute: (sql: string, cb: Function) => {
built.executed.push(sql);
cb(null, { /* ... */ });
},then expect(executed).toEqual([MYSQL_TIME_ZONE, 'SELECT 1']), symmetric with the query() tests above.
Codecov Report✅ All modified and coverable lines are covered by tests.
Additional details and impacted files@@ Coverage Diff @@
## master #11456 +/- ##
===========================================
- Coverage 79.43% 59.00% -20.43%
===========================================
Files 480 223 -257
Lines 98778 17891 -80887
Branches 3636 3636
===========================================
- Hits 78460 10556 -67904
+ Misses 19800 6817 -12983
Partials 518 518
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Issue
JDBCDriver.query()passed the array returned byprepareConnectionQueries()asqueryPromised'soptionsargument, butqueryPromisedreadsoptions.prepareConnectionQueriesoff it — which on an array isundefined. The replay loop therefore ran zero times, and no connection query was ever executed on the query path.stream()was separately missing the replay entirely, so neither path applied it. The defect dates back to the driver's original commit.supported-drivers.tsshipsprepareConnectionQueries: ["SET time_zone = '+00:00'"]for MySQL, so MySQL accessed through this driver has never applied it — timestamps are read in the server's local timezone rather than UTC. Any driver config passingprepareConnectionQueriesexplicitly was silently ignored too.Fix
query()passes a proper options object soqueryPromisedreads the statements. Fixing the caller rather than wideningqueryPromised's contract keeps that method'soptionsshape intact — it isprotectedwith this as its only call site.stream()replays the same statements before opening the stream, inside the existingtryso a failing connection query still releases the pooled connection through thecatch.prepareConnectionQueries()itself is unchanged, as is every driver's statement list.Blast radius
mysqlis the only dbType insupported-drivers.tswith a non-empty list —athena,sparksqlandhiveall ship[], andDatabricksDriverresolves to[]because its dbType is absent fromSupportedDrivers(and it overrides none ofquery/stream/queryPromised/prepareConnectionQueries). Reaching the non-empty case requiresCUBEJS_DB_TYPE=jdbc; a plainCUBEJS_DB_TYPE=mysqlroutes to the native MySQL driver, not this one.So the change is confined to MySQL-over-JDBC and to configs that set
prepareConnectionQueriesexplicitly. In both cases the behaviour moves from "silently ignored" to "does what it says" — but it does mean MySQL-over-JDBC starts returning UTC timestamps where it previously returned local-time ones. Worth a release note for anyone who compensated for the old behaviour downstream.Tests
test/unit/connection-queries.test.ts— 8 tests over both paths, following the existingescape-dialect.test.tspattern (JVM mocked out, driver built viaObject.createwith a stub pool). Per path: dbType built-ins replay before the primary query, explicit config replays, an empty-array dbType runs none, and a failing connection query still releases the connection exactly once.Each was verified to fail on the unfixed code: reverting the fix turns the 5 replay-asserting tests red while the 3 negative controls stay green.
Out of scope
Two pre-existing issues this touches but does not change:
executeStatement()never closes the JDBCStatementit creates. The query path already leaks these; the stream path now creates one more per connection query, held for the stream's lifetime. Fixing it means changing the shared query path.withConnection()(used for metadata/schema queries) does not replay connection queries, so it stays asymmetric withquery()/stream().