Skip to content

Commit 955dae4

Browse files
Merge remote-tracking branch 'origin/sqlalchemy_ddl_dialect_conditional' into sqlalchemy-2.0-fixes
2 parents fd81680 + 69d9b64 commit 955dae4

File tree

5 files changed

+99
-7
lines changed

5 files changed

+99
-7
lines changed

cardinal_pythonlib/sqlalchemy/schema.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -390,9 +390,11 @@ def execute_ddl(
390390
Previously we would use DDL(sql, bind=engine).execute(), but this has gone
391391
in SQLAlchemy 2.0.
392392
393-
If you want dialect-conditional execution, create the DDL object with e.g.
394-
ddl = DDL(sql).execute_if(dialect=SqlaDialectName.SQLSERVER), and pass that
395-
DDL object to this function.
393+
Note that creating the DDL object with e.g. ddl =
394+
DDL(sql).execute_if(dialect=SqlaDialectName.SQLSERVER), and passing that
395+
DDL object to this function, does NOT make execution condition; it executes
396+
regardless. The execute_if() construct is used for listeners; see
397+
https://docs.sqlalchemy.org/en/20/core/ddl.html#sqlalchemy.schema.ExecutableDDLElement.execute_if
396398
"""
397399
assert bool(sql) ^ (ddl is not None) # one or the other.
398400
if sql:

cardinal_pythonlib/sqlalchemy/sqlserver.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from sqlalchemy.schema import DDL
3434

3535
from cardinal_pythonlib.sqlalchemy.dialect import (
36+
get_dialect_name,
3637
quote_identifier,
3738
SqlaDialectName,
3839
)
@@ -49,7 +50,12 @@ def _exec_ddl_if_sqlserver(engine: Engine, sql: str) -> None:
4950
"""
5051
Execute DDL only if we are running on Microsoft SQL Server.
5152
"""
52-
ddl = DDL(sql).execute_if(dialect=SqlaDialectName.SQLSERVER)
53+
# DO NOT USE DDL(sql).execute_if(dialect=SqlaDialectName.SQLSERVER).
54+
# IT IS NOT EXECUTED CONDITIONALLY VIA THIS METHOD.
55+
dialect_name = get_dialect_name(engine)
56+
if dialect_name != SqlaDialectName.SQLSERVER:
57+
return
58+
ddl = DDL(sql)
5359
execute_ddl(engine, ddl=ddl)
5460

5561

cardinal_pythonlib/sqlalchemy/tests/schema_tests.py

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,19 @@
2828

2929
import logging
3030
import unittest
31+
import sys
3132

3233
from sqlalchemy import event, inspect, select
3334
from sqlalchemy.dialects.mssql.base import MSDialect, DECIMAL as MS_DECIMAL
3435
from sqlalchemy.dialects.mysql.base import MySQLDialect
3536
from sqlalchemy.engine import create_engine
3637
from sqlalchemy.exc import NoSuchTableError, OperationalError
3738
from sqlalchemy.ext import compiler
38-
from sqlalchemy.orm import declarative_base
39+
from sqlalchemy.orm import declarative_base, Session, sessionmaker
3940
from sqlalchemy.schema import (
4041
Column,
4142
CreateTable,
43+
DDL,
4244
DDLElement,
4345
Index,
4446
MetaData,
@@ -59,6 +61,10 @@
5961
Time,
6062
)
6163

64+
from cardinal_pythonlib.sqlalchemy.engine_func import (
65+
get_dialect_name,
66+
SqlaDialectName,
67+
)
6268
from cardinal_pythonlib.sqlalchemy.schema import (
6369
add_index,
6470
column_creation_ddl,
@@ -98,6 +104,9 @@
98104
view_exists,
99105
)
100106
from cardinal_pythonlib.sqlalchemy.session import SQLITE_MEMORY_URL
107+
from cardinal_pythonlib.sqlalchemy.sqlserver import (
108+
if_sqlserver_disable_constraints,
109+
)
101110

102111
Base = declarative_base()
103112
log = logging.getLogger(__name__)
@@ -485,7 +494,7 @@ def test_mssql_transaction_count(self) -> None:
485494

486495

487496
class YetMoreSchemaTests(unittest.TestCase):
488-
def __init__(self, *args, echo: bool = False, **kwargs) -> None:
497+
def __init__(self, *args, echo: bool = True, **kwargs) -> None:
489498
self.echo = echo
490499
super().__init__(*args, **kwargs)
491500

@@ -631,6 +640,75 @@ def test_execute_ddl(self) -> None:
631640
with self.assertRaises(AssertionError):
632641
execute_ddl(self.engine) # neither
633642

643+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
644+
# Dialect conditionality for DDL
645+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
646+
@staticmethod
647+
def _present_in_log_output(_cm, msg: str) -> bool:
648+
"""
649+
Detects whether a string is present, INCLUDING AS A SUBSTRING, in
650+
log output captured from an assertLogs() context manager.
651+
"""
652+
return any(msg in line for line in _cm.output)
653+
654+
def test_ddl_dialect_conditionality_1(self) -> None:
655+
self.engine.echo = True # will write to log at INFO level
656+
657+
# 1. Check that logging capture works, and our _present_in_log_output
658+
# function.
659+
with self.assertLogs(level=logging.INFO) as cm:
660+
log.info("dummy call")
661+
self.assertTrue(self._present_in_log_output(cm, "dummy"))
662+
663+
# 2. Check our dialect is as expected: SQLite.
664+
dialect_name = get_dialect_name(self.engine)
665+
self.assertEqual(dialect_name, SqlaDialectName.SQLITE)
666+
667+
# 3. Seeing if DDL built with execute_if() will execute "directly" when
668+
# set to execute-if-SQLite. It executes - but not conditionally!
669+
ddl_yes = DDL("CREATE TABLE yesplease (a INT)").execute_if(
670+
dialect=SqlaDialectName.SQLITE
671+
)
672+
with self.assertLogs(level=logging.INFO) as cm:
673+
execute_ddl(self.engine, ddl=ddl_yes)
674+
self.assertTrue(
675+
self._present_in_log_output(cm, "CREATE TABLE yesplease")
676+
)
677+
678+
# 4. Seeing if DDL built with execute_if() will execute "directly" when
679+
# set to execute-if-MySQL. It executes - therefore not conditionally!
680+
# I'd misunderstood this: it is NOT conditionally executed.
681+
ddl_no = DDL("CREATE TABLE nothanks (a INT)").execute_if(
682+
dialect=SqlaDialectName.MYSQL
683+
)
684+
with self.assertLogs(level=logging.INFO) as cm:
685+
execute_ddl(self.engine, ddl=ddl_no)
686+
self.assertTrue(
687+
self._present_in_log_output(cm, "CREATE TABLE nothanks")
688+
)
689+
# I'd thought this would be false, but it is true.
690+
691+
def test_ddl_dialect_conditionality_2(self) -> None:
692+
# Therefore:
693+
self.engine.echo = True # will write to log at INFO level
694+
# The test above (test_ddl_dialect_conditionality_1) proves that
695+
# this code will log something if SQL is emitted.
696+
697+
session = sessionmaker(
698+
bind=self.engine, future=True
699+
)() # type: Session
700+
701+
if sys.version_info < (3, 10):
702+
log.warning(
703+
"Unable to use unittest.TestCase.assertNoLogs; "
704+
"needs Python 3.10; skipping test"
705+
)
706+
return
707+
with self.assertNoLogs(level=logging.INFO):
708+
with if_sqlserver_disable_constraints(session, tablename="person"):
709+
pass
710+
# Should do nothing, therefore emit no logs.
711+
634712

635713
class SchemaAbstractTests(unittest.TestCase):
636714
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

cardinal_pythonlib/version_string.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,5 @@
3131
3232
"""
3333

34-
VERSION_STRING = "2.0.0"
34+
VERSION_STRING = "2.0.1"
3535
# Use semantic versioning: https://semver.org/

docs/source/changelog.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -846,3 +846,9 @@ Quick links:
846846
was apply filters (if required) and execute.
847847

848848
- Multiple internal changes to support SQLAlchemy 2.
849+
850+
**2.0.1 (2025-01-22)**
851+
852+
- Bugfix to ``cardinal_pythonlib.sqlalchemy.sqlserver`` functions as they
853+
were executing unconditionally, regardless of SQLAlchemy dialect (they should
854+
have been conditional to SQL Server).

0 commit comments

Comments
 (0)