Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions docs/db2.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,25 @@ 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 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
`;` 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:

Expand Down
4 changes: 3 additions & 1 deletion pkg/database/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 27 additions & 6 deletions pkg/database/db2/dsn.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand All @@ -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
}

Expand All @@ -44,17 +45,25 @@ 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 {
return "", fmt.Errorf("invalid hostname: %w", err)
}
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, "/")
Expand All @@ -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)
Expand Down
17 changes: 16 additions & 1 deletion pkg/database/db2/dsn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading