Skip to content

Commit

Permalink
Add .runJSON to query builder expressions (#316)
Browse files Browse the repository at this point in the history
* Add `.runJSON`
* Release v0.20.1
  • Loading branch information
colinhacks committed Mar 28, 2022
1 parent 3cb5c0f commit 231feb0
Show file tree
Hide file tree
Showing 11 changed files with 178 additions and 46 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,15 @@ const result = await query.run(client);
result.actors[0].name; // => Timothee Chalamet
```

You can also fetch the results as a JSON-serialized string with `.runJSON`.
This serialization happens inside the database and is much faster than calling
`JSON.stringify` yourself.

```ts
const result = await query.runJSON(client);
// => '{"actors": [{"name": "Timothee Chalamet", ...}]}'
```

## Contribute

Contributing to this library requires a local installation of EdgeDB. Install
Expand Down
54 changes: 27 additions & 27 deletions docs/expressions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
Expressions
-----------


The query builder is exact what it sounds like: a way to declare EdgeQL
queries with code. By convention, it's imported from the directory where it was
generated as a single, default import called ``e``.
Expand Down Expand Up @@ -43,6 +42,7 @@ These building blocks are used to define *expressions*. Everything you create
using the query builder is an expression. Expressions have a few things in
common.


Expressions produce EdgeQL
^^^^^^^^^^^^^^^^^^^^^^^^^^

Expand All @@ -68,6 +68,18 @@ any expression calling the ``.toEdgeQL()`` method.
e.select(e.Movie, () => ({ id: true, title: true })).toEdgeQL();
// select Movie { id, title }
Type inference
^^^^^^^^^^^^^^

The query builder *automatically infers* the TypeScript type that best represents the result of a given expression. This inferred type can be extracted with the ``$infer`` helper.

.. code-block:: typescript
import e, {$infer} from "./dbschema/edgeql-js";
const query = e.select(e.Movie, () => ({ id: true, title: true }));
type result = $infer<typeof query>;
// {id: string; title: string}[]
Expressions are runnable
^^^^^^^^^^^^^^^^^^^^^^^^
Expand All @@ -79,9 +91,13 @@ Expressions can be executed with the ``.run`` method.
import * as edgedb from "edgedb";
const client = edgedb.createClient();
const myQuery = e.str("Hello world")
const myQuery = e.select(e.Movie, () => ({
id: true,
title: true
}));
const result = await myQuery.run(client)
// => "Hello world"
// => [{ id: "abc...", title: "The Avengers" }, ...]
Note that the ``.run`` method accepts an instance of :js:class:`Client` (or
``Transaction``) as it's first argument. See :ref:`Creating a Client
Expand All @@ -94,31 +110,15 @@ that later.
.run(client: Client | Transaction, params: Params): Promise<T>
Expressions have a type and a cardinality
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Just like expressions in EdgeQL, all expressions are associated with a type
and a cardinality. The query builder is extremely good at *inferring* these.
You can see the values of these with the special ``__element__`` and
``__cardinality__`` properties.

.. code-block:: typescript
const q1 = e.str("Hello");
q1.__element__; // e.str
q1.__cardinality__; // "One"
const q2 = e.Movie;
q2.__element__; // e.Movie
q2.__cardinality__; // "Many"
**JSON serialization**

The inferred type of *any* expression can be extracted with the ``$infer``
helper.
You can also use the ``runJSON`` method to retrieve the query results as a serialized JSON-formatted *string*. This serialization happens inside the database and is much faster than calling ``JSON.stringify``.

.. code-block:: typescript
import e, {$infer} from "./dbschema/edgeql-js";
const query = e.select(e.Movie, () => ({ id: true, title: true }));
type result = $infer<typeof query>;
// {id: string; title: string}[]
const myQuery = e.select(e.Movie, () => ({
id: true,
title: true
}));
const result = await myQuery.runJSON(client);
// => '[{ "id": "abc...", "title": "The Avengers" }, ...]'
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "edgedb",
"version": "0.20.0",
"version": "0.20.1",
"description": "The official Node.js client library for EdgeDB",
"homepage": "https://edgedb.com/docs",
"author": "EdgeDB <info@edgedb.com>",
Expand Down
1 change: 1 addition & 0 deletions qb/dbschema/default.esdl
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ module default {
abstract link movie_character {
property character_name -> str;
}

abstract type Person {
required property name -> str {
constraint exclusive;
Expand Down
27 changes: 12 additions & 15 deletions qb/playground.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,23 @@ import {setupTests} from "./test/setupTeardown";
import e, * as types from "./dbschema/edgeql-js/index";

async function run() {
const asd = {
"3": "asdf",
qwer: "sdf",
} as const;

function infer<T>() {}

console.log(asd[3]);

const {client} = await setupTests();
const query = e.set(
e.tuple({a: 1, b: "asdf", c: e.int16(214)}),
e.tuple({a: 3, b: "asdf", c: e.int64(5)})
);
const query = e.select(e.Villain, () => ({
id: true,
name: true,
nemesis: nemesis => {
const nameLen = e.len(nemesis.name);
return {
name: true,
nameLen,
nameLen2: nameLen,
};
},
}));

console.log(query.toEdgeQL());
const result = await query.run(client);
console.log(result);

// e.literal(e.tuple({a: e.int16}), )
}

run();
Expand Down
43 changes: 43 additions & 0 deletions qb/test/json.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type * as edgedb from "edgedb";
import {$} from "edgedb";
import * as tc from "conditional-type-checks";

import e, {$infer} from "../dbschema/edgeql-js";
import {number} from "../dbschema/edgeql-js/modules/std";
import {setupTests, teardownTests, TestData} from "./setupTeardown";

let client: edgedb.Client;
let data: TestData;

beforeAll(async () => {
const setup = await setupTests();
({client, data} = setup);
});

afterAll(async () => {
await teardownTests(client);
});

test("basic select", async () => {
const query = e.select(e.Movie, movie => ({
title: true,
order_by: movie.title,
}));

const result = await query.runJSON(client);
tc.assert<tc.IsExact<typeof result, string>>(true);
expect(result).toEqual(
'[{"title" : "Captain America: Civil War"}, {"title" : "The Avengers"}]'
);
});

test("select one", async () => {
const query = e.select(e.Movie, movie => ({
title: true,
filter: e.op(movie.title, "=", "The Avengers"),
}));

const result = await query.runJSON(client);
tc.assert<tc.IsExact<typeof result, string>>(true);
expect(result).toEqual('{"title" : "The Avengers"}');
});
57 changes: 56 additions & 1 deletion qb/test/select.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ afterAll(async () => {

test("basic select", () => {
const result = e.select(e.std.str("asdf"));
type result = $.BaseTypeToTsType<typeof result["__element__"]>;
type result = $infer<typeof result>;
tc.assert<tc.IsExact<result, "asdf">>(true);
});

Expand Down Expand Up @@ -820,6 +820,61 @@ test("filters in subqueries", async () => {
>(true);
});

test("repeated computed", async () => {
const query = e.select(e.Villain, () => ({
id: true,
name: true,
nemesis: nemesis => {
const nameLen = e.len(nemesis.name);
return {
name: true,
nameLen,
nameLen2: nameLen,
};
},
}));

expect(query.toEdgeQL()).toEqual(`WITH
__scope_0_Villain := DETACHED default::Villain
SELECT __scope_0_Villain {
id,
name,
nemesis := (
WITH
__scope_1_Hero_expr := __scope_0_Villain.nemesis,
__scope_1_Hero := (FOR __scope_1_Hero_inner IN {__scope_1_Hero_expr} UNION (
WITH
__withVar_2 := std::len(__scope_1_Hero_inner.name)
SELECT __scope_1_Hero_inner {
__withVar_2 := __withVar_2
}
))
SELECT __scope_1_Hero {
name,
single nameLen := __scope_1_Hero.__withVar_2,
single nameLen2 := __scope_1_Hero.__withVar_2
}
)
}`);

const res = await query.run(client);

tc.assert<
tc.IsExact<
typeof res,
{
id: string;
name: string;
nemesis: {
name: string;
nameLen: number;
nameLen2: number;
} | null;
}[]
>
>(true);
});

test("polymorphic subqueries", async () => {
const query = e.select(e.Movie.characters, character => ({
id: true,
Expand Down
6 changes: 5 additions & 1 deletion src/reflection/typesystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ export type Expression<
(BaseType extends Set["__element__"] // short-circuit non-specific types
? {
run(cxn: Executor): any;
runJSON(cxn: Executor): any;
toEdgeQL(): string;
is: any;
assert_single: any;
Expand All @@ -145,7 +146,10 @@ export type Expression<
: $pathify<Set> &
ExpressionMethods<stripSet<Set>> &
(Runnable extends true
? {run(cxn: Executor): Promise<setToTsType<Set>>}
? {
run(cxn: Executor): Promise<setToTsType<Set>>;
runJSON(cxn: Executor): Promise<string>;
}
: {}) &
$tuplePathify<Set> &
$arrayLikeIndexify<Set> &
Expand Down
1 change: 1 addition & 0 deletions src/syntax/params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export type QueryableWithParamsExpression<
cxn: Executor,
args: paramsToParamArgs<Params>
): Promise<setToTsType<Set>>;
runJSON(cxn: Executor, args: paramsToParamArgs<Params>): Promise<string>;
};

export type $expr_WithParams<
Expand Down
3 changes: 2 additions & 1 deletion src/syntax/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import type {
import {literalToTypeSet} from "@generated/castMaps";
import {$arrayLikeIndexify, $tuplePathify} from "./collections";
import {$toEdgeQL} from "./toEdgeQL";
import {$queryFunc} from "./query";
import {$queryFunc, $queryFuncJSON} from "./query";

function PathLeaf<
Root extends TypeSet,
Expand Down Expand Up @@ -215,6 +215,7 @@ export function $expressionify<T extends ExpressionRoot>(
) as any;

expr.run = $queryFunc.bind(expr) as any;
expr.runJSON = $queryFuncJSON.bind(expr) as any;
expr.is = isFunc.bind(expr) as any;
expr.toEdgeQL = $toEdgeQL.bind(expr);
expr.assert_single = () => assert_single(expr) as any;
Expand Down
21 changes: 21 additions & 0 deletions src/syntax/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,24 @@ export async function $queryFunc(this: any, cxn: edgedb.Executor, args: any) {
return cxn.query(expr.toEdgeQL(), _args);
}
}

export async function $queryFuncJSON(
this: any,
cxn: edgedb.Executor,
args: any
) {
const expr = runnableExpressionKinds.has(this.__kind__)
? this
: wrappedExprCache.get(this) ??
wrappedExprCache.set(this, select(this)).get(this);
const _args = jsonifyComplexParams(expr, args);

if (
expr.__cardinality__ === Cardinality.One ||
expr.__cardinality__ === Cardinality.AtMostOne
) {
return cxn.querySingleJSON(expr.toEdgeQL(), _args);
} else {
return cxn.queryJSON(expr.toEdgeQL(), _args);
}
}

0 comments on commit 231feb0

Please sign in to comment.