diff --git a/internal/repository/postgres.go b/internal/repository/postgres.go index e0fcf46..a91f131 100644 --- a/internal/repository/postgres.go +++ b/internal/repository/postgres.go @@ -687,7 +687,7 @@ func (p *Postgres) GetRevision(tenantID, revisionID string) (*store.Revision, er if mj.PreviewFragments == nil { mj.PreviewFragments = map[string]string{} } - r.PreviewFragments = mj.PreviewFragments + r.PreviewFragments = loadRevisionPreview(ctx, p.pool, revisionID, mj.PreviewFragments) r.MaterializedPrefixCount = mj.MaterializedPrefixCount return &r, nil } @@ -848,26 +848,39 @@ func (p *Postgres) CreateRollbackRevision(tenantID, sourceRevisionID string) (st ctx := context.Background() newID := uuid.NewString() parent := sourceRevisionID - meta, _ := json.Marshal(map[string]any{ - "preview_fragments": src.PreviewFragments, - "materialized_prefix_count": src.MaterializedPrefixCount, - }) + meta, err := revisionMetaWithoutPreview(src.MaterializedPrefixCount) + if err != nil { + return "", err + } var modArg any if strings.TrimSpace(src.ModuleID) != "" { modArg = src.ModuleID } - _, err = p.pool.Exec(ctx, ` - INSERT INTO config_revision (id, tenant_id, module_id, content_hash, parent_revision_id, meta_json) - VALUES ($1,$2,$3,$4,$5::uuid,$6::jsonb)`, - newID, tenantID, modArg, src.ContentHash+":rollback", parent, string(meta)) + tx, err := p.pool.Begin(ctx) if err != nil { return "", err } - // copy materialized prefixes - _, _ = p.pool.Exec(ctx, ` + defer func() { _ = tx.Rollback(ctx) }() + _, err = tx.Exec(ctx, ` + INSERT INTO config_revision (id, tenant_id, module_id, content_hash, parent_revision_id, meta_json) + VALUES ($1,$2,$3,$4,$5::uuid,$6::jsonb)`, + newID, tenantID, modArg, src.ContentHash+":rollback", parent, meta) + if err != nil { + return "", err + } + if err := copyRevisionPreview(ctx, tx, newID, sourceRevisionID); err != nil { + return "", err + } + _, err = tx.Exec(ctx, ` INSERT INTO revision_materialized_prefix (revision_id, prefix, community_id, source, meta_json) SELECT $1::uuid, prefix, community_id, source, meta_json FROM revision_materialized_prefix WHERE revision_id=$2::uuid`, newID, sourceRevisionID) + if err != nil { + return "", err + } + if err := tx.Commit(ctx); err != nil { + return "", err + } return newID, nil } @@ -1056,10 +1069,7 @@ func (p *Postgres) CreateRenderRevision(revisionID, tenantID, moduleID string, p if previewFragments == nil { previewFragments = map[string]string{} } - meta, err := json.Marshal(map[string]any{ - "preview_fragments": previewFragments, - "materialized_prefix_count": len(prefixes), - }) + meta, err := revisionMetaWithoutPreview(len(prefixes)) if err != nil { return err } @@ -1073,13 +1083,17 @@ func (p *Postgres) CreateRenderRevision(revisionID, tenantID, moduleID string, p if parentRevisionID != nil && strings.TrimSpace(*parentRevisionID) != "" { parent = strings.TrimSpace(*parentRevisionID) } + revID := strings.TrimSpace(revisionID) _, err = tx.Exec(ctx, ` INSERT INTO config_revision (id, tenant_id, module_id, content_hash, parent_revision_id, meta_json) VALUES ($1::uuid, $2::uuid, $3::uuid, $4, $5::uuid, $6::jsonb)`, - strings.TrimSpace(revisionID), tenantID, moduleID, strings.TrimSpace(contentHash), parent, string(meta)) + revID, tenantID, moduleID, strings.TrimSpace(contentHash), parent, meta) if err != nil { return err } + if err := insertRevisionPreview(ctx, tx, revID, previewFragments); err != nil { + return err + } if len(prefixes) > 0 { _, err = tx.CopyFrom(ctx, pgx.Identifier{"revision_materialized_prefix"}, @@ -1094,7 +1108,7 @@ func (p *Postgres) CreateRenderRevision(revisionID, tenantID, moduleID string, p if strings.TrimSpace(src) == "" { src = "render" } - return []any{strings.TrimSpace(revisionID), strings.TrimSpace(pr.Prefix), comm, src}, nil + return []any{revID, strings.TrimSpace(pr.Prefix), comm, src}, nil })) if err != nil { return err diff --git a/internal/repository/postgres_revision_preview.go b/internal/repository/postgres_revision_preview.go new file mode 100644 index 0000000..5b15de6 --- /dev/null +++ b/internal/repository/postgres_revision_preview.go @@ -0,0 +1,90 @@ +package repository + +import ( + "context" + "encoding/json" + "errors" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" +) + +func revisionPreviewTableExists(ctx context.Context, q queryRower) bool { + var n int + err := q.QueryRow(ctx, ` + SELECT 1 FROM information_schema.tables + WHERE table_schema = 'public' AND table_name = 'config_revision_preview' + LIMIT 1`).Scan(&n) + return err == nil +} + +type queryRower interface { + QueryRow(ctx context.Context, sql string, args ...any) pgx.Row +} + +func loadRevisionPreview(ctx context.Context, q queryRower, revisionID string, metaPreview map[string]string) map[string]string { + if revisionPreviewTableExists(ctx, q) { + var raw []byte + err := q.QueryRow(ctx, ` + SELECT fragments FROM config_revision_preview WHERE revision_id = $1::uuid`, + revisionID).Scan(&raw) + if err == nil { + out := map[string]string{} + _ = json.Unmarshal(raw, &out) + if out == nil { + out = map[string]string{} + } + return out + } + if !errors.Is(err, pgx.ErrNoRows) { + return metaPreview + } + } + if metaPreview == nil { + return map[string]string{} + } + return metaPreview +} + +type execQuerier interface { + queryRower + Exec(ctx context.Context, sql string, arguments ...any) (pgconn.CommandTag, error) +} + +func insertRevisionPreview(ctx context.Context, db execQuerier, revisionID string, preview map[string]string) error { + if !revisionPreviewTableExists(ctx, db) { + return nil + } + if preview == nil { + preview = map[string]string{} + } + raw, err := json.Marshal(preview) + if err != nil { + return err + } + _, err = db.Exec(ctx, ` + INSERT INTO config_revision_preview (revision_id, fragments) + VALUES ($1::uuid, $2::jsonb) + ON CONFLICT (revision_id) DO UPDATE SET fragments = EXCLUDED.fragments`, + revisionID, string(raw)) + return err +} + +func copyRevisionPreview(ctx context.Context, db execQuerier, dstRevisionID, srcRevisionID string) error { + if !revisionPreviewTableExists(ctx, db) { + return nil + } + _, err := db.Exec(ctx, ` + INSERT INTO config_revision_preview (revision_id, fragments) + SELECT $1::uuid, fragments FROM config_revision_preview WHERE revision_id = $2::uuid + ON CONFLICT (revision_id) DO UPDATE SET fragments = EXCLUDED.fragments`, + dstRevisionID, srcRevisionID) + return err +} + +func revisionMetaWithoutPreview(materializedPrefixCount int) (string, error) { + raw, err := json.Marshal(map[string]any{ + "materialized_prefix_count": materializedPrefixCount, + }) + return string(raw), err +} diff --git a/internal/repository/postgres_seed.go b/internal/repository/postgres_seed.go index 593412c..431b4e6 100644 --- a/internal/repository/postgres_seed.go +++ b/internal/repository/postgres_seed.go @@ -26,9 +26,8 @@ func (p *Postgres) seedDemo(ctx context.Context) error { p1 := uuid.NewString() p2 := uuid.NewString() - preview := map[string]any{ - "preview_fragments": map[string]string{ - "bird.conf": `# EvoBGP demo bundle + previewFrags := map[string]string{ + "bird.conf": `# EvoBGP demo bundle router id 192.0.2.1; protocol device { @@ -39,15 +38,12 @@ protocol direct { ipv6; } `, - "bird.d/evobgp_demo.conf": "# static demo fragment\n", - }, - "materialized_prefix_count": 128, + "bird.d/evobgp_demo.conf": "# static demo fragment\n", } - previewB, _ := json.Marshal(preview) - parentMeta, _ := json.Marshal(map[string]any{ - "preview_fragments": map[string]string{"bird.conf": "# parent revision\n"}, - "materialized_prefix_count": 0, - }) + previewMeta, _ := json.Marshal(map[string]any{"materialized_prefix_count": 128}) + parentMeta, _ := json.Marshal(map[string]any{"materialized_prefix_count": 0}) + parentPreview, _ := json.Marshal(map[string]string{"bird.conf": "# parent revision\n"}) + previewFragsB, _ := json.Marshal(previewFrags) tx, err := p.pool.Begin(ctx) if err != nil { @@ -76,9 +72,17 @@ protocol direct { VALUES ($1,$2,'sha256:parent',NULL,$3::jsonb)`, parent, tid, string(parentMeta)); err != nil { return err } + if _, err := tx.Exec(ctx, ` + INSERT INTO config_revision_preview (revision_id, fragments) VALUES ($1::uuid, $2::jsonb)`, parent, string(parentPreview)); err != nil { + return err + } if _, err := tx.Exec(ctx, ` INSERT INTO config_revision (id, tenant_id, content_hash, parent_revision_id, meta_json) - VALUES ($1,$2,'sha256:demo-rev-1',$3::uuid,$4::jsonb)`, rid, tid, parent, string(previewB)); err != nil { + VALUES ($1,$2,'sha256:demo-rev-1',$3::uuid,$4::jsonb)`, rid, tid, parent, string(previewMeta)); err != nil { + return err + } + if _, err := tx.Exec(ctx, ` + INSERT INTO config_revision_preview (revision_id, fragments) VALUES ($1::uuid, $2::jsonb)`, rid, string(previewFragsB)); err != nil { return err } if _, err := tx.Exec(ctx, ` diff --git a/migrations/postgres/000015_revision_preview.down.sql b/migrations/postgres/000015_revision_preview.down.sql new file mode 100644 index 0000000..9ca7344 --- /dev/null +++ b/migrations/postgres/000015_revision_preview.down.sql @@ -0,0 +1,6 @@ +UPDATE config_revision cr +SET meta_json = cr.meta_json || jsonb_build_object('preview_fragments', COALESCE(p.fragments, '{}'::jsonb)) +FROM config_revision_preview p +WHERE p.revision_id = cr.id; + +DROP TABLE IF EXISTS config_revision_preview; diff --git a/migrations/postgres/000015_revision_preview.up.sql b/migrations/postgres/000015_revision_preview.up.sql new file mode 100644 index 0000000..330b792 --- /dev/null +++ b/migrations/postgres/000015_revision_preview.up.sql @@ -0,0 +1,15 @@ +-- Split BIRD preview fragments out of config_revision.meta_json (H1). + +CREATE TABLE config_revision_preview ( + revision_id UUID PRIMARY KEY REFERENCES config_revision (id) ON DELETE CASCADE, + fragments JSONB NOT NULL DEFAULT '{}' +); + +INSERT INTO config_revision_preview (revision_id, fragments) +SELECT id, COALESCE(meta_json->'preview_fragments', '{}'::jsonb) +FROM config_revision +WHERE meta_json ? 'preview_fragments'; + +UPDATE config_revision +SET meta_json = meta_json - 'preview_fragments' +WHERE meta_json ? 'preview_fragments'; diff --git a/migrations/sqlite/000015_revision_preview.down.sql b/migrations/sqlite/000015_revision_preview.down.sql new file mode 100644 index 0000000..f13fdfe --- /dev/null +++ b/migrations/sqlite/000015_revision_preview.down.sql @@ -0,0 +1,9 @@ +UPDATE config_revision +SET meta_json = json_set( + meta_json, + '$.preview_fragments', + json(COALESCE((SELECT fragments FROM config_revision_preview p WHERE p.revision_id = config_revision.id), '{}')) +) +WHERE id IN (SELECT revision_id FROM config_revision_preview); + +DROP TABLE IF EXISTS config_revision_preview; diff --git a/migrations/sqlite/000015_revision_preview.up.sql b/migrations/sqlite/000015_revision_preview.up.sql new file mode 100644 index 0000000..8420423 --- /dev/null +++ b/migrations/sqlite/000015_revision_preview.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE config_revision_preview ( + revision_id TEXT NOT NULL PRIMARY KEY REFERENCES config_revision (id) ON DELETE CASCADE, + fragments TEXT NOT NULL DEFAULT '{}' +); + +INSERT INTO config_revision_preview (revision_id, fragments) +SELECT id, json(COALESCE(json_extract(meta_json, '$.preview_fragments'), '{}')) +FROM config_revision +WHERE json_extract(meta_json, '$.preview_fragments') IS NOT NULL; + +UPDATE config_revision +SET meta_json = json_remove(meta_json, '$.preview_fragments') +WHERE json_extract(meta_json, '$.preview_fragments') IS NOT NULL;