From 2b2c3e0b8e9c13d23aa54e7a85a861c61410bd4b Mon Sep 17 00:00:00 2001 From: Alvaro Viebrantz Date: Wed, 13 Mar 2024 10:27:31 -0700 Subject: [PATCH] fix: BigQueryTimestamp should keep accepting floats (#1339) --- src/bigquery.ts | 46 +++++++++++++++++++++++++++++++++++----------- test/bigquery.ts | 36 ++++++++++++------------------------ 2 files changed, 47 insertions(+), 35 deletions(-) diff --git a/src/bigquery.ts b/src/bigquery.ts index 0df263dd..0cfc686a 100644 --- a/src/bigquery.ts +++ b/src/bigquery.ts @@ -640,7 +640,8 @@ export class BigQuery extends Service { break; } case 'TIMESTAMP': { - value = BigQuery.timestamp(value); + const pd = new PreciseDate(BigInt(value) * BigInt(1000)); + value = BigQuery.timestamp(pd); break; } case 'GEOGRAPHY': { @@ -881,6 +882,10 @@ export class BigQuery extends Service { * A timestamp represents an absolute point in time, independent of any time * zone or convention such as Daylight Savings Time. * + * The recommended input here is a `Date` or `PreciseDate` class. + * If passing as a `string`, it should be Timestamp literals: https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#timestamp_literals. + * When passing a `number` input, it should be epoch seconds in float representation. + * * @method BigQuery.timestamp * @param {Date|string} value The time. * @@ -890,12 +895,19 @@ export class BigQuery extends Service { * const timestamp = BigQuery.timestamp(new Date()); * ``` */ + static timestamp(value: Date | PreciseDate | string | number) { + return new BigQueryTimestamp(value); + } /** * A timestamp represents an absolute point in time, independent of any time * zone or convention such as Daylight Savings Time. * - * @param {Date|string} value The time. + * The recommended input here is a `Date` or `PreciseDate` class. + * If passing as a `string`, it should be Timestamp literals: https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#timestamp_literals. + * When passing a `number` input, it should be epoch seconds in float representation. + * + * @param {Date|string|string|number} value The time. * * @example * ``` @@ -904,10 +916,6 @@ export class BigQuery extends Service { * const timestamp = bigquery.timestamp(new Date()); * ``` */ - static timestamp(value: Date | PreciseDate | string | number) { - return new BigQueryTimestamp(value); - } - timestamp(value: Date | PreciseDate | string | number) { return BigQuery.timestamp(value); } @@ -2204,6 +2212,11 @@ export class Geography { /** * Timestamp class for BigQuery. + * + * The recommended input here is a `Date` or `PreciseDate` class. + * If passing as a `string`, it should be Timestamp literals: https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#timestamp_literals. + * When passing a `number` input, it should be epoch seconds in float representation. + * */ export class BigQueryTimestamp { value: string; @@ -2217,13 +2230,15 @@ export class BigQueryTimestamp { if (/^\d{4}-\d{1,2}-\d{1,2}/.test(value)) { pd = new PreciseDate(value); } else { - pd = new PreciseDate(BigInt(value) * BigInt(1000)); + const floatValue = Number.parseFloat(value); + if (!Number.isNaN(floatValue)) { + pd = this.fromFloatValue_(floatValue); + } else { + pd = new PreciseDate(value); + } } - } else if (value) { - pd = new PreciseDate(BigInt(value) * BigInt(1000)); } else { - // Nan or 0 - invalid dates - pd = new PreciseDate(value); + pd = this.fromFloatValue_(value); } // to keep backward compatibility, only converts with microsecond // precision if needed. @@ -2233,6 +2248,15 @@ export class BigQueryTimestamp { this.value = new Date(pd.getTime()).toJSON(); } } + + fromFloatValue_(value: number): PreciseDate { + const secs = Math.trunc(value); + // Timestamps in BigQuery have microsecond precision, so we must + // return a round number of microseconds. + const micros = Math.trunc((value - secs) * 1e6 + 0.5); + const pd = new PreciseDate([secs, micros * 1000]); + return pd; + } } /** diff --git a/test/bigquery.ts b/test/bigquery.ts index 914980ac..8efa158d 100644 --- a/test/bigquery.ts +++ b/test/bigquery.ts @@ -471,7 +471,7 @@ describe('BigQuery', () => { f: [ {v: '3'}, {v: 'Milo'}, - {v: now.valueOf() * 1000}, + {v: now.valueOf() * 1000}, // int64 microseconds {v: 'false'}, {v: 'true'}, {v: '5.222330009847'}, @@ -523,7 +523,7 @@ describe('BigQuery', () => { id: 3, name: 'Milo', dob: { - input: now.valueOf() * 1000, + input: new PreciseDate(BigInt(now.valueOf()) * BigInt(1_000_000)), type: 'fakeTimestamp', }, has_claws: false, @@ -850,10 +850,8 @@ describe('BigQuery', () => { describe('timestamp', () => { const INPUT_STRING = '2016-12-06T12:00:00.000Z'; const INPUT_STRING_MICROS = '2016-12-06T12:00:00.123456Z'; - const INPUT_STRING_NEGATIVE = '1969-12-25T00:00:00.000Z'; const INPUT_DATE = new Date(INPUT_STRING); const INPUT_PRECISE_DATE = new PreciseDate(INPUT_STRING_MICROS); - const INPUT_PRECISE_NEGATIVE_DATE = new PreciseDate(INPUT_STRING_NEGATIVE); const EXPECTED_VALUE = INPUT_DATE.toJSON(); const EXPECTED_VALUE_MICROS = INPUT_PRECISE_DATE.toISOString(); @@ -883,31 +881,21 @@ describe('BigQuery', () => { assert.strictEqual(timestamp.value, EXPECTED_VALUE); }); - it('should accept a number in microseconds', () => { - let ms = INPUT_PRECISE_DATE.valueOf(); // milliseconds - let us = ms * 1000 + INPUT_PRECISE_DATE.getMicroseconds(); // microseconds - let timestamp = bq.timestamp(us); - assert.strictEqual(timestamp.value, EXPECTED_VALUE_MICROS); - - let usStr = `${us}`; - timestamp = bq.timestamp(usStr); - assert.strictEqual(timestamp.value, EXPECTED_VALUE_MICROS); - - ms = INPUT_PRECISE_NEGATIVE_DATE.valueOf(); - us = ms * 1000; - timestamp = bq.timestamp(us); - assert.strictEqual(timestamp.value, INPUT_STRING_NEGATIVE); - - usStr = `${us}`; - timestamp = bq.timestamp(usStr); - assert.strictEqual(timestamp.value, INPUT_STRING_NEGATIVE); - }); - it('should accept a string with microseconds', () => { const timestamp = bq.timestamp(INPUT_STRING_MICROS); assert.strictEqual(timestamp.value, EXPECTED_VALUE_MICROS); }); + it('should accept a float number', () => { + const d = new Date(); + const f = d.valueOf() / 1000; // float seconds + let timestamp = bq.timestamp(f); + assert.strictEqual(timestamp.value, d.toJSON()); + + timestamp = bq.timestamp(f.toString()); + assert.strictEqual(timestamp.value, d.toJSON()); + }); + it('should accept a Date object', () => { const timestamp = bq.timestamp(INPUT_DATE); assert.strictEqual(timestamp.value, EXPECTED_VALUE);