Skip to content

Commit 2b2c3e0

Browse files
authored
fix: BigQueryTimestamp should keep accepting floats (#1339)
1 parent 024b1f6 commit 2b2c3e0

File tree

2 files changed

+47
-35
lines changed

2 files changed

+47
-35
lines changed

src/bigquery.ts

+35-11
Original file line numberDiff line numberDiff line change
@@ -640,7 +640,8 @@ export class BigQuery extends Service {
640640
break;
641641
}
642642
case 'TIMESTAMP': {
643-
value = BigQuery.timestamp(value);
643+
const pd = new PreciseDate(BigInt(value) * BigInt(1000));
644+
value = BigQuery.timestamp(pd);
644645
break;
645646
}
646647
case 'GEOGRAPHY': {
@@ -881,6 +882,10 @@ export class BigQuery extends Service {
881882
* A timestamp represents an absolute point in time, independent of any time
882883
* zone or convention such as Daylight Savings Time.
883884
*
885+
* The recommended input here is a `Date` or `PreciseDate` class.
886+
* If passing as a `string`, it should be Timestamp literals: https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#timestamp_literals.
887+
* When passing a `number` input, it should be epoch seconds in float representation.
888+
*
884889
* @method BigQuery.timestamp
885890
* @param {Date|string} value The time.
886891
*
@@ -890,12 +895,19 @@ export class BigQuery extends Service {
890895
* const timestamp = BigQuery.timestamp(new Date());
891896
* ```
892897
*/
898+
static timestamp(value: Date | PreciseDate | string | number) {
899+
return new BigQueryTimestamp(value);
900+
}
893901

894902
/**
895903
* A timestamp represents an absolute point in time, independent of any time
896904
* zone or convention such as Daylight Savings Time.
897905
*
898-
* @param {Date|string} value The time.
906+
* The recommended input here is a `Date` or `PreciseDate` class.
907+
* If passing as a `string`, it should be Timestamp literals: https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#timestamp_literals.
908+
* When passing a `number` input, it should be epoch seconds in float representation.
909+
*
910+
* @param {Date|string|string|number} value The time.
899911
*
900912
* @example
901913
* ```
@@ -904,10 +916,6 @@ export class BigQuery extends Service {
904916
* const timestamp = bigquery.timestamp(new Date());
905917
* ```
906918
*/
907-
static timestamp(value: Date | PreciseDate | string | number) {
908-
return new BigQueryTimestamp(value);
909-
}
910-
911919
timestamp(value: Date | PreciseDate | string | number) {
912920
return BigQuery.timestamp(value);
913921
}
@@ -2204,6 +2212,11 @@ export class Geography {
22042212

22052213
/**
22062214
* Timestamp class for BigQuery.
2215+
*
2216+
* The recommended input here is a `Date` or `PreciseDate` class.
2217+
* If passing as a `string`, it should be Timestamp literals: https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#timestamp_literals.
2218+
* When passing a `number` input, it should be epoch seconds in float representation.
2219+
*
22072220
*/
22082221
export class BigQueryTimestamp {
22092222
value: string;
@@ -2217,13 +2230,15 @@ export class BigQueryTimestamp {
22172230
if (/^\d{4}-\d{1,2}-\d{1,2}/.test(value)) {
22182231
pd = new PreciseDate(value);
22192232
} else {
2220-
pd = new PreciseDate(BigInt(value) * BigInt(1000));
2233+
const floatValue = Number.parseFloat(value);
2234+
if (!Number.isNaN(floatValue)) {
2235+
pd = this.fromFloatValue_(floatValue);
2236+
} else {
2237+
pd = new PreciseDate(value);
2238+
}
22212239
}
2222-
} else if (value) {
2223-
pd = new PreciseDate(BigInt(value) * BigInt(1000));
22242240
} else {
2225-
// Nan or 0 - invalid dates
2226-
pd = new PreciseDate(value);
2241+
pd = this.fromFloatValue_(value);
22272242
}
22282243
// to keep backward compatibility, only converts with microsecond
22292244
// precision if needed.
@@ -2233,6 +2248,15 @@ export class BigQueryTimestamp {
22332248
this.value = new Date(pd.getTime()).toJSON();
22342249
}
22352250
}
2251+
2252+
fromFloatValue_(value: number): PreciseDate {
2253+
const secs = Math.trunc(value);
2254+
// Timestamps in BigQuery have microsecond precision, so we must
2255+
// return a round number of microseconds.
2256+
const micros = Math.trunc((value - secs) * 1e6 + 0.5);
2257+
const pd = new PreciseDate([secs, micros * 1000]);
2258+
return pd;
2259+
}
22362260
}
22372261

22382262
/**

test/bigquery.ts

+12-24
Original file line numberDiff line numberDiff line change
@@ -471,7 +471,7 @@ describe('BigQuery', () => {
471471
f: [
472472
{v: '3'},
473473
{v: 'Milo'},
474-
{v: now.valueOf() * 1000},
474+
{v: now.valueOf() * 1000}, // int64 microseconds
475475
{v: 'false'},
476476
{v: 'true'},
477477
{v: '5.222330009847'},
@@ -523,7 +523,7 @@ describe('BigQuery', () => {
523523
id: 3,
524524
name: 'Milo',
525525
dob: {
526-
input: now.valueOf() * 1000,
526+
input: new PreciseDate(BigInt(now.valueOf()) * BigInt(1_000_000)),
527527
type: 'fakeTimestamp',
528528
},
529529
has_claws: false,
@@ -850,10 +850,8 @@ describe('BigQuery', () => {
850850
describe('timestamp', () => {
851851
const INPUT_STRING = '2016-12-06T12:00:00.000Z';
852852
const INPUT_STRING_MICROS = '2016-12-06T12:00:00.123456Z';
853-
const INPUT_STRING_NEGATIVE = '1969-12-25T00:00:00.000Z';
854853
const INPUT_DATE = new Date(INPUT_STRING);
855854
const INPUT_PRECISE_DATE = new PreciseDate(INPUT_STRING_MICROS);
856-
const INPUT_PRECISE_NEGATIVE_DATE = new PreciseDate(INPUT_STRING_NEGATIVE);
857855
const EXPECTED_VALUE = INPUT_DATE.toJSON();
858856
const EXPECTED_VALUE_MICROS = INPUT_PRECISE_DATE.toISOString();
859857

@@ -883,31 +881,21 @@ describe('BigQuery', () => {
883881
assert.strictEqual(timestamp.value, EXPECTED_VALUE);
884882
});
885883

886-
it('should accept a number in microseconds', () => {
887-
let ms = INPUT_PRECISE_DATE.valueOf(); // milliseconds
888-
let us = ms * 1000 + INPUT_PRECISE_DATE.getMicroseconds(); // microseconds
889-
let timestamp = bq.timestamp(us);
890-
assert.strictEqual(timestamp.value, EXPECTED_VALUE_MICROS);
891-
892-
let usStr = `${us}`;
893-
timestamp = bq.timestamp(usStr);
894-
assert.strictEqual(timestamp.value, EXPECTED_VALUE_MICROS);
895-
896-
ms = INPUT_PRECISE_NEGATIVE_DATE.valueOf();
897-
us = ms * 1000;
898-
timestamp = bq.timestamp(us);
899-
assert.strictEqual(timestamp.value, INPUT_STRING_NEGATIVE);
900-
901-
usStr = `${us}`;
902-
timestamp = bq.timestamp(usStr);
903-
assert.strictEqual(timestamp.value, INPUT_STRING_NEGATIVE);
904-
});
905-
906884
it('should accept a string with microseconds', () => {
907885
const timestamp = bq.timestamp(INPUT_STRING_MICROS);
908886
assert.strictEqual(timestamp.value, EXPECTED_VALUE_MICROS);
909887
});
910888

889+
it('should accept a float number', () => {
890+
const d = new Date();
891+
const f = d.valueOf() / 1000; // float seconds
892+
let timestamp = bq.timestamp(f);
893+
assert.strictEqual(timestamp.value, d.toJSON());
894+
895+
timestamp = bq.timestamp(f.toString());
896+
assert.strictEqual(timestamp.value, d.toJSON());
897+
});
898+
911899
it('should accept a Date object', () => {
912900
const timestamp = bq.timestamp(INPUT_DATE);
913901
assert.strictEqual(timestamp.value, EXPECTED_VALUE);

0 commit comments

Comments
 (0)