Skip to content

Commit db84c6d

Browse files
authored
feat: make constraint testing more flexible (#13)
1 parent 0989e63 commit db84c6d

2 files changed

Lines changed: 155 additions & 121 deletions

File tree

adbc_drivers_validation/model.py

Lines changed: 19 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -81,88 +81,34 @@ class DriverFeatures:
8181
connection_set_current_catalog: bool = False
8282
connection_set_current_schema: bool = False
8383
connection_transactions: bool = False
84+
get_objects_constraints_check: bool = False
85+
get_objects_constraints_foreign: bool = False
86+
get_objects_constraints_primary: bool = False
87+
get_objects_constraints_unique: bool = False
8488
statement_bulk_ingest: bool = False
8589
statement_bulk_ingest_catalog: bool = False
8690
statement_bulk_ingest_schema: bool = False
8791
statement_bulk_ingest_temporary: bool = False
8892
statement_execute_schema: bool = False
8993
statement_get_parameter_schema: bool = False
9094
statement_prepare: bool = True
91-
_current_catalog: str | FromEnv | None = None
92-
_current_schema: str | FromEnv | None = None
93-
_secondary_schema: str | FromEnv | None = None
94-
_secondary_catalog: str | FromEnv | None = None
95-
_secondary_catalog_schema: str | FromEnv | None = None
95+
current_catalog: str | None = None
96+
current_schema: str | None = None
97+
secondary_schema: str | None = None
98+
secondary_catalog: str | None = None
99+
secondary_catalog_schema: str | None = None
96100
supported_xdbc_fields: list[str] = dataclasses.field(default_factory=list)
101+
# Some vendors sort the columns, so declaring FOREIGN KEY(b, a) REFERENCES
102+
# foo(d, c) still gets returned in the order (a, c), (b, d)
103+
quirk_get_objects_constraints_foreign_normalized: bool = False
104+
quirk_get_objects_constraints_primary_normalized: bool = False
105+
quirk_get_objects_constraints_unique_normalized: bool = False
97106

98-
def __init__(
99-
self,
100-
*,
101-
connection_get_table_schema=False,
102-
connection_set_current_catalog=False,
103-
connection_set_current_schema=False,
104-
connection_transactions=False,
105-
statement_bulk_ingest=False,
106-
statement_bulk_ingest_catalog=False,
107-
statement_bulk_ingest_schema=False,
108-
statement_bulk_ingest_temporary=False,
109-
statement_execute_schema=False,
110-
statement_get_parameter_schema=False,
111-
statement_prepare=True,
112-
current_catalog=None,
113-
current_schema=None,
114-
secondary_schema=None,
115-
secondary_catalog=None,
116-
secondary_catalog_schema=None,
117-
supported_xdbc_fields=None,
118-
):
119-
self.connection_get_table_schema = connection_get_table_schema
120-
self.connection_set_current_catalog = connection_set_current_catalog
121-
self.connection_set_current_schema = connection_set_current_schema
122-
self.connection_transactions = connection_transactions
123-
self.statement_bulk_ingest = statement_bulk_ingest
124-
self.statement_bulk_ingest_catalog = statement_bulk_ingest_catalog
125-
self.statement_bulk_ingest_schema = statement_bulk_ingest_schema
126-
self.statement_bulk_ingest_temporary = statement_bulk_ingest_temporary
127-
self.statement_execute_schema = statement_execute_schema
128-
self.statement_get_parameter_schema = statement_get_parameter_schema
129-
self.statement_prepare = statement_prepare
130-
self._current_catalog = current_catalog
131-
self._current_schema = current_schema
132-
self._secondary_schema = secondary_schema
133-
self._secondary_catalog = secondary_catalog
134-
self._secondary_catalog_schema = secondary_catalog_schema
135-
self.supported_xdbc_fields = supported_xdbc_fields or []
136-
137-
@property
138-
def current_catalog(self) -> str | None:
139-
if isinstance(self._current_catalog, FromEnv):
140-
return self._current_catalog.get_or_raise()
141-
return self._current_catalog
142-
143-
@property
144-
def current_schema(self) -> str | None:
145-
if isinstance(self._current_schema, FromEnv):
146-
return self._current_schema.get_or_raise()
147-
return self._current_schema
148-
149-
@property
150-
def secondary_schema(self) -> str | None:
151-
if isinstance(self._secondary_schema, FromEnv):
152-
return self._secondary_schema.get_or_raise()
153-
return self._secondary_schema
154-
155-
@property
156-
def secondary_catalog(self) -> str | None:
157-
if isinstance(self._secondary_catalog, FromEnv):
158-
return self._secondary_catalog.get_or_raise()
159-
return self._secondary_catalog
160-
161-
@property
162-
def secondary_catalog_schema(self) -> str | None:
163-
if isinstance(self._secondary_catalog_schema, FromEnv):
164-
return self._secondary_catalog_schema.get_or_raise()
165-
return self._secondary_catalog_schema
107+
def __init__(self, **kwargs) -> None:
108+
for key, value in kwargs.items():
109+
if isinstance(value, FromEnv):
110+
value = value.get_or_raise()
111+
setattr(self, key, value)
166112

167113

168114
class DriverQuirks(abc.ABC):

adbc_drivers_validation/tests/connection.py

Lines changed: 136 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,20 @@ def generate_tests(quirks: model.DriverQuirks, metafunc) -> None:
5252
indirect=["driver"],
5353
)
5454
else:
55+
marks = []
56+
if (
57+
enabled := {
58+
"test_get_objects_constraints_check": quirks.features.get_objects_constraints_check,
59+
"test_get_objects_constraints_foreign": quirks.features.get_objects_constraints_foreign,
60+
"test_get_objects_constraints_primary": quirks.features.get_objects_constraints_primary,
61+
"test_get_objects_constraints_unique": quirks.features.get_objects_constraints_unique,
62+
}.get(metafunc.definition.name)
63+
) is not None:
64+
if not enabled:
65+
marks.append(pytest.mark.skip(reason="not implemented"))
5566
metafunc.parametrize(
5667
"driver",
57-
[pytest.param(quirks.name, id=quirks.name)],
68+
[pytest.param(quirks.name, id=quirks.name, marks=marks)],
5869
scope="module",
5970
indirect=["driver"],
6071
)
@@ -602,7 +613,8 @@ def test_get_objects_column_xdbc(
602613
elif field == "xdbc_is_nullable":
603614
assert column[field] == "YES"
604615

605-
def test_get_objects_constraints(
616+
@pytest.fixture(scope="class")
617+
def get_objects_constraints(
606618
self,
607619
driver: model.DriverQuirks,
608620
conn: adbc_driver_manager.dbapi.Connection,
@@ -614,6 +626,7 @@ def test_get_objects_constraints(
614626
"constraint_foreign_multi",
615627
"constraint_primary",
616628
"constraint_primary_multi",
629+
"constraint_primary_multi2",
617630
)
618631
with conn.cursor() as cursor:
619632
for table in table_names:
@@ -622,12 +635,18 @@ def test_get_objects_constraints(
622635
for stmt in driver.sample_ddl_constraints:
623636
cursor.execute(stmt)
624637

638+
def get_constraints(
639+
self,
640+
driver: model.DriverQuirks,
641+
conn: adbc_driver_manager.dbapi.Connection,
642+
table_filter: str,
643+
) -> dict[str, list[dict]]:
625644
objects = (
626645
conn.adbc_get_objects(
627646
depth="columns",
628647
catalog_filter=driver.features.current_catalog,
629648
db_schema_filter=driver.features.current_schema,
630-
table_name_filter="constraint_%",
649+
table_name_filter=table_filter,
631650
)
632651
.read_all()
633652
.to_pylist()
@@ -638,11 +657,17 @@ def test_get_objects_constraints(
638657
for schema in obj["catalog_db_schemas"]
639658
for table in schema["db_schema_tables"]
640659
}
641-
642-
assert set(tables) == set(table_names)
643660
for table, constraints in tables.items():
644661
assert constraints is not None, table
662+
return tables
645663

664+
def test_get_objects_constraints_check(
665+
self,
666+
driver: model.DriverQuirks,
667+
conn: adbc_driver_manager.dbapi.Connection,
668+
get_objects_constraints: None,
669+
) -> None:
670+
tables = self.get_constraints(driver, conn, "constraint_check")
646671
assert len(tables["constraint_check"]) == 2
647672
constraints = list(
648673
sorted(
@@ -667,6 +692,81 @@ def test_get_objects_constraints(
667692
},
668693
)
669694

695+
def test_get_objects_constraints_foreign(
696+
self,
697+
driver: model.DriverQuirks,
698+
conn: adbc_driver_manager.dbapi.Connection,
699+
get_objects_constraints: None,
700+
) -> None:
701+
tables = self.get_constraints(driver, conn, "constraint_foreign%")
702+
703+
assert len(tables["constraint_foreign"]) == 1, repr(tables)
704+
compare.match_fields(
705+
tables["constraint_foreign"][0],
706+
{
707+
"constraint_type": "FOREIGN KEY",
708+
"constraint_column_names": ["b"],
709+
"constraint_column_usage": [
710+
{
711+
"fk_catalog": driver.features.current_catalog,
712+
"fk_db_schema": driver.features.current_schema,
713+
"fk_table": "constraint_primary",
714+
"fk_column_name": "a",
715+
}
716+
],
717+
},
718+
)
719+
720+
# Some databases don't preserve the order of columns in a multi-column
721+
# foreign key
722+
assert len(tables["constraint_foreign_multi"]) == 1, repr(tables)
723+
constraint = tables["constraint_foreign_multi"][0]
724+
compare.match_fields(
725+
constraint,
726+
{"constraint_type": "FOREIGN KEY"},
727+
)
728+
cols = constraint["constraint_column_names"]
729+
if driver.features.quirk_get_objects_constraints_foreign_normalized:
730+
assert cols == ["b", "c"]
731+
assert constraint["constraint_column_usage"] == [
732+
{
733+
"fk_catalog": driver.features.current_catalog,
734+
"fk_db_schema": driver.features.current_schema,
735+
"fk_table": "constraint_primary_multi2",
736+
"fk_column_name": "b",
737+
},
738+
{
739+
"fk_catalog": driver.features.current_catalog,
740+
"fk_db_schema": driver.features.current_schema,
741+
"fk_table": "constraint_primary_multi2",
742+
"fk_column_name": "a",
743+
},
744+
], repr(constraint)
745+
else:
746+
assert cols == ["c", "b"]
747+
assert constraint["constraint_column_usage"] == [
748+
{
749+
"fk_catalog": driver.features.current_catalog,
750+
"fk_db_schema": driver.features.current_schema,
751+
"fk_table": "constraint_primary_multi2",
752+
"fk_column_name": "a",
753+
},
754+
{
755+
"fk_catalog": driver.features.current_catalog,
756+
"fk_db_schema": driver.features.current_schema,
757+
"fk_table": "constraint_primary_multi2",
758+
"fk_column_name": "b",
759+
},
760+
], repr(constraint)
761+
762+
def test_get_objects_constraints_primary(
763+
self,
764+
driver: model.DriverQuirks,
765+
conn: adbc_driver_manager.dbapi.Connection,
766+
get_objects_constraints: None,
767+
) -> None:
768+
tables = self.get_constraints(driver, conn, "constraint_primary%")
769+
670770
assert len(tables["constraint_primary"]) == 1
671771
compare.match_fields(
672772
tables["constraint_primary"][0],
@@ -678,14 +778,37 @@ def test_get_objects_constraints(
678778
)
679779

680780
assert len(tables["constraint_primary_multi"]) == 1
781+
constraint = tables["constraint_primary_multi"][0]
782+
compare.match_fields(
783+
constraint,
784+
{
785+
"constraint_type": "PRIMARY KEY",
786+
"constraint_column_usage": None,
787+
},
788+
)
789+
if driver.features.quirk_get_objects_constraints_primary_normalized:
790+
assert constraint["constraint_column_names"] == ["a", "b"]
791+
else:
792+
assert constraint["constraint_column_names"] == ["b", "a"]
793+
794+
assert len(tables["constraint_primary_multi2"]) == 1
795+
constraint = tables["constraint_primary_multi2"][0]
681796
compare.match_fields(
682-
tables["constraint_primary_multi"][0],
797+
constraint,
683798
{
684799
"constraint_type": "PRIMARY KEY",
685-
"constraint_column_names": ["a", "b"],
686800
"constraint_column_usage": None,
687801
},
688802
)
803+
assert constraint["constraint_column_names"] == ["a", "b"]
804+
805+
def test_get_objects_constraints_unique(
806+
self,
807+
driver: model.DriverQuirks,
808+
conn: adbc_driver_manager.dbapi.Connection,
809+
get_objects_constraints: None,
810+
) -> None:
811+
tables = self.get_constraints(driver, conn, "constraint_unique%")
689812

690813
assert len(tables["constraint_unique"]) == 2
691814
constraints = list(
@@ -702,54 +825,19 @@ def test_get_objects_constraints(
702825
"constraint_column_usage": None,
703826
},
704827
)
828+
829+
# Even if declared as UNIQUE(c, b), some databases return [b, c]
705830
compare.match_fields(
706831
constraints[1],
707832
{
708833
"constraint_type": "UNIQUE",
709-
"constraint_column_names": ["b", "c"],
710834
"constraint_column_usage": None,
711835
},
712836
)
713-
714-
assert len(tables["constraint_foreign"]) == 1
715-
compare.match_fields(
716-
tables["constraint_foreign"][0],
717-
{
718-
"constraint_type": "FOREIGN KEY",
719-
"constraint_column_names": ["b"],
720-
"constraint_column_usage": [
721-
{
722-
"fk_catalog": "master",
723-
"fk_db_schema": "dbo",
724-
"fk_table": "constraint_primary",
725-
"fk_column_name": "a",
726-
}
727-
],
728-
},
729-
)
730-
731-
assert len(tables["constraint_foreign_multi"]) == 1
732-
compare.match_fields(
733-
tables["constraint_foreign_multi"][0],
734-
{
735-
"constraint_type": "FOREIGN KEY",
736-
"constraint_column_names": ["b", "c"],
737-
"constraint_column_usage": [
738-
{
739-
"fk_catalog": "master",
740-
"fk_db_schema": "dbo",
741-
"fk_table": "constraint_primary_multi",
742-
"fk_column_name": "a",
743-
},
744-
{
745-
"fk_catalog": "master",
746-
"fk_db_schema": "dbo",
747-
"fk_table": "constraint_primary_multi",
748-
"fk_column_name": "b",
749-
},
750-
],
751-
},
752-
)
837+
if driver.features.quirk_get_objects_constraints_unique_normalized:
838+
assert constraints[1]["constraint_column_names"] == ["b", "c"]
839+
else:
840+
assert constraints[1]["constraint_column_names"] == ["c", "b"]
753841

754842
def test_get_table_schema(
755843
self,

0 commit comments

Comments
 (0)