Skip to content

Commit

Permalink
Add a way to overload codec for SQL Row (client.querySQL() results)
Browse files Browse the repository at this point in the history
This commit enables a `client.withCodecs({ sql_row: {...} })` call.
Caveat: `toDatabase()` must be specified even though it's not currently
possible to send rows as query arguments. Here's an example:

  res = await client
    .withCodecs({
      sql_row: {
        fromDatabase(data, desc) {
          return Object.fromEntries(
            desc.names.map((key, index) => [key, data[index]]),
          );
        },
        toDatabase() {
          throw "cannot encode SQL record as a query argument";
        },
      },
    })
    .querySQL( ... );

Here we decode SQL rows into JS objects {colName: colValue}. Note
that toDatabase() is specified, albeit, it's a no op and is there
just to satisfy the type checker.

Most likely we'll end up adding a more high level API for this, e.g.

  client.withSQLRowsAsObjects().querySQL(...)

or

  client.withCodecs(gel.SQLRowsAsObjects).querySQL(...)

Note that either of these APIs would allow to specialize unpacking
SQL rows into objects inside the actual codec, for better performance.

In any case, the underlying machinery for those future new APIs
will likely be the one that this commit implements.
  • Loading branch information
1st1 committed Jan 28, 2025
1 parent b719b91 commit 9917bfc
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 6 deletions.
20 changes: 17 additions & 3 deletions packages/driver/src/codecs/codecs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,10 @@ export namespace Codecs {
fromDatabase: (data: T) => any;
};

export type AnyCodec = Codec<any>;
export type AnyCodec = {
toDatabase: (data: any, ...extras: any[]) => any;
fromDatabase: (data: any, ...extras: any[]) => any;
};

export type BoolCodec = Codec<boolean>;
export type Int16Codec = Codec<number>;
Expand Down Expand Up @@ -115,7 +118,7 @@ export namespace Codecs {
]
>;

export type KnownCodecs = {
export type ScalarCodecs = {
["std::bool"]: BoolCodec;
["std::int16"]: Int16Codec;
["std::int32"]: Int32Codec;
Expand Down Expand Up @@ -155,6 +158,17 @@ export namespace Codecs {
["ext::postgis::box3d"]: PostgisBox3dCodec;
};

export type SQLRowCodec = {
fromDatabase: (data: any[], desc: { names: string[] }) => any;
toDatabase: (data: never) => never;
};

export type ContainerCodecs = {
sql_row: SQLRowCodec;
};

export type KnownCodecs = ScalarCodecs & ContainerCodecs;

export type CodecSpec = Partial<KnownCodecs> & {
[key: string]: AnyCodec;
};
Expand Down Expand Up @@ -196,7 +210,7 @@ type ScalarCodecType = {
};

type CodecsToRegister = {
[key in keyof Codecs.KnownCodecs]: ScalarCodecType;
[key in keyof Codecs.ScalarCodecs]: ScalarCodecType;
};

function registerScalarCodecs(codecs: CodecsToRegister): void {
Expand Down
15 changes: 15 additions & 0 deletions packages/driver/src/codecs/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ const NOOP: Codecs.AnyCodec = {

export type ReadonlyCodecMap = ReadonlyMap<string, Codecs.AnyCodec>;

type ContainerNames = keyof Codecs.ContainerCodecs;
type ContainerOverload<T extends string> = T extends ContainerNames
? Codecs.ContainerCodecs[T] | undefined
: undefined;

export class CodecContext {
private readonly spec: ReadonlyCodecMap | null;
private readonly map: Map<string, Codecs.AnyCodec>;
Expand Down Expand Up @@ -62,6 +67,16 @@ export class CodecContext {
return NOOP;
}

getContainerOverload<T extends ContainerNames>(
kind: T,
): ContainerOverload<T> {
if (this.spec === null || !this.spec.size) {
return void 0 as ContainerOverload<T>;
}

return this.spec.get(kind) as ContainerOverload<T>;
}

hasOverload(codec: ScalarCodec): boolean {
if (this.spec === null || !this.spec.size) {
return false;
Expand Down
5 changes: 5 additions & 0 deletions packages/driver/src/codecs/record.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ export class RecordCodec extends Codec implements ICodec {
result[i] = val;
}

const overload = ctx.getContainerOverload("sql_row");
if (overload != null) {
return overload.fromDatabase(result, { names: this.names });
}

return result;
}

Expand Down
42 changes: 39 additions & 3 deletions packages/driver/test/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2444,11 +2444,11 @@ if (getEdgeDBVersion().major >= 5) {
| "ext::postgis::box3d";

type CodecsToTest = SkipCodecs extends never
? keyof Codecs.KnownCodecs
: Exclude<keyof Codecs.KnownCodecs, SkipCodecs>;
? keyof Codecs.ScalarCodecs
: Exclude<keyof Codecs.ScalarCodecs, SkipCodecs>;

type TestedCodecs = {
[key in CodecsToTest]: CodecValueType<Codecs.KnownCodecs[key]>;
[key in CodecsToTest]: CodecValueType<Codecs.ScalarCodecs[key]>;
};

const allCodecs: TestedCodecs = {
Expand Down Expand Up @@ -2602,6 +2602,42 @@ if (getEdgeDBVersion().major >= 6) {
}
});

test("querySQL codec", async () => {
let client = getClient();

try {
let res = await client.querySQL("select 1");
expect(JSON.stringify(res)).toEqual("[[1]]");

res = await client
.withCodecs({
sql_row: {
fromDatabase(data, desc) {
return Object.fromEntries(
desc.names.map((key, index) => [key, data[index]]),
);
},
toDatabase() {
// toDatabase is required to be specified (limitation
// of TypeScript type system). This isn't super elegant
// and most likely we'll need a higher-level nicer API
// in top of this.
//
// Maybe `client.withCodecs(gel.SQLRowAsObject)`?
throw "cannot encode SQL record as a query argument";
},
},
})
.querySQL("select 1 AS foo, 2 AS bar");
expect(JSON.stringify(res)).toEqual('[{"foo":1,"bar":2}]');

res = await client.querySQL("select 1 + $1::int8", [41]);
expect(JSON.stringify(res)).toEqual("[[42]]");
} finally {
await client.close();
}
});

test("executeSQL", async () => {
let client = getClient();

Expand Down

0 comments on commit 9917bfc

Please sign in to comment.