Skip to content

Commit d5db2b6

Browse files
committed
Initial implementations for backlinks.
1 parent c0d58a1 commit d5db2b6

9 files changed

Lines changed: 378 additions & 2 deletions

File tree

gel/_internal/_codegen/_models/_pydantic.py

Lines changed: 236 additions & 0 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
@@ -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 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 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,195 @@ 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+
backlinks_model_name = self._mangle_backlinks_model_name(name)
4405+
4406+
base_object_types = [
4407+
base_type
4408+
for base_ref in objtype.bases
4409+
if (base_type := self._types.get(base_ref.id, None))
4410+
if isinstance(base_type, reflection.ObjectType)
4411+
]
4412+
4413+
backlinks_base_types: list[str]
4414+
base_backlinks_reflections: list[str]
4415+
base_backlinks_models: list[str]
4416+
4417+
if objtype.name == "std::BaseObject":
4418+
object_backlinks_model = self.import_name(
4419+
BASE_IMPL, "GelObjectBacklinksModel"
4420+
)
4421+
backlinks_base_types = [object_backlinks_model]
4422+
base_backlinks_reflections = [
4423+
f"{object_backlinks_model}.__gel_reflection__"
4424+
]
4425+
base_backlinks_models = []
4426+
else:
4427+
backlinks_base_types = [
4428+
self.get_object(
4429+
SchemaPath(
4430+
base_type.schemapath.parent,
4431+
self._mangle_backlinks_model_name(
4432+
base_type.schemapath.name
4433+
),
4434+
),
4435+
aspect=ModuleAspect.SHAPES,
4436+
)
4437+
for base_type in base_object_types
4438+
]
4439+
base_backlinks_reflections = [
4440+
f"{bbt}.__gel_reflection__" for bbt in backlinks_base_types
4441+
]
4442+
base_backlinks_models = backlinks_base_types
4443+
4444+
object_backlinks = self._backlinks.get(objtype, {})
4445+
4446+
# The backlinks model class
4447+
with self._class_def(backlinks_model_name, backlinks_base_types):
4448+
with self._class_def(
4449+
"__gel_reflection__", base_backlinks_reflections
4450+
):
4451+
obj_name = type_name.as_python_code(
4452+
schema_path, parametric_type_name
4453+
)
4454+
self.write(f"name = {obj_name}")
4455+
self.write(f"type_name = {obj_name}")
4456+
self._write_backlinks_pointers_reflection(
4457+
object_backlinks, base_backlinks_models
4458+
)
4459+
4460+
for backlink_name in object_backlinks:
4461+
backlink_t = f"{computed_multi_link}[{std_base_object_t}]"
4462+
self.write(f"{backlink_name}: {backlink_t}")
4463+
4464+
self.export(backlinks_model_name)
4465+
4466+
self.write()
4467+
self.write()
4468+
4469+
def _write_backlinks_pointers_reflection(
4470+
self,
4471+
object_backlinks: Mapping[str, Sequence[Backlink]],
4472+
base_backlinks_models: Sequence[str],
4473+
) -> None:
4474+
dict_ = self.import_name(
4475+
"builtins", "dict", import_time=ImportTime.typecheck
4476+
)
4477+
str_ = self.import_name(
4478+
"builtins", "str", import_time=ImportTime.typecheck
4479+
)
4480+
gel_ptr_ref = self.import_name(
4481+
BASE_IMPL,
4482+
"GelPointerReflection",
4483+
import_time=ImportTime.runtime
4484+
if object_backlinks
4485+
else ImportTime.typecheck,
4486+
)
4487+
lazyclassproperty = self.import_name(BASE_IMPL, "LazyClassProperty")
4488+
ptr_ref_t = f"{dict_}[{str_}, {gel_ptr_ref}]"
4489+
with self._classmethod_def(
4490+
"pointers",
4491+
[],
4492+
ptr_ref_t,
4493+
decorators=(f'{lazyclassproperty}["{ptr_ref_t}"]',),
4494+
):
4495+
if object_backlinks:
4496+
self.write(f"my_ptrs: {ptr_ref_t} = {{")
4497+
classes = {
4498+
"SchemaPath": self.import_name(BASE_IMPL, "SchemaPath"),
4499+
"ParametricTypeName": self.import_name(
4500+
BASE_IMPL, "ParametricTypeName"
4501+
),
4502+
"GelPointerReflection": gel_ptr_ref,
4503+
"Cardinality": self.import_name(BASE_IMPL, "Cardinality"),
4504+
"PointerKind": self.import_name(BASE_IMPL, "PointerKind"),
4505+
"StdBaseObject": self.get_object(
4506+
SchemaPath.from_segments("std", "BaseObject"),
4507+
aspect=ModuleAspect.SHAPES,
4508+
),
4509+
}
4510+
with self.indented():
4511+
for (
4512+
backlink_name,
4513+
backlink_values,
4514+
) in object_backlinks.items():
4515+
r = self._reflect_backlink(
4516+
backlink_name, backlink_values, classes
4517+
)
4518+
self.write(f"{backlink_name!r}: {r},")
4519+
self.write("}")
4520+
else:
4521+
self.write(f"my_ptrs: {ptr_ref_t} = {{}}")
4522+
4523+
if base_backlinks_models:
4524+
pp = "__gel_reflection__.pointers"
4525+
ret = self.format_list(
4526+
"return ({list})",
4527+
[
4528+
"my_ptrs",
4529+
*_map_name(
4530+
lambda s: f"{s}.{pp}", base_backlinks_models
4531+
),
4532+
],
4533+
separator=" | ",
4534+
carry_separator=True,
4535+
)
4536+
else:
4537+
ret = "return my_ptrs"
4538+
4539+
self.write(ret)
4540+
4541+
self.write()
4542+
4543+
def _reflect_backlink(
4544+
self,
4545+
name: str,
4546+
backlinks: Sequence[Backlink],
4547+
classes: dict[str, str],
4548+
) -> str:
4549+
kwargs: dict[str, str] = {
4550+
"name": repr(name),
4551+
"type": classes["StdBaseObject"],
4552+
"kind": (
4553+
f"{classes['PointerKind']}({str(reflection.PointerKind.Link)!r})"
4554+
),
4555+
"cardinality": (
4556+
f"{classes['Cardinality']}({str(reflection.Cardinality.Many)!r})"
4557+
),
4558+
"computed": "True",
4559+
"readonly": "True",
4560+
"has_default": "False",
4561+
"mutable": "False",
4562+
}
4563+
4564+
# For now don't get any back link props
4565+
kwargs["properties"] = "None"
4566+
4567+
return self.format_list(
4568+
f"{classes['GelPointerReflection']}({{list}})",
4569+
[f"{k}={v}" for k, v in kwargs.items()],
4570+
)
4571+
43364572
@contextlib.contextmanager
43374573
def _object_type_variant(
43384574
self,

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)

