Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,35 @@ migrate-check-validate: ## Validate migration checksums
$(call print-target)
atlas migrate --env local validate

NAMESPACE_FK_CHILD_TABLES := billing_profiles,billing_customer_overrides,billing_invoices
NAMESPACE_FK_SCHEMA := tools/migrate/schema/namespace_foreign_keys.sql
PG_SCHEMA_DIFF_DEV_DSN ?= postgres://postgres:postgres@127.0.0.1:5432/postgres?sslmode=disable
PG_SCHEMA_DIFF_OUTPUT ?= -

.PHONY: generate-namespace-fks
generate-namespace-fks: ## Generate additive namespace foreign keys from the Ent schema
$(call print-target)
go generate ./openmeter/ent/...
go run ./tools/migrate/cmd/namespacefks \
-child-tables "$(NAMESPACE_FK_CHILD_TABLES)" \
-output "$(NAMESPACE_FK_SCHEMA)"

.PHONY: check-namespace-fks
check-namespace-fks: ## Ensure generated namespace foreign keys are current
$(call print-target)
go run ./tools/migrate/cmd/namespacefks \
-child-tables "$(NAMESPACE_FK_CHILD_TABLES)" \
-output "$(NAMESPACE_FK_SCHEMA)" \
-check

.PHONY: pgschema-diff-poc
pgschema-diff-poc: ## Generate a pg-schema-diff PoC plan from migrations to Ent, namespace FKs, and views
$(call print-target)
go run ./tools/migrate/cmd/pgschemadiff \
-dev-dsn "$(PG_SCHEMA_DIFF_DEV_DSN)" \
-namespace-fk-child-tables "$(NAMESPACE_FK_CHILD_TABLES)" \
-output "$(PG_SCHEMA_DIFF_OUTPUT)"

.PHONY: generate-sqlc-testdata
generate-sqlc-testdata: ## Generate SQLC testdata for a specific version (make generate-sqlc-testdata VERSION=20240826120919)
$(call print-target)
Expand Down
13 changes: 13 additions & 0 deletions go.mod

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 Open source vulnerabilities detected - critical severity
Aikido detected 2 vulnerabilities across 2 packages, it includes 1 critical and 1 high vulnerabilities.

Details

Remediation:

  • github.com/jackc/pgx/v4 — 1 CVE (critical) — no fix version available
  • github.com/jackc/pgproto3/v2 — 1 CVE (high) — no fix version available

Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info

Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,10 @@ require (
google.golang.org/protobuf v1.36.11
)

require github.com/stripe/pg-schema-diff v1.0.9

