Skip to content

Commit b555b76

Browse files
feat: Add IBM Db2 engine adapter with CI/CD integration
Adds a complete IBM Db2 LUW engine adapter for SQLMesh, including connection configuration, Docker-based CI, integration tests, and engine-specific documentation. ## What is included ### Core adapter (sqlmesh/core/engine_adapter/db2.py) - Db2EngineAdapter implementing all standard SQLMesh engine operations - SYSCAT-based metadata queries (columns, tables, schemas, indexes, views) - CTAS with mandatory WITH DATA clause - MERGE INTO with TARGET/SOURCE alias substitution - CREATE INDEX with SYSCAT.INDEXES existence check - CREATE/DROP SCHEMA using SYSCAT.SCHEMATA - DROP VIEW using SYSCAT.VIEWS existence check - GRANT/REVOKE using SYSCAT.TABAUTH (not INFORMATION_SCHEMA) - Truncate implemented as DELETE FROM (Db2 has no TRUNCATE) - TIMESTAMPTZ stripped to TIMESTAMP (SQL0180N fix) - normalize_identifiers before quoting (SQL0204N fix) - catalog_support = SINGLE_CATALOG_ONLY (Db2 databases are isolated) - SUPPORTED_DROP_CASCADE_OBJECT_KINDS = [] (SQL0104N fix) - MAX_IDENTIFIER_LENGTH = 128 - set_current_catalog via CONNECT TO ### Connection config (sqlmesh/core/config/connection.py) - Db2ConnectionConfig with host/port/database/username/password/db2_schema - SSL options (ssl, ssl_cert, ssl_key, ssl_ca) - get_catalog() returns .upper() to match Db2 UPPERCASE normalisation - Excluded from FORBIDDEN_STATE_SYNC_ENGINES with explanation comment (Db2 rejects table names starting with underscore) ### Framework integration - sqlmesh/core/engine_adapter/__init__.py: conditional import guarded by Python >= 3.10 AND find_spec('db2_sqlglot') — prevents import crash in environments without the db2 extra installed - sqlmesh/utils/migration.py: db2 added to MAX_TEXT_INDEX_LENGTH (255) and blob_text_type() returns VARCHAR(32000) ## CI / infrastructure - .github/workflows/pr.yaml: db2 added to engine-tests-docker matrix - .github/scripts/install-prerequisites.sh: db2 installs libxml2-dev - .github/scripts/wait-for-db.sh: db2_ready() readiness probe - tests/.../docker/compose.db2.yaml: IBM Db2 Community Edition image - Makefile: db2-test target with junitxml; db2 added to install-dev - pyproject.toml: db2 optional extra + pytest marker ## Tests - tests/core/engine_adapter/test_db2.py: 22 unit tests (all passing) - tests/core/engine_adapter/integration/test_integration_db2.py: 18 Db2-specific integration tests - tests/core/engine_adapter/integration/__init__.py: SYSCAT comment queries, role-based grant infrastructure for Db2 - tests/core/engine_adapter/integration/config.yaml: inttest_db2 gateway with DuckDB state_connection - tests/core/engine_adapter/integration/test_integration.py: skip blocks with documented reasons for 20 tests; adaptations for uppercase identifiers, TIMESTAMPTZ, grants ## CI results (stable baseline on Docker Db2 Community Edition) - 81 passed · 46 skipped · 0 failed ## Known limitations and skipped tests ### SCD Type 2 (4 tests skipped) Db2 SQL preprocessor treats identifiers starting with '_' as conditional compilation directives (SQL20521N reason 7). SQLMesh generates _exists, _key0, _row_number, _t as aliases. Additionally SCD staging uses CTAS which Db2 does not support natively. Fix: override _scd_type_2() in Db2EngineAdapter to rename aliases. ### View comments (3 tests skipped) Db2 has no COMMENT ON VIEW statement (SQL0104N). ### test_sushi (1 test skipped) CREATE SCHEMA IF NOT EXISTS is not valid Db2 SQL (SQL0104N). Fix: add create_sql() override to db2-sqlglot-dialect to strip IF NOT EXISTS from CREATE SCHEMA. ### ctx.create_context() tests (6 tests skipped) shared.py:346 uses a case-sensitive == comparison between catalog_name (TESTDB, Db2 uppercase) and _default_catalog (testdb, duckdb-dialect lowercase). This is a one-line upstream fix in shared.py that cannot be made in this PR: catalog_name.upper() != (engine_adapter._default_catalog or '').upper() ## Documentation - docs/integrations/engines/db2.md: connection options, state connection guidance, limitations, example config - docs/integrations/overview.md: Db2 entry added - docs/guides/connections.md: Db2 link added - mkdocs.yml: db2.md nav entry added Signed-off-by: IBM Db2 Eco System <Hdm-dev-persona-db2-eco-system@ibm.com>
1 parent b1e36b9 commit b555b76

