Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
32 changes: 9 additions & 23 deletions drivers/mssql/internal/cdc.go
Original file line number Diff line number Diff line change
Expand Up @@ -357,35 +357,25 @@ func (m *MSSQL) fetchTableChangesInLSNRange(ctx context.Context, stream types.St

// Query CDC rows for this capture instance between the two LSNs.
query := jdbc.MSSQLCDCGetChangesQuery(capture.instanceName)
rows, err := m.client.QueryContext(ctx, query, fromLSNBytes, toLSNBytes)
if err != nil {
return fmt.Errorf("failed to query MSSQL CDC changes: %s", err)
}
defer rows.Close()

for rows.Next() {
// Use MapScan to properly convert data types including binary types
// TODO: check if we can use MapScanConcurrent for mssql
// rowBytes is the after-image data-column byte sum (excludes __$* metadata columns), attached to the emitted change below.
record := make(map[string]interface{})
rowBytes, err := jdbc.MapScan(rows, record, m.dataTypeConverter, mssqlCDCColumnSizer)
if err != nil {
return fmt.Errorf("failed to scan MSSQL CDC row: %s", err)
}
setter := jdbc.NewReader(ctx, query, func(ctx context.Context, q string, args ...any) (*sql.Rows, error) {
return m.client.QueryContext(ctx, q, args...)
}, fromLSNBytes, toLSNBytes)

return jdbc.MapScanConcurrent(setter, m.dataTypeConverter, func(ctx context.Context, record map[string]any, rowBytes int64) error {
// Determine operation type from SQL Server CDC operation codes.
// For updates, CDC emits "before" (3) and "after" (4); we skip "before".
var operationType string
if val, ok := record["__$operation"]; ok {
var opCode = val.(int32)
if opCode == 3 {
continue
return nil
}
operationType = operationTypeFromCDCCode(opCode)
}
extraColumns := map[string]any{
CDCStartLSN: fmt.Sprintf("%x", record["__$start_lsn"]),
CDCSeqVal: fmt.Sprintf("%x", record["__$seqval"])}
CDCSeqVal: fmt.Sprintf("%x", record["__$seqval"]),
}
// Remove metadata columns
delete(record, "__$operation")
delete(record, "__$start_lsn")
Expand All @@ -397,12 +387,8 @@ func (m *MSSQL) fetchTableChangesInLSNRange(ctx context.Context, stream types.St
extraColumns, rowBytes)); err != nil {
return fmt.Errorf("failed to process MSSQL CDC change: %s", err)
}
}

if err := rows.Err(); err != nil {
return err
}
return nil
return nil
}, mssqlCDCColumnSizer)
}

func (m *MSSQL) currentMaxLSN(ctx context.Context) (string, error) {
Expand Down
24 changes: 6 additions & 18 deletions drivers/mssql/internal/incremental.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,27 +22,15 @@ func (m *MSSQL) StreamIncrementalChanges(ctx context.Context, stream types.Strea
return fmt.Errorf("failed to build incremental condition: %s", err)
}

var rows *sql.Rows
rows, err = m.client.QueryContext(ctx, incrementalQuery, queryArgs...)
if err != nil {
return fmt.Errorf("failed to execute incremental query: %s", err)
}
defer rows.Close()

// Scan rows and process
for rows.Next() {
record := make(types.Record)
rowBytes, err := jdbc.MapScan(rows, record, m.dataTypeConverter, mssqlColumnSizer)
if err != nil {
return fmt.Errorf("failed to scan record: %s", err)
}
setter := jdbc.NewReader(ctx, incrementalQuery, func(ctx context.Context, query string, args ...any) (*sql.Rows, error) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We need to handle closing the opened rows explicitly. JDBC backfills use defer tx.Rollback() which handles this but I don't think this is the case here...

Find out if there is a way to do this from here, but if we need to change the jdbc.MapScanConcurrent then we will need to test it thoroughly

return m.client.QueryContext(ctx, query, args...)
}, queryArgs...)

if err := processFn(ctx, record, rowBytes); err != nil {
return fmt.Errorf("process error: %s", err)
}
if err := jdbc.MapScanConcurrent(setter, m.dataTypeConverter, processFn, mssqlColumnSizer); err != nil {
return fmt.Errorf("incremental process error: %s", err)
}

return rows.Err()
return nil
}