require (
github.com/Masterminds/semver/v3 v3.4.0 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/awalterschulze/goderive v0.5.1 // indirect
github.com/bhmj/xpression v0.9.4 // indirect
Expand All @@ -101,9 +104,18 @@ require (
github.com/go-test/deep v1.0.8 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/huandu/go-clone v1.7.3 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.14.3 // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.3.3 // indirect
github.com/jackc/pgtype v1.14.0 // indirect
github.com/jackc/pgx/v4 v4.18.2 // indirect
github.com/jmattheis/goverter v1.9.3 // indirect
github.com/jonboulle/clockwork v0.5.0 // indirect
github.com/kisielk/gotool v1.0.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/oapi-codegen/oapi-codegen/v2 v2.7.1 // indirect
github.com/oasdiff/yaml v0.1.1 // indirect
github.com/oasdiff/yaml3 v0.0.14 // indirect
Expand All @@ -112,6 +124,7 @@ require (
github.com/pb33f/ordered-map/v2 v2.3.1 // indirect
github.com/prometheus/otlptranslator v1.0.0 // indirect
github.com/rickb777/plural/v2 v2.1.0 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/samber/slog-common v0.21.0 // indirect
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
github.com/speakeasy-api/jsonpath v0.6.3 // indirect
Expand Down
133 changes: 133 additions & 0 deletions go.sum

Large diffs are not rendered by default.

24 changes: 24 additions & 0 deletions tools/migrate/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,27 @@ make generate-view-sql
```

This writes `tools/migrate/views.sql` by loading `openmeter/ent/schema` via Ent's schema loader and emitting Postgres `CREATE VIEW` statements from `EntSQL` view annotations.

## pg-schema-diff proof of concept

Generate a review-only SQL plan from the current migration history to the
desired Ent schema, generated namespace foreign keys, and Ent-managed views:

```bash
make pgschema-diff-poc
```

The command creates and drops disposable databases on `PG_SCHEMA_DIFF_DEV_DSN`.
It does not mutate an existing application database, but the configured role
must be allowed to create databases. Set `PG_SCHEMA_DIFF_OUTPUT` to write the
plan to a file instead of stdout. The migration state table (`schema_om`) is
removed from both disposable databases before comparison, so it cannot become
part of the generated application migration. The rendered SQL also removes the
`public` schema qualification to match the existing migration style.

This is intentionally a proof of concept. The output is not added to the
migration directory, no down migration is generated, and `atlas.sum` is not
updated. The generated plan is replay-validated against another disposable
database by default; pass `-skip-plan-validation` directly to the command only
when investigating pg-schema-diff behavior. Review the emitted statements and
hazard comments before using them in a migration.
119 changes: 119 additions & 0 deletions tools/migrate/cmd/namespacefks/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package main

import (
"bytes"
"errors"
"flag"
"fmt"
"os"
"path/filepath"
"strings"

entmigrate "github.com/openmeterio/openmeter/openmeter/ent/db/migrate"
"github.com/openmeterio/openmeter/tools/migrate/namespacefks"
)

func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}

func run() error {
var (
childTables string
check bool
output string
)

flag.StringVar(&childTables, "child-tables", "", "comma-separated child tables to include; empty includes all eligible tables")
flag.BoolVar(&check, "check", false, "fail when the generated SQL differs from the output file")
flag.StringVar(&output, "output", "-", "output file, or - for stdout")
flag.Parse()

generated, err := namespacefks.Generate(namespacefks.GenerateInput{
Tables: entmigrate.Tables,
ChildTables: splitCommaSeparated(childTables),
})
if err != nil {
return err
}

if output == "-" {
if check {
return errors.New("check requires an output file")
}

_, err := os.Stdout.Write(generated)
return err
}

if check {
existing, err := os.ReadFile(output)
if err != nil {
return fmt.Errorf("read generated namespace foreign keys: %w", err)
}

if !bytes.Equal(existing, generated) {
return fmt.Errorf("%s is stale; run make generate-namespace-fks", output)
}

return nil
}

if err := os.MkdirAll(filepath.Dir(output), 0o755); err != nil {
return fmt.Errorf("create output directory: %w", err)
}

temporary, err := os.CreateTemp(filepath.Dir(output), ".namespace-fks-*.sql")
if err != nil {
return fmt.Errorf("create temporary output: %w", err)
}
temporaryName := temporary.Name()
defer os.Remove(temporaryName)

if err := temporary.Chmod(0o644); err != nil {
_ = temporary.Close()
return fmt.Errorf("set temporary output permissions: %w", err)
}

if _, err := temporary.Write(generated); err != nil {
_ = temporary.Close()
return fmt.Errorf("write temporary output: %w", err)
}

if err := temporary.Close(); err != nil {
return fmt.Errorf("close temporary output: %w", err)
}

if err := os.Rename(temporaryName, output); err != nil {
return fmt.Errorf("replace generated namespace foreign keys: %w", err)
}

return nil
}

func splitCommaSeparated(value string) []string {
if strings.TrimSpace(value) == "" {
return nil
}

parts := strings.Split(value, ",")
tables := make([]string, 0, len(parts))
seen := make(map[string]struct{}, len(parts))
for _, part := range parts {
table := strings.TrimSpace(part)
if table == "" {
continue
}
if _, ok := seen[table]; ok {
continue
}

seen[table] = struct{}{}
tables = append(tables, table)
}

return tables
}
88 changes: 88 additions & 0 deletions tools/migrate/cmd/pgschemadiff/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package main

import (
"context"
"flag"
"fmt"
"io"
"log/slog"
"os"
"os/signal"
"path/filepath"
"strings"

"github.com/openmeterio/openmeter/tools/migrate/pgschemadiff"
)

const defaultNamespaceChildTables = "billing_profiles,billing_customer_overrides,billing_invoices"

func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()

if err := run(ctx, os.Args[1:], os.Stdout, os.Stderr); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}

func run(ctx context.Context, args []string, stdout, stderr io.Writer) error {
flags := flag.NewFlagSet("pgschemadiff", flag.ContinueOnError)
flags.SetOutput(stderr)

devDatabaseURL := flags.String("dev-dsn", "postgres://postgres:postgres@127.0.0.1:5432/postgres?sslmode=disable", "PostgreSQL instance on which disposable databases can be created")
entSchemaPath := flags.String("ent-schema", "./openmeter/ent/schema", "path to the Ent schema package")
namespaceChildTables := flags.String("namespace-fk-child-tables", defaultNamespaceChildTables, "comma-separated child tables for generated namespace foreign keys")
skipPlanValidation := flags.Bool("skip-plan-validation", false, "skip replaying the generated plan against a disposable database")
outputPath := flags.String("output", "-", "output SQL file, or - for stdout")
if err := flags.Parse(args); err != nil {
return err
}

logger := slog.New(slog.NewTextHandler(stderr, &slog.HandlerOptions{Level: slog.LevelWarn}))
plan, err := pgschemadiff.GeneratePlan(ctx, pgschemadiff.GeneratePlanInput{
DevDatabaseURL: *devDatabaseURL,
EntSchemaPath: *entSchemaPath,
NamespaceChildTables: splitCommaSeparated(*namespaceChildTables),
SkipPlanValidation: *skipPlanValidation,
Logger: logger,
})
if err != nil {
return err
}

output := pgschemadiff.RenderSQL(plan, !*skipPlanValidation)
if *outputPath == "-" {
_, err := stdout.Write(output)
return err
}

if err := os.MkdirAll(filepath.Dir(*outputPath), 0o755); err != nil {
return fmt.Errorf("create output directory: %w", err)
}
if err := os.WriteFile(*outputPath, output, 0o644); err != nil {
return fmt.Errorf("write schema diff: %w", err)
}

return nil
}

func splitCommaSeparated(value string) []string {
parts := strings.Split(value, ",")
values := make([]string, 0, len(parts))
seen := make(map[string]struct{}, len(parts))
for _, part := range parts {
value := strings.TrimSpace(part)
if value == "" {
continue
}
if _, ok := seen[value]; ok {
continue
}

seen[value] = struct{}{}
values = append(values, value)
}

return values
}
Loading
Loading