feat(bigquery-jdbc): infer undeclared fields in PreparedStatement - #14400
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces parameter type inference for BigQueryPreparedStatement by executing dry-run queries to retrieve undeclared query parameters. It also adds validation to ensure all parameters are set before execution. The review feedback highlights a compilation error in BigQueryConnection where clear() was replaced with git(), recommends handling potential exceptions when parsing SQL type names to prevent loop termination, advises removing leftover debugging print statements in production code, and suggests correcting tautological assertions in the integration tests to verify actual expected JDBC types.
…is/google-cloud-java into add-undeclared-query-parameters
| QueryJobConfiguration dryRunConfig = | ||
| getJobConfig(query).setDryRun(true).setParameterMode("POSITIONAL").build(); | ||
| Job dryRunJob = this.bigQuery.create((JobInfo.of(dryRunConfig))); | ||
| QueryStatistics jobStatistics = dryRunJob.getStatistics(); |
There was a problem hiding this comment.
Does it wait for it to complete?
There was a problem hiding this comment.
I think dryRun jobs don't need wait-t-finish since thay are not actually executing a job. I will double check.
| } | ||
| } | ||
| } catch (Exception ex) { | ||
| LOG.warning("Could not infer parameter types via dryRun: " + ex.getMessage()); |
There was a problem hiding this comment.
This doesn't look like just a warning, it likely indicates there is an issue with the query.
Also can we do this only in case when this information is needed? e.g. if customer code calls directly setString, do we need to validate the type or just allow it to fail at the 'execute' time?
There was a problem hiding this comment.
The dryRun could be failing due to a transient error. Should the statement creation fail because of that? Since the Prepared Statement can work even without the dryRun, doesn't it make more sense to fail at execute.
There was a problem hiding this comment.
This logic is incorrect. E.g. SELECT "data??" as s will return 2 as parameter count. We should leverage dry-run data.
There was a problem hiding this comment.
Makes sense. I changed the code to update paramCount from the dryRun stats. But i am keeping this as a fallback in case dryRun fails due to a transient issue or if stats don't get correctly populated for some reason.
| for (int i = 1; i <= this.parametersArraySize; i++) { | ||
|
|
||
| int arrayIndex = i - 1; | ||
| if (this.parametersList.size() <= arrayIndex |
There was a problem hiding this comment.
I think this check should happen as a part of getParameter(i).
That way it can distinguish between "unset parameter" and "parameter set to null"
There was a problem hiding this comment.
getParameter(i) only looks at 1 parameter. This if block is about "Are there any missing parameters in the PreparedStatement?". In addition, Callable Statement calls getParameter as well. So I don't think it should be in getParameter.
🤖 I have created a release *beep* *boop* --- <details><summary>1.93.0</summary> ## [1.93.0](v1.92.0...v1.93.0) (2026-09-30) ### ⚠ BREAKING CHANGES * **automl:** remove java-automl library ([#14540](#14540)) ### Features * **automl:** remove java-automl library ([#14540](#14540)) ([6f13a1d](6f13a1d)) * **bigquery-jdbc:** infer undeclared fields in PreparedStatement ([#14400](#14400)) ([e989a72](e989a72)) * **gax,gapic-generator-java:** retry start upload call and rename ResumableUploadCallSettings to ResumableUploadOptions ([#14511](#14511)) ([98c2073](98c2073)) * **gax:** add chunk upload recovery loop for resumable uploads ([#14424](#14424)) ([8ecd1e5](8ecd1e5)) * **gax:** add ResumableUploadProgressListener and related infrastructure ([#14426](#14426)) ([c7f10d8](c7f10d8)) * **gax:** add RewindableStreamBuffer in prep for chunk upload recovery ([#14423](#14423)) ([349c315](349c315)) * **gax:** enforce global timeout for resumable uploads ([#14425](#14425)) ([3cb9805](3cb9805)) * **gax:** include upload-status header in resumable upload command response objects ([#14420](#14420)) ([23e49c1](23e49c1)) * **gax:** replace InputStream with InputStreamSupplier in resumable upload public surfaces ([#14521](#14521)) ([3c79730](3c79730)) * **gax:** retry chunk upload on transient errors ([#14422](#14422)) ([e72365e](e72365e)) * **gax:** treat resumable upload server rejection as terminal ([#14516](#14516)) ([b288788](b288788)) * **gax:** wire up ResumableUploadProgressTracker ([#14427](#14427)) ([e7ba3e8](e7ba3e8)) * **grpc-gcp:** Add shared fallback state and probing recovery options to GcpFallbackChannel ([#14013](#14013)) ([675f639](675f639)) * **pubsub:** add publish start time to client telemetry header ([#14496](#14496)) ([4b6a671](4b6a671)) ### Bug Fixes * **auth:** exclude javax.annotation-api from api-common dependency ([#14538](#14538)) ([668fd18](668fd18)), refs [#12363](#12363) * **bigquery-jdbc:** abort session when connection is closed ([#14273](#14273)) ([5bd4520](5bd4520)), refs [#13922](#13922) * **storage:** add App Hub storage.googleapis.com prefix to destination.id ([#14527](#14527)) ([0f6f9b5](0f6f9b5)) ### Documentation * Add gRPC Post-Quantum Cryptography Guide ([#14245](#14245)) ([340a239](340a239)) * **samples:** add zonal bucket pre-warmed writer pool sample ([#14517](#14517)) ([32e6f87](32e6f87)) * **samples:** pre-warm writer pool channels with flush() after open() ([#14537](#14537)) ([df06290](df06290)) * update PQC guide to follow standard template format ([#14333](#14333)) ([0b969d5](0b969d5)) </details> --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --------- Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com>
Fixes b/557150710
POSITIONAL-modedry run when a
PreparedStatementis prepared.of counting
?characters, so placeholders inside string literals(
SELECT 'Hello, ?World!', ?) no longer inflate the count.UNBOUND/TYPED/BOUND) so aninferred type never overwrites a value the caller set, and so batch copies and
clearParameters()preserve that distinction.setNullandsetObject(i, null)resolve the type ascaller-declared, then dry-run inferred, then
STRING.getParameterMetaData()before any value is bound, so callers candiscover parameter types rather than receiving
Types.OTHER.getMetaData()with a SELECT's result schema before execution, andreturns
nullfor statements that produce noResultSet, per the JDBC contract.INSERTs for the StorageWrite API, retried during
executeBatchso tables created after prepare timestill qualify.
be derived from caller-supplied values and the count from the query text.