diff --git a/src/codec.ts b/src/codec.ts index 14e9056b2..663a76873 100644 --- a/src/codec.ts +++ b/src/codec.ts @@ -249,6 +249,25 @@ export class PGJsonb { } } +/** + * @typedef PGOid + * @see Spanner.pgOid + */ +export class PGOid extends WrappedNumber { + value: string; + constructor(value: string) { + super(); + this.value = value.toString(); + } + valueOf(): number { + const num = Number(this.value); + if (num > Number.MAX_SAFE_INTEGER) { + throw new GoogleError(`PG.OID ${this.value} is out of bounds.`); + } + return num; + } +} + /** * @typedef JSONOptions * @property {boolean} [wrapNumbers=false] Indicates if the numbers should be @@ -364,6 +383,14 @@ function decode(value: Value, type: spannerClient.spanner.v1.Type): Value { break; case spannerClient.spanner.v1.TypeCode.INT64: case 'INT64': + if ( + type.typeAnnotation === + spannerClient.spanner.v1.TypeAnnotationCode.PG_OID || + type.typeAnnotation === 'PG_OID' + ) { + decoded = new PGOid(decoded); + break; + } decoded = new Int(decoded); break; case spannerClient.spanner.v1.TypeCode.NUMERIC: @@ -503,6 +530,7 @@ const TypeCode: { unspecified: 'TYPE_CODE_UNSPECIFIED', bool: 'BOOL', int64: 'INT64', + pgOid: 'INT64', float64: 'FLOAT64', numeric: 'NUMERIC', pgNumeric: 'NUMERIC', @@ -593,6 +621,10 @@ function getType(value: Value): Type { return {type: 'pgJsonb'}; } + if (value instanceof PGOid) { + return {type: 'pgOid'}; + } + if (is.boolean(value)) { return {type: 'bool'}; } @@ -733,6 +765,8 @@ function createTypeObject( spannerClient.spanner.v1.TypeAnnotationCode.PG_NUMERIC; } else if (friendlyType.type === 'jsonb') { type.typeAnnotation = spannerClient.spanner.v1.TypeAnnotationCode.PG_JSONB; + } else if (friendlyType.type === 'pgOid') { + type.typeAnnotation = spannerClient.spanner.v1.TypeAnnotationCode.PG_OID; } return type; } @@ -748,6 +782,7 @@ export const codec = { Numeric, PGNumeric, PGJsonb, + PGOid, convertFieldsToJson, decode, encode, diff --git a/system-test/spanner.ts b/system-test/spanner.ts index 736e9e5a9..011cba1f5 100644 --- a/system-test/spanner.ts +++ b/system-test/spanner.ts @@ -827,6 +827,38 @@ describe('Spanner', () => { }); }); + describe('oids', () => { + it('POSTGRESQL should read non-null pgOid values', function (done) { + if (IS_EMULATOR_ENABLED) { + this.skip(); + } + PG_DATABASE.run('SELECT 123::oid', (err, rows) => { + assert.ifError(err); + let queriedValue = rows[0][0].value; + if (rows[0][0].value) { + queriedValue = rows[0][0].value.value; + } + assert.strictEqual(queriedValue, '123'); + done(); + }); + }); + + it('POSTGRESQL should read null pgOid values', function (done) { + if (IS_EMULATOR_ENABLED) { + this.skip(); + } + PG_DATABASE.run('SELECT null::oid', (err, rows) => { + assert.ifError(err); + let queriedValue = rows[0][0].value; + if (rows[0][0].value) { + queriedValue = rows[0][0].value.value; + } + assert.strictEqual(queriedValue, null); + done(); + }); + }); + }); + describe('float64s', () => { const float64Insert = (done, dialect, value) => { insert({FloatValue: value}, dialect, (err, row) => { @@ -4951,6 +4983,52 @@ describe('Spanner', () => { }); }); + describe('pgOid', () => { + const oidQuery = (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; + } + assert.strictEqual(queriedValue, value); + done(); + }); + }; + + it('POSTGRESQL should bind the value', function (done) { + if (IS_EMULATOR_ENABLED) { + this.skip(); + } + const query = { + sql: 'SELECT $1', + params: { + p1: 1234, + }, + types: { + v: 'pgOid', + }, + }; + oidQuery(done, PG_DATABASE, query, '1234'); + }); + + 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: 'pgOid', + }, + }; + oidQuery(done, PG_DATABASE, query, null); + }); + }); + describe('float64', () => { const float64Query = (done, database, query, value) => { database.run(query, (err, rows) => { diff --git a/test/codec.ts b/test/codec.ts index 43b17bbb4..64085fce3 100644 --- a/test/codec.ts +++ b/test/codec.ts @@ -192,6 +192,35 @@ describe('codec', () => { }); }); + describe('PGOid', () => { + it('should stringify the value', () => { + const value = 8; + const oid = new codec.PGOid(value); + + assert.strictEqual(oid.value, '8'); + }); + + it('should return as a number', () => { + const value = 8; + const oid = new codec.PGOid(value); + + assert.strictEqual(oid.valueOf(), 8); + assert.strictEqual(oid + 2, 10); + }); + + it('should throw if number is out of bounds', () => { + const value = '9223372036854775807'; + const oid = new codec.PGOid(value); + + assert.throws( + () => { + oid.valueOf(); + }, + new RegExp('PG.OID ' + value + ' is out of bounds.') + ); + }); + }); + describe('Numeric', () => { it('should store value as a string', () => { const value = '8.01911'; @@ -601,6 +630,18 @@ describe('codec', () => { assert.deepStrictEqual(decoded.toString(), expected); }); + it('should decode PG OID', () => { + const value = '64'; + + const decoded = codec.decode(value, { + code: google.spanner.v1.TypeCode.INT64, + typeAnnotation: google.spanner.v1.TypeAnnotationCode.PG_OID, + }); + + assert(decoded instanceof codec.PGOid); + assert.strictEqual(decoded.value, value); + }); + it('should decode TIMESTAMP', () => { const value = new Date(); const expected = new PreciseDate(value.getTime()); @@ -821,6 +862,14 @@ describe('codec', () => { assert.strictEqual(encoded, '10'); }); + it('should encode PG OID', () => { + const value = new codec.PGOid(10); + + const encoded = codec.encode(value); + + assert.strictEqual(encoded, '10'); + }); + it('should encode FLOAT64', () => { const value = new codec.Float(10); @@ -986,6 +1035,12 @@ describe('codec', () => { type: 'pgNumeric', }); }); + + it('should determine if the value is a PGOid', () => { + assert.deepStrictEqual(codec.getType(new codec.PGOid(5678)), { + type: 'pgOid', + }); + }); }); describe('convertToListValue', () => { @@ -1211,5 +1266,23 @@ describe('codec', () => { typeAnnotation: google.spanner.v1.TypeAnnotationCode.PG_NUMERIC, }); }); + + it('should set code and typeAnnotation for pgOid string', () => { + const type = codec.createTypeObject('pgOid'); + + assert.deepStrictEqual(type, { + code: google.spanner.v1.TypeCode[google.spanner.v1.TypeCode.INT64], + typeAnnotation: google.spanner.v1.TypeAnnotationCode.PG_OID, + }); + }); + + it('should set code and typeAnnotation for pgOid friendlyType object', () => { + const type = codec.createTypeObject({type: 'pgOid'}); + + assert.deepStrictEqual(type, { + code: google.spanner.v1.TypeCode[google.spanner.v1.TypeCode.INT64], + typeAnnotation: google.spanner.v1.TypeAnnotationCode.PG_OID, + }); + }); }); });