Skip to content
Merged
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
167 changes: 111 additions & 56 deletions handwritten/spanner/src/codec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,12 @@
import {PreciseDate} from '@google-cloud/precise-date';
import {
isArray,
isBoolean,
isDate,
isDecimal,
isInfinite,
isInteger,
isNull,
isNumber,
isObject,
isString,
isUndefined,
isUuid,
toArray,
Expand Down Expand Up @@ -82,8 +79,6 @@
return isValidDate(year, month, day);
}

let uuidUntypedFlagWarned = false;

export interface Field {
name: string;
value: Value;
Expand Down Expand Up @@ -938,7 +933,7 @@
return val => val;
}

let decoder: (val: any) => any;

Check warning on line 936 in handwritten/spanner/src/codec.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type

Check warning on line 936 in handwritten/spanner/src/codec.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type

switch (type.code) {
case spannerClient.spanner.v1.TypeCode.BYTES:
Expand All @@ -952,8 +947,8 @@
decoder = val => {
const decoded = Buffer.from(val, 'base64');
if (columnMetadata) {
return (columnMetadata as any)['toObject'](

Check warning on line 950 in handwritten/spanner/src/codec.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
(columnMetadata as any)['decode'](decoded),

Check warning on line 951 in handwritten/spanner/src/codec.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
);
}
return decoded.toString();
Expand Down Expand Up @@ -981,7 +976,7 @@
columnMetadata &&
Object.prototype.hasOwnProperty.call(columnMetadata, val)
) {
enumVal = (columnMetadata as any)[val];

Check warning on line 979 in handwritten/spanner/src/codec.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
} else {
throw new GoogleError(
'protoEnumParams cannot be used for constructing the ProtoEnum. Pass the number as the value or provide the enum string constant as the value along with the corresponding enumObject generated by protobufjs-cli.',
Expand All @@ -997,7 +992,7 @@
return proto[enumVal];
}
if (Object.prototype.hasOwnProperty.call(columnMetadata, enumVal)) {
return (columnMetadata as any)[enumVal];

Check warning on line 995 in handwritten/spanner/src/codec.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
}
}
return enumVal;
Expand Down Expand Up @@ -1129,7 +1124,7 @@
name !== null &&
name !== undefined &&
Object.prototype.hasOwnProperty.call(columnMetadata, name)
? (columnMetadata as any)[name]

Check warning on line 1127 in handwritten/spanner/src/codec.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
: undefined,
options,
),
Expand Down Expand Up @@ -1398,6 +1393,7 @@
bytes: 'BYTES',
json: 'JSON',
jsonb: 'JSON',
pgJsonb: 'JSON',
interval: 'INTERVAL',
proto: 'PROTO',
enum: 'ENUM',
Expand All @@ -1422,6 +1418,24 @@
name: string;
}

// Parameterless type singletons. These are frozen and reused across `getType`
// calls to eliminate heap allocations and GC pressure during parameter encoding.
const TYPE_STRING: Type = Object.freeze({type: 'string'});
const TYPE_BOOL: Type = Object.freeze({type: 'bool'});
const TYPE_INT64: Type = Object.freeze({type: 'int64'});
const TYPE_FLOAT64: Type = Object.freeze({type: 'float64'});
const TYPE_FLOAT32: Type = Object.freeze({type: 'float32'});
const TYPE_NUMERIC: Type = Object.freeze({type: 'numeric'});
const TYPE_PG_NUMERIC: Type = Object.freeze({type: 'pgNumeric'});
const TYPE_PG_JSONB: Type = Object.freeze({type: 'pgJsonb'});
const TYPE_PG_OID: Type = Object.freeze({type: 'pgOid'});
const TYPE_INTERVAL: Type = Object.freeze({type: 'interval'});
const TYPE_BYTES: Type = Object.freeze({type: 'bytes'});
const TYPE_DATE: Type = Object.freeze({type: 'date'});
const TYPE_TIMESTAMP: Type = Object.freeze({type: 'timestamp'});
const TYPE_JSON: Type = Object.freeze({type: 'json'});
const TYPE_UNSPECIFIED: Type = Object.freeze({type: 'unspecified'});

