Skip to content
This repository was archived by the owner on Feb 5, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 56 commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
a6dd325
feat: add system tests for timestampOutputFormat options
google-labs-jules[bot] Jan 23, 2026
173e65a
feat: add system tests for timestampOutputFormat options
google-labs-jules[bot] Jan 23, 2026
489be58
Add error visibility to readrows calls
danieljbruce Jan 23, 2026
a1f1bba
Add a comment about why the try/catch block is there
danieljbruce Jan 23, 2026
a0f7fd3
Do an assertion check and report the error
danieljbruce Jan 23, 2026
e6b03c1
Fix another test to expect an error
danieljbruce Jan 23, 2026
e934164
Change other tests to expect errors
danieljbruce Jan 23, 2026
9567663
Change string error messages
danieljbruce Jan 23, 2026
b9e81ac
Adjust another error message
danieljbruce Jan 23, 2026
da842e5
Revert "Adjust another error message"
danieljbruce Jan 23, 2026
38148e1
Change another test to expect an error
danieljbruce Jan 23, 2026
c52c9f4
Change the schema to include timestamp precision
danieljbruce Jan 26, 2026
f0ab80e
Source code changes for int64 and timestamp
danieljbruce Jan 26, 2026
07aa7f1
test: add tests for timestampOutputFormat/useInt64Timestamp defaults
google-labs-jules[bot] Jan 26, 2026
1b09a9c
Implement proper timestamp parsing
danieljbruce Jan 26, 2026
1ff7a2c
more test corrections
danieljbruce Jan 26, 2026
c206170
Support proper business logic for timestamps
danieljbruce Jan 26, 2026
d83f2a5
listParams pass through
danieljbruce Jan 26, 2026
c131f4e
correct test value
danieljbruce Jan 26, 2026
8c4efef
fix: support high precision timestamp strings in BigQueryTimestamp
google-labs-jules[bot] Jan 26, 2026
4366e9e
run the linter
danieljbruce Jan 26, 2026
074d05f
Remove unnecessary line of code
danieljbruce Jan 26, 2026
5fb38d4
add Z
danieljbruce Jan 26, 2026
7e5d6b0
list all users of the mergeSchemaWithRows_ method
danieljbruce Jan 26, 2026
e5506e9
run linter
danieljbruce Jan 26, 2026
ad2211a
remove only
danieljbruce Jan 26, 2026
3d072d0
Update file header
danieljbruce Jan 26, 2026
44e6e7b
fix: support high precision timestamp strings in BigQueryTimestamp
google-labs-jules[bot] Jan 27, 2026
dbea718
Revert "fix: support high precision timestamp strings in BigQueryTime…
danieljbruce Jan 27, 2026
14bb0a8
fix: support high precision timestamp strings in BigQueryTimestamp
google-labs-jules[bot] Jan 27, 2026
1cf914e
parameterize the new system tests
danieljbruce Jan 27, 2026
02601d3
feat: Parameterize timestamp output format tests
danieljbruce Jan 27, 2026
feb0f9a
feat: Re-add undefined test cases for timestamp output format
danieljbruce Jan 27, 2026
26a4f07
Merge branch 'fix-high-precision-timestamps-12704073509468064918' of …
danieljbruce Jan 27, 2026
6e9de44
fix: support high precision timestamp strings in BigQueryTimestamp
google-labs-jules[bot] Jan 27, 2026
c45bde8
run the linter
danieljbruce Jan 27, 2026
bdde5d4
fix: support high precision timestamp strings in BigQueryTimestamp
google-labs-jules[bot] Jan 27, 2026
7ebd6ee
Merge branch 'fix-high-precision-timestamps-12704073509468064918' of …
danieljbruce Jan 27, 2026
a17f421
meet requirements change
danieljbruce Jan 27, 2026
fb38379
cleaner inclusion code
danieljbruce Jan 28, 2026
506edb8
Number cast was removed by mistake
danieljbruce Jan 28, 2026
efacb53
Adding listParams back in
danieljbruce Jan 28, 2026
41c5948
Pass list params
danieljbruce Jan 28, 2026
33f05ab
selectively use ISO8601_STRING as timestampOutputF
danieljbruce Jan 28, 2026
6913244
Fix the tests not to expect errors
danieljbruce Jan 28, 2026
d53b6c5
run the linter
danieljbruce Jan 28, 2026
00b8bc7
remove only
danieljbruce Jan 28, 2026
1077b8e
don’t change this file
danieljbruce Jan 28, 2026
a1bbfc1
Correct unit tests
danieljbruce Jan 28, 2026
08b0b62
add a TODO
danieljbruce Jan 29, 2026
0547069
Merge branch 'main' into fix-high-precision-timestamps-12704073509468…
danieljbruce Jan 29, 2026
d199177
linter fix
danieljbruce Jan 29, 2026
629b519
Remove this import
danieljbruce Jan 29, 2026
cd5d35a
Add a test that ensures the request is correct
danieljbruce Jan 29, 2026
7de53d4
test should send back an error
danieljbruce Jan 30, 2026
3df3939
Update src/table.ts
danieljbruce Jan 30, 2026
1b27a26
simpler logic for default timestamp
danieljbruce Jan 30, 2026
eaaa4d9
broader only
danieljbruce Jan 30, 2026
6f58be2
remove useInt64Timestamp changes
danieljbruce Jan 30, 2026
678ff01
Modify logic slightly to account for no parameter
danieljbruce Jan 30, 2026
5e094af
run the linter
danieljbruce Jan 30, 2026
703de99
remove only
danieljbruce Jan 30, 2026
1ae67bc
Add void keyword
danieljbruce Jan 30, 2026
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
49 changes: 46 additions & 3 deletions src/bigquery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,9 @@ export class BigQuery extends Service {
wrapIntegers: boolean | IntegerTypeCastOptions;
selectedFields?: string[];
parseJSON?: boolean;
listParams?:
| bigquery.tabledata.IListParams
| bigquery.jobs.IGetQueryResultsParams;
},
) {
// deep copy schema fields to avoid mutation
Expand Down Expand Up @@ -2471,6 +2474,9 @@ function convertSchemaFieldValue(
wrapIntegers: boolean | IntegerTypeCastOptions;
selectedFields?: string[];
parseJSON?: boolean;
listParams?:
| bigquery.tabledata.IListParams
| bigquery.jobs.IGetQueryResultsParams;
},
) {
if (value === null) {
Expand Down Expand Up @@ -2530,9 +2536,36 @@ function convertSchemaFieldValue(
break;
}
case 'TIMESTAMP': {
const pd = new PreciseDate();
pd.setFullTime(PreciseDate.parseFull(BigInt(value) * BigInt(1000)));
value = BigQuery.timestamp(pd);
/*
At this point, 'value' will equal the timestamp value returned from the
server. We need to parse this value differently depending on its format.
For example, value could be any of the following:
1672574400123456
1672574400.123456
2023-01-01T12:00:00.123456789123Z
*/
const listParams = options.listParams;
const timestampOutputFormat = listParams
? listParams['formatOptions.timestampOutputFormat']
: undefined;
const useInt64Timestamp = listParams
? listParams['formatOptions.useInt64Timestamp']
: undefined;
if (timestampOutputFormat === 'ISO8601_STRING') {
// value is ISO string, create BigQueryTimestamp wrapping the string
value = BigQuery.timestamp(value);
} else if (
useInt64Timestamp === false &&
timestampOutputFormat !== 'INT64'
) {
// value is float seconds, convert to BigQueryTimestamp
value = BigQuery.timestamp(Number(value));
} else {
// Expect int64 micros (default or explicit INT64)
const pd = new PreciseDate();
pd.setFullTime(PreciseDate.parseFull(BigInt(value) * BigInt(1000)));
value = BigQuery.timestamp(pd);
}
break;
}
case 'GEOGRAPHY': {
Expand Down Expand Up @@ -2727,6 +2760,16 @@ export class BigQueryTimestamp {
} else if (typeof value === 'string') {
if (/^\d{4}-\d{1,2}-\d{1,2}/.test(value)) {
pd = new PreciseDate(value);
if (value.match(/\.\d{10,}/) && !Number.isNaN(pd.getTime())) {
/*
TODO:
When https://github.com/googleapis/nodejs-precise-date/pull/302
is released and we have full support for picoseconds in PreciseData
then we can remove this if block.
*/
this.value = value;
return;
}
} else {
const floatValue = Number.parseFloat(value);
if (!Number.isNaN(floatValue)) {
Expand Down
38 changes: 31 additions & 7 deletions src/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ 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.
Expand Down Expand Up @@ -1865,17 +1866,40 @@ class Table extends ServiceObject {
callback!(err, null, null, resp);
return;
}
rows = BigQuery.mergeSchemaWithRows_(this.metadata.schema, rows || [], {
wrapIntegers,
selectedFields,
parseJSON,
});
try {
/*
Without this try/catch block, calls to getRows will hang indefinitely if
a call to mergeSchemaWithRows_ fails because the error never makes it to
the callback. Instead, pass the error to the callback the user provides
so that the user can see the error.
*/
rows = BigQuery.mergeSchemaWithRows_(this.metadata.schema, rows || [], {
wrapIntegers,
selectedFields,
parseJSON,
listParams: qs,
});
} catch (err) {
callback!(err as Error | null, null, null, resp);
return;
}
callback!(null, rows, nextQuery, resp);
};

const hasAnyFormatOpts = options['formatOptions.timestampOutputFormat'] !== undefined || options['formatOptions.useInt64Timestamp'] !== undefined;
const defaultOpts = hasAnyFormatOptions ? {} : {
'formatOptions.timestampOutputFormat': 'ISO8601_STRING'
};
const qs = extend(defaultOpts, options)
'INT64',
'TIMESTAMP_OUTPUT_FORMAT_UNSPECIFIED',
].includes(options['formatOptions.timestampOutputFormat'] as string);
const qs = extend(
{
'formatOptions.useInt64Timestamp': true,
'formatOptions.useInt64Timestamp': defaultToInt64Timestamp,
'formatOptions.timestampOutputFormat':
options['formatOptions.useInt64Timestamp'] === undefined
? 'ISO8601_STRING'
: undefined,
},
options,
);
Expand Down
208 changes: 208 additions & 0 deletions system-test/timestamp_output_format.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
// 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 * as assert from 'assert';
import {describe, it, before, after} from 'mocha';
import {BigQuery} from '../src/bigquery';
import {randomUUID} from 'crypto';
import {RequestResponse} from '@google-cloud/common/build/src/service-object';

const bigquery = new BigQuery();

interface TestCase {
name: string;
timestampOutputFormat?: string;
useInt64Timestamp?: boolean;
expectedError?: string;
expectedTsValue?: string;
}

describe('Timestamp Output Format System Tests', () => {
const datasetId = `timestamp_test_${randomUUID().replace(/-/g, '_')}`;
const tableId = `timestamp_table_${randomUUID().replace(/-/g, '_')}`;
const dataset = bigquery.dataset(datasetId);
const table = dataset.table(tableId);
const insertedTsValue = '2023-01-01T12:00:00.123456789123Z';
const expectedTsValueMicroseconds = '2023-01-01T12:00:00.123456000Z';
const expectedTsValueNanoseconds = '2023-01-01T12:00:00.123456789123Z';
Copy link
Contributor

Choose a reason for hiding this comment

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

can we already test Picosecond resolution or do we need the update from PreciseDate ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

can we already test Picosecond resolution or do we need the update from PreciseDate ?

I'm not sure I understand the question, but I'll try to answer it. It appears that with the current code changes that whenever we specify ISO8601_STRING in the POST request we correctly return 2023-01-01T12:00:00.123456789123Z to the user. That is because of the line of code that says if (value.match(/\.\d{10,}/) && !Number.isNaN(pd.getTime())) {. But when we get Picosecond support we can remove that if block.


before(async () => {
await dataset.create();
await table.create({
schema: [{name: 'ts', type: 'TIMESTAMP', timestampPrecision: '12'}],
});
// Insert a row to test retrieval
await table.insert([{ts: insertedTsValue}]);
});

after(async () => {
try {
await dataset.delete({force: true});
} catch (e) {
console.error('Error deleting dataset:', e);
}
});

const testCases: TestCase[] = [
{
name: 'should call getRows with TIMESTAMP_OUTPUT_FORMAT_UNSPECIFIED and useInt64Timestamp=true',
timestampOutputFormat: 'TIMESTAMP_OUTPUT_FORMAT_UNSPECIFIED',
useInt64Timestamp: true,
expectedTsValue: expectedTsValueMicroseconds,
},
{
name: 'should call getRows with TIMESTAMP_OUTPUT_FORMAT_UNSPECIFIED and useInt64Timestamp=false',
timestampOutputFormat: 'TIMESTAMP_OUTPUT_FORMAT_UNSPECIFIED',
useInt64Timestamp: false,
expectedTsValue: expectedTsValueMicroseconds,
},
{
name: 'should call getRows with FLOAT64 and useInt64Timestamp=true (expect error)',
timestampOutputFormat: 'FLOAT64',
useInt64Timestamp: true,
expectedError:
'Cannot specify both use_int64_timestamp and timestamp_output_format.',
},
{
name: 'should call getRows with FLOAT64 and useInt64Timestamp=false',
timestampOutputFormat: 'FLOAT64',
useInt64Timestamp: false,
expectedTsValue: expectedTsValueMicroseconds,
},
{
name: 'should call getRows with INT64 and useInt64Timestamp=true',
timestampOutputFormat: 'INT64',
useInt64Timestamp: true,
expectedTsValue: expectedTsValueMicroseconds,
},
{
name: 'should call getRows with INT64 and useInt64Timestamp=false',
timestampOutputFormat: 'INT64',
useInt64Timestamp: false,
expectedTsValue: expectedTsValueMicroseconds,
},
{
name: 'should call getRows with ISO8601_STRING and useInt64Timestamp=true (expect error)',
timestampOutputFormat: 'ISO8601_STRING',
useInt64Timestamp: true,
expectedError:
'Cannot specify both use_int64_timestamp and timestamp_output_format.',
},
{
name: 'should call getRows with ISO8601_STRING and useInt64Timestamp=false',
timestampOutputFormat: 'ISO8601_STRING',
useInt64Timestamp: false,
expectedTsValue: expectedTsValueNanoseconds,
},
// Additional test cases for undefined combinations
{
name: 'should call getRows with timestampOutputFormat undefined and useInt64Timestamp undefined',
timestampOutputFormat: undefined,
useInt64Timestamp: undefined,
expectedTsValue: expectedTsValueNanoseconds,
},
{
name: 'should call getRows with timestampOutputFormat undefined and useInt64Timestamp=true',
timestampOutputFormat: undefined,
useInt64Timestamp: true,
expectedTsValue: expectedTsValueMicroseconds,
},
{
name: 'should call getRows with timestampOutputFormat undefined and useInt64Timestamp=false',
timestampOutputFormat: undefined,
useInt64Timestamp: false,
expectedTsValue: expectedTsValueMicroseconds,
},
{
name: 'should call getRows with TIMESTAMP_OUTPUT_FORMAT_UNSPECIFIED and useInt64Timestamp undefined',
timestampOutputFormat: 'TIMESTAMP_OUTPUT_FORMAT_UNSPECIFIED',
useInt64Timestamp: undefined,
expectedTsValue: expectedTsValueMicroseconds,
},
{
name: 'should call getRows with FLOAT64 and useInt64Timestamp undefined (expect error)',
timestampOutputFormat: 'FLOAT64',
useInt64Timestamp: undefined,
expectedTsValue: expectedTsValueMicroseconds,
},
{
name: 'should call getRows with INT64 and useInt64Timestamp undefined',
timestampOutputFormat: 'INT64',
useInt64Timestamp: undefined,
expectedTsValue: expectedTsValueMicroseconds,
},
{
name: 'should call getRows with ISO8601_STRING and useInt64Timestamp undefined (expect error)',
timestampOutputFormat: 'ISO8601_STRING',
useInt64Timestamp: undefined,
expectedTsValue: expectedTsValueNanoseconds,
},
];

testCases.forEach(
({
name,
timestampOutputFormat,
useInt64Timestamp,
expectedError,
expectedTsValue,
}) => {
it(name, async () => {
const options: {[key: string]: any} = {};
if (timestampOutputFormat !== undefined) {
options['formatOptions.timestampOutputFormat'] =
timestampOutputFormat;
}
if (useInt64Timestamp !== undefined) {
options['formatOptions.useInt64Timestamp'] = useInt64Timestamp;
}

if (expectedError) {
try {
await table.getRows(options);
assert.fail('The call should have thrown an error.');
} catch (e) {
assert.strictEqual((e as Error).message, expectedError);
}
} else {
const [rows] = await table.getRows(options);
assert(rows.length > 0);
assert.strictEqual(rows[0].ts.value, expectedTsValue);
}
});
},
);

it.only('should make a request with ISO8601_STRING when no format options are being used', done => {
(async () => {
const originalRequest = table.request;
const requestPromise: Promise<RequestResponse> = new Promise((resolve, reject) => {
const innerPromise = new Promise((innerResolve, innerReject) => {
innerResolve({});
});
resolve(innerPromise as Promise<RequestResponse>);
})
table.request = (reqOpts) => {
table.request = originalRequest;
if (reqOpts.qs['formatOptions.timestampOutputFormat'] === 'ISO8601_STRING') {
done();
} else {
done(new Error('The default timestampOutputFormat should be ISO8601_STRING'));
}
return requestPromise;
}
await table.getRows({});
})();
});
});
12 changes: 8 additions & 4 deletions test/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2046,7 +2046,8 @@ describe('BigQuery/Table', () => {
assert.strictEqual(reqOpts.uri, '/data');
assert.deepStrictEqual(reqOpts.qs, {
...options,
'formatOptions.useInt64Timestamp': true,
'formatOptions.useInt64Timestamp': false,
Copy link
Contributor

Choose a reason for hiding this comment

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

I think ideally, only one of the formatOptions should be send. So in those samples here, just 'formatOptions.timestampOutputFormat': 'ISO8601_STRING', is in the request options. Also instead of so many variations of system tests, I believe it's going to be easier to exercise the different format options via those tests in this file, so we can assert the correct parameter is being send on the request.

The system tests right now are checking the timestamp format and seeing if it has microsecond vs nanosecond resolution, but you can get nanosecond resolution by passing either formatOptions.useInt64Timestamp = true and 'formatOptions.timestampOutputFormat': 'ISO8601_STRING'

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think ideally, only one of the formatOptions should be send

I think my most recent changes meet this requirement.

Also instead of so many variations of system tests, I believe it's going to be easier to exercise the different format options via those tests in this file

I believe the system tests we have now are more valuable. The return value we get when calling getRows is what really matters here. I don't think we should remove the system tests we have right now and replace them with unit tests.

'formatOptions.timestampOutputFormat': 'ISO8601_STRING',
});
callback(null, {});
};
Expand Down Expand Up @@ -2208,7 +2209,8 @@ describe('BigQuery/Table', () => {
assert.deepStrictEqual(nextQuery, {
a: 'b',
c: 'd',
'formatOptions.useInt64Timestamp': true,
'formatOptions.useInt64Timestamp': false,
'formatOptions.timestampOutputFormat': 'ISO8601_STRING',
pageToken,
});
// Original object isn't affected.
Expand Down Expand Up @@ -2425,7 +2427,8 @@ describe('BigQuery/Table', () => {

table.request = (reqOpts: DecorateRequestOptions, callback: Function) => {
assert.deepStrictEqual(reqOpts.qs, {
'formatOptions.useInt64Timestamp': true,
'formatOptions.useInt64Timestamp': false,
'formatOptions.timestampOutputFormat': 'ISO8601_STRING',
});
callback(null, {});
};
Expand All @@ -2449,7 +2452,8 @@ describe('BigQuery/Table', () => {

table.request = (reqOpts: DecorateRequestOptions, callback: Function) => {
assert.deepStrictEqual(reqOpts.qs, {
'formatOptions.useInt64Timestamp': true,
'formatOptions.useInt64Timestamp': false,
'formatOptions.timestampOutputFormat': 'ISO8601_STRING',
});
callback(null, {});
};
Expand Down
Loading