Skip to content

Commit 8a956d6

Browse files
authored
Add backlinks to query builder (#961)
Object models now have a `__backlinks__` field, which is set to a `GelObjectBacklinksModel`. This backlinks model contains multi links to `std::BaseObject`. These new backlinks models are generated during reflection. At runtime, intersections generate a backlink model alongside the intersection type. A simple query with backlinks: ```py default.Inh_A.__backlinks__.l .is_(default.Link_Inh_A) .select(n=True, l=True) ``` ```edgeql WITH baseobject_and_link_inh_a := ( default::Inh_A.<l [is default::Link_Inh_A] ) SELECT baseobject_and_link_inh_a { n, l := baseobject_and_link_inh_a.l { * }, } ``` A more complex query with backlinks: ```py default.Link_Inh_AB.l # Link .is_(default.Inh_AC) # Intersection .__backlinks__.l.is_(default.Link_Inh_A) # Backlink .select(n=True, l=True) ``` ```edgeql WITH baseobject_and_link_inh_a := ( ( default::Link_Inh_AB.l [is default::Inh_AC] ).<l [is default::Link_Inh_A] ) SELECT baseobject_and_link_inh_a { n, l := baseobject_and_link_inh_a.l { * }, } ``` Also works inside shapes: ```py default.Inh_AB.select( a=lambda x: x.__backlinks__.l.is_(default.Link_Inh_AB).limit(1).n ) ``` ```edgeql WITH inh_ab := default::Inh_AB SELECT inh_ab { a := ( SELECT (inh_ab.<l [is default::Link_Inh_AB]) { * } LIMIT 1 ).n, } ```
1 parent e0a14b6 commit 8a956d6

File tree

15 files changed

+1109
-69
lines changed

15 files changed

+1109
-69
lines changed

gel/_internal/_codegen/_models/_pydantic.py

Lines changed: 253 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,12 @@ class ModuleAspect(enum.Enum):
435435
LATE = enum.auto()
436436

437437

438+
@dataclasses.dataclass(frozen=True, kw_only=True)
439+
class Backlink:
440+
source: reflection.ObjectType
441+
pointer: reflection.Pointer
442+
443+
438444
class SchemaGenerator:
439445
def __init__(
440446
self,
@@ -451,6 +457,9 @@ def __init__(
451457
self._std_modules: list[SchemaPath] = []
452458
self._types: Mapping[str, reflection.Type] = {}
453459
self._casts: reflection.CastMatrix
460+
self._backlinks: dict[
461+
reflection.ObjectType, dict[str, list[Backlink]]
462+
] = {}
454463
self._operators: reflection.OperatorMatrix
455464
self._functions: list[reflection.Function]
456465
self._globals: list[reflection.Global]
@@ -579,6 +588,7 @@ def run(self, outdir: pathlib.Path) -> tuple[Schema, set[pathlib.Path]]:
579588
all_casts=self._casts,
580589
all_operators=self._operators,
581590
all_globals=self._globals,
591+
all_backlinks=self._backlinks,
582592
modules=self._modules,
583593
schema_part=self._schema_part,
584594
)
@@ -604,6 +614,7 @@ def run(self, outdir: pathlib.Path) -> tuple[Schema, set[pathlib.Path]]:
604614
all_casts=self._casts,
605615
all_operators=self._operators,
606616
all_globals=self._globals,
617+
all_backlinks=self._backlinks,
607618
modules=all_modules,
608619
schema_part=self._schema_part,
609620
)
@@ -659,6 +670,24 @@ def introspect_schema(self) -> Schema:
659670
if reflection.is_object_type(t):
660671
name = t.schemapath
661672
self._modules[name.parent]["object_types"][name.name] = t
673+
674+
for p in t.pointers:
675+
if (
676+
reflection.is_link(p)
677+
# For now don't include std::BaseObject.__type__
678+
# Users should just select on the appropriate type
679+
and p.name != "__type__"
680+
):
681+
target = self._types[p.target_id]
682+
assert isinstance(target, reflection.ObjectType)
683+
if target not in self._backlinks:
684+
self._backlinks[target] = {}
685+
if p.name not in self._backlinks[target]:
686+
self._backlinks[target][p.name] = []
687+
self._backlinks[target][p.name].append(
688+
Backlink(source=t, pointer=p)
689+
)
690+
662691
elif reflection.is_scalar_type(t):
663692
name = t.schemapath
664693
self._modules[name.parent]["scalar_types"][name.name] = t
@@ -694,6 +723,7 @@ def _generate_common_types(
694723
all_casts=self._casts,
695724
all_operators=self._operators,
696725
all_globals=self._globals,
726+
all_backlinks=self._backlinks,
697727
modules=self._modules,
698728
schema_part=self._schema_part,
699729
)
@@ -944,6 +974,9 @@ def __init__(
944974
all_casts: reflection.CastMatrix,
945975
all_operators: reflection.OperatorMatrix,
946976
all_globals: list[reflection.Global],
977+
all_backlinks: Mapping[
978+
reflection.ObjectType, Mapping[str, Sequence[Backlink]]
979+
],
947980
modules: Collection[SchemaPath],
948981
schema_part: reflection.SchemaPart,
949982
) -> None:
@@ -954,6 +987,7 @@ def __init__(
954987
self._casts = all_casts
955988
self._operators = all_operators
956989
self._globals = all_globals
990+
self._backlinks = all_backlinks
957991
schema_obj_type = None
958992
for t in all_types.values():
959993
self._types_by_name[t.name] = t
@@ -2340,7 +2374,7 @@ def write_generic_types(
23402374
unpack = self.import_name("typing_extensions", "Unpack")
23412375
geltype = self.import_name(BASE_IMPL, "GelType")
23422376
geltypemeta = self.import_name(BASE_IMPL, "GelTypeMeta")
2343-
gelmodel = self.import_name(BASE_IMPL, "GelModel")
2377+
gelobjectmodel = self.import_name(BASE_IMPL, "GelObjectModel")
23442378
gelmodelmeta = self.import_name(BASE_IMPL, "GelModelMeta")
23452379
anytuple = self.import_name(BASE_IMPL, "AnyTuple")
23462380
anynamedtuple = self.import_name(BASE_IMPL, "AnyNamedTuple")
@@ -2367,7 +2401,7 @@ def write_generic_types(
23672401
geltype,
23682402
],
23692403
SchemaPath("std", "anyobject"): [
2370-
gelmodel,
2404+
gelobjectmodel,
23712405
"anytype",
23722406
],
23732407
SchemaPath("std", "anytuple"): [
@@ -4086,6 +4120,9 @@ def _mangle_default_shape(name: str) -> str:
40864120
if proplinks:
40874121
self.write_object_type_link_models(objtype)
40884122

4123+
if objtype.name != "std::FreeObject":
4124+
self._write_object_backlinks(objtype)
4125+
40894126
anyobject_meta = self.get_object(
40904127
SchemaPath("std", "__anyobject_meta__"),
40914128
aspect=ModuleAspect.SHAPES,
@@ -4213,6 +4250,16 @@ def write_id_computed(
42134250
self.write(f"__gel_type_class__ = __{name}_ops__")
42144251
if objtype.name == "std::BaseObject":
42154252
write_id_attr(objtype, "RequiredId")
4253+
4254+
if objtype.name != "std::FreeObject":
4255+
backlinks_model_name = self._mangle_backlinks_model_name(name)
4256+
g_oblm_desc = self.import_name(
4257+
BASE_IMPL, "GelObjectBacklinksModelDescriptor"
4258+
)
4259+
oblm_desc = f"{g_oblm_desc}[{backlinks_model_name}]"
4260+
self.write(f"__backlinks__: {oblm_desc} = {oblm_desc}()")
4261+
self.write()
4262+
42164263
self._write_base_object_type_body(objtype, include_tname=True)
42174264
with self.type_checking():
42184265
self._write_object_type_qb_methods(objtype)
@@ -4333,6 +4380,208 @@ def write_id_computed(
43334380

43344381
self.write()
43354382

4383+
@staticmethod
4384+
def _mangle_backlinks_model_name(name: str) -> str:
4385+
return f"__{name}_backlinks__"
4386+
4387+
def _write_object_backlinks(
4388+
self,
4389+
objtype: reflection.ObjectType,
4390+
) -> None:
4391+
type_name = objtype.schemapath
4392+
name = type_name.name
4393+
4394+
schema_path = self.import_name(BASE_IMPL, "SchemaPath")
4395+
parametric_type_name = self.import_name(
4396+
BASE_IMPL, "ParametricTypeName"
4397+
)
4398+
computed_multi_link = self.import_name(BASE_IMPL, "ComputedMultiLink")
4399+
std_base_object_t = self.get_object(
4400+
SchemaPath.from_segments("std", "BaseObject"),
4401+
aspect=ModuleAspect.SHAPES,
4402+
)
4403+
4404+
objtype_bases = [
4405+
base_type
4406+
for base_ref in objtype.bases
4407+
if (base_type := self._types.get(base_ref.id, None))
4408+
if isinstance(base_type, reflection.ObjectType)
4409+
]
4410+
objtype_name = type_name.as_python_code(
4411+
schema_path, parametric_type_name
4412+
)
4413+
4414+
backlinks_model_name = self._mangle_backlinks_model_name(name)
4415+
4416+
backlinks_class_bases: list[str]
4417+
backlinks_reflection_class_bases: list[str]
4418+
4419+
# Backlinks' reflections' pointers combine the current types
4420+
# backlinks with those of their base types.
4421+
#
4422+
# Eg. With `type A` and `type B extending A`,
4423+
# - __A_backlinks__.__gel_reflection__.pointers will contain
4424+
# all backlinks to A (and BaseObject)
4425+
# - __B_backlinks__.__gel_reflection__.pointers will contain
4426+
# all backlinks to B, backlinks to A.
4427+
#
4428+
# No base type backlinks are needed for BaseObject.
4429+
backlinks_reflection_pointer_bases: list[str]
4430+
4431+
if objtype.name == "std::BaseObject":
4432+
# __BaseObject_backlinks__ derives from GelObjectBacklinksModel
4433+
# while all other backlinks derive from it directly on indirectly.
4434+
object_backlinks_model = self.import_name(
4435+
BASE_IMPL, "GelObjectBacklinksModel"
4436+
)
4437+
backlinks_class_bases = [object_backlinks_model]
4438+
backlinks_reflection_class_bases = [
4439+
f"{object_backlinks_model}.__gel_reflection__"
4440+
]
4441+
backlinks_reflection_pointer_bases = []
4442+
else:
4443+
backlinks_class_bases = [
4444+
self.get_object(
4445+
SchemaPath(
4446+
base_type.schemapath.parent,
4447+
self._mangle_backlinks_model_name(
4448+
base_type.schemapath.name
4449+
),
4450+
),
4451+
aspect=ModuleAspect.SHAPES,
4452+
)
4453+
for base_type in objtype_bases
4454+
]
4455+
backlinks_reflection_class_bases = [
4456+
f"{bbt}.__gel_reflection__" for bbt in backlinks_class_bases
4457+
]
4458+
backlinks_reflection_pointer_bases = backlinks_class_bases
4459+
4460+
object_backlinks = self._backlinks.get(objtype, {})
4461+
4462+
# The backlinks model class
4463+
with self._class_def(backlinks_model_name, backlinks_class_bases):
4464+
with self._class_def(
4465+
"__gel_reflection__", backlinks_reflection_class_bases
4466+
):
4467+
self.write(f"name = {objtype_name}")
4468+
self.write(f"type_name = {objtype_name}")
4469+
self._write_backlinks_pointers_reflection(
4470+
object_backlinks, backlinks_reflection_pointer_bases
4471+
)
4472+
4473+
for backlink_name in object_backlinks:
4474+
backlink_t = f"{computed_multi_link}[{std_base_object_t}]"
4475+
self.write(f"{backlink_name}: {backlink_t}")
4476+
4477+
self.export(backlinks_model_name)
4478+
4479+
self.write()
4480+
4481+
def _write_backlinks_pointers_reflection(
4482+
self,
4483+
object_backlinks: Mapping[str, Sequence[Backlink]],
4484+
backlinks_reflection_pointer_bases: Sequence[str],
4485+
) -> None:
4486+
dict_ = self.import_name(
4487+
"builtins", "dict", import_time=ImportTime.typecheck
4488+
)
4489+
str_ = self.import_name(
4490+
"builtins", "str", import_time=ImportTime.typecheck
4491+
)
4492+
gel_ptr_ref = self.import_name(
4493+
BASE_IMPL,
4494+
"GelPointerReflection",
4495+
import_time=ImportTime.runtime
4496+
if object_backlinks
4497+
else ImportTime.typecheck,
4498+
)
4499+
lazyclassproperty = self.import_name(BASE_IMPL, "LazyClassProperty")
4500+
ptr_ref_t = f"{dict_}[{str_}, {gel_ptr_ref}]"
4501+
with self._classmethod_def(
4502+
"pointers",
4503+
[],
4504+
ptr_ref_t,
4505+
decorators=(f'{lazyclassproperty}["{ptr_ref_t}"]',),
4506+
):
4507+
if object_backlinks:
4508+
self.write(f"my_ptrs: {ptr_ref_t} = {{")
4509+
classes = {
4510+
"SchemaPath": self.import_name(BASE_IMPL, "SchemaPath"),
4511+
"ParametricTypeName": self.import_name(
4512+
BASE_IMPL, "ParametricTypeName"
4513+
),
4514+
"GelPointerReflection": gel_ptr_ref,
4515+
"Cardinality": self.import_name(BASE_IMPL, "Cardinality"),
4516+
"PointerKind": self.import_name(BASE_IMPL, "PointerKind"),
4517+
"StdBaseObject": self.get_object(
4518+
SchemaPath.from_segments("std", "BaseObject"),
4519+
aspect=ModuleAspect.SHAPES,
4520+
),
4521+
}
4522+
with self.indented():
4523+
for (
4524+
backlink_name,
4525+
backlink_values,
4526+
) in object_backlinks.items():
4527+
r = self._reflect_backlink(
4528+
backlink_name, backlink_values, classes
4529+
)
4530+
self.write(f"{backlink_name!r}: {r},")
4531+
self.write("}")
4532+
else:
4533+
self.write(f"my_ptrs: {ptr_ref_t} = {{}}")
4534+
4535+
if backlinks_reflection_pointer_bases:
4536+
pp = "__gel_reflection__.pointers"
4537+
ret = self.format_list(
4538+
"return ({list})",
4539+
[
4540+
"my_ptrs",
4541+
*_map_name(
4542+
lambda s: f"{s}.{pp}",
4543+
backlinks_reflection_pointer_bases,
4544+
),
4545+
],
4546+
separator=" | ",
4547+
carry_separator=True,
4548+
)
4549+
else:
4550+
ret = "return my_ptrs"
4551+
4552+
self.write(ret)
4553+
4554+
self.write()
4555+
4556+
def _reflect_backlink(
4557+
self,
4558+
name: str,
4559+
backlinks: Sequence[Backlink],
4560+
classes: dict[str, str],
4561+
) -> str:
4562+
kwargs: dict[str, str] = {
4563+
"name": repr(name),
4564+
"type": classes["StdBaseObject"],
4565+
"kind": (
4566+
f"{classes['PointerKind']}({str(reflection.PointerKind.Link)!r})"
4567+
),
4568+
"cardinality": (
4569+
f"{classes['Cardinality']}({str(reflection.Cardinality.Many)!r})"
4570+
),
4571+
"computed": "True",
4572+
"readonly": "True",
4573+
"has_default": "False",
4574+
"mutable": "False",
4575+
}
4576+
4577+
# For now don't get any back link props
4578+
kwargs["properties"] = "None"
4579+
4580+
return self.format_list(
4581+
f"{classes['GelPointerReflection']}({{list}})",
4582+
[f"{k}={v}" for k, v in kwargs.items()],
4583+
)
4584+
43364585
@contextlib.contextmanager
43374586
def _object_type_variant(
43384587
self,
@@ -4354,8 +4603,8 @@ def _object_type_variant(
43544603
)
43554604

43564605
if not list(variant_bases):
4357-
gel_model = self.import_name(BASE_IMPL, "GelModel")
4358-
bases.append(gel_model)
4606+
gel_object_model = self.import_name(BASE_IMPL, "GelObjectModel")
4607+
bases.append(gel_object_model)
43594608

43604609
with self._class_def(
43614610
variant,

gel/_internal/_qb/_abstract.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ class PathExpr(AtomicExpr):
156156
name: str
157157
is_lprop: bool = False
158158
is_link: bool = False
159+
is_backlink: bool = False
159160

160161
def subnodes(self) -> Iterable[Node]:
161162
return (self.source,)

gel/_internal/_qb/_expressions.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,8 @@ def __edgeql_expr__(self, *, ctx: ScopeContext) -> str:
217217
source = current.source
218218
if isinstance(source, PathPrefix) and source.lprop_pivot:
219219
step = f"@{_edgeql.quote_ident(current.name)}"
220+
elif current.is_backlink:
221+
step = f".<{_edgeql.quote_ident(current.name)}"
220222
else:
221223
step = f".{_edgeql.quote_ident(current.name)}"
222224
steps.append(step)

0 commit comments

Comments
 (0)