From 6d482a2b9de18b2dfd744565793b00fbd233b991 Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Fri, 13 Dec 2024 22:13:58 -0800 Subject: [PATCH] Implement proper array-like SQL record/row compatible with 6b1 --- packages/driver/src/codecs/ifaces.ts | 3 +- packages/driver/src/codecs/object.ts | 3 +- packages/driver/src/codecs/record.ts | 84 ++++++++++++++++++++++++++ packages/driver/src/codecs/registry.ts | 30 +++++++++ packages/driver/test/client.test.ts | 10 +-- 5 files changed, 123 insertions(+), 7 deletions(-) create mode 100644 packages/driver/src/codecs/record.ts diff --git a/packages/driver/src/codecs/ifaces.ts b/packages/driver/src/codecs/ifaces.ts index ab9dd93f5..b06d96721 100644 --- a/packages/driver/src/codecs/ifaces.ts +++ b/packages/driver/src/codecs/ifaces.ts @@ -34,7 +34,8 @@ export type CodecKind = | "scalar" | "sparse_object" | "range" - | "multirange"; + | "multirange" + | "record"; export interface ICodec { readonly tid: uuid; diff --git a/packages/driver/src/codecs/object.ts b/packages/driver/src/codecs/object.ts index ee623de22..c08136da6 100644 --- a/packages/driver/src/codecs/object.ts +++ b/packages/driver/src/codecs/object.ts @@ -77,7 +77,8 @@ export class ObjectCodec extends Codec implements ICodec { } encodeArgs(args: any): Uint8Array { - if (this.fields[0].name === "0") { + // EdgeQL query parameters start at 0, SQL start at 1. + if (this.fields[0].name === "0" || this.fields[0].name === "1") { return this._encodePositionalArgs(args); } return this._encodeNamedArgs(args); diff --git a/packages/driver/src/codecs/record.ts b/packages/driver/src/codecs/record.ts new file mode 100644 index 000000000..d18f70b98 --- /dev/null +++ b/packages/driver/src/codecs/record.ts @@ -0,0 +1,84 @@ +/*! + * This source file is part of the EdgeDB open source project. + * + * Copyright 2019-present MagicStack Inc. and the EdgeDB authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { ICodec, uuid, CodecKind } from "./ifaces"; +import { Codec } from "./ifaces"; +import { ReadBuffer, WriteBuffer } from "../primitives/buffer"; +import { + InvalidArgumentError, + ProtocolError, +} from "../errors"; + +export class RecordCodec extends Codec implements ICodec { + private subCodecs: ICodec[]; + private names: string[]; + + constructor( + tid: uuid, + codecs: ICodec[], + names: string[], + ) { + super(tid); + this.subCodecs = codecs; + this.names = names; + } + + encode(buf: WriteBuffer, object: any): void { + throw new InvalidArgumentError( + "SQL records cannot be passed as arguments"); + } + + decode(buf: ReadBuffer): any { + const els = buf.readUInt32(); + const subCodecs = this.subCodecs; + if (els !== subCodecs.length) { + throw new ProtocolError( + `cannot decode Record: expected ` + + `${subCodecs.length} elements, got ${els}`, + ); + } + + const elemBuf = ReadBuffer.alloc(); + const result: any[] = new Array(els); + for (let i = 0; i < els; i++) { + buf.discard(4); // reserved + const elemLen = buf.readInt32(); + let val = null; + if (elemLen !== -1) { + buf.sliceInto(elemBuf, elemLen); + val = subCodecs[i].decode(elemBuf); + elemBuf.finish(); + } + result[i] = val; + } + + return result; + } + + getSubcodecs(): ICodec[] { + return Array.from(this.subCodecs); + } + + getNames(): string[] { + return Array.from(this.names); + } + + getKind(): CodecKind { + return "record"; + } +} diff --git a/packages/driver/src/codecs/registry.ts b/packages/driver/src/codecs/registry.ts index d13eae5a1..0de6e0dc6 100644 --- a/packages/driver/src/codecs/registry.ts +++ b/packages/driver/src/codecs/registry.ts @@ -30,6 +30,7 @@ import { NamedTupleCodec } from "./namedtuple"; import { EnumCodec } from "./enum"; import { ObjectCodec } from "./object"; import { SetCodec } from "./set"; +import { RecordCodec } from "./record"; import { MultiRangeCodec, RangeCodec } from "./range"; import type { ProtocolVersion } from "../ifaces"; import { versionGreaterThanOrEqual } from "../utils"; @@ -52,6 +53,7 @@ const CTYPE_RANGE = 9; const CTYPE_OBJECT = 10; const CTYPE_COMPOUND = 11; const CTYPE_MULTIRANGE = 12; +const CTYPE_RECORD = 13; export interface CustomCodecSpec { int64_bigint?: boolean; @@ -261,6 +263,15 @@ export class CodecsRegistry { break; } + case CTYPE_RECORD: { + const els = frb.readUInt16(); + for (let i = 0; i < els; i++) { + const elm_length = frb.readUInt32(); + frb.discard(elm_length + 2); + } + break; + } + case CTYPE_ARRAY: { frb.discard(2); const els = frb.readUInt16(); @@ -567,6 +578,25 @@ export class CodecsRegistry { break; } + case CTYPE_RECORD: { + const els = frb.readUInt16(); + const codecs = new Array(els); + const names = new Array(els); + for (let i = 0; i < els; i++) { + names[i] = frb.readString(); + const pos = frb.readUInt16(); + const subCodec = cl[pos]; + if (subCodec == null) { + throw new ProtocolError( + "could not build record codec: missing subcodec", + ); + } + codecs[i] = subCodec; + } + res = new RecordCodec(tid, codecs, names); + break; + } + case CTYPE_ENUM: { let typeName: string | null = null; if (isProtoV2) { diff --git a/packages/driver/test/client.test.ts b/packages/driver/test/client.test.ts index 875aad4c1..e2beae04f 100644 --- a/packages/driver/test/client.test.ts +++ b/packages/driver/test/client.test.ts @@ -2366,13 +2366,13 @@ if (getEdgeDBVersion().major >= 6) { try { let res = await client.querySQL("select 1"); - expect(JSON.stringify(res)).toEqual('[{"col~1":1}]'); + expect(JSON.stringify(res)).toEqual('[[1]]'); res = await client.querySQL("select 1 AS foo, 2 AS bar"); - expect(JSON.stringify(res)).toEqual('[{"foo":1,"bar":2}]'); + expect(JSON.stringify(res)).toEqual('[[1,2]]'); res = await client.querySQL("select 1 + $1::int8", [41]); - expect(JSON.stringify(res)).toEqual('[{"col~1":42}]'); + expect(JSON.stringify(res)).toEqual('[[42]]'); } finally { await client.close(); } @@ -2439,11 +2439,11 @@ if (getEdgeDBVersion().major >= 6) { try { for (const [typename, val] of pgTypes) { - const res = await client.querySQL<{ val: any }>( + const res = await client.querySQL( `select $1::${typename} as "val"`, [val], ); - expect(JSON.stringify(res[0].val)).toEqual(JSON.stringify(val)); + expect(JSON.stringify(res[0][0])).toEqual(JSON.stringify(val)); } } finally { await client.close();