func (m *MSSQL) FetchMaxCursorValues(ctx context.Context, stream types.StreamInterface) (any, any, error) {
Expand Down
24 changes: 6 additions & 18 deletions drivers/mysql/internal/incremental.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,27 +22,15 @@ func (m *MySQL) StreamIncrementalChanges(ctx context.Context, stream types.Strea
return fmt.Errorf("failed to build incremental condition: %s", err)
}

var rows *sql.Rows
rows, err = m.client.QueryContext(ctx, incrementalQuery, queryArgs...)
if err != nil {
return fmt.Errorf("failed to execute incremental query: %s", err)
}
defer rows.Close()

// Scan rows and process
for rows.Next() {
record := make(types.Record)
rowBytes, err := jdbc.MapScan(rows, record, m.dataTypeConverter, mysqlColumnSizer)
if err != nil {
return fmt.Errorf("failed to scan record: %s", err)
}
setter := jdbc.NewReader(ctx, incrementalQuery, func(ctx context.Context, query string, args ...any) (*sql.Rows, error) {
return m.client.QueryContext(ctx, query, args...)
}, queryArgs...)

if err := processFn(ctx, record, rowBytes); err != nil {
return fmt.Errorf("process error: %s", err)
}
if err := jdbc.MapScanConcurrent(setter, m.dataTypeConverter, processFn, mysqlColumnSizer); err != nil {
return fmt.Errorf("incremental process error: %s", err)
}

return rows.Err()
return nil
}

func (m *MySQL) FetchMaxCursorValues(ctx context.Context, stream types.StreamInterface) (any, any, error) {
Expand Down
23 changes: 7 additions & 16 deletions drivers/oracle/internal/incremental.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package driver

import (
"context"
"database/sql"
"fmt"
"strings"
"time"
Expand All @@ -26,24 +27,14 @@ func (o *Oracle) StreamIncrementalChanges(ctx context.Context, stream types.Stre
return fmt.Errorf("failed to build incremental condition: %s", err)
}

rows, err := o.client.QueryContext(ctx, incrementalQuery, queryArgs...)
if err != nil {
return fmt.Errorf("failed to execute incremental query: %s", err)
}
defer rows.Close()

for rows.Next() {
record := make(types.Record)
rowBytes, err := jdbc.MapScan(rows, record, o.dataTypeConverter, oracleColumnSizer)
if err != nil {
return fmt.Errorf("failed to scan record: %s", err)
}
setter := jdbc.NewReader(ctx, incrementalQuery, func(ctx context.Context, query string, args ...any) (*sql.Rows, error) {
return o.client.QueryContext(ctx, query, args...)
}, queryArgs...)

if err := processFn(ctx, record, rowBytes); err != nil {
return fmt.Errorf("process error: %s", err)
}
if err := jdbc.MapScanConcurrent(setter, o.dataTypeConverter, processFn, oracleColumnSizer); err != nil {
return fmt.Errorf("incremental process error: %s", err)
}
return rows.Err()
return nil
}

func (o *Oracle) FetchMaxCursorValues(ctx context.Context, stream types.StreamInterface) (any, any, error) {
Expand Down
25 changes: 8 additions & 17 deletions drivers/postgres/internal/incremental.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import (
"context"
"database/sql"
"fmt"

"github.com/datazip-inc/olake/constants"
Expand All @@ -21,25 +22,15 @@
return fmt.Errorf("failed to build incremental condition: %s", err)
}

rows, err := p.client.QueryContext(ctx, incrementalQuery, queryArgs...)
if err != nil {
return fmt.Errorf("failed to execute incremental query: %s", err)
}
defer rows.Close()
setter := jdbc.NewReader(ctx, incrementalQuery, func(ctx context.Context, query string, args ...any) (*sql.Rows, error) {
return p.client.QueryContext(ctx, query, args...)
}, queryArgs...)

for rows.Next() {
record := make(types.Record)
rowBytes, err := jdbc.MapScan(rows, record, p.dataTypeConverter, pgColumnSizer)
if err != nil {
return fmt.Errorf("failed to scan record: %s", err)
}

if err := processFn(ctx, record, rowBytes); err != nil {
return fmt.Errorf("process error: %s", err)
}
if err := jdbc.MapScanConcurrent(setter, p.dataTypeConverter, processFn, pgColumnSizer); err != nil {
return fmt.Errorf("incremental process error: %s", err)
}

return rows.Err()

Check failure on line 32 in drivers/postgres/internal/incremental.go

View workflow job for this annotation

GitHub Actions / golangci-lint

File is not properly formatted (gofmt)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Redundant line here, install go formatter in your IDE to avoid such mistakes...

return nil
}

func (p *Postgres) FetchMaxCursorValues(ctx context.Context, stream types.StreamInterface) (any, any, error) {
Expand Down
45 changes: 3 additions & 42 deletions pkg/jdbc/reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ func (o *Reader[T]) Capture(onCapture func(T) error) error {
if err != nil {
return err
}
if closer, ok := any(rows).(interface{ Close() error }); ok {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Instead of doing this, we should just add Close() in iterable as we already use sql.Rows which needs closing so its better to have it in the interface by design rather than guessing

defer func() { _ = closer.Close() }()
}

for rows.Next() {
err := onCapture(rows)
Expand Down Expand Up @@ -87,48 +90,6 @@ func normalizeDataTypeAndConvert(rawData any, colType *sql.ColumnType, converter
return conv, nil
}

// TODO: Use MapScanConcurrent instead of MapScan for incremental as well
//
// MapScan scans the current row into dest and returns the row's source-DB byte
// size. columnSizer maps a SQL column type to a function that sizes one raw
// (pre-conversion) value of that column; the size is summed inline in the scan
// loop. NULL values carry no data and are skipped (0 bytes).
func MapScan(rows *sql.Rows, dest map[string]any, converter func(value interface{}, columnType string) (interface{}, error), columnSizer func(colType *sql.ColumnType) func(v any) int64) (int64, error) {
columns, colTypes, err := getColumnMetadata(rows)
if err != nil {
return 0, err
}

scanValues := make([]any, len(columns))
for i := range scanValues {
scanValues[i] = new(any) // Allocate pointers for scanning
}

if err := rows.Scan(scanValues...); err != nil {
return 0, err
}

var rowBytes int64
for i, col := range columns {
rawData := *(scanValues[i].(*any)) // Dereference pointer before storing
// If rawData is nil, no byte is added
if rawData != nil {
rowBytes += columnSizer(colTypes[i])(rawData)
}
if converter != nil {
conv, err := normalizeDataTypeAndConvert(rawData, colTypes[i], converter)
if err != nil {
return 0, err
}
dest[col] = conv
} else {
dest[col] = rawData
}
}

return rowBytes, nil
}

// MapScanConcurrent scans rows concurrently using a producer/consumer pattern.
// columnSizer maps a SQL column type to a function that sizes one raw
// (pre-conversion) value of that column. Since column types are constant for the
Expand Down
Loading
Loading