From 6b36382810a3036768779dd51394cca892750f61 Mon Sep 17 00:00:00 2001 From: dnwpark Date: Thu, 23 Oct 2025 10:23:02 -0700 Subject: [PATCH 1/3] Fix passing type intersections to overloaded functions. --- gel/_internal/_typing_dispatch.py | 5 ++ tests/test_qb.py | 82 ++++++++++++++++++++++++++++--- 2 files changed, 81 insertions(+), 6 deletions(-) diff --git a/gel/_internal/_typing_dispatch.py b/gel/_internal/_typing_dispatch.py index 04319f4a..f74ac598 100644 --- a/gel/_internal/_typing_dispatch.py +++ b/gel/_internal/_typing_dispatch.py @@ -38,6 +38,7 @@ from gel._internal import _typing_inspect from gel._internal import _typing_parametric from gel._internal._utils import type_repr +from gel._internal._qbmodel._abstract._methods import BaseGelModelIntersection _P = ParamSpec("_P") _R_co = TypeVar("_R_co", covariant=True) @@ -66,6 +67,10 @@ def _issubclass(lhs: Any, tp: Any, fn: Any) -> bool: # subtypes of the variable bounds. # This lets us handle cases like: # std.array[Object] <: std.array[_T_anytype]. + + if issubclass(lhs, BaseGelModelIntersection): + return any(_issubclass(c, tp, fn) for c in (lhs.lhs, lhs.rhs)) + if _typing_inspect.is_generic_alias(tp): origin = typing.get_origin(tp) args = typing.get_args(tp) diff --git a/tests/test_qb.py b/tests/test_qb.py index 674e9b3c..dc53b37c 100644 --- a/tests/test_qb.py +++ b/tests/test_qb.py @@ -1695,9 +1695,7 @@ def test_qb_is_type_basic_07(self): # Link TypeIntersection from models.orm_qb import default - result = self.client.query( - default.Link_Inh_A.l.is_(default.Inh_B) - ) + result = self.client.query(default.Link_Inh_A.l.is_(default.Inh_B)) self._assertObjectsWithFields( result, @@ -1900,9 +1898,9 @@ def test_qb_is_type_for_01(self): from models.orm_qb import default, std result = self.client.query( - std.for_( - default.Inh_A.is_(default.Inh_B), lambda x: x - ).select(a=True) + std.for_(default.Inh_A.is_(default.Inh_B), lambda x: x).select( + a=True + ) ) self._assertObjectsWithFields( @@ -2014,6 +2012,78 @@ def test_qb_is_type_for_03(self): excluded_fields={'b', 'c', 'ab', 'ac', 'bc', 'abc', 'ab_ac'}, ) + def test_qb_is_type_as_function_arg_01(self): + # Test that type exprs produced by is_ can be passed as function args + from models.orm_qb import default, std + + result = self.client.query( + std.distinct(default.Inh_A.is_(default.Inh_B)).select('*') + ) + + self._assertObjectsWithFields( + result, + "a", + [ + ( + default.Inh_AB, + { + "a": 4, + "b": 5, + }, + ), + ( + default.Inh_ABC, + { + "a": 13, + "b": 14, + }, + ), + ( + default.Inh_AB_AC, + { + "a": 17, + "b": 18, + }, + ), + ], + excluded_fields={'c', 'ab', 'ac', 'bc', 'abc', 'ab_ac'}, + ) + + def test_qb_is_type_as_function_arg_02(self): + # Test that complex type exprs produced by is_ can be passed as + # function args + from models.orm_qb import default, std + + result = self.client.query( + std.distinct( + default.Inh_A.is_(default.Inh_B).is_(default.Inh_C) + ).select('*') + ) + + self._assertObjectsWithFields( + result, + "a", + [ + ( + default.Inh_ABC, + { + "a": 13, + "b": 14, + "c": 15, + }, + ), + ( + default.Inh_AB_AC, + { + "a": 17, + "b": 18, + "c": 19, + }, + ), + ], + excluded_fields={'ab', 'ac', 'bc', 'abc', 'ab_ac'}, + ) + class TestQueryBuilderModify(tb.ModelTestCase): """This test suite is for data manipulation using QB.""" From e347ba40901af0c7c83d09d9b1237eb18cb700d2 Mon Sep 17 00:00:00 2001 From: dnwpark Date: Thu, 23 Oct 2025 11:31:27 -0700 Subject: [PATCH 2/3] Test user defined functions too. --- tests/dbsetup/orm_qb.gel | 4 ++++ tests/test_qb.py | 17 +++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/tests/dbsetup/orm_qb.gel b/tests/dbsetup/orm_qb.gel index 3edfb5a8..ecef9227 100644 --- a/tests/dbsetup/orm_qb.gel +++ b/tests/dbsetup/orm_qb.gel @@ -618,4 +618,8 @@ type Link_Inh_A { }; }; +function Read_Inh_A(x: Inh_A) -> int64 using (x.a ?? -1); +function Read_Inh_A_Overload(x: Inh_A) -> int64 using (x.a ?? -1); +function Read_Inh_A_Overload(x: Inh_AB) -> int64 using (x.ab ?? -1); + } diff --git a/tests/test_qb.py b/tests/test_qb.py index dc53b37c..f54dd5c0 100644 --- a/tests/test_qb.py +++ b/tests/test_qb.py @@ -2084,6 +2084,23 @@ def test_qb_is_type_as_function_arg_02(self): excluded_fields={'ab', 'ac', 'bc', 'abc', 'ab_ac'}, ) + def test_qb_is_type_as_function_arg_03(self): + # Test that exprs produced by is_ can be passed as function args to + # user defined function + from models.orm_qb import default + + # Note, we do Inh_A[is Inh_B] since is_ currently pretends its return + # type is its argument type. + result = self.client.query( + default.Read_Inh_A(default.Inh_B.is_(default.Inh_A)) + ) + self.assertEqual(sorted(result), [4, 13, 17]) + + result = self.client.query( + default.Read_Inh_A_Overload(default.Inh_B.is_(default.Inh_A)) + ) + self.assertEqual(sorted(result), [6, 13, 20]) + class TestQueryBuilderModify(tb.ModelTestCase): """This test suite is for data manipulation using QB.""" From aeaea2d27464a2faa5c97b6cbde18bc1fbbd5e86 Mon Sep 17 00:00:00 2001 From: dnwpark Date: Wed, 29 Oct 2025 10:00:21 -0700 Subject: [PATCH 3/3] Add _type_expression. --- gel/_internal/_qbmodel/_abstract/_methods.py | 2 ++ gel/_internal/_type_expression.py | 15 +++++++++++++++ gel/_internal/_typing_dispatch.py | 4 ++-- 3 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 gel/_internal/_type_expression.py diff --git a/gel/_internal/_qbmodel/_abstract/_methods.py b/gel/_internal/_qbmodel/_abstract/_methods.py index 5818bda8..621beb45 100644 --- a/gel/_internal/_qbmodel/_abstract/_methods.py +++ b/gel/_internal/_qbmodel/_abstract/_methods.py @@ -20,6 +20,7 @@ from gel._internal._schemapath import ( TypeNameIntersection, ) +from gel._internal import _type_expression from gel._internal._xmethod import classonlymethod from ._base import AbstractGelModel @@ -246,6 +247,7 @@ def __edgeql_qb_expr__(cls) -> _qb.Expr: # pyright: ignore [reportIncompatibleM class BaseGelModelIntersection( BaseGelModel, + _type_expression.Intersection, Generic[_T_Lhs, _T_Rhs], ): __gel_type_class__: ClassVar[type] diff --git a/gel/_internal/_type_expression.py b/gel/_internal/_type_expression.py new file mode 100644 index 00000000..3b69e28e --- /dev/null +++ b/gel/_internal/_type_expression.py @@ -0,0 +1,15 @@ +# SPDX-PackageName: gel-python +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright Gel Data Inc. and the contributors. + +import typing + + +class Intersection: + lhs: typing.ClassVar[type] + rhs: typing.ClassVar[type] + + +class Union: + lhs: typing.ClassVar[type] + rhs: typing.ClassVar[type] diff --git a/gel/_internal/_typing_dispatch.py b/gel/_internal/_typing_dispatch.py index f74ac598..d74bc23f 100644 --- a/gel/_internal/_typing_dispatch.py +++ b/gel/_internal/_typing_dispatch.py @@ -34,11 +34,11 @@ import typing from gel._internal import _namespace +from gel._internal import _type_expression from gel._internal import _typing_eval from gel._internal import _typing_inspect from gel._internal import _typing_parametric from gel._internal._utils import type_repr -from gel._internal._qbmodel._abstract._methods import BaseGelModelIntersection _P = ParamSpec("_P") _R_co = TypeVar("_R_co", covariant=True) @@ -68,7 +68,7 @@ def _issubclass(lhs: Any, tp: Any, fn: Any) -> bool: # This lets us handle cases like: # std.array[Object] <: std.array[_T_anytype]. - if issubclass(lhs, BaseGelModelIntersection): + if issubclass(lhs, _type_expression.Intersection): return any(_issubclass(c, tp, fn) for c in (lhs.lhs, lhs.rhs)) if _typing_inspect.is_generic_alias(tp):