From 43e614b876542ba94fd4fc4a9574705cdf6bc01b Mon Sep 17 00:00:00 2001 From: Ali Falahi Date: Tue, 28 Jul 2026 14:28:16 -0600 Subject: [PATCH 1/5] docs: add DB2 for i (AS/400) connection guidance IBM i differs from LUW on every DSN component: port 446 (DDM/DRDA, not the 8471 IBM i Access host server), DATABASE is the RDB name rather than an application library, libraries map to schemas via CurrentSchema, and a customer-supplied Db2 Connect license is a hard prerequisite (SQL1598N without it). --- docs/db2.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/docs/db2.md b/docs/db2.md index 6922a829..bb7b02d1 100644 --- a/docs/db2.md +++ b/docs/db2.md @@ -116,6 +116,38 @@ DB2's native form is also accepted as-is: HOSTNAME=localhost;PORT=50000;DATABASE=TESTDB;UID=db2inst1;PWD=pass123;PROTOCOL=TCPIP ``` +## DB2 for i (AS/400) + +Connecting to DB2 for i works through the same CLI driver but differs from LUW on every +DSN component, and connection details copied from an existing IBM i Access / JDBC (jt400) +setup will **not** work as-is: + +- **License (go/no-go):** the CLI driver requires a **Db2 Connect license** for IBM i + targets. The customer copies their `db2consv_*.lic` into `clidriver/license/`. Without it + every connection fails with `SQL1598N` — if the customer has no Db2 Connect entitlement, + this driver cannot connect at all. +- **Port is 446** (the DDM/DRDA service; confirm it's running with `STRTCPSVR SERVER(*DDM)` + on the i side). Port **8471** belongs to the IBM i Access database host server — a + different protocol our driver does not speak; using it fails with `SQL30081N`. +- **Database = the RDB name**, not an application library. Find it with `WRKRDBDIRE` + (the `*LOCAL` entry) on the system, or `SELECT CURRENT SERVER FROM SYSIBM.SYSDUMMY1` + from any working connection. It is usually the system name. +- **Libraries are schemas.** Set the default with `?CurrentSchema=MYLIB`, or + schema-qualify tables in the configured queries (`MYLIB.USERS`), which is more explicit + and recommended. The multi-library `DBQ` list is an IBM i Access ODBC feature and does + not exist here. + +```text +db2://USER:PASS@my-ibmi.example.com:446/RDBNAME?CurrentSchema=MYLIB +``` + +`USER`, `PASS`, `RDBNAME`, and `MYLIB` are placeholders — in particular `RDBNAME` is the +value `WRKRDBDIRE` reports, not the application library name. + +Columns tagged CCSID 65535 (binary) are common on IBM i application libraries and come +back untranslated; cast them in the configured query (`CAST(col AS CHAR(n) CCSID 37)`) if +values look like garbage. + ## Troubleshooting **`'sqlcli1.h' file not found`** — clidriver missing or `DB2HOME` wrong. Check that From 3067c8aa901d0583dfb68f2df4ff9cc2aaa73957 Mon Sep 17 00:00:00 2001 From: Ali Falahi Date: Tue, 28 Jul 2026 14:49:39 -0600 Subject: [PATCH 2/5] docs: document Db2 Connect license sourcing and SQL1598N remediation --- docs/db2.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/db2.md b/docs/db2.md index bb7b02d1..e7a4a1b0 100644 --- a/docs/db2.md +++ b/docs/db2.md @@ -125,7 +125,12 @@ setup will **not** work as-is: - **License (go/no-go):** the CLI driver requires a **Db2 Connect license** for IBM i targets. The customer copies their `db2consv_*.lic` into `clidriver/license/`. Without it every connection fails with `SQL1598N` — if the customer has no Db2 Connect entitlement, - this driver cannot connect at all. + this driver cannot connect at all. The `.lic` file comes from the customer's IBM Passport + Advantage downloads or from any Db2 Connect server they already run (`sqllib/license/`); + only the file is needed — no Db2 Connect installation on the connector host. + Alternatively, if they run a **Db2 Connect gateway server**, point the DSN at the gateway + instead of the IBM i system — licensing is then handled server-side and no local `.lic` + file is required. - **Port is 446** (the DDM/DRDA service; confirm it's running with `STRTCPSVR SERVER(*DDM)` on the i side). Port **8471** belongs to the IBM i Access database host server — a different protocol our driver does not speak; using it fails with `SQL30081N`. @@ -169,6 +174,12 @@ binaries — the baked paths are the reliable option. **`error while loading shared libraries: libxml2.so.2`** (Linux) — the clidriver depends on the OS libxml2 package: `apt-get install libxml2` / `yum install libxml2`. +**`SQL1598N` (licensing problem on connect)** — the target is z/OS or IBM i and no Db2 +Connect license is present. Diagnostically this means networking, port, and RDB name are +all correct — the DRDA handshake succeeded and only the entitlement check failed. Fix per +the license bullet in the DB2 for i section above: drop the customer's `db2consv_*.lic` +into `clidriver/license/` (no rebuild needed) or route through their Db2 Connect gateway. + **`go vet` / `golangci-lint` with `-tags db2` fails** — type-checking the tagged path needs the clidriver headers too. Default-tag lint and vet need nothing. From 7d254296aef1f2f30dd74ab4b6b9b63c4354b2a2 Mon Sep 17 00:00:00 2001 From: Ali Falahi Date: Tue, 28 Jul 2026 15:00:21 -0600 Subject: [PATCH 3/5] docs: add example configs for DB2 LUW and DB2 for i LUW example syncs authorization IDs and roles from SYSCAT with grant/revoke provisioning; IBM i example syncs user profiles and group profile membership from QSYS2, sync-only since profile changes go through CL commands rather than SQL. Both parse-tested alongside the existing examples. --- examples/db2-ibmi-test.yml | 98 +++++++++++++++++++++++++++++ examples/db2-test.yml | 122 +++++++++++++++++++++++++++++++++++++ pkg/bsql/config_test.go | 32 ++++++++++ 3 files changed, 252 insertions(+) create mode 100644 examples/db2-ibmi-test.yml create mode 100644 examples/db2-test.yml diff --git a/examples/db2-ibmi-test.yml b/examples/db2-ibmi-test.yml new file mode 100644 index 00000000..0c2b7f6d --- /dev/null +++ b/examples/db2-ibmi-test.yml @@ -0,0 +1,98 @@ +--- +# Application name for this connector configuration +app_name: DB2 for i Test + +# Connection settings for DB2 for i (AS/400 / IBM i). +# Requires a binary built with `make build-db2` AND a customer-supplied Db2 Connect +# license in clidriver/license/ — connections fail with SQL1598N without it. +# See the "DB2 for i (AS/400)" section in docs/db2.md. +connect: + # Port 446 is the DDM/DRDA service (NOT 8471, which is the IBM i Access host server). + # DB_RDB_NAME is the relational database name from WRKRDBDIRE (*LOCAL entry) — usually + # the system name, never an application library. CurrentSchema sets the default + # library for unqualified table names; queries below qualify QSYS2 explicitly, so it + # only matters for application-table queries you add. + dsn: "db2://${DB_HOST}:446/${DB_RDB_NAME}?CurrentSchema=${DB_LIBRARY}" + # Username and password are provided separately so they can be properly URL encoded. + user: "${DB_USER}" + password: "${DB_PASSWORD}" + +# This example is sync-only. Provisioning user profiles or group membership on IBM i +# is done with CL commands (CRTUSRPRF/CHGUSRPRF), not SQL — wire those through +# CALL QSYS2.QCMDEXC(...) only with care, since GRPPRF/SUPGRPPRF changes replace the +# current values rather than appending. +resource_types: + # IBM i user profiles + user: + name: "User" + description: "A user profile on the IBM i system" + list: + # QSYS2.USER_INFO is the SQL catalog view over user profiles. + # Group profiles also appear here; GROUP_ID_NUMBER > 0 filters them out of the + # user list (they are synced as groups below). OFFSET/FETCH needs IBM i 7.3+. + query: | + SELECT + AUTHORIZATION_NAME AS "username", + STATUS AS "status", + TEXT_DESCRIPTION AS "description", + USER_CLASS_NAME AS "user_class", + PREVIOUS_SIGNON AS "last_signon" + FROM QSYS2.USER_INFO + WHERE GROUP_ID_NUMBER = 0 + ORDER BY AUTHORIZATION_NAME + OFFSET ? ROWS FETCH NEXT ? ROWS ONLY + pagination: + strategy: "offset" + primary_key: "username" + map: + id: ".username" + display_name: ".username" + description: ".description" + traits: + user: + # *ENABLED / *DISABLED from the profile status + status: '.status == "*ENABLED" ? "enabled" : "disabled"' + login: ".username" + last_login: ".last_signon" + profile: + username: ".username" + user_class: ".user_class" + + # IBM i group profiles (a group is a user profile with a group ID) + group: + name: "Group" + description: "A group profile on the IBM i system" + list: + query: | + SELECT DISTINCT + GROUP_PROFILE_NAME AS "group_name" + FROM QSYS2.GROUP_PROFILE_ENTRIES + ORDER BY GROUP_PROFILE_NAME + map: + id: ".group_name" + display_name: ".group_name" + description: "" + traits: + group: + profile: + group_name: ".group_name" + static_entitlements: + - id: "member" + display_name: "resource.DisplayName + ' Group Member'" + description: "'Member of the ' + resource.DisplayName + ' group profile'" + purpose: "assignment" + grantable_to: + - "user" + grants: + # QSYS2.GROUP_PROFILE_ENTRIES lists every (group profile, member) pair, + # covering both the primary group (GRPPRF) and supplemental groups (SUPGRPPRF). + - query: | + SELECT + GROUP_PROFILE_NAME AS "group_name", + USER_PROFILE_NAME AS "username" + FROM QSYS2.GROUP_PROFILE_ENTRIES + map: + - skip_if: ".group_name != resource.ID" + principal_id: ".username" + principal_type: "user" + entitlement_id: "member" diff --git a/examples/db2-test.yml b/examples/db2-test.yml new file mode 100644 index 00000000..db9f5990 --- /dev/null +++ b/examples/db2-test.yml @@ -0,0 +1,122 @@ +--- +# Application name for this connector configuration +app_name: DB2 LUW Test + +# Connection settings for DB2 LUW (Linux/Unix/Windows). +# Requires a binary built with `make build-db2` — see docs/db2.md. +connect: + # Data Source Name (DSN) — default DB2 LUW port is 50000. + # Extra CLI keywords can be passed as query parameters, e.g. ?CurrentSchema=MYSCHEMA + dsn: "db2://${DB_HOST}:${DB_PORT}/${DB_NAME}" + # Username and password are provided separately so they can be properly URL encoded. + user: "${DB_USER}" + password: "${DB_PASSWORD}" + +# Definition of different resource types managed by this connector +resource_types: + # DB2 LUW does not store users itself (authentication is OS/LDAP); the closest + # identity inventory is the set of authorization IDs that hold database authorities. + user: + name: "User" + description: "An authorization ID with database authorities in DB2" + list: + # SYSCAT.DBAUTH lists database-level authorities; GRANTEETYPE 'U' = user IDs + query: | + SELECT DISTINCT + GRANTEE AS "username" + FROM SYSCAT.DBAUTH + WHERE GRANTEETYPE = 'U' + ORDER BY GRANTEE + OFFSET ? ROWS FETCH NEXT ? ROWS ONLY + pagination: + strategy: "offset" + primary_key: "username" + map: + id: ".username" + display_name: ".username" + description: "" + traits: + user: + login: ".username" + profile: + username: ".username" + + # Roles defined in the database (CREATE ROLE) + role: + name: "Role" + description: "A role within the DB2 database" + list: + query: | + SELECT + ROLENAME AS "role_name" + FROM SYSCAT.ROLES + ORDER BY ROLENAME + map: + id: ".role_name" + display_name: ".role_name" + description: "" + traits: + role: + profile: + role_name: ".role_name" + static_entitlements: + - id: "assigned" + display_name: "resource.DisplayName + ' Role Member'" + description: "'Member of the ' + resource.DisplayName + ' role'" + purpose: "assignment" + grantable_to: + - "user" + provisioning: + vars: + principal_name: principal.ID + role_name: resource.ID + grant: + no_transaction: true + queries: + # GRANT is DDL — parameter binding is not allowed, so identifiers are + # inlined with ?<...|unquoted> (values are sanitized, never escaped). + - | + GRANT ROLE ? TO USER ? + revoke: + no_transaction: true + queries: + - | + REVOKE ROLE ? FROM USER ? + - id: "admin" + display_name: "resource.DisplayName + ' Role Admin'" + description: "'Can administer the ' + resource.DisplayName + ' role'" + purpose: "permission" + grantable_to: + - "user" + provisioning: + vars: + principal_name: principal.ID + role_name: resource.ID + grant: + no_transaction: true + queries: + - | + GRANT ROLE ? TO USER ? WITH ADMIN OPTION + revoke: + no_transaction: true + queries: + - | + REVOKE ADMIN OPTION FOR ROLE ? FROM USER ? + grants: + # SYSCAT.ROLEAUTH holds role memberships; ADMIN = 'Y' marks WITH ADMIN OPTION + - query: | + SELECT + GRANTEE AS "username", + ROLENAME AS "role_name", + ADMIN AS "admin" + FROM SYSCAT.ROLEAUTH + WHERE GRANTEETYPE = 'U' + map: + - skip_if: ".role_name != resource.ID" + principal_id: ".username" + principal_type: "user" + entitlement_id: "assigned" + - skip_if: ".role_name != resource.ID || .admin != 'Y'" + principal_id: ".username" + principal_type: "user" + entitlement_id: "admin" diff --git a/pkg/bsql/config_test.go b/pkg/bsql/config_test.go index e966389b..47940d9d 100644 --- a/pkg/bsql/config_test.go +++ b/pkg/bsql/config_test.go @@ -248,6 +248,38 @@ resource_types: require.Equal(t, "'Grant cancelled by policy.'", grant.RejectIf.Reason) }, }, + { + name: "db2-luw-example", + input: loadExampleConfig(t, "db2-test"), + validate: func(t *testing.T, c *Config) { + require.Equal(t, "DB2 LUW Test", c.AppName) + require.Equal(t, "db2://${DB_HOST}:${DB_PORT}/${DB_NAME}", c.Connect.DSN) + require.Equal(t, "${DB_USER}", c.Connect.User) + require.Len(t, c.ResourceTypes, 2) + + roleResourceType := c.ResourceTypes["role"] + require.Len(t, roleResourceType.StaticEntitlements, 2) + require.Len(t, roleResourceType.Grants, 1) + require.NotNil(t, roleResourceType.StaticEntitlements[0].Provisioning) + }, + }, + { + name: "db2-ibmi-example", + input: loadExampleConfig(t, "db2-ibmi-test"), + validate: func(t *testing.T, c *Config) { + require.Equal(t, "DB2 for i Test", c.AppName) + require.Equal(t, "db2://${DB_HOST}:446/${DB_RDB_NAME}?CurrentSchema=${DB_LIBRARY}", c.Connect.DSN) + require.Len(t, c.ResourceTypes, 2) + + userResourceType := c.ResourceTypes["user"] + require.NotNil(t, userResourceType.List) + require.Equal(t, "offset", userResourceType.List.Pagination.Strategy) + + groupResourceType := c.ResourceTypes["group"] + require.Len(t, groupResourceType.StaticEntitlements, 1) + require.Len(t, groupResourceType.Grants, 1) + }, + }, } for _, tt := range tests { From e60dcd5880ea48e8a68ad26c7922ea50163c569a Mon Sep 17 00:00:00 2001 From: Ali Falahi Date: Tue, 28 Jul 2026 15:11:28 -0600 Subject: [PATCH 4/5] docs: note Db2 Connect license version matching and db2cli validate --- docs/db2.md | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/docs/db2.md b/docs/db2.md index e7a4a1b0..363c5446 100644 --- a/docs/db2.md +++ b/docs/db2.md @@ -174,11 +174,17 @@ binaries — the baked paths are the reliable option. **`error while loading shared libraries: libxml2.so.2`** (Linux) — the clidriver depends on the OS libxml2 package: `apt-get install libxml2` / `yum install libxml2`. -**`SQL1598N` (licensing problem on connect)** — the target is z/OS or IBM i and no Db2 -Connect license is present. Diagnostically this means networking, port, and RDB name are -all correct — the DRDA handshake succeeded and only the entitlement check failed. Fix per -the license bullet in the DB2 for i section above: drop the customer's `db2consv_*.lic` -into `clidriver/license/` (no rebuild needed) or route through their Db2 Connect gateway. +**`SQL1598N` / SQLSTATE `42968` (licensing problem on connect)** — the target is z/OS or +IBM i and no valid Db2 Connect license is present. Diagnostically this means networking, +port, and RDB name are all correct — the DRDA handshake succeeded and only the entitlement +check failed. Fix per the license bullet in the DB2 for i section above: drop the +customer's `db2consv_*.lic` into `clidriver/license/` (no rebuild needed) or route through +their Db2 Connect gateway. License files are **Db2-version-specific**: a v11.5 license +will not activate a v12.x clidriver and produces this same error — check the driver level +with `clidriver/bin/db2level` and match the clidriver version to the customer's +entitlement (IBM's download site keeps per-version directories). `clidriver/bin/db2cli +validate` tests the DSN and license directly against the driver, independent of the +connector. **`go vet` / `golangci-lint` with `-tags db2` fails** — type-checking the tagged path needs the clidriver headers too. Default-tag lint and vet need nothing. From fd05add371b30431184c500708ef9cf5c6fca7f9 Mon Sep 17 00:00:00 2001 From: Ali Falahi Date: Tue, 28 Jul 2026 15:55:44 -0600 Subject: [PATCH 5/5] docs: normalize nullable columns in DB2 for i example query CEL string operations on NULL or timestamp values fail with "no such overload"; COALESCE in the query keeps the mapped values string-typed. --- examples/db2-ibmi-test.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/examples/db2-ibmi-test.yml b/examples/db2-ibmi-test.yml index 0c2b7f6d..220c97c8 100644 --- a/examples/db2-ibmi-test.yml +++ b/examples/db2-ibmi-test.yml @@ -30,13 +30,16 @@ resource_types: # QSYS2.USER_INFO is the SQL catalog view over user profiles. # Group profiles also appear here; GROUP_ID_NUMBER > 0 filters them out of the # user list (they are synced as groups below). OFFSET/FETCH needs IBM i 7.3+. + # Nullable columns are normalized in SQL (COALESCE) so CEL expressions only ever + # see strings — CEL operations on NULL or timestamp values fail with + # "no such overload". query: | SELECT AUTHORIZATION_NAME AS "username", STATUS AS "status", - TEXT_DESCRIPTION AS "description", + COALESCE(TEXT_DESCRIPTION, '') AS "description", USER_CLASS_NAME AS "user_class", - PREVIOUS_SIGNON AS "last_signon" + COALESCE(VARCHAR(PREVIOUS_SIGNON), '') AS "last_signon" FROM QSYS2.USER_INFO WHERE GROUP_ID_NUMBER = 0 ORDER BY AUTHORIZATION_NAME