1 package sinks
2
3 import (
4 "context"
5 _ "embed"
6 "errors"
7 "fmt"
8 "maps"
9 "slices"
10 "strings"
11 "sync"
12 "time"
13
14 jsoniter "github.com/json-iterator/go"
15
16 "github.com/cybertec-postgresql/pgwatch/v6/internal/db"
17 "github.com/cybertec-postgresql/pgwatch/v6/internal/log"
18 "github.com/cybertec-postgresql/pgwatch/v6/internal/metrics"
19 migrator "github.com/cybertec-postgresql/pgx-migrator"
20 "github.com/jackc/pgx/v5"
21 "github.com/jackc/pgx/v5/pgconn"
22 "github.com/jackc/pgx/v5/pgxpool"
23 )
24
25 var (
26 cacheLimit = 256
27 highLoadTimeout = time.Second * 5
28 targetColumns = [...]string{"time", "dbname", "data", "tag_data"}
29 )
30
31
32 var sqlMetricAdminSchema string
33
34
35 var sqlMetricAdminFunctions string
36
37
38 var sqlMetricEnsurePartitionPostgres string
39
40
41 var sqlMetricEnsurePartitionTimescale string
42
43
44 var sqlMetricChangeChunkIntervalTimescale string
45
46
47 var sqlMetricChangeCompressionIntervalTimescale string
48
49 var (
50 metricSchemaSQLs = []string{
51 sqlMetricAdminSchema,
52 sqlMetricAdminFunctions,
53 sqlMetricEnsurePartitionPostgres,
54 sqlMetricEnsurePartitionTimescale,
55 sqlMetricChangeChunkIntervalTimescale,
56 sqlMetricChangeCompressionIntervalTimescale,
57 }
58 )
59
60
61
62
63
64 type PostgresWriter struct {
65 ctx context.Context
66 sinkDb db.PgxPoolIface
67 metricSchema DbStorageSchemaType
68 opts *CmdOpts
69 retentionInterval time.Duration
70 maintenanceInterval time.Duration
71 input chan metrics.MeasurementEnvelope
72 lastError chan error
73 forceRecreatePartitions bool
74 partitionMapMetric map[string]ExistingPartitionInfo
75
76 mu sync.Mutex
77 }
78
79
80 var _ db.Migrator = (*PostgresWriter)(nil)
81
82 func NewPostgresWriter(ctx context.Context, connstr string, opts *CmdOpts) (pgw *PostgresWriter, err error) {
83 var conn db.PgxPoolIface
84 if conn, err = db.New(ctx, connstr); err != nil {
85 return
86 }
87 return NewWriterFromPostgresConn(ctx, conn, opts)
88 }
89
90 var ErrNeedsMigration = errors.New("sink database schema is outdated, please run migrations using `pgwatch config upgrade` command")
91
92 func NewWriterFromPostgresConn(ctx context.Context, conn db.PgxPoolIface, opts *CmdOpts) (pgw *PostgresWriter, err error) {
93 l := log.GetLogger(ctx).WithField("sink", "postgres").WithField("db", conn.Config().ConnConfig.Database)
94 ctx = log.WithLogger(ctx, l)
95 pgw = &PostgresWriter{
96 ctx: ctx,
97 opts: opts,
98 input: make(chan metrics.MeasurementEnvelope, cacheLimit),
99 lastError: make(chan error),
100 sinkDb: conn,
101 forceRecreatePartitions: false,
102 partitionMapMetric: make(map[string]ExistingPartitionInfo),
103 }
104 l.Info("initialising measurements database...")
105 if err = pgw.init(); err != nil {
106 return nil, err
107 }
108 if err = pgw.ReadMetricSchemaType(); err != nil {
109 return nil, err
110 }
111 if err = pgw.EnsureBuiltinMetricDummies(); err != nil {
112 return nil, err
113 }
114 pgw.scheduleJob(pgw.maintenanceInterval, func() {
115 pgw.DeleteOldPartitions()
116 pgw.MaintainUniqueSources()
117 })
118 go pgw.poll()
119 l.Info(`measurements sink is activated`)
120 return
121 }
122
123 func (pgw *PostgresWriter) init() (err error) {
124 return db.Init(pgw.ctx, pgw.sinkDb, func(ctx context.Context, conn db.PgxIface) error {
125 var isValidPartitionInterval bool
126 if err = conn.QueryRow(ctx,
127 "SELECT extract(epoch from $1::interval), extract(epoch from $2::interval), $3::interval >= '1h'::interval",
128 pgw.opts.RetentionInterval, pgw.opts.MaintenanceInterval, pgw.opts.PartitionInterval,
129 ).Scan(&pgw.retentionInterval, &pgw.maintenanceInterval, &isValidPartitionInterval); err != nil {
130 return err
131 }
132
133
134 pgw.retentionInterval *= time.Second
135 pgw.maintenanceInterval *= time.Second
136
137 if !isValidPartitionInterval {
138 return fmt.Errorf("--partition-interval must be at least 1 hour, got: %s", pgw.opts.PartitionInterval)
139 }
140 if pgw.maintenanceInterval < 0 {
141 return errors.New("--maintenance-interval must be a positive PostgreSQL interval or 0 to disable it")
142 }
143 if pgw.retentionInterval < time.Hour && pgw.retentionInterval != 0 {
144 return errors.New("--retention must be at least 1 hour PostgreSQL interval or 0 to disable it")
145 }
146
147 exists, err := db.DoesSchemaExist(ctx, conn, "admin")
148 if err != nil || exists {
149 return err
150 }
151 for _, sql := range metricSchemaSQLs {
152 if _, err = conn.Exec(ctx, sql); err != nil {
153 return err
154 }
155 }
156 return nil
157 })
158 }
159
160 type ExistingPartitionInfo struct {
161 StartTime time.Time
162 EndTime time.Time
163 }
164
165 type MeasurementMessagePostgres struct {
166 Time time.Time
167 DBName string
168 Metric string
169 Data map[string]any
170 TagData map[string]string
171 }
172
173 type DbStorageSchemaType int
174
175 const (
176 DbStorageSchemaPostgres DbStorageSchemaType = iota
177 DbStorageSchemaTimescale
178 )
179
180 func (pgw *PostgresWriter) scheduleJob(interval time.Duration, job func()) {
181 if interval > 0 {
182 go func() {
183 for {
184 select {
185 case <-pgw.ctx.Done():
186 return
187 case <-time.After(interval):
188 job()
189 }
190 }
191 }()
192 }
193 }
194
195 func (pgw *PostgresWriter) ReadMetricSchemaType() (err error) {
196 var isTs bool
197 pgw.metricSchema = DbStorageSchemaPostgres
198 sqlSchemaType := `SELECT schema_type = 'timescale' FROM admin.storage_schema_type`
199 if err = pgw.sinkDb.QueryRow(pgw.ctx, sqlSchemaType).Scan(&isTs); err == nil && isTs {
200 pgw.metricSchema = DbStorageSchemaTimescale
201 }
202 return
203 }
204
205
206 func (pgw *PostgresWriter) SyncMetric(sourceName, metricName string, op SyncOp) error {
207 pgw.mu.Lock()
208 defer pgw.mu.Unlock()
209 if op == AddOp {
210 return errors.Join(
211 pgw.AddDBUniqueMetricToListingTable(sourceName, metricName),
212 pgw.EnsureMetricDummy(metricName),
213 )
214 }
215 return nil
216 }
217
218
219 func (pgw *PostgresWriter) EnsureBuiltinMetricDummies() (err error) {
220 for _, name := range metrics.GetDefaultBuiltInMetrics() {
221 err = errors.Join(err, pgw.EnsureMetricDummy(name))
222 }
223 return
224 }
225
226
227 func (pgw *PostgresWriter) EnsureMetricDummy(metric string) (err error) {
228 _, err = pgw.sinkDb.Exec(pgw.ctx, "SELECT admin.ensure_dummy_metrics_table($1)", metric)
229 return
230 }
231
232
233 func (pgw *PostgresWriter) Write(msg metrics.MeasurementEnvelope) error {
234 if pgw.ctx.Err() != nil {
235 return pgw.ctx.Err()
236 }
237 select {
238 case pgw.input <- msg:
239
240 case <-time.After(highLoadTimeout):
241
242 }
243 select {
244 case err := <-pgw.lastError:
245 return err
246 default:
247 return nil
248 }
249 }
250
251
252 func (pgw *PostgresWriter) poll() {
253 cache := make([]metrics.MeasurementEnvelope, 0, cacheLimit)
254 cacheTimeout := pgw.opts.BatchingDelay
255 tick := time.NewTicker(cacheTimeout)
256 for {
257 select {
258 case <-pgw.ctx.Done():
259 return
260 default:
261 select {
262 case entry := <-pgw.input:
263 cache = append(cache, entry)
264 if len(cache) < cacheLimit {
265 break
266 }
267 tick.Stop()
268 pgw.flush(cache)
269 cache = cache[:0]
270 tick = time.NewTicker(cacheTimeout)
271 case <-tick.C:
272 pgw.flush(cache)
273 cache = cache[:0]
274 case <-pgw.ctx.Done():
275 return
276 }
277 }
278 }
279 }
280
281 func newCopyFromMeasurements(rows []metrics.MeasurementEnvelope) *copyFromMeasurements {
282 return ©FromMeasurements{envelopes: rows, envelopeIdx: -1, measurementIdx: -1}
283 }
284
285 type copyFromMeasurements struct {
286 envelopes []metrics.MeasurementEnvelope
287 envelopeIdx int
288 measurementIdx int
289 metricName string
290 err error
291 }
292
293 func (c *copyFromMeasurements) NextEnvelope() bool {
294 c.envelopeIdx++
295 c.measurementIdx = -1
296 return c.envelopeIdx < len(c.envelopes)
297 }
298
299 func (c *copyFromMeasurements) Next() bool {
300 for {
301
302 if c.envelopeIdx < 0 || c.measurementIdx+1 >= len(c.envelopes[c.envelopeIdx].Data) {
303
304 if ok := c.NextEnvelope(); !ok {
305 return false
306 }
307
308 if c.metricName == "" {
309 c.metricName = c.envelopes[c.envelopeIdx].MetricName
310 } else if c.metricName != c.envelopes[c.envelopeIdx].MetricName {
311
312
313 c.envelopeIdx--
314 c.measurementIdx = len(c.envelopes[c.envelopeIdx].Data)
315 c.metricName = ""
316 return false
317 }
318 }
319
320
321 c.measurementIdx++
322 if c.measurementIdx < len(c.envelopes[c.envelopeIdx].Data) {
323 return true
324 }
325
326 }
327 }
328
329 func (c *copyFromMeasurements) EOF() bool {
330 return c.envelopeIdx >= len(c.envelopes)
331 }
332
333 func (c *copyFromMeasurements) Values() ([]any, error) {
334 row := maps.Clone(c.envelopes[c.envelopeIdx].Data[c.measurementIdx])
335 tagRow := maps.Clone(c.envelopes[c.envelopeIdx].CustomTags)
336 if tagRow == nil {
337 tagRow = make(map[string]string)
338 }
339 for k, v := range row {
340 if after, ok := strings.CutPrefix(k, metrics.TagPrefix); ok {
341 tagRow[after] = fmt.Sprintf("%v", v)
342 delete(row, k)
343 }
344 }
345 jsonTags, terr := jsoniter.ConfigFastest.MarshalToString(tagRow)
346 json, err := jsoniter.ConfigFastest.MarshalToString(row)
347 if err != nil || terr != nil {
348 c.err = errors.Join(err, terr)
349 return nil, c.err
350 }
351 return []any{time.Unix(0, c.envelopes[c.envelopeIdx].Data.GetEpoch()), c.envelopes[c.envelopeIdx].DBName, json, jsonTags}, nil
352 }
353
354 func (c *copyFromMeasurements) Err() error {
355 return c.err
356 }
357
358 func (c *copyFromMeasurements) MetricName() (ident pgx.Identifier) {
359 if c.envelopeIdx+1 < len(c.envelopes) {
360
361 ident = pgx.Identifier{c.envelopes[c.envelopeIdx+1].MetricName}
362 }
363 return
364 }
365
366
367 func (pgw *PostgresWriter) flush(msgs []metrics.MeasurementEnvelope) {
368 if len(msgs) == 0 {
369 return
370 }
371 logger := log.GetLogger(pgw.ctx)
372 pgPartBounds := make(map[string]ExistingPartitionInfo)
373 var err error
374
375 slices.SortFunc(msgs, func(a, b metrics.MeasurementEnvelope) int {
376 if a.MetricName < b.MetricName {
377 return -1
378 } else if a.MetricName > b.MetricName {
379 return 1
380 }
381 return 0
382 })
383
384 for _, msg := range msgs {
385 if len(msg.Data) > 0 {
386 epochTime := time.Unix(0, msg.Data.GetEpoch())
387 bounds, ok := pgPartBounds[msg.MetricName]
388 if !ok || (ok && epochTime.Before(bounds.StartTime)) {
389 bounds.StartTime = epochTime
390 pgPartBounds[msg.MetricName] = bounds
391 }
392 if !ok || (ok && epochTime.After(bounds.EndTime)) {
393 bounds.EndTime = epochTime
394 pgPartBounds[msg.MetricName] = bounds
395 }
396 }
397 }
398
399 switch pgw.metricSchema {
400 case DbStorageSchemaPostgres:
401 err = pgw.EnsureMetricTimePartsExist(pgPartBounds)
402 case DbStorageSchemaTimescale:
403 err = pgw.EnsureMetricTimescale(pgPartBounds)
404 default:
405 logger.Fatal("unknown storage schema...")
406 }
407 pgw.forceRecreatePartitions = false
408 if err != nil {
409 select {
410 case pgw.lastError <- err:
411 default:
412 }
413 }
414
415 var rowsBatched, n int64
416 t1 := time.Now()
417 cfm := newCopyFromMeasurements(msgs)
418 for !cfm.EOF() {
419 n, err = pgw.sinkDb.CopyFrom(context.Background(), cfm.MetricName(), targetColumns[:], cfm)
420 rowsBatched += n
421 if err != nil {
422 logger.Error(err)
423 if _, ok := err.(*pgconn.ConnectError); ok {
424 logger.Errorf("Sink DB not reachable, dropping %d cached measurements", len(msgs))
425 break
426 }
427 if PgError, ok := err.(*pgconn.PgError); ok {
428 pgw.forceRecreatePartitions = PgError.Code == "23514"
429 }
430 if pgw.forceRecreatePartitions {
431 logger.Warning("Some metric partitions might have been removed, halting all metric storage. Trying to re-create all needed partitions on next run")
432 }
433 }
434 }
435 diff := time.Since(t1)
436 if err == nil {
437 logger.WithField("rows", rowsBatched).WithField("elapsed", diff).Info("measurements written")
438 return
439 }
440 select {
441 case pgw.lastError <- err:
442 default:
443 }
444 }
445
446 func (pgw *PostgresWriter) EnsureMetricTimescale(pgPartBounds map[string]ExistingPartitionInfo) (err error) {
447 pgw.mu.Lock()
448 defer pgw.mu.Unlock()
449 logger := log.GetLogger(pgw.ctx)
450 sqlEnsure := `select * from admin.ensure_partition_timescale($1)`
451 for metric := range pgPartBounds {
452 if _, ok := pgw.partitionMapMetric[metric]; !ok {
453 if _, err = pgw.sinkDb.Exec(pgw.ctx, sqlEnsure, metric); err != nil {
454 logger.Errorf("Failed to create a TimescaleDB table for metric '%s': %v", metric, err)
455 return err
456 }
457 pgw.partitionMapMetric[metric] = ExistingPartitionInfo{}
458 }
459 }
460 return
461 }
462
463 func (pgw *PostgresWriter) EnsureMetricTimePartsExist(metricPartBounds map[string]ExistingPartitionInfo) error {
464 pgw.mu.Lock()
465 defer pgw.mu.Unlock()
466 var err error
467 var rows pgx.Rows
468 sqlEnsure := `select * from admin.ensure_partition_metric_time($1, $2, $3)`
469 for metric, pb := range metricPartBounds {
470 if pb.StartTime.IsZero() || pb.EndTime.IsZero() {
471 return fmt.Errorf("zero StartTime/EndTime in partitioning request: [%s:%v]", metric, pb)
472 }
473 partInfo, ok := pgw.partitionMapMetric[metric]
474 if !ok || pb.StartTime.Before(partInfo.StartTime) || pgw.forceRecreatePartitions {
475 if rows, err = pgw.sinkDb.Query(pgw.ctx, sqlEnsure, metric, pb.StartTime, pgw.opts.PartitionInterval); err != nil {
476 return err
477 }
478 if partInfo, err = pgx.CollectOneRow(rows, pgx.RowToStructByPos[ExistingPartitionInfo]); err != nil {
479 return err
480 }
481 pgw.partitionMapMetric[metric] = partInfo
482 }
483 if pb.EndTime.After(partInfo.EndTime) || pb.EndTime.Equal(partInfo.EndTime) || pgw.forceRecreatePartitions {
484 if rows, err = pgw.sinkDb.Query(pgw.ctx, sqlEnsure, metric, pb.EndTime, pgw.opts.PartitionInterval); err != nil {
485 return err
486 }
487 if partInfo, err = pgx.CollectOneRow(rows, pgx.RowToStructByPos[ExistingPartitionInfo]); err != nil {
488 return err
489 }
490 pgw.partitionMapMetric[metric] = partInfo
491 }
492 }
493 return nil
494 }
495
496
497 func (pgw *PostgresWriter) DeleteOldPartitions() {
498 l := log.GetLogger(pgw.ctx)
499 var partsDropped int
500 err := pgw.sinkDb.QueryRow(pgw.ctx, `SELECT admin.drop_old_time_partitions(older_than => $1::interval)`,
501 pgw.opts.RetentionInterval).Scan(&partsDropped)
502 if err != nil {
503 l.Error("Could not drop old time partitions:", err)
504 } else if partsDropped > 0 {
505 l.Infof("Dropped %d old time partitions", partsDropped)
506 }
507 }
508
509
510
511
512 func (pgw *PostgresWriter) MaintainUniqueSources() {
513 logger := log.GetLogger(pgw.ctx)
514 var rowsAffected int
515 if err := pgw.sinkDb.QueryRow(pgw.ctx, `SELECT admin.maintain_unique_sources()`).Scan(&rowsAffected); err != nil {
516 logger.Error("Failed to run admin.all_distinct_dbname_metrics maintenance:", err)
517 return
518 }
519 logger.WithField("rows", rowsAffected).Info("Successfully processed admin.all_distinct_dbname_metrics")
520 }
521
522 func (pgw *PostgresWriter) AddDBUniqueMetricToListingTable(dbUnique, metric string) error {
523 sql := `INSERT INTO admin.all_distinct_dbname_metrics
524 SELECT $1, $2
525 WHERE NOT EXISTS (
526 SELECT * FROM admin.all_distinct_dbname_metrics WHERE dbname = $1 AND metric = $2
527 )`
528 _, err := pgw.sinkDb.Exec(pgw.ctx, sql, dbUnique, metric)
529 return err
530 }
531
532 func NewPostgresSinkMigrator(ctx context.Context, connStr string) (db.Migrator, error) {
533 conn, err := pgxpool.New(ctx, connStr)
534 if err != nil {
535 return nil, err
536 }
537 pgw := &PostgresWriter{
538 ctx: ctx,
539 sinkDb: conn,
540 }
541 exists, err := db.DoesSchemaExist(ctx, conn, "admin")
542 if err != nil {
543 return nil, err
544 }
545 if exists {
546 return pgw, nil
547 }
548 for _, sql := range metricSchemaSQLs {
549 if _, err = conn.Exec(ctx, sql); err != nil {
550 return nil, err
551 }
552 }
553 return pgw, nil
554 }
555
556 var initMigrator = func(pgw *PostgresWriter) (*migrator.Migrator, error) {
557 return migrator.New(
558 migrator.TableName("admin.migration"),
559 migrator.SetNotice(func(s string) {
560 log.GetLogger(pgw.ctx).Info(s)
561 }),
562 migrations(),
563 )
564 }
565
566
567 func (pgw *PostgresWriter) Migrate() error {
568 m, err := initMigrator(pgw)
569 if err != nil {
570 return fmt.Errorf("cannot initialize migration: %w", err)
571 }
572 return m.Migrate(pgw.ctx, pgw.sinkDb)
573 }
574
575
576 func (pgw *PostgresWriter) NeedsMigration() (bool, error) {
577 m, err := initMigrator(pgw)
578 if err != nil {
579 return false, err
580 }
581 return m.NeedUpgrade(pgw.ctx, pgw.sinkDb)
582 }
583
584
585
586 var migrations func() migrator.Option = func() migrator.Option {
587 return migrator.Migrations(
588 &migrator.Migration{
589 Name: "01110 Apply postgres sink schema migrations",
590 Func: func(context.Context, pgx.Tx) error {
591
592 return nil
593 },
594 },
595
596 &migrator.Migration{
597 Name: "01180 Apply admin functions migrations for v5",
598 Func: func(ctx context.Context, tx pgx.Tx) error {
599 _, err := tx.Exec(ctx, `
600 DROP FUNCTION IF EXISTS admin.ensure_partition_metric_dbname_time;
601 DROP FUNCTION IF EXISTS admin.ensure_partition_metric_time;
602 DROP FUNCTION IF EXISTS admin.get_old_time_partitions(integer, text);
603 DROP FUNCTION IF EXISTS admin.drop_old_time_partitions(integer, boolean, text);
604 `)
605 if err != nil {
606 return err
607 }
608
609 _, err = tx.Exec(ctx, sqlMetricEnsurePartitionPostgres)
610 if err != nil {
611 return err
612 }
613 _, err = tx.Exec(ctx, sqlMetricAdminFunctions)
614 return err
615 },
616 },
617
618 &migrator.MigrationNoTx{
619 Name: "01409 Switch to time-only partitioning",
620 Func: func(ctx context.Context, conn migrator.PgxIface) error {
621 const (
622 sqlDropOldEnsurePartitionDbnameTime = `DROP FUNCTION IF EXISTS admin.ensure_partition_metric_dbname_time;`
623
624 sqlListMetricTables = `SELECT c.relname
625 FROM pg_description d
626 JOIN pg_class c ON c.oid = d.objoid
627 WHERE d.description = 'pgwatch-generated-metric-lvl'`
628
629 sqlIsTableMigrated = `SELECT EXISTS (
630 SELECT 1 FROM pg_partitioned_table WHERE partrelid = to_regclass($1) AND partstrat = 'r')`
631
632 sqlRenameMetricTable = `ALTER TABLE %s RENAME TO %s`
633
634 sqlMetricTableBounds = `SELECT COALESCE(MIN(time), NOW()),
635 COALESCE(CEIL(EXTRACT(EPOCH FROM (MAX(time) - MIN(time))::interval) / 86400) + 1, 0)
636 FROM %s`
637
638 sqlEnsurePartitionMetricTime = `SELECT admin.ensure_partition_metric_time($1::text, $2::timestamptz, '1 day'::interval, $3)`
639
640 sqlListSubpartitions = `SELECT relid::regclass, parentrelid::regclass
641 FROM pg_partition_tree(to_regclass($1)) WHERE relid::text LIKE 'subpartitions%'
642 ORDER BY isleaf DESC, relid::text`
643
644
645
646 sqlMoveSubpartition = `INSERT INTO %[1]s (time, dbname, data, tag_data) SELECT time, dbname, data, tag_data FROM %[2]s;
647 ALTER TABLE %[3]s DETACH PARTITION %[2]s;
648 DROP TABLE %[2]s;`
649
650 sqlDropTableIfExists = `DROP TABLE IF EXISTS %s`
651 )
652 if _, err := conn.Exec(ctx, sqlDropOldEnsurePartitionDbnameTime+
653 sqlMetricAdminFunctions+
654 sqlMetricEnsurePartitionPostgres); err != nil {
655 return err
656 }
657
658 rows, _ := conn.Query(ctx, sqlListMetricTables)
659 metricTables, err := pgx.CollectRows(rows, pgx.RowTo[string])
660 if err != nil {
661 return err
662 }
663
664
665
666 const suffix = "_before_v6_migration"
667 for _, metricTableRawName := range metricTables {
668 if strings.HasSuffix(metricTableRawName, suffix) {
669 continue
670 }
671
672 metricTableRawRename := metricTableRawName + suffix
673 metricTableRenamed := pgx.Identifier{metricTableRawRename}.Sanitize()
674 metricTable := pgx.Identifier{metricTableRawName}.Sanitize()
675
676
677 var isTableMigrated bool
678 if err = conn.QueryRow(ctx, sqlIsTableMigrated, metricTableRawName).Scan(&isTableMigrated); err != nil {
679 return err
680 } else if isTableMigrated {
681 continue
682 }
683
684 err = pgx.BeginFunc(ctx, conn, func(tx pgx.Tx) (ferr error) {
685 if _, ferr = tx.Exec(ctx, fmt.Sprintf(sqlRenameMetricTable, metricTable, metricTableRenamed)); ferr != nil {
686 return
687 }
688
689
690
691 var minTime time.Time
692 var daysToPrecreate int32
693 if ferr = tx.QueryRow(ctx, fmt.Sprintf(sqlMetricTableBounds, metricTableRenamed)).Scan(&minTime, &daysToPrecreate); ferr == nil {
694 _, ferr = tx.Exec(ctx, sqlEnsurePartitionMetricTime, metricTableRawName, minTime, daysToPrecreate)
695 }
696 return
697 })
698
699 if err != nil {
700 return err
701 }
702
703 type partitionInfo struct {
704 Rel string
705 ParentRel string
706 }
707 rows, _ := conn.Query(ctx, sqlListSubpartitions, metricTableRawRename)
708 partitionsInfo, err := pgx.CollectRows(rows, pgx.RowToStructByPos[partitionInfo])
709 if err != nil {
710 return err
711 }
712
713 for _, partInfo := range partitionsInfo {
714 if _, err := conn.Exec(ctx, fmt.Sprintf(sqlMoveSubpartition, metricTable, partInfo.Rel, partInfo.ParentRel)); err != nil {
715 return err
716 }
717 }
718
719 if _, err := conn.Exec(ctx, fmt.Sprintf(sqlDropTableIfExists, metricTableRenamed)); err != nil {
720 return err
721 }
722 }
723
724 return nil
725 },
726 },
727
728 &migrator.Migration{
729 Name: "01474 Change drop_all_metric_tables to procedure",
730 Func: func(ctx context.Context, tx pgx.Tx) error {
731 _, err := tx.Exec(ctx, sqlMetricAdminFunctions)
732 return err
733 },
734 },
735
736 &migrator.Migration{
737 Name: "01529 Fix ensure_partition_metric_time partitioning strategy",
738 Func: func(ctx context.Context, tx pgx.Tx) error {
739 _, err := tx.Exec(ctx, sqlMetricEnsurePartitionPostgres)
740 return err
741 },
742 },
743
744
745
746
747
748
749
750
751
752 )
753 }
754
755
756
757
758
759 func registeredMigrationsCount() int {
760 m, err := migrator.New(migrations())
761 if err != nil {
762 panic(fmt.Errorf("registeredMigrationsCount: %w", err))
763 }
764 return m.Count()
765 }
766