/**
* @typedef {ParamType} StructField
* @property {string} name The name of the field.
Expand All @@ -1434,6 +1448,9 @@
* - float64
* - int64
* - numeric
* - pgNumeric
* - pgJsonb
* - pgOid
* - bool
* - string
* - bytes
Expand All @@ -1445,112 +1462,136 @@
* - date
* - struct
* - array
* - unspecified
* @property {StructField[]} [fields] **For struct types only**. Type
* definitions for the individual fields.
* @property {string|ParamType} [child] **For array types only**. The array
* element type.
*/
let uuidUntypedFlagWarned = false;

function _resetUuidUntypedFlagWarnedForTest(): void {
uuidUntypedFlagWarned = false;
}

function isUuidUntypedEnv(): boolean {
if (process.env['SPANNER_ENABLE_UUID_AS_UNTYPED']?.toLowerCase() === 'true') {
if (!uuidUntypedFlagWarned) {
process.emitWarning(
'SPANNER_ENABLE_UUID_AS_UNTYPED environment variable is deprecated and will be removed in a future release.',
'DeprecationWarning',
);
uuidUntypedFlagWarned = true;
}
return true;
}
return false;
}
Comment thread
olavloite marked this conversation as resolved.
Comment thread
olavloite marked this conversation as resolved.

