1515package databricks
1616
1717import (
18+ "bytes"
1819 "context"
19- "database/sql/driver"
2020 "fmt"
2121 "strings"
2222
2323 "github.com/adbc-drivers/driverbase-go/driverbase"
2424 "github.com/apache/arrow-adbc/go/adbc"
2525 "github.com/apache/arrow-go/v18/arrow"
26- "github.com/apache/arrow-go/v18/arrow/array"
2726)
2827
29- // executeIngest performs bulk insert using parameterized INSERT statements
30- func (s * statementImpl ) executeIngest (ctx context.Context ) (int64 , error ) {
31- if s .boundStream == nil {
32- return - 1 , s .ErrorHelper .Errorf (adbc .StatusInvalidState , "no data bound for ingestion" )
33- }
34-
35- defer func () {
36- s .boundStream .Release ()
37- s .boundStream = nil
38- }()
39-
40- opts := & s .bulkIngestOptions
41-
42- tableName := buildTableName (opts .CatalogName , opts .SchemaName , opts .TableName )
43-
44- if err := s .createTableIfNeeded (ctx , tableName , s .boundStream .Schema (), opts ); err != nil {
45- return - 1 , err
46- }
47-
48- insertSQL , err := buildInsertSQL (tableName , s .boundStream .Schema ())
49- if err != nil {
50- return - 1 , err
51- }
52-
53- totalRows := int64 (0 )
54- params := make ([]driver.NamedValue , s .boundStream .Schema ().NumFields ())
55-
56- for s .boundStream .Next () {
57- recordBatch := s .boundStream .RecordBatch ()
58-
59- for rowIdx := range int (recordBatch .NumRows ()) {
60- // Extract Go values from Arrow columns
61- for colIdx := range int (recordBatch .NumCols ()) {
62- arr := recordBatch .Column (colIdx )
63- val , err := extractGoValue (arr , rowIdx )
64- if err != nil {
65- return totalRows , s .ErrorHelper .Errorf (adbc .StatusInternal , "failed to extract go value: %v" , err )
66- }
67- params [colIdx ].Value = val
68- }
69-
70- // Use ExecContext directly instead of PrepareContext because Databricks doesn't do server-side statement preparation
71- result , err := s .conn .conn .ExecContext (ctx , insertSQL , valuesToInterfaces (params )... )
72- if err != nil {
73- return totalRows , s .ErrorHelper .Errorf (adbc .StatusInternal , "failed to execute the query: %v" , err )
74- }
28+ // databricksBulkIngest implements driverbase.BulkIngestImpl for the
29+ // Databricks Staging + COPY INTO pattern. It uploads Parquet files to a
30+ // Unity Catalog Volume via the Databricks Files API, then uses COPY INTO
31+ // to load them into the target table.
32+ type databricksBulkIngest struct {
33+ conn * connectionImpl
34+ stagingClient * stagingClient
35+ errorHelper * driverbase.ErrorHelper
36+ options * driverbase.BulkIngestOptions
37+ }
7538
76- rows , _ := result .RowsAffected ()
77- totalRows += rows
78- }
79- }
39+ // pendingCopy tracks a file uploaded to staging that is ready for COPY INTO.
40+ // It implements driverbase.BulkIngestPendingCopy.
41+ type pendingCopy struct {
42+ path string
43+ rows int64
44+ }
8045
81- if err := s .boundStream .Err (); err != nil {
82- return totalRows , s .ErrorHelper .Errorf (adbc .StatusInternal , "stream error: %v" , err )
83- }
46+ func (p * pendingCopy ) String () string { return p .path }
47+ func (p * pendingCopy ) Rows () int64 { return p .rows }
8448
85- return totalRows , nil
49+ // CreateSink returns an in-memory buffer for Parquet data to be written to.
50+ func (bi * databricksBulkIngest ) CreateSink (ctx context.Context , options * driverbase.BulkIngestOptions ) (driverbase.BulkIngestSink , error ) {
51+ return & driverbase.BufferBulkIngestSink {}, nil
8652}
8753
88- // createTableIfNeeded creates/drops table based on ingest mode
89- func (s * statementImpl ) createTableIfNeeded (ctx context.Context , tableName string , schema * arrow.Schema , opts * driverbase.BulkIngestOptions ) error {
90- switch opts .Mode {
91- case adbc .OptionValueIngestModeCreate :
92- return s .createTable (ctx , tableName , schema , false )
54+ // CreateTable creates or drops/recreates the target table based on the
55+ // specified table existence and missing behaviors.
56+ func (bi * databricksBulkIngest ) CreateTable (ctx context.Context , schema * arrow.Schema , ifTableExists driverbase.BulkIngestTableExistsBehavior , ifTableMissing driverbase.BulkIngestTableMissingBehavior ) error {
57+ tableName := buildTableName (bi .options .CatalogName , bi .options .SchemaName , bi .options .TableName )
9358
94- case adbc .OptionValueIngestModeCreateAppend :
95- return s .createTable (ctx , tableName , schema , true )
96-
97- case adbc .OptionValueIngestModeReplace :
59+ if ifTableExists == driverbase .BulkIngestTableExistsDrop {
9860 dropSQL := fmt .Sprintf ("DROP TABLE IF EXISTS %s" , tableName )
99- if _ , err := s .conn .conn .ExecContext (ctx , dropSQL ); err != nil {
100- return s . ErrorHelper .Errorf (adbc .StatusInternal , "failed to drop the table: %v" , err )
61+ if _ , err := bi .conn .conn .ExecContext (ctx , dropSQL ); err != nil {
62+ return bi . errorHelper .Errorf (adbc .StatusInternal , "failed to drop table: %v" , err )
10163 }
102- return s .createTable (ctx , tableName , schema , false )
103-
104- case adbc .OptionValueIngestModeAppend :
105- return nil
64+ }
10665
107- default :
108- return s .ErrorHelper .Errorf (adbc .StatusInvalidArgument , "invalid ingest mode: %s" , opts .Mode )
66+ if ifTableMissing == driverbase .BulkIngestTableMissingCreate {
67+ ifNotExists := ifTableExists == driverbase .BulkIngestTableExistsIgnore
68+ return bi .createTableDDL (ctx , tableName , schema , ifNotExists )
10969 }
70+
71+ return nil
11072}
11173
112- // createTable generates and executes CREATE TABLE DDL
113- func (s * statementImpl ) createTable (ctx context.Context , tableName string , schema * arrow.Schema , ifNotExists bool ) error {
74+ // createTableDDL generates and executes CREATE TABLE DDL from an Arrow schema.
75+ func (bi * databricksBulkIngest ) createTableDDL (ctx context.Context , tableName string , schema * arrow.Schema , ifNotExists bool ) error {
11476 var sql strings.Builder
11577 sql .WriteString ("CREATE TABLE " )
11678 if ifNotExists {
@@ -132,48 +94,59 @@ func (s *statementImpl) createTable(ctx context.Context, tableName string, schem
13294 }
13395 sql .WriteString (")" )
13496
135- _ , err := s .conn .conn .ExecContext (ctx , sql .String ())
97+ _ , err := bi .conn .conn .ExecContext (ctx , sql .String ())
13698 if err != nil {
137- return s . ErrorHelper .Errorf (adbc .StatusInternal , "failed to create table: %v" , err )
99+ return bi . errorHelper .Errorf (adbc .StatusInternal , "failed to create table: %v" , err )
138100 }
139101 return nil
140102}
141103
142- // buildInsertSQL generates parameterized INSERT statement
143- func buildInsertSQL (tableName string , schema * arrow.Schema ) (string , error ) {
144- var sql strings.Builder
104+ // Upload uploads the Parquet data from a buffer to the staging volume.
105+ func (bi * databricksBulkIngest ) Upload (ctx context.Context , chunk driverbase.BulkIngestPendingUpload ) (driverbase.BulkIngestPendingCopy , error ) {
106+ buf , ok := chunk .Data .(* driverbase.BufferBulkIngestSink )
107+ if ! ok {
108+ return nil , bi .errorHelper .Errorf (adbc .StatusInternal , "unexpected sink type: %T" , chunk .Data )
109+ }
145110
146- sql .WriteString ("INSERT INTO " )
147- sql .WriteString (tableName )
148- sql .WriteString (" (" )
111+ path , err := bi .stagingClient .generateFileName ()
112+ if err != nil {
113+ return nil , bi .errorHelper .Errorf (adbc .StatusInternal , "failed to generate staging file name: %v" , err )
114+ }
149115
150- for i , field := range schema .Fields () {
151- if i > 0 {
152- sql .WriteString (", " )
153- }
154- sql .WriteString (quoteIdentifier (field .Name ))
116+ if err := bi .stagingClient .Upload (ctx , path , bytes .NewReader (buf .Bytes ())); err != nil {
117+ return nil , bi .errorHelper .Errorf (adbc .StatusIO , "failed to upload staging file: %v" , err )
155118 }
156119
157- sql .WriteString (") VALUES (" )
120+ return & pendingCopy {path : path , rows : chunk .Rows }, nil
121+ }
158122
159- for i , field := range schema .Fields () {
160- if i > 0 {
161- sql .WriteString (", " )
162- }
123+ // Copy executes COPY INTO to load a staged Parquet file into the target table.
124+ func (bi * databricksBulkIngest ) Copy (ctx context.Context , chunk driverbase.BulkIngestPendingCopy ) error {
125+ tableName := buildTableName (bi .options .CatalogName , bi .options .SchemaName , bi .options .TableName )
163126
164- if field .Type .ID () == arrow .FIXED_SIZE_BINARY {
165- // Use UNHEX() to convert hex string to binary
166- sql .WriteString ("UNHEX(?)" )
167- } else {
168- sql .WriteString ("?" )
169- }
127+ // The path from pendingCopy has the form "Volumes/catalog/schema/volume/prefix/file.parquet".
128+ // COPY INTO expects the path with a leading slash: '/Volumes/...'
129+ copySQL := fmt .Sprintf (
130+ "COPY INTO %s FROM '/%s' FILEFORMAT = PARQUET" ,
131+ tableName , chunk .String (),
132+ )
133+
134+ _ , err := bi .conn .conn .ExecContext (ctx , copySQL )
135+ if err != nil {
136+ return bi .errorHelper .Errorf (adbc .StatusInternal , "COPY INTO failed: %v" , err )
170137 }
138+ return nil
139+ }
171140
172- sql .WriteString (")" )
173- return sql .String (), nil
141+ // Delete removes the staging file after it has been copied into the target table.
142+ func (bi * databricksBulkIngest ) Delete (ctx context.Context , chunk driverbase.BulkIngestPendingCopy ) error {
143+ if err := bi .stagingClient .Delete (ctx , chunk .String ()); err != nil {
144+ return bi .errorHelper .Errorf (adbc .StatusIO , "failed to delete staging file: %v" , err )
145+ }
146+ return nil
174147}
175148
176- // buildTableName constructs catalog.schema.table name
149+ // buildTableName constructs a fully qualified catalog.schema.table name.
177150func buildTableName (catalog , schema , table string ) string {
178151 parts := []string {}
179152 if catalog != "" {
@@ -186,102 +159,13 @@ func buildTableName(catalog, schema, table string) string {
186159 return strings .Join (parts , "." )
187160}
188161
189- // quoteIdentifier quotes a Databricks identifier with backticks
162+ // quoteIdentifier quotes a Databricks identifier with backticks.
190163func quoteIdentifier (id string ) string {
191164 escaped := strings .ReplaceAll (id , "`" , "``" )
192165 return fmt .Sprintf ("`%s`" , escaped )
193166}
194167
195- // valuesToInterfaces converts driver.NamedValue slice to []any for ExecContext
196- func valuesToInterfaces (params []driver.NamedValue ) []any {
197- result := make ([]any , len (params ))
198- for i , p := range params {
199- result [i ] = p .Value
200- }
201- return result
202- }
203-
204- // extractGoValue extracts a Go value from an Arrow array at the given index
205- func extractGoValue (arr arrow.Array , idx int ) (any , error ) {
206- if arr .IsNull (idx ) {
207- return nil , nil
208- }
209-
210- switch arr .DataType ().ID () {
211- case arrow .BOOL :
212- return arr .(* array.Boolean ).Value (idx ), nil
213-
214- case arrow .INT8 :
215- return int64 (arr .(* array.Int8 ).Value (idx )), nil
216- case arrow .INT16 :
217- return int64 (arr .(* array.Int16 ).Value (idx )), nil
218- case arrow .INT32 :
219- return int64 (arr .(* array.Int32 ).Value (idx )), nil
220- case arrow .INT64 :
221- // https://github.com/databricks/databricks-sql-go/issues/315
222- // databricks-sql-go incorrectly maps int64 to SqlInteger (INT) instead of SqlBigInt (BIGINT)
223- // Pass as string to preserve full range
224- return fmt .Sprintf ("%d" , arr .(* array.Int64 ).Value (idx )), nil
225-
226- case arrow .UINT8 :
227- return int64 (arr .(* array.Uint8 ).Value (idx )), nil
228- case arrow .UINT16 :
229- return int64 (arr .(* array.Uint16 ).Value (idx )), nil
230- case arrow .UINT32 :
231- // https://github.com/databricks/databricks-sql-go/issues/315
232- // databricks-sql-go incorrectly maps int64 to SqlInteger (INT)
233- // Pass as string to preserve full uint32 range
234- return fmt .Sprintf ("%d" , arr .(* array.Uint32 ).Value (idx )), nil
235- case arrow .UINT64 :
236- // Pass as string to preserve full uint64 range (may still overflow if > int64 max)
237- return fmt .Sprintf ("%d" , arr .(* array.Uint64 ).Value (idx )), nil
238-
239- case arrow .FLOAT32 :
240- return float64 (arr .(* array.Float32 ).Value (idx )), nil
241- case arrow .FLOAT64 :
242- // https://github.com/databricks/databricks-sql-go/issues/314
243- // databricks-sql-go has a bug where float64 is treated as SqlFloat instead of SqlDouble
244- // causing precision loss. Pass as string to preserve full precision.
245- val := arr .(* array.Float64 ).Value (idx )
246- return fmt .Sprintf ("%.17g" , val ), nil
247-
248- case arrow .STRING :
249- return arr .(* array.String ).Value (idx ), nil
250- case arrow .LARGE_STRING :
251- return arr .(* array.LargeString ).Value (idx ), nil
252- case arrow .STRING_VIEW :
253- return arr .(* array.StringView ).Value (idx ), nil
254-
255- case arrow .BINARY :
256- return arr .(* array.Binary ).Value (idx ), nil
257- case arrow .LARGE_BINARY :
258- return arr .(* array.LargeBinary ).Value (idx ), nil
259- case arrow .BINARY_VIEW :
260- return arr .(* array.BinaryView ).Value (idx ), nil
261- case arrow .FIXED_SIZE_BINARY :
262- // Convert to hex string for use with UNHEX() SQL function
263- return fmt .Sprintf ("%x" , arr .(* array.FixedSizeBinary ).Value (idx )), nil
264-
265- case arrow .DATE32 :
266- return arr .(* array.Date32 ).Value (idx ).ToTime (), nil
267- case arrow .DATE64 :
268- return arr .(* array.Date64 ).Value (idx ).ToTime (), nil
269-
270- case arrow .TIMESTAMP :
271- ts := arr .DataType ().(* arrow.TimestampType )
272- return arr .(* array.Timestamp ).Value (idx ).ToTime (ts .Unit ), nil
273-
274- case arrow .DECIMAL128 :
275- dec := arr .(* array.Decimal128 )
276- // Return as string, databricks-sql-go will infer DECIMAL type
277- return dec .ValueStr (idx ), nil
278-
279- default :
280- return nil , fmt .Errorf ("unsupported Arrow type: %s" , arr .DataType ())
281- }
282- }
283-
284- // arrowTypeToDatabricksType maps Arrow types to Databricks DDL types for CREATE TABLE
168+ // arrowTypeToDatabricksType maps Arrow types to Databricks DDL types for CREATE TABLE.
285169func arrowTypeToDatabricksType (dt arrow.DataType ) string {
286170 switch dt .ID () {
287171 case arrow .BOOL :
0 commit comments