gel/_internal/_qbmodel/_abstract/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
AbstractGelLinkModel,
1212
AbstractGelModel,
1313
AbstractGelModelMeta,
14+
AbstractGelObjectBacklinksModel,
1415
AbstractGelSourceModel,
1516
DefaultValue,
1617
GelType,
@@ -19,6 +20,7 @@
1920
)
2021

2122
from ._descriptors import (
23+
AbstractGelObjectModel,
2224
AbstractGelProxyModel,
2325
AnyLinkDescriptor,
2426
AnyPropertyDescriptor,
@@ -27,6 +29,7 @@
2729
ComputedMultiPropertyDescriptor,
2830
ComputedPropertyDescriptor,
2931
GelLinkModelDescriptor,
32+
GelObjectBacklinksModelDescriptor,
3033
LinkDescriptor,
3134
ModelFieldDescriptor,
3235
MultiLinkDescriptor,
@@ -118,6 +121,8 @@
118121
"AbstractGelLinkModel",
119122
"AbstractGelModel",
120123
"AbstractGelModelMeta",
124+
"AbstractGelObjectBacklinksModel",
125+
"AbstractGelObjectModel",
121126
"AbstractGelProxyModel",
122127
"AbstractGelSourceModel",
123128
"AbstractLinkSet",
@@ -144,6 +149,7 @@
144149
"DateTimeLike",
145150
"DefaultValue",
146151
"GelLinkModelDescriptor",
152+
"GelObjectBacklinksModelDescriptor",
147153
"GelPrimitiveType",
148154
"GelScalarType",
149155
"GelType",

gel/_internal/_qbmodel/_abstract/_base.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,27 @@ def __edgeql__(self) -> tuple[type, str]:
227227
)
228228

229229

230+
class AbstractGelObjectBacklinksModel(
231+
AbstractGelSourceModel,
232+
_qb.GelTypeMetadata,
233+
):
234+
if TYPE_CHECKING:
235+
# Whether the model was copied by reference and must
236+
# be copied by value before being accessed by the user.
237+
__gel_copied_by_ref__: bool
238+
239+
class __gel_reflection__( # noqa: N801
240+
_qb.GelSourceMetadata.__gel_reflection__,
241+
_qb.GelTypeMetadata.__gel_reflection__,
242+
):
243+
pass
244+
245+
@classmethod
246+
def __edgeql_qb_expr__(cls) -> _qb.Expr: # pyright: ignore [reportIncompatibleMethodOverride]
247+
this_type = cls.__gel_reflection__.type_name
248+
return _qb.SchemaSet(type_=this_type)
249+
250+
230251
class AbstractGelLinkModel(AbstractGelSourceModel):
231252
if TYPE_CHECKING:
232253
# Whether the model was copied by reference and must

0 commit comments

Comments
 (0)