diff --git a/src/codec.ts b/src/codec.ts index 1c49d8599..dff450564 100644 --- a/src/codec.ts +++ b/src/codec.ts @@ -21,6 +21,7 @@ import * as is from 'is'; import {common as p} from 'protobufjs'; import {google as spannerClient} from '../protos/protos'; import {GoogleError} from 'google-gax'; +import * as uuid from 'uuid'; // eslint-disable-next-line @typescript-eslint/no-explicit-any export type Value = any; @@ -697,6 +698,7 @@ const TypeCode: { bool: 'BOOL', int64: 'INT64', pgOid: 'INT64', + uuid: 'UUID', float32: 'FLOAT32', float64: 'FLOAT64', numeric: 'NUMERIC', @@ -737,6 +739,7 @@ interface FieldType extends Type { /** * @typedef {object} ParamType * @property {string} type The param type. Must be one of the following: + * - uuid * - float32 * - float64 * - int64 @@ -814,6 +817,10 @@ function getType(value: Value): Type { return {type: 'bool'}; } + if (uuid.validate(value)) { + return {type: 'unspecified'}; + } + if (is.string(value)) { return {type: 'string'}; } diff --git a/src/transaction.ts b/src/transaction.ts index f774f5567..35606864f 100644 --- a/src/transaction.ts +++ b/src/transaction.ts @@ -1521,7 +1521,15 @@ export class Snapshot extends EventEmitter { if (!is.empty(typeMap)) { Object.keys(typeMap).forEach(param => { const type = typeMap[param]; - paramTypes[param] = codec.createTypeObject(type); + const typeObject = codec.createTypeObject(type); + if ( + (type.child && + typeObject.code === 'ARRAY' && + typeObject.arrayElementType?.code !== 'TYPE_CODE_UNSPECIFIED') || + (!type.child && typeObject.code !== 'TYPE_CODE_UNSPECIFIED') + ) { + paramTypes[param] = codec.createTypeObject(type); + } }); } diff --git a/system-test/spanner.ts b/system-test/spanner.ts index 09996119a..35323715d 100644 --- a/system-test/spanner.ts +++ b/system-test/spanner.ts @@ -427,6 +427,7 @@ describe('Spanner', () => { before(async () => { if (IS_EMULATOR_ENABLED) { // TODO: add column Float32Value FLOAT32 and FLOAT32Array Array while using float32 feature. + // TODO: add column UUIDValue UUID and UUIDArray Array while using uuid feature. const [googleSqlOperationUpdateDDL] = await DATABASE.updateSchema( ` CREATE TABLE ${TABLE_NAME} @@ -454,6 +455,7 @@ describe('Spanner', () => { ); await googleSqlOperationUpdateDDL.promise(); // TODO: add column Float32Value DOUBLE PRECISION and FLOAT32Array DOUBLE PRECISION[] while using float32 feature. + // TODO: add column UUIDValue UUID and UUIDArray UUID[] while using uuid feature. const [postgreSqlOperationUpdateDDL] = await PG_DATABASE.updateSchema( ` CREATE TABLE ${TABLE_NAME} @@ -484,6 +486,7 @@ describe('Spanner', () => { await postgreSqlOperationUpdateDDL.promise(); } else { // TODO: add column Float32Value FLOAT32 and FLOAT32Array Array while using float32 feature. + // TODO: add column UUIDValue UUID and UUIDArray Array while using uuid feature. const [googleSqlOperationUpdateDDL] = await DATABASE.updateSchema( ` CREATE TABLE ${TABLE_NAME} @@ -517,6 +520,7 @@ describe('Spanner', () => { ); await googleSqlOperationUpdateDDL.promise(); // TODO: add column Float32Value DOUBLE PRECISION and FLOAT32Array DOUBLE PRECISION[] while using float32 feature. + // TODO: add column UUIDValue UUID and UUIDArray UUID[] while using uuid feature. const [postgreSqlOperationUpdateDDL] = await PG_DATABASE.updateSchema( ` CREATE TABLE ${TABLE_NAME} @@ -930,6 +934,68 @@ describe('Spanner', () => { }); }); + // TODO: Enable when the uuid feature has been released. + describe.skip('uuids', () => { + const uuidInsert = (done, dialect, value) => { + insert({UUIDValue: value}, dialect, (err, row) => { + assert.ifError(err); + if (typeof value === 'object' && value !== null) { + value = value.value; + } + assert.deepStrictEqual(row.toJSON().UUIDValue, value); + done(); + }); + }; + + it('GOOGLE_STANDARD_SQL should write uuid values', done => { + uuidInsert(done, Spanner.GOOGLE_STANDARD_SQL, uuid.v4()); + }); + + it('POSTGRESQL should write uuid values', done => { + uuidInsert(done, Spanner.POSTGRESQL, uuid.v4()); + }); + + it('GOOGLE_STANDARD_SQL should write empty uuid array values', done => { + insert({UUIDArray: []}, Spanner.GOOGLE_STANDARD_SQL, (err, row) => { + assert.ifError(err); + assert.deepStrictEqual(row.toJSON().UUIDArray, []); + done(); + }); + }); + + it('POSTGRESQL should write empty uuid array values', done => { + insert({UUIDArray: []}, Spanner.POSTGRESQL, (err, row) => { + assert.ifError(err); + assert.deepStrictEqual(row.toJSON().UUIDArray, []); + done(); + }); + }); + + it('GOOGLE_STANDARD_SQL should write uuid array values', done => { + const values = [uuid.v4(), uuid.v4(), uuid.v4()]; + + insert({UUIDArray: values}, Spanner.GOOGLE_STANDARD_SQL, (err, row) => { + assert.ifError(err); + for (let i = 0; i < values.length; i++) { + assert.deepStrictEqual(row.toJSON().UUIDArray[i], values[i]); + } + done(); + }); + }); + + it('POSTGRESQL should write uuid array values', done => { + const values = [uuid.v4(), uuid.v4(), uuid.v4()]; + + insert({UUIDArray: values}, Spanner.POSTGRESQL, (err, row) => { + assert.ifError(err); + for (let i = 0; i < values.length; i++) { + assert.deepStrictEqual(row.toJSON().UUIDArray[i], values[i]); + } + done(); + }); + }); + }); + // TODO: Enable when the float32 feature has been released. describe.skip('float32s', () => { const float32Insert = (done, dialect, value) => { @@ -4016,6 +4082,7 @@ describe('Spanner', () => { before(async () => { // TODO: Add column Float32 FLOAT32 while using float32 feature. + // TODO: Add column UUID UUID while using uuid feature. const googleSqlCreateTable = await googleSqlTable.create( `CREATE TABLE ${TABLE_NAME} ( @@ -4035,6 +4102,7 @@ describe('Spanner', () => { await onPromiseOperationComplete(googleSqlCreateTable); // TODO: Add column "Float32" DOUBLE PRECISION while using float32 feature. + // TODO: Add column "UUID" UUID while using uuid feature. const postgreSqlCreateTable = await postgreSqlTable.create( `CREATE TABLE ${TABLE_NAME} ( @@ -4553,6 +4621,7 @@ describe('Spanner', () => { describe('insert & query', () => { const ID = generateName('id'); const NAME = generateName('name'); + // const UUID = uuid.v4(); // TODO: Uncomment while using uuid feature. // const FLOAT32 = 8.2; // TODO: Uncomment while using float32 feature. const FLOAT = 8.2; const INT = 2; @@ -4566,6 +4635,7 @@ describe('Spanner', () => { const GOOGLE_SQL_INSERT_ROW = { SingerId: ID, Name: NAME, + // UUID: UUID, // TODO: Uncomment while using uuid feature. // Float32: FLOAT32, // TODO: Uncomment while using float32 feature. Float: FLOAT, Int: INT, @@ -4580,6 +4650,7 @@ describe('Spanner', () => { const POSTGRESQL_INSERT_ROW = { SingerId: ID, Name: NAME, + // UUID: UUID, // TODO: Uncomment while using uuid feature. // Float32: FLOAT32, // TODO: Uncomment while using float32 feature. Float: FLOAT, Int: INT, @@ -4797,8 +4868,10 @@ describe('Spanner', () => { assert.strictEqual(metadata.rowType!.fields!.length, 10); assert.strictEqual(metadata.rowType!.fields![0].name, 'SingerId'); assert.strictEqual(metadata.rowType!.fields![1].name, 'Name'); - // TODO: Uncomment while using float32 feature and increase the index by 1 for all the asserts below this. - // assert.strictEqual(metadata.rowType!.fields![2].name, 'Float32'); + // TODO: Uncomment while using uuid feature. + // assert.strictEqual(metadata.rowType!.fields![2].name, 'uuid'); + // TODO: Uncomment while using float32 feature and increase the index by 2(if using float32 otherwise increase the index by 1) for all the asserts below this. + // assert.strictEqual(metadata.rowType!.fields![3].name, 'Float32'); assert.strictEqual(metadata.rowType!.fields![2].name, 'Float'); assert.strictEqual(metadata.rowType!.fields![3].name, 'Int'); assert.strictEqual(metadata.rowType!.fields![4].name, 'Info'); @@ -4820,8 +4893,10 @@ describe('Spanner', () => { assert.strictEqual(metadata.rowType!.fields!.length, 7); assert.strictEqual(metadata.rowType!.fields![0].name, 'SingerId'); assert.strictEqual(metadata.rowType!.fields![1].name, 'Name'); - // uncomment while using float32 feature and increase the index by 1 for all the asserts below this. - // assert.strictEqual(metadata.rowType!.fields![2].name, 'Float32'); + // uncomment while using uuid feature. + // assert.strictEqual(metadata.rowType!.fields![2].name, 'UUID'); + // uncomment while using float32 feature and increase the index by 2(if using Float32 otherwise increase the index by 1) for all the asserts below this. + // assert.strictEqual(metadata.rowType!.fields![3].name, 'Float32'); assert.strictEqual(metadata.rowType!.fields![2].name, 'Float'); assert.strictEqual(metadata.rowType!.fields![3].name, 'Int'); assert.strictEqual(metadata.rowType!.fields![4].name, 'Info'); @@ -5170,6 +5245,196 @@ describe('Spanner', () => { }); }); + // TODO: Enable when the uuid feature has been released. + describe.skip('uuid', () => { + const uuidQuery = (done, database, query, value) => { + database.run(query, (err, rows) => { + assert.ifError(err); + const queriedValue = rows[0][0].value; + assert.deepStrictEqual(queriedValue, value); + done(); + }); + }; + + it('GOOGLE_STANDARD_SQL should bind the value when param type uuid is used', done => { + const value = uuid.v4(); + const query = { + sql: 'SELECT @v', + params: { + v: value, + }, + types: { + v: 'uuid', + }, + }; + uuidQuery(done, DATABASE, query, value); + }); + + it('GOOGLE_STANDARD_SQL should bind the value as uuid when param type is not specified', async () => { + const val = uuid.v4(); + const id = generateName('id'); + try { + await googleSqlTable.insert({SingerId: id, UUID: val}); + const query = { + sql: + 'SELECT SingerId, UUID FROM `' + + googleSqlTable.name + + '` WHERE UUID = @v', + params: { + v: val, + }, + }; + const [rows] = await DATABASE.run(query); + assert.strictEqual(rows[0][0].value, id); + assert.strictEqual(uuid.validate(rows[0][1].value), true); + } catch (err) { + assert.ifError(err); + } + }); + + it('POSTGRESQL should bind the value when param type uuid is used', done => { + const value = uuid.v4(); + const query = { + sql: 'SELECT $1', + params: { + p1: value, + }, + types: { + p1: 'uuid', + }, + }; + uuidQuery(done, PG_DATABASE, query, value); + }); + + it('POSTGRESQL should bind the value as uuid when param type is not specified', async () => { + const val = uuid.v4(); + const id = generateName('id'); + try { + await postgreSqlTable.insert({SingerId: id, UUID: val}); + const query = { + sql: + 'SELECT "SingerId", "UUID" FROM ' + + postgreSqlTable.name + + ' WHERE "UUID" = $1', + params: { + p1: val, + }, + }; + const [rows] = await PG_DATABASE.run(query); + assert.strictEqual(rows[0][0].value, id); + assert.strictEqual(uuid.validate(rows[0][1].value), true); + } catch (err) { + assert.ifError(err); + } + }); + + it('GOOGLE_STANDARD_SQL should bind arrays', done => { + const values = [uuid.v4(), uuid.v4(), uuid.v4()]; + + const query = { + sql: 'SELECT @v', + params: { + v: values, + }, + types: { + v: { + type: 'array', + child: 'uuid', + }, + }, + }; + + DATABASE.run(query, (err, rows) => { + assert.ifError(err); + + const expected = values.map(val => { + return val; + }); + + for (let i = 0; i < rows[0][0].value.length; i++) { + assert.deepStrictEqual(rows[0][0].value[i], expected[i]); + } + done(); + }); + }); + + it('GOOGLE_STANDARD_SQL should bind empty arrays', done => { + const values = []; + + const query: ExecuteSqlRequest = { + sql: 'SELECT @v', + params: { + v: values, + }, + types: { + v: { + type: 'array', + child: 'uuid', + }, + }, + }; + + DATABASE.run(query, (err, rows) => { + assert.ifError(err); + assert.deepStrictEqual(rows![0][0].value, values); + done(); + }); + }); + + it('POSTGRESQL should bind arrays', done => { + const values = [uuid.v4(), uuid.v4(), uuid.v4()]; + + const query = { + sql: 'SELECT $1', + params: { + p1: values, + }, + types: { + p1: { + type: 'array', + child: 'uuid', + }, + }, + }; + + PG_DATABASE.run(query, (err, rows) => { + assert.ifError(err); + + const expected = values.map(val => { + return val; + }); + + for (let i = 0; i < rows[0][0].value.length; i++) { + assert.deepStrictEqual(rows[0][0].value[i], expected[i]); + } + done(); + }); + }); + + it('POSTGRESQL should bind empty arrays', done => { + const values = []; + + const query: ExecuteSqlRequest = { + sql: 'SELECT $1', + params: { + p1: values, + }, + types: { + p1: { + type: 'array', + child: 'uuid', + }, + }, + }; + + PG_DATABASE.run(query, (err, rows) => { + assert.ifError(err); + assert.deepStrictEqual(rows![0][0].value, values); + done(); + }); + }); + }); + // TODO: Enable when the float32 feature has been released. describe.skip('float32', () => { const float32Query = (done, database, query, value) => { diff --git a/test/codec.ts b/test/codec.ts index 9a925be86..1dcbff620 100644 --- a/test/codec.ts +++ b/test/codec.ts @@ -18,6 +18,7 @@ import * as assert from 'assert'; import {before, beforeEach, afterEach, describe, it} from 'mocha'; import * as proxyquire from 'proxyquire'; import * as sinon from 'sinon'; +import * as uuid from 'uuid'; import {Big} from 'big.js'; import {PreciseDate} from '@google-cloud/precise-date'; import {GrpcService} from '../src/common-grpc/service'; @@ -680,6 +681,16 @@ describe('codec', () => { assert.deepStrictEqual(decoded, expected); }); + it.skip('should decode UUID', () => { + const value = uuid.v4(); + + const decoded = codec.decode(value, { + code: google.spanner.v1.TypeCode.UUID, + }); + + assert.strictEqual(decoded, value); + }); + it.skip('should decode FLOAT32', () => { const value = 'Infinity'; @@ -1070,6 +1081,14 @@ describe('codec', () => { assert.strictEqual(encoded, '10'); }); + it.skip('should encode UUID', () => { + const value = uuid.v4(); + + const encoded = codec.encode(value); + + assert.strictEqual(encoded, value); + }); + it.skip('should encode FLOAT32', () => { const value = new codec.Float32(10); @@ -1165,6 +1184,12 @@ describe('codec', () => { }); }); + it.skip('should determine if the uuid value is unspecified', () => { + assert.deepStrictEqual(codec.getType(uuid.v4()), { + type: 'unspecified', + }); + }); + it.skip('should determine if the value is a float32', () => { assert.deepStrictEqual(codec.getType(new codec.Float32(1.1)), { type: 'float32', @@ -1317,6 +1342,9 @@ describe('codec', () => { google.spanner.v1.TypeCode.TYPE_CODE_UNSPECIFIED ], }, + uuid: { + code: google.spanner.v1.TypeCode[google.spanner.v1.TypeCode.UUID], + }, bool: { code: google.spanner.v1.TypeCode[google.spanner.v1.TypeCode.BOOL], }, diff --git a/test/transaction.ts b/test/transaction.ts index dbccff289..fc53f8bab 100644 --- a/test/transaction.ts +++ b/test/transaction.ts @@ -1133,7 +1133,10 @@ describe('Transaction', () => { const fakeParams = {a: 'foo', b: 3}; const fakeTypes = {b: 'number'}; const fakeMissingType = {type: 'string'}; - const expectedType = {code: google.spanner.v1.TypeCode.STRING}; + const expectedTypes = { + a: {code: google.spanner.v1.TypeCode.STRING}, + b: {code: google.spanner.v1.TypeCode.INT64}, + }; sandbox .stub(codec, 'getType') @@ -1142,15 +1145,17 @@ describe('Transaction', () => { sandbox .stub(codec, 'createTypeObject') + .withArgs(fakeTypes.b) + .returns(expectedTypes.b as google.spanner.v1.Type) .withArgs(fakeMissingType) - .returns(expectedType as google.spanner.v1.Type); + .returns(expectedTypes.a as google.spanner.v1.Type); const {paramTypes} = Snapshot.encodeParams({ params: fakeParams, types: fakeTypes, }); - assert.strictEqual(paramTypes.a, expectedType); + assert.strictEqual(paramTypes.a, expectedTypes.a); }); }); });