20 files changed

Lines changed: 2243 additions & 16 deletions

File tree

.github/scripts/install-prerequisites.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ ENGINE_DEPENDENCIES=""
1717

1818
if [ "$ENGINE" == "spark" ]; then
1919
ENGINE_DEPENDENCIES="default-jdk"
20+
elif [ "$ENGINE" == "db2" ]; then
21+
ENGINE_DEPENDENCIES="libxml2-dev build-essential"
2022
elif [ "$ENGINE" == "fabric" ]; then
2123
echo "Installing Microsoft package repository"
2224

.github/scripts/wait-for-db.sh

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,20 @@ risingwave_ready() {
9090
probe_port 4566
9191
}
9292

93+
db2_ready() {
94+
probe_port 50001
95+
96+
echo "Waiting for Db2 to finish initialising (this can take 2-4 minutes)..."
97+
while true; do
98+
if docker exec db2 su - db2inst1 -c "db2 connect to TESTDB" > /dev/null 2>&1; then
99+
echo "Db2 is accepting connections"
100+
break
101+
fi
102+
echo "Db2 not yet ready; sleeping 15s..."
103+
sleep 15
104+
done
105+
}
106+
93107
echo "Waiting for $ENGINE to be ready..."
94108

95109
READINESS_FUNC="${ENGINE}_ready"

.github/workflows/pr.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ jobs:
252252
fail-fast: false
253253
matrix:
254254
engine:
255-
[duckdb, postgres, mysql, mssql, trino, spark, clickhouse, risingwave, starrocks]
255+
[duckdb, postgres, mysql, mssql, trino, spark, clickhouse, risingwave, starrocks, db2]
256256
env:
257257
PYTEST_XDIST_AUTO_NUM_WORKERS: 2
258258
SQLMESH__DISABLE_ANONYMIZED_ANALYTICS: '1'

Makefile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ else
1414
endif
1515

1616
install-dev:
17-
$(PIP) install -e ".[dev,web,slack,dlt,lsp]" ./examples/custom_materializations
17+
$(PIP) install -e ".[dev,web,slack,dlt,lsp,db2]" ./examples/custom_materializations
1818

1919
install-doc:
2020
$(PIP) install -r ./docs/requirements.txt
@@ -222,6 +222,9 @@ risingwave-test: engine-risingwave-up
222222

223223
starrocks-test: engine-starrocks-up
224224
pytest -n auto -m "starrocks" --reruns 3 --junitxml=test-results/junit-starrocks.xml
225+
226+
db2-test: engine-db2-up
227+
pytest -n auto -m "db2" --reruns 3 --junitxml=test-results/junit-db2.xml
225228

226229
#################
227230
# Cloud Engines #

docs/guides/connections.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ default_gateway: local_db
8484
* [BigQuery](../integrations/engines/bigquery.md)
8585
* [ClickHouse](../integrations/engines/clickhouse.md)
8686
* [Databricks](../integrations/engines/databricks.md)
87+
* [Db2](../integrations/engines/db2.md)
8788
* [DuckDB](../integrations/engines/duckdb.md)
8889
* [Fabric](../integrations/engines/fabric.md)
8990
* [MotherDuck](../integrations/engines/motherduck.md)

