Skip to content

Commit 25e1577

Browse files
committed
feat(database): implement native pgx connection for Cloud SQL COPY operations
1 parent b51a82f commit 25e1577

2 files changed

Lines changed: 164 additions & 3 deletions

File tree

core/dbio/database/database_postgres.go

Lines changed: 163 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ import (
66
"database/sql"
77
"fmt"
88
"io"
9+
"net"
910
"os/exec"
1011
"strings"
1112
"time"
1213

1314
"cloud.google.com/go/cloudsqlconn"
1415
"cloud.google.com/go/cloudsqlconn/postgres/pgxv5"
16+
"github.com/jackc/pgx/v5"
1517
"github.com/jmoiron/sqlx"
1618
"github.com/samber/lo"
1719
"github.com/slingdata-io/sling-cli/core/dbio"
@@ -28,6 +30,7 @@ type PostgresConn struct {
2830
URL string
2931
isCloudSQL bool
3032
cloudSQLCleanup func()
33+
cloudSQLConn *pgx.Conn
3134
}
3235

3336
// Init initiates the object
@@ -195,6 +198,14 @@ func (conn *PostgresConn) connectCloudSQL(timeOut ...int) error {
195198
return g.Error(err, "could not ping Cloud SQL instance. Verify: 1) IAM auth flag 'cloudsql.iam_authentication=on', 2) SA '%s' has 'Cloud SQL Instance User' role, 3) SA added as IAM db user with full email, 4) DB privileges granted to '%s', 5) Credentials file valid and matches SA, 6) Cloud SQL Admin API enabled", user, user)
196199
}
197200

201+
// Open native pgx connection for COPY operations
202+
// This connection will be reused throughout the connection lifecycle
203+
err = conn.openCloudSQLNativePgxConn(ctx, instanceName, user, dbName, dialerOpts)
204+
if err != nil {
205+
g.Warn("could not open native pgx connection for COPY operations: %s", err.Error())
206+
conn.SetProp("use_bulk", "false") // set to not use COPY since IAM auth need pgx conn to COPY
207+
}
208+
198209
// Set role if provided
199210
if val := conn.GetProp("role"); val != "" {
200211
_, err = conn.Exec("SET ROLE " + val)
@@ -212,12 +223,57 @@ func (conn *PostgresConn) connectCloudSQL(timeOut ...int) error {
212223
return nil
213224
}
214225

226+
// openCloudSQLNativePgxConn opens a native pgx connection for Cloud SQL COPY operations
227+
// This is called once during connectCloudSQL and the connection is reused
228+
func (conn *PostgresConn) openCloudSQLNativePgxConn(ctx context.Context, instanceName, user, dbName string, dialerOpts []cloudsqlconn.Option) error {
229+
// Create Cloud SQL dialer
230+
dialer, err := cloudsqlconn.NewDialer(ctx, dialerOpts...)
231+
if err != nil {
232+
return g.Error(err, "could not create Cloud SQL dialer for native pgx connection")
233+
}
234+
235+
// Build pgx connection config
236+
pgxConfig, err := pgx.ParseConfig(fmt.Sprintf("user=%s dbname=%s sslmode=disable", user, dbName))
237+
if err != nil {
238+
return g.Error(err, "could not parse pgx config")
239+
}
240+
241+
// Set the dialer function to use Cloud SQL connector
242+
pgxConfig.DialFunc = func(ctx context.Context, network, addr string) (net.Conn, error) {
243+
return dialer.Dial(ctx, instanceName)
244+
}
245+
246+
// Open native pgx connection
247+
conn.cloudSQLConn, err = pgx.ConnectConfig(ctx, pgxConfig)
248+
if err != nil {
249+
return g.Error(err, "could not open native pgx connection for Cloud SQL COPY")
250+
}
251+
252+
g.Trace("opened native pgx connection for Cloud SQL COPY operations")
253+
return nil
254+
}
255+
256+
// closeCloudSQLNativeConn closes the native pgx connection
257+
func (conn *PostgresConn) closeCloudSQLNativeConn() {
258+
if conn.cloudSQLConn != nil {
259+
ctx := conn.Context().Ctx
260+
if err := conn.cloudSQLConn.Close(ctx); err != nil {
261+
g.LogError(err, "error closing native Cloud SQL connection")
262+
}
263+
conn.cloudSQLConn = nil
264+
g.Trace("closed native pgx connection for Cloud SQL COPY operations")
265+
}
266+
}
267+
215268
// Close closes the database connection and cleans up Cloud SQL resources
216269
func (conn *PostgresConn) Close() error {
217270
// Cleanup Cloud SQL resources first
218271
if conn.isCloudSQL {
219272
g.Trace("closing Cloud SQL connection and cleaning up resources")
220273

274+
// Close native pgx connection if open
275+
conn.closeCloudSQLNativeConn()
276+
221277
// Call cleanup function to unregister driver
222278
if conn.cloudSQLCleanup != nil {
223279
conn.cloudSQLCleanup()
@@ -310,6 +366,12 @@ func (conn *PostgresConn) BulkExportStream(table Table) (ds *iop.Datastream, err
310366

311367
// BulkImportStream inserts a stream into a table
312368
func (conn *PostgresConn) BulkImportStream(tableFName string, ds *iop.Datastream) (count uint64, err error) {
369+
// For Cloud SQL with pgx, use a specialized implementation
370+
if conn.isCloudSQL && conn.cloudSQLConn != nil {
371+
return conn.bulkImportStreamPgx(tableFName, ds)
372+
}
373+
374+
// Standard lib/pq implementation for normal PostgreSQL
313375
var columns iop.Columns
314376

315377
mux := ds.Context.Mux
@@ -383,7 +445,6 @@ func (conn *PostgresConn) BulkImportStream(tableFName string, ds *iop.Datastream
383445
if err != nil {
384446
ds.Context.CaptureErr(g.Error(err, "could not COPY into table %s", tableFName))
385447
ds.Context.Cancel()
386-
g.Warn(g.Marshal(err))
387448
g.Trace("error for rec: %s", g.Pretty(batch.Columns.MakeRec(row)))
388449
return g.Error(err, "could not execute statement")
389450
}
@@ -410,7 +471,107 @@ func (conn *PostgresConn) BulkImportStream(tableFName string, ds *iop.Datastream
410471

411472
ds.SetEmpty()
412473

413-
g.Trace("cOPY %d ROWS", count)
474+
g.Trace("COPY %d ROWS", count)
475+
return count, nil
476+
}
477+
478+
// bulkImportStreamPgx handles COPY for Cloud SQL using native pgx
479+
func (conn *PostgresConn) bulkImportStreamPgx(tableFName string, ds *iop.Datastream) (count uint64, err error) {
480+
var columns iop.Columns
481+
482+
mux := ds.Context.Mux
483+
484+
table, err := ParseTableName(tableFName, conn.GetType())
485+
if err != nil {
486+
err = g.Error(err, "could not get table name for import")
487+
return
488+
}
489+
490+
// Verify native pgx connection is available
491+
if conn.cloudSQLConn == nil {
492+
return count, g.Error("native pgx connection not available for Cloud SQL COPY")
493+
}
494+
495+
// set OnSchemaChange
496+
if df := ds.Df(); df != nil && cast.ToBool(conn.GetProp("adjust_column_type")) {
497+
oldOnColumnChanged := df.OnColumnChanged
498+
df.OnColumnChanged = func(col iop.Column) error {
499+
500+
// sleep to allow transaction to close
501+
time.Sleep(300 * time.Millisecond)
502+
503+
mux.Lock()
504+
defer mux.Unlock()
505+
506+
// use pre-defined function
507+
err = oldOnColumnChanged(col)
508+
if err != nil {
509+
return g.Error(err, "could not process ColumnChange for Postgres (pgx)")
510+
}
511+
512+
return nil
513+
}
514+
}
515+
516+
// close the transaction from main connection to allow cloudSQLConn connection to COPY
517+
if err = conn.Commit(); err != nil {
518+
return 0, g.Error(err, "could not commit for COPY operation")
519+
}
520+
521+
for batch := range ds.BatchChan {
522+
if batch.ColumnsChanged() || batch.IsFirst() {
523+
mux.Lock()
524+
columns, err = conn.GetColumns(tableFName, batch.Columns.Names()...)
525+
mux.Unlock()
526+
if err != nil {
527+
return count, g.Error(err, "could not get matching list of columns from table")
528+
}
529+
530+
err = batch.Shape(columns)
531+
if err != nil {
532+
return count, g.Error(err, "could not shape batch stream")
533+
}
534+
}
535+
536+
err = func() error {
537+
// Collect rows from batch for CopyFrom
538+
var rows [][]interface{}
539+
for row := range batch.Rows {
540+
rows = append(rows, row)
541+
}
542+
543+
if len(rows) == 0 {
544+
return nil
545+
}
546+
547+
// Use pgx CopyFrom with pgx.Identifier for schema.table
548+
mux.Lock()
549+
rowsInserted, err := conn.cloudSQLConn.CopyFrom(
550+
conn.Context().Ctx,
551+
pgx.Identifier{table.Schema, table.Name},
552+
columns.Names(),
553+
pgx.CopyFromRows(rows),
554+
)
555+
mux.Unlock()
556+
557+
if err != nil {
558+
return g.Error(err, "could not COPY into table %s using pgx", tableFName)
559+
}
560+
561+
count += uint64(rowsInserted)
562+
g.Trace("pgx CopyFrom inserted %d rows", rowsInserted)
563+
564+
return nil
565+
}()
566+
567+
if err != nil {
568+
return count, g.Error(err, "could not copy data (pgx)")
569+
}
570+
}
571+
572+
ds.SetEmpty()
573+
574+
g.Trace("pgx COPY %d ROWS", count)
414575
return count, nil
415576
}
416577

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ require (
4444
github.com/google/uuid v1.6.0
4545
github.com/integrii/flaggy v1.5.2
4646
github.com/itchyny/timefmt-go v0.1.6
47+
github.com/jackc/pgx/v5 v5.7.6
4748
github.com/jaswdr/faker v1.19.1
4849
github.com/jedib0t/go-pretty v4.3.0+incompatible
4950
github.com/jinzhu/copier v0.4.0
@@ -212,7 +213,6 @@ require (
212213
github.com/imdario/mergo v0.3.16 // indirect
213214
github.com/jackc/pgpassfile v1.0.0 // indirect
214215
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
215-
github.com/jackc/pgx/v5 v5.7.6 // indirect
216216
github.com/jackc/puddle/v2 v2.2.2 // indirect
217217
github.com/jcmturner/aescts/v2 v2.0.0 // indirect
218218
github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect

0 commit comments

Comments
 (0)