Skip to content
Merged
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
25 changes: 14 additions & 11 deletions go/bulk_ingest.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package databricks
import (
"context"
"database/sql/driver"
"encoding/hex"
"fmt"
"strings"

Expand Down Expand Up @@ -52,6 +53,8 @@ func (s *statementImpl) executeIngest(ctx context.Context) (int64, error) {

totalRows := int64(0)
params := make([]driver.NamedValue, s.boundStream.Schema().NumFields())
errorHelper := s.ErrorHelper
errorHelper.ErrorInspector = DatabricksErrorInspector{ingest: true}

for s.boundStream.Next() {
recordBatch := s.boundStream.RecordBatch()
Expand All @@ -70,7 +73,7 @@ func (s *statementImpl) executeIngest(ctx context.Context) (int64, error) {
// Use ExecContext directly instead of PrepareContext because Databricks doesn't do server-side statement preparation
result, err := s.conn.conn.ExecContext(ctx, insertSQL, valuesToInterfaces(params)...)
if err != nil {
return totalRows, s.ErrorHelper.Errorf(adbc.StatusInternal, "failed to execute the query: %v", err)
return totalRows, errorHelper.WrapInternal(err, "failed to execute the query")
}

rows, _ := result.RowsAffected()
Expand All @@ -97,7 +100,7 @@ func (s *statementImpl) createTableIfNeeded(ctx context.Context, tableName strin
case adbc.OptionValueIngestModeReplace:
dropSQL := fmt.Sprintf("DROP TABLE IF EXISTS %s", tableName)
if _, err := s.conn.conn.ExecContext(ctx, dropSQL); err != nil {
return s.ErrorHelper.Errorf(adbc.StatusInternal, "failed to drop the table: %v", err)
return s.ErrorHelper.WrapInternal(err, "failed to drop the table")
}
return s.createTable(ctx, tableName, schema, false)

Expand Down Expand Up @@ -134,7 +137,7 @@ func (s *statementImpl) createTable(ctx context.Context, tableName string, schem

_, err := s.conn.conn.ExecContext(ctx, sql.String())
if err != nil {
return s.ErrorHelper.Errorf(adbc.StatusInternal, "failed to create table: %v", err)
return s.ErrorHelper.WrapInternal(err, "failed to create table")
}
return nil
}
Expand All @@ -161,10 +164,11 @@ func buildInsertSQL(tableName string, schema *arrow.Schema) (string, error) {
sql.WriteString(", ")
}

if field.Type.ID() == arrow.FIXED_SIZE_BINARY {
// Use UNHEX() to convert hex string to binary
switch field.Type.ID() {
case arrow.BINARY, arrow.LARGE_BINARY, arrow.BINARY_VIEW, arrow.FIXED_SIZE_BINARY:
// Send hex strings to preserve arbitrary bytes through text parameters.
sql.WriteString("UNHEX(?)")
} else {
default:
sql.WriteString("?")
}
}
Expand Down Expand Up @@ -253,14 +257,13 @@ func extractGoValue(arr arrow.Array, idx int) (any, error) {
return arr.(*array.StringView).Value(idx), nil

case arrow.BINARY:
return arr.(*array.Binary).Value(idx), nil
return hex.EncodeToString(arr.(*array.Binary).Value(idx)), nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Low — This PR adds dictionary-encoded array support to the parameter-binding path (parameterValueType + the new arrow.DICTIONARY case in arrowValueToParameter), but the ingest path is left asymmetric. extractGoValue has no DICTIONARY case, so it falls through to the default branch and returns unsupported Arrow type; arrowTypeToDatabricksType similarly falls back to STRING for a dictionary column. As a result, binding the same dictionary-encoded Arrow data succeeds for ExecuteQuery/ExecuteUpdate but fails for ingestion. If dictionary ingestion is intentionally out of scope for this PR, consider resolving the value type here too (mirroring parameterValueType) in a follow-up, or at least documenting the gap.

(Anchored to the nearest changed line — see the description for the exact location.)

case arrow.LARGE_BINARY:
return arr.(*array.LargeBinary).Value(idx), nil
return hex.EncodeToString(arr.(*array.LargeBinary).Value(idx)), nil
case arrow.BINARY_VIEW:
return arr.(*array.BinaryView).Value(idx), nil
return hex.EncodeToString(arr.(*array.BinaryView).Value(idx)), nil
case arrow.FIXED_SIZE_BINARY:
// Convert to hex string for use with UNHEX() SQL function
return fmt.Sprintf("%x", arr.(*array.FixedSizeBinary).Value(idx)), nil
return hex.EncodeToString(arr.(*array.FixedSizeBinary).Value(idx)), nil

case arrow.DATE32:
return arr.(*array.Date32).Value(idx).ToTime(), nil
Expand Down
4 changes: 3 additions & 1 deletion go/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,10 @@ func NewDriver(alloc memory.Allocator) driverbase.DriverWithContext {
panic(err)
}

base := driverbase.NewDriverImplBase(info, alloc)
base.ErrorHelper.ErrorInspector = DatabricksErrorInspector{}
return driverbase.NewDriver(&driverImpl{
DriverImplBase: driverbase.NewDriverImplBase(info, alloc),
DriverImplBase: base,
})
}

Expand Down
52 changes: 52 additions & 0 deletions go/error_inspector.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright (c) 2026 ADBC Drivers Contributors
//
// Licensed 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 databricks

import (
"errors"

"github.com/apache/arrow-adbc/go/adbc"
dbsqlerr "github.com/databricks/databricks-sql-go/errors"
)

type DatabricksErrorInspector struct {
ingest bool
}

// InspectError maps Databricks execution errors to ADBC errors and preserves SQLSTATE.
func (d DatabricksErrorInspector) InspectError(err error, defaultStatus adbc.Status) adbc.Error {
adbcErr := adbc.Error{Code: defaultStatus, Msg: err.Error()}
var executionErr dbsqlerr.DBExecutionError
for errors.As(err, &executionErr) {
if sqlState := executionErr.SqlState(); sqlState != "" {
copy(adbcErr.SqlState[:], sqlState)
switch sqlState {
case "42P07": // TABLE_OR_VIEW_ALREADY_EXISTS
adbcErr.Code = adbc.StatusAlreadyExists
case "42703": // UNRESOLVED_COLUMN
adbcErr.Code = adbc.StatusInvalidArgument
if d.ingest {
// A missing destination column means the existing table's
// schema conflicts with the ingest input.
adbcErr.Code = adbc.StatusAlreadyExists

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Low — In ingest mode, UNRESOLVED_COLUMN (SQLSTATE 42703) is remapped from StatusInvalidArgument to StatusAlreadyExists. This conflates two genuinely different ingest failures: (a) the target table pre-exists with a schema that conflicts with the bound input, and (b) the caller simply named a column that doesn't exist (e.g. a typo in a bound field name, or an append into a table that legitimately lacks that column). Case (b) is a true invalid-argument condition, but it will now surface to the ADBC caller as ALREADY_EXISTS, which is misleading and may hide a real input mistake. The comment documents the intended case (a), and this is clearly deliberate to satisfy the validation fixture, so flagging as low — but consider narrowing the override (e.g. only for Create/CreateAppend modes where a pre-existing table is the plausible cause) rather than applying it to every ingest path.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Low — During ingest, a 42703 (UNRESOLVED_COLUMN) server error is remapped from StatusInvalidArgument to StatusAlreadyExists. This is a surprising conflation: 42703 fires whenever a referenced column can't be resolved against the destination table, and the driver builds the INSERT column list from the input Arrow schema — so this maps "input has a column the table doesn't" to a status that callers normally read as "the table already exists." That is a reasonable interpretation for the append-into-incompatible-table case the comment describes, but it will also mask genuinely different column-resolution failures under the same status. If the validation suite specifically expects ALREADY_EXISTS here this is intended — worth a confirming note, since StatusAlreadyExists is a lossy label for "schema conflict."

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Low — During ingest, SQLSTATE 42703 (UNRESOLVED_COLUMN) is remapped to StatusAlreadyExists. The generated INSERT is driver-built from the bound schema, so in the common case a missing column really does mean a schema conflict with an existing table — but 42703 can also surface for unrelated reasons (e.g. an identifier the driver quotes/qualifies unexpectedly, or a server-side resolution quirk), and those would now be reported to callers as AlreadyExists rather than a more accurate InvalidArgument/Internal. Worth confirming the mapping is narrow enough in practice, or consider also matching on the message text before overriding to AlreadyExists.

}
}
break
}
// A wrapper may have no SQLSTATE even when its cause has one.
err = errors.Unwrap(executionErr)
}
return adbcErr
}
6 changes: 3 additions & 3 deletions go/statement.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ func (s *statementImpl) Prepare(ctx context.Context) error {

stmt, err := s.conn.conn.PrepareContext(ctx, s.query)
if err != nil {
return s.ErrorHelper.Errorf(adbc.StatusInvalidState, "failed to prepare statement: %v", err)
return s.ErrorHelper.WrapInvalidState(err, "failed to prepare statement")
}

s.prepared = stmt
Expand Down Expand Up @@ -150,7 +150,7 @@ func executeQuery(ctx context.Context, conn *sql.Conn, query string, args []driv
})

if err != nil {
return nil, errorHelper.Errorf(adbc.StatusInternal, "failed to execute query: %v", err)
return nil, errorHelper.WrapInternal(err, "failed to execute query")
}

defer func() {
Expand Down Expand Up @@ -241,7 +241,7 @@ func (s *statementImpl) executeUpdate(ctx context.Context, query string, args []
result, err = s.conn.conn.ExecContext(ctx, query, values...)
}
if err != nil {
return -1, s.ErrorHelper.Errorf(adbc.StatusInternal, "failed to execute update: %v", err)
return -1, s.ErrorHelper.WrapInternal(err, "failed to execute update")
}

rowsAffected, err := result.RowsAffected()
Expand Down
17 changes: 17 additions & 0 deletions go/validation/queries/ingest/decimal_scale_negative.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Copyright (c) 2026 ADBC Drivers Contributors
#
# Licensed 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.

# DECIMAL scale must be between zero and precision.
# https://docs.databricks.com/aws/en/sql/language-manual/data-types/decimal-type
hide = true
2 changes: 2 additions & 0 deletions go/validation/queries/ingest/timestamp_ns.txtcase
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
// part: metadata

[tags]
partial-support = true
caveats = ["Nanosecond timestamps are truncated to microseconds."]
sql-type-name = "TIMESTAMP_NTZ"

// part: expected_schema
Expand Down
2 changes: 2 additions & 0 deletions go/validation/queries/ingest/timestamptz_ns.txtcase
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
// part: metadata

[tags]
partial-support = true
caveats = ["Nanosecond timestamps are truncated to microseconds."]
sql-type-name = "TIMESTAMP"

// part: expected_schema
Expand Down
22 changes: 22 additions & 0 deletions go/validation/queries/type/bind/float64.txtcase
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright (c) 2026 ADBC Drivers Contributors
//
// Licensed 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.

// part: metadata

[tags]
sql-type-name = "DOUBLE"

// part: setup_query

CREATE TABLE test_float64 (res DOUBLE);
5 changes: 5 additions & 0 deletions go/validation/queries/type/bind/timestamp_ms.txtcase
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.

// part: metadata

[tags]
sql-type-name = "TIMESTAMP_NTZ"

// part: setup_query

CREATE TABLE test_timestamp (idx INT, res TIMESTAMP_NTZ);
Expand Down
7 changes: 7 additions & 0 deletions go/validation/queries/type/bind/timestamp_ns.txtcase
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.

// part: metadata

[tags]
sql-type-name = "TIMESTAMP_NTZ"
partial-support = true
caveats = ["Nanosecond timestamps are truncated to microseconds."]

// part: setup_query

CREATE TABLE test_timestamp (idx INT, res TIMESTAMP_NTZ);
Expand Down
5 changes: 5 additions & 0 deletions go/validation/queries/type/bind/timestamp_s.txtcase
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.

// part: metadata

[tags]
sql-type-name = "TIMESTAMP_NTZ"

// part: setup_query

CREATE TABLE test_timestamp (idx INT, res TIMESTAMP_NTZ);
Expand Down
5 changes: 5 additions & 0 deletions go/validation/queries/type/bind/timestamp_us.txtcase
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.

// part: metadata

[tags]
sql-type-name = "TIMESTAMP_NTZ"

// part: setup_query

CREATE TABLE test_timestamp (idx INT, res TIMESTAMP_NTZ);
9 changes: 9 additions & 0 deletions go/validation/queries/type/bind/timestamptz_ms.txtcase
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@
// See the License for the specific language governing permissions and
// limitations under the License.

// part: metadata

[tags]
sql-type-name = "TIMESTAMP"

// part: setup_query

CREATE TABLE test_timestamptz (idx INT, res TIMESTAMP, res2 TIMESTAMP);

// part: expected_schema

{"format": "+s", "children": [
Expand Down
11 changes: 11 additions & 0 deletions go/validation/queries/type/bind/timestamptz_ns.txtcase
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,17 @@
// See the License for the specific language governing permissions and
// limitations under the License.

// part: metadata

[tags]
sql-type-name = "TIMESTAMP"
partial-support = true
caveats = ["Nanosecond timestamps are truncated to microseconds."]

// part: setup_query

CREATE TABLE test_timestamptz (idx INT, res TIMESTAMP, res2 TIMESTAMP);

// part: expected_schema

{"format": "+s", "children": [
Expand Down
9 changes: 9 additions & 0 deletions go/validation/queries/type/bind/timestamptz_s.txtcase
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@
// See the License for the specific language governing permissions and
// limitations under the License.

// part: metadata

[tags]
sql-type-name = "TIMESTAMP"

// part: setup_query

CREATE TABLE test_timestamptz (idx INT, res TIMESTAMP, res2 TIMESTAMP);

// part: expected_schema

{"format": "+s", "children": [
Expand Down
9 changes: 9 additions & 0 deletions go/validation/queries/type/bind/timestamptz_us.txtcase
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@
// See the License for the specific language governing permissions and
// limitations under the License.

// part: metadata

[tags]
sql-type-name = "TIMESTAMP"

// part: setup_query

CREATE TABLE test_timestamptz (idx INT, res TIMESTAMP, res2 TIMESTAMP);

// part: expected_schema

{"format": "+s", "children": [
Expand Down
17 changes: 17 additions & 0 deletions go/validation/queries/type/select/timestamp0.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Copyright (c) 2026 ADBC Drivers Contributors
#
# Licensed 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.

# Databricks timestamp precision starts at 6 fractional digits.
# https://docs.databricks.com/aws/en/sql/language-manual/data-types/timestamp-type
hide = true
17 changes: 17 additions & 0 deletions go/validation/queries/type/select/timestamp0tz.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Copyright (c) 2026 ADBC Drivers Contributors
#
# Licensed 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.

# Databricks timestamp precision starts at 6 fractional digits.
# https://docs.databricks.com/aws/en/sql/language-manual/data-types/timestamp-type
hide = true
17 changes: 17 additions & 0 deletions go/validation/queries/type/select/timestamp1.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Copyright (c) 2026 ADBC Drivers Contributors
#
# Licensed 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.

# Databricks timestamp precision starts at 6 fractional digits.
# https://docs.databricks.com/aws/en/sql/language-manual/data-types/timestamp-type
hide = true
Loading
Loading