/**
* Get the corresponding Spanner data type for the provided value.
*
* NOTE: This function is internal and not exposed in the public API.
* Parameterless types return shared, frozen singletons to eliminate allocation
* and GC overhead. Callers must treat the returned Type descriptor as read-only.
*
* @private
*
* @param {*} value - The value.
* @returns {object}
* @param {boolean} [enableUuidAsUntyped] - Whether UUID strings should be
* typed as unspecified.
* @returns {Type}
*
* @example
* ```
* codec.getType(NaN);
* // {type: 'float64'}
* ```
*/
function getType(value: Value): Type {
const isSpecialNumber =
isInfinite(value) || (isNumber(value) && isNaN(value));
function getType(value: Value, enableUuidAsUntyped?: boolean): Type {
if (value === null || value === undefined) {
return TYPE_UNSPECIFIED;
}

if (value instanceof Float32) {
return {type: 'float32'};
if (typeof value === 'string') {
const isUuidUntyped = enableUuidAsUntyped ?? isUuidUntypedEnv();
if (isUuidUntyped && isUuid(value)) {
return TYPE_UNSPECIFIED;
}
return TYPE_STRING;
}

if (isDecimal(value) || isSpecialNumber || value instanceof Float) {
return {type: 'float64'};
if (typeof value === 'number') {
return Number.isInteger(value) ? TYPE_INT64 : TYPE_FLOAT64;
}
Comment thread
olavloite marked this conversation as resolved.

if (isNumber(value) || value instanceof Int) {
return {type: 'int64'};
if (typeof value === 'boolean') {
return TYPE_BOOL;
}

if (value instanceof Numeric) {
return {type: 'numeric'};
if (Buffer.isBuffer(value)) {
return TYPE_BYTES;
}

if (value instanceof PGNumeric) {
return {type: 'pgNumeric'};
if (value instanceof SpannerDate) {
return TYPE_DATE;
}

if (value instanceof PGJsonb) {
return {type: 'pgJsonb'};
if (value instanceof Date || isDate(value)) {
return TYPE_TIMESTAMP;
}

if (value instanceof PGOid) {
return {type: 'pgOid'};
if (value instanceof Int) {
return TYPE_INT64;
}

if (value instanceof Interval) {
return {type: 'interval'};
if (value instanceof Float) {
return TYPE_FLOAT64;
}

if (value instanceof ProtoMessage) {
return {type: 'proto', fullName: value.fullName};
if (value instanceof Float32) {
return TYPE_FLOAT32;
}

if (value instanceof ProtoEnum) {
return {type: 'enum', fullName: value.fullName};
if (value instanceof Numeric) {
return TYPE_NUMERIC;
}

if (isBoolean(value)) {
return {type: 'bool'};
if (value instanceof PGNumeric) {
return TYPE_PG_NUMERIC;
}

if (process.env['SPANNER_ENABLE_UUID_AS_UNTYPED'] === 'true') {
if (!uuidUntypedFlagWarned) {
process.emitWarning(
'SPANNER_ENABLE_UUID_AS_UNTYPED environment variable is deprecated and will be removed in a future release.',
'DeprecationWarning',
);
uuidUntypedFlagWarned = true;
}
if (isUuid(value)) {
return {type: 'unspecified'};
}
if (value instanceof PGJsonb) {
return TYPE_PG_JSONB;
}

if (isString(value)) {
return {type: 'string'};
if (value instanceof PGOid) {
return TYPE_PG_OID;
}

if (Buffer.isBuffer(value)) {
return {type: 'bytes'};
if (value instanceof Interval) {
return TYPE_INTERVAL;
}

if (value instanceof SpannerDate) {
return {type: 'date'};
if (value instanceof ProtoMessage) {
return {type: 'proto', fullName: value.fullName};
}

if (isDate(value)) {
return {type: 'timestamp'};
if (value instanceof ProtoEnum) {
return {type: 'enum', fullName: value.fullName};
}

if (value instanceof Struct) {
const isUuidUntyped = enableUuidAsUntyped ?? isUuidUntypedEnv();
return {
type: 'struct',
fields: Array.from(value).map(({name, value}) => {
return Object.assign({name}, getType(value));
return Object.assign({name}, getType(value, isUuidUntyped));
}),
};
}

if (isArray(value)) {
if (Array.isArray(value)) {
let child;

for (let i = 0; i < value.length; i++) {
Expand All @@ -1563,15 +1604,27 @@

return {
type: 'array',
child: getType(child),
child: getType(child, enableUuidAsUntyped),
};
}

if (value instanceof String) {
return TYPE_STRING;
}

if (value instanceof Number) {
return Number.isInteger(value.valueOf()) ? TYPE_INT64 : TYPE_FLOAT64;
}

if (value instanceof Boolean) {
return TYPE_BOOL;
}

if (isObject(value)) {
return {type: 'json'};
return TYPE_JSON;
}

return {type: 'unspecified'};
return TYPE_UNSPECIFIED;
}

/**
Expand Down Expand Up @@ -1666,7 +1719,7 @@
if (friendlyType.type === 'pgNumeric') {
type.typeAnnotation =
spannerClient.spanner.v1.TypeAnnotationCode.PG_NUMERIC;
} else if (friendlyType.type === 'jsonb') {
} else if (friendlyType.type === 'jsonb' || friendlyType.type === 'pgJsonb') {
type.typeAnnotation = spannerClient.spanner.v1.TypeAnnotationCode.PG_JSONB;
} else if (friendlyType.type === 'pgOid') {
type.typeAnnotation = spannerClient.spanner.v1.TypeAnnotationCode.PG_OID;
Expand Down Expand Up @@ -1696,4 +1749,6 @@
encode,
getType,
Struct,
_resetUuidUntypedFlagWarnedForTest,
Comment thread
olavloite marked this conversation as resolved.
isUuidUntypedEnv,
};
5 changes: 3 additions & 2 deletions handwritten/spanner/src/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@
* Injects a key-value pair into the gaxOpts.otherArgs.options object
* without mutating the original.
*/
function injectGaxOpt(existingOpts: any, key: string, value: any): any {

Check warning on line 82 in handwritten/spanner/src/transaction.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type

Check warning on line 82 in handwritten/spanner/src/transaction.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type

Check warning on line 82 in handwritten/spanner/src/transaction.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
return Object.assign({}, existingOpts, {
otherArgs: Object.assign({}, existingOpts?.otherArgs, {
options: Object.assign({}, existingOpts?.otherArgs?.options, {
Expand Down Expand Up @@ -2208,6 +2208,7 @@
* @returns {object}
*/
static encodeParams(request: ExecuteSqlRequest) {
const isUuidUntyped = codec.isUuidUntypedEnv();
const typeMap = request.types || {};

const params: p.IStruct = {fields: request.params?.fields || {}};
Expand All @@ -2221,7 +2222,7 @@
const value = request.params![param];

if (!typeMap[param]) {
typeMap[param] = codec.getType(value);
typeMap[param] = codec.getType(value, isUuidUntyped);
}
fields[param] = codec.encode(value);
});
Expand All @@ -2232,7 +2233,7 @@
if (!isEmpty(typeMap)) {
Object.keys(typeMap).forEach(param => {
const type = typeMap[param];
if (process.env['SPANNER_ENABLE_UUID_AS_UNTYPED'] === 'true') {
if (isUuidUntyped) {
const typeObject = codec.createTypeObject(type);
if (
(type.child &&
Expand Down
Loading
Loading