Skip to content
Open
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
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
23 changes: 7 additions & 16 deletions drivers/postgres/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"

"github.com/datazip-inc/olake/constants"
Expand All @@ -21,25 +22,15 @@ func (p *Postgres) StreamIncrementalChanges(ctx context.Context, stream types.St
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()

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)
}
setter := jdbc.NewReader(ctx, incrementalQuery, func(ctx context.Context, query string, args ...any) (*sql.Rows, error) {
return p.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, p.dataTypeConverter, processFn, pgColumnSizer); err != nil {
return fmt.Errorf("incremental process error: %s", err)
}

return rows.Err()
return nil
}

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

for rows.Next() {
err := onCapture(rows)
Expand Down Expand Up @@ -87,48 +88,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 Expand Up @@ -172,8 +131,7 @@ func MapScanConcurrent(setter *Reader[*sql.Rows], converter func(value interface

select {
case <-ctx.Done():
// If the processor failed, errgroup cancels the ctx; return nil so the original error wins.
return nil
return ctx.Err()
case valuesCh <- vals:
return nil
}
Expand Down
Loading