From aa4881b878d341fabc186697c9296e2912b3e251 Mon Sep 17 00:00:00 2001 From: Charles Kramer Date: Wed, 29 Jul 2026 16:13:27 -0500 Subject: [PATCH 1/3] db2: add db2i:// scheme for DB2 for i (IBM i / AS-400) The existing db2:// path hardcodes the DB2 LUW port (50000) and always appends PROTOCOL=TCPIP, so DB2 for i (IBM i / AS-400) can only be reached via the native-DSN passthrough. Add a db2i:// scheme that defaults the DRDA port to 446 and make PROTOCOL overridable via a ?PROTOCOL= query param. Same engine/driver as db2://; dialect layer is unchanged. Routed to the DB2 engine in the dispatcher. Co-Authored-By: Claude Opus 4.8 (1M context) --- pkg/database/database.go | 4 +++- pkg/database/db2/dsn.go | 33 +++++++++++++++++++++++++++------ pkg/database/db2/dsn_test.go | 17 ++++++++++++++++- 3 files changed, 46 insertions(+), 8 deletions(-) diff --git a/pkg/database/database.go b/pkg/database/database.go index 005164ef..af3161c4 100644 --- a/pkg/database/database.go +++ b/pkg/database/database.go @@ -456,7 +456,9 @@ func Connect(ctx context.Context, opts ConnectOptions) (*sql.DB, DbEngine, error } return db, Vertica, nil - case "db2": + case "db2", "db2i": + // "db2" = DB2 LUW; "db2i" = DB2 for i (IBM i / AS-400). Same engine/driver; + // they differ only in the default DRDA port (see db2.convertToDB2DSN). db, err := db2.Connect(ctx, parsedDsn.String()) if err != nil { return nil, Unknown, err diff --git a/pkg/database/db2/dsn.go b/pkg/database/db2/dsn.go index 87b7d3ed..5e862878 100644 --- a/pkg/database/db2/dsn.go +++ b/pkg/database/db2/dsn.go @@ -9,11 +9,12 @@ import ( // Keywords derived from the URL itself; query parameters may not override them. // Anyone needing full control over these can pass a native DB2 DSN instead. +// PROTOCOL is intentionally NOT reserved: it defaults to TCPIP but may be +// overridden via a ?PROTOCOL= query parameter (needed by some DB2-for-i paths). var reservedDSNKeywords = map[string]bool{ "HOSTNAME": true, "DATABASE": true, "PORT": true, - "PROTOCOL": true, "UID": true, "PWD": true, } @@ -35,7 +36,7 @@ func quoteDB2Value(v string) (string, error) { func convertToDB2DSN(dsn string) (string, error) { // If it's already in DB2 format (contains HOSTNAME= or DATABASE=), return as-is. // URL-format DSNs are exempt from this check so those markers may appear in credentials. - if !strings.HasPrefix(dsn, "db2://") && (strings.Contains(dsn, "HOSTNAME=") || strings.Contains(dsn, "DATABASE=")) { + if !strings.HasPrefix(dsn, "db2://") && !strings.HasPrefix(dsn, "db2i://") && (strings.Contains(dsn, "HOSTNAME=") || strings.Contains(dsn, "DATABASE=")) { return dsn, nil } @@ -44,9 +45,12 @@ func convertToDB2DSN(dsn string) (string, error) { return "", fmt.Errorf("invalid DSN format: %w", err) } - if parsedURL.Scheme != "db2" { - return "", fmt.Errorf("expected db2:// scheme, got %s", parsedURL.Scheme) + // "db2" targets DB2 LUW; "db2i" targets DB2 for i (IBM i / AS-400), which + // differs only in its default DRDA port. All other handling is identical. + if parsedURL.Scheme != "db2" && parsedURL.Scheme != "db2i" { + return "", fmt.Errorf("expected db2:// or db2i:// scheme, got %s", parsedURL.Scheme) } + isIBMi := parsedURL.Scheme == "db2i" hostname, err := quoteDB2Value(parsedURL.Hostname()) if err != nil { @@ -54,7 +58,12 @@ func convertToDB2DSN(dsn string) (string, error) { } port := parsedURL.Port() if port == "" { - port = "50000" // Default DB2 port + // DB2 LUW defaults to 50000; DB2 for i (DRDA) defaults to 446. + if isIBMi { + port = "446" + } else { + port = "50000" + } } database := strings.TrimPrefix(parsedURL.Path, "/") @@ -78,7 +87,19 @@ func convertToDB2DSN(dsn string) (string, error) { dsnParts = append(dsnParts, fmt.Sprintf("HOSTNAME=%s", hostname)) dsnParts = append(dsnParts, fmt.Sprintf("DATABASE=%s", quotedDatabase)) dsnParts = append(dsnParts, fmt.Sprintf("PORT=%s", port)) - dsnParts = append(dsnParts, "PROTOCOL=TCPIP") + // PROTOCOL defaults to TCPIP but is overridable via a ?PROTOCOL= query param + // (appended, uppercased, by the query-parameter loop below). Only set the + // default here when the caller did not supply one. + hasProtocol := false + for k := range parsedURL.Query() { + if strings.EqualFold(k, "PROTOCOL") { + hasProtocol = true + break + } + } + if !hasProtocol { + dsnParts = append(dsnParts, "PROTOCOL=TCPIP") + } if username != "" { quoted, err := quoteDB2Value(username) diff --git a/pkg/database/db2/dsn_test.go b/pkg/database/db2/dsn_test.go index 9ab7956a..e58a7ed2 100644 --- a/pkg/database/db2/dsn_test.go +++ b/pkg/database/db2/dsn_test.go @@ -28,10 +28,25 @@ func TestConvertToDB2DSN(t *testing.T) { dsn: "HOSTNAME=dbhost;PORT=50000;DATABASE=testdb;UID=user;PWD=pass", want: "HOSTNAME=dbhost;PORT=50000;DATABASE=testdb;UID=user;PWD=pass", }, + { + name: "db2i scheme without port uses DRDA default 446", + dsn: "db2i://user:pass@ibmi/MYRDB", + want: "HOSTNAME=ibmi;DATABASE=MYRDB;PORT=446;PROTOCOL=TCPIP;UID=user;PWD=pass", + }, + { + name: "db2i scheme with explicit port", + dsn: "db2i://user:pass@ibmi:8471/MYRDB", + want: "HOSTNAME=ibmi;DATABASE=MYRDB;PORT=8471;PROTOCOL=TCPIP;UID=user;PWD=pass", + }, + { + name: "protocol overridable via query param (no duplicate default)", + dsn: "db2://user:pass@dbhost:50000/testdb?PROTOCOL=IPC", + want: "HOSTNAME=dbhost;DATABASE=testdb;PORT=50000;UID=user;PWD=pass;PROTOCOL=IPC", + }, { name: "wrong scheme", dsn: "postgres://dbhost/testdb", - wantErr: "expected db2:// scheme", + wantErr: "expected db2:// or db2i:// scheme", }, { name: "missing database name", From 06483cfcf8fafe0862ee87996f82213cc385c917 Mon Sep 17 00:00:00 2001 From: Charles Kramer Date: Thu, 30 Jul 2026 00:24:08 -0500 Subject: [PATCH 2/3] docs(db2): document db2i:// scheme and overridable PROTOCOL --- docs/db2.md | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/docs/db2.md b/docs/db2.md index 6922a829..841c807e 100644 --- a/docs/db2.md +++ b/docs/db2.md @@ -104,11 +104,24 @@ db2://username:password@hostname:port/database db2://db2inst1:pass123@localhost:50000/TESTDB ``` +For **DB2 for i** (IBM i / AS-400), use the `db2i://` scheme. It behaves identically to +`db2://` and uses the same engine and driver — the only difference is the default port, +**446** (DRDA) instead of `50000`: + +``` +db2i://username:password@hostname/rdb-name +db2i://svc:pass123@ibmi.corp/MYRDB +``` + +For IBM i the database segment is the RDB directory entry name (see `WRKRDBDIRE`), and a +direct connection over DRDA requires a DB2 Connect license (see the licensing note above). + Query parameters are forwarded as additional DB2 connection keywords (e.g. `db2://user:pass@host:50000/DB?Security=SSL` adds `SECURITY=SSL`). Values containing `;` are brace-quoted automatically; parameters that would override the URL-derived keywords -(`HOSTNAME`, `DATABASE`, `PORT`, `PROTOCOL`, `UID`, `PWD`) are rejected — use the native -form below for full control. +(`HOSTNAME`, `DATABASE`, `PORT`, `UID`, `PWD`) are rejected — use the native +form below for full control. `PROTOCOL` is the exception: it defaults to `TCPIP` but may be +overridden with a `?PROTOCOL=` query parameter (needed by some DB2 for i connection modes). DB2's native form is also accepted as-is: From a2d3ffabdbefb9913d676186040c13e4c105fa2a Mon Sep 17 00:00:00 2001 From: Charles Kramer Date: Thu, 30 Jul 2026 00:34:51 -0500 Subject: [PATCH 3/3] docs(db2): fix db2i licensing cross-reference direction (below, not above) --- docs/db2.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/db2.md b/docs/db2.md index 841c807e..ecba86ec 100644 --- a/docs/db2.md +++ b/docs/db2.md @@ -114,7 +114,8 @@ db2i://svc:pass123@ibmi.corp/MYRDB ``` For IBM i the database segment is the RDB directory entry name (see `WRKRDBDIRE`), and a -direct connection over DRDA requires a DB2 Connect license (see the licensing note above). +direct connection over DRDA requires a customer-supplied DB2 Connect license +(`db2consv_*.lic`) — see the Redistribution licensing section below. Query parameters are forwarded as additional DB2 connection keywords (e.g. `db2://user:pass@host:50000/DB?Security=SSL` adds `SECURITY=SSL`). Values containing