From 2a01db72f48e964c615521999e77ee0c04296d6b Mon Sep 17 00:00:00 2001 From: Korinne Adler Date: Wed, 22 Apr 2026 13:35:11 -0500 Subject: [PATCH 1/5] Convert to PEP 621 Convert from (now deprecated) [tool.poetry] settings to PEP 621 standardized [project] settings. --- pyproject.toml | 89 ++++++++++++++++++++++++++++---------------------- 1 file changed, 50 insertions(+), 39 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index fc2bb08..fdfa37f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,23 +1,66 @@ -[tool.poetry] +[project] name = "sqlalchemy-ibmi" version = "0.9.4-dev" description = "SQLAlchemy support for Db2 on IBM i" readme = "README.md" -authors = [ - "Naveen Ram ", - "Kevin Adler " -] license = "Apache-2.0" keywords = ["sqlalchemy", "database", "ibm", "ibmi", "db2"] +dynamic = ["classifiers"] +authors = [ + {name = "Korinne Adler", email = "kadler@us.ibm.com"}, + {name = "Naveen Ram", email = "naveen.ram@ibm.com"} +] +requires-python = ">=3.7,<3.13" +dependencies = [ + "sqlalchemy (>=1.4.0,<3.0)", + "pyodbc (>=4.0)", +] + +[dependency-groups] +test = [ + "pytest >=7.2.1; python_version>='3.11'", + "pytest >=6.2.5; python_version~='3.10'", + "pytest <5.4; python_version<'3.10'", +] +lint = [ + "flake8 ~=3.7.9", + "black ~=22.8.0", +] +dev = [ + { include-group = "test" }, + { include-group = "lint" }, + "bumpversion ~=0.5.0", +] +docs = [ + "sphinx >1.0.0", + "sphinx-rtd-theme >0.4.0", +] + +[project.urls] +Homepage = "https://github.com/IBM/sqlalchemy-ibmi" +Repository = "https://github.com/IBM/sqlalchemy-ibmi" +Documentation = "https://sqlalchemy-ibmi.readthedocs.io" + +[project.entry-points."sqlalchemy.dialects"] +ibmi = "sqlalchemy_ibmi.base:IBMiDb2Dialect" +"ibmi.pyodbc" = "sqlalchemy_ibmi.base:IBMiDb2Dialect" + +[build-system] +requires = ["poetry-core (>=2.0.0,<3.0.0)"] +build-backend = "poetry.core.masonry.api" + +[tool.poetry] classifiers =[ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "Operating System :: OS Independent", "Topic :: Database :: Front-Ends", ] + packages = [ {include = "sqlalchemy_ibmi"} ] + include = [ { path = "test/*", format = "sdist" }, { path = "contributing/*", format = "sdist" }, @@ -27,40 +70,8 @@ include = [ { path = "LICENSE", format = "sdist" }, ] +requires-poetry = ">=2.0.0,<3.0.0" + [tool.black] line-length = 88 required-version = 22 -target-version = [ - 'py37', - 'py38', - 'py39', - 'py310', - 'py311', - 'py312', -] - -[tool.poetry.plugins."sqlalchemy.dialects"] -ibmi = "sqlalchemy_ibmi.base:IBMiDb2Dialect" -"ibmi.pyodbc" = "sqlalchemy_ibmi.base:IBMiDb2Dialect" - -[tool.poetry.dependencies] -python = ">=3.7,<3.13" -sqlalchemy = ">=1.4.0, <3.0" -pyodbc = ">=4.0" - -[tool.poetry.dev-dependencies] -pytest = [ - { version = ">=7.2.1", python = ">=3.11" }, - { version = ">=6.2.5", python = ">=3.10, <3.11" }, - { version = "<5.4", python = "<3.10" }, -] -flake8 = "^3.7.9" -bumpversion = "^0.5.0" -sphinx = ">1.0.0" -sphinx-rtd-theme = ">0.4.0" -black = "^22.8.0" - -[build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" - From 29bd4eb85379d4fbf7757e670a18969c7ac0b607 Mon Sep 17 00:00:00 2001 From: Korinne Adler Date: Wed, 8 Jul 2026 13:09:54 -0500 Subject: [PATCH 2/5] Drop Python < 3.10 Python 3.9 and below are EOL upstream and no longer available in GitHub actions. --- .github/workflows/lint.yml | 2 +- .github/workflows/pythonpackage.yml | 6 +++--- .github/workflows/release.yml | 4 ++-- README.md | 2 +- pyproject.toml | 8 ++++---- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 05a9376..d7751e7 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -16,7 +16,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v1 with: - python-version: '3.9' + python-version: '3.10' - name: Install dependencies run: | pip install poetry diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 2636b2c..3608b04 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -14,11 +14,11 @@ jobs: fail-fast: false matrix: python-version: + - '3.14' + - '3.13' + - '3.12' - '3.11' - '3.10' - - '3.9' - - '3.8' - - '3.7' steps: - uses: actions/checkout@v1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e47f54f..da20376 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,11 +16,11 @@ jobs: - name: Set up Python uses: actions/setup-python@v2 with: - python-version: "3.9" + python-version: "3.10" - name: Install Poetry run: | - curl -sSL https://install.python-poetry.org | python - -y --version 1.6.1 + curl -sSL https://install.python-poetry.org | python - -y --version 2.4 - name: Update PATH run: echo "$HOME/.local/bin" >> $GITHUB_PATH diff --git a/README.md b/README.md index 7291876..ae9d8fe 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ interface to Db2 for [IBM i](https://en.wikipedia.org/wiki/IBM_i). ### Python -Python 3.7 - 3.12+ are supported. +Python 3.10+ is supported. ### IBM i Access ODBC Driver diff --git a/pyproject.toml b/pyproject.toml index fdfa37f..9fa0695 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,20 +10,20 @@ authors = [ {name = "Korinne Adler", email = "kadler@us.ibm.com"}, {name = "Naveen Ram", email = "naveen.ram@ibm.com"} ] -requires-python = ">=3.7,<3.13" +requires-python = ">=3.10" dependencies = [ "sqlalchemy (>=1.4.0,<3.0)", - "pyodbc (>=4.0)", + "pyodbc (>=5.0); python_version>='3.13'", + "pyodbc (>=4.0); python_version<'3.13'", ] [dependency-groups] test = [ "pytest >=7.2.1; python_version>='3.11'", "pytest >=6.2.5; python_version~='3.10'", - "pytest <5.4; python_version<'3.10'", ] lint = [ - "flake8 ~=3.7.9", + "flake8", "black ~=22.8.0", ] dev = [ From 82f5643f7180720eacf45339f1ea6900c5f9d122 Mon Sep 17 00:00:00 2001 From: Korinne Adler Date: Wed, 8 Jul 2026 13:23:24 -0500 Subject: [PATCH 3/5] Fix flake and black issues - Reformat code with black - Remove SQLAlchemy 1.3 code path for render_literal_execute since we don't support 1.3 anymore --- sqlalchemy_ibmi/base.py | 84 +++++++++++++++++++++-------------------- 1 file changed, 44 insertions(+), 40 deletions(-) diff --git a/sqlalchemy_ibmi/base.py b/sqlalchemy_ibmi/base.py index 2feb3bf..f4156d1 100644 --- a/sqlalchemy_ibmi/base.py +++ b/sqlalchemy_ibmi/base.py @@ -560,13 +560,13 @@ def visit_empty_set_expr(self, element_types, **kw): def visit_over(self, over, **kw): """Override window function handling to avoid CAST in frame clause. - + IBM i DB2 doesn't support CAST expressions in ROWS BETWEEN clauses. We need to render literal values directly instead of using bind parameters. """ # Render with literal binds to avoid CAST(? AS BIGINT) in frame clause kw = kw.copy() - kw['literal_binds'] = True + kw["literal_binds"] = True return super().visit_over(over, **kw) def visit_null(self, expr, **kw): @@ -644,11 +644,7 @@ def visit_bindparam( if not type_.precision: # Render this value as a literal in post-process use_cast = False - try: - bindparam = bindparam.render_literal_execute() - except AttributeError: - # SQLAlchemy 1.3 doesn't have render_literal_execute - literal_binds = True + bindparam = bindparam.render_literal_execute() elif isinstance(type_, sa_types.String): if not type_.length: type_ = type_.copy() @@ -741,7 +737,10 @@ def visit_create_index( self, create, include_schema=True, include_table_schema=True, **kw ): sql = super().visit_create_index( - create, include_schema=include_schema, include_table_schema=include_table_schema, **kw + create, + include_schema=include_schema, + include_table_schema=include_table_schema, + **kw, ) if getattr(create.element, "uConstraint_as_index", None): sql += " EXCLUDE NULL KEYS" @@ -794,7 +793,7 @@ def post_exec(self): row = self.cursor.fetchall()[0] if row[0] is not None: self._lastrowid = int(row[0]) - + # Mark this as a DML statement with no user-facing cursor # This ensures returns_rows is False even though we fetched lastrowid self.cursor_fetch_strategy = _cursor._NO_CURSOR_DML @@ -809,18 +808,18 @@ def fire_sequence(self, seq, type_): def _strtobool(val): """Convert a string representation of truth to boolean. - + This replaces distutils.util.strtobool which was removed in Python 3.12. True values are y, yes, t, true, on and 1. False values are n, no, f, false, off and 0. Raises ValueError if val is anything else. - + This implementation follows PEP 632 guidance for replacing distutils functions. """ val = str(val).lower() - if val in {'y', 'yes', 't', 'true', 'on', '1'}: + if val in {"y", "yes", "t", "true", "on", "1"}: return True - elif val in {'n', 'no', 'f', 'false', 'off', '0'}: + elif val in {"n", "no", "f", "false", "off", "0"}: return False else: raise ValueError(f"Invalid truth value: {val}") @@ -884,26 +883,28 @@ def initialize(self, connection): def get_check_constraints(self, connection, table_name, schema=None, **kw): current_schema = self.denormalize_name(schema or self.default_schema_name) table_name = self.denormalize_name(table_name) - + # Check if table exists if not self.has_table(connection, table_name, schema): raise exc.NoSuchTableError( f"Table '{table_name}' not found in schema '{current_schema}'" ) - + sysconst = self.sys_table_constraints syschkconst = self.sys_check_constraints - query = select( - syschkconst.c.conname, syschkconst.c.chkclause - ).where( - and_( - syschkconst.c.conschema == sysconst.c.conschema, - syschkconst.c.conname == sysconst.c.conname, - sysconst.c.tabschema == current_schema, - sysconst.c.tabname == table_name, + query = ( + select(syschkconst.c.conname, syschkconst.c.chkclause) + .where( + and_( + syschkconst.c.conschema == sysconst.c.conschema, + syschkconst.c.conname == sysconst.c.conname, + sysconst.c.tabschema == current_schema, + sysconst.c.tabname == table_name, + ) ) - ).order_by(syschkconst.c.conname) + .order_by(syschkconst.c.conname) + ) check_consts = [] for res in connection.execute(query): @@ -954,7 +955,7 @@ def get_isolation_level(self, dbapi_conn): def set_isolation_level(self, connection, level): """Set the isolation level for this connection. - + This method attempts to set the isolation level using ODBC attributes. Due to IBM i ODBC driver limitations, this may fail with error HY011 if called during a transaction. @@ -978,7 +979,7 @@ def reset_isolation_level(self, connection): @classmethod def import_dbapi(cls): return __import__("pyodbc") - + # Backwards compatibility alias @classmethod def dbapi(cls): @@ -1000,7 +1001,7 @@ def dbapi(cls): "use_system_naming": ("NAM", to_bool, False), "trim_char_fields": ("TRIMCHAR", to_bool, None), "lob_threshold_kb": ("MAXFIELDLEN", int, None), - "ssl": ("SSL", to_bool, False) + "ssl": ("SSL", to_bool, False), } DRIVER_KEYWORDS_SPECIAL = { @@ -1097,7 +1098,9 @@ def _get_server_version_info(self, connection, allow_chars=True): return tuple(version[0:2]) def _get_default_schema_name(self, connection): - return self.normalize_name(connection.execute(text("VALUES CURRENT_SCHEMA")).scalar()) + return self.normalize_name( + connection.execute(text("VALUES CURRENT_SCHEMA")).scalar() + ) # Driver version for IBM i Access ODBC Driver is given as # VV.RR.SSSF where VV (major), RR (release), and SSS (service pack) @@ -1341,13 +1344,13 @@ def get_view_definition(self, connection, viewname, schema=None, **kw): def get_columns(self, connection, table_name, schema=None, **kw): current_schema = self.denormalize_name(schema or self.default_schema_name) table_name = self.denormalize_name(table_name) - + # Check if table exists if not self.has_table(connection, table_name, schema): raise exc.NoSuchTableError( f"Table '{table_name}' not found in schema '{current_schema}'" ) - + syscols = self.sys_columns query = ( @@ -1363,7 +1366,8 @@ def get_columns(self, connection, table_name, schema=None, **kw): ) .where( and_( - syscols.c.tabschema == current_schema, syscols.c.tabname == table_name + syscols.c.tabschema == current_schema, + syscols.c.tabname == table_name, ) ) .order_by(syscols.c.colno) @@ -1401,13 +1405,13 @@ def get_columns(self, connection, table_name, schema=None, **kw): def get_pk_constraint(self, connection, table_name, schema=None, **kw): current_schema = self.denormalize_name(schema or self.default_schema_name) table_name = self.denormalize_name(table_name) - + # Check if table exists if not self.has_table(connection, table_name, schema): raise exc.NoSuchTableError( f"Table '{table_name}' not found in schema '{current_schema}'" ) - + sysconst = self.sys_table_constraints syskeyconst = self.sys_key_constraints @@ -1462,13 +1466,13 @@ def get_foreign_keys(self, connection, table_name, schema=None, **kw): current_schema = self.denormalize_name(schema or default_schema) default_schema = self.normalize_name(default_schema) table_name = self.denormalize_name(table_name) - + # Check if table exists if not self.has_table(connection, table_name, schema): raise exc.NoSuchTableError( f"Table '{table_name}' not found in schema '{current_schema}'" ) - + sysfkeys = self.sys_foreignkeys query = ( select( @@ -1518,13 +1522,13 @@ def get_foreign_keys(self, connection, table_name, schema=None, **kw): def get_indexes(self, connection, table_name, schema=None, **kw): current_schema = self.denormalize_name(schema or self.default_schema_name) table_name = self.denormalize_name(table_name) - + # Check if table exists if not self.has_table(connection, table_name, schema): raise exc.NoSuchTableError( f"Table '{table_name}' not found in schema '{current_schema}'" ) - + sysidx = self.sys_indexes syskey = self.sys_keys @@ -1549,7 +1553,7 @@ def get_indexes(self, connection, table_name, schema=None, **kw): indexes[key] = { "name": self.normalize_name(row[0]), "column_names": [self.normalize_name(row[2])], - "unique": row[1] in ('U', 'P'), # U=unique, P=primary key + "unique": row[1] in ("U", "P"), # U=unique, P=primary key } return [value for key, value in indexes.items()] @@ -1557,13 +1561,13 @@ def get_indexes(self, connection, table_name, schema=None, **kw): def get_unique_constraints(self, connection, table_name, schema=None, **kw): current_schema = self.denormalize_name(schema or self.default_schema_name) table_name = self.denormalize_name(table_name) - + # Check if table exists if not self.has_table(connection, table_name, schema): raise exc.NoSuchTableError( f"Table '{table_name}' not found in schema '{current_schema}'" ) - + sysconst = self.sys_table_constraints sysconstcol = self.sys_constraints_columns From 29053643f2e72c5a431054d7763c8a172cf7704a Mon Sep 17 00:00:00 2001 From: Korinne Adler Date: Wed, 8 Jul 2026 13:25:08 -0500 Subject: [PATCH 4/5] Remove SQLAlchemy 1.3 tests We no longer support SQLAlchemy 1.3 so remove test code paths. --- test/test_cache.py | 11 +---------- test/test_suite.py | 34 +++++++++------------------------- 2 files changed, 10 insertions(+), 35 deletions(-) diff --git a/test/test_cache.py b/test/test_cache.py index b9a295c..d2a6de0 100644 --- a/test/test_cache.py +++ b/test/test_cache.py @@ -2,19 +2,10 @@ from datetime import date, time, datetime from sqlalchemy import literal +from sqlalchemy import select from sqlalchemy import testing from sqlalchemy.testing import fixtures -from .util import SA_Version - -if SA_Version >= [1, 4]: - from sqlalchemy import select -else: - from sqlalchemy import select as _select - - def select(*args): - return _select(args) - class CachingTest(fixtures.TestBase): @testing.combinations( diff --git a/test/test_suite.py b/test/test_suite.py index 14eb724..37d0812 100644 --- a/test/test_suite.py +++ b/test/test_suite.py @@ -1,4 +1,3 @@ -from .util import SA_Version from sqlalchemy.testing.suite import * # noqa - need * to import test suite from sqlalchemy.testing.suite import testing @@ -10,30 +9,15 @@ from sqlalchemy.testing.suite import UnicodeTextTest as _UnicodeTextTest from sqlalchemy.testing.suite import UnicodeVarcharTest as _UnicodeVarcharTest -if SA_Version < [1, 4]: - - class ComponentReflectionTest(_ComponentReflectionTest): - @testing.requires.unique_constraint_reflection - @testing.requires.schemas - @testing.skip("ibmi", "Db2 doesn't support duplicate constraints") - def test_get_unique_constraints_with_schema(self): - pass - - @testing.requires.unique_constraint_reflection - @testing.skip("ibmi", "Db2 doesn't support duplicate constraints") - def test_get_unique_constraints(self): - pass - -else: - - class ComponentReflectionTest(_ComponentReflectionTest): - @testing.combinations( - (True, testing.requires.schemas), (False,), argnames="use_schema" - ) - @testing.requires.unique_constraint_reflection - @testing.skip("ibmi", "Db2 doesn't support duplicate constraints") - def test_get_unique_constraints(self, metadata, connection, use_schema): - pass + +class ComponentReflectionTest(_ComponentReflectionTest): + @testing.combinations( + (True, testing.requires.schemas), (False,), argnames="use_schema" + ) + @testing.requires.unique_constraint_reflection + @testing.skip("ibmi", "Db2 doesn't support duplicate constraints") + def test_get_unique_constraints(self, metadata, connection, use_schema): + pass # empty set tests not possible on DB2 for i From cb1a3fc92fa756ca62514aeb7b2e368fed63cf9b Mon Sep 17 00:00:00 2001 From: Korinne Adler Date: Wed, 1 Jul 2026 13:47:55 -0500 Subject: [PATCH 5/5] Update GitHub Actions - Update to actions/checkout to v7 - Update to actions/setup-python to v6 - Update softprops/action-gh-release to v3 --- .github/workflows/lint.yml | 4 ++-- .github/workflows/pythonpackage.yml | 4 ++-- .github/workflows/release.yml | 10 ++++------ 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index d7751e7..7060aca 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -12,9 +12,9 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v7 - name: Set up Python - uses: actions/setup-python@v1 + uses: actions/setup-python@v6 with: python-version: '3.10' - name: Install dependencies diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 3608b04..43bc1b2 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -21,9 +21,9 @@ jobs: - '3.10' steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v7 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Add ODBC repo diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index da20376..c86048e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,25 +11,23 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v7 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v6 with: python-version: "3.10" - name: Install Poetry run: | curl -sSL https://install.python-poetry.org | python - -y --version 2.4 - - - name: Update PATH - run: echo "$HOME/.local/bin" >> $GITHUB_PATH + echo "$HOME/.local/bin" >> $GITHUB_PATH - name: Build project for distribution run: poetry build - name: Create Release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v3 with: files: "dist/*" generate_release_notes: true