Skip to content

Commit

Permalink
feat: spanner NUMERIC support (#1163)
Browse files Browse the repository at this point in the history
* deps: import big.js to represent NUMERIC

* fix: added NUMERIC support in codec

* test: added unit tests for NUMERIC support

* fix: lint

* fix: type issue

* fix: export numeric type from Spanner

* test: added system tests for NUMERIC type

* fix: lint

* test: skip NUMERIC tests on emulator

* fix: added NUMERIC code samples and tests

* fix: promisified numeric

* fix: typo in test description

* test: added test for Spanner.numeric().

* fix: lint
  • Loading branch information
skuruppu authored Sep 4, 2020
1 parent 7046d0b commit 4724ba3
Show file tree
Hide file tree
Showing 8 changed files with 330 additions and 27 deletions.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,10 @@
"@google-cloud/precise-date": "^2.0.0",
"@google-cloud/projectify": "^2.0.0",
"@google-cloud/promisify": "^2.0.0",
"@types/big.js": "^4.0.5",
"@types/stack-trace": "0.0.29",
"arrify": "^2.0.0",
"big.js": "^5.2.2",
"checkpoint-stream": "^0.1.1",
"events-intercept": "^2.0.0",
"extend": "^3.0.2",
Expand All @@ -75,6 +77,7 @@
},
"devDependencies": {
"@grpc/proto-loader": "^0.5.1",
"@types/big.js": "^4.0.5",
"@types/concat-stream": "^1.6.0",
"@types/extend": "^3.0.0",
"@types/is": "0.0.21",
Expand Down
74 changes: 73 additions & 1 deletion samples/datatypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,15 @@ async function createVenuesTable(instanceId, databaseId, projectId) {

const request = [
`CREATE TABLE Venues (
VenueId INT64 NOT NULL,
VenueId INT64 NOT NULL,
VenueName STRING(100),
VenueInfo BYTES(MAX),
Capacity INT64,
AvailableDates ARRAY<DATE>,
LastContactDate Date,
OutdoorVenue BOOL,
PopularityScore FLOAT64,
Revenue NUMERIC,
LastUpdateTime TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true)
) PRIMARY KEY (VenueId)`,
];
Expand Down Expand Up @@ -102,6 +103,7 @@ async function insertData(instanceId, databaseId, projectId) {
LastContactDate: '2018-09-02',
OutdoorVenue: false,
PopularityScore: 0.85543,
Revenue: Spanner.numeric('215100.10'),
LastUpdateTime: 'spanner.commit_timestamp()',
},
{
Expand All @@ -113,6 +115,7 @@ async function insertData(instanceId, databaseId, projectId) {
LastContactDate: '2019-01-15',
OutdoorVenue: true,
PopularityScore: 0.98716,
Revenue: Spanner.numeric('1200100.00'),
LastUpdateTime: 'spanner.commit_timestamp()',
},
{
Expand All @@ -124,6 +127,7 @@ async function insertData(instanceId, databaseId, projectId) {
LastContactDate: '2018-10-01',
OutdoorVenue: false,
PopularityScore: 0.72598,
Revenue: Spanner.numeric('390650.99'),
LastUpdateTime: 'spanner.commit_timestamp()',
},
];
Expand Down Expand Up @@ -607,6 +611,64 @@ async function queryWithTimestamp(instanceId, databaseId, projectId) {
// [END spanner_query_with_timestamp_parameter]
}

async function queryWithNumeric(instanceId, databaseId, projectId) {
// [START spanner_query_with_numeric_parameter]
// Imports the Google Cloud client library.
const {Spanner} = require('@google-cloud/spanner');

/**
* TODO(developer): Uncomment the following lines before running the sample.
*/
// const projectId = 'my-project-id';
// const instanceId = 'my-instance';
// const databaseId = 'my-database';

// Creates a client
const spanner = new Spanner({
projectId: projectId,
});

// Gets a reference to a Cloud Spanner instance and database.
const instance = spanner.instance(instanceId);
const database = instance.database(databaseId);

const fieldType = {
type: 'numeric',
};

const exampleNumeric = Spanner.numeric('300000');

const query = {
sql: `SELECT VenueId, VenueName, Revenue FROM Venues
WHERE Revenue >= @revenue`,
params: {
revenue: exampleNumeric,
},
types: {
revenue: fieldType,
},
};

// Queries rows from the Venues table.
try {
const [rows] = await database.run(query);

rows.forEach(row => {
const json = row.toJSON();
console.log(
`VenueId: ${json.VenueId}, VenueName: ${json.VenueName},` +
` Revenue: ${json.Revenue.value}`
);
});
} catch (err) {
console.error('ERROR:', err);
} finally {
// Close the database when finished.
database.close();
}
// [END spanner_query_with_numeric_parameter]
}

require('yargs')
.demand(1)
.command(
Expand Down Expand Up @@ -672,6 +734,13 @@ require('yargs')
opts =>
queryWithTimestamp(opts.instanceName, opts.databaseName, opts.projectId)
)
.command(
'queryWithNumeric <instanceName> <databaseName> <projectId>',
"Query data from the sample 'Venues' table with a NUMERIC datatype.",
{},
opts =>
queryWithNumeric(opts.instanceName, opts.databaseName, opts.projectId)
)
.example(
'node $0 createVenuesTable "my-instance" "my-database" "my-project-id"'
)
Expand All @@ -688,6 +757,9 @@ require('yargs')
.example(
'node $0 queryWithTimestamp "my-instance" "my-database" "my-project-id"'
)
.example(
'node $0 queryWithNumeric "my-instance" "my-database" "my-project-id"'
)
.wrap(120)
.recommendCommands()
.epilogue('For more information, see https://cloud.google.com/spanner/docs')
Expand Down
12 changes: 12 additions & 0 deletions samples/system-test/spanner.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -774,6 +774,18 @@ describe('Spanner', () => {
assert.match(output, /VenueId: 42, VenueName: Venue 42, LastUpdateTime:/);
});

// query_with_numeric_parameter
it('should use a NUMERIC query parameter to query record from the Venues example table', async () => {
const output = execSync(
`${datatypesCmd} queryWithNumeric ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}`
);
assert.match(output, /VenueId: 19, VenueName: Venue 19, Revenue: 1200100/);
assert.match(
output,
/VenueId: 42, VenueName: Venue 42, Revenue: 390650.99/
);
});

// create_backup
it('should create a backup of the database', async () => {
const output = execSync(
Expand Down
33 changes: 33 additions & 0 deletions src/codec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import {GrpcService} from './common-grpc/service';
import {PreciseDate} from '@google-cloud/precise-date';
import arrify = require('arrify');
import {Big} from 'big.js';
import * as is from 'is';
import {common as p} from 'protobufjs';
import {google as spannerClient} from '../protos/protos';
Expand Down Expand Up @@ -194,6 +195,23 @@ export class Struct extends Array<Field> {
}
}

/**
* @typedef Numeric
* @see Spanner.numeric
*/
export class Numeric {
value: string;
constructor(value: string) {
this.value = value;
}
valueOf(): Big {
return new Big(this.value);
}
toJSON(): string {
return this.valueOf().toJSON();
}
}

/**
* @typedef JSONOptions
* @property {boolean} [wrapNumbers=false] Indicates if the numbers should be
Expand Down Expand Up @@ -299,6 +317,10 @@ function decode(value: Value, type: spannerClient.spanner.v1.Type): Value {
case 'INT64':
decoded = new Int(decoded);
break;
case spannerClient.spanner.v1.TypeCode.NUMERIC:
case 'NUMERIC':
decoded = new Numeric(decoded);
break;
case spannerClient.spanner.v1.TypeCode.TIMESTAMP:
case 'TIMESTAMP':
decoded = new PreciseDate(decoded);
Expand Down Expand Up @@ -369,6 +391,10 @@ function encodeValue(value: Value): Value {
return value.value;
}

if (value instanceof Numeric) {
return value.value;
}

if (Buffer.isBuffer(value)) {
return value.toString('base64');
}
Expand Down Expand Up @@ -397,6 +423,7 @@ const TypeCode: {
bool: 'BOOL',
int64: 'INT64',
float64: 'FLOAT64',
numeric: 'NUMERIC',
timestamp: 'TIMESTAMP',
date: 'DATE',
string: 'STRING',
Expand Down Expand Up @@ -430,6 +457,7 @@ interface FieldType extends Type {
* @property {string} type The param type. Must be one of the following:
* - float64
* - int64
* - numeric
* - bool
* - string
* - bytes
Expand Down Expand Up @@ -466,6 +494,10 @@ function getType(value: Value): Type {
return {type: 'int64'};
}

if (value instanceof Numeric) {
return {type: 'numeric'};
}

if (is.boolean(value)) {
return {type: 'bool'};
}
Expand Down Expand Up @@ -611,6 +643,7 @@ export const codec = {
SpannerDate,
Float,
Int,
Numeric,
convertFieldsToJson,
decode,
encode,
Expand Down
26 changes: 24 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import * as path from 'path';
import {common as p} from 'protobufjs';
import * as streamEvents from 'stream-events';
import * as through from 'through2';
import {codec, Float, Int, SpannerDate, Struct} from './codec';
import {codec, Float, Int, Numeric, SpannerDate, Struct} from './codec';
import {Backup} from './backup';
import {Database} from './database';
import {
Expand Down Expand Up @@ -1057,6 +1057,20 @@ class Spanner extends GrpcService {
return new codec.Int(value);
}

/**
* Helper function to get a Cloud Spanner Numeric object.
*
* @param {string} value The numeric value as a string.
* @returns {Numeric}
*
* @example
* const {Spanner} = require('@google-cloud/spanner');
* const numeric = Spanner.numeric("3.141592653");
*/
static numeric(value): Numeric {
return new codec.Numeric(value);
}

/**
* Helper function to get a Cloud Spanner Struct object.
*
Expand Down Expand Up @@ -1084,7 +1098,15 @@ class Spanner extends GrpcService {
* that a callback is omitted.
*/
promisifyAll(Spanner, {
exclude: ['date', 'float', 'instance', 'int', 'operation', 'timestamp'],
exclude: [
'date',
'float',
'instance',
'int',
'numeric',
'operation',
'timestamp',
],
});

/**
Expand Down
Loading

0 comments on commit 4724ba3

Please sign in to comment.