Skip to content

Commit

Permalink
Support union type links (#279)
Browse files Browse the repository at this point in the history
  • Loading branch information
jaclarke committed May 4, 2023
1 parent 17bd516 commit ea04f85
Show file tree
Hide file tree
Showing 11 changed files with 185 additions and 30 deletions.
2 changes: 1 addition & 1 deletion packages/driver/src/reflection/queries/scalars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ select InheritingObject {
is_abstract,
bases: { id, name },
ancestors: { id, name },
children := .<bases[IS Type] { id, name },
children := (select .<bases)[IS Type] { id, name },
descendants := .<ancestors[IS Type] { id, name }
}
FILTER
Expand Down
57 changes: 47 additions & 10 deletions packages/driver/src/reflection/queries/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export const typeMapping = new Map([
]
]);

export async function _types(
export async function getTypes(
cxn: Executor,
params?: {debug?: boolean}
): Promise<Types> {
Expand All @@ -133,7 +133,10 @@ export async function _types(
SELECT Type {
id,
name,
name :=
array_join(array_agg([IS ObjectType].union_of.name), ' | ')
IF EXISTS [IS ObjectType].union_of
ELSE .name,
is_abstract := .abstract,
kind := 'object' IF Type IS ObjectType ELSE
Expand Down Expand Up @@ -183,7 +186,11 @@ export async function _types(
) {
target := (.subject[is schema::Property].name ?? .subject[is schema::Link].name ?? .subjectexpr)
} filter .name = 'std::exclusive'),
backlinks := (SELECT DETACHED Link FILTER .target = Type) {
backlinks := (
SELECT DETACHED Link
FILTER .target = Type
AND NOT EXISTS .source[IS ObjectType].union_of
) {
card := "AtMostOne"
IF
EXISTS (select .constraints filter .name = 'std::exclusive')
Expand Down Expand Up @@ -221,12 +228,12 @@ export async function _types(
ORDER BY .name;
`;

const types: Type[] = JSON.parse(await cxn.queryJSON(QUERY));
const _types: Type[] = JSON.parse(await cxn.queryJSON(QUERY));
// tslint:disable-next-line
if (debug) console.log(JSON.stringify(types, null, 2));
if (debug) console.log(JSON.stringify(_types, null, 2));

// remap types
for (const type of types) {
for (const type of _types) {
if (Array.isArray((type as ObjectType).backlinks)) {
for (const backlink of (type as ObjectType).backlinks) {
const isName = backlink.name.match(/\[is (.+)\]/)![1];
Expand Down Expand Up @@ -268,7 +275,7 @@ export async function _types(
}

const rawExclusives: {target: string}[] = type.exclusives as any;
const exclusives: typeof type["exclusives"] = [];
const exclusives: (typeof type)["exclusives"] = [];
for (const ex of rawExclusives) {
const target = ex.target;
if (target in ptrs) {
Expand Down Expand Up @@ -305,10 +312,40 @@ export async function _types(
break;
}
}
types.push(numberType);
_types.push(numberType);

// Now sort `types` topologically:
return topoSort(types);
const types = topoSort(_types);

// For union types, set pointers to be pointers common to all
// types in the union
for (const [_, type] of types) {
if (type.kind === "object" && type.union_of.length) {
const unionTypes = type.union_of.map(({id}) => {
const t = types.get(id);
if (t.kind !== "object") {
throw new Error(
`type '${t.name}' of union '${type.name}' is not an object type`
);
}
return t;
});

const [first, ...rest] = unionTypes;
const restPointerNames = rest.map(
t => new Set(t.pointers.map(p => p.name))
);
for (const pointer of first.pointers) {
if (restPointerNames.every(names => names.has(pointer.name))) {
(type.pointers as Pointer[]).push(pointer);
}
}
type.backlinks = [];
type.backlink_stubs = [];
}
}

return types;
}

export function topoSort(types: Type[]) {
Expand Down Expand Up @@ -366,4 +403,4 @@ export function topoSort(types: Type[]) {
return sorted;
}

export {_types as types};
export {getTypes as types};
12 changes: 12 additions & 0 deletions packages/generate/dbschema/default.esdl
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,18 @@ module default {

type Simple extending HasName, HasAge {}

type X {
property a -> str;
property b -> int32;
}
type Y {
property a -> str;
property c -> bool;
}
type Z {
link xy -> X | Y;
}

# Unicode handling
# https://github.com/edgedb/edgedb/blob/master/tests/schemas/dump02_default.esdl

Expand Down
15 changes: 15 additions & 0 deletions packages/generate/dbschema/migrations/00018.edgeql
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
CREATE MIGRATION m1j6yidiiae7thrsrtlajwzgbmzuwuhsoeikzxje4ok4dpka7lyhha
ONTO m1yg2okbivunkolgbdsz43aktxfipd47wmaxp4cq3kaq5giox5jqvq
{
CREATE TYPE default::X {
CREATE PROPERTY a -> std::str;
CREATE PROPERTY b -> std::int32;
};
CREATE TYPE default::Y {
CREATE PROPERTY a -> std::str;
CREATE PROPERTY c -> std::bool;
};
CREATE TYPE default::Z {
CREATE LINK xy -> (default::Y | default::X);
};
};
30 changes: 23 additions & 7 deletions packages/generate/src/edgeql-js/generateInterfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,11 @@ export const generateInterfaces = (params: GenerateInterfacesParams) => {
if (type.kind !== "object") {
continue;
}
if (
(type.union_of && type.union_of.length) ||
(type.intersection_of && type.intersection_of.length)
) {

const isUnionType = Boolean(type.union_of?.length);
const isIntersectionType = Boolean(type.intersection_of?.length);

if (isIntersectionType) {
continue;
}

Expand All @@ -115,7 +116,18 @@ export const generateInterfaces = (params: GenerateInterfacesParams) => {

const getTSType = (pointer: $.introspect.Pointer): string => {
const targetType = types.get(pointer.target_id);
if (pointer.kind === "link") {
const isLink = pointer.kind === "link";
const isUnion =
isLink &&
targetType.kind === "object" &&
Boolean(targetType.union_of?.length);

if (isUnion) {
return targetType.union_of
.map(({id}) => types.get(id))
.map(member => getTypeName(member.name))
.join(" | ");
} else if (isLink) {
return getTypeName(targetType.name);
} else {
return toTSScalarType(
Expand All @@ -131,9 +143,13 @@ export const generateInterfaces = (params: GenerateInterfacesParams) => {

const {module: plainTypeModule} = getPlainTypeModule(type.name);
const pointers = type.pointers.filter(ptr => ptr.name !== "__type__");
plainTypeModule.types.set(name, getTypeName(type.name, true));

if (!isUnionType) {
plainTypeModule.types.set(name, getTypeName(type.name, true));
}

plainTypeModule.buf.writeln([
t`export interface ${getTypeName(type.name)}${
t`${isUnionType ? "" : "export "}interface ${getTypeName(type.name)}${
type.bases.length
? ` extends ${type.bases
.map(({id}) => {
Expand Down
46 changes: 36 additions & 10 deletions packages/generate/src/edgeql-js/generateObjectTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,21 @@ export const getStringRepresentation: (
runtimeType: [getRef(type.name)]
};
}
if (type.union_of?.length) {
const items = type.union_of.map(it =>
getStringRepresentation(types.get(it.id), params)
);
return {
staticType: joinFrags(
items.map(it => it.staticType),
" | "
),
runtimeType: joinFrags(
items.map(it => it.runtimeType),
" | "
)
};
}
return {
staticType: [getRef(type.name)],
runtimeType: [getRef(type.name)]
Expand Down Expand Up @@ -225,10 +240,11 @@ export const generateObjectTypes = (params: GeneratorParams) => {
// }
continue;
}
if (
(type.union_of && type.union_of.length) ||
(type.intersection_of && type.intersection_of.length)
) {

const isUnionType = Boolean(type.union_of?.length);
const isIntersectionType = Boolean(type.intersection_of?.length);

if (isIntersectionType) {
continue;
}

Expand Down Expand Up @@ -264,9 +280,13 @@ export const generateObjectTypes = (params: GeneratorParams) => {

// const {module: plainTypeModule} = getPlainTypeModule(type.name);

// plainTypeModule.types.set(name, getTypeName(type.name, true));
// if (!isUnionType) {
// plainTypeModule.types.set(name, getTypeName(type.name, true));
// }
// plainTypeModule.buf.writeln([
// t`export interface ${getTypeName(type.name)}${
// t`${
// !isUnionType ? "export " : ""
// }interface ${getTypeName(type.name)}${
// type.bases.length
// ? ` extends ${type.bases
// .map(({id}) => {
Expand All @@ -279,19 +299,20 @@ export const generateObjectTypes = (params: GeneratorParams) => {
// type.pointers.length
// ? `{\n${type.pointers
// .map(pointer => {
// const isOptional = pointer.card === $.Cardinality.AtMostOne;
// const isOptional =
// pointer.real_cardinality === Cardinality.AtMostOne;
// return ` ${quote(pointer.name)}${
// isOptional ? "?" : ""
// }: ${getTSType(pointer)}${
// pointer.card === $.Cardinality.Many ||
// pointer.card === $.Cardinality.AtLeastOne
// pointer.card === Cardinality.Many ||
// pointer.card === Cardinality.AtLeastOne
// ? "[]"
// : ""
// }${isOptional ? " | null" : ""};`;
// })
// .join("\n")}\n}`
// : "{}"
// }\n`
// }\n`,
// ]);

/////////
Expand Down Expand Up @@ -459,6 +480,11 @@ export const generateObjectTypes = (params: GeneratorParams) => {
/////////
// generate runtime type
/////////
if (isUnionType) {
// union types don't need runtime type
continue;
}

const literal = getRef(type.name, {prefix: ""});

body.writeln([
Expand Down
2 changes: 1 addition & 1 deletion packages/generate/src/genutil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type {
DirBuilder,
IdentRef
} from "./builders";
import type * as introspect from "edgedb/dist/reflection/queries/types";
import * as introspect from "edgedb/dist/reflection/queries/types";
import {util} from "edgedb/dist/reflection/index";

export {$} from "edgedb";
Expand Down
10 changes: 10 additions & 0 deletions packages/generate/test/insert.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -394,3 +394,13 @@ test("insert named tuple as shape", async () => {
profiles: [{a: "a", b: "b", c: "c"}]
});
});

test("type union links", async () => {
const query = e.insert(e.Z, {
xy: e.insert(e.Y, {
c: true
})
});

await query.run(client);
});
6 changes: 5 additions & 1 deletion packages/generate/test/interfaces.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as edgedb from "edgedb";
import * as tc from "conditional-type-checks";

import type {Movie, schema} from "../dbschema/interfaces";
import type {Movie, X, Y, Z} from "../dbschema/interfaces";

export type Genre = "Horror" | "Action" | "RomCom" | "Science Fiction";

Expand All @@ -26,8 +26,12 @@ export interface test_Profile extends BaseObject {
b?: string | null;
c?: string | null;
}
interface test_Z extends BaseObject {
xy?: X | Y | null;
}

test("check generated interfaces", () => {
// TODO: re-enable test when 2.0 is stable
tc.assert<tc.IsExact<Movie, test_Movie>>(true);
tc.assert<tc.IsExact<Z, test_Z>>(true);
});
15 changes: 15 additions & 0 deletions packages/generate/test/objectTypes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const $Hero = e.default.Hero.__element__;
const $AnnotationSubject = e.schema.AnnotationSubject.__element__;
const $Bag = e.default.Bag.__element__;
const $Simple = e.default.Simple.__element__;
const $Z = e.default.Z.__element__;

test("property hydration", () => {
expect(typeof $Hero).toBe("object");
Expand Down Expand Up @@ -37,6 +38,20 @@ test("link hydration", () => {
"default::Villain"
);
expect($Hero.__pointers__.villains.properties).toEqual({});

// type union link
expect($Z.__pointers__.xy.__kind__).toEqual("link");
expect($Z.__pointers__.xy.target.__name__).toEqual(
"default::X | default::Y"
);
expect(Object.keys($Z.__pointers__.xy.target.__pointers__).sort()).toEqual([
"__type__",
"a",
"id",
]);
expect($Z.__pointers__.xy.target.__pointers__.a.target.__name__).toEqual(
"std::str"
);
});

const link = $AnnotationSubject.__pointers__.annotations;
Expand Down
20 changes: 20 additions & 0 deletions packages/generate/test/select.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1349,6 +1349,26 @@ test("filter_single card mismatch", async () => {
// await query.run(client);
// })

test("type union links", async () => {
const query = e.select(e.Z, z => ({
xy: {
a: true,
...e.is(e.X, {
b: true
})
}
}));

const result = await query.run(client);

tc.assert<
tc.IsExact<
typeof result,
{xy: {a: string | null; b: number | null} | null}[]
>
>(true);
});

// Modifier methods removed for now, until we can fix typescript inference
// problems / excessively deep errors

Expand Down

0 comments on commit ea04f85

Please sign in to comment.