docs/integrations/engines/db2.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Db2
2+
3+
This page provides information about how to use SQLMesh with [IBM Db2](https://www.ibm.com/products/db2).
4+
5+
!!! info
6+
The Db2 engine adapter is a community contribution. Due to this, only limited community support is available.
7+
8+
## Local/Built-in Scheduler
9+
10+
**Engine Adapter Type**: `db2`
11+
12+
### Installation
13+
14+
```
15+
pip install "sqlmesh[db2]"
16+
```
17+
18+
### Connection options
19+
20+
| Option | Description | Type | Required |
21+
|---------------------|------------------------------------------------------------------------------------------------|:------:|:--------:|
22+
| `type` | Engine type name - must be `db2` | string | Y |
23+
| `host` | The hostname of the Db2 server | string | Y |
24+
| `port` | The port number of the Db2 server. Default: `50000` | int | N |
25+
| `database` | The name of the Db2 database to connect to | string | Y |
26+
| `username` | The username to use for authentication with the Db2 server | string | Y |
27+
| `password` | The password to use for authentication with the Db2 server | string | Y |
28+
| `db2_schema` | Sets `CURRENTSCHEMA` on the connection. Controls the default schema for unqualified references. Typically set to the same value as `username`. | string | Y |
29+
| `ssl` | Enable TLS/SSL encryption. Default: `false` | bool | N |
30+
| `connect_timeout` | The number of seconds to wait for the connection to the server. Default: `30` | int | N |
31+
| `concurrent_tasks` | Maximum number of tasks to run concurrently. Default: `4` | int | N |
32+
33+
## Important Notes
34+
35+
**State connection:** Db2 is **not supported** as a SQLMesh `state_connection`. Use DuckDB (recommended) or another supported engine for SQLMesh state storage:
36+
37+
```yaml linenums="1"
38+
gateways:
39+
db2:
40+
connection:
41+
type: db2
42+
host: localhost
43+
port: 50000
44+
database: TESTDB
45+
username: db2inst1
46+
password: your_password
47+
db2_schema: db2inst1
48+
state_connection:
49+
type: duckdb
50+
database: ./state/sqlmesh_state.db
51+
52+
default_gateway: db2
53+
54+
model_defaults:
55+
dialect: db2
56+
```
57+
58+
**Table naming:** Db2 rejects table names that start with an underscore (`_`). SQLMesh's default physical table naming convention can generate names beginning with `_`. To avoid this, set `physical_table_naming_convention` to `hash_md5` in your project config:
59+
60+
```yaml
61+
physical_table_naming_convention: hash_md5
62+
```
63+
64+
## Limitations
65+
66+
- **Single catalog only**: Db2 operates in single-catalog mode; cross-catalog queries are not supported.
67+
- **No inline column comments**: Column-level comments cannot be set inline during table creation.
68+
- **No atomic table replacement**: Db2 does not support `CREATE OR REPLACE TABLE`, so full model refreshes are not atomic. There is a brief window during which the table may be empty or partially populated.
69+
- **Identifier length**: Maximum identifier length is 128 characters.
70+
- **No `SELECT ... FOR UPDATE`**: Db2 does not support `SELECT ... FOR UPDATE` in the same way as OLTP databases; SQLMesh removes this clause when executing queries.
71+
72+
## Resources
73+
74+
- [IBM Db2 Documentation](https://www.ibm.com/docs/en/db2)
75+
- [IBM Db2 SQL Reference](https://www.ibm.com/docs/en/db2/11.5?topic=db2-sql)

docs/integrations/overview.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ SQLMesh supports the following execution engines for running SQLMesh projects (e
1616
* [BigQuery](./engines/bigquery.md) (bigquery)
1717
* [ClickHouse](./engines/clickhouse.md) (clickhouse)
1818
* [Databricks](./engines/databricks.md) (databricks)
19+
* [Db2](./engines/db2.md) (db2)
1920
* [DuckDB](./engines/duckdb.md) (duckdb)
2021
* [Fabric](./engines/fabric.md) (fabric)
2122
* [MotherDuck](./engines/motherduck.md) (motherduck)

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ nav:
8282
- integrations/engines/bigquery.md
8383
- integrations/engines/clickhouse.md
8484
- integrations/engines/databricks.md
85+
- integrations/engines/db2.md
8586
- integrations/engines/duckdb.md
8687
- integrations/engines/fabric.md
8788
- integrations/engines/motherduck.md

pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,10 @@ dev = [
112112
]
113113
dbt = ["dbt-core<2"]
114114
dlt = ["dlt"]
115+
db2 = [
116+
"ibm_db",
117+
"db2-sqlglot-dialect;python_version>=\"3.10\""
118+
]
115119
duckdb = []
116120
fabric = ["pyodbc>=5.0.0"]
117121
fabric-mssql-python = ["mssql-python>=1.1.0;python_version>=\"3.10\""]
@@ -270,6 +274,7 @@ markers = [
270274
"clickhouse: test for Clickhouse (standalone mode / cluster mode)",
271275
"clickhouse_cloud: test for Clickhouse (cloud mode)",
272276
"databricks: test for Databricks",
277+
"db2: test for Db2",
273278
"duckdb: test for DuckDB",
274279
"fabric: test for Fabric",
275280
"motherduck: test for MotherDuck",

sqlmesh/core/config/connection.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@
5353
"mssql",
5454
"azuresql",
5555
}
56+
# Note: Db2 is excluded because it doesn't allow table names starting with underscore (_)
57+
# which SQLMesh uses for state tables (_versions, _snapshots, _environments, _intervals).
58+
# Use a separate state_connection (e.g., DuckDB) for Db2 gateways.
5659
FORBIDDEN_STATE_SYNC_ENGINES = {
5760
# Do not support row-level operations
5861
"spark",
@@ -2602,6 +2605,91 @@ def _connection_factory(self) -> t.Callable:
26022605
BaseDuckDBConnectionConfig, # type: ignore[type-abstract]
26032606
}
26042607

2608+
2609+
class Db2ConnectionConfig(ConnectionConfig):
2610+
host: str
2611+
port: int = 50000
2612+
database: str
2613+
db2_schema: str
2614+
username: str
2615+
password: str
2616+
ssl: bool = False
2617+
ssl_cert: t.Optional[str] = None
2618+
ssl_key: t.Optional[str] = None
2619+
ssl_ca: t.Optional[str] = None
2620+
connect_timeout: int = 30
2621+
2622+
concurrent_tasks: int = 4
2623+
register_comments: bool = True
2624+
pre_ping: bool = True
2625+
2626+
type_: t.Literal["db2"] = Field(alias="type", default="db2")
2627+
DIALECT: t.ClassVar[t.Literal["db2"]] = "db2"
2628+
DISPLAY_NAME: t.ClassVar[t.Literal["Db2"]] = "Db2"
2629+
DISPLAY_ORDER: t.ClassVar[t.Literal[19]] = 19
2630+
2631+
_engine_import_validator = _get_engine_import_validator("ibm_db", "db2")
2632+
2633+
@property
2634+
def _connection_kwargs_keys(self) -> t.Set[str]:
2635+
return {
2636+
"host",
2637+
"port",
2638+
"database",
2639+
"db2_schema",
2640+
"username",
2641+
"password",
2642+
}
2643+
2644+
@property
2645+
def _engine_adapter(self) -> t.Type[EngineAdapter]:
2646+
# DB2 adapter requires Python 3.10+ for db2-sqlglot-dialect
2647+
# Use getattr to avoid mypy errors on Python 3.9
2648+
return t.cast(
2649+
t.Type[EngineAdapter], getattr(engine_adapter, "Db2EngineAdapter", EngineAdapter)
2650+
)
2651+
2652+
def get_catalog(self) -> t.Optional[str]:
2653+
"""Db2 stores catalog names in uppercase; normalise here so the default_catalog
2654+
passed to the adapter matches what get_current_catalog() returns at runtime."""
2655+
catalog = super().get_catalog()
2656+
return catalog.upper() if catalog else None
2657+
2658+
@property
2659+
def _connection_factory(self) -> t.Callable:
2660+
import ibm_db_dbi # type: ignore
2661+
2662+
ssl = self.ssl
2663+
ssl_cert = self.ssl_cert
2664+
ssl_key = self.ssl_key
2665+
ssl_ca = self.ssl_ca
2666+
connect_timeout = self.connect_timeout
2667+
2668+
def connect_db2(**kwargs: t.Any) -> t.Any:
2669+
conn_str_parts = [
2670+
f"DATABASE={kwargs['database']}",
2671+
f"HOSTNAME={kwargs['host']}",
2672+
f"PORT={kwargs['port']}",
2673+
"PROTOCOL=TCPIP",
2674+
f"UID={kwargs['username']}",
2675+
f"PWD={kwargs['password']}",
2676+
f"CURRENTSCHEMA={kwargs['db2_schema']}",
2677+
f"CONNECTTIMEOUT={connect_timeout}",
2678+
]
2679+
if ssl:
2680+
conn_str_parts.append("SECURITY=SSL")
2681+
if ssl_cert:
2682+
conn_str_parts.append(f"SSLClientCertificate={ssl_cert}")
2683+
if ssl_key:
2684+
conn_str_parts.append(f"SSLClientKey={ssl_key}")
2685+
if ssl_ca:
2686+
conn_str_parts.append(f"SSLServerCertificate={ssl_ca}")
2687+
conn_str = ";".join(conn_str_parts) + ";"
2688+
return ibm_db_dbi.connect(conn_str, "", "")
2689+
2690+
return connect_db2
2691+
2692+
26052693
CONNECTION_CONFIG_TO_TYPE = {
26062694
# Map all subclasses of ConnectionConfig to the value of their `type_` field.
26072695
tpe.all_field_infos()["type_"].default: tpe

0 commit comments

Comments
 (0)