From b5a8eed7f6a76a2403d977086d31a9147087d61b Mon Sep 17 00:00:00 2001 From: Oleg Ovcharuk Date: Fri, 18 Sep 2026 11:39:13 +0300 Subject: [PATCH] Expand SQLAlchemy dialect test coverage --- tests/integration/test_inspect.py | 78 ++++++++++------------- tests/integration/test_suite.py | 73 +++++++++++++++++---- ydb_sqlalchemy/sqlalchemy/requirements.py | 18 ++++++ 3 files changed, 110 insertions(+), 59 deletions(-) diff --git a/tests/integration/test_inspect.py b/tests/integration/test_inspect.py index b6640a1..839113d 100644 --- a/tests/integration/test_inspect.py +++ b/tests/integration/test_inspect.py @@ -1,12 +1,13 @@ import posixpath -import pytest import sqlalchemy as sa from sqlalchemy import Column, Integer, Numeric, Table, Unicode from sqlalchemy.testing.fixtures import TablesTest class TestInspection(TablesTest): + __backend__ = True + @classmethod def define_tables(cls, metadata): Table( @@ -17,31 +18,6 @@ def define_tables(cls, metadata): Column("num", Numeric(22, 9)), ) - @pytest.fixture - def test_view(self, connection): - raw_connection = connection.connection - driver_connection = getattr(raw_connection, "driver_connection", raw_connection) - view_name = "test_view" - table_path = posixpath.join(driver_connection.database, driver_connection.table_path_prefix, "test") - cursor = driver_connection.cursor() - try: - try: - cursor.execute_scheme(f"DROP VIEW `{view_name}`") - except Exception: - pass - - cursor.execute_scheme( - f"CREATE VIEW `{view_name}` WITH (security_invoker = TRUE) AS " - f"SELECT `id`, `value`, `num` FROM `{table_path}`" - ) - yield view_name - finally: - try: - cursor.execute_scheme(f"DROP VIEW `{view_name}`") - except Exception: - pass - cursor.close() - def test_get_columns(self, connection): inspect = sa.inspect(connection) @@ -66,7 +42,7 @@ def test_reflection_ignores_schema(self, connection): # so reflection always targets the connected database (the convention two-tier # SQLAlchemy tooling relies on). inspect = sa.inspect(connection) - bound_database = connection.connection.driver_connection.database.strip("/") + bound_database = (connection.engine.url.database or "").strip("/") for schema in (bound_database, "some_other_database"): assert "test" in inspect.get_table_names(schema=schema) @@ -74,7 +50,7 @@ def test_reflection_ignores_schema(self, connection): assert inspect.get_columns("test", schema=schema) def test_compile_ignores_schema_prefix(self, connection): - bound_database = connection.connection.driver_connection.database.strip("/") + bound_database = (connection.engine.url.database or "").strip("/") # A table addressed via the connected database as schema (the way two-tier # tooling does) must compile without a schema prefix and execute against YDB. @@ -90,21 +66,33 @@ def test_compile_ignores_schema_prefix(self, connection): compiled_foreign = str(sa.select(sa.func.count()).select_from(foreign).compile(connection)) assert "some_other_database." not in compiled_foreign - def test_view_reflection(self, connection, test_view): - view_name = test_view - inspect = sa.inspect(connection) + def test_view_reflection(self, connection): + view_name = "test_view" + database = "/" + (connection.engine.url.database or "").strip("/") + table_path = posixpath.join(database, "test") + try: + connection.execute(sa.DDL(f"DROP VIEW IF EXISTS `{view_name}`")) + connection.execute( + sa.DDL( + f"CREATE VIEW `{view_name}` WITH (security_invoker = TRUE) AS " + f"SELECT `id`, `value`, `num` FROM `{table_path}`" + ) + ) - assert view_name in inspect.get_view_names() - assert inspect.has_table(view_name) - assert inspect.get_view_definition(view_name).startswith(f"CREATE VIEW `{view_name}`") - - columns = {column["name"]: column for column in inspect.get_columns(view_name)} - assert set(columns) == {"id", "value", "num"} - assert isinstance(columns["id"]["type"], sa.INTEGER) - assert columns["id"]["nullable"] is False - assert isinstance(columns["value"]["type"], sa.TEXT) - assert columns["value"]["nullable"] is True - assert isinstance(columns["num"]["type"], sa.DECIMAL) - assert columns["num"]["type"].precision == 22 - assert columns["num"]["type"].scale == 9 - assert columns["num"]["nullable"] is True + inspect = sa.inspect(connection) + assert view_name in inspect.get_view_names() + assert inspect.has_table(view_name) + assert inspect.get_view_definition(view_name).startswith(f"CREATE VIEW `{view_name}`") + + columns = {column["name"]: column for column in inspect.get_columns(view_name)} + assert set(columns) == {"id", "value", "num"} + assert isinstance(columns["id"]["type"], sa.INTEGER) + assert columns["id"]["nullable"] is False + assert isinstance(columns["value"]["type"], sa.TEXT) + assert columns["value"]["nullable"] is True + assert isinstance(columns["num"]["type"], sa.DECIMAL) + assert columns["num"]["type"].precision == 22 + assert columns["num"]["type"].scale == 9 + assert columns["num"]["nullable"] is True + finally: + connection.execute(sa.DDL(f"DROP VIEW IF EXISTS `{view_name}`")) diff --git a/tests/integration/test_suite.py b/tests/integration/test_suite.py index 95d4bef..4a3092b 100644 --- a/tests/integration/test_suite.py +++ b/tests/integration/test_suite.py @@ -117,6 +117,10 @@ def _check_list(self, result, exp, req_keys=None, msg=None): def test_get_indexes(self, connection, use_schema): pass + @pytest.mark.skip("SQLAlchemy's view requirement also assumes unsupported multi-reflection semantics") + def test_get_view_names(self, connection, use_schema): + pass + @classmethod def define_reflected_tables(cls, metadata, schema): Table( @@ -168,10 +172,6 @@ def define_reflected_tables(cls, metadata, schema): schema=schema, ) - @pytest.mark.skip("views unsupported") - def test_get_view_names(self, connection, use_schema): - pass - def test_metadata(self, connection, **kwargs): m = MetaData() m.reflect(connection, resolve_fks=False) @@ -208,10 +208,6 @@ def _type_round_trip(self, connection, metadata, *types): t.create(connection) return [c["type"] for c in inspect(connection).get_columns("t")] - @pytest.mark.skip("YDB: Only Decimal(22,9) is supported for table columns") - def test_numeric_reflection(self): - pass - @pytest.mark.skip("TODO: varchar with length unsupported") def test_varchar_reflection(self): pass @@ -277,10 +273,46 @@ def test_huge_int_auto_accommodation(self, connection, intvalue): pass -@pytest.mark.skip("Use YdbDecimalTest for Decimal type testing") class NumericTest(_NumericTest): - # SqlAlchemy maybe eat Decimal and throw Double - pass + @pytest.mark.skip("Float columns cannot be used as YDB primary keys in the upstream fixture") + def test_float_as_decimal(self): + pass + + @pytest.mark.skip("Float columns cannot be used as YDB primary keys in the upstream fixture") + def test_float_as_float(self): + pass + + @pytest.mark.skip("YDB numeric bind and literal handling is incomplete") + def test_float_coerce_round_trip(self): + pass + + @pytest.mark.skip("Float columns cannot be used as YDB primary keys in the upstream fixture") + def test_float_custom_scale(self): + pass + + @pytest.mark.skip("YDB numeric bind and literal handling is incomplete") + def test_numeric_as_decimal(self): + pass + + @pytest.mark.skip("YDB numeric bind and literal handling is incomplete") + def test_numeric_as_float(self): + pass + + @pytest.mark.skip("YDB numeric bind and literal handling is incomplete") + def test_numeric_null_as_float(self): + pass + + @pytest.mark.skip("Float columns cannot be used as YDB primary keys in the upstream fixture") + def test_render_literal_float(self): + pass + + @pytest.mark.skip("YDB numeric bind and literal handling is incomplete") + def test_render_literal_numeric(self): + pass + + @pytest.mark.skip("YDB numeric bind and literal handling is incomplete") + def test_render_literal_numeric_asfloat(self): + pass class BinaryTest(_BinaryTest): @@ -318,6 +350,15 @@ def test_truediv_float(self, connection, left, right, expected): ) +if OLD_SA: + from sqlalchemy.testing.suite.test_select import WindowFunctionTest as _WindowFunctionTest + + class WindowFunctionTest(_WindowFunctionTest): + @pytest.mark.skip("YDB window frame offsets require literal rather than bound values") + def test_window_rows_between(self, connection): + pass + + class ExistsTest(_ExistsTest): """ YDB says: Filtering is not allowed without FROM so rewrite queries @@ -510,6 +551,8 @@ def test_nolength_string(self): class ContainerTypesTest(fixtures.TablesTest): + __backend__ = True + @classmethod def define_tables(cls, metadata): Table( @@ -600,6 +643,8 @@ def test_tuple_list_type_bind_variable_text(self, connection): class ConcatTest(fixtures.TablesTest): + __backend__ = True + @classmethod def define_tables(cls, metadata): Table( @@ -645,14 +690,14 @@ class LongNameBlowoutTest(_LongNameBlowoutTest): class RowFetchTest(_RowFetchTest): - @pytest.mark.skip("scalar subquery unsupported") - def test_row_w_scalar_select(self, connection): - pass + pass class DecimalTest(fixtures.TablesTest): """Tests for YDB Decimal type using standard sa.DECIMAL""" + __backend__ = True + @classmethod def define_tables(cls, metadata): Table( diff --git a/ydb_sqlalchemy/sqlalchemy/requirements.py b/ydb_sqlalchemy/sqlalchemy/requirements.py index 4b0cf70..5ab8287 100644 --- a/ydb_sqlalchemy/sqlalchemy/requirements.py +++ b/ydb_sqlalchemy/sqlalchemy/requirements.py @@ -3,6 +3,10 @@ class Requirements(SuiteRequirements): + @property + def table_ddl_if_exists(self): + return exclusions.open() + @property def json_type(self): return exclusions.open() @@ -51,8 +55,22 @@ def index_reflection(self): @property def view_reflection(self): + # Basic view reflection is covered separately; SQLAlchemy's flag also + # enables unsupported multi-reflection semantics for views. return exclusions.closed() + @property + def boolean_col_expressions(self): + return exclusions.open() + + @property + def tuple_in(self): + return exclusions.open() + + @property + def window_functions(self): + return exclusions.open() + @property def unique_constraint_reflection(self): return exclusions.closed()