feat(bigquery): implement watermark column query based replication for bigquery sources - #4739
feat(bigquery): implement watermark column query based replication for bigquery sources#4739dtunikov wants to merge 18 commits into
Conversation
| BigqueryCdcEventsFunction bigquery_cdc_events_function = 10; | ||
| // the column to use as a cursor for query-based CDC replication | ||
| // required if replication_mode is BIGQUERY_REPLICATION_MODE_QUERY | ||
| string watermark_column = 11; |
There was a problem hiding this comment.
The plan is to have an option on the UI to configure watermark_column per table + to be able to type in a global watermark_column name (since in most cases the same column name is used for all tables, like created_at).
I think it can be handled completely on the UI and here we just get the column for every configured table. (e.g. we won't need to store global_watermark_column on the flow config lvl)
There was a problem hiding this comment.
another thing is that I decided not to add bigquery_ prefix to its name, because it might be used for other query-based CDC connectors in the future.
Code reviewTwo issues found. Checked for bugs and CLAUDE.md compliance. 1.
|
|
d7a13e0 to
4aa256b
Compare
| ) (map[string]time.Time, error) { | ||
| checkpointByTable := make(map[string]time.Time, len(cfg.TableMappings)) | ||
| for _, tableMapping := range cfg.TableMappings { | ||
| watermark, err := c.maxWatermarkValue(ctx, tableMapping.SourceTableIdentifier, tableMapping.GetWatermarkColumn()) |
There was a problem hiding this comment.
We could also use BQ current timestamp as an initial watermark value.
I don't have a strong preference here, both ways should work fine.
| enum BigqueryCdcEventsFunction { | ||
| BIGQUERY_CDC_EVENTS_FUNCTION_APPENDS = 0; | ||
| BIGQUERY_CDC_EVENTS_FUNCTION_CHANGES = 1; | ||
| BIGQUERY_CDC_EVENTS_FUNCTION_UNSPECIFIED = 0; |
There was a problem hiding this comment.
since it's not used in production yet - it's safe to update these enums
would make validation more predictable in peerdb and clickpipes
| buildQueryModePullQuery := func(dsTable string, watermarkColumn string, exclude map[string]struct{}) string { | ||
| col := quotedIdentifier(watermarkColumn) | ||
| return fmt.Sprintf("SELECT *%s FROM %s WHERE TIMESTAMP(%s) > @start AND TIMESTAMP(%s) <= @end ORDER BY %s", | ||
| exceptClause(exclude), dsTable, col, col, col) | ||
| } |
There was a problem hiding this comment.
nit: Could we maybe extra this to a package level function, then above for runPullQuery we can use a named type instead of the closure definition. Would also ensure that if we ever change the signature, we don't need to update it in multiple places.
There was a problem hiding this comment.
will do that in the follow-up PR where I replaced EXCEPT with explicit column selection set (since this code will change a bit there)
4955cc0 to
c0f6c21
Compare
❌ Test FailureAnalysis: Not flaky — the PR's newly added replication-mode validation rejects BIGQUERY_REPLICATION_MODE_UNSPECIFIED, deterministically breaking three Test_BigQuery_Source_CDC_Validation subtests (which pass a nil SourceConnectorConfig) identically across all three matrix jobs. |
47774c4 to
08a8139
Compare
🔄 Flaky Test DetectedAnalysis: TestPeerFlowE2ETestSuitePG_CH/Test_CTID_Inherited_Table hit the 60s "UNEXPECTED STATUS TIMEOUT STATUS_SNAPSHOT" poll cap in only the pg16 matrix job while passing on pg17 and pg18 with the same code, indicating a slow initial snapshot under CI load rather than a functional failure. ✅ Automatically retrying the workflow |
🔄 Flaky Test DetectedAnalysis: Three TestApiPg subtests on a single matrix leg all hit the same 60s "UNEXPECTED STATUS TIMEOUT STATUS_SNAPSHOT" wait within a 5-second window while the identical wait helper passed in many other tests in the same run and on the other two matrix legs, indicating a transient snapshot/worker stall rather than a code defect. ✅ Automatically retrying the workflow |
- use native bigquery.FieldType in pkg ColumnInfo
Tables without a primary key constraint have TableConstraints == nil, which panicked on tableMeta.TableConstraints.PrimaryKey.Columns.
Adding BIGQUERY_REPLICATION_MODE_UNSPECIFIED = 0 (and BIGQUERY_CDC_EVENTS_FUNCTION_UNSPECIFIED = 0) moved EVENTS and APPENDS off the proto zero value, so Test_BigQuery_Source_CDC_Validation's base config, which relied on those defaults, started tripping the new "invalid replication mode" guard in ValidateMirrorSource. Set the replication mode and CDC events function explicitly on the base config, restore it (rather than nil) after the QUERY subtest, and cover the unspecified-mode rejection directly. Also reject an unset replication mode in ValidateSourceCDC. The flow connector already guards it, but the pkg is meant to be callable from outside the flow module, and there it silently skipped every CDC check. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The regenerated TableMapping interface has a required watermarkColumn field, and TableMapRow derives from it, so both table-mapping object literals in the mirror create handlers stopped type checking. Pass the row's value through in reformattedTableMapping and default it to empty when building rows from the source schema; there is no UI control for it yet. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
9955a78 to
183fbdc
Compare
🔄 Flaky Test DetectedAnalysis: Two MariaDB→ClickHouse e2e tests hit the 60s "UNEXPECTED STATUS TIMEOUT" wait in SetupCDCFlowStatusQuery (mirrors still in STATUS_SETUP/STATUS_SNAPSHOT) within ~22s of each other on one matrix leg, while the other two matrix legs passed on the same commit and the PR only touches BigQuery code — a transient CI resource stall rather than a real bug. ✅ Automatically retrying the workflow |
| BigqueryCdcEventsFunction bigquery_cdc_events_function = 10; | ||
| // the column to use as a cursor for query-based CDC replication | ||
| // required if replication_mode is BIGQUERY_REPLICATION_MODE_QUERY | ||
| string query_cdc_watermark_column = 11; |
There was a problem hiding this comment.
no bigquery prefix since it will be used by other query-cdc connectors in the future
| } | ||
|
|
||
| // GetTables returns information about the specified tables | ||
| func GetTables( |
There was a problem hiding this comment.
if we encounter issues with datasets that have a lot of tables - we can consider using plain SELECT over bigquery system tables.
| } | ||
|
|
||
| // ColumnInfo describes a single column of a BigQuery table. | ||
| type ColumnInfo struct { |
There was a problem hiding this comment.
tried to move everything related to validation here so that it'd be re-usable from the ClickPipes
There was a problem hiding this comment.
for my own understanding: we'll also call this directly from discovery and that's why we moved this here?
| "github.com/PeerDB-io/peerdb/flow/shared/exceptions" | ||
| ) | ||
|
|
||
| func (c *BigQueryConnector) ValidateMirrorSource(ctx context.Context, cfg *protos.FlowConnectionConfigsCore) error { |
There was a problem hiding this comment.
logic was moved to pkg so that it'd be easy to share with the ClickPipes
ValidateMirrorSource just builds bqvalidate.SourceConfig and passed it to pkg functions
itsbilal
left a comment
There was a problem hiding this comment.
Mostly looks good to me, just some clarifying questions. I'll look at the tests next!
| from := fmt.Sprintf("%s FOR SYSTEM_TIME AS OF TIMESTAMP('%s UTC')", dsTable.stringQuoted(), boundLiteral) | ||
| if watermarkColumn != "" { | ||
| from = fmt.Sprintf("%s WHERE TIMESTAMP(%s) <= TIMESTAMP('%s UTC')", | ||
| dsTable.stringQuoted(), quotedIdentifier(watermarkColumn), boundLiteral) |
There was a problem hiding this comment.
watermarkColumn should never be pre-quoted right?
| } | ||
|
|
||
| // ColumnInfo describes a single column of a BigQuery table. | ||
| type ColumnInfo struct { |
There was a problem hiding this comment.
for my own understanding: we'll also call this directly from discovery and that's why we moved this here?
| _, err = it.Next() | ||
| if err != nil && !errors.Is(err, iterator.Done) { | ||
| return fmt.Errorf("failed to access staging bucket: %w", exceptions.NewBigQueryError(err)) | ||
| tablesByKey, err := bqvalidate.ValidateSourceTables(ctx, sourceConfig) |
There was a problem hiding this comment.
nit: maybe we can have a function in bqvalidate that calls both this and ValidateSourceCDC in the CDC case? Feels a little awkward to thread state from one call into another (tablesByKey).
| // SELECT ... WHERE watermark_column > lower AND watermark_column <= upper scan, | ||
| // rather than APPENDS()/CHANGES(). The initial snapshot is bounded by the | ||
| // watermark column's max value at setup time instead of FOR SYSTEM_TIME AS OF. | ||
| func (s BigQueryClickhouseSuite) Test_BigQuery_CDC_Query_Mode() { |
There was a problem hiding this comment.
nit: I see this is a convention in this file but usually we don't use underscore in test function names right? It'd just be TestBigQueryCDCQueryMode
This PR implement the third BigQuery replication mode - query based replication based on user-specified watermark/cursor column (for example
created_at).TableMappingcalled watermark_column. It's a required field for BigQuery sources with query-based replication mode.pullTableQueryfunction in biguqery/cdc.go. It fetches data from the table using a simple SQL query like:SELECT ... FROM ... WHERE col > lower AND col <= upper.SELECT max(watermark_column) FROM tableas a snapshot boundary and store it as an initial CDC checkpoint. So, in this mode we don't useFOR SYSTEM_TIME AS OF TIMESTAMP, instead we query:SELECT ... FROM ... WHERE col < max(watermark_col).Resolves DBI-1056.