diff --git a/handwritten/bigquery/src/bigquery.ts b/handwritten/bigquery/src/bigquery.ts index d52d07343b5..e0e8f1411e2 100644 --- a/handwritten/bigquery/src/bigquery.ts +++ b/handwritten/bigquery/src/bigquery.ts @@ -1101,10 +1101,12 @@ export class BigQuery extends Service { }), }; } else if ((providedType as string).toUpperCase() === 'TIMESTAMP(12)') { - return { - type: 'TIMESTAMP', - timestampPrecision: '12', - }; + if (process.env.BIGQUERY_PICOSECOND_SUPPORT === 'true') { + return { + type: 'TIMESTAMP', + timestampPrecision: '12', + }; + } } providedType = (providedType as string).toUpperCase(); @@ -2263,7 +2265,6 @@ export class BigQuery extends Service { the callback. Instead, pass the error to the callback the user provides so that the user can see the error. */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any const listParams = { 'formatOptions.timestampOutputFormat': queryReq.formatOptions?.timestampOutputFormat, @@ -2367,11 +2368,18 @@ export class BigQuery extends Service { const hasAnyFormatOpts = options['formatOptions.timestampOutputFormat'] !== undefined || options['formatOptions.useInt64Timestamp'] !== undefined; - const defaultOpts = hasAnyFormatOpts + let defaultOpts: bigquery.IDataFormatOptions = hasAnyFormatOpts ? {} : { - timestampOutputFormat: 'ISO8601_STRING', + useInt64Timestamp: true, }; + if (process.env.BIGQUERY_PICOSECOND_SUPPORT === 'true') { + defaultOpts = hasAnyFormatOpts + ? {} + : { + timestampOutputFormat: 'ISO8601_STRING', + }; + } const formatOptions = extend(defaultOpts, { timestampOutputFormat: options['formatOptions.timestampOutputFormat'], useInt64Timestamp: options['formatOptions.useInt64Timestamp'], @@ -2585,12 +2593,9 @@ function convertSchemaFieldValue( 2023-01-01T12:00:00.123456789123Z */ const listParams = options.listParams; - const timestampOutputFormat = listParams - ? listParams['formatOptions.timestampOutputFormat'] - : undefined; - const useInt64Timestamp = listParams - ? listParams['formatOptions.useInt64Timestamp'] - : undefined; + const timestampOutputFormat = + listParams?.['formatOptions.timestampOutputFormat']; + const useInt64Timestamp = listParams?.['formatOptions.useInt64Timestamp']; if (timestampOutputFormat === 'ISO8601_STRING') { // value is ISO string, create BigQueryTimestamp wrapping the string value = BigQuery.timestamp(value); diff --git a/handwritten/bigquery/src/table.ts b/handwritten/bigquery/src/table.ts index e92c6a6791d..6993ac58abf 100644 --- a/handwritten/bigquery/src/table.ts +++ b/handwritten/bigquery/src/table.ts @@ -55,7 +55,6 @@ import {JobMetadata, JobOptions} from './job'; import bigquery from './types'; import {IntegerTypeCastOptions} from './bigquery'; import {RowQueue} from './rowQueue'; -import IDataFormatOptions = bigquery.IDataFormatOptions; // This is supposed to be a @google-cloud/storage `File` type. The storage npm // module includes these types, but is current installed as a devDependency. @@ -1894,15 +1893,24 @@ class Table extends ServiceObject { } callback!(null, rows, nextQuery, resp); }; + const hasAnyFormatOpts = options['formatOptions.timestampOutputFormat'] !== undefined || options['formatOptions.useInt64Timestamp'] !== undefined; - const defaultOpts = hasAnyFormatOpts + let defaultOpts: GetRowsOptions = hasAnyFormatOpts ? {} : { - 'formatOptions.timestampOutputFormat': 'ISO8601_STRING', + 'formatOptions.useInt64Timestamp': true, }; - const qs = extend(defaultOpts, options); + if (process.env.BIGQUERY_PICOSECOND_SUPPORT === 'true') { + defaultOpts = hasAnyFormatOpts + ? {} + : { + 'formatOptions.timestampOutputFormat': 'ISO8601_STRING', + }; + } + const qs: GetRowsOptions = extend(defaultOpts, options); + this.request( { uri: '/data', diff --git a/handwritten/bigquery/system-test/bigquery.ts b/handwritten/bigquery/system-test/bigquery.ts index 92550709de1..8a104d9d9db 100644 --- a/handwritten/bigquery/system-test/bigquery.ts +++ b/handwritten/bigquery/system-test/bigquery.ts @@ -1042,25 +1042,27 @@ describe('BigQuery', () => { }); it('should create a table with timestampPrecision', async () => { - const table = dataset.table(generateName('timestamp-precision-table')); - const schema = { - fields: [ - { - name: 'ts_field', - type: 'TIMESTAMP', - timestampPrecision: 12, - }, - ], - }; - try { - await table.create({schema}); - const [metadata] = await table.getMetadata(); - assert.deepStrictEqual( - metadata.schema.fields[0].timestampPrecision, - '12', - ); - } catch (e) { - assert.ifError(e); + if (process.env.BIGQUERY_PICOSECOND_SUPPORT === 'true') { + const table = dataset.table(generateName('timestamp-precision-table')); + const schema = { + fields: [ + { + name: 'ts_field', + type: 'TIMESTAMP', + timestampPrecision: 12, + }, + ], + }; + try { + await table.create({schema}); + const [metadata] = await table.getMetadata(); + assert.deepStrictEqual( + metadata.schema.fields[0].timestampPrecision, + '12', + ); + } catch (e) { + assert.ifError(e); + } } }); @@ -1562,6 +1564,11 @@ describe('BigQuery', () => { testCases.forEach(testCase => { it(`should handle ${testCase.name}`, async () => { + if (process.env.BIGQUERY_PICOSECOND_SUPPORT !== 'true') { + // These tests are only important when the high precision + // timestamp support is turned on. + return; + } /* The users use the new TIMESTAMP(12) type to indicate they want to opt in to using timestampPrecision=12. The reason is that some queries @@ -1614,6 +1621,11 @@ describe('BigQuery', () => { } }); it(`should handle nested ${testCase.name}`, async () => { + if (process.env.BIGQUERY_PICOSECOND_SUPPORT !== 'true') { + // These tests are only important when the high precision + // timestamp support is turned on. + return; + } /* The users use the new TIMESTAMP(12) type to indicate they want to opt in to using timestampPrecision=12. The reason is that some queries @@ -2009,6 +2021,11 @@ describe('BigQuery', () => { testCases.forEach(testCase => { it(`should handle ${testCase.name}`, async () => { + if (process.env.BIGQUERY_PICOSECOND_SUPPORT !== 'true') { + // These tests are only important when the high precision + // timestamp support is turned on. + return; + } /* The users use the new TIMESTAMP(12) type to indicate they want to opt in to using timestampPrecision=12. The reason is that some queries @@ -2063,6 +2080,11 @@ describe('BigQuery', () => { } }); it(`should handle nested ${testCase.name}`, async () => { + if (process.env.BIGQUERY_PICOSECOND_SUPPORT !== 'true') { + // These tests are only important when the high precision + // timestamp support is turned on. + return; + } /* The users use the new TIMESTAMP(12) type to indicate they want to opt in to using timestampPrecision=12. The reason is that some queries diff --git a/handwritten/bigquery/system-test/high_precision_timestamp.ts b/handwritten/bigquery/system-test/high_precision_timestamp.ts new file mode 100644 index 00000000000..17c32320980 --- /dev/null +++ b/handwritten/bigquery/system-test/high_precision_timestamp.ts @@ -0,0 +1,78 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import {describe, before, after} from 'mocha'; +import * as path from 'path'; + +// This file registers a second run of the BigQuery system tests with picosecond support enabled. +// It uses targeted cache clearing to ensure the library and tests are re-initialized with the +// 'BIGQUERY_PICOSECOND_SUPPORT' environment variable set. + +const originalValue = process.env.BIGQUERY_PICOSECOND_SUPPORT; + +/** + * Helper to clear the require cache for all files within the current package. + */ +function clearPackageCache() { + // Find the root of the current package by looking for package.json + const packageJsonPath = require.resolve('../../package.json'); + const packageDir = path.dirname(packageJsonPath); + + Object.keys(require.cache).forEach(key => { + if (key.startsWith(packageDir) && !key.includes('node_modules')) { + delete require.cache[key]; + } + }); +} + +// 1. REGISTRATION PHASE: +// Set the environment variable and clear the cache so that the subsequent 'require' +// of the system test file (and any library files it imports) sees the updated state. +process.env.BIGQUERY_PICOSECOND_SUPPORT = 'true'; +clearPackageCache(); + +describe('BigQuery System Tests (High Precision)', () => { + // 2. EXECUTION PHASE: + // Mocha runs 'before' hooks before the tests in the required file are executed. + before(() => { + process.env.BIGQUERY_PICOSECOND_SUPPORT = 'true'; + // We don't clear cache here as it's too late for Mocha's registration phase, + // but we ensure the env var is set for any execution-time checks. + }); + + after(() => { + if (originalValue === undefined) { + delete process.env.BIGQUERY_PICOSECOND_SUPPORT; + } else { + process.env.BIGQUERY_PICOSECOND_SUPPORT = originalValue; + } + }); + + describe('Run all tests', () => { + // Wrap all the other tests in a describe block to ensure the entire file is + // executed when the environment variable is set to true. + + // Programmatically require the tests. Mocha will discover and register them inside this 'describe' block. + require('./bigquery'); + require('./timestamp_output_format'); + }); +}); + +// 3. CLEANUP: +// Restore the environment variable after the registration phase to minimize side effects on other test files. +if (originalValue === undefined) { + delete process.env.BIGQUERY_PICOSECOND_SUPPORT; +} else { + process.env.BIGQUERY_PICOSECOND_SUPPORT = originalValue; +} diff --git a/handwritten/bigquery/system-test/timestamp_output_format.ts b/handwritten/bigquery/system-test/timestamp_output_format.ts index 0fe388e1e3b..bd622b9dfec 100644 --- a/handwritten/bigquery/system-test/timestamp_output_format.ts +++ b/handwritten/bigquery/system-test/timestamp_output_format.ts @@ -38,6 +38,9 @@ describe('Timestamp Output Format System Tests', () => { const expectedTsValuePicoseconds = '2023-01-01T12:00:00.123456789123Z'; before(async () => { + if (process.env.BIGQUERY_PICOSECOND_SUPPORT !== 'true') { + return; + } await dataset.create(); await table.create({ schema: [{name: 'ts', type: 'TIMESTAMP', timestampPrecision: '12'}], @@ -159,6 +162,9 @@ describe('Timestamp Output Format System Tests', () => { expectedTsValue, }) => { it(name, async () => { + if (process.env.BIGQUERY_PICOSECOND_SUPPORT !== 'true') { + return; + } const options: {[key: string]: any} = {}; if (timestampOutputFormat !== undefined) { options['formatOptions.timestampOutputFormat'] = @@ -185,6 +191,10 @@ describe('Timestamp Output Format System Tests', () => { ); it('should make a request with ISO8601_STRING when no format options are being used', done => { + if (process.env.BIGQUERY_PICOSECOND_SUPPORT !== 'true') { + done(); + return; + } void (async () => { const originalRequest = table.request; const requestPromise: Promise = new Promise( diff --git a/handwritten/bigquery/test/bigquery.ts b/handwritten/bigquery/test/bigquery.ts index 74fbbedf1c7..e5bbf0e222a 100644 --- a/handwritten/bigquery/test/bigquery.ts +++ b/handwritten/bigquery/test/bigquery.ts @@ -3439,6 +3439,14 @@ describe('BigQuery', () => { delete req[key]; } } + const formatOptions = + process.env.BIGQUERY_PICOSECOND_SUPPORT === 'true' + ? { + timestampOutputFormat: 'ISO8601_STRING', + } + : { + useInt64Timestamp: true, + }; const expectedReq = { query: QUERY_STRING, useLegacySql: false, @@ -3465,9 +3473,7 @@ describe('BigQuery', () => { key: 'value', }, jobCreationMode: 'JOB_CREATION_REQUIRED', - formatOptions: { - timestampOutputFormat: 'ISO8601_STRING', - }, + formatOptions, }; assert.deepStrictEqual(req, expectedReq); }); @@ -3508,6 +3514,9 @@ describe('BigQuery', () => { testCases.forEach(testCase => { it(`should handle ${testCase.name}`, () => { + if (process.env.BIGQUERY_PICOSECOND_SUPPORT !== 'true') { + return; + } const req = bq.buildQueryRequest_(QUERY_STRING, testCase.opts); const expectedReq = { @@ -3535,6 +3544,7 @@ describe('BigQuery', () => { }); }); }); + it('should create a QueryRequest from a SQL string', () => { const req = bq.buildQueryRequest_(QUERY_STRING, {}); for (const key in req) { @@ -3542,14 +3552,20 @@ describe('BigQuery', () => { delete req[key]; } } + const formatOptions = + process.env.BIGQUERY_PICOSECOND_SUPPORT === 'true' + ? { + timestampOutputFormat: 'ISO8601_STRING', + } + : { + useInt64Timestamp: true, + }; const expectedReq = { query: QUERY_STRING, useLegacySql: false, requestId: req.requestId, jobCreationMode: 'JOB_CREATION_OPTIONAL', - formatOptions: { - timestampOutputFormat: 'ISO8601_STRING', - }, + formatOptions, }; assert.deepStrictEqual(req, expectedReq); }); diff --git a/handwritten/bigquery/test/table.ts b/handwritten/bigquery/test/table.ts index 2e13f1572d6..f3e59d7f3d9 100644 --- a/handwritten/bigquery/test/table.ts +++ b/handwritten/bigquery/test/table.ts @@ -2041,12 +2041,20 @@ describe('BigQuery/Table', () => { it('should make correct API request', done => { const options = {a: 'b', c: 'd'}; + const formatOptions = + process.env.BIGQUERY_PICOSECOND_SUPPORT === 'true' + ? { + 'formatOptions.timestampOutputFormat': 'ISO8601_STRING', + } + : { + 'formatOptions.useInt64Timestamp': true, + }; table.request = (reqOpts: DecorateRequestOptions, callback: Function) => { assert.strictEqual(reqOpts.uri, '/data'); assert.deepStrictEqual(reqOpts.qs, { ...options, - 'formatOptions.timestampOutputFormat': 'ISO8601_STRING', + ...formatOptions, }); callback(null, {}); }; @@ -2201,13 +2209,16 @@ describe('BigQuery/Table', () => { sandbox.restore(); const mergeStub = sandbox.stub(BigQuery, 'mergeSchemaWithRows_'); - table.getRows({skipParsing: true}, (err: Error, rows_: {}[], nextQuery: {}, apiResponse: any) => { - assert.ifError(err); - assert.strictEqual(rows_, rows); - assert.strictEqual(mergeStub.called, false); - assert.deepStrictEqual(apiResponse.rows, rows); - done(); - }); + table.getRows( + {skipParsing: true}, + (err: Error, rows_: {}[], nextQuery: {}, apiResponse: any) => { + assert.ifError(err); + assert.strictEqual(rows_, rows); + assert.strictEqual(mergeStub.called, false); + assert.deepStrictEqual(apiResponse.rows, rows); + done(); + }, + ); }); it('should pass nextQuery if pageToken is returned', done => { @@ -2217,18 +2228,33 @@ describe('BigQuery/Table', () => { // Set a schema so it doesn't try to refresh the metadata. table.metadata = {schema: {}}; + const callbackResponse = + process.env.BIGQUERY_PICOSECOND_SUPPORT === 'true' + ? { + 'formatOptions.useInt64Timestamp': true, + pageToken, + } + : { + pageToken, + }; table.request = (reqOpts: DecorateRequestOptions, callback: Function) => { - callback(null, { - pageToken, - }); + callback(null, callbackResponse); }; + const formatOptions = + process.env.BIGQUERY_PICOSECOND_SUPPORT === 'true' + ? { + 'formatOptions.timestampOutputFormat': 'ISO8601_STRING', + } + : { + 'formatOptions.useInt64Timestamp': true, + }; table.getRows(options, (err: Error, rows: {}, nextQuery: {}) => { assert.ifError(err); assert.deepStrictEqual(nextQuery, { a: 'b', c: 'd', - 'formatOptions.timestampOutputFormat': 'ISO8601_STRING', + ...formatOptions, pageToken, }); // Original object isn't affected. @@ -2442,10 +2468,18 @@ describe('BigQuery/Table', () => { const wrapIntegers = {integerTypeCastFunction: sinon.stub()}; const options = {wrapIntegers}; const merged = [{name: 'stephen'}]; + const formatOptions = + process.env.BIGQUERY_PICOSECOND_SUPPORT === 'true' + ? { + 'formatOptions.timestampOutputFormat': 'ISO8601_STRING', + } + : { + 'formatOptions.useInt64Timestamp': true, + }; table.request = (reqOpts: DecorateRequestOptions, callback: Function) => { assert.deepStrictEqual(reqOpts.qs, { - 'formatOptions.timestampOutputFormat': 'ISO8601_STRING', + ...formatOptions, }); callback(null, {}); }; @@ -2466,10 +2500,18 @@ describe('BigQuery/Table', () => { parseJSON: true, }; const merged = [{name: 'stephen'}]; + const formatOptions = + process.env.BIGQUERY_PICOSECOND_SUPPORT === 'true' + ? { + 'formatOptions.timestampOutputFormat': 'ISO8601_STRING', + } + : { + 'formatOptions.useInt64Timestamp': true, + }; table.request = (reqOpts: DecorateRequestOptions, callback: Function) => { assert.deepStrictEqual(reqOpts.qs, { - 'formatOptions.timestampOutputFormat': 'ISO8601_STRING', + ...formatOptions, }); callback(null, {}); };