From d8a10c3be608e854f05b4b764305771a6ef493ee Mon Sep 17 00:00:00 2001 From: Amogh Bharadwaj Date: Tue, 9 Apr 2024 00:51:15 +0530 Subject: [PATCH 01/18] Move datatype folders to super folder (#1583) Moves geo, hstore, interval and numeric files where we do special operations on those types, to a `datatypes` folder --- flow/connectors/bigquery/bigquery.go | 2 +- flow/connectors/bigquery/qrep_avro_sync.go | 2 +- flow/connectors/clickhouse/normalize.go | 8 ++++---- flow/connectors/postgres/cdc.go | 2 +- flow/connectors/postgres/client.go | 2 +- flow/connectors/postgres/qrep_query_executor.go | 7 +++---- flow/connectors/postgres/qvalue_convert.go | 4 ++-- flow/connectors/snowflake/merge_stmt_generator.go | 2 +- flow/connectors/snowflake/snowflake.go | 2 +- flow/{geo => datatypes}/geo.go | 2 +- flow/{hstore => datatypes}/hstore.go | 2 +- flow/{hstore => datatypes}/hstore_test.go | 2 +- flow/{interval => datatypes}/interval.go | 2 +- flow/{model/numeric/scale.go => datatypes/numeric.go} | 2 +- flow/model/qrecord_batch.go | 2 +- flow/model/qvalue/avro_converter.go | 4 ++-- flow/model/qvalue/dwh.go | 2 +- flow/model/qvalue/equals.go | 4 ++-- flow/model/record_items.go | 4 ++-- 19 files changed, 28 insertions(+), 29 deletions(-) rename flow/{geo => datatypes}/geo.go (98%) rename flow/{hstore => datatypes}/hstore.go (99%) rename flow/{hstore => datatypes}/hstore_test.go (99%) rename flow/{interval => datatypes}/interval.go (93%) rename flow/{model/numeric/scale.go => datatypes/numeric.go} (96%) diff --git a/flow/connectors/bigquery/bigquery.go b/flow/connectors/bigquery/bigquery.go index f1ff016b87..b30625286a 100644 --- a/flow/connectors/bigquery/bigquery.go +++ b/flow/connectors/bigquery/bigquery.go @@ -19,10 +19,10 @@ import ( metadataStore "github.com/PeerDB-io/peer-flow/connectors/external_metadata" "github.com/PeerDB-io/peer-flow/connectors/utils" + numeric "github.com/PeerDB-io/peer-flow/datatypes" "github.com/PeerDB-io/peer-flow/generated/protos" "github.com/PeerDB-io/peer-flow/logger" "github.com/PeerDB-io/peer-flow/model" - "github.com/PeerDB-io/peer-flow/model/numeric" "github.com/PeerDB-io/peer-flow/model/qvalue" "github.com/PeerDB-io/peer-flow/peerdbenv" "github.com/PeerDB-io/peer-flow/shared" diff --git a/flow/connectors/bigquery/qrep_avro_sync.go b/flow/connectors/bigquery/qrep_avro_sync.go index acfd7fc9b1..19548a3b59 100644 --- a/flow/connectors/bigquery/qrep_avro_sync.go +++ b/flow/connectors/bigquery/qrep_avro_sync.go @@ -14,9 +14,9 @@ import ( "go.temporal.io/sdk/activity" avro "github.com/PeerDB-io/peer-flow/connectors/utils/avro" + numeric "github.com/PeerDB-io/peer-flow/datatypes" "github.com/PeerDB-io/peer-flow/generated/protos" "github.com/PeerDB-io/peer-flow/model" - "github.com/PeerDB-io/peer-flow/model/numeric" "github.com/PeerDB-io/peer-flow/model/qvalue" "github.com/PeerDB-io/peer-flow/shared" ) diff --git a/flow/connectors/clickhouse/normalize.go b/flow/connectors/clickhouse/normalize.go index 148ab771fd..9237a18d91 100644 --- a/flow/connectors/clickhouse/normalize.go +++ b/flow/connectors/clickhouse/normalize.go @@ -8,9 +8,9 @@ import ( "strconv" "strings" + "github.com/PeerDB-io/peer-flow/datatypes" "github.com/PeerDB-io/peer-flow/generated/protos" "github.com/PeerDB-io/peer-flow/model" - "github.com/PeerDB-io/peer-flow/model/numeric" "github.com/PeerDB-io/peer-flow/model/qvalue" ) @@ -84,10 +84,10 @@ func generateCreateTableSQLForNormalizedTable( switch colType { case qvalue.QValueKindNumeric: - precision, scale := numeric.ParseNumericTypmod(column.TypeModifier) + precision, scale := datatypes.ParseNumericTypmod(column.TypeModifier) if column.TypeModifier == -1 || precision > 76 || scale > precision { - precision = numeric.PeerDBClickhousePrecision - scale = numeric.PeerDBClickhouseScale + precision = datatypes.PeerDBClickhousePrecision + scale = datatypes.PeerDBClickhouseScale } stmtBuilder.WriteString(fmt.Sprintf("`%s` DECIMAL(%d, %d), ", colName, precision, scale)) diff --git a/flow/connectors/postgres/cdc.go b/flow/connectors/postgres/cdc.go index e31ecda8a2..306e783dd3 100644 --- a/flow/connectors/postgres/cdc.go +++ b/flow/connectors/postgres/cdc.go @@ -19,8 +19,8 @@ import ( "github.com/PeerDB-io/peer-flow/connectors/utils" "github.com/PeerDB-io/peer-flow/connectors/utils/cdc_records" + geo "github.com/PeerDB-io/peer-flow/datatypes" "github.com/PeerDB-io/peer-flow/generated/protos" - "github.com/PeerDB-io/peer-flow/geo" "github.com/PeerDB-io/peer-flow/logger" "github.com/PeerDB-io/peer-flow/model" "github.com/PeerDB-io/peer-flow/model/qvalue" diff --git a/flow/connectors/postgres/client.go b/flow/connectors/postgres/client.go index e147b8f6f2..8bb172c746 100644 --- a/flow/connectors/postgres/client.go +++ b/flow/connectors/postgres/client.go @@ -13,9 +13,9 @@ import ( "github.com/lib/pq/oid" "github.com/PeerDB-io/peer-flow/connectors/utils" + numeric "github.com/PeerDB-io/peer-flow/datatypes" "github.com/PeerDB-io/peer-flow/generated/protos" "github.com/PeerDB-io/peer-flow/model" - "github.com/PeerDB-io/peer-flow/model/numeric" "github.com/PeerDB-io/peer-flow/shared" ) diff --git a/flow/connectors/postgres/qrep_query_executor.go b/flow/connectors/postgres/qrep_query_executor.go index ce50a6c8a1..c77eb3b054 100644 --- a/flow/connectors/postgres/qrep_query_executor.go +++ b/flow/connectors/postgres/qrep_query_executor.go @@ -10,9 +10,8 @@ import ( "github.com/jackc/pgx/v5/pgtype" "go.temporal.io/sdk/log" - "github.com/PeerDB-io/peer-flow/geo" + datatypes "github.com/PeerDB-io/peer-flow/datatypes" "github.com/PeerDB-io/peer-flow/model" - "github.com/PeerDB-io/peer-flow/model/numeric" "github.com/PeerDB-io/peer-flow/model/qvalue" "github.com/PeerDB-io/peer-flow/shared" ) @@ -79,7 +78,7 @@ func (qe *QRepQueryExecutor) fieldDescriptionsToSchema(fds []pgconn.FieldDescrip // TODO fix this. cnullable := true if ctype == qvalue.QValueKindNumeric { - precision, scale := numeric.ParseNumericTypmod(fd.TypeModifier) + precision, scale := datatypes.ParseNumericTypmod(fd.TypeModifier) qfields[i] = qvalue.QField{ Name: cname, Type: ctype, @@ -426,7 +425,7 @@ func (qe *QRepQueryExecutor) mapRowToQRecord( switch customQKind { case qvalue.QValueKindGeography, qvalue.QValueKindGeometry: wkbString, ok := values[i].(string) - wkt, err := geo.GeoValidate(wkbString) + wkt, err := datatypes.GeoValidate(wkbString) if err != nil || !ok { record[i] = qvalue.QValueNull(qvalue.QValueKindGeography) } else if customQKind == qvalue.QValueKindGeography { diff --git a/flow/connectors/postgres/qvalue_convert.go b/flow/connectors/postgres/qvalue_convert.go index 01164672e8..7052b064d4 100644 --- a/flow/connectors/postgres/qvalue_convert.go +++ b/flow/connectors/postgres/qvalue_convert.go @@ -13,7 +13,7 @@ import ( "github.com/lib/pq/oid" "github.com/shopspring/decimal" - peerdb_interval "github.com/PeerDB-io/peer-flow/interval" + datatypes "github.com/PeerDB-io/peer-flow/datatypes" "github.com/PeerDB-io/peer-flow/model/qvalue" "github.com/PeerDB-io/peer-flow/shared" ) @@ -229,7 +229,7 @@ func parseFieldFromQValueKind(qvalueKind qvalue.QValueKind, value interface{}) ( return qvalue.QValueTimestampTZ{Val: timestamp}, nil case qvalue.QValueKindInterval: intervalObject := value.(pgtype.Interval) - var interval peerdb_interval.PeerDBInterval + var interval datatypes.PeerDBInterval interval.Hours = int(intervalObject.Microseconds / 3600000000) interval.Minutes = int((intervalObject.Microseconds % 3600000000) / 60000000) interval.Seconds = float64(intervalObject.Microseconds%60000000) / 1000000.0 diff --git a/flow/connectors/snowflake/merge_stmt_generator.go b/flow/connectors/snowflake/merge_stmt_generator.go index ab95b22da3..2292855f0c 100644 --- a/flow/connectors/snowflake/merge_stmt_generator.go +++ b/flow/connectors/snowflake/merge_stmt_generator.go @@ -5,8 +5,8 @@ import ( "strings" "github.com/PeerDB-io/peer-flow/connectors/utils" + numeric "github.com/PeerDB-io/peer-flow/datatypes" "github.com/PeerDB-io/peer-flow/generated/protos" - "github.com/PeerDB-io/peer-flow/model/numeric" "github.com/PeerDB-io/peer-flow/model/qvalue" "github.com/PeerDB-io/peer-flow/shared" ) diff --git a/flow/connectors/snowflake/snowflake.go b/flow/connectors/snowflake/snowflake.go index c66f8a296f..c78a438a3d 100644 --- a/flow/connectors/snowflake/snowflake.go +++ b/flow/connectors/snowflake/snowflake.go @@ -21,10 +21,10 @@ import ( metadataStore "github.com/PeerDB-io/peer-flow/connectors/external_metadata" "github.com/PeerDB-io/peer-flow/connectors/utils" + numeric "github.com/PeerDB-io/peer-flow/datatypes" "github.com/PeerDB-io/peer-flow/generated/protos" "github.com/PeerDB-io/peer-flow/logger" "github.com/PeerDB-io/peer-flow/model" - "github.com/PeerDB-io/peer-flow/model/numeric" "github.com/PeerDB-io/peer-flow/model/qvalue" "github.com/PeerDB-io/peer-flow/peerdbenv" "github.com/PeerDB-io/peer-flow/shared" diff --git a/flow/geo/geo.go b/flow/datatypes/geo.go similarity index 98% rename from flow/geo/geo.go rename to flow/datatypes/geo.go index 7e35d9bfe6..83637d69e6 100644 --- a/flow/geo/geo.go +++ b/flow/datatypes/geo.go @@ -1,4 +1,4 @@ -package geo +package datatypes import ( "encoding/hex" diff --git a/flow/hstore/hstore.go b/flow/datatypes/hstore.go similarity index 99% rename from flow/hstore/hstore.go rename to flow/datatypes/hstore.go index aca1412ba0..a7bd73f3cf 100644 --- a/flow/hstore/hstore.go +++ b/flow/datatypes/hstore.go @@ -5,7 +5,7 @@ https://github.com/postgres/postgres/blob/bea18b1c949145ba2ca79d4765dba3cc9494a4 This package is an implementation based on the above code. It's simplified to only parse the subset which `hstore_out` outputs. */ -package hstore_util +package datatypes import ( "encoding/json" diff --git a/flow/hstore/hstore_test.go b/flow/datatypes/hstore_test.go similarity index 99% rename from flow/hstore/hstore_test.go rename to flow/datatypes/hstore_test.go index 721a6bf523..1cd78386cf 100644 --- a/flow/hstore/hstore_test.go +++ b/flow/datatypes/hstore_test.go @@ -1,4 +1,4 @@ -package hstore_util +package datatypes import ( "testing" diff --git a/flow/interval/interval.go b/flow/datatypes/interval.go similarity index 93% rename from flow/interval/interval.go rename to flow/datatypes/interval.go index 79fbdc3ecf..c3df44d4ab 100644 --- a/flow/interval/interval.go +++ b/flow/datatypes/interval.go @@ -1,4 +1,4 @@ -package peerdb_interval +package datatypes type PeerDBInterval struct { Hours int `json:"hours,omitempty"` diff --git a/flow/model/numeric/scale.go b/flow/datatypes/numeric.go similarity index 96% rename from flow/model/numeric/scale.go rename to flow/datatypes/numeric.go index 3e0d1381e9..8eb5703845 100644 --- a/flow/model/numeric/scale.go +++ b/flow/datatypes/numeric.go @@ -1,4 +1,4 @@ -package numeric +package datatypes const ( PeerDBNumericPrecision = 38 diff --git a/flow/model/qrecord_batch.go b/flow/model/qrecord_batch.go index 4340fbdc00..bd4b9c30a3 100644 --- a/flow/model/qrecord_batch.go +++ b/flow/model/qrecord_batch.go @@ -10,7 +10,7 @@ import ( "github.com/google/uuid" "github.com/jackc/pgx/v5/pgtype" - "github.com/PeerDB-io/peer-flow/geo" + geo "github.com/PeerDB-io/peer-flow/datatypes" "github.com/PeerDB-io/peer-flow/model/qvalue" ) diff --git a/flow/model/qvalue/avro_converter.go b/flow/model/qvalue/avro_converter.go index 92e06f36e0..03db6eaf3c 100644 --- a/flow/model/qvalue/avro_converter.go +++ b/flow/model/qvalue/avro_converter.go @@ -13,8 +13,8 @@ import ( "github.com/shopspring/decimal" "go.temporal.io/sdk/log" + "github.com/PeerDB-io/peer-flow/datatypes" "github.com/PeerDB-io/peer-flow/generated/protos" - hstore_util "github.com/PeerDB-io/peer-flow/hstore" ) // https://avro.apache.org/docs/1.11.0/spec.html @@ -544,7 +544,7 @@ func (c *QValueAvroConverter) processArrayDate(arrayDate []time.Time) interface{ } func (c *QValueAvroConverter) processHStore(hstore string) (interface{}, error) { - jsonString, err := hstore_util.ParseHstore(hstore) + jsonString, err := datatypes.ParseHstore(hstore) if err != nil { return "", fmt.Errorf("cannot parse %s: %w", hstore, err) } diff --git a/flow/model/qvalue/dwh.go b/flow/model/qvalue/dwh.go index cd21e3182d..7e04803958 100644 --- a/flow/model/qvalue/dwh.go +++ b/flow/model/qvalue/dwh.go @@ -5,8 +5,8 @@ import ( "go.temporal.io/sdk/log" + numeric "github.com/PeerDB-io/peer-flow/datatypes" "github.com/PeerDB-io/peer-flow/generated/protos" - "github.com/PeerDB-io/peer-flow/model/numeric" ) func DetermineNumericSettingForDWH(precision int16, scale int16, dwh protos.DBType) (int16, int16) { diff --git a/flow/model/qvalue/equals.go b/flow/model/qvalue/equals.go index 713f71134a..3f7253ddc7 100644 --- a/flow/model/qvalue/equals.go +++ b/flow/model/qvalue/equals.go @@ -14,7 +14,7 @@ import ( "github.com/shopspring/decimal" geom "github.com/twpayne/go-geos" - hstore_util "github.com/PeerDB-io/peer-flow/hstore" + "github.com/PeerDB-io/peer-flow/datatypes" ) func valueEmpty(value any) bool { @@ -174,7 +174,7 @@ func compareHStore(str1 string, value2 interface{}) bool { if str1 == str2 { return true } - parsedHStore1, err := hstore_util.ParseHstore(str1) + parsedHStore1, err := datatypes.ParseHstore(str1) if err != nil { panic(err) } diff --git a/flow/model/record_items.go b/flow/model/record_items.go index b7d18a86fa..d6702cf777 100644 --- a/flow/model/record_items.go +++ b/flow/model/record_items.go @@ -7,7 +7,7 @@ import ( "github.com/google/uuid" - hstore_util "github.com/PeerDB-io/peer-flow/hstore" + "github.com/PeerDB-io/peer-flow/datatypes" "github.com/PeerDB-io/peer-flow/model/qvalue" ) @@ -132,7 +132,7 @@ func (r RecordItems) toMap(hstoreAsJSON bool, opts ToJSONOptions) (map[string]in if !hstoreAsJSON { jsonStruct[col] = hstoreVal } else { - jsonVal, err := hstore_util.ParseHstore(hstoreVal) + jsonVal, err := datatypes.ParseHstore(hstoreVal) if err != nil { return nil, fmt.Errorf("unable to convert hstore column %s to json for value %T: %w", col, v, err) } From e01406a4c6355a3a838404fb41a763c440732f21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= Date: Tue, 9 Apr 2024 14:57:18 +0000 Subject: [PATCH 02/18] Remove unused [16]byte cases, fix macaddr (#1585) Follow up #1412 --- flow/connectors/postgres/qvalue_convert.go | 9 +++------ flow/e2e/test_utils.go | 7 ++++--- flow/model/qvalue/avro_converter.go | 2 +- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/flow/connectors/postgres/qvalue_convert.go b/flow/connectors/postgres/qvalue_convert.go index 7052b064d4..a2d64c6f7e 100644 --- a/flow/connectors/postgres/qvalue_convert.go +++ b/flow/connectors/postgres/qvalue_convert.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "net" "net/netip" "strings" "time" @@ -316,8 +317,6 @@ func parseFieldFromQValueKind(qvalueKind qvalue.QValueKind, value interface{}) ( switch v := value.(type) { case string: return qvalue.QValueINET{Val: v}, nil - case [16]byte: - return qvalue.QValueINET{Val: string(v[:])}, nil case netip.Prefix: return qvalue.QValueINET{Val: v.String()}, nil default: @@ -327,8 +326,6 @@ func parseFieldFromQValueKind(qvalueKind qvalue.QValueKind, value interface{}) ( switch v := value.(type) { case string: return qvalue.QValueCIDR{Val: v}, nil - case [16]byte: - return qvalue.QValueCIDR{Val: string(v[:])}, nil case netip.Prefix: return qvalue.QValueCIDR{Val: v.String()}, nil default: @@ -338,8 +335,8 @@ func parseFieldFromQValueKind(qvalueKind qvalue.QValueKind, value interface{}) ( switch v := value.(type) { case string: return qvalue.QValueMacaddr{Val: v}, nil - case [16]byte: - return qvalue.QValueMacaddr{Val: string(v[:])}, nil + case net.HardwareAddr: + return qvalue.QValueCIDR{Val: v.String()}, nil default: return nil, fmt.Errorf("failed to parse MACADDR: %v", value) } diff --git a/flow/e2e/test_utils.go b/flow/e2e/test_utils.go index 74e24ac2d4..74cee5553b 100644 --- a/flow/e2e/test_utils.go +++ b/flow/e2e/test_utils.go @@ -275,6 +275,7 @@ func CreateTableForQRep(conn *pgx.Conn, suffix string, tableName string) error { "myreal3 REAL", "myinet INET", "mycidr CIDR", + "mymac MACADDR", } tblFieldStr := strings.Join(tblFields, ",") var pgErr *pgconn.PgError @@ -332,8 +333,8 @@ func PopulateSourceTable(conn *pgx.Conn, suffix string, tableName string, rowCou 'LINESTRING(0 0, 1 1, 2 2)', 'LINESTRING(-74.0060 40.7128, -73.9352 40.7306, -73.9123 40.7831)', 'POLYGON((0 0, 0 1, 1 1, 1 0, 0 0))','POLYGON((0 0, 0 1, 1 1, 1 0, 0 0))', - 3.14159, 1, 1.0, - '10.0.0.0/32', '1.1.10.2'::cidr + pi(), 1, 1.0, + '10.0.0.0/32', '1.1.10.2'::cidr, 'a1:b2:c3:d4:e5:f6' )`, id, uuid.New().String(), uuid.New().String(), uuid.New().String(), uuid.New().String(), uuid.New().String(), uuid.New().String()) @@ -352,7 +353,7 @@ func PopulateSourceTable(conn *pgx.Conn, suffix string, tableName string, rowCou my_time, my_mood, myh, "geometryPoint", geography_point,geometry_linestring, geography_linestring,geometry_polygon, geography_polygon, myreal, myreal2, myreal3, - myinet, mycidr + myinet, mycidr, mymac ) VALUES %s; `, suffix, tableName, strings.Join(rows, ","))) if err != nil { diff --git a/flow/model/qvalue/avro_converter.go b/flow/model/qvalue/avro_converter.go index 03db6eaf3c..763009b2c6 100644 --- a/flow/model/qvalue/avro_converter.go +++ b/flow/model/qvalue/avro_converter.go @@ -63,7 +63,7 @@ func GetAvroSchemaFromQValueKind(kind QValueKind, targetDWH protos.DBType, preci switch kind { case QValueKindString: return "string", nil - case QValueKindQChar, QValueKindCIDR, QValueKindINET: + case QValueKindQChar, QValueKindCIDR, QValueKindINET, QValueKindMacaddr: return "string", nil case QValueKindInterval: return "string", nil From 90dc6460d381e4122d2f583197dd60c9ff67f795 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= Date: Tue, 9 Apr 2024 15:10:37 +0000 Subject: [PATCH 03/18] Misc cleanup (#1586) From working on #1565 --- flow/connectors/postgres/qvalue_convert.go | 29 +++++++++++----------- flow/connectors/utils/gcp.go | 11 +++----- flow/model/model.go | 15 ----------- flow/model/record.go | 15 +++++++++++ 4 files changed, 32 insertions(+), 38 deletions(-) diff --git a/flow/connectors/postgres/qvalue_convert.go b/flow/connectors/postgres/qvalue_convert.go index a2d64c6f7e..eb71327121 100644 --- a/flow/connectors/postgres/qvalue_convert.go +++ b/flow/connectors/postgres/qvalue_convert.go @@ -89,23 +89,24 @@ func (c *PostgresConnector) postgresOIDToQValueKind(recvOID uint32) qvalue.QValu typeName, ok := pgtype.NewMap().TypeForOID(recvOID) if !ok { // workaround for some types not being defined by pgtype - if recvOID == uint32(oid.T_timetz) { + switch recvOID { + case uint32(oid.T_timetz): return qvalue.QValueKindTimeTZ - } else if recvOID == uint32(oid.T_xml) { // XML + case uint32(oid.T_xml): return qvalue.QValueKindString - } else if recvOID == uint32(oid.T_money) { // MONEY + case uint32(oid.T_money): return qvalue.QValueKindString - } else if recvOID == uint32(oid.T_txid_snapshot) { // TXID_SNAPSHOT + case uint32(oid.T_txid_snapshot): return qvalue.QValueKindString - } else if recvOID == uint32(oid.T_tsvector) { // TSVECTOR + case uint32(oid.T_tsvector): return qvalue.QValueKindString - } else if recvOID == uint32(oid.T_tsquery) { // TSQUERY + case uint32(oid.T_tsquery): return qvalue.QValueKindString - } else if recvOID == uint32(oid.T_point) { // POINT + case uint32(oid.T_point): return qvalue.QValueKindPoint + default: + return qvalue.QValueKindInvalid } - - return qvalue.QValueKindInvalid } else { _, warned := c.hushWarnOID[recvOID] if !warned { @@ -446,16 +447,14 @@ func numericToDecimal(numVal pgtype.Numeric) (qvalue.QValue, error) { } func customTypeToQKind(typeName string) qvalue.QValueKind { - var qValueKind qvalue.QValueKind switch typeName { case "geometry": - qValueKind = qvalue.QValueKindGeometry + return qvalue.QValueKindGeometry case "geography": - qValueKind = qvalue.QValueKindGeography + return qvalue.QValueKindGeography case "hstore": - qValueKind = qvalue.QValueKindHStore + return qvalue.QValueKindHStore default: - qValueKind = qvalue.QValueKindString + return qvalue.QValueKindString } - return qValueKind } diff --git a/flow/connectors/utils/gcp.go b/flow/connectors/utils/gcp.go index 09aa19c8a7..4320e886a2 100644 --- a/flow/connectors/utils/gcp.go +++ b/flow/connectors/utils/gcp.go @@ -53,14 +53,9 @@ func (sa *GcpServiceAccount) Validate() error { return nil } -// Return GcpServiceAccount as JSON byte array -func (sa *GcpServiceAccount) ToJSON() ([]byte, error) { - return json.Marshal(sa) -} - // CreateBigQueryClient creates a new BigQuery client from a GcpServiceAccount. func (sa *GcpServiceAccount) CreateBigQueryClient(ctx context.Context) (*bigquery.Client, error) { - saJSON, err := sa.ToJSON() + saJSON, err := json.Marshal(sa) if err != nil { return nil, fmt.Errorf("failed to get json: %v", err) } @@ -79,7 +74,7 @@ func (sa *GcpServiceAccount) CreateBigQueryClient(ctx context.Context) (*bigquer // CreateStorageClient creates a new Storage client from a GcpServiceAccount. func (sa *GcpServiceAccount) CreateStorageClient(ctx context.Context) (*storage.Client, error) { - saJSON, err := sa.ToJSON() + saJSON, err := json.Marshal(sa) if err != nil { return nil, fmt.Errorf("failed to get json: %v", err) } @@ -97,7 +92,7 @@ func (sa *GcpServiceAccount) CreateStorageClient(ctx context.Context) (*storage. // CreatePubSubClient creates a new PubSub client from a GcpServiceAccount. func (sa *GcpServiceAccount) CreatePubSubClient(ctx context.Context) (*pubsub.Client, error) { - saJSON, err := sa.ToJSON() + saJSON, err := json.Marshal(sa) if err != nil { return nil, fmt.Errorf("failed to get json: %v", err) } diff --git a/flow/model/model.go b/flow/model/model.go index 53f4f28652..2dfb08e36e 100644 --- a/flow/model/model.go +++ b/flow/model/model.go @@ -66,21 +66,6 @@ func NewToJSONOptions(unnestCols []string, hstoreAsJSON bool) ToJSONOptions { } } -func (r *InsertRecord) GetSourceTableName() string { - return r.SourceTableName -} - -func (r *InsertRecord) GetItems() RecordItems { - return r.Items -} - -func (r *InsertRecord) PopulateCountMap(mapOfCounts map[string]*RecordTypeCounts) { - recordCount, ok := mapOfCounts[r.DestinationTableName] - if ok { - recordCount.InsertCount++ - } -} - type TableWithPkey struct { TableName string // SHA256 hash of the primary key columns diff --git a/flow/model/record.go b/flow/model/record.go index 8d7f20e484..85e8723853 100644 --- a/flow/model/record.go +++ b/flow/model/record.go @@ -47,6 +47,21 @@ func (r *InsertRecord) GetDestinationTableName() string { return r.DestinationTableName } +func (r *InsertRecord) GetSourceTableName() string { + return r.SourceTableName +} + +func (r *InsertRecord) GetItems() RecordItems { + return r.Items +} + +func (r *InsertRecord) PopulateCountMap(mapOfCounts map[string]*RecordTypeCounts) { + recordCount, ok := mapOfCounts[r.DestinationTableName] + if ok { + recordCount.InsertCount++ + } +} + type UpdateRecord struct { // OldItems is a map of column name to value. OldItems RecordItems From 144236c0312b669e0f0d8290a3f9757fa590b7c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= Date: Tue, 9 Apr 2024 15:22:29 +0000 Subject: [PATCH 04/18] grpc: use NewClient (#1587) DialContext is deprecated, WithBlock doesn't work with NewClient, nor is it recommended: https://github.com/grpc/grpc-go/blob/master/Documentation/anti-patterns.md --- flow/cmd/api.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/flow/cmd/api.go b/flow/cmd/api.go index f556012ba8..8b8be80c6e 100644 --- a/flow/cmd/api.go +++ b/flow/cmd/api.go @@ -39,11 +39,8 @@ type APIServerParams struct { // setupGRPCGatewayServer sets up the grpc-gateway mux func setupGRPCGatewayServer(args *APIServerParams) (*http.Server, error) { - //nolint:staticcheck - conn, err := grpc.DialContext( - context.Background(), + conn, err := grpc.NewClient( fmt.Sprintf("0.0.0.0:%d", args.Port), - grpc.WithBlock(), grpc.WithTransportCredentials(insecure.NewCredentials()), ) if err != nil { From 1a63ca7b562ca1f1735719ea87b7018024241613 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= Date: Tue, 9 Apr 2024 17:41:04 +0000 Subject: [PATCH 05/18] Fix macaddr fix (#1589) Accidentally mapped to QValueCIDR instead of QValueMacaddr in #1585 --- flow/connectors/postgres/qvalue_convert.go | 2 +- flow/model/qrecord_batch.go | 2 +- flow/model/qvalue/equals.go | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/flow/connectors/postgres/qvalue_convert.go b/flow/connectors/postgres/qvalue_convert.go index eb71327121..2a03a1a7d4 100644 --- a/flow/connectors/postgres/qvalue_convert.go +++ b/flow/connectors/postgres/qvalue_convert.go @@ -337,7 +337,7 @@ func parseFieldFromQValueKind(qvalueKind qvalue.QValueKind, value interface{}) ( case string: return qvalue.QValueMacaddr{Val: v}, nil case net.HardwareAddr: - return qvalue.QValueCIDR{Val: v.String()}, nil + return qvalue.QValueMacaddr{Val: v.String()}, nil default: return nil, fmt.Errorf("failed to parse MACADDR: %v", value) } diff --git a/flow/model/qrecord_batch.go b/flow/model/qrecord_batch.go index bd4b9c30a3..7c28c64d89 100644 --- a/flow/model/qrecord_batch.go +++ b/flow/model/qrecord_batch.go @@ -112,7 +112,7 @@ func (src *QRecordBatchCopyFromSource) Values() ([]interface{}, error) { values[i] = rune(v.Val) case qvalue.QValueString: values[i] = v.Val - case qvalue.QValueCIDR, qvalue.QValueINET: + case qvalue.QValueCIDR, qvalue.QValueINET, qvalue.QValueMacaddr: str, ok := v.Value().(string) if !ok { src.err = errors.New("invalid INET/CIDR value") diff --git a/flow/model/qvalue/equals.go b/flow/model/qvalue/equals.go index 3f7253ddc7..20e7392906 100644 --- a/flow/model/qvalue/equals.go +++ b/flow/model/qvalue/equals.go @@ -63,6 +63,8 @@ func Equals(qv QValue, other QValue) bool { return compareString(q.Val, otherValue) case QValueCIDR: return compareString(q.Val, otherValue) + case QValueMacaddr: + return compareString(q.Val, otherValue) // all internally represented as a Golang time.Time case QValueDate, QValueTimestamp, QValueTimestampTZ, QValueTime, QValueTimeTZ: return compareGoTime(qvValue, otherValue) From e2070dd460629ed1c8de585072225bc2265c9421 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= Date: Wed, 10 Apr 2024 15:59:27 +0000 Subject: [PATCH 06/18] Touch up Scripts UI (#1592) Sort scripts. Adjust UI since editor doesn't handle resizing well. Tighten header --- ui/app/scripts/new/page.tsx | 17 ++++++----------- ui/app/scripts/page.tsx | 4 +++- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/ui/app/scripts/new/page.tsx b/ui/app/scripts/new/page.tsx index 9553e8eb3f..e9c9534765 100644 --- a/ui/app/scripts/new/page.tsx +++ b/ui/app/scripts/new/page.tsx @@ -102,14 +102,13 @@ end style={{ display: 'flex', flexDirection: 'column', - rowGap: '1rem', + rowGap: '0.5em', width: '100%', }} >
- for your Kafka messages.
-
+
@@ -155,16 +153,13 @@ end />
-
- +
setNewScript((prev) => ({ ...prev, source: newQuery })) } code={newScript?.source} - height={'30vh'} + height={'100%'} language='lua' />
diff --git a/ui/app/scripts/page.tsx b/ui/app/scripts/page.tsx index dc828fa908..e0c34cce6b 100644 --- a/ui/app/scripts/page.tsx +++ b/ui/app/scripts/page.tsx @@ -8,7 +8,9 @@ export const dynamic = 'force-dynamic'; export const revalidate = 5; const ScriptsPage = async () => { - const existingScripts = await prisma.scripts.findMany(); + const existingScripts = await prisma.scripts.findMany({ + orderBy: { name: 'asc' }, + }); const scripts: ScriptsType[] = existingScripts.map((script) => ({ ...script, source: script.source.toString(), From e99137c30d8fae47a7200fbd4abf4817dd2256f4 Mon Sep 17 00:00:00 2001 From: Kevin Biju <52661649+heavycrystal@users.noreply.github.com> Date: Wed, 10 Apr 2024 21:40:06 +0530 Subject: [PATCH 07/18] remove SendBatch in PG normalize (#1593) seems to be flaky, hangs sometimes --- flow/connectors/postgres/postgres.go | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/flow/connectors/postgres/postgres.go b/flow/connectors/postgres/postgres.go index 6fdf77f9d8..b438aec4ed 100644 --- a/flow/connectors/postgres/postgres.go +++ b/flow/connectors/postgres/postgres.go @@ -14,7 +14,6 @@ import ( "github.com/google/uuid" "github.com/jackc/pglogrepl" "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgconn" "github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgxpool" "go.temporal.io/sdk/log" @@ -602,7 +601,6 @@ func (c *PostgresConnector) NormalizeRecords(ctx context.Context, req *model.Nor if err != nil { return nil, err } - mergeStatementsBatch := &pgx.Batch{} totalRowsAffected := 0 normalizeStmtGen := &normalizeStmtGenerator{ Logger: c.logger, @@ -620,18 +618,11 @@ func (c *PostgresConnector) NormalizeRecords(ctx context.Context, req *model.Nor for _, destinationTableName := range destinationTableNames { normalizeStatements := normalizeStmtGen.generateNormalizeStatements(destinationTableName) for _, normalizeStatement := range normalizeStatements { - mergeStatementsBatch.Queue(normalizeStatement, normBatchID, req.SyncBatchID, destinationTableName).Exec( - func(ct pgconn.CommandTag) error { - totalRowsAffected += int(ct.RowsAffected()) - return nil - }) - } - } - if mergeStatementsBatch.Len() > 0 { - mergeResults := normalizeRecordsTx.SendBatch(ctx, mergeStatementsBatch) - err = mergeResults.Close() - if err != nil { - return nil, fmt.Errorf("error executing merge statements: %w", err) + ct, err := normalizeRecordsTx.Exec(ctx, normalizeStatement, normBatchID, req.SyncBatchID, destinationTableName) + if err != nil { + return nil, fmt.Errorf("error executing normalize statement: %w", err) + } + totalRowsAffected += int(ct.RowsAffected()) } } c.logger.Info(fmt.Sprintf("normalized %d records", totalRowsAffected)) From 039814e0fdce83dcceb2bbe182f61d5ae3e43ff5 Mon Sep 17 00:00:00 2001 From: Amogh Bharadwaj Date: Thu, 11 Apr 2024 02:09:42 +0530 Subject: [PATCH 08/18] Warehouse numeric interface (#1591) This PR is an implementation of [this comment](https://github.com/PeerDB-io/peerdb/pull/1319#discussion_r1492826644) which talks about organising our numeric handling as an interface. Preceding PR to #1590 --- flow/connectors/bigquery/bigquery.go | 6 +- flow/connectors/bigquery/qrep_avro_sync.go | 7 +- flow/connectors/clickhouse/normalize.go | 6 +- .../snowflake/merge_stmt_generator.go | 6 +- flow/connectors/snowflake/snowflake.go | 6 +- flow/datatypes/numeric.go | 99 ++++++++++++++++++- flow/model/qvalue/dwh.go | 24 +++-- 7 files changed, 118 insertions(+), 36 deletions(-) diff --git a/flow/connectors/bigquery/bigquery.go b/flow/connectors/bigquery/bigquery.go index b30625286a..b375a7c8c2 100644 --- a/flow/connectors/bigquery/bigquery.go +++ b/flow/connectors/bigquery/bigquery.go @@ -620,11 +620,7 @@ func (c *BigQueryConnector) SetupNormalizedTable( for _, column := range tableSchema.Columns { genericColType := column.Type if genericColType == "numeric" { - precision, scale := numeric.ParseNumericTypmod(column.TypeModifier) - if column.TypeModifier == -1 || precision > 38 || scale > 37 { - precision = numeric.PeerDBNumericPrecision - scale = numeric.PeerDBNumericScale - } + precision, scale := numeric.GetNumericTypeForWarehouse(column.TypeModifier, numeric.BigQueryNumericCompatibility{}) columns = append(columns, &bigquery.FieldSchema{ Name: column.Name, Type: bigquery.BigNumericFieldType, diff --git a/flow/connectors/bigquery/qrep_avro_sync.go b/flow/connectors/bigquery/qrep_avro_sync.go index 19548a3b59..5048777f71 100644 --- a/flow/connectors/bigquery/qrep_avro_sync.go +++ b/flow/connectors/bigquery/qrep_avro_sync.go @@ -278,10 +278,9 @@ func DefineAvroSchema(dstTableName string, func GetAvroType(bqField *bigquery.FieldSchema) (interface{}, error) { avroNumericPrecision := int16(bqField.Precision) avroNumericScale := int16(bqField.Scale) - if avroNumericPrecision > 38 || avroNumericPrecision <= 0 || - avroNumericScale > 38 || avroNumericScale < 0 { - avroNumericPrecision = numeric.PeerDBNumericPrecision - avroNumericScale = numeric.PeerDBNumericScale + bqNumeric := numeric.BigQueryNumericCompatibility{} + if !bqNumeric.IsValidPrevisionAndScale(avroNumericPrecision, avroNumericScale) { + avroNumericPrecision, avroNumericScale = bqNumeric.DefaultPrecisionAndScale() } considerRepeated := func(typ string, repeated bool) interface{} { diff --git a/flow/connectors/clickhouse/normalize.go b/flow/connectors/clickhouse/normalize.go index 9237a18d91..0813e59fae 100644 --- a/flow/connectors/clickhouse/normalize.go +++ b/flow/connectors/clickhouse/normalize.go @@ -84,11 +84,7 @@ func generateCreateTableSQLForNormalizedTable( switch colType { case qvalue.QValueKindNumeric: - precision, scale := datatypes.ParseNumericTypmod(column.TypeModifier) - if column.TypeModifier == -1 || precision > 76 || scale > precision { - precision = datatypes.PeerDBClickhousePrecision - scale = datatypes.PeerDBClickhouseScale - } + precision, scale := datatypes.GetNumericTypeForWarehouse(column.TypeModifier, datatypes.ClickHouseNumericCompatibility{}) stmtBuilder.WriteString(fmt.Sprintf("`%s` DECIMAL(%d, %d), ", colName, precision, scale)) default: diff --git a/flow/connectors/snowflake/merge_stmt_generator.go b/flow/connectors/snowflake/merge_stmt_generator.go index 2292855f0c..aa637dadf6 100644 --- a/flow/connectors/snowflake/merge_stmt_generator.go +++ b/flow/connectors/snowflake/merge_stmt_generator.go @@ -62,11 +62,7 @@ func (m *mergeStmtGenerator) generateMergeStmt(dstTable string) (string, error) // "Microseconds*1000) "+ // "AS %s", toVariantColumnName, columnName, columnName)) case qvalue.QValueKindNumeric: - precision, scale := numeric.ParseNumericTypmod(column.TypeModifier) - if column.TypeModifier == -1 || precision > 38 || scale > 37 { - precision = numeric.PeerDBNumericPrecision - scale = numeric.PeerDBNumericScale - } + precision, scale := numeric.GetNumericTypeForWarehouse(column.TypeModifier, numeric.SnowflakeNumericCompatibility{}) numericType := fmt.Sprintf("NUMERIC(%d,%d)", precision, scale) flattenedCastsSQLArray = append(flattenedCastsSQLArray, fmt.Sprintf("TRY_CAST((%s:\"%s\")::text AS %s) AS %s", diff --git a/flow/connectors/snowflake/snowflake.go b/flow/connectors/snowflake/snowflake.go index c78a438a3d..fc925cbf53 100644 --- a/flow/connectors/snowflake/snowflake.go +++ b/flow/connectors/snowflake/snowflake.go @@ -675,11 +675,7 @@ func generateCreateTableSQLForNormalizedTable( } if genericColumnType == "numeric" { - precision, scale := numeric.ParseNumericTypmod(column.TypeModifier) - if column.TypeModifier == -1 || precision > 38 || scale > 37 { - precision = numeric.PeerDBNumericPrecision - scale = numeric.PeerDBNumericScale - } + precision, scale := numeric.GetNumericTypeForWarehouse(column.TypeModifier, numeric.SnowflakeNumericCompatibility{}) sfColType = fmt.Sprintf("NUMERIC(%d,%d)", precision, scale) } diff --git a/flow/datatypes/numeric.go b/flow/datatypes/numeric.go index 8eb5703845..65d64d4164 100644 --- a/flow/datatypes/numeric.go +++ b/flow/datatypes/numeric.go @@ -1,12 +1,94 @@ package datatypes const ( - PeerDBNumericPrecision = 38 - PeerDBNumericScale = 20 + // defaults + PeerDBBigQueryPrecision = 38 + PeerDBBigQueryScale = 20 + PeerDBSnowflakePrecision = 38 + PeerDBSnowflakeScale = 20 PeerDBClickhousePrecision = 76 PeerDBClickhouseScale = 38 ) +type WarehouseNumericCompatibility interface { + MaxPrecision() int16 + MaxScale() int16 + DefaultPrecisionAndScale() (int16, int16) + IsValidPrevisionAndScale(precision, scale int16) bool +} + +type ClickHouseNumericCompatibility struct{} + +func (ClickHouseNumericCompatibility) MaxPrecision() int16 { + return 76 +} + +func (ClickHouseNumericCompatibility) MaxScale() int16 { + return 38 +} + +func (ClickHouseNumericCompatibility) DefaultPrecisionAndScale() (int16, int16) { + return PeerDBClickhousePrecision, PeerDBClickhouseScale +} + +func (ClickHouseNumericCompatibility) IsValidPrevisionAndScale(precision, scale int16) bool { + return precision > 0 && precision <= PeerDBClickhousePrecision && scale < precision +} + +type SnowflakeNumericCompatibility struct{} + +func (SnowflakeNumericCompatibility) MaxPrecision() int16 { + return 38 +} + +func (SnowflakeNumericCompatibility) MaxScale() int16 { + return 37 +} + +func (SnowflakeNumericCompatibility) DefaultPrecisionAndScale() (int16, int16) { + return PeerDBSnowflakePrecision, PeerDBSnowflakeScale +} + +func (SnowflakeNumericCompatibility) IsValidPrevisionAndScale(precision, scale int16) bool { + return precision > 0 && precision <= 38 && scale < precision +} + +type BigQueryNumericCompatibility struct{} + +func (BigQueryNumericCompatibility) MaxPrecision() int16 { + return 38 +} + +func (BigQueryNumericCompatibility) MaxScale() int16 { + return 37 +} + +func (BigQueryNumericCompatibility) DefaultPrecisionAndScale() (int16, int16) { + return PeerDBBigQueryPrecision, PeerDBBigQueryScale +} + +func (BigQueryNumericCompatibility) IsValidPrevisionAndScale(precision, scale int16) bool { + return precision > 0 && precision <= 38 && scale < precision +} + +type DefaultNumericCompatibility struct{} + +func (DefaultNumericCompatibility) MaxPrecision() int16 { + return 38 +} + +func (DefaultNumericCompatibility) MaxScale() int16 { + return 37 +} + +func (DefaultNumericCompatibility) DefaultPrecisionAndScale() (int16, int16) { + return 38, 20 +} + +func (DefaultNumericCompatibility) IsValidPrevisionAndScale(precision, scale int16) bool { + return true +} + // This is to reverse what make_numeric_typmod of Postgres does: // https://github.com/postgres/postgres/blob/21912e3c0262e2cfe64856e028799d6927862563/src/backend/utils/adt/numeric.c#L897 func ParseNumericTypmod(typmod int32) (int16, int16) { @@ -15,3 +97,16 @@ func ParseNumericTypmod(typmod int32) (int16, int16) { scale := int16(offsetMod & 0x7FFF) return precision, scale } + +func GetNumericTypeForWarehouse(typmod int32, warehouseNumeric WarehouseNumericCompatibility) (int16, int16) { + if typmod == -1 { + return warehouseNumeric.DefaultPrecisionAndScale() + } + + precision, scale := ParseNumericTypmod(typmod) + if !warehouseNumeric.IsValidPrevisionAndScale(precision, scale) { + return warehouseNumeric.DefaultPrecisionAndScale() + } + + return precision, scale +} diff --git a/flow/model/qvalue/dwh.go b/flow/model/qvalue/dwh.go index 7e04803958..5dd7440baf 100644 --- a/flow/model/qvalue/dwh.go +++ b/flow/model/qvalue/dwh.go @@ -10,16 +10,20 @@ import ( ) func DetermineNumericSettingForDWH(precision int16, scale int16, dwh protos.DBType) (int16, int16) { - if dwh == protos.DBType_CLICKHOUSE { - if precision > numeric.PeerDBClickhousePrecision || precision <= 0 || - scale > precision || scale < 0 { - return numeric.PeerDBClickhousePrecision, numeric.PeerDBClickhouseScale - } - } else { - if precision > numeric.PeerDBNumericPrecision || precision <= 0 || - scale > numeric.PeerDBNumericScale || scale < 0 { - return numeric.PeerDBNumericPrecision, numeric.PeerDBNumericScale - } + var warehouseNumeric numeric.WarehouseNumericCompatibility + switch dwh { + case protos.DBType_CLICKHOUSE: + warehouseNumeric = numeric.ClickHouseNumericCompatibility{} + case protos.DBType_SNOWFLAKE: + warehouseNumeric = numeric.SnowflakeNumericCompatibility{} + case protos.DBType_BIGQUERY: + warehouseNumeric = numeric.BigQueryNumericCompatibility{} + default: + warehouseNumeric = numeric.DefaultNumericCompatibility{} + } + + if !warehouseNumeric.IsValidPrevisionAndScale(precision, scale) { + precision, scale = warehouseNumeric.DefaultPrecisionAndScale() } return precision, scale From ac00a38fa4d27ddbd6774d80a7f9bddad32417e4 Mon Sep 17 00:00:00 2001 From: Amogh Bharadwaj Date: Thu, 11 Apr 2024 03:06:11 +0530 Subject: [PATCH 09/18] Better sync flow cleanup (#1596) This PR: - Deletes (if not exists) raw table for snowflake - Adds sync flow cleanup method for clickhouse (same as above) - Deletes (if not exists) raw table for bigquery thereby fixing drop mirror for Qrep BQ mirrors --- flow/connectors/bigquery/bigquery.go | 12 ++++++++---- flow/connectors/clickhouse/cdc.go | 27 +++++++++++++++++--------- flow/connectors/snowflake/snowflake.go | 14 +++++-------- 3 files changed, 31 insertions(+), 22 deletions(-) diff --git a/flow/connectors/bigquery/bigquery.go b/flow/connectors/bigquery/bigquery.go index b375a7c8c2..15d813fdb4 100644 --- a/flow/connectors/bigquery/bigquery.go +++ b/flow/connectors/bigquery/bigquery.go @@ -687,10 +687,14 @@ func (c *BigQueryConnector) SyncFlowCleanup(ctx context.Context, jobName string) } dataset := c.client.DatasetInProject(c.projectID, c.datasetID) - // deleting PeerDB specific tables - err = dataset.Table(c.getRawTableName(jobName)).Delete(ctx) - if err != nil { - return fmt.Errorf("failed to delete raw table: %w", err) + rawTableHandle := dataset.Table(c.getRawTableName(jobName)) + // check if exists, then delete + _, err = rawTableHandle.Metadata(ctx) + if err == nil { + deleteErr := rawTableHandle.Delete(ctx) + if deleteErr != nil { + return fmt.Errorf("failed to delete raw table: %w", deleteErr) + } } return nil diff --git a/flow/connectors/clickhouse/cdc.go b/flow/connectors/clickhouse/cdc.go index 73961fbeb0..d6640f7469 100644 --- a/flow/connectors/clickhouse/cdc.go +++ b/flow/connectors/clickhouse/cdc.go @@ -19,8 +19,8 @@ import ( ) const ( - checkIfTableExistsSQL = `SELECT exists(SELECT 1 FROM system.tables WHERE database = ? AND name = ?) AS table_exists;` - mirrorJobsTableIdentifier = "PEERDB_MIRROR_JOBS" + checkIfTableExistsSQL = `SELECT exists(SELECT 1 FROM system.tables WHERE database = ? AND name = ?) AS table_exists;` + dropTableIfExistsSQL = `DROP TABLE IF EXISTS %s;` ) // getRawTableName returns the raw table name for the given table identifier. @@ -44,13 +44,6 @@ func (c *ClickhouseConnector) checkIfTableExists(ctx context.Context, databaseNa return result.Int32 == 1, nil } -type MirrorJobRow struct { - MirrorJobName string - Offset int - SyncBatchID int - NormalizeBatchID int -} - func (c *ClickhouseConnector) CreateRawTable(ctx context.Context, req *protos.CreateRawTableInput) (*protos.CreateRawTableOutput, error) { rawTableName := c.getRawTableName(req.FlowJobName) @@ -188,3 +181,19 @@ func (c *ClickhouseConnector) ReplayTableSchemaDeltas(ctx context.Context, flowJ return nil } + +func (c *ClickhouseConnector) SyncFlowCleanup(ctx context.Context, jobName string) error { + err := c.PostgresMetadata.SyncFlowCleanup(ctx, jobName) + if err != nil { + return fmt.Errorf("[snowflake drop mirror] unable to clear metadata for sync flow cleanup: %w", err) + } + + // delete raw table if exists + rawTableIdentifier := c.getRawTableName(jobName) + _, err = c.database.ExecContext(ctx, fmt.Sprintf(dropTableIfExistsSQL, rawTableIdentifier)) + if err != nil { + return fmt.Errorf("[snowflake drop mirror] unable to drop raw table: %w", err) + } + + return nil +} diff --git a/flow/connectors/snowflake/snowflake.go b/flow/connectors/snowflake/snowflake.go index fc925cbf53..d29e3d8e09 100644 --- a/flow/connectors/snowflake/snowflake.go +++ b/flow/connectors/snowflake/snowflake.go @@ -622,19 +622,15 @@ func (c *SnowflakeConnector) CreateRawTable(ctx context.Context, req *protos.Cre func (c *SnowflakeConnector) SyncFlowCleanup(ctx context.Context, jobName string) error { err := c.PostgresMetadata.SyncFlowCleanup(ctx, jobName) if err != nil { - return fmt.Errorf("unable to clear metadata for sync flow cleanup: %w", err) + return fmt.Errorf("[snowflake drop mirror] unable to clear metadata for sync flow cleanup: %w", err) } - syncFlowCleanupTx, err := c.database.BeginTx(ctx, nil) + // delete raw table if exists + rawTableIdentifier := getRawTableIdentifier(jobName) + _, err = c.database.ExecContext(ctx, fmt.Sprintf(dropTableIfExistsSQL, c.rawSchema, rawTableIdentifier)) if err != nil { - return fmt.Errorf("unable to begin transaction for sync flow cleanup: %w", err) + return fmt.Errorf("[snowflake drop mirror] unable to drop raw table: %w", err) } - defer func() { - deferErr := syncFlowCleanupTx.Rollback() - if deferErr != sql.ErrTxDone && deferErr != nil { - c.logger.Error("error while rolling back transaction for flow cleanup", "error", deferErr) - } - }() err = c.dropStage(ctx, "", jobName) if err != nil { From ae99ef00e2b5bc11ef4a26d5398074ab795d8461 Mon Sep 17 00:00:00 2001 From: Amogh Bharadwaj Date: Thu, 11 Apr 2024 03:26:23 +0530 Subject: [PATCH 10/18] Fix PG-PG QRep Overwrite (#1594) Overwrite mode was using the sync records branch for Upsert, which expects unique key columns. It failed there. This PR fixes this by making it use the append branch. It also moves the truncate step to this branch from setup flow, since after the first sync, qrep flow continues as new, enters setup flow again where it would truncate what it just synced even though no new rows were ingested necessarily Automated test added Functionally tested --- flow/connectors/postgres/qrep.go | 9 --- flow/connectors/postgres/qrep_sql_sync.go | 13 ++++- flow/e2e/postgres/qrep_flow_pg_test.go | 70 +++++++++++++++++++++++ 3 files changed, 82 insertions(+), 10 deletions(-) diff --git a/flow/connectors/postgres/qrep.go b/flow/connectors/postgres/qrep.go index e695e6de79..7ce588d006 100644 --- a/flow/connectors/postgres/qrep.go +++ b/flow/connectors/postgres/qrep.go @@ -429,15 +429,6 @@ func (c *PostgresConnector) SetupQRepMetadataTables(ctx context.Context, config } c.logger.Info("Setup metadata table.") - if config.WriteMode != nil && - config.WriteMode.WriteType == protos.QRepWriteType_QREP_WRITE_MODE_OVERWRITE { - _, err = c.conn.Exec(ctx, - "TRUNCATE TABLE "+config.DestinationTableIdentifier) - if err != nil { - return fmt.Errorf("failed to TRUNCATE table before query replication: %w", err) - } - } - return nil } diff --git a/flow/connectors/postgres/qrep_sql_sync.go b/flow/connectors/postgres/qrep_sql_sync.go index 5a90cfdf18..9f7d4b4e48 100644 --- a/flow/connectors/postgres/qrep_sql_sync.go +++ b/flow/connectors/postgres/qrep_sql_sync.go @@ -83,7 +83,18 @@ func (s *QRepStagingTableSync) SyncQRepRecords( var numRowsSynced int64 if writeMode == nil || - writeMode.WriteType == protos.QRepWriteType_QREP_WRITE_MODE_APPEND { + writeMode.WriteType == protos.QRepWriteType_QREP_WRITE_MODE_APPEND || + writeMode.WriteType == protos.QRepWriteType_QREP_WRITE_MODE_OVERWRITE { + if writeMode != nil && writeMode.WriteType == protos.QRepWriteType_QREP_WRITE_MODE_OVERWRITE { + // Truncate the destination table before copying records + s.connector.logger.Info(fmt.Sprintf("Truncating table %s for overwrite mode", dstTableName), syncLog) + _, err = tx.Exec(ctx, + "TRUNCATE TABLE "+dstTableName.String()) + if err != nil { + return -1, fmt.Errorf("failed to TRUNCATE table before copy: %w", err) + } + } + // Perform the COPY FROM operation numRowsSynced, err = tx.CopyFrom( context.Background(), diff --git a/flow/e2e/postgres/qrep_flow_pg_test.go b/flow/e2e/postgres/qrep_flow_pg_test.go index 5e738228f2..166668c1c5 100644 --- a/flow/e2e/postgres/qrep_flow_pg_test.go +++ b/flow/e2e/postgres/qrep_flow_pg_test.go @@ -32,6 +32,12 @@ func (s PeerFlowE2ETestSuitePG) setupSourceTable(tableName string, rowCount int) } } +func (s PeerFlowE2ETestSuitePG) populateSourceTable(tableName string, rowCount int) { + if rowCount > 0 { + require.NoError(s.t, e2e.PopulateSourceTable(s.Conn(), s.suffix, tableName, rowCount)) + } +} + func (s PeerFlowE2ETestSuitePG) comparePGTables(srcSchemaQualified, dstSchemaQualified, selector string) error { // Execute the two EXCEPT queries return errors.Join( @@ -92,6 +98,20 @@ func (s PeerFlowE2ETestSuitePG) compareQuery(srcSchemaQualified, dstSchemaQualif return nil } +func (s PeerFlowE2ETestSuitePG) compareCounts(dstSchemaQualified string, expectedCount int64) error { + query := "SELECT COUNT(*) FROM " + dstSchemaQualified + count, err := s.RunInt64Query(query) + if err != nil { + return err + } + + if count != expectedCount { + return fmt.Errorf("expected %d rows, got %d", expectedCount, count) + } + + return nil +} + func (s PeerFlowE2ETestSuitePG) checkSyncedAt(dstSchemaQualified string) error { query := `SELECT "_PEERDB_SYNCED_AT" FROM ` + dstSchemaQualified @@ -236,6 +256,56 @@ func (s PeerFlowE2ETestSuitePG) Test_PeerDB_Columns_QRep_PG() { require.NoError(s.t, err) } +func (s PeerFlowE2ETestSuitePG) Test_Overwrite_PG() { + numRows := 10 + + srcTable := "test_overwrite_pg_1" + s.setupSourceTable(srcTable, numRows) + + dstTable := "test_overwrite_pg_2" + + srcSchemaQualified := fmt.Sprintf("%s_%s.%s", "e2e_test", s.suffix, srcTable) + dstSchemaQualified := fmt.Sprintf("%s_%s.%s", "e2e_test", s.suffix, dstTable) + + query := fmt.Sprintf("SELECT * FROM e2e_test_%s.%s WHERE updated_at BETWEEN {{.start}} AND {{.end}}", + s.suffix, srcTable) + + postgresPeer := e2e.GeneratePostgresPeer() + + qrepConfig, err := e2e.CreateQRepWorkflowConfig( + "test_overwrite_pg", + srcSchemaQualified, + dstSchemaQualified, + query, + postgresPeer, + "", + true, + "_PEERDB_SYNCED_AT", + ) + require.NoError(s.t, err) + qrepConfig.WriteMode = &protos.QRepWriteMode{ + WriteType: protos.QRepWriteType_QREP_WRITE_MODE_OVERWRITE, + } + qrepConfig.InitialCopyOnly = false + + tc := e2e.NewTemporalClient(s.t) + env := e2e.RunQRepFlowWorkflow(tc, qrepConfig) + e2e.EnvWaitFor(s.t, env, 3*time.Minute, "waiting for first sync to complete", func() bool { + err = s.compareCounts(dstSchemaQualified, int64(numRows)) + return err == nil + }) + + newRowCount := 5 + s.populateSourceTable(srcTable, newRowCount) + + e2e.EnvWaitFor(s.t, env, 2*time.Minute, "waiting for overwrite sync to complete", func() bool { + err = s.compareCounts(dstSchemaQualified, int64(newRowCount)) + return err == nil + }) + + require.NoError(s.t, env.Error()) +} + func (s PeerFlowE2ETestSuitePG) Test_No_Rows_QRep_PG() { numRows := 0 From 94f5a670696d864dd0df64404a7251826b369da2 Mon Sep 17 00:00:00 2001 From: Amogh Bharadwaj Date: Thu, 11 Apr 2024 03:37:38 +0530 Subject: [PATCH 11/18] UI: Fix GCS Link (#1598) Link for GCS create peer meant to point to google cloud doc --- ui/components/PeerForms/S3Form.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/components/PeerForms/S3Form.tsx b/ui/components/PeerForms/S3Form.tsx index cbb258a3e0..f4cfcc351f 100644 --- a/ui/components/PeerForms/S3Form.tsx +++ b/ui/components/PeerForms/S3Form.tsx @@ -51,7 +51,7 @@ const S3Form = ({ setter }: S3Props) => {

More information on how to setup HMAC for GCS. From d406b0a5c1803bc65a75480226fd82a3a6f3005c Mon Sep 17 00:00:00 2001 From: Amogh Bharadwaj Date: Thu, 11 Apr 2024 14:03:41 +0530 Subject: [PATCH 12/18] BigQuery: add flag-gated time partitioning (#1588) This PR adds support for partitioning the target tables we create in BigQuery mirrors by the `_PEERDB_SYNCED_AT` column, by **days** This feature is gated by a dynamic configured env variable - `PEERDB_BIGQUERY_ENABLE_SYNCED_AT_PARTITIONING_BY_DAYS` --- flow/connectors/bigquery/bigquery.go | 17 ++++++++++--- flow/dynamicconf/dynamicconf.go | 36 ++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/flow/connectors/bigquery/bigquery.go b/flow/connectors/bigquery/bigquery.go index 15d813fdb4..81fb8266a6 100644 --- a/flow/connectors/bigquery/bigquery.go +++ b/flow/connectors/bigquery/bigquery.go @@ -20,6 +20,7 @@ import ( metadataStore "github.com/PeerDB-io/peer-flow/connectors/external_metadata" "github.com/PeerDB-io/peer-flow/connectors/utils" numeric "github.com/PeerDB-io/peer-flow/datatypes" + "github.com/PeerDB-io/peer-flow/dynamicconf" "github.com/PeerDB-io/peer-flow/generated/protos" "github.com/PeerDB-io/peer-flow/logger" "github.com/PeerDB-io/peer-flow/model" @@ -665,10 +666,20 @@ func (c *BigQueryConnector) SetupNormalizedTable( } } + timePartitionEnabled := dynamicconf.PeerDBBigQueryEnableSyncedAtPartitioning(ctx) + var timePartitioning *bigquery.TimePartitioning + if timePartitionEnabled && syncedAtColName != "" { + timePartitioning = &bigquery.TimePartitioning{ + Type: bigquery.DayPartitioningType, + Field: syncedAtColName, + } + } + metadata := &bigquery.TableMetadata{ - Schema: schema, - Name: datasetTable.table, - Clustering: clustering, + Schema: schema, + Name: datasetTable.table, + Clustering: clustering, + TimePartitioning: timePartitioning, } err = table.Create(ctx, metadata) diff --git a/flow/dynamicconf/dynamicconf.go b/flow/dynamicconf/dynamicconf.go index 080ae4ec00..aedbce65ce 100644 --- a/flow/dynamicconf/dynamicconf.go +++ b/flow/dynamicconf/dynamicconf.go @@ -52,6 +52,34 @@ func dynamicConfUint32(ctx context.Context, key string, defaultValue uint32) uin return uint32(result) } +func dynamicConfBool(ctx context.Context, key string, defaultValue bool) bool { + conn, err := peerdbenv.GetCatalogConnectionPoolFromEnv(ctx) + if err != nil { + logger.LoggerFromCtx(ctx).Error("Failed to get catalog connection pool: %v", err) + return defaultValue + } + + if !dynamicConfKeyExists(ctx, conn, key) { + return defaultValue + } + + var value pgtype.Text + query := "SELECT config_value FROM alerting_settings WHERE config_name = $1" + err = conn.QueryRow(ctx, query, key).Scan(&value) + if err != nil { + logger.LoggerFromCtx(ctx).Error("Failed to get key: %v", err) + return defaultValue + } + + result, err := strconv.ParseBool(value.String) + if err != nil { + logger.LoggerFromCtx(ctx).Error("Failed to parse bool: %v", err) + return defaultValue + } + + return result +} + // PEERDB_SLOT_LAG_MB_ALERT_THRESHOLD, 0 disables slot lag alerting entirely func PeerDBSlotLagMBAlertThreshold(ctx context.Context) uint32 { return dynamicConfUint32(ctx, "PEERDB_SLOT_LAG_MB_ALERT_THRESHOLD", 5000) @@ -67,3 +95,11 @@ func PeerDBAlertingGapMinutesAsDuration(ctx context.Context) time.Duration { func PeerDBOpenConnectionsAlertThreshold(ctx context.Context) uint32 { return dynamicConfUint32(ctx, "PEERDB_PGPEER_OPEN_CONNECTIONS_ALERT_THRESHOLD", 5) } + +// PEERDB_BIGQUERY_ENABLE_SYNCED_AT_PARTITIONING_BY_DAYS, for creating target tables with +// partitioning by _PEERDB_SYNCED_AT column +// If true, the target tables will be partitioned by _PEERDB_SYNCED_AT column +// If false, the target tables will not be partitioned +func PeerDBBigQueryEnableSyncedAtPartitioning(ctx context.Context) bool { + return dynamicConfBool(ctx, "PEERDB_BIGQUERY_ENABLE_SYNCED_AT_PARTITIONING_BY_DAYS", false) +} From f386c4e219fc5578500e18ec7075999e717c3fe0 Mon Sep 17 00:00:00 2001 From: Amogh Bharadwaj Date: Thu, 11 Apr 2024 20:42:49 +0530 Subject: [PATCH 13/18] Add missing flow name keys to some logs (#1604) Logs in PullRecords and ConsolidatePartitions were missing the flow name key. Added values to context in activity for the latter For PullRecords, `p.logger` (and p.PostgresConnector.logger) for some reason does not print flow name even though it takes from loggerfromctx where ctx should have the flowName key. Using loggerfromctx in the pullrecords function explicitly instead, does print it --- flow/activities/flowable.go | 3 +++ flow/connectors/postgres/cdc.go | 43 ++++++++++++++++--------------- flow/connectors/snowflake/qrep.go | 2 -- flow/workflows/qrep_flow.go | 2 -- 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/flow/activities/flowable.go b/flow/activities/flowable.go index e1795e3253..c8de2ba3e4 100644 --- a/flow/activities/flowable.go +++ b/flow/activities/flowable.go @@ -734,6 +734,7 @@ func (a *FlowableActivity) replicateQRepPartition(ctx context.Context, func (a *FlowableActivity) ConsolidateQRepPartitions(ctx context.Context, config *protos.QRepConfig, runUUID string, ) error { + ctx = context.WithValue(ctx, shared.FlowNameKey, config.FlowJobName) dstConn, err := connectors.GetQRepConsolidateConnector(ctx, config.DestinationPeer) if errors.Is(err, connectors.ErrUnsupportedFunctionality) { return monitoring.UpdateEndTimeForQRepRun(ctx, a.CatalogPool, runUUID) @@ -757,6 +758,7 @@ func (a *FlowableActivity) ConsolidateQRepPartitions(ctx context.Context, config } func (a *FlowableActivity) CleanupQRepFlow(ctx context.Context, config *protos.QRepConfig) error { + ctx = context.WithValue(ctx, shared.FlowNameKey, config.FlowJobName) dst, err := connectors.GetQRepConsolidateConnector(ctx, config.DestinationPeer) if errors.Is(err, connectors.ErrUnsupportedFunctionality) { return nil @@ -780,6 +782,7 @@ func (a *FlowableActivity) DropFlowSource(ctx context.Context, config *protos.Sh } func (a *FlowableActivity) DropFlowDestination(ctx context.Context, config *protos.ShutdownRequest) error { + ctx = context.WithValue(ctx, shared.FlowNameKey, config.FlowJobName) dstConn, err := connectors.GetCDCSyncConnector(ctx, config.DestinationPeer) if err != nil { return fmt.Errorf("failed to get destination connector: %w", err) diff --git a/flow/connectors/postgres/cdc.go b/flow/connectors/postgres/cdc.go index 306e783dd3..11eb66139c 100644 --- a/flow/connectors/postgres/cdc.go +++ b/flow/connectors/postgres/cdc.go @@ -112,6 +112,7 @@ func GetChildToParentRelIDMap(ctx context.Context, conn *pgx.Conn) (map[uint32]u // PullRecords pulls records from the cdc stream func (p *PostgresCDCSource) PullRecords(ctx context.Context, req *model.PullRecordsRequest) error { + logger := logger.LoggerFromCtx(ctx) conn := p.replConn.PgConn() records := req.RecordStream // clientXLogPos is the last checkpoint id, we need to ack that we have processed @@ -135,17 +136,17 @@ func (p *PostgresCDCSource) PullRecords(ctx context.Context, req *model.PullReco if cdcRecordsStorage.IsEmpty() { records.SignalAsEmpty() } - p.logger.Info(fmt.Sprintf("[finished] PullRecords streamed %d records", cdcRecordsStorage.Len())) + logger.Info(fmt.Sprintf("[finished] PullRecords streamed %d records", cdcRecordsStorage.Len())) err := cdcRecordsStorage.Close() if err != nil { - p.logger.Warn("failed to clean up records storage", slog.Any("error", err)) + logger.Warn("failed to clean up records storage", slog.Any("error", err)) } }() shutdown := utils.HeartbeatRoutine(ctx, func() string { currRecords := cdcRecordsStorage.Len() msg := fmt.Sprintf("pulling records, currently have %d records", currRecords) - p.logger.Info(msg) + logger.Info(msg) return msg }) defer shutdown() @@ -153,7 +154,6 @@ func (p *PostgresCDCSource) PullRecords(ctx context.Context, req *model.PullReco standbyMessageTimeout := req.IdleTimeout nextStandbyMessageDeadline := time.Now().Add(standbyMessageTimeout) - logger := logger.LoggerFromCtx(ctx) addRecordWithKey := func(key model.TableWithPkey, rec model.Record) error { err := cdcRecordsStorage.Set(logger, key, rec) if err != nil { @@ -164,7 +164,7 @@ func (p *PostgresCDCSource) PullRecords(ctx context.Context, req *model.PullReco if cdcRecordsStorage.Len() == 1 { records.SignalAsNotEmpty() nextStandbyMessageDeadline = time.Now().Add(standbyMessageTimeout) - p.logger.Info(fmt.Sprintf("pushing the standby deadline to %s", nextStandbyMessageDeadline)) + logger.Info(fmt.Sprintf("pushing the standby deadline to %s", nextStandbyMessageDeadline)) } return nil } @@ -183,7 +183,7 @@ func (p *PostgresCDCSource) PullRecords(ctx context.Context, req *model.PullReco if time.Since(standByLastLogged) > 10*time.Second { numRowsProcessedMessage := fmt.Sprintf("processed %d rows", cdcRecordsStorage.Len()) - p.logger.Info("Sent Standby status message. " + numRowsProcessedMessage) + logger.Info("Sent Standby status message. " + numRowsProcessedMessage) standByLastLogged = time.Now() } } @@ -195,7 +195,7 @@ func (p *PostgresCDCSource) PullRecords(ctx context.Context, req *model.PullReco } if waitingForCommit { - p.logger.Info(fmt.Sprintf( + logger.Info(fmt.Sprintf( "[%s] commit received, returning currently accumulated records - %d", p.flowJobName, cdcRecordsStorage.Len()), @@ -207,20 +207,20 @@ func (p *PostgresCDCSource) PullRecords(ctx context.Context, req *model.PullReco // if we are past the next standby deadline (?) if time.Now().After(nextStandbyMessageDeadline) { if !cdcRecordsStorage.IsEmpty() { - p.logger.Info(fmt.Sprintf("standby deadline reached, have %d records", cdcRecordsStorage.Len())) + logger.Info(fmt.Sprintf("standby deadline reached, have %d records", cdcRecordsStorage.Len())) if p.commitLock == nil { - p.logger.Info( + logger.Info( fmt.Sprintf("no commit lock, returning currently accumulated records - %d", cdcRecordsStorage.Len())) return nil } else { - p.logger.Info(fmt.Sprintf("commit lock, waiting for commit to return records - %d", + logger.Info(fmt.Sprintf("commit lock, waiting for commit to return records - %d", cdcRecordsStorage.Len())) waitingForCommit = true } } else { - p.logger.Info(fmt.Sprintf("[%s] standby deadline reached, no records accumulated, continuing to wait", + logger.Info(fmt.Sprintf("[%s] standby deadline reached, no records accumulated, continuing to wait", p.flowJobName), ) } @@ -244,7 +244,7 @@ func (p *PostgresCDCSource) PullRecords(ctx context.Context, req *model.PullReco if err != nil && p.commitLock == nil { if pgconn.Timeout(err) { - p.logger.Info(fmt.Sprintf("Stand-by deadline reached, returning currently accumulated records - %d", + logger.Info(fmt.Sprintf("Stand-by deadline reached, returning currently accumulated records - %d", cdcRecordsStorage.Len())) return nil } else { @@ -253,7 +253,7 @@ func (p *PostgresCDCSource) PullRecords(ctx context.Context, req *model.PullReco } if errMsg, ok := rawMsg.(*pgproto3.ErrorResponse); ok { - p.logger.Error(fmt.Sprintf("received Postgres WAL error: %+v", errMsg)) + logger.Error(fmt.Sprintf("received Postgres WAL error: %+v", errMsg)) return fmt.Errorf("received Postgres WAL error: %+v", errMsg) } @@ -269,7 +269,7 @@ func (p *PostgresCDCSource) PullRecords(ctx context.Context, req *model.PullReco return fmt.Errorf("ParsePrimaryKeepaliveMessage failed: %w", err) } - p.logger.Debug( + logger.Debug( fmt.Sprintf("Primary Keepalive Message => ServerWALEnd: %s ServerTime: %s ReplyRequested: %t", pkm.ServerWALEnd, pkm.ServerTime, pkm.ReplyRequested)) @@ -287,7 +287,7 @@ func (p *PostgresCDCSource) PullRecords(ctx context.Context, req *model.PullReco return fmt.Errorf("ParseXLogData failed: %w", err) } - p.logger.Debug(fmt.Sprintf("XLogData => WALStart %s ServerWALEnd %s ServerTime %s\n", + logger.Debug(fmt.Sprintf("XLogData => WALStart %s ServerWALEnd %s ServerTime %s\n", xld.WALStart, xld.ServerWALEnd, xld.ServerTime)) rec, err := p.processMessage(ctx, records, xld, clientXLogPos) if err != nil { @@ -395,7 +395,7 @@ func (p *PostgresCDCSource) PullRecords(ctx context.Context, req *model.PullReco case *model.RelationRecord: tableSchemaDelta := r.TableSchemaDelta if len(tableSchemaDelta.AddedColumns) > 0 { - p.logger.Info(fmt.Sprintf("Detected schema change for table %s, addedColumns: %v", + logger.Info(fmt.Sprintf("Detected schema change for table %s, addedColumns: %v", tableSchemaDelta.SrcTableName, tableSchemaDelta.AddedColumns)) records.AddSchemaDelta(req.TableNameMapping, tableSchemaDelta) } @@ -426,6 +426,7 @@ func (p *PostgresCDCSource) processMessage( xld pglogrepl.XLogData, currentClientXlogPos pglogrepl.LSN, ) (model.Record, error) { + logger := logger.LoggerFromCtx(ctx) logicalMsg, err := pglogrepl.Parse(xld.WALData) if err != nil { return nil, fmt.Errorf("error parsing logical message: %w", err) @@ -433,8 +434,8 @@ func (p *PostgresCDCSource) processMessage( switch msg := logicalMsg.(type) { case *pglogrepl.BeginMessage: - p.logger.Debug(fmt.Sprintf("BeginMessage => FinalLSN: %v, XID: %v", msg.FinalLSN, msg.Xid)) - p.logger.Debug("Locking PullRecords at BeginMessage, awaiting CommitMessage") + logger.Debug(fmt.Sprintf("BeginMessage => FinalLSN: %v, XID: %v", msg.FinalLSN, msg.Xid)) + logger.Debug("Locking PullRecords at BeginMessage, awaiting CommitMessage") p.commitLock = msg case *pglogrepl.InsertMessage: return p.processInsertMessage(xld.WALStart, msg) @@ -444,7 +445,7 @@ func (p *PostgresCDCSource) processMessage( return p.processDeleteMessage(xld.WALStart, msg) case *pglogrepl.CommitMessage: // for a commit message, update the last checkpoint id for the record batch. - p.logger.Debug(fmt.Sprintf("CommitMessage => CommitLSN: %v, TransactionEndLSN: %v", + logger.Debug(fmt.Sprintf("CommitMessage => CommitLSN: %v, TransactionEndLSN: %v", msg.CommitLSN, msg.TransactionEndLSN)) batch.UpdateLatestCheckpoint(int64(msg.CommitLSN)) p.commitLock = nil @@ -456,13 +457,13 @@ func (p *PostgresCDCSource) processMessage( return nil, nil } - p.logger.Debug(fmt.Sprintf("RelationMessage => RelationID: %d, Namespace: %s, RelationName: %s, Columns: %v", + logger.Debug(fmt.Sprintf("RelationMessage => RelationID: %d, Namespace: %s, RelationName: %s, Columns: %v", msg.RelationID, msg.Namespace, msg.RelationName, msg.Columns)) return p.processRelationMessage(ctx, currentClientXlogPos, msg) case *pglogrepl.TruncateMessage: - p.logger.Warn("TruncateMessage not supported") + logger.Warn("TruncateMessage not supported") } return nil, nil diff --git a/flow/connectors/snowflake/qrep.go b/flow/connectors/snowflake/qrep.go index 15cf3e5e72..c622a55522 100644 --- a/flow/connectors/snowflake/qrep.go +++ b/flow/connectors/snowflake/qrep.go @@ -160,8 +160,6 @@ func (c *SnowflakeConnector) createExternalStage(stageName string, config *proto } func (c *SnowflakeConnector) ConsolidateQRepPartitions(ctx context.Context, config *protos.QRepConfig) error { - c.logger.Info("Consolidating partitions") - destTable := config.DestinationTableIdentifier stageName := c.getStageNameForJob(config.FlowJobName) diff --git a/flow/workflows/qrep_flow.go b/flow/workflows/qrep_flow.go index 25cfa2cebb..4b3a59873b 100644 --- a/flow/workflows/qrep_flow.go +++ b/flow/workflows/qrep_flow.go @@ -288,8 +288,6 @@ func (q *QRepFlowExecution) processPartitions( // For some targets we need to consolidate all the partitions from stages before // we proceed to next batch. func (q *QRepFlowExecution) consolidatePartitions(ctx workflow.Context) error { - q.logger.Info("consolidating partitions") - // only an operation for Snowflake currently. ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{ StartToCloseTimeout: 24 * time.Hour, From 5ccd062d0c2bc8008ba48bc7dbe61d64526d0589 Mon Sep 17 00:00:00 2001 From: Amogh Bharadwaj Date: Thu, 11 Apr 2024 20:48:41 +0530 Subject: [PATCH 14/18] UI Partitions table: remove extra column (#1603) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug in one of the qrep tables where run uuid was removed from the fetch but the column was still in the table causing the table headers to be mismatched. This PR fixes this Screenshot 2024-04-11 at 7 30 33 PM --- ui/app/mirrors/status/qrep/[mirrorId]/page.tsx | 1 - ui/app/mirrors/status/qrep/[mirrorId]/qrepStatusTable.tsx | 3 --- 2 files changed, 4 deletions(-) diff --git a/ui/app/mirrors/status/qrep/[mirrorId]/page.tsx b/ui/app/mirrors/status/qrep/[mirrorId]/page.tsx index c50d016633..02e0a758b6 100644 --- a/ui/app/mirrors/status/qrep/[mirrorId]/page.tsx +++ b/ui/app/mirrors/status/qrep/[mirrorId]/page.tsx @@ -34,7 +34,6 @@ export default async function QRepMirrorStatus({ endTime: run.end_time, pulledRows: run.rows_in_partition, syncedRows: run.rows_synced, - status: '', }; return ret; }); diff --git a/ui/app/mirrors/status/qrep/[mirrorId]/qrepStatusTable.tsx b/ui/app/mirrors/status/qrep/[mirrorId]/qrepStatusTable.tsx index e0a7577c33..93346d4e85 100644 --- a/ui/app/mirrors/status/qrep/[mirrorId]/qrepStatusTable.tsx +++ b/ui/app/mirrors/status/qrep/[mirrorId]/qrepStatusTable.tsx @@ -13,7 +13,6 @@ import ReactSelect from 'react-select'; export type QRepPartitionStatus = { partitionId: string; - status: string; startTime: Date | null; endTime: Date | null; pulledRows: number | null; @@ -30,7 +29,6 @@ function TimeOrProgressBar({ time }: { time: Date | null }) { function RowPerPartition({ partitionId, - status, startTime, endTime, pulledRows: numRows, @@ -204,7 +202,6 @@ export default function QRepStatusTable({ partitions }: QRepStatusTableProps) { {[ 'Partition UUID', - 'Run UUID', 'Duration', 'Start Time', 'End Time', From 94efd33055294a54505d54462879146c2dc64dbf Mon Sep 17 00:00:00 2001 From: Amogh Bharadwaj Date: Thu, 11 Apr 2024 22:51:31 +0530 Subject: [PATCH 15/18] UI Peer dropdowns: add tembo and crunchy (#1599) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds dropdown items in the source selection page of create peer UI. - Tembo - Crunchy Postgres are the items Functionally tested Screenshot 2024-04-11 at 4 17 21 AM --- ui/app/mirrors/create/cdc/guide.tsx | 2 ++ ui/app/peers/create/[peerType]/page.tsx | 22 ++++++++++------------ ui/components/PeerComponent.tsx | 4 ++++ ui/components/SelectSource.tsx | 5 ++++- ui/public/images/crunchy.png | Bin 0 -> 38623 bytes ui/public/images/tembo.png | Bin 0 -> 102915 bytes 6 files changed, 20 insertions(+), 13 deletions(-) create mode 100644 ui/public/images/crunchy.png create mode 100644 ui/public/images/tembo.png diff --git a/ui/app/mirrors/create/cdc/guide.tsx b/ui/app/mirrors/create/cdc/guide.tsx index b27204b991..c53562cff0 100644 --- a/ui/app/mirrors/create/cdc/guide.tsx +++ b/ui/app/mirrors/create/cdc/guide.tsx @@ -20,6 +20,8 @@ const GuideForDestinationSetup = ({ return 'https://docs.peerdb.io/connect/azure_flexible_server_postgres'; case 'GOOGLE CLOUD POSTGRESQL': return 'https://docs.peerdb.io/connect/cloudsql_postgres'; + case 'CRUNCHY POSTGRES': + return 'https://docs.peerdb.io/connect/crunchy_bridge'; default: return ''; } diff --git a/ui/app/peers/create/[peerType]/page.tsx b/ui/app/peers/create/[peerType]/page.tsx index dfe1068e79..8325571fab 100644 --- a/ui/app/peers/create/[peerType]/page.tsx +++ b/ui/app/peers/create/[peerType]/page.tsx @@ -45,24 +45,22 @@ export default function CreateConfig({ const [loading, setLoading] = useState(false); const peerLabel = peerType.toUpperCase().replaceAll('%20', ' '); const getDBType = () => { - if (peerType.includes('POSTGRESQL')) { + if (peerType.includes('POSTGRES') || peerType.includes('TEMBO')) { return 'POSTGRES'; } return peerType; }; const configComponentMap = (peerType: string) => { - if (peerType.includes('POSTGRESQL')) { - return ( - - ); - } - - switch (peerType) { + switch (getDBType()) { + case 'POSTGRES': + return ( + + ); case 'SNOWFLAKE': return ; case 'BIGQUERY': diff --git a/ui/components/PeerComponent.tsx b/ui/components/PeerComponent.tsx index 0f7157cc0c..55659080b8 100644 --- a/ui/components/PeerComponent.tsx +++ b/ui/components/PeerComponent.tsx @@ -14,6 +14,10 @@ export const DBTypeToImageMapping = (peerType: DBType | string) => { return '/svgs/rds.svg'; case 'GOOGLE CLOUD POSTGRESQL': return '/svgs/gcp.svg'; + case 'TEMBO': + return '/images/tembo.png'; + case 'CRUNCHY POSTGRES': + return '/images/crunchy.png'; case DBType.POSTGRES: case 'POSTGRES': return '/svgs/pg.svg'; diff --git a/ui/components/SelectSource.tsx b/ui/components/SelectSource.tsx index ac30d37c65..668fed3c10 100644 --- a/ui/components/SelectSource.tsx +++ b/ui/components/SelectSource.tsx @@ -45,7 +45,9 @@ export default function SelectSource({ { value: 'POSTGRESQL', label: 'POSTGRESQL' }, { value: 'POSTGRESQL', label: 'RDS POSTGRESQL' }, { value: 'POSTGRESQL', label: 'GOOGLE CLOUD POSTGRESQL' }, - { value: 'POSTGRESQL', label: 'AZURE FLEXIBLE POSTGRESQL' } + { value: 'POSTGRESQL', label: 'AZURE FLEXIBLE POSTGRESQL' }, + { value: 'POSTGRESQL', label: 'TEMBO' }, + { value: 'POSTGRESQL', label: 'CRUNCHY POSTGRES' } ); return ( val && setPeerType(val.label)} formatOptionLabel={SourceLabel} theme={SelectTheme} + getOptionValue={(option) => option.label} /> ); } diff --git a/ui/public/images/crunchy.png b/ui/public/images/crunchy.png new file mode 100644 index 0000000000000000000000000000000000000000..e238a688dd8614e2694e60cbe43d489f76357bc9 GIT binary patch literal 38623 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�SMn4yv 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 literal 0 HcmV?d00001 diff --git a/ui/public/images/tembo.png b/ui/public/images/tembo.png new file mode 100644 index 0000000000000000000000000000000000000000..5008c33081af4ca210fdf2a91523618f4d3e776f GIT binary patch literal 102915 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 Date: Thu, 11 Apr 2024 20:23:00 +0000 Subject: [PATCH 16/18] Lua: add unchanged_columns field to Record (#1608) This allows disambiguating a null value as unchanged for toast columns Ignore UnchangedToastColumns on DeleteRecord since queues don't care about soft delete --- flow/pua/peerdb.go | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/flow/pua/peerdb.go b/flow/pua/peerdb.go index ccbd06aed6..7a6dab9652 100644 --- a/flow/pua/peerdb.go +++ b/flow/pua/peerdb.go @@ -214,6 +214,16 @@ func LuaRecordIndex(ls *lua.LState) int { ls.Push(lua.LString(record.GetDestinationTableName())) case "source": ls.Push(lua.LString(record.GetSourceTableName())) + case "unchanged_columns": + if ur, ok := record.(*model.UpdateRecord); ok { + tbl := ls.CreateTable(0, len(ur.UnchangedToastColumns)) + for col := range ur.UnchangedToastColumns { + tbl.RawSetString(col, lua.LTrue) + } + ls.Push(tbl) + } else { + ls.Push(lua.LNil) + } default: return 0 } @@ -221,13 +231,22 @@ func LuaRecordIndex(ls *lua.LState) int { } func LuaRecordJson(ls *lua.LState) int { - ud := ls.Get(1) - tbl := ls.CreateTable(0, 6) + ud := ls.CheckUserData(1) + tbl := ls.CreateTable(0, 7) for _, key := range []string{ "kind", "old", "new", "checkpoint", "commit_time", "source", } { tbl.RawSetString(key, ls.GetField(ud, key)) } + if ur, ok := ud.Value.(*model.UpdateRecord); ok { + if len(ur.UnchangedToastColumns) > 0 { + unchanged := ls.CreateTable(len(ur.UnchangedToastColumns), 0) + for col := range ur.UnchangedToastColumns { + unchanged.Append(lua.LString(col)) + } + tbl.RawSetString("unchanged_columns", unchanged) + } + } ls.Push(tbl) return 1 } From 7bb2f844b80751dfa2b08d913f424cb253373830 Mon Sep 17 00:00:00 2001 From: Amogh Bharadwaj Date: Fri, 12 Apr 2024 17:45:29 +0530 Subject: [PATCH 17/18] UI: split initial load table into 2 (#1607) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR splits the initial load table in the mirror page into two - completed and in progress Screenshot 2024-04-11 at 11 36 13 PM --- ui/app/mirrors/[mirrorId]/snapshot.tsx | 248 ++----------------- ui/app/mirrors/[mirrorId]/snapshotTable.tsx | 249 ++++++++++++++++++++ 2 files changed, 268 insertions(+), 229 deletions(-) create mode 100644 ui/app/mirrors/[mirrorId]/snapshotTable.tsx diff --git a/ui/app/mirrors/[mirrorId]/snapshot.tsx b/ui/app/mirrors/[mirrorId]/snapshot.tsx index d0a3e69e1a..f60c3241d9 100644 --- a/ui/app/mirrors/[mirrorId]/snapshot.tsx +++ b/ui/app/mirrors/[mirrorId]/snapshot.tsx @@ -1,18 +1,7 @@ 'use client'; -import SelectTheme from '@/app/styles/select'; -import TimeLabel from '@/components/TimeComponent'; import { CloneTableSummary, SnapshotStatus } from '@/grpc_generated/route'; -import { Badge } from '@/lib/Badge/Badge'; -import { Button } from '@/lib/Button'; -import { Icon } from '@/lib/Icon'; -import { Label } from '@/lib/Label'; -import { ProgressBar } from '@/lib/ProgressBar'; -import { SearchField } from '@/lib/SearchField'; -import { Table, TableCell, TableRow } from '@/lib/Table'; import moment, { Duration, Moment } from 'moment'; -import Link from 'next/link'; -import { useMemo, useState } from 'react'; -import ReactSelect from 'react-select'; +import SnapshotTable from './snapshotTable'; export class TableCloneSummary { cloneStartTime: Moment | null = null; @@ -53,233 +42,34 @@ type SnapshotStatusProps = { status: SnapshotStatus; }; -const ROWS_PER_PAGE = 5; export const SnapshotStatusTable = ({ status }: SnapshotStatusProps) => { - const [sortField, setSortField] = useState< - 'cloneStartTime' | 'avgTimePerPartition' - >('cloneStartTime'); - const allRows = status.clones.map(summarizeTableClone); - const [currentPage, setCurrentPage] = useState(1); - const totalPages = Math.ceil(allRows.length / ROWS_PER_PAGE); - const [searchQuery, setSearchQuery] = useState(''); - const [sortDir, setSortDir] = useState<'asc' | 'dsc'>('dsc'); - const displayedRows = useMemo(() => { - const shownRows = allRows.filter((row: TableCloneSummary) => - row.cloneTableSummary.tableName - ?.toLowerCase() - .includes(searchQuery.toLowerCase()) - ); - shownRows.sort((a, b) => { - const aValue = a[sortField]; - const bValue = b[sortField]; - if (aValue === null || bValue === null) { - return 0; - } - - if (aValue < bValue) { - return sortDir === 'dsc' ? 1 : -1; - } else if (aValue > bValue) { - return sortDir === 'dsc' ? -1 : 1; - } else { - return 0; - } - }); - - const startRow = (currentPage - 1) * ROWS_PER_PAGE; - const endRow = startRow + ROWS_PER_PAGE; - return shownRows.length > ROWS_PER_PAGE - ? shownRows.slice(startRow, endRow) - : shownRows; - }, [allRows, currentPage, searchQuery, sortField, sortDir]); - - const handlePrevPage = () => { - if (currentPage > 1) { - setCurrentPage(currentPage - 1); - } - }; - - const handleNextPage = () => { - if (currentPage < totalPages) { - setCurrentPage(currentPage + 1); - } - }; - - const getStatus = (clone: CloneTableSummary) => { - if (clone.consolidateCompleted) { - return ( - - -
Done
-
- ); - } - if (!clone.fetchCompleted) { - return ( - - -
Fetching
-
- ); - } - - if (clone.numPartitionsCompleted == clone.numPartitionsTotal) { - return ( - - -
Consolidating
-
- ); - } - - return ( - - -
Syncing
-
- ); - }; + const allTableLoads = status.clones.map(summarizeTableClone); + const completedTableLoads = allTableLoads.filter( + (row) => row.cloneTableSummary.consolidateCompleted === true + ); + const inProgressTableLoads = allTableLoads.filter( + (row) => !row.cloneTableSummary.consolidateCompleted + ); - const sortOptions = [ - { value: 'cloneStartTime', label: 'Start Time' }, - { value: 'avgTimePerPartition', label: 'Time Per Partition' }, - ]; return (
- Initial Copy} - toolbar={{ - left: ( -
- - - - -
- { - const sortVal = - (val?.value as - | 'cloneStartTime' - | 'avgTimePerPartition') ?? 'cloneStartTime'; - setSortField(sortVal); - }} - value={{ - value: sortField, - label: sortOptions.find((opt) => opt.value === sortField) - ?.label, - }} - defaultValue={{ - value: 'cloneStartTime', - label: 'Start Time', - }} - theme={SelectTheme} - /> -
- - -
- ), - right: ( - ) => - setSearchQuery(e.target.value) - } - /> - ), - }} - header={ - - Table Identifier - Status - Sync Start - Progress Partitions - Num Rows Processed - Avg Time Per Partition - - } - > - {displayedRows.map((clone, index) => ( - - - - - {getStatus(clone.cloneTableSummary)} - - {clone.cloneStartTime ? ( - - ) : ( - 'N/A' - )} - - {clone.cloneTableSummary.fetchCompleted ? ( - - - {clone.cloneTableSummary.numPartitionsCompleted} /{' '} - {clone.cloneTableSummary.numPartitionsTotal} - - ) : ( - N/A - )} - - {clone.cloneTableSummary.fetchCompleted - ? clone.cloneTableSummary.numRowsSynced - : 0} - - {clone.cloneTableSummary.fetchCompleted ? ( - - - - ) : ( - N/A - )} - - ))} -
+ {[ + { data: inProgressTableLoads, title: 'In progress' }, + { data: completedTableLoads, title: 'Completed tables' }, + ].map((tableLoads, index) => ( + + ))}
); }; diff --git a/ui/app/mirrors/[mirrorId]/snapshotTable.tsx b/ui/app/mirrors/[mirrorId]/snapshotTable.tsx new file mode 100644 index 0000000000..65776d5dd6 --- /dev/null +++ b/ui/app/mirrors/[mirrorId]/snapshotTable.tsx @@ -0,0 +1,249 @@ +'use client'; +import SelectTheme from '@/app/styles/select'; +import TimeLabel from '@/components/TimeComponent'; +import { CloneTableSummary } from '@/grpc_generated/route'; +import { Badge } from '@/lib/Badge/Badge'; +import { Button } from '@/lib/Button'; +import { Icon } from '@/lib/Icon'; +import { Label } from '@/lib/Label'; +import { ProgressBar } from '@/lib/ProgressBar'; +import { SearchField } from '@/lib/SearchField'; +import { Table, TableCell, TableRow } from '@/lib/Table'; +import Link from 'next/link'; +import { useMemo, useState } from 'react'; +import ReactSelect from 'react-select'; +import { TableCloneSummary } from './snapshot'; + +const ROWS_PER_PAGE = 5; + +const getStatus = (clone: CloneTableSummary) => { + if (clone.consolidateCompleted) { + return ( + + +
Done
+
+ ); + } + if (!clone.fetchCompleted) { + return ( + + +
Fetching
+
+ ); + } + + if (clone.numPartitionsCompleted == clone.numPartitionsTotal) { + return ( + + +
Consolidating
+
+ ); + } + + return ( + + +
Syncing
+
+ ); +}; + +const SnapshotTable = ({ + tableLoads, + title, +}: { + tableLoads: TableCloneSummary[]; + title: string; +}) => { + const [sortField, setSortField] = useState< + 'cloneStartTime' | 'avgTimePerPartition' + >('cloneStartTime'); + + const [currentPage, setCurrentPage] = useState(1); + const totalPages = Math.ceil(tableLoads.length / ROWS_PER_PAGE); + const [searchQuery, setSearchQuery] = useState(''); + const [sortDir, setSortDir] = useState<'asc' | 'dsc'>('dsc'); + const displayedLoads = useMemo(() => { + const shownRows = tableLoads.filter((row: TableCloneSummary) => + row.cloneTableSummary.tableName + ?.toLowerCase() + .includes(searchQuery.toLowerCase()) + ); + shownRows.sort((a, b) => { + const aValue = a[sortField]; + const bValue = b[sortField]; + if (aValue === null || bValue === null) { + return 0; + } + + if (aValue < bValue) { + return sortDir === 'dsc' ? 1 : -1; + } else if (aValue > bValue) { + return sortDir === 'dsc' ? -1 : 1; + } else { + return 0; + } + }); + + const startRow = (currentPage - 1) * ROWS_PER_PAGE; + const endRow = startRow + ROWS_PER_PAGE; + return shownRows.length > ROWS_PER_PAGE + ? shownRows.slice(startRow, endRow) + : shownRows; + }, [tableLoads, currentPage, searchQuery, sortField, sortDir]); + + const handlePrevPage = () => { + if (currentPage > 1) { + setCurrentPage(currentPage - 1); + } + }; + + const handleNextPage = () => { + if (currentPage < totalPages) { + setCurrentPage(currentPage + 1); + } + }; + + const sortOptions = [ + { value: 'cloneStartTime', label: 'Start Time' }, + { value: 'avgTimePerPartition', label: 'Time Per Partition' }, + ]; + return ( + {title}} + toolbar={{ + left: ( +
+ + + + +
+ { + const sortVal = + (val?.value as 'cloneStartTime' | 'avgTimePerPartition') ?? + 'cloneStartTime'; + setSortField(sortVal); + }} + value={{ + value: sortField, + label: sortOptions.find((opt) => opt.value === sortField) + ?.label, + }} + defaultValue={{ + value: 'cloneStartTime', + label: 'Start Time', + }} + theme={SelectTheme} + /> +
+ + +
+ ), + right: ( + ) => + setSearchQuery(e.target.value) + } + /> + ), + }} + header={ + + {[ + 'Table Identifier', + 'Status', + 'Sync Start', + 'Progress Partitions', + 'Num Rows Processed', + 'Avg Time Per Partition', + ].map((header) => ( + + {header} + + ))} + + } + > + {displayedLoads.map((clone, index) => ( + + + + + {getStatus(clone.cloneTableSummary)} + + {clone.cloneStartTime ? ( + + ) : ( + 'N/A' + )} + + {clone.cloneTableSummary.fetchCompleted ? ( + + + {clone.cloneTableSummary.numPartitionsCompleted} /{' '} + {clone.cloneTableSummary.numPartitionsTotal} + + ) : ( + N/A + )} + + {clone.cloneTableSummary.fetchCompleted + ? clone.cloneTableSummary.numRowsSynced + : 0} + + {clone.cloneTableSummary.fetchCompleted ? ( + + + + ) : ( + N/A + )} + + ))} +
+ ); +}; + +export default SnapshotTable; From 7a402b60b5ee8a1afbf26a3318243909b7c72d79 Mon Sep 17 00:00:00 2001 From: Amogh Bharadwaj Date: Fri, 12 Apr 2024 17:45:46 +0530 Subject: [PATCH 18/18] UI Mirror page: better overview (#1605) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Moves pause and resume to actions - Remove redundant state change onclick functions - Use tremor's select instead of radix dropdown - Adds tooltip for edit button saying you need to pause first - Shows sync interval instead of last synced at, which was a misleading piece of info as it implies data is on target but actually normalise is still running - Adds line in edit page saying you need to alter publication for custom publications Screenshot 2024-04-11 at 10 27 23 PM Screenshot 2024-04-11 at 10 30 38 PM --- ui/app/mirrors/[mirrorId]/cdcDetails.tsx | 115 +++++++---------------- ui/app/mirrors/[mirrorId]/edit/page.tsx | 36 ++++--- ui/app/mirrors/[mirrorId]/handlers.ts | 38 ++++++++ ui/components/EditButton.tsx | 35 ++++--- ui/components/MirrorActionsDropdown.tsx | 74 +++++++-------- ui/components/PauseOrResumeButton.tsx | 48 ++++++++++ ui/components/ResyncDialog.tsx | 4 +- 7 files changed, 197 insertions(+), 153 deletions(-) create mode 100644 ui/app/mirrors/[mirrorId]/handlers.ts create mode 100644 ui/components/PauseOrResumeButton.tsx diff --git a/ui/app/mirrors/[mirrorId]/cdcDetails.tsx b/ui/app/mirrors/[mirrorId]/cdcDetails.tsx index 7b0cfe9a74..456bacb0c7 100644 --- a/ui/app/mirrors/[mirrorId]/cdcDetails.tsx +++ b/ui/app/mirrors/[mirrorId]/cdcDetails.tsx @@ -5,13 +5,12 @@ import PeerButton from '@/components/PeerComponent'; import TimeLabel from '@/components/TimeComponent'; import { FlowConnectionConfigs, FlowStatus } from '@/grpc_generated/flow'; import { dBTypeFromJSON } from '@/grpc_generated/peers'; -import { FlowStateChangeRequest } from '@/grpc_generated/route'; -import { Button } from '@/lib/Button'; -import { Icon } from '@/lib/Icon'; import { Label } from '@/lib/Label'; -import moment from 'moment'; +import { ProgressCircle } from '@/lib/ProgressCircle'; import Link from 'next/link'; +import { useEffect, useState } from 'react'; import MirrorValues from './configValues'; +import { getCurrentIdleTimeout } from './handlers'; import { RowDataFormatter } from './rowsDisplay'; import TablePairs from './tablePairs'; @@ -22,13 +21,8 @@ type props = { mirrorStatus: FlowStatus; }; function CdcDetails({ syncs, createdAt, mirrorConfig, mirrorStatus }: props) { - let lastSyncedAt = moment( - syncs.length > 1 - ? syncs[1]?.endTime - : syncs.length - ? syncs[0]?.startTime - : new Date() - ).fromNow(); + const [syncInterval, getSyncInterval] = useState(); + let rowsSynced = syncs.reduce((acc, sync) => { if (sync.endTime !== null) { return acc + sync.numRows; @@ -37,6 +31,11 @@ function CdcDetails({ syncs, createdAt, mirrorConfig, mirrorStatus }: props) { }, 0); const tablesSynced = mirrorConfig.tableMappings; + useEffect(() => { + getCurrentIdleTimeout(mirrorConfig.flowJobName).then((res) => { + getSyncInterval(res); + }); + }, [mirrorConfig.flowJobName]); return ( <>
@@ -53,15 +52,11 @@ function CdcDetails({ syncs, createdAt, mirrorConfig, mirrorStatus }: props) { width: 'fit-content', borderRadius: '1rem', border: '1px solid rgba(0,0,0,0.1)', - cursor: 'pointer', - display: 'flex', - alignItems: 'center', }} > - {statusChangeHandle(mirrorConfig, mirrorStatus)}
@@ -105,11 +100,11 @@ function CdcDetails({ syncs, createdAt, mirrorConfig, mirrorStatus }: props) {
- +
@@ -144,71 +139,6 @@ function CdcDetails({ syncs, createdAt, mirrorConfig, mirrorStatus }: props) { ); } -function statusChangeHandle( - mirrorConfig: FlowConnectionConfigs, - mirrorStatus: FlowStatus -) { - // hopefully there's a better way to do this cast - if (mirrorStatus.toString() === FlowStatus[FlowStatus.STATUS_RUNNING]) { - return ( - - ); - } else if (mirrorStatus.toString() === FlowStatus[FlowStatus.STATUS_PAUSED]) { - return ( - - ); - } else { - return ( - - ); - } -} - function formatStatus(mirrorStatus: FlowStatus) { const mirrorStatusLower = mirrorStatus .toString() @@ -220,4 +150,25 @@ function formatStatus(mirrorStatus: FlowStatus) { ); } +const SyncIntervalLabel: React.FC<{ syncInterval?: number }> = ({ + syncInterval, +}) => { + let formattedInterval: string; + + if (!syncInterval) { + return ; + } + if (syncInterval >= 3600) { + const hours = Math.floor(syncInterval / 3600); + formattedInterval = `${hours} hour${hours !== 1 ? 's' : ''}`; + } else if (syncInterval >= 60) { + const minutes = Math.floor(syncInterval / 60); + formattedInterval = `${minutes} minute${minutes !== 1 ? 's' : ''}`; + } else { + formattedInterval = `${syncInterval} second${syncInterval !== 1 ? 's' : ''}`; + } + + return ; +}; + export default CdcDetails; diff --git a/ui/app/mirrors/[mirrorId]/edit/page.tsx b/ui/app/mirrors/[mirrorId]/edit/page.tsx index 9f65a72d8b..9c738fb712 100644 --- a/ui/app/mirrors/[mirrorId]/edit/page.tsx +++ b/ui/app/mirrors/[mirrorId]/edit/page.tsx @@ -21,6 +21,7 @@ import TableMapping from '../../create/cdc/tablemapping'; import { reformattedTableMapping } from '../../create/handlers'; import { blankCDCSetting } from '../../create/helpers/common'; import * as styles from '../../create/styles'; +import { getMirrorState } from '../handlers'; type EditMirrorProps = { params: { mirrorId: string }; @@ -41,26 +42,19 @@ const EditMirror = ({ params: { mirrorId } }: EditMirrorProps) => { const { push } = useRouter(); const fetchStateAndUpdateDeps = useCallback(async () => { - await fetch('/api/mirrors/state', { - method: 'POST', - body: JSON.stringify({ - flowJobName: mirrorId, - }), - }) - .then((res) => res.json()) - .then((res) => { - setMirrorState(res); - - setConfig({ - batchSize: - (res as MirrorStatusResponse).cdcStatus?.config?.maxBatchSize || - defaultBatchSize, - idleTimeout: - (res as MirrorStatusResponse).cdcStatus?.config - ?.idleTimeoutSeconds || defaultIdleTimeout, - additionalTables: [], - }); + await getMirrorState(mirrorId).then((res) => { + setMirrorState(res); + + setConfig({ + batchSize: + (res as MirrorStatusResponse).cdcStatus?.config?.maxBatchSize || + defaultBatchSize, + idleTimeout: + (res as MirrorStatusResponse).cdcStatus?.config?.idleTimeoutSeconds || + defaultIdleTimeout, + additionalTables: [], }); + }); }, [mirrorId, defaultBatchSize, defaultIdleTimeout]); useEffect(() => { @@ -183,6 +177,10 @@ const EditMirror = ({ params: { mirrorId } }: EditMirrorProps) => { been completed.

The replication slot will grow during this period. +

+ For custom publications, ensure that the tables are part of the + publication you provided. This can be done with ALTER PUBLICATION + pubname ADD TABLE table1, table2; )} diff --git a/ui/app/mirrors/[mirrorId]/handlers.ts b/ui/app/mirrors/[mirrorId]/handlers.ts new file mode 100644 index 0000000000..bd6e0d3e2f --- /dev/null +++ b/ui/app/mirrors/[mirrorId]/handlers.ts @@ -0,0 +1,38 @@ +import { FlowConnectionConfigs, FlowStatus } from '@/grpc_generated/flow'; +import { + FlowStateChangeRequest, + MirrorStatusResponse, +} from '@/grpc_generated/route'; + +export const getMirrorState = async (mirrorId: string) => { + return await fetch('/api/mirrors/state', { + method: 'POST', + body: JSON.stringify({ + flowJobName: mirrorId, + }), + }).then((res) => res.json()); +}; + +export const getCurrentIdleTimeout = async (mirrorId: string) => { + return await getMirrorState(mirrorId).then((res) => { + return (res as MirrorStatusResponse).cdcStatus?.config?.idleTimeoutSeconds; + }); +}; + +export const changeFlowState = async ( + mirrorConfig: FlowConnectionConfigs, + flowState: FlowStatus +) => { + const req: FlowStateChangeRequest = { + flowJobName: mirrorConfig.flowJobName, + sourcePeer: mirrorConfig.source, + destinationPeer: mirrorConfig.destination, + requestedFlowState: flowState, + }; + await fetch(`/api/mirrors/state_change`, { + method: 'POST', + body: JSON.stringify(req), + cache: 'no-store', + }); + window.location.reload(); +}; diff --git a/ui/components/EditButton.tsx b/ui/components/EditButton.tsx index 5e87cbfaa8..965b5a1855 100644 --- a/ui/components/EditButton.tsx +++ b/ui/components/EditButton.tsx @@ -2,6 +2,7 @@ import { Button } from '@/lib/Button'; import { Label } from '@/lib/Label'; import { ProgressCircle } from '@/lib/ProgressCircle'; +import { Tooltip } from '@/lib/Tooltip'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; @@ -20,22 +21,32 @@ const EditButton = ({ router.push(toLink); }; return ( - + + ); }; diff --git a/ui/components/MirrorActionsDropdown.tsx b/ui/components/MirrorActionsDropdown.tsx index c11e68e019..dd1eafe787 100644 --- a/ui/components/MirrorActionsDropdown.tsx +++ b/ui/components/MirrorActionsDropdown.tsx @@ -1,12 +1,12 @@ 'use client'; +import { getMirrorState } from '@/app/mirrors/[mirrorId]/handlers'; import EditButton from '@/components/EditButton'; import { ResyncDialog } from '@/components/ResyncDialog'; -import { FlowConnectionConfigs } from '@/grpc_generated/flow'; -import { Button } from '@/lib/Button/Button'; -import { Icon } from '@/lib/Icon'; -import { Label } from '@/lib/Label/Label'; -import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; +import { FlowConnectionConfigs, FlowStatus } from '@/grpc_generated/flow'; +import { MirrorStatusResponse } from '@/grpc_generated/route'; +import { Select, SelectItem } from '@tremor/react'; import { useEffect, useState } from 'react'; +import PauseOrResumeButton from './PauseOrResumeButton'; const MirrorActions = ({ mirrorConfig, @@ -21,50 +21,48 @@ const MirrorActions = ({ canResync: boolean; isNotPaused: boolean; }) => { + const [mirrorStatus, setMirrorStatus] = useState(); const [mounted, setMounted] = useState(false); - const [open, setOpen] = useState(false); - const handleToggle = () => { - setOpen((prevOpen) => !prevOpen); - }; - useEffect(() => setMounted(true), []); + + useEffect(() => { + getMirrorState(mirrorConfig.flowJobName).then( + (res: MirrorStatusResponse) => { + setMirrorStatus(res.currentFlowState); + } + ); + setMounted(true); + }, [mirrorConfig.flowJobName]); + if (mounted) return ( - - - - - - - +
+ +
); return <>; }; diff --git a/ui/components/PauseOrResumeButton.tsx b/ui/components/PauseOrResumeButton.tsx new file mode 100644 index 0000000000..cdf3b600ef --- /dev/null +++ b/ui/components/PauseOrResumeButton.tsx @@ -0,0 +1,48 @@ +'use client'; +import { changeFlowState } from '@/app/mirrors/[mirrorId]/handlers'; +import { FlowConnectionConfigs, FlowStatus } from '@/grpc_generated/flow'; +import { Button } from '@/lib/Button'; +import { Label } from '@/lib/Label/Label'; + +function PauseOrResumeButton({ + mirrorConfig, + mirrorStatus, +}: { + mirrorConfig: FlowConnectionConfigs; + mirrorStatus: FlowStatus; +}) { + if (mirrorStatus.toString() === FlowStatus[FlowStatus.STATUS_RUNNING]) { + return ( + + ); + } else if (mirrorStatus.toString() === FlowStatus[FlowStatus.STATUS_PAUSED]) { + return ( + + ); + } else { + return ( + + ); + } +} + +export default PauseOrResumeButton; diff --git a/ui/components/ResyncDialog.tsx b/ui/components/ResyncDialog.tsx index 6caeea3821..14d6c13cfb 100644 --- a/ui/components/ResyncDialog.tsx +++ b/ui/components/ResyncDialog.tsx @@ -62,8 +62,8 @@ export const ResyncDialog = ({ noInteract={true} size='xLarge' triggerButton={ - } >