From e77bddc802f0652e1d5df89db4f6bc9cdcefc947 Mon Sep 17 00:00:00 2001 From: Alvaro Viebrantz Date: Fri, 10 Mar 2023 18:28:58 -0400 Subject: [PATCH 1/4] feat: add support for microseconds precision --- package.json | 1 + src/bigquery.ts | 44 +++++++++++++++++++++++++++++++++++++++----- test/bigquery.ts | 14 ++++++++++++++ 3 files changed, 54 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index be0f696f..75ca9e02 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "dependencies": { "@google-cloud/common": "^4.0.0", "@google-cloud/paginator": "^4.0.0", + "@google-cloud/precise-date": "^3.0.1", "@google-cloud/promisify": "^3.0.0", "arrify": "^2.0.1", "big.js": "^6.0.0", diff --git a/src/bigquery.ts b/src/bigquery.ts index 2a33c8b3..bf8e0363 100644 --- a/src/bigquery.ts +++ b/src/bigquery.ts @@ -23,6 +23,7 @@ import { } from '@google-cloud/common'; import {paginator, ResourceStream} from '@google-cloud/paginator'; import {promisifyAll} from '@google-cloud/promisify'; +import {PreciseDate} from '@google-cloud/precise-date'; import arrify = require('arrify'); import {Big} from 'big.js'; import * as extend from 'extend'; @@ -585,7 +586,7 @@ export class BigQuery extends Service { break; } case 'TIMESTAMP': { - value = BigQuery.timestamp(new Date(value * 1000)); + value = BigQuery.timestamp(value); break; } case 'GEOGRAPHY': { @@ -844,11 +845,11 @@ export class BigQuery extends Service { * const timestamp = bigquery.timestamp(new Date()); * ``` */ - static timestamp(value: Date | string) { + static timestamp(value: Date | PreciseDate | string | number) { return new BigQueryTimestamp(value); } - timestamp(value: Date | string) { + timestamp(value: Date | PreciseDate | string | number) { return BigQuery.timestamp(value); } @@ -2125,8 +2126,41 @@ export class Geography { */ export class BigQueryTimestamp { value: string; - constructor(value: Date | string) { - this.value = new Date(value).toJSON(); + constructor(value: Date | PreciseDate | string | number) { + let pd: PreciseDate; + if (value instanceof PreciseDate) { + pd = value; + } else if (value instanceof Date) { + pd = new PreciseDate(value); + } else if (typeof value === 'string') { + if (/^\d{4}-\d{1,2}-\d{1,2}/.test(value)) { + pd = new PreciseDate(value); + } else { + const floatValue = Number.parseFloat(value); + if (!Number.isNaN(floatValue)) { + pd = this.fromFloatValue_(floatValue); + } else { + pd = new PreciseDate(value); + } + } + } else { + pd = this.fromFloatValue_(value); + } + // to keep backward compatibility, only + if (pd.getMicroseconds() > 0) { + this.value = pd.toISOString(); + } else { + 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 1d41e5f8..f0ab2c22 100644 --- a/test/bigquery.ts +++ b/test/bigquery.ts @@ -42,6 +42,7 @@ import { TableField, } from '../src'; import {SinonStub} from 'sinon'; +import { PreciseDate } from '@google-cloud/precise-date'; const fakeUuid = extend(true, {}, uuid); @@ -803,8 +804,11 @@ 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_DATE = new Date(INPUT_STRING); + const INPUT_PRECISE_DATE = new PreciseDate(INPUT_STRING_MICROS); const EXPECTED_VALUE = INPUT_DATE.toJSON(); + const EXPECTED_VALUE_MICROS = INPUT_PRECISE_DATE.toISOString(); // tslint:disable-next-line ban it.skip('should expose static and instance constructors', () => { @@ -827,10 +831,20 @@ 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 Date object', () => { const timestamp = bq.timestamp(INPUT_DATE); assert.strictEqual(timestamp.value, EXPECTED_VALUE); }); + + it('should accept a PreciseDate object', () => { + const timestamp = bq.timestamp(INPUT_PRECISE_DATE); + assert.strictEqual(timestamp.value, EXPECTED_VALUE_MICROS); + }); }); describe('geography', () => { From 2e4954537d16268984a0b5c930cf0433c30dd4b0 Mon Sep 17 00:00:00 2001 From: Alvaro Viebrantz Date: Fri, 10 Mar 2023 18:31:37 -0400 Subject: [PATCH 2/4] fix: lint issues --- src/bigquery.ts | 3 ++- test/bigquery.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/bigquery.ts b/src/bigquery.ts index bf8e0363..a4930aa4 100644 --- a/src/bigquery.ts +++ b/src/bigquery.ts @@ -2146,7 +2146,8 @@ export class BigQueryTimestamp { } else { pd = this.fromFloatValue_(value); } - // to keep backward compatibility, only + // to keep backward compatibility, only converts with microsecond + // precision if needed. if (pd.getMicroseconds() > 0) { this.value = pd.toISOString(); } else { diff --git a/test/bigquery.ts b/test/bigquery.ts index f0ab2c22..ecfc5eac 100644 --- a/test/bigquery.ts +++ b/test/bigquery.ts @@ -42,7 +42,7 @@ import { TableField, } from '../src'; import {SinonStub} from 'sinon'; -import { PreciseDate } from '@google-cloud/precise-date'; +import {PreciseDate} from '@google-cloud/precise-date'; const fakeUuid = extend(true, {}, uuid); From 27f09ea8a8bcea1245dd6b8454f95281bd6e31cd Mon Sep 17 00:00:00 2001 From: Alvaro Viebrantz Date: Fri, 10 Mar 2023 19:08:25 -0400 Subject: [PATCH 3/4] fix: test mergeRows with timestamps --- test/bigquery.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/bigquery.ts b/test/bigquery.ts index ecfc5eac..3e47a62e 100644 --- a/test/bigquery.ts +++ b/test/bigquery.ts @@ -454,7 +454,7 @@ describe('BigQuery', () => { f: [ {v: '3'}, {v: 'Milo'}, - {v: String(now.valueOf() / 1000)}, + {v: now.valueOf() * 1000}, {v: 'false'}, {v: 'true'}, {v: '5.222330009847'}, @@ -506,7 +506,7 @@ describe('BigQuery', () => { id: 3, name: 'Milo', dob: { - input: now, + input: now.valueOf() * 1000, type: 'fakeTimestamp', }, has_claws: false, From 6b4f06ff7947378c48f70da39448302ef7a640cc Mon Sep 17 00:00:00 2001 From: Alvaro Viebrantz Date: Fri, 17 Mar 2023 13:13:24 -0400 Subject: [PATCH 4/4] test: add test for NaN with BigQuery.timestamp --- test/bigquery.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/bigquery.ts b/test/bigquery.ts index 3e47a62e..23fe2399 100644 --- a/test/bigquery.ts +++ b/test/bigquery.ts @@ -826,6 +826,11 @@ describe('BigQuery', () => { assert.strictEqual(timestamp.constructor.name, 'BigQueryTimestamp'); }); + it('should accept a NaN', () => { + const timestamp = bq.timestamp(NaN); + assert.strictEqual(timestamp.value, null); + }); + it('should accept a string', () => { const timestamp = bq.timestamp(INPUT_STRING); assert.strictEqual(timestamp.value, EXPECTED_VALUE);