From 99e2c1d4791a5ca86fdccb3f600aa4592efe0a45 Mon Sep 17 00:00:00 2001 From: alkatrivedi <58396306+alkatrivedi@users.noreply.github.com> Date: Tue, 26 Mar 2024 14:12:17 +0000 Subject: [PATCH] feat(spanner): add support for float32 (#2020) This PR contains support for FLOAT32 type on Cloud Spanner. --- .gitignore | 1 + src/codec.ts | 26 +++ src/index.ts | 20 +- system-test/spanner.ts | 485 ++++++++++++++++++++++++++++++++++++++++- test/codec.ts | 45 ++++ test/index.ts | 18 ++ 6 files changed, 593 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index d4f03a0df..14050d4e4 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ system-test/*key.json .DS_Store package-lock.json __pycache__ +.vscode \ No newline at end of file diff --git a/src/codec.ts b/src/codec.ts index ac018b310..53c643200 100644 --- a/src/codec.ts +++ b/src/codec.ts @@ -118,6 +118,21 @@ abstract class WrappedNumber { abstract valueOf(): number; } +/** + * @typedef Float32 + * @see Spanner.float32 + */ +export class Float32 extends WrappedNumber { + value: number; + constructor(value: number) { + super(); + this.value = value; + } + valueOf(): number { + return Number(this.value); + } +} + /** * @typedef Float * @see Spanner.float @@ -377,6 +392,10 @@ function decode(value: Value, type: spannerClient.spanner.v1.Type): Value { case 'BYTES': decoded = Buffer.from(decoded, 'base64'); break; + case spannerClient.spanner.v1.TypeCode.FLOAT32: + case 'FLOAT32': + decoded = new Float32(decoded); + break; case spannerClient.spanner.v1.TypeCode.FLOAT64: case 'FLOAT64': decoded = new Float(decoded); @@ -531,6 +550,7 @@ const TypeCode: { bool: 'BOOL', int64: 'INT64', pgOid: 'INT64', + float32: 'FLOAT32', float64: 'FLOAT64', numeric: 'NUMERIC', pgNumeric: 'NUMERIC', @@ -567,6 +587,7 @@ interface FieldType extends Type { /** * @typedef {object} ParamType * @property {string} type The param type. Must be one of the following: + * - float32 * - float64 * - int64 * - numeric @@ -601,6 +622,10 @@ function getType(value: Value): Type { const isSpecialNumber = is.infinite(value) || (is.number(value) && isNaN(value)); + if (value instanceof Float32) { + return {type: 'float32'}; + } + if (is.decimal(value) || isSpecialNumber || value instanceof Float) { return {type: 'float64'}; } @@ -780,6 +805,7 @@ export const codec = { convertProtoTimestampToDate, createTypeObject, SpannerDate, + Float32, Float, Int, Numeric, diff --git a/src/index.ts b/src/index.ts index eaa44f408..62e3fc23d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,6 +26,7 @@ import * as streamEvents from 'stream-events'; import * as through from 'through2'; import { codec, + Float32, Float, Int, Numeric, @@ -1671,6 +1672,22 @@ class Spanner extends GrpcService { return new PreciseDate(value as number); } + /** + * Helper function to get a Cloud Spanner Float32 object. + * + * @param {string|number} value The float as a number or string. + * @returns {Float32} + * + * @example + * ``` + * const {Spanner} = require('@google-cloud/spanner'); + * const float = Spanner.float32(10); + * ``` + */ + static float32(value): Float32 { + return new codec.Float32(value); + } + /** * Helper function to get a Cloud Spanner Float64 object. * @@ -1786,6 +1803,7 @@ class Spanner extends GrpcService { promisifyAll(Spanner, { exclude: [ 'date', + 'float32', 'float', 'instance', 'instanceConfig', @@ -1946,4 +1964,4 @@ import * as protos from '../protos/protos'; import IInstanceConfig = instanceAdmin.spanner.admin.instance.v1.IInstanceConfig; export {v1, protos}; export default {Spanner}; -export {Float, Int, Struct, Numeric, PGNumeric, SpannerDate}; +export {Float32, Float, Int, Struct, Numeric, PGNumeric, SpannerDate}; diff --git a/system-test/spanner.ts b/system-test/spanner.ts index 6690b5a18..8849f6e2b 100644 --- a/system-test/spanner.ts +++ b/system-test/spanner.ts @@ -31,6 +31,7 @@ import { InstanceConfig, Session, protos, + Float, } from '../src'; import {Key} from '../src/table'; import { @@ -379,7 +380,6 @@ describe('Spanner', () => { callback(err); return; } - callback(null, rows.shift(), insertResp, readResp); }); }); @@ -387,6 +387,7 @@ describe('Spanner', () => { before(async () => { if (IS_EMULATOR_ENABLED) { + // TODO: add column Float32Value FLOAT32 and FLOAT32Array Array while using float32 feature. const [googleSqlOperationUpdateDDL] = await DATABASE.updateSchema( ` CREATE TABLE ${TABLE_NAME} @@ -414,6 +415,7 @@ describe('Spanner', () => { ); await googleSqlOperationUpdateDDL.promise(); } else { + // TODO: add column Float32Value FLOAT32 and FLOAT32Array Array while using float32 feature. const [googleSqlOperationUpdateDDL] = await DATABASE.updateSchema( ` CREATE TABLE ${TABLE_NAME} @@ -442,6 +444,7 @@ describe('Spanner', () => { ` ); await googleSqlOperationUpdateDDL.promise(); + // TODO: add column Float32Value DOUBLE PRECISION and FLOAT32Array DOUBLE PRECISION[] while using float32 feature. const [postgreSqlOperationUpdateDDL] = await PG_DATABASE.updateSchema( ` CREATE TABLE ${TABLE_NAME} @@ -900,6 +903,163 @@ describe('Spanner', () => { }); }); + // TODO: Enable when the float32 feature has been released. + describe.skip('float32s', () => { + const float32Insert = (done, dialect, value) => { + insert({Float32Value: value}, dialect, (err, row) => { + assert.ifError(err); + if (typeof value === 'object' && value !== null) { + value = value.value; + } + if (Number.isNaN(row.toJSON().Float32Value)) { + assert.deepStrictEqual(row.toJSON().Float32Value, value); + } else if (row.toJSON().Float32Value === value) { + assert.deepStrictEqual(row.toJSON().Float32Value, value); + } else { + assert.ok(row.toJSON().Float32Value - value <= 0.00001); + } + done(); + }); + }; + + it('GOOGLE_STANDARD_SQL should write float32 values', done => { + float32Insert(done, Spanner.GOOGLE_STANDARD_SQL, 8.2); + }); + + it('POSTGRESQL should write float32 values', function (done) { + if (IS_EMULATOR_ENABLED) { + this.skip(); + } + float32Insert(done, Spanner.POSTGRESQL, 8.2); + }); + + it('GOOGLE_STANDARD_SQL should write null float32 values', done => { + float32Insert(done, Spanner.GOOGLE_STANDARD_SQL, null); + }); + + it('POSTGRESQL should write null float32 values', function (done) { + if (IS_EMULATOR_ENABLED) { + this.skip(); + } + float32Insert(done, Spanner.POSTGRESQL, null); + }); + + it('GOOGLE_STANDARD_SQL should accept a Float object with an Int-like value', done => { + float32Insert(done, Spanner.GOOGLE_STANDARD_SQL, Spanner.float32(8)); + }); + + it('POSTGRESQL should accept a Float object with an Int-like value', function (done) { + if (IS_EMULATOR_ENABLED) { + this.skip(); + } + float32Insert(done, Spanner.POSTGRESQL, Spanner.float32(8)); + }); + + it('GOOGLE_STANDARD_SQL should handle Infinity', done => { + float32Insert(done, Spanner.GOOGLE_STANDARD_SQL, Infinity); + }); + + it('POSTGRESQL should handle Infinity', function (done) { + if (IS_EMULATOR_ENABLED) { + this.skip(); + } + float32Insert(done, Spanner.POSTGRESQL, Infinity); + }); + + it('GOOGLE_STANDARD_SQL should handle -Infinity', done => { + float32Insert(done, Spanner.GOOGLE_STANDARD_SQL, -Infinity); + }); + + it('POSTGRESQL should handle -Infinity', function (done) { + if (IS_EMULATOR_ENABLED) { + this.skip(); + } + float32Insert(done, Spanner.POSTGRESQL, -Infinity); + }); + + it('GOOGLE_STANDARD_SQL should handle NaN', done => { + float32Insert(done, Spanner.GOOGLE_STANDARD_SQL, NaN); + }); + + it('POSTGRESQL should handle NaN', function (done) { + if (IS_EMULATOR_ENABLED) { + this.skip(); + } + float32Insert(done, Spanner.POSTGRESQL, NaN); + }); + + it('GOOGLE_STANDARD_SQL should write empty float32 array values', done => { + insert({Float32Array: []}, Spanner.GOOGLE_STANDARD_SQL, (err, row) => { + assert.ifError(err); + assert.deepStrictEqual(row.toJSON().Float32Array, []); + done(); + }); + }); + + it('POSTGRESQL should write empty float32 array values', function (done) { + if (IS_EMULATOR_ENABLED) { + this.skip(); + } + insert({Float32Array: []}, Spanner.POSTGRESQL, (err, row) => { + assert.ifError(err); + assert.deepStrictEqual(row.toJSON().Float32Array, []); + done(); + }); + }); + + it('GOOGLE_STANDARD_SQL should write null float32 array values', done => { + insert( + {Float32Array: [null]}, + Spanner.GOOGLE_STANDARD_SQL, + (err, row) => { + assert.ifError(err); + assert.deepStrictEqual(row.toJSON().Float32Array, [null]); + done(); + } + ); + }); + + it('POSTGRESQL should write null float32 array values', function (done) { + if (IS_EMULATOR_ENABLED) { + this.skip(); + } + insert({Float32Array: [null]}, Spanner.POSTGRESQL, (err, row) => { + assert.ifError(err); + assert.deepStrictEqual(row.toJSON().Float32Array, [null]); + done(); + }); + }); + + it('GOOGLE_STANDARD_SQL should write float32 array values', done => { + const values = [1.2, 2.3, 3.4]; + + insert( + {Float32Array: values}, + Spanner.GOOGLE_STANDARD_SQL, + (err, row) => { + assert.ifError(err); + for (let i = 0; i < values.length; i++) { + assert.ok(row.toJSON().Float32Array[i] - values[i] <= 0.00001); + } + done(); + } + ); + }); + + it('POSTGRESQL should write float32 array values', function (done) { + if (IS_EMULATOR_ENABLED) { + this.skip(); + } + const values = [1.2, 2.3, 3.4]; + + insert({Float32Array: values}, Spanner.POSTGRESQL, (err, row) => { + assert.ifError(err); + assert.deepStrictEqual(row.toJSON().Float32Array, values); + done(); + }); + }); + }); + describe('float64s', () => { const float64Insert = (done, dialect, value) => { insert({FloatValue: value}, dialect, (err, row) => { @@ -3732,6 +3892,7 @@ describe('Spanner', () => { const postgreSqlTable = PG_DATABASE.table(TABLE_NAME); before(async () => { + // TODO: Add column Float32 FLOAT32 while using float32 feature. const googleSqlCreateTable = await googleSqlTable.create( `CREATE TABLE ${TABLE_NAME} ( @@ -3751,6 +3912,7 @@ describe('Spanner', () => { await onPromiseOperationComplete(googleSqlCreateTable); if (!IS_EMULATOR_ENABLED) { + // TODO: Add column "Float32" DOUBLE PRECISION while using float32 feature. const postgreSqlCreateTable = await postgreSqlTable.create( `CREATE TABLE ${TABLE_NAME} ( @@ -4303,6 +4465,7 @@ describe('Spanner', () => { describe('insert & query', () => { const ID = generateName('id'); const NAME = generateName('name'); + // const FLOAT32 = 8.2; // TODO: Uncomment while using float32 feature. const FLOAT = 8.2; const INT = 2; const INFO = Buffer.from(generateName('info')); @@ -4315,6 +4478,7 @@ describe('Spanner', () => { const GOOGLE_SQL_INSERT_ROW = { SingerId: ID, Name: NAME, + // Float32: FLOAT32, // TODO: Uncomment while using float32 feature. Float: FLOAT, Int: INT, Info: INFO, @@ -4328,6 +4492,7 @@ describe('Spanner', () => { const POSTGRESQL_INSERT_ROW = { SingerId: ID, Name: NAME, + // Float32: FLOAT32, // TODO: Uncomment while using float32 feature. Float: FLOAT, Int: INT, Info: INFO, @@ -4470,6 +4635,8 @@ 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'); assert.strictEqual(metadata.rowType!.fields![2].name, 'Float'); assert.strictEqual(metadata.rowType!.fields![3].name, 'Int'); assert.strictEqual(metadata.rowType!.fields![4].name, 'Info'); @@ -4494,6 +4661,8 @@ 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'); assert.strictEqual(metadata.rowType!.fields![2].name, 'Float'); assert.strictEqual(metadata.rowType!.fields![3].name, 'Int'); assert.strictEqual(metadata.rowType!.fields![4].name, 'Info'); @@ -4863,6 +5032,320 @@ describe('Spanner', () => { }); }); + // TODO: Enable when the float32 feature has been released. + describe.skip('float32', () => { + const float32Query = (done, database, query, value) => { + database.run(query, (err, rows) => { + assert.ifError(err); + let queriedValue = rows[0][0].value; + if (rows[0][0].value) { + queriedValue = rows[0][0].value.value; + } + if (Number.isNaN(queriedValue)) { + assert.deepStrictEqual(queriedValue, value); + } else if (queriedValue === value) { + assert.deepStrictEqual(queriedValue, value); + } else { + assert.ok(queriedValue - value <= 0.00001); + } + done(); + }); + }; + + it('GOOGLE_STANDARD_SQL should bind the value when param type float32 is used', done => { + const query = { + sql: 'SELECT @v', + params: { + v: 2.2, + }, + types: { + v: 'float32', + }, + }; + float32Query(done, DATABASE, query, 2.2); + }); + + it('GOOGLE_STANDARD_SQL should bind the value when spanner.float32 is used', done => { + const query = { + sql: 'SELECT @v', + params: { + v: Spanner.float32(2.2), + }, + }; + float32Query(done, DATABASE, query, 2.2); + }); + + it('GOOGLE_STANDARD_SQL should bind the value as float64 when param type is not specified', done => { + const query = { + sql: 'SELECT @v', + params: { + v: 2.2, + }, + }; + DATABASE.run(query, (err, rows) => { + assert.ifError(err); + assert.strictEqual(rows[0][0].value instanceof Float, true); + done(); + }); + }); + + it('POSTGRESQL should bind the value when param type float32 is used', function (done) { + if (IS_EMULATOR_ENABLED) { + this.skip(); + } + const query = { + sql: 'SELECT $1', + params: { + p1: 2.2, + }, + types: { + p1: 'float32', + }, + }; + float32Query(done, PG_DATABASE, query, 2.2); + }); + + it('POSTGRESQL should bind the value when Spanner.float32 is used', function (done) { + if (IS_EMULATOR_ENABLED) { + this.skip(); + } + const query = { + sql: 'SELECT $1', + params: { + p1: Spanner.float32(2.2), + }, + }; + float32Query(done, PG_DATABASE, query, 2.2); + }); + + it('GOOGLE_STANDARD_SQL should allow for null values', done => { + const query = { + sql: 'SELECT @v', + params: { + v: null, + }, + types: { + v: 'float32', + }, + }; + float32Query(done, DATABASE, query, null); + }); + + it('POSTGRESQL should allow for null values', function (done) { + if (IS_EMULATOR_ENABLED) { + this.skip(); + } + const query = { + sql: 'SELECT $1', + params: { + p1: null, + }, + types: { + p1: 'float32', + }, + }; + float32Query(done, PG_DATABASE, query, null); + }); + + it('GOOGLE_STANDARD_SQL should bind arrays', done => { + const values = [null, 1.1, 2.3, 3.5, null]; + + const query = { + sql: 'SELECT @v', + params: { + v: values, + }, + types: { + v: { + type: 'array', + child: 'float32', + }, + }, + }; + + DATABASE.run(query, (err, rows) => { + assert.ifError(err); + + const expected = values.map(val => { + return is.number(val) ? Spanner.float32(val) : val; + }); + + for (let i = 0; i < rows[0][0].value.length; i++) { + if (rows[0][0].value[i] === null || expected[i] === null) { + assert.deepStrictEqual(rows[0][0].value[i], expected[i]); + } else { + assert.ok( + rows[0][0].value[i] - expected[i]!['value'] <= 0.00001 + ); + } + } + 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: 'float32', + }, + }, + }; + + DATABASE.run(query, (err, rows) => { + assert.ifError(err); + assert.deepStrictEqual(rows![0][0].value, values); + done(); + }); + }); + + it('GOOGLE_STANDARD_SQL should bind null arrays', done => { + const query: ExecuteSqlRequest = { + sql: 'SELECT @v', + params: { + v: null, + }, + types: { + v: { + type: 'array', + child: 'float32', + }, + }, + }; + + DATABASE.run(query, (err, rows) => { + assert.ifError(err); + assert.deepStrictEqual(rows![0][0].value, null); + done(); + }); + }); + + it('GOOGLE_STANDARD_SQL should bind Infinity', done => { + const query = { + sql: 'SELECT @v', + params: { + v: Infinity, + }, + types: { + v: 'float32', + }, + }; + float32Query(done, DATABASE, query, 'Infinity'); + }); + + it('POSTGRESQL should bind Infinity', function (done) { + if (IS_EMULATOR_ENABLED) { + this.skip(); + } + const query = { + sql: 'SELECT $1', + params: { + p1: Infinity, + }, + types: { + p1: 'float32', + }, + }; + float32Query(done, PG_DATABASE, query, 'Infinity'); + }); + + it('GOOGLE_STANDARD_SQL should bind -Infinity', done => { + const query = { + sql: 'SELECT @v', + params: { + v: -Infinity, + }, + types: { + v: 'float32', + }, + }; + float32Query(done, DATABASE, query, '-Infinity'); + }); + + it('POSTGRESQL should bind -Infinity', function (done) { + if (IS_EMULATOR_ENABLED) { + this.skip(); + } + const query = { + sql: 'SELECT $1', + params: { + p1: -Infinity, + }, + types: { + p1: 'float32', + }, + }; + float32Query(done, PG_DATABASE, query, '-Infinity'); + }); + + it('GOOGLE_STANDARD_SQL should bind NaN', done => { + const query = { + sql: 'SELECT @v', + params: { + v: NaN, + }, + types: { + v: 'float32', + }, + }; + float32Query(done, DATABASE, query, 'NaN'); + }); + + it('POSTGRESQL should bind NaN', function (done) { + if (IS_EMULATOR_ENABLED) { + this.skip(); + } + const query = { + sql: 'SELECT $1', + params: { + p1: NaN, + }, + types: { + p1: 'float32', + }, + }; + float32Query(done, PG_DATABASE, query, 'NaN'); + }); + + it('GOOGLE_STANDARD_SQL should bind an array of Infinity and NaN', done => { + const values = [Infinity, -Infinity, NaN]; + + const query = { + sql: 'SELECT @v', + params: { + v: values, + }, + types: { + v: { + type: 'array', + child: 'float32', + }, + }, + }; + + DATABASE.run(query, (err, rows) => { + assert.ifError(err); + + const expected = values.map(val => { + return is.number(val) ? {value: val + ''} : val; + }); + + assert.strictEqual( + JSON.stringify(rows[0][0].value), + JSON.stringify(expected) + ); + done(); + }); + }); + }); + describe('float64', () => { const float64Query = (done, database, query, value) => { database.run(query, (err, rows) => { diff --git a/test/codec.ts b/test/codec.ts index b604e4354..73b9f2132 100644 --- a/test/codec.ts +++ b/test/codec.ts @@ -163,6 +163,23 @@ describe('codec', () => { }); }); + describe.skip('Float32', () => { + it('should store the value', () => { + const value = 8; + const float32 = new codec.Float32(value); + + assert.strictEqual(float32.value, value); + }); + + it('should return as a float32', () => { + const value = '8.2'; + const float32 = new codec.Float32(value); + + assert.strictEqual(float32.valueOf(), Number(value)); + assert.strictEqual(float32 + 2, Number(value) + 2); + }); + }); + describe('Int', () => { it('should stringify the value', () => { const value = 8; @@ -527,6 +544,17 @@ describe('codec', () => { assert.deepStrictEqual(decoded, expected); }); + it.skip('should decode FLOAT32', () => { + const value = 'Infinity'; + + const decoded = codec.decode(value, { + code: google.spanner.v1.TypeCode.FLOAT32, + }); + + assert(decoded instanceof codec.Float32); + assert.strictEqual(decoded.value, value); + }); + it('should decode FLOAT64', () => { const value = 'Infinity'; @@ -870,6 +898,14 @@ describe('codec', () => { assert.strictEqual(encoded, '10'); }); + it.skip('should encode FLOAT32', () => { + const value = new codec.Float32(10); + + const encoded = codec.encode(value); + + assert.strictEqual(encoded, 10); + }); + it('should encode FLOAT64', () => { const value = new codec.Float(10); @@ -957,6 +993,12 @@ describe('codec', () => { }); }); + it.skip('should determine if the value is a float32', () => { + assert.deepStrictEqual(codec.getType(new codec.Float32(1.1)), { + type: 'float32', + }); + }); + it('should determine if the value is an int', () => { assert.deepStrictEqual(codec.getType(1234), {type: 'int64'}); assert.deepStrictEqual(codec.getType(new codec.Int(1)), {type: 'int64'}); @@ -1109,6 +1151,9 @@ describe('codec', () => { int64: { code: google.spanner.v1.TypeCode[google.spanner.v1.TypeCode.INT64], }, + float32: { + code: google.spanner.v1.TypeCode[google.spanner.v1.TypeCode.FLOAT32], + }, float64: { code: google.spanner.v1.TypeCode[google.spanner.v1.TypeCode.FLOAT64], }, diff --git a/test/index.ts b/test/index.ts index cd60bbbcd..dd2b4b7b9 100644 --- a/test/index.ts +++ b/test/index.ts @@ -82,6 +82,7 @@ const fakePfy = extend({}, pfy, { promisified = true; assert.deepStrictEqual(options.exclude, [ 'date', + 'float32', 'float', 'instance', 'instanceConfig', @@ -494,6 +495,23 @@ describe('Spanner', () => { }); }); + describe.skip('float32', () => { + it('should create a Float32 instance', () => { + const value = {}; + const customValue = {}; + + fakeCodec.Float32 = class { + constructor(value_) { + assert.strictEqual(value_, value); + return customValue; + } + }; + + const float32 = Spanner.float32(value); + assert.strictEqual(float32, customValue); + }); + }); + describe('int', () => { it('should create an Int instance', () => { const value = {};