Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 33 additions & 45 deletions tests/integration/test_inspect.py
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -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)

Expand All @@ -66,15 +42,15 @@ 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)
assert inspect.has_table("test", schema=schema)
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.
Expand All @@ -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}`"))
73 changes: 59 additions & 14 deletions tests/integration/test_suite.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -510,6 +551,8 @@ def test_nolength_string(self):


class ContainerTypesTest(fixtures.TablesTest):
__backend__ = True

@classmethod
def define_tables(cls, metadata):
Table(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down
18 changes: 18 additions & 0 deletions ydb_sqlalchemy/sqlalchemy/requirements.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down
Loading