Skip to content

Commit 2718e7a

Browse files
authored
Refactoring/654 mark tests for new features (#655)
* Change marking for tests added for new features which are not yet implemented in sqlalchemy-exasol * Upon checking what needed to be done to resolve these, it was found that test_get_multi_pk_constraint fully succeeds * Resolve test_get_view_definition_does_not_exist by raising error when view does not exist * Refactor names in base.py to be clearer * Fix test_get_multi_columns as resolvable due to differing nullable expectation only * Fix test_get_multi_foreign_keys as resolvable with new sorting, as custom constraints cannot be used in Exasol * Fix test as now throws exception if view does not exist * Switch skipif to xfail * Add missing strict=True for xfail * Apply review comments to improve comments for modifications
1 parent c032ded commit 2718e7a

File tree

6 files changed

+122
-49
lines changed

6 files changed

+122
-49
lines changed

doc/changes/unreleased.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,8 @@ strings should be altered to start with `exa+websocket://`.
2020
- `ComponentReflectionTest.test_not_existing_table` is used to indicate that specific `EXADialect` methods (i.e. `get_columns`) check to see if the requested table/view exists and if not, they will now toss a `NoSuchTableError` exception
2121
- #403: Dropped support for Turbodbc
2222
- #404: Dropped support for pyodbc
23+
- #654: Reinstated `sqlalchemy` tests after minor modifications to work for Exasol:
24+
- `ComponentReflectionTest.test_get_multi_columns`
25+
- `ComponentReflectionTest.test_get_multi_foreign_keys`
26+
- `ComponentReflectionTest.test_get_multi_pk_constraint`
27+
- `ComponentReflectionTest.test_get_view_definition_does_not_exist`

sqlalchemy_exasol/base.py

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -934,7 +934,10 @@ def get_table_names(self, connection, schema, **kw):
934934
tables = [self.normalize_name(row[0]) for row in result]
935935
return tables
936936

937-
def has_table(self, connection, table_name, schema=None, **kw):
937+
@reflection.cache
938+
def has_table(self, connection, table_name, schema=None, **kw) -> bool:
939+
self._ensure_has_table_connection(connection)
940+
938941
schema = self._get_schema_for_input(connection, schema)
939942
sql_statement = (
940943
"SELECT OBJECT_NAME FROM SYS.EXA_ALL_OBJECTS "
@@ -956,34 +959,38 @@ def has_table(self, connection, table_name, schema=None, **kw):
956959

957960
@reflection.cache
958961
def get_view_names(self, connection, schema=None, **kw):
959-
schema = self._get_schema_for_input(connection, schema)
962+
schema_name = self._get_schema_for_input(connection, schema)
960963
sql_statement = "SELECT view_name FROM SYS.EXA_ALL_VIEWS WHERE view_schema = "
961-
if schema is None:
964+
if schema_name is None:
962965
sql_statement += "CURRENT_SCHEMA ORDER BY view_name"
963966
result = connection.execute(sql.text(sql_statement))
964967
else:
965968
sql_statement += ":schema ORDER BY view_name"
966969
result = connection.execute(
967-
sql.text(sql_statement), {"schema": self.denormalize_name(schema)}
970+
sql.text(sql_statement), {"schema": self.denormalize_name(schema_name)}
968971
)
969972
return [self.normalize_name(row[0]) for row in result]
970973

971974
@reflection.cache
972975
def get_view_definition(self, connection, view_name, schema=None, **kw):
973-
schema = self._get_schema_for_input(connection, schema)
974-
sql_stmnt = "SELECT view_text FROM sys.exa_all_views WHERE view_name = :view_name AND view_schema = "
975-
if schema is None:
976-
sql_stmnt += "CURRENT_SCHEMA"
976+
schema_name = self._get_schema_for_input(connection, schema)
977+
sql_statement = "SELECT view_text FROM sys.exa_all_views WHERE view_name = :view_name AND view_schema = "
978+
if schema_name is None:
979+
sql_statement += "CURRENT_SCHEMA"
977980
else:
978-
sql_stmnt += ":schema"
981+
sql_statement += ":schema"
979982
result = connection.execute(
980-
sql.text(sql_stmnt),
983+
sql.text(sql_statement),
981984
{
982985
"view_name": self.denormalize_name(view_name),
983-
"schema": self.denormalize_name(schema),
986+
"schema": self.denormalize_name(schema_name),
984987
},
985988
).scalar()
986-
return result if result else None
989+
if result:
990+
return result
991+
raise sqlalchemy.exc.NoSuchTableError(
992+
f"{schema_name}.{view_name}" if schema_name else view_name
993+
)
987994

988995
@staticmethod
989996
def quote_string_value(string_value):

test/integration/exasol/test_certificate.py

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,6 @@
1212
from sqlalchemy.testing import config
1313
from sqlalchemy.testing.fixtures import TestBase
1414

15-
FINGERPRINT_SECURITY_RATIONALE = (
16-
"Only websocket supports giving a fingerprint in the connection"
17-
)
18-
1915

2016
def get_fingerprint(dsn):
2117
import websocket
@@ -60,9 +56,10 @@ def remove_ssl_settings(url):
6056
pass
6157
return url.set(query=query)
6258

63-
@pytest.mark.skipif(
59+
@pytest.mark.xfail(
6460
testing.db.dialect.server_version_info < (7, 1, 0),
6561
reason="DB version(s) before 7.1.0 don't enforce ssl/tls",
62+
strict=True,
6663
)
6764
def test_db_connection_fails_with_default_settings_for_self_signed_certificates(
6865
self,
@@ -79,10 +76,6 @@ def test_db_connection_fails_with_default_settings_for_self_signed_certificates(
7976
expected_substrings = ["self-signed certificate", "self signed certificate"]
8077
assert any([e in actual_message for e in expected_substrings])
8178

82-
@pytest.mark.skipif(
83-
"websocket" not in testing.db.dialect.driver,
84-
reason="Only websocket supports passing on connect_args like this.",
85-
)
8679
def test_db_skip_certification_validation_passes(self):
8780
url = self.remove_ssl_settings(config.db.url)
8881

@@ -99,10 +92,6 @@ def test_db_with_ssl_verify_none_passes(self):
9992
result = self.perform_test_query(engine)
10093
assert result == [(42,)]
10194

102-
@pytest.mark.skipif(
103-
"websocket" not in testing.db.dialect.driver,
104-
reason=FINGERPRINT_SECURITY_RATIONALE,
105-
)
10695
def test_db_with_fingerprint_passes(self):
10796
url = self.remove_ssl_settings(config.db.url)
10897
connect_args = url.translate_connect_args(database="schema")
@@ -116,10 +105,6 @@ def test_db_with_fingerprint_passes(self):
116105
result = self.perform_test_query(engine)
117106
assert result == [(42,)]
118107

119-
@pytest.mark.skipif(
120-
"websocket" not in testing.db.dialect.driver,
121-
reason=FINGERPRINT_SECURITY_RATIONALE,
122-
)
123108
def test_db_with_wrong_fingerprint_fails(self):
124109
url = self.remove_ssl_settings(config.db.url)
125110
connect_args = url.translate_connect_args(database="schema")

test/integration/exasol/test_deadlock.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
from threading import Thread
33

44
import pytest
5-
import sqlalchemy.testing as testing
65
from sqlalchemy import (
76
create_engine,
87
inspect,

test/integration/exasol/test_get_metadata_functions.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -200,12 +200,12 @@ def test_get_view_definition(self, engine_name):
200200
def test_get_view_definition_view_name_none(self, engine_name):
201201
with self.engine_map[engine_name].begin() as c:
202202
dialect = inspect(c).dialect
203-
view_definition = dialect.get_view_definition(
204-
connection=c,
205-
schema=self.schema,
206-
view_name=None,
207-
)
208-
assert view_definition is None
203+
with pytest.raises(NoSuchTableError):
204+
dialect.get_view_definition(
205+
connection=c,
206+
schema=self.schema,
207+
view_name=None,
208+
)
209209

210210
@pytest.mark.parametrize(
211211
"engine_name",

test/integration/sqlalchemy/test_suite.py

Lines changed: 90 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import pytest
66
import sqlalchemy as sa
77
from pyexasol import ExaQueryError
8+
from sqlalchemy import Inspector
89
from sqlalchemy.schema import (
910
DDL,
1011
Index,
@@ -23,6 +24,7 @@
2324
from sqlalchemy.testing.suite import ReturningGuardsTest as _ReturningGuardsTest
2425
from sqlalchemy.testing.suite import RowCountTest as _RowCountTest
2526
from sqlalchemy.testing.suite import RowFetchTest as _RowFetchTest
27+
from sqlalchemy.testing.suite.test_reflection import _multi_combination
2628

2729
"""
2830
Here, all tests are imported from the testing suite of sqlalchemy to ensure that the
@@ -334,25 +336,100 @@ class sqlalchemy.testing.suite.ComponentReflectionTest
334336
if not schema and testing.requires.temp_table_reflection.enabled:
335337
cls.define_temp_tables(metadata)
336338

337-
@pytest.mark.xfail(reason=XfailRationale.MANUAL_INDEX.value)
339+
@staticmethod
340+
def _convert_view_nullable(expected_multi_output):
341+
"""
342+
Convert expected nullable to None
343+
344+
For columns of a view in Exasol, nullable is always NULL,
345+
so the expected result needs to be modified. For more reference, see:
346+
https://docs.exasol.com/saas/sql_references/system_tables/metadata/exa_all_columns.htm
347+
"""
348+
for key, value_list in expected_multi_output.items():
349+
schema, table_or_view = key
350+
if not table_or_view.endswith("_v"):
351+
continue
352+
for column_def in value_list:
353+
# Replace nullable: <ANY> with nullable: None
354+
column_def["nullable"] = None
355+
356+
@pytest.mark.xfail(reason=XfailRationale.MANUAL_INDEX.value, strict=True)
338357
def test_get_indexes(self, connection, use_schema):
339358
super().test_get_indexes()
340359

341-
@pytest.mark.xfail(reason=BREAKING_CHANGES_SQL_ALCHEMY_2x, strict=True)
342-
def test_get_multi_columns(self):
343-
super().test_get_multi_columns()
360+
@_multi_combination
361+
def test_get_multi_columns(self, get_multi_exp, schema, scope, kind, use_filter):
362+
"""
363+
The default implementation of test_get_multi_columns in
364+
class sqlalchemy.testing.suite.ComponentReflectionTest
365+
needs to be overridden here, as Exasol always requires nullable to be NULL
366+
for the columns of views. The code given in this overriding class
367+
method was directly copied. See notes, marked with 'Added', highlighting
368+
the changed place.
369+
"""
370+
insp, kws, exp = get_multi_exp(
371+
schema,
372+
scope,
373+
kind,
374+
use_filter,
375+
Inspector.get_columns,
376+
self.exp_columns,
377+
)
344378

345-
@pytest.mark.xfail(reason=BREAKING_CHANGES_SQL_ALCHEMY_2x, strict=True)
346-
def test_get_multi_foreign_keys(self):
347-
super().test_get_multi_foreign_keys()
379+
# Added to convert nullable for columns in views
380+
self._convert_view_nullable(exp)
348381

349-
@pytest.mark.xfail(reason=BREAKING_CHANGES_SQL_ALCHEMY_2x, strict=True)
350-
def test_get_multi_pk_constraint(self):
351-
super().test_get_multi_pk_constraint()
382+
for kw in kws:
383+
insp.clear_cache()
384+
result = insp.get_multi_columns(**kw)
385+
self._check_table_dict(result, exp, self._required_column_keys)
352386

353-
@pytest.mark.xfail(reason=BREAKING_CHANGES_SQL_ALCHEMY_2x, strict=True)
354-
def test_get_view_definition_does_not_exist(self):
355-
super().test_get_view_definition_does_not_exist()
387+
@_multi_combination
388+
def test_get_multi_foreign_keys(
389+
self, get_multi_exp, schema, scope, kind, use_filter
390+
):
391+
"""
392+
The default implementation of test_get_multi_foreign_keys in
393+
class sqlalchemy.testing.suite.ComponentReflectionTest
394+
needs to be overridden here as Exasol does not support custom constraints.
395+
The code given in this overriding class method was directly copied. See notes,
396+
marked with 'Replaced', highlighting the changed place.
397+
"""
398+
399+
def sort_entries_in_place(result_set):
400+
for key, value_list in result_set.items():
401+
result_set[key] = sorted(value_list, key=lambda x: x["referred_table"])
402+
403+
insp, kws, exp = get_multi_exp(
404+
schema,
405+
scope,
406+
kind,
407+
use_filter,
408+
Inspector.get_foreign_keys,
409+
self.exp_fks,
410+
)
411+
412+
for kw in kws:
413+
insp.clear_cache()
414+
result = insp.get_multi_foreign_keys(**kw)
415+
416+
# Replaced as self._adjust_sort did not work, as some constraints
417+
# cannot be added into an Exasol DB as described in define_reflected_tables
418+
# self._adjust_sort(
419+
# result, exp, lambda d: tuple(d["referred_table"])
420+
# )
421+
sort_entries_in_place(exp)
422+
sort_entries_in_place(result)
423+
424+
self._check_table_dict(
425+
result,
426+
exp,
427+
{
428+
"name",
429+
"constrained_columns",
430+
"referred_schema",
431+
},
432+
)
356433

357434

358435
class HasIndexTest(_HasIndexTest):

0 commit comments

Comments
 (0)