@@ -20,6 +20,28 @@ import (
2020 "github.com/PeerDB-io/peerdb/flow/shared/types"
2121)
2222
23+ func (c * MySqlConnector ) GetDataTypeOfWatermarkColumn (
24+ ctx context.Context ,
25+ watermarkTableName string ,
26+ watermarkColumn string ,
27+ ) (types.QValueKind , error ) {
28+ if watermarkColumn == "" {
29+ return "" , errors .New ("watermark column is not specified in the config" )
30+ }
31+
32+ query := fmt .Sprintf ("SELECT `%s` FROM %s LIMIT 0" , watermarkColumn , watermarkTableName )
33+ rs , err := c .Execute (ctx , query )
34+ if err != nil {
35+ return "" , fmt .Errorf ("failed to execute query for watermark column type: %w" , err )
36+ }
37+
38+ if len (rs .Fields ) == 0 {
39+ return "" , errors .New ("no fields returned from select query: " + query )
40+ }
41+
42+ return qkindFromMysql (rs .Fields [0 ])
43+ }
44+
2345func (c * MySqlConnector ) GetQRepPartitions (
2446 ctx context.Context ,
2547 config * protos.QRepConfig ,
@@ -46,7 +68,7 @@ func (c *MySqlConnector) GetQRepPartitions(
4668
4769 whereClause := ""
4870 if last != nil && last .Range != nil {
49- whereClause = fmt .Sprintf ("WHERE %s > $1 " , quotedWatermarkColumn )
71+ whereClause = fmt .Sprintf ("WHERE %s > ? " , quotedWatermarkColumn )
5072 }
5173 parsedWatermarkTable , err := utils .ParseSchemaTable (config .WatermarkTable )
5274 if err != nil {
@@ -64,7 +86,7 @@ func (c *MySqlConnector) GetQRepPartitions(
6486 case * protos.PartitionRange_UintRange :
6587 minVal = lastRange .UintRange .End
6688 case * protos.PartitionRange_TimestampRange :
67- minVal = lastRange .TimestampRange .End .AsTime ()
89+ minVal = lastRange .TimestampRange .End .AsTime (). String ()
6890 }
6991 c .logger .Info (fmt .Sprintf ("count query: %s - minVal: %v" , countQuery , minVal ))
7092
@@ -99,62 +121,115 @@ func (c *MySqlConnector) GetQRepPartitions(
99121 if totalRows % numRowsPerPartition != 0 {
100122 numPartitions ++
101123 }
124+
125+ watermarkQKind , err := c .GetDataTypeOfWatermarkColumn (ctx , parsedWatermarkTable .MySQL (), config .WatermarkColumn )
126+ if err != nil {
127+ return nil , fmt .Errorf ("failed to get data type of watermark column %s: %w" , config .WatermarkColumn , err )
128+ }
129+
102130 c .logger .Info (fmt .Sprintf ("total rows: %d, num partitions: %d, num rows per partition: %d" ,
103131 totalRows , numPartitions , numRowsPerPartition ))
104132 var rs * mysql.Result
105- if minVal != nil {
106- // Query to get partitions using window functions
107- partitionsQuery := fmt .Sprintf (
108- `SELECT bucket, MIN(%[2]s) AS start, MAX(%[2]s) AS end
109- FROM (
110- SELECT NTILE(%[1]d) OVER (ORDER BY %[2]s) AS bucket, %[2]s
111- FROM %[3]s WHERE %[2]s > $1
112- ) AS subquery
133+
134+ switch watermarkQKind {
135+ case types .QValueKindInt8 , types .QValueKindInt16 , types .QValueKindInt32 , types .QValueKindInt64 ,
136+ types .QValueKindUInt8 , types .QValueKindUInt16 , types .QValueKindUInt32 , types .QValueKindUInt64 :
137+ if minVal != nil {
138+ partitionsQuery := fmt .Sprintf (
139+ `WITH stats AS (
140+ SELECT MIN(%[2]s) AS min_watermark,
141+ 1.0 * (MAX(%[2]s) - MIN(%[2]s)) / (%[1]d) AS range_size
142+ FROM %[3]s WHERE %[2]s > ?
143+ )
144+ SELECT FLOOR((w.%[2]s - s.min_watermark) / s.range_size) AS bucket,
145+ MIN(w.%[2]s) AS start, MAX(w.%[2]s) AS end
146+ FROM %[3]s AS w
147+ CROSS JOIN stats AS s
148+ WHERE w.%[2]s > ?
113149 GROUP BY bucket
114- ORDER BY start` ,
115- numPartitions ,
116- quotedWatermarkColumn ,
117- parsedWatermarkTable .MySQL (),
118- )
119- c .logger .Info ("partitions query" , slog .String ("query" , partitionsQuery ), slog .Any ("minVal" , minVal ))
120- rs , err = c .Execute (ctx , partitionsQuery , minVal )
121- } else {
122- partitionsQuery := fmt .Sprintf (
123- `SELECT bucket, MIN(%[2]s) AS start, MAX(%[2]s) AS end
124- FROM (
125- SELECT NTILE(%[1]d) OVER (ORDER BY %[2]s) AS bucket, %[2]s FROM %[3]s
126- ) AS subquery
150+ ORDER BY start;` ,
151+ numPartitions ,
152+ quotedWatermarkColumn ,
153+ parsedWatermarkTable .MySQL (),
154+ )
155+ c .logger .Info ("partitions query" , slog .String ("query" , partitionsQuery ), slog .Any ("minVal" , minVal ))
156+ rs , err = c .Execute (ctx , partitionsQuery , minVal , minVal )
157+ } else {
158+ partitionsQuery := fmt .Sprintf (
159+ `WITH stats AS (
160+ SELECT MIN(%[2]s) AS min_watermark,
161+ 1.0 * (MAX(%[2]s) - MIN(%[2]s)) / (%[1]d) AS range_size
162+ FROM %[3]s
163+ )
164+ SELECT FLOOR((w.%[2]s - s.min_watermark) / s.range_size) AS bucket,
165+ MIN(w.%[2]s) AS start, MAX(w.%[2]s) AS end
166+ FROM %[3]s AS w
167+ CROSS JOIN stats AS s
127168 GROUP BY bucket
128- ORDER BY start` ,
129- numPartitions ,
130- quotedWatermarkColumn ,
131- parsedWatermarkTable .MySQL (),
132- )
133- c .logger .Info ("partitions query" , slog .String ("query" , partitionsQuery ))
134- rs , err = c .Execute (ctx , partitionsQuery )
135- }
136- if err != nil {
137- return nil , fmt .Errorf ("failed to query for partitions: %w" , err )
169+ ORDER BY start;` ,
170+ numPartitions ,
171+ quotedWatermarkColumn ,
172+ parsedWatermarkTable .MySQL (),
173+ )
174+ c .logger .Info ("partitions query" , slog .String ("query" , partitionsQuery ))
175+ rs , err = c .Execute (ctx , partitionsQuery )
176+ }
177+ if err != nil {
178+ return nil , fmt .Errorf ("failed to query for partitions: %w" , err )
179+ }
180+ case types .QValueKindTimestamp , types .QValueKindTimestampTZ :
181+ if minVal != nil {
182+ partitionsQuery := fmt .Sprintf (
183+ `WITH stats AS (
184+ SELECT MIN(%[2]s) AS min_watermark,
185+ 1.0 * (TIMESTAMPDIFF(MICROSECOND, MAX(%[2]s), MIN(%[2]s)) / (%[1]d)) AS range_size
186+ FROM %[3]s WHERE %[2]s > ?
187+ )
188+ SELECT FLOOR(TIMESTAMPDIFF(MICROSECOND, w.%[2]s, s.min_watermark) / s.range_size) AS bucket,
189+ MIN(w.%[2]s) AS start, MAX(w.%[2]s) AS end
190+ FROM %[3]s AS w
191+ CROSS JOIN stats AS s
192+ WHERE w.%[2]s > ?
193+ GROUP BY bucket
194+ ORDER BY start;` ,
195+ numPartitions ,
196+ quotedWatermarkColumn ,
197+ parsedWatermarkTable .MySQL (),
198+ )
199+ c .logger .Info ("partitions query" , slog .String ("query" , partitionsQuery ), slog .Any ("minVal" , minVal ))
200+ rs , err = c .Execute (ctx , partitionsQuery , minVal )
201+ } else {
202+ partitionsQuery := fmt .Sprintf (
203+ `WITH stats AS (
204+ SELECT MIN(%[2]s) AS min_watermark,
205+ 1.0 * (TIMESTAMPDIFF(MICROSECOND, MAX(%[2]s), MIN(%[2]s)) / (%[1]d)) AS range_size
206+ FROM %[3]s
207+ )
208+ SELECT FLOOR(TIMESTAMPDIFF(MICROSECOND, w.%[2]s, s.min_watermark) / s.range_size) AS bucket,
209+ MIN(w.%[2]s) AS start, MAX(w.%[2]s) AS end
210+ FROM %[3]s AS w
211+ CROSS JOIN stats AS s
212+ GROUP BY bucket
213+ ORDER BY start;` ,
214+ numPartitions ,
215+ quotedWatermarkColumn ,
216+ parsedWatermarkTable .MySQL (),
217+ )
218+ c .logger .Info ("partitions query" , slog .String ("query" , partitionsQuery ))
219+ rs , err = c .Execute (ctx , partitionsQuery )
220+ }
221+ if err != nil {
222+ return nil , fmt .Errorf ("failed to query for partitions: %w" , err )
223+ }
138224 }
139225
140- qk1 , err := qkindFromMysql (rs .Fields [1 ])
141- if err != nil {
142- return nil , err
143- }
144- qk2 , err := qkindFromMysql (rs .Fields [2 ])
145- if err != nil {
146- return nil , err
147- }
148- if qk1 != qk2 {
149- return nil , fmt .Errorf ("low/high of partition range should be same type, got low:%s, high:%s" , qk1 , qk2 )
150- }
151226 partitionHelper := utils .NewPartitionHelper (c .logger )
152227 for _ , row := range rs .Values {
153- val1 , err := QValueFromMysqlFieldValue (qk1 , row [1 ])
228+ val1 , err := QValueFromMysqlFieldValue (watermarkQKind , row [1 ])
154229 if err != nil {
155230 return nil , err
156231 }
157- val2 , err := QValueFromMysqlFieldValue (qk2 , row [2 ])
232+ val2 , err := QValueFromMysqlFieldValue (watermarkQKind , row [2 ])
158233 if err != nil {
159234 return nil , err
160235 }
@@ -169,7 +244,7 @@ func (c *MySqlConnector) GetQRepPartitions(
169244func (c * MySqlConnector ) PullQRepRecords (
170245 ctx context.Context ,
171246 config * protos.QRepConfig ,
172- last * protos.QRepPartition ,
247+ partition * protos.QRepPartition ,
173248 stream * model.QRecordStream ,
174249) (int64 , int64 , error ) {
175250 tableSchema , err := c .getTableSchemaForTable (ctx , config .Env ,
@@ -241,7 +316,7 @@ func (c *MySqlConnector) PullQRepRecords(
241316 return nil
242317 }
243318
244- if last .FullTablePartition {
319+ if partition .FullTablePartition {
245320 // this is a full table partition, so just run the query
246321 if err := c .ExecuteSelectStreaming (ctx , config .Query , & rs , onRow , onResult ); err != nil {
247322 return 0 , 0 , err
@@ -251,7 +326,7 @@ func (c *MySqlConnector) PullQRepRecords(
251326 var rangeEnd string
252327
253328 // Depending on the type of the range, convert the range into the correct type
254- switch x := last .Range .Range .(type ) {
329+ switch x := partition .Range .Range .(type ) {
255330 case * protos.PartitionRange_IntRange :
256331 rangeStart = strconv .FormatInt (x .IntRange .Start , 10 )
257332 rangeEnd = strconv .FormatInt (x .IntRange .End , 10 )
0 commit comments