From 9ebcea55d705ae3daebda5931855ec15df5ffb16 Mon Sep 17 00:00:00 2001 From: Alvaro Viebrantz Date: Mon, 11 Mar 2024 14:45:16 -0400 Subject: [PATCH 1/5] fix: BigQueryTimestamp should keep accepting floats --- src/bigquery.ts | 29 +++++++++++++++++++++++++++-- test/bigquery.ts | 10 ++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/bigquery.ts b/src/bigquery.ts index 0df263dd..4dd284c3 100644 --- a/src/bigquery.ts +++ b/src/bigquery.ts @@ -2217,10 +2217,10 @@ 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)); + pd = this.fromNumber_(value); } } else if (value) { - pd = new PreciseDate(BigInt(value) * BigInt(1000)); + pd = this.fromNumber_(value); } else { // Nan or 0 - invalid dates pd = new PreciseDate(value); @@ -2233,6 +2233,31 @@ export class BigQueryTimestamp { this.value = new Date(pd.getTime()).toJSON(); } } + + fromNumber_(value: number | string): PreciseDate { + let numValue; + if (typeof value === 'string') { + numValue = Number.parseFloat(value); + if (Number.isNaN(numValue)) { + return new PreciseDate(numValue); // invalid date + } + } else { + numValue = value; + } + if (Number.isInteger(numValue)) { + return new PreciseDate(BigInt(numValue) * BigInt(1000)); + } + return this.fromFloatValue_(numValue); + } + + 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..8277d20d 100644 --- a/test/bigquery.ts +++ b/test/bigquery.ts @@ -883,6 +883,16 @@ describe('BigQuery', () => { assert.strictEqual(timestamp.value, EXPECTED_VALUE); }); + 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 number in microseconds', () => { let ms = INPUT_PRECISE_DATE.valueOf(); // milliseconds let us = ms * 1000 + INPUT_PRECISE_DATE.getMicroseconds(); // microseconds From 37879fdfabe2390c127ff758d39c6e2b46a36088 Mon Sep 17 00:00:00 2001 From: Alvaro Viebrantz Date: Mon, 11 Mar 2024 15:21:14 -0400 Subject: [PATCH 2/5] fix: lint issues --- test/bigquery.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/bigquery.ts b/test/bigquery.ts index 8277d20d..8d604935 100644 --- a/test/bigquery.ts +++ b/test/bigquery.ts @@ -887,10 +887,10 @@ describe('BigQuery', () => { const d = new Date(); const f = d.valueOf() / 1000; // float seconds let timestamp = bq.timestamp(f); - assert.strictEqual(timestamp.value, d.toJSON()); + assert.strictEqual(timestamp.value, d.toJSON()); timestamp = bq.timestamp(f.toString()); - assert.strictEqual(timestamp.value, d.toJSON()); + assert.strictEqual(timestamp.value, d.toJSON()); }); it('should accept a number in microseconds', () => { From 8a0738fb20ea3cc0dafee6144a254407c2e14b33 Mon Sep 17 00:00:00 2001 From: Alvaro Viebrantz Date: Wed, 13 Mar 2024 11:49:00 -0400 Subject: [PATCH 3/5] fix: restore BigQueryTimestamp float parsing and convert int64 timestamp beforehand --- src/bigquery.ts | 31 +++++++++---------------------- test/bigquery.ts | 31 ++----------------------------- 2 files changed, 11 insertions(+), 51 deletions(-) diff --git a/src/bigquery.ts b/src/bigquery.ts index 4dd284c3..de892439 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': { @@ -2217,13 +2218,15 @@ export class BigQueryTimestamp { if (/^\d{4}-\d{1,2}-\d{1,2}/.test(value)) { pd = new PreciseDate(value); } else { - pd = this.fromNumber_(value); + const floatValue = Number.parseFloat(value); + if (!Number.isNaN(floatValue)) { + pd = this.fromFloatValue_(floatValue); + } else { + pd = new PreciseDate(value); + } } - } else if (value) { - pd = this.fromNumber_(value); } 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. @@ -2234,22 +2237,6 @@ export class BigQueryTimestamp { } } - fromNumber_(value: number | string): PreciseDate { - let numValue; - if (typeof value === 'string') { - numValue = Number.parseFloat(value); - if (Number.isNaN(numValue)) { - return new PreciseDate(numValue); // invalid date - } - } else { - numValue = value; - } - if (Number.isInteger(numValue)) { - return new PreciseDate(BigInt(numValue) * BigInt(1000)); - } - return this.fromFloatValue_(numValue); - } - fromFloatValue_(value: number): PreciseDate { const secs = Math.trunc(value); // Timestamps in BigQuery have microsecond precision, so we must diff --git a/test/bigquery.ts b/test/bigquery.ts index 8d604935..d4782d52 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(); @@ -893,31 +891,6 @@ describe('BigQuery', () => { assert.strictEqual(timestamp.value, d.toJSON()); }); - 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 Date object', () => { const timestamp = bq.timestamp(INPUT_DATE); assert.strictEqual(timestamp.value, EXPECTED_VALUE); From 4f6ca808780835b1a2a3520eedd3d3442aa51f3e Mon Sep 17 00:00:00 2001 From: Alvaro Viebrantz Date: Wed, 13 Mar 2024 11:53:51 -0400 Subject: [PATCH 4/5] test: restore microsecond datetime string test --- test/bigquery.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/bigquery.ts b/test/bigquery.ts index d4782d52..8efa158d 100644 --- a/test/bigquery.ts +++ b/test/bigquery.ts @@ -881,6 +881,11 @@ describe('BigQuery', () => { assert.strictEqual(timestamp.value, EXPECTED_VALUE); }); + 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 From 3d59d910ec6b684de85ead76936fc94215035c21 Mon Sep 17 00:00:00 2001 From: Alvaro Viebrantz Date: Wed, 13 Mar 2024 12:45:47 -0400 Subject: [PATCH 5/5] docs: input for .timestamp function and BigQueryTimestamp class --- src/bigquery.ts | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/bigquery.ts b/src/bigquery.ts index de892439..0cfc686a 100644 --- a/src/bigquery.ts +++ b/src/bigquery.ts @@ -882,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. * @@ -891,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 * ``` @@ -905,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); } @@ -2205,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;