Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,12 @@
"@google-cloud/paginator": "^6.0.0",
"@google-cloud/precise-date": "^5.0.0",
"@google-cloud/promisify": "^5.0.0",
"teeny-request": "^10.0.0",
"arrify": "^3.0.0",
"big.js": "^7.0.0",
"duplexify": "^4.1.3",
"extend": "^3.0.2",
"stream-events": "^1.0.5"
"stream-events": "^1.0.5",
"teeny-request": "^10.0.0"
},
"overrides": {
"@google-cloud/common": {
Expand Down
81 changes: 71 additions & 10 deletions src/bigquery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,17 +376,17 @@
private _universeDomain: string;
private _defaultJobCreationMode: JobCreationMode;

createQueryStream(options?: Query | string): ResourceStream<RowMetadata> {

Check warning on line 379 in src/bigquery.ts

View workflow job for this annotation

GitHub Actions / lint

'options' is defined but never used
// placeholder body, overwritten in constructor
return new ResourceStream<RowMetadata>({}, () => {});
}

getDatasetsStream(options?: GetDatasetsOptions): ResourceStream<Dataset> {

Check warning on line 384 in src/bigquery.ts

View workflow job for this annotation

GitHub Actions / lint

'options' is defined but never used
// placeholder body, overwritten in constructor
return new ResourceStream<Dataset>({}, () => {});
}

getJobsStream(options?: GetJobsOptions): ResourceStream<Job> {

Check warning on line 389 in src/bigquery.ts

View workflow job for this annotation

GitHub Actions / lint

'options' is defined but never used
// placeholder body, overwritten in constructor
return new ResourceStream<Job>({}, () => {});
}
Expand Down Expand Up @@ -1596,7 +1596,7 @@
const parameterMode = isArray(params) ? 'positional' : 'named';
const queryParameters: bigquery.IQueryParameter[] = [];
if (parameterMode === 'named') {
const namedParams = params as {[param: string]: any};

Check warning on line 1599 in src/bigquery.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
for (const namedParameter of Object.getOwnPropertyNames(namedParams)) {
const value = namedParams[namedParameter];
let queryParameter;
Expand Down Expand Up @@ -2244,7 +2244,7 @@

options = extend({job}, queryOpts, options);
if (res && res.jobComplete) {
let rows: any = [];

Check warning on line 2247 in src/bigquery.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
if (res.schema && res.rows) {
rows = BigQuery.mergeSchemaWithRows_(res.schema, res.rows, {
wrapIntegers: options.wrapIntegers || false,
Expand Down Expand Up @@ -2720,41 +2720,102 @@
value: string;
constructor(value: Date | PreciseDate | string | number) {
let pd: PreciseDate;
let originalValue: string | undefined;

if (value instanceof PreciseDate) {
pd = value;
} else if (value instanceof Date) {
pd = new PreciseDate(value);
} else if (typeof value === 'string') {
originalValue = value;
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);
// If the string has more than 9 fractional digits, PreciseDate parsing
// might fail or misinterpret it (e.g. treating excess digits as seconds).
// We truncate to 9 digits for the PreciseDate object.
const match = value.match(/\.(\d+)/);
if (match && match[1].length > 9) {
const truncatedValue = value.replace(

Check failure on line 2737 in src/bigquery.ts

View workflow job for this annotation

GitHub Actions / lint

Replace `⏎············match[1],⏎············match[1].slice(0,·9)⏎··········` with `match[1],·match[1].slice(0,·9)`
match[1],
match[1].slice(0, 9)
);
pd = new PreciseDate(truncatedValue);
} else {
pd = new PreciseDate(value);
}
} else {
// Numeric string (epoch seconds).
// Parse manually to avoid precision loss from parseFloat.
pd = this.fromNumericString_(value);
}
} else {
pd = this.fromFloatValue_(value);
}

// to keep backward compatibility, only converts with microsecond
// precision if needed.
if (pd.getMicroseconds() > 0) {
if (pd.getMicroseconds() > 0 || pd.getNanoseconds() > 0) {
this.value = pd.toISOString();
} else {
this.value = new Date(pd.getTime()).toJSON();
}

// If the original input string had higher precision than what PreciseDate
// could store (nanoseconds), or if we parsed a high-precision numeric string,
// we should try to preserve that precision in the string representation.
if (typeof originalValue === 'string') {
if (/^\d{4}-\d{1,2}-\d{1,2}/.test(originalValue)) {
// ISO string. If it had > 9 fractional digits, restore them.
const match = originalValue.match(/\.(\d+)/);
if (match && match[1].length > 9) {
this.value = originalValue;
}
} else {
// Numeric string.
// We want to verify if the original numeric string had more precision
// than the generated ISO string.
// Note: originalValue might be scientific notation or just large fraction.
// We only replace if we successfully identified it as decimal fraction > 9 digits.
const parts = originalValue.split('.');
if (
parts.length === 2 &&
parts[1].length > 9 &&
!/e/i.test(originalValue)
) {
// We have > 9 digits of fraction.
// Replace the fraction in the generated ISO string.
// pd.toISOString() returns 9 digits of fraction.
this.value = this.value.replace(/\.\d+Z$/, `.${parts[1]}Z`);
}
}
}
}

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]);
// Timestamps in BigQuery can have picosecond precision, but float only supports
// limited precision. We use best effort here (nanoseconds).
const nanos = Math.trunc((value - secs) * 1e9 + 0.5);
const pd = new PreciseDate([secs, nanos]);
return pd;
}

fromNumericString_(value: string): PreciseDate {
// Use big.js to handle precision if possible, otherwise fallback.
try {
const bigVal = new Big(value);
// round(0, 0) rounds to 0 decimal places, rounding down (towards zero).
const secsBig = bigVal.round(0, 0);
const secsNum = secsBig.toNumber();
const subSeconds = bigVal.minus(secsBig);
// subSeconds is e.g. 0.123456789123
const nanos = subSeconds.times(1e9).toNumber();
return new PreciseDate([secsNum, Math.trunc(nanos)]);
} catch (e) {
// If parsing fails (e.g. not a valid number), fallback to PreciseDate default
// which might result in Invalid Date.
return new PreciseDate(value);
}
}
}

/**
Expand Down
10 changes: 8 additions & 2 deletions test/bigquery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -972,10 +972,16 @@ describe('BigQuery', () => {
const d = new Date();
const f = d.valueOf() / 1000; // float seconds
let timestamp = bq.timestamp(f);
assert.strictEqual(timestamp.value, d.toJSON());

// Verify it is a valid timestamp string representing the same time.
// Note: We use Date comparison because string comparison might fail due to
// precision differences (PreciseDate uses 9 fractional digits vs Date's 3).
const tsDate = new Date(timestamp.value);
assert.strictEqual(tsDate.valueOf(), d.valueOf());

timestamp = bq.timestamp(f.toString());
assert.strictEqual(timestamp.value, d.toJSON());
const tsDate2 = new Date(timestamp.value);
assert.strictEqual(tsDate2.valueOf(), d.valueOf());
});

it('should accept a Date object', () => {
Expand Down
Loading