From 0b3729789b54804e0b2fe4c14eff8a4b942477f8 Mon Sep 17 00:00:00 2001 From: Balram Choudhary Date: Thu, 17 Sep 2026 13:10:19 +0530 Subject: [PATCH] Fix version-agnostic FOR UPDATE and quote reserved word 'role' Signed-off-by: Balram Choudhary --- ibm_db_sa/base.py | 53 ++++++++++++++++++++++++++++++++++++++++------- setup.py | 4 ++-- 2 files changed, 47 insertions(+), 10 deletions(-) diff --git a/ibm_db_sa/base.py b/ibm_db_sa/base.py index a67eeab..b0169ce 100644 --- a/ibm_db_sa/base.py +++ b/ibm_db_sa/base.py @@ -171,7 +171,7 @@ 'unnest', 'element', 'percentile_disc', 'upper', 'exec', 'power', 'var_pop', 'exp', 'real', 'var_samp', 'false', 'recursive', 'varchar', 'filter', 'ref', 'varying', 'float', 'regr_avgx', 'width_bucket', 'floor', 'regr_avgy', 'window', 'fusion', - 'regr_count', 'within', 'asc']) + 'regr_count', 'within', 'asc', 'role']) class _IBM_Boolean(sa_types.Boolean): @@ -499,14 +499,34 @@ def visit_now_func(self, fn, **kw): @log_entry_exit def for_update_clause(self, select, **kw): - for_update = select.for_update - logger.debug(f"Processing FOR UPDATE clause -> value={for_update}") - if for_update is True: - clause = " WITH RS USE AND KEEP UPDATE LOCKS" - elif for_update == "read": - clause = " WITH RS USE AND KEEP SHARE LOCKS" + # Version-agnostic FOR UPDATE handling (SQLAlchemy 0.7.3 -> 2.0.x). + # + # SQLAlchemy >= 1.0 stores the lock request on ``select._for_update_arg`` + # (a ForUpdateArg object, or None when no locking was requested) and the + # legacy ``select.for_update`` attribute was removed. + # + # SQLAlchemy < 1.0 exposes the legacy ``select.for_update`` attribute + # whose value is False / True / "read" / "nowait" / "read_nowait" / etc. + if hasattr(select, "_for_update_arg"): + # Modern API (SQLAlchemy 1.0+ including 2.0.x) + for_update_arg = select._for_update_arg + logger.debug(f"Processing FOR UPDATE clause (modern) -> value={for_update_arg}") + if for_update_arg is None: + clause = "" + elif getattr(for_update_arg, "read", False): + clause = " WITH RS USE AND KEEP SHARE LOCKS" + else: + clause = " WITH RS USE AND KEEP UPDATE LOCKS" else: - clause = "" + # Legacy API (SQLAlchemy < 1.0) + for_update = getattr(select, "for_update", None) + logger.debug(f"Processing FOR UPDATE clause (legacy) -> value={for_update}") + if for_update in ("read", "read_nowait"): + clause = " WITH RS USE AND KEEP SHARE LOCKS" + elif for_update: + clause = " WITH RS USE AND KEEP UPDATE LOCKS" + else: + clause = "" logger.debug(f"Generated FOR UPDATE clause -> {clause}") return clause @@ -1197,6 +1217,23 @@ def __init__(self, dialect): f"illegal_initial_characters={self.illegal_initial_characters}" ) + def quote_identifier(self, value): + """Override to uppercase normalized identifiers before quoting. + Db2 folds unquoted identifiers to uppercase, so when we need to quote + a name that was stored as lowercase (SQLAlchemy's normalized form), + we must uppercase it to match the catalog (e.g., "ROLE" not "role"). + + Only case-insensitive names are uppercased. If the name is a SQLAlchemy + ``quoted_name`` that was explicitly marked to preserve quoting + (``quote=True``) — i.e. a deliberately case-sensitive identifier — it is + left exactly as-is so lowercase-quoted objects still resolve correctly. + Mixed-case names are likewise preserved. + """ + force_quote = getattr(value, "quote", None) + if force_quote is not True and value == value.lower(): + value = value.upper() + return self.initial_quote + value + self.final_quote + class _SelectLastRowIDMixin(object): _select_lastrowid = False diff --git a/setup.py b/setup.py index 7126bcb..ce772e2 100644 --- a/setup.py +++ b/setup.py @@ -10,12 +10,12 @@ v.close() readme = os.path.join(os.path.dirname(__file__), 'README.md') -sqlalchemy_requirement = "sqlalchemy>=1.3.5" +sqlalchemy_requirement = "sqlalchemy>=1.3.5,<2.1" if 'USE_PYODBC' in os.environ and os.environ['USE_PYODBC'] == '1': require = [sqlalchemy_requirement] else: require = [sqlalchemy_requirement,'ibm_db>=2.0.0'] - + setup( name='ibm_db_sa',