From c040943054519aa49fae62e9ff8e67737a800bcb Mon Sep 17 00:00:00 2001 From: Artur Rakhmatulin Date: Mon, 6 Jul 2026 11:13:31 +0100 Subject: [PATCH 1/2] chore: surface ADBC driver build version for debugging --- go/adbc/cmd/adbc-driver-info/main.go | 118 +++++++++++ .../driver/flightsql/flightsql_database.go | 22 ++- go/adbc/driver/internal/driverbase/driver.go | 54 ++++- .../driverbase/driver_buildinfo_test.go | 69 +++++++ .../driver/internal/driverbase/driver_test.go | 185 +++++++++--------- 5 files changed, 353 insertions(+), 95 deletions(-) create mode 100644 go/adbc/cmd/adbc-driver-info/main.go create mode 100644 go/adbc/driver/internal/driverbase/driver_buildinfo_test.go diff --git a/go/adbc/cmd/adbc-driver-info/main.go b/go/adbc/cmd/adbc-driver-info/main.go new file mode 100644 index 0000000000..dab046614c --- /dev/null +++ b/go/adbc/cmd/adbc-driver-info/main.go @@ -0,0 +1,118 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package main + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "os" + "strings" + + "github.com/apache/arrow-adbc/go/adbc" + "github.com/apache/arrow-adbc/go/adbc/drivermgr" +) + +type optionsFlag map[string]string + +func (o *optionsFlag) String() string { + if o == nil { + return "" + } + parts := make([]string, 0, len(*o)) + for key, val := range *o { + parts = append(parts, key+"="+val) + } + return strings.Join(parts, ",") +} + +func (o *optionsFlag) Set(value string) error { + key, val, ok := strings.Cut(value, "=") + if !ok || strings.TrimSpace(key) == "" { + return fmt.Errorf("option must be key=value, got %q", value) + } + if *o == nil { + *o = make(map[string]string) + } + (*o)[key] = val + return nil +} + +func main() { + var ( + driverPath string + entrypoint string + options optionsFlag + ) + + flag.StringVar(&driverPath, "driver", "", "Path or driver name to load via ADBC driver manager") + flag.StringVar(&entrypoint, "entrypoint", "", "Optional driver entrypoint symbol") + flag.Var(&options, "option", "Database option in key=value form; may be repeated") + flag.Parse() + + if strings.TrimSpace(driverPath) == "" { + fmt.Fprintln(os.Stderr, "missing required --driver") + flag.Usage() + os.Exit(2) + } + + dbOptions := make(map[string]string, len(options)+2) + for key, val := range options { + dbOptions[key] = val + } + dbOptions["driver"] = driverPath + if strings.TrimSpace(entrypoint) != "" { + dbOptions["entrypoint"] = entrypoint + } + + var drv drivermgr.Driver + db, err := drv.NewDatabase(dbOptions) + if err != nil { + fail("create database", err) + } + defer closeOrWarn("database", db.Close) + + cnxn, err := db.Open(context.Background()) + if err != nil { + fail("open connection", err) + } + defer closeOrWarn("connection", cnxn.Close) + + info, err := adbc.GetDriverInfo(context.Background(), cnxn) + if err != nil { + fail("get driver info", err) + } + + encoder := json.NewEncoder(os.Stdout) + encoder.SetIndent("", " ") + if err := encoder.Encode(info); err != nil { + fail("encode output", err) + } +} + +func closeOrWarn(label string, fn func() error) { + if err := fn(); err != nil { + fmt.Fprintf(os.Stderr, "warning: failed to close %s: %v\n", label, err) + } +} + +func fail(action string, err error) { + fmt.Fprintf(os.Stderr, "%s: %v\n", action, err) + os.Exit(1) +} diff --git a/go/adbc/driver/flightsql/flightsql_database.go b/go/adbc/driver/flightsql/flightsql_database.go index 259e534edb..023027ee6f 100644 --- a/go/adbc/driver/flightsql/flightsql_database.go +++ b/go/adbc/driver/flightsql/flightsql_database.go @@ -72,6 +72,19 @@ type databaseImpl struct { oauthToken credentials.PerRPCCredentials } +func getDriverInfoString(info *driverbase.DriverInfo, code adbc.InfoCode, fallback string) string { + value, ok := info.GetInfoForInfoCode(code) + if !ok { + return fallback + } + + str, ok := value.(string) + if !ok || str == "" { + return fallback + } + return str +} + func (d *databaseImpl) SetOptions(cnOptions map[string]string) error { var tlsConfig tls.Config @@ -408,8 +421,7 @@ func getFlightClient(ctx context.Context, loc string, d *databaseImpl, authMiddl target = "unix:" + uri.Path } - dv, _ := d.DriverInfo.GetInfoForInfoCode(adbc.InfoDriverVersion) - driverVersion := dv.(string) + driverVersion := getDriverInfoString(d.DriverInfo, adbc.InfoDriverVersion, "unknown") dialOpts := append(d.dialOpts.opts, grpc.WithConnectParams(d.timeout.connectParams()), grpc.WithTransportCredentials(creds), grpc.WithUserAgent("ADBC Flight SQL Driver "+driverVersion)) dialOpts = append(dialOpts, d.userDialOpts...) @@ -582,10 +594,16 @@ func (d *databaseImpl) Open(ctx context.Context) (adbc.Connection, error) { conn.id = newRandomID("conn") conn.openedAt = time.Now() conn.Logger = safeLogger(conn.Logger).With("connection_id", conn.id) + driverVersion := getDriverInfoString(d.DriverInfo, adbc.InfoDriverVersion, "unknown") + driverArrowVersion := getDriverInfoString(d.DriverInfo, adbc.InfoDriverArrowVersion, "unknown") + driverADBCVersion := getDriverInfoString(d.DriverInfo, adbc.InfoDriverADBCVersion, "unknown") conn.Logger.InfoContext(ctx, "FlightSQL connection opened", "target", d.uri.String(), "transactionsSupported", cnxnSupport.transactions, "driver", infoDriverName, + "driver_version", driverVersion, + "driver_arrow_version", driverArrowVersion, + "driver_adbc_version", driverADBCVersion, ) return driverbase.NewConnectionBuilder(conn). diff --git a/go/adbc/driver/internal/driverbase/driver.go b/go/adbc/driver/internal/driverbase/driver.go index 019bcbc6ff..8e0bdd57cc 100644 --- a/go/adbc/driver/internal/driverbase/driver.go +++ b/go/adbc/driver/internal/driverbase/driver.go @@ -22,6 +22,7 @@ package driverbase import ( "context" + "fmt" "runtime/debug" "strings" @@ -36,12 +37,10 @@ var ( func init() { if info, ok := debug.ReadBuildInfo(); ok { + infoDriverVersion = buildDriverVersion(info) for _, s := range info.Settings { - switch s.Key { - case "vcs.modified": - if s.Value == "true" { - infoDriverVersion += "-dev" - } + if s.Key == "vcs.modified" && s.Value == "true" && infoDriverVersion == "" { + infoDriverVersion = UnknownVersion } } for _, dep := range info.Deps { @@ -54,6 +53,51 @@ func init() { } } +func buildDriverVersion(info *debug.BuildInfo) string { + if info == nil { + return "" + } + + version := strings.TrimSpace(info.Main.Version) + if version == "" || version == "(devel)" { + version = "" + } + + var revision string + var modified bool + for _, s := range info.Settings { + switch s.Key { + case "vcs.revision": + revision = shortRevision(s.Value) + case "vcs.modified": + modified = s.Value == "true" + } + } + + switch { + case version != "" && revision != "": + version = fmt.Sprintf("%s+%s", version, revision) + case version == "" && revision != "": + version = revision + case version == "": + return "" + } + + if modified { + version += "-dev" + } + + return version +} + +func shortRevision(revision string) string { + revision = strings.TrimSpace(revision) + if len(revision) > 12 { + return revision[:12] + } + return revision +} + // DriverImpl is an interface that drivers implement to provide // vendor-specific functionality. type DriverImpl interface { diff --git a/go/adbc/driver/internal/driverbase/driver_buildinfo_test.go b/go/adbc/driver/internal/driverbase/driver_buildinfo_test.go new file mode 100644 index 0000000000..35c6c7e67b --- /dev/null +++ b/go/adbc/driver/internal/driverbase/driver_buildinfo_test.go @@ -0,0 +1,69 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package driverbase + +import ( + "runtime/debug" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestBuildDriverVersion(t *testing.T) { + t.Run("release version with revision", func(t *testing.T) { + info := &debug.BuildInfo{ + Main: debug.Module{Version: "1.2.3"}, + Settings: []debug.BuildSetting{ + {Key: "vcs.revision", Value: "1234567890abcdef"}, + }, + } + + require.Equal(t, "1.2.3+1234567890ab", buildDriverVersion(info)) + }) + + t.Run("devel revision dirty", func(t *testing.T) { + info := &debug.BuildInfo{ + Main: debug.Module{Version: "(devel)"}, + Settings: []debug.BuildSetting{ + {Key: "vcs.revision", Value: "abcdef1234567890"}, + {Key: "vcs.modified", Value: "true"}, + }, + } + + require.Equal(t, "abcdef123456-dev", buildDriverVersion(info)) + }) + + t.Run("release version dirty without revision", func(t *testing.T) { + info := &debug.BuildInfo{ + Main: debug.Module{Version: "2.0.0"}, + Settings: []debug.BuildSetting{ + {Key: "vcs.modified", Value: "true"}, + }, + } + + require.Equal(t, "2.0.0-dev", buildDriverVersion(info)) + }) + + t.Run("no useful metadata", func(t *testing.T) { + info := &debug.BuildInfo{ + Main: debug.Module{Version: "(devel)"}, + } + + require.Empty(t, buildDriverVersion(info)) + }) +} diff --git a/go/adbc/driver/internal/driverbase/driver_test.go b/go/adbc/driver/internal/driverbase/driver_test.go index ab8efb4c80..95ae931ef9 100644 --- a/go/adbc/driver/internal/driverbase/driver_test.go +++ b/go/adbc/driver/internal/driverbase/driver_test.go @@ -94,44 +94,24 @@ func TestDefaultDriver(t *testing.T) { // This is what the driverbase provided GetInfo result should look like out of the box, // with one custom setting registered at initialization - expectedGetInfoTable, err := array.TableFromJSON(alloc, adbc.GetInfoSchema, []string{`[ - { - "info_name": 0, - "info_value": [0, "MockDriver"] - }, - { - "info_name": 1, - "info_value": [0, "(unknown or development build)"] - }, - { - "info_name": 2, - "info_value": [0, "(unknown or development build)"] - }, - { - "info_name": 100, - "info_value": [0, "ADBC MockDriver Driver - Go"] - }, - { - "info_name": 101, - "info_value": [0, "(unknown or development build)"] - }, - { - "info_name": 102, - "info_value": [0, "(unknown or development build)"] - }, - { - "info_name": 103, - "info_value": [2, 1001000] - }, - { - "info_name": 10001, - "info_value": [0, "my custom info"] - } - ]`}) - require.NoError(t, err) - defer expectedGetInfoTable.Release() - - require.Truef(t, array.TableEqual(expectedGetInfoTable, getInfoTable), "expected: %s\ngot: %s", expectedGetInfoTable, getInfoTable) + infoValues := getInfoValuesFromTable(t, getInfoTable) + require.Equal(t, map[adbc.InfoCode]any{ + adbc.InfoVendorName: "MockDriver", + adbc.InfoVendorVersion: driverbase.UnknownVersion, + adbc.InfoVendorArrowVersion: driverbase.UnknownVersion, + adbc.InfoDriverName: "ADBC MockDriver Driver - Go", + adbc.InfoDriverADBCVersion: int64(adbc.AdbcVersion1_1_0), + adbc.InfoCode(10_001): "my custom info", + }, filterInfoValues(infoValues, + adbc.InfoVendorName, + adbc.InfoVendorVersion, + adbc.InfoVendorArrowVersion, + adbc.InfoDriverName, + adbc.InfoDriverADBCVersion, + adbc.InfoCode(10_001), + )) + require.NotEmpty(t, infoValues[adbc.InfoDriverVersion]) + require.NotEmpty(t, infoValues[adbc.InfoDriverArrowVersion]) _, err = cnxn.GetObjects(ctx, adbc.ObjectDepthAll, nil, nil, nil, nil, nil) require.Error(t, err) @@ -223,56 +203,30 @@ func TestCustomizedDriver(t *testing.T) { // - the default DriverInfo set at initialization // - the DriverInfo set once in the NewDriver constructor // - the DriverInfo set dynamically when GetInfo is called by implementing DriverInfoPreparer interface - expectedGetInfoTable, err := array.TableFromJSON(alloc, adbc.GetInfoSchema, []string{`[ - { - "info_name": 0, - "info_value": [0, "MockDriver"] - }, - { - "info_name": 1, - "info_value": [0, "(unknown or development build)"] - }, - { - "info_name": 2, - "info_value": [0, "(unknown or development build)"] - }, - { - "info_name": 3, - "info_value": [1, true] - }, - { - "info_name": 4, - "info_value": [1, false] - }, - { - "info_name": 100, - "info_value": [0, "ADBC MockDriver Driver - Go"] - }, - { - "info_name": 101, - "info_value": [0, "(unknown or development build)"] - }, - { - "info_name": 102, - "info_value": [0, "(unknown or development build)"] - }, - { - "info_name": 103, - "info_value": [2, 1001000] - }, - { - "info_name": 10001, - "info_value": [0, "my custom info"] - }, - { - "info_name": 10002, - "info_value": [0, "this was fetched dynamically"] - } - ]`}) - require.NoError(t, err) - defer expectedGetInfoTable.Release() - - require.Truef(t, array.TableEqual(expectedGetInfoTable, getInfoTable), "expected: %s\ngot: %s", expectedGetInfoTable, getInfoTable) + infoValues := getInfoValuesFromTable(t, getInfoTable) + require.Equal(t, map[adbc.InfoCode]any{ + adbc.InfoVendorName: "MockDriver", + adbc.InfoVendorVersion: driverbase.UnknownVersion, + adbc.InfoVendorArrowVersion: driverbase.UnknownVersion, + adbc.InfoVendorSql: true, + adbc.InfoVendorSubstrait: false, + adbc.InfoDriverName: "ADBC MockDriver Driver - Go", + adbc.InfoDriverADBCVersion: int64(adbc.AdbcVersion1_1_0), + adbc.InfoCode(10_001): "my custom info", + adbc.InfoCode(10_002): "this was fetched dynamically", + }, filterInfoValues(infoValues, + adbc.InfoVendorName, + adbc.InfoVendorVersion, + adbc.InfoVendorArrowVersion, + adbc.InfoVendorSql, + adbc.InfoVendorSubstrait, + adbc.InfoDriverName, + adbc.InfoDriverADBCVersion, + adbc.InfoCode(10_001), + adbc.InfoCode(10_002), + )) + require.NotEmpty(t, infoValues[adbc.InfoDriverVersion]) + require.NotEmpty(t, infoValues[adbc.InfoDriverArrowVersion]) dbObjects, err := cnxn.GetObjects(ctx, adbc.ObjectDepthAll, nil, nil, nil, nil, nil) require.NoError(t, err) @@ -769,6 +723,61 @@ func messagesEqual(expected, actual logMessage) bool { return true } +func filterInfoValues(values map[adbc.InfoCode]any, codes ...adbc.InfoCode) map[adbc.InfoCode]any { + filtered := make(map[adbc.InfoCode]any, len(codes)) + for _, code := range codes { + filtered[code] = values[code] + } + return filtered +} + +func getInfoValuesFromTable(t *testing.T, table arrow.Table) map[adbc.InfoCode]any { + t.Helper() + + values := make(map[adbc.InfoCode]any) + codeChunks := table.Column(0).Data().Chunks() + valueChunks := table.Column(1).Data().Chunks() + require.Len(t, codeChunks, len(valueChunks)) + + for chunkIdx := range codeChunks { + codeArr, ok := codeChunks[chunkIdx].(*array.Uint32) + require.True(t, ok) + unionArr, ok := valueChunks[chunkIdx].(*array.DenseUnion) + require.True(t, ok) + + offsets := unionArr.RawValueOffsets() + for row := 0; row < codeArr.Len(); row++ { + code := adbc.InfoCode(codeArr.Value(row)) + childID := unionArr.ChildID(row) + offset := int(offsets[row]) + child := unionArr.Field(childID) + if child.IsNull(offset) { + values[code] = nil + continue + } + + switch childID { + case 0: + strArray, ok := child.(*array.String) + require.True(t, ok) + values[code] = strArray.Value(offset) + case 1: + boolArray, ok := child.(*array.Boolean) + require.True(t, ok) + values[code] = boolArray.Value(offset) + case 2: + intArray, ok := child.(*array.Int64) + require.True(t, ok) + values[code] = intArray.Value(offset) + default: + t.Fatalf("unexpected dense union child id %d for info code %d", childID, code) + } + } + } + + return values +} + func tableFromRecordReader(rdr array.RecordReader) arrow.Table { defer rdr.Release() From a567ca1f4fc137d090b6ad57b9d7404151b0e9dc Mon Sep 17 00:00:00 2001 From: Artur Rakhmatulin Date: Mon, 6 Jul 2026 11:29:42 +0100 Subject: [PATCH 2/2] chore: narrow driver version debugging changes --- go/adbc/cmd/adbc-driver-info/main.go | 118 ------------------ .../driver/flightsql/flightsql_database.go | 22 +--- 2 files changed, 2 insertions(+), 138 deletions(-) delete mode 100644 go/adbc/cmd/adbc-driver-info/main.go diff --git a/go/adbc/cmd/adbc-driver-info/main.go b/go/adbc/cmd/adbc-driver-info/main.go deleted file mode 100644 index dab046614c..0000000000 --- a/go/adbc/cmd/adbc-driver-info/main.go +++ /dev/null @@ -1,118 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -package main - -import ( - "context" - "encoding/json" - "flag" - "fmt" - "os" - "strings" - - "github.com/apache/arrow-adbc/go/adbc" - "github.com/apache/arrow-adbc/go/adbc/drivermgr" -) - -type optionsFlag map[string]string - -func (o *optionsFlag) String() string { - if o == nil { - return "" - } - parts := make([]string, 0, len(*o)) - for key, val := range *o { - parts = append(parts, key+"="+val) - } - return strings.Join(parts, ",") -} - -func (o *optionsFlag) Set(value string) error { - key, val, ok := strings.Cut(value, "=") - if !ok || strings.TrimSpace(key) == "" { - return fmt.Errorf("option must be key=value, got %q", value) - } - if *o == nil { - *o = make(map[string]string) - } - (*o)[key] = val - return nil -} - -func main() { - var ( - driverPath string - entrypoint string - options optionsFlag - ) - - flag.StringVar(&driverPath, "driver", "", "Path or driver name to load via ADBC driver manager") - flag.StringVar(&entrypoint, "entrypoint", "", "Optional driver entrypoint symbol") - flag.Var(&options, "option", "Database option in key=value form; may be repeated") - flag.Parse() - - if strings.TrimSpace(driverPath) == "" { - fmt.Fprintln(os.Stderr, "missing required --driver") - flag.Usage() - os.Exit(2) - } - - dbOptions := make(map[string]string, len(options)+2) - for key, val := range options { - dbOptions[key] = val - } - dbOptions["driver"] = driverPath - if strings.TrimSpace(entrypoint) != "" { - dbOptions["entrypoint"] = entrypoint - } - - var drv drivermgr.Driver - db, err := drv.NewDatabase(dbOptions) - if err != nil { - fail("create database", err) - } - defer closeOrWarn("database", db.Close) - - cnxn, err := db.Open(context.Background()) - if err != nil { - fail("open connection", err) - } - defer closeOrWarn("connection", cnxn.Close) - - info, err := adbc.GetDriverInfo(context.Background(), cnxn) - if err != nil { - fail("get driver info", err) - } - - encoder := json.NewEncoder(os.Stdout) - encoder.SetIndent("", " ") - if err := encoder.Encode(info); err != nil { - fail("encode output", err) - } -} - -func closeOrWarn(label string, fn func() error) { - if err := fn(); err != nil { - fmt.Fprintf(os.Stderr, "warning: failed to close %s: %v\n", label, err) - } -} - -func fail(action string, err error) { - fmt.Fprintf(os.Stderr, "%s: %v\n", action, err) - os.Exit(1) -} diff --git a/go/adbc/driver/flightsql/flightsql_database.go b/go/adbc/driver/flightsql/flightsql_database.go index 023027ee6f..259e534edb 100644 --- a/go/adbc/driver/flightsql/flightsql_database.go +++ b/go/adbc/driver/flightsql/flightsql_database.go @@ -72,19 +72,6 @@ type databaseImpl struct { oauthToken credentials.PerRPCCredentials } -func getDriverInfoString(info *driverbase.DriverInfo, code adbc.InfoCode, fallback string) string { - value, ok := info.GetInfoForInfoCode(code) - if !ok { - return fallback - } - - str, ok := value.(string) - if !ok || str == "" { - return fallback - } - return str -} - func (d *databaseImpl) SetOptions(cnOptions map[string]string) error { var tlsConfig tls.Config @@ -421,7 +408,8 @@ func getFlightClient(ctx context.Context, loc string, d *databaseImpl, authMiddl target = "unix:" + uri.Path } - driverVersion := getDriverInfoString(d.DriverInfo, adbc.InfoDriverVersion, "unknown") + dv, _ := d.DriverInfo.GetInfoForInfoCode(adbc.InfoDriverVersion) + driverVersion := dv.(string) dialOpts := append(d.dialOpts.opts, grpc.WithConnectParams(d.timeout.connectParams()), grpc.WithTransportCredentials(creds), grpc.WithUserAgent("ADBC Flight SQL Driver "+driverVersion)) dialOpts = append(dialOpts, d.userDialOpts...) @@ -594,16 +582,10 @@ func (d *databaseImpl) Open(ctx context.Context) (adbc.Connection, error) { conn.id = newRandomID("conn") conn.openedAt = time.Now() conn.Logger = safeLogger(conn.Logger).With("connection_id", conn.id) - driverVersion := getDriverInfoString(d.DriverInfo, adbc.InfoDriverVersion, "unknown") - driverArrowVersion := getDriverInfoString(d.DriverInfo, adbc.InfoDriverArrowVersion, "unknown") - driverADBCVersion := getDriverInfoString(d.DriverInfo, adbc.InfoDriverADBCVersion, "unknown") conn.Logger.InfoContext(ctx, "FlightSQL connection opened", "target", d.uri.String(), "transactionsSupported", cnxnSupport.transactions, "driver", infoDriverName, - "driver_version", driverVersion, - "driver_arrow_version", driverArrowVersion, - "driver_adbc_version", driverADBCVersion, ) return driverbase.NewConnectionBuilder(conn).