Skip to content

Commit 17035e8

Browse files
authored
Add type intersection expressions (#948)
Given a schema: ```edgeql type Inh_A { a: int64; }; type Inh_B { b: int64; }; type Inh_C { c: int64; }; type Inh_AB extending Inh_A, Inh_B { ab: int64; }; type Inh_AC extending Inh_A, Inh_C { ac: int64; }; type Inh_BC extending Inh_B, Inh_C { bc: int64; }; type Inh_ABC extending Inh_A, Inh_B, Inh_C { abc: int64; }; type Inh_AB_AC extending Inh_AB, Inh_AC { ab_ac: int64; }; ``` Supports queries such as: ```py client.query( default.Inh_A.when_type(default.Inh_B).is_(default.Inh_C) ) client.query( default.Inh_AB.select(a=lambda x: x.is_(default.Inh_C).c) ) ```
1 parent b5ceeea commit 17035e8

File tree

16 files changed

+1564
-206
lines changed

16 files changed

+1564
-206
lines changed

gel/_internal/_codegen/_models/_pydantic.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2909,15 +2909,15 @@ def _write_prefix_op_method_node_ctor(
29092909
) -> None:
29102910
"""Generate the query node constructor for a prefix operator method.
29112911
2912-
Creates the code that builds a PrefixOp query node for unary operator
2912+
Creates the code that builds a UnaryOp query node for unary operator
29132913
methods like __neg__. The operator is applied to 'self' as the
29142914
operand.
29152915
29162916
Args:
29172917
op: The operator reflection object containing metadata
29182918
"""
29192919
op_name = op.schemapath.name
2920-
node_cls = self.import_name(BASE_IMPL, "PrefixOp")
2920+
node_cls = self.import_name(BASE_IMPL, "UnaryOp")
29212921

29222922
args = [
29232923
"expr=self", # The operand is always 'self' for method calls
@@ -2967,15 +2967,15 @@ def _write_prefix_op_func_node_ctor(
29672967
) -> None:
29682968
"""Generate the query node constructor for a prefix operator function.
29692969
2970-
Creates the code that builds a PrefixOp query node for unary operator
2970+
Creates the code that builds a UnaryOp query node for unary operator
29712971
functions. Unlike method versions, this takes the operand from function
29722972
arguments and applies special type casting for tuple parameters.
29732973
29742974
Args:
29752975
op: The operator reflection object containing metadata
29762976
"""
29772977
op_name = op.schemapath.name
2978-
node_cls = self.import_name(BASE_IMPL, "PrefixOp")
2978+
node_cls = self.import_name(BASE_IMPL, "UnaryOp")
29792979
expr_compat = self.import_name(BASE_IMPL, "ExprCompatible")
29802980
cast_ = self.import_name("typing", "cast")
29812981

gel/_internal/_qb/__init__.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
AbstractFieldDescriptor,
1212
Expr,
1313
PathPrefix,
14+
PathTypeIntersectionPrefix,
1415
Scope,
1516
ScopeContext,
1617
Stmt,
@@ -36,14 +37,14 @@
3637
IntLiteral,
3738
Limit,
3839
Literal,
40+
ObjectWhenType,
3941
Offset,
4042
OrderBy,
4143
OrderByElem,
4244
OrderByExpr,
4345
OrderDirection,
4446
OrderEmptyDirection,
4547
Path,
46-
PrefixOp,
4748
SchemaSet,
4849
SelectStmt,
4950
SetLiteral,
@@ -52,11 +53,13 @@
5253
ShapeOp,
5354
Splat,
5455
StringLiteral,
56+
UnaryOp,
5557
UpdateStmt,
5658
Variable,
5759
construct_infix_op_chain,
5860
empty_set,
5961
empty_set_if_none,
62+
expr_uses_auto_splat,
6063
get_object_type_splat,
6164
toplevel_edgeql,
6265
)
@@ -95,6 +98,7 @@
9598
GelSchemaMetadata,
9699
GelSourceMetadata,
97100
GelTypeMetadata,
101+
GelObjectTypeExprMetadata,
98102
GelObjectTypeMetadata,
99103
)
100104

@@ -128,6 +132,7 @@
128132
"ForStmt",
129133
"FuncCall",
130134
"GelLinkMetadata",
135+
"GelObjectTypeExprMetadata",
131136
"GelObjectTypeMetadata",
132137
"GelPointerReflection",
133138
"GelReflectionProto",
@@ -141,6 +146,7 @@
141146
"IntLiteral",
142147
"Limit",
143148
"Literal",
149+
"ObjectWhenType",
144150
"Offset",
145151
"OrderBy",
146152
"OrderByElem",
@@ -150,7 +156,7 @@
150156
"Path",
151157
"PathAlias",
152158
"PathPrefix",
153-
"PrefixOp",
159+
"PathTypeIntersectionPrefix",
154160
"SchemaSet",
155161
"Scope",
156162
"ScopeContext",
@@ -163,6 +169,7 @@
163169
"Splat",
164170
"Stmt",
165171
"StringLiteral",
172+
"UnaryOp",
166173
"UpdateStmt",
167174
"VarAlias",
168175
"Variable",
@@ -171,6 +178,7 @@
171178
"edgeql_qb_expr",
172179
"empty_set",
173180
"empty_set_if_none",
181+
"expr_uses_auto_splat",
174182
"exprmethod",
175183
"get_object_type_splat",
176184
"get_origin",

gel/_internal/_qb/_abstract.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
if TYPE_CHECKING:
2121
from collections.abc import Iterable, Iterator, Mapping
22-
from gel._internal._schemapath import TypeName
22+
from gel._internal._schemapath import TypeNameExpr
2323

2424

2525
@dataclass(kw_only=True, frozen=True)
@@ -89,7 +89,7 @@ class Expr(Node):
8989
def precedence(self) -> _edgeql.Precedence: ...
9090

9191
@abc.abstractproperty
92-
def type(self) -> TypeName: ...
92+
def type(self) -> TypeNameExpr: ...
9393

9494
@abc.abstractmethod
9595
def __edgeql_expr__(self, *, ctx: ScopeContext) -> str: ...
@@ -100,10 +100,10 @@ def __edgeql_qb_expr__(self) -> Self:
100100

101101
@dataclass(kw_only=True, frozen=True)
102102
class TypedExpr(Expr):
103-
type_: TypeName
103+
type_: TypeNameExpr
104104

105105
@property
106-
def type(self) -> TypeName:
106+
def type(self) -> TypeNameExpr:
107107
return self.type_
108108

109109

@@ -494,12 +494,20 @@ def compute_must_bind_refs(
494494
return (self,)
495495

496496

497+
@dataclass(frozen=True, kw_only=True)
498+
class PathTypeIntersectionPrefix(IdentLikeExpr):
499+
type_filter: TypeNameExpr
500+
501+
def __edgeql_expr__(self, *, ctx: ScopeContext) -> str:
502+
return f"[is {self.type_filter.as_quoted_schema_name()}]"
503+
504+
497505
@dataclass(kw_only=True, frozen=True)
498506
class ImplicitIteratorStmt(IteratorExpr, Stmt):
499507
"""Base class for statements that are implicit iterators"""
500508

501509
@property
502-
def type(self) -> TypeName:
510+
def type(self) -> TypeNameExpr:
503511
return self.iter_expr.type
504512

505513
@property

gel/_internal/_qb/_expressions.py

Lines changed: 59 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121
from gel._internal import _edgeql
2222
from gel._internal._polyfills import _strenum
23-
from gel._internal._schemapath import SchemaPath, TypeName
23+
from gel._internal._schemapath import SchemaPath, TypeName, TypeNameExpr
2424

2525
from ._abstract import (
2626
AtomicExpr,
@@ -239,7 +239,7 @@ def __init__(
239239
/,
240240
*,
241241
op: _edgeql.Token | str,
242-
type_: TypeName,
242+
type_: TypeNameExpr,
243243
) -> None:
244244
super().__init__(type_=type_)
245245
if not isinstance(op, _edgeql.Token):
@@ -252,15 +252,15 @@ def precedence(self) -> _edgeql.Precedence:
252252

253253

254254
@dataclass(kw_only=True, frozen=True)
255-
class PrefixOp(Op):
255+
class UnaryOp(Op):
256256
expr: Expr
257257

258258
def __init__(
259259
self,
260260
*,
261261
expr: ExprCompatible,
262262
op: _edgeql.Token | str,
263-
type_: TypeName,
263+
type_: TypeNameExpr,
264264
) -> None:
265265
object.__setattr__(self, "expr", edgeql_qb_expr(expr))
266266
super().__init__(op=op, type_=type_)
@@ -276,7 +276,7 @@ def __edgeql_expr__(self, *, ctx: ScopeContext) -> str:
276276

277277

278278
@dataclass(kw_only=True, frozen=True)
279-
class CastOp(PrefixOp):
279+
class CastOp(UnaryOp):
280280
def __init__(
281281
self,
282282
*,
@@ -300,6 +300,35 @@ def __edgeql_expr__(self, *, ctx: ScopeContext) -> str:
300300
return f"<{self.type.as_quoted_schema_name()}>{expr}"
301301

302302

303+
@dataclass(kw_only=True, frozen=True)
304+
class ObjectWhenType(UnaryOp):
305+
type_filter: TypeNameExpr
306+
307+
def __init__(
308+
self,
309+
*,
310+
expr: ExprCompatible,
311+
type_filter: TypeNameExpr,
312+
type_: TypeNameExpr,
313+
) -> None:
314+
op = _edgeql.Token.RANGBRACKET
315+
object.__setattr__(self, "type_filter", type_filter)
316+
super().__init__(expr=expr, op=op, type_=type_)
317+
318+
@property
319+
def precedence(self) -> _edgeql.Precedence:
320+
return _edgeql.PRECEDENCE[_edgeql.Token.LBRACKET]
321+
322+
def subnodes(self) -> Iterable[Node]:
323+
return (self.expr,)
324+
325+
def __edgeql_expr__(self, *, ctx: ScopeContext) -> str:
326+
expr = edgeql(self.expr, ctx=ctx)
327+
if _need_left_parens(self.precedence, self.expr):
328+
expr = f"({expr})"
329+
return f"{expr} [is {self.type_filter.as_quoted_schema_name()}]"
330+
331+
303332
def empty_set(type_: TypeName) -> CastOp:
304333
return CastOp(expr=SetLiteral(items=(), type_=type_), type_=type_)
305334

@@ -319,7 +348,7 @@ def __init__(
319348
lexpr: ExprCompatible,
320349
rexpr: ExprCompatible,
321350
op: _edgeql.Token | str,
322-
type_: TypeName,
351+
type_: TypeNameExpr,
323352
) -> None:
324353
object.__setattr__(self, "lexpr", edgeql_qb_expr(lexpr))
325354
object.__setattr__(self, "rexpr", edgeql_qb_expr(rexpr))
@@ -337,7 +366,7 @@ def __init__(
337366
lexpr: ExprCompatible,
338367
rexpr: ExprCompatible,
339368
op: _edgeql.Token | str,
340-
type_: TypeName,
369+
type_: TypeNameExpr,
341370
) -> None:
342371
super().__init__(lexpr=lexpr, rexpr=rexpr, op=op, type_=type_)
343372

@@ -406,7 +435,7 @@ def __init__(
406435
fname: str,
407436
args: list[ExprCompatible] | None = None,
408437
kwargs: dict[str, ExprCompatible] | None = None,
409-
type_: TypeName,
438+
type_: TypeNameExpr,
410439
) -> None:
411440
object.__setattr__(self, "fname", fname)
412441
if args is not None:
@@ -508,7 +537,7 @@ def subnodes(self) -> Iterable[Node]:
508537
return (self.expr,)
509538

510539
@property
511-
def type(self) -> TypeName:
540+
def type(self) -> TypeNameExpr:
512541
return self.expr.type
513542

514543
@property
@@ -588,9 +617,15 @@ def __edgeql_expr__(self, *, ctx: ScopeContext) -> str:
588617

589618
@dataclass(kw_only=True, frozen=True)
590619
class InsertStmt(Stmt, TypedExpr):
620+
type_: TypeName # insert can only deal with simple type names
621+
591622
stmt: _edgeql.Token = _edgeql.Token.INSERT
592623
shape: Shape | None = None
593624

625+
@property
626+
def type(self) -> TypeName:
627+
return self.type_
628+
594629
def subnodes(self) -> Iterable[Node | None]:
595630
return (self.shape,)
596631

@@ -661,7 +696,7 @@ def wrap(
661696
kwargs = {}
662697
if isinstance(expr, ShapeOp):
663698
kwargs["body_scope"] = expr.scope
664-
elif isinstance(expr, (SchemaSet, Path)):
699+
elif expr_uses_auto_splat(expr):
665700
if splat_cb is not None:
666701
shape = splat_cb()
667702
else:
@@ -757,7 +792,7 @@ def __post_init__(self) -> None:
757792
object.__setattr__(self, "var", var)
758793

759794
@property
760-
def type(self) -> TypeName:
795+
def type(self) -> TypeNameExpr:
761796
return self.body.type
762797

763798
def subnodes(self) -> Iterable[Node]:
@@ -786,7 +821,7 @@ class Splat(_strenum.StrEnum):
786821
@dataclass(kw_only=True, frozen=True)
787822
class ShapeElement(Node):
788823
name: str | Splat
789-
origin: TypeName
824+
origin: TypeNameExpr
790825
expr: Expr | None = None
791826

792827
def subnodes(self) -> Iterable[Node]:
@@ -798,7 +833,7 @@ def subnodes(self) -> Iterable[Node]:
798833
@classmethod
799834
def splat(
800835
cls,
801-
source: TypeName,
836+
source: TypeNameExpr,
802837
*,
803838
kind: Splat = Splat.STAR,
804839
) -> Self:
@@ -815,14 +850,23 @@ def subnodes(self) -> Iterable[Node]:
815850
@classmethod
816851
def splat(
817852
cls,
818-
source: TypeName,
853+
source: TypeNameExpr,
819854
*,
820855
kind: Splat = Splat.STAR,
821856
) -> Self:
822857
elements = [ShapeElement.splat(source=source, kind=kind)]
823858
return cls(elements=elements)
824859

825860

861+
def expr_uses_auto_splat(expr: Expr) -> bool:
862+
if isinstance(expr, (SchemaSet, Path)):
863+
return True
864+
elif isinstance(expr, ObjectWhenType):
865+
return expr_uses_auto_splat(expr.expr)
866+
else:
867+
return False
868+
869+
826870
def _render_shape(
827871
shape: Shape,
828872
source: Expr,
@@ -873,7 +917,7 @@ def subnodes(self) -> Iterable[Node]:
873917
return (self.iter_expr, self.shape)
874918

875919
@property
876-
def type(self) -> TypeName:
920+
def type(self) -> TypeNameExpr:
877921
return self.iter_expr.type
878922

879923
@property

0 commit comments

Comments
 (0)