-
Notifications
You must be signed in to change notification settings - Fork 198
Expand file tree
/
Copy pathqrep_query_executor.go
More file actions
330 lines (294 loc) · 9.57 KB
/
Copy pathqrep_query_executor.go
File metadata and controls
330 lines (294 loc) · 9.57 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
package connpostgres
import (
"context"
"fmt"
"log/slog"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"github.com/jackc/pgx/v5/pgtype"
"go.temporal.io/sdk/log"
"github.com/PeerDB-io/peerdb/flow/model"
"github.com/PeerDB-io/peerdb/flow/model/qvalue"
"github.com/PeerDB-io/peerdb/flow/shared"
)
type QRepQueryExecutor struct {
*PostgresConnector
logger log.Logger
snapshot string
flowJobName string
partitionID string
}
func (c *PostgresConnector) NewQRepQueryExecutor(ctx context.Context,
flowJobName string, partitionID string,
) (*QRepQueryExecutor, error) {
return c.NewQRepQueryExecutorSnapshot(ctx, "", flowJobName, partitionID)
}
func (c *PostgresConnector) NewQRepQueryExecutorSnapshot(ctx context.Context,
snapshot string, flowJobName string, partitionID string,
) (*QRepQueryExecutor, error) {
_, err := c.fetchCustomTypeMapping(ctx)
if err != nil {
c.logger.Error("[pg_query_executor] failed to fetch custom type mapping", slog.Any("error", err))
return nil, fmt.Errorf("failed to fetch custom type mapping: %w", err)
}
return &QRepQueryExecutor{
PostgresConnector: c,
snapshot: snapshot,
flowJobName: flowJobName,
partitionID: partitionID,
logger: log.With(c.logger, slog.String(string(shared.PartitionIDKey), partitionID)),
}, nil
}
func (qe *QRepQueryExecutor) ExecuteQuery(ctx context.Context, query string, args ...any) (pgx.Rows, error) {
rows, err := qe.conn.Query(ctx, query, args...)
if err != nil {
qe.logger.Error("[pg_query_executor] failed to execute query", slog.Any("error", err))
return nil, err
}
return rows, nil
}
func (qe *QRepQueryExecutor) executeQueryInTx(ctx context.Context, tx pgx.Tx, cursorName string, fetchSize int) (pgx.Rows, error) {
qe.logger.Info("Executing query in transaction")
q := fmt.Sprintf("FETCH %d FROM %s", fetchSize, cursorName)
rows, err := tx.Query(ctx, q)
if err != nil {
qe.logger.Error("[pg_query_executor] failed to execute query in tx", slog.Any("error", err))
return nil, err
}
return rows, nil
}
// FieldDescriptionsToSchema converts a slice of pgconn.FieldDescription to a QRecordSchema.
func (qe *QRepQueryExecutor) fieldDescriptionsToSchema(fds []pgconn.FieldDescription) qvalue.QRecordSchema {
qfields := make([]qvalue.QField, len(fds))
for i, fd := range fds {
ctype := qe.postgresOIDToQValueKind(fd.DataTypeOID, qe.customTypeMapping)
// there isn't a way to know if a column is nullable or not
if ctype == qvalue.QValueKindNumeric {
precision, scale := shared.ParseNumericTypmod(fd.TypeModifier)
qfields[i] = qvalue.QField{
Name: fd.Name,
Type: ctype,
Nullable: true,
Precision: precision,
Scale: scale,
}
} else {
qfields[i] = qvalue.QField{
Name: fd.Name,
Type: ctype,
Nullable: true,
}
}
}
return qvalue.NewQRecordSchema(qfields)
}
func (qe *QRepQueryExecutor) processRowsStream(
ctx context.Context,
cursorName string,
stream *model.QRecordStream,
rows pgx.Rows,
fieldDescriptions []pgconn.FieldDescription,
) (int64, int64, error) {
var numRows int64
var numBytes int64
const logPerRows = 10000
for rows.Next() {
if err := ctx.Err(); err != nil {
qe.logger.Info("Context canceled, exiting processRowsStream early")
return numRows, numBytes, err
}
record, err := qe.mapRowToQRecord(rows, fieldDescriptions)
if err != nil {
qe.logger.Error("[pg_query_executor] failed to map row to QRecord", slog.Any("error", err))
return numRows, numBytes, fmt.Errorf("failed to map row to QRecord: %w", err)
}
stream.Records <- record
numRows++
for _, val := range rows.RawValues() {
numBytes += int64(len(val))
}
if numRows%logPerRows == 0 {
qe.logger.Info("processing row stream", slog.String("cursor", cursorName),
slog.Int64("records", numRows), slog.Int64("bytes", numBytes))
}
}
qe.logger.Info("processed row stream", slog.String("cursor", cursorName), slog.Int64("records", numRows), slog.Int64("bytes", numBytes))
return numRows, numBytes, nil
}
func (qe *QRepQueryExecutor) processFetchedRows(
ctx context.Context,
query string,
tx pgx.Tx,
cursorName string,
fetchSize int,
stream *model.QRecordStream,
) (int64, int64, error) {
rows, err := qe.executeQueryInTx(ctx, tx, cursorName, fetchSize)
if err != nil {
qe.logger.Error("[pg_query_executor] failed to execute query in tx",
slog.Any("error", err), slog.String("query", query))
return 0, 0, fmt.Errorf("[pg_query_executor] failed to execute query in tx: %w", err)
}
defer rows.Close()
fieldDescriptions := rows.FieldDescriptions()
if !stream.IsSchemaSet() {
schema := qe.fieldDescriptionsToSchema(fieldDescriptions)
stream.SetSchema(schema)
}
numRows, numBytes, err := qe.processRowsStream(ctx, cursorName, stream, rows, fieldDescriptions)
if err != nil {
qe.logger.Error("[pg_query_executor] failed to process rows", slog.Any("error", err))
return numRows, numBytes, fmt.Errorf("failed to process rows: %w", err)
}
if err := rows.Err(); err != nil {
qe.logger.Error("[pg_query_executor] row iteration failed",
slog.String("query", query), slog.Any("error", err))
return numRows, numBytes, fmt.Errorf("[pg_query_executor] row iteration failed '%s': %w", query, err)
}
return numRows, numBytes, nil
}
func (qe *QRepQueryExecutor) ExecuteAndProcessQuery(
ctx context.Context,
query string,
args ...any,
) (*model.QRecordBatch, error) {
stream := model.NewQRecordStream(1024)
errors := make(chan struct{})
var errorsError error
qe.logger.Info("Executing and processing query", slog.String("query", query))
// must wait on errors to close before returning to maintain qe.conn exclusion
go func() {
defer close(errors)
if _, _, err := qe.ExecuteAndProcessQueryStream(ctx, stream, query, args...); err != nil {
qe.logger.Error("[pg_query_executor] failed to execute and process query stream", slog.Any("error", err))
errorsError = err
}
}()
select {
case <-errors:
if errorsError == nil {
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-stream.SchemaChan():
schema, err := stream.Schema()
if err != nil {
return nil, err
}
return &model.QRecordBatch{
Schema: schema,
Records: nil,
}, nil
}
}
return nil, errorsError
case <-stream.SchemaChan():
schema, err := stream.Schema()
if err != nil {
return nil, err
}
batch := &model.QRecordBatch{
Schema: schema,
Records: nil,
}
for record := range stream.Records {
batch.Records = append(batch.Records, record)
}
<-errors
if errorsError != nil {
return nil, errorsError
}
if err := stream.Err(); err != nil {
return nil, fmt.Errorf("[pg] failed to get record from stream: %w", err)
}
return batch, nil
}
}
func (qe *QRepQueryExecutor) ExecuteAndProcessQueryStream(
ctx context.Context,
stream *model.QRecordStream,
query string,
args ...any,
) (int64, int64, error) {
return qe.ExecuteQueryIntoSink(
ctx,
RecordStreamSink{QRecordStream: stream},
query,
args...,
)
}
func (qe *QRepQueryExecutor) ExecuteQueryIntoSink(
ctx context.Context,
sink QRepPullSink,
query string,
args ...any,
) (int64, int64, error) {
qe.logger.Info("Executing and processing query stream", slog.String("query", query))
defer sink.Close(nil)
tx, err := qe.conn.BeginTx(ctx, pgx.TxOptions{
AccessMode: pgx.ReadOnly,
IsoLevel: pgx.RepeatableRead,
})
if err != nil {
qe.logger.Error("[pg_query_executor] failed to begin transaction", slog.Any("error", err))
err := fmt.Errorf("[pg_query_executor] failed to begin transaction: %w", err)
sink.Close(err)
return 0, 0, err
}
totalRecords, totalBytes, err := sink.ExecuteQueryWithTx(ctx, qe, tx, query, args...)
if err != nil {
sink.Close(err)
}
return totalRecords, totalBytes, err
}
func (qe *QRepQueryExecutor) ExecuteQueryIntoSinkGettingCurrentSnapshotXmin(
ctx context.Context,
sink QRepPullSink,
query string,
args ...any,
) (int64, int64, int64, error) {
var currentSnapshotXmin pgtype.Int8
qe.logger.Info("Executing and processing query stream", slog.String("query", query))
defer sink.Close(nil)
tx, err := qe.conn.BeginTx(ctx, pgx.TxOptions{
AccessMode: pgx.ReadOnly,
IsoLevel: pgx.RepeatableRead,
})
if err != nil {
qe.logger.Error("[pg_query_executor] failed to begin transaction", slog.Any("error", err))
err := fmt.Errorf("[pg_query_executor] failed to begin transaction: %w", err)
sink.Close(err)
return 0, 0, currentSnapshotXmin.Int64, err
}
if err := tx.QueryRow(ctx, "select txid_snapshot_xmin(txid_current_snapshot())").Scan(¤tSnapshotXmin); err != nil {
qe.logger.Error("[pg_query_executor] failed to get current snapshot xmin", slog.Any("error", err))
sink.Close(err)
return 0, 0, currentSnapshotXmin.Int64, err
}
totalRecords, totalBytes, err := sink.ExecuteQueryWithTx(ctx, qe, tx, query, args...)
if err != nil {
sink.Close(err)
}
return totalRecords, totalBytes, currentSnapshotXmin.Int64, err
}
func (qe *QRepQueryExecutor) mapRowToQRecord(
row pgx.Rows,
fds []pgconn.FieldDescription,
) ([]qvalue.QValue, error) {
// make vals an empty array of QValue of size len(fds)
record := make([]qvalue.QValue, len(fds))
values, err := row.Values()
if err != nil {
qe.logger.Error("[pg_query_executor] failed to get values from row", slog.Any("error", err))
return nil, fmt.Errorf("failed to scan row: %w", err)
}
for i, fd := range fds {
tmp, err := qe.parseFieldFromPostgresOID(fd.DataTypeOID, values[i], qe.customTypeMapping)
if err != nil {
qe.logger.Error("[pg_query_executor] failed to parse field", slog.Any("error", err))
return nil, fmt.Errorf("failed to parse field: %w", err)
}
record[i] = tmp
}
return record, nil
}