Skip to content
Open
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
93 changes: 93 additions & 0 deletions cmd/do_index_desired_state_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package cmd

import (
"context"
"fmt"
"strings"
"testing"

"github.com/pgplex/pgschema/internal/postgres"
"github.com/pgplex/pgschema/ir"
"github.com/pgplex/pgschema/testutil"
)

func TestExternalDesiredStateDOIndexReferences(t *testing.T) {
if testing.Short() {
t.Skip("embedded PostgreSQL integration")
}
pg := testutil.SetupPostgres(t)
defer pg.Stop()
db, host, port, database, user, password := testutil.ConnectToPostgres(t, pg)
defer db.Close()
major, err := testutil.GetMajorVersion(db)
if err != nil {
t.Fatal(err)
}
for _, index := range []struct{ name, definition string }{
{"idx", "CREATE INDEX idx ON public.t(id);"},
{"idx", "CREATE UNIQUE INDEX IF NOT EXISTS idx ON ONLY public.t(id);"},
{"idx", "CREATE INDEX idx ON public.t(id); CREATE INDEX IF NOT EXISTS idx ON public.outside_table(id);"},
{"idx", "CREATE INDEX IF NOT EXISTS idx ON public.outside_table(id); CREATE UNIQUE INDEX idx ON public.t(id);"},
{`Index."Quoted`, `CREATE INDEX "Index.""Quoted" ON public.t(id);`},
} {
for _, shadow := range []bool{false, true} {
t.Run(fmt.Sprintf("%s/shadow=%v", index.definition, shadow), func(t *testing.T) {
if _, err := db.Exec(`DROP SCHEMA public CASCADE; CREATE SCHEMA public;
CREATE TABLE public.outside_table(id int);
CREATE INDEX untouched ON public.outside_table(id);
CREATE FUNCTION public.idx() RETURNS int LANGUAGE sql IMMUTABLE AS $$ SELECT 42 $$;`); err != nil {
t.Fatal(err)
}
if shadow {
if _, err := db.Exec("CREATE INDEX " + ir.QuoteIdentifier(index.name) + " ON public.outside_table(id)"); err != nil {
t.Fatal(err)
}
}
provider, err := postgres.NewExternalDatabase(&postgres.ExternalDatabaseConfig{
Host: host, Port: port, Database: database, Username: user, Password: password,
SSLMode: "disable", TargetMajorVersion: major,
})
if err != nil {
t.Fatal(err)
}
defer provider.Stop()
indexLiteral := "'public." + strings.ReplaceAll(ir.QuoteIdentifier(index.name), "'", "''") + "'"
fixture := `CREATE TABLE public.t(id int);` + index.definition + `
CREATE TABLE public.resolved(index_id oid, untouched_id oid);
CREATE FUNCTION public.fetch_value() RETURNS int LANGUAGE sql AS $$ SELECT public.idx() $$;
CREATE VIEW public.result AS SELECT public.fetch_value() AS value;
DO $$ BEGIN INSERT INTO resolved VALUES (` + indexLiteral + `::pg_catalog.regclass, 'public.untouched'::regclass); END $$;`
if err := provider.ApplySchema(context.Background(), "public", fixture); err != nil {
t.Fatal(err)
}
var indexSchema, tableName, untouchedSchema, untouchedTable string
err = db.QueryRow(`SELECT n.nspname, t.relname, un.nspname, ut.relname
FROM `+ir.QuoteIdentifier(provider.GetSchemaName())+`.resolved r
JOIN pg_catalog.pg_class c ON c.oid=r.index_id
JOIN pg_catalog.pg_namespace n ON n.oid=c.relnamespace
JOIN pg_catalog.pg_index i ON i.indexrelid=c.oid
JOIN pg_catalog.pg_class t ON t.oid=i.indrelid
JOIN pg_catalog.pg_class uc ON uc.oid=r.untouched_id
JOIN pg_catalog.pg_namespace un ON un.oid=uc.relnamespace
JOIN pg_catalog.pg_index ui ON ui.indexrelid=uc.oid
JOIN pg_catalog.pg_class ut ON ut.oid=ui.indrelid`).Scan(&indexSchema, &tableName, &untouchedSchema, &untouchedTable)
if err != nil {
t.Fatal(err)
}
if indexSchema != provider.GetSchemaName() || tableName != "t" {
t.Fatalf("created index resolved to %s.%s", indexSchema, tableName)
}
if untouchedSchema != "public" || untouchedTable != "outside_table" {
t.Fatalf("unmanaged index resolved to %s.%s", untouchedSchema, untouchedTable)
}
var value int
if err := db.QueryRow("SELECT value FROM " + ir.QuoteIdentifier(provider.GetSchemaName()) + ".result").Scan(&value); err != nil {
t.Fatal(err)
}
if value != 42 {
t.Fatalf("external function reference changed: %d", value)
}
})
}
}
}
114 changes: 114 additions & 0 deletions cmd/do_literal_desired_state_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package cmd

import (
"context"
"errors"
"strings"
"testing"

"github.com/jackc/pgx/v5/pgconn"
"github.com/pgplex/pgschema/internal/postgres"
"github.com/pgplex/pgschema/ir"
"github.com/pgplex/pgschema/testutil"
)

// Compare the loader with PostgreSQL's own literal semantics on an owned local
// instance. Invalid SQL must fail before executing the desired-state script.
func TestDesiredStateDOLiteralSemantics(t *testing.T) {
if testing.Short() {
t.Skip("embedded PostgreSQL integration")
}
pg := testutil.SetupPostgres(t)
defer pg.Stop()
db, h, p, d, u, w := testutil.ConnectToPostgres(t, pg)
defer db.Close()
major, err := testutil.GetMajorVersion(db)
if err != nil {
t.Fatal(err)
}
external, err := postgres.NewExternalDatabase(&postgres.ExternalDatabaseConfig{Host: h, Port: p, Database: d, Username: u, Password: w, SSLMode: "disable", TargetMajorVersion: major})
if err != nil {
t.Fatal(err)
}
defer external.Stop()
embeddedDB, _, _, _, _, _ := testutil.ConnectToPostgres(t, sharedEmbeddedPG)
defer embeddedDB.Close()
for _, tc := range []struct{ name, body string }{
{"incomplete_u", `DO E'BEGIN RAISE NOTICE ''\u12''; END';`},
{"incomplete_U", `DO E'BEGIN RAISE NOTICE ''\U0001F60''; END';`},
{"invalid_hex", `DO E'BEGIN RAISE NOTICE ''\u12GG''; END';`},
{"out_of_range", `DO E'BEGIN RAISE NOTICE ''\U00110000''; END';`},
{"maximum_uint32", `DO E'BEGIN RAISE NOTICE ''\UFFFFFFFF''; END';`},
{"zero", `DO E'BEGIN RAISE NOTICE ''\u0000''; END';`},
{"unpaired_low_surrogate", `DO E'BEGIN RAISE NOTICE ''\uDE00''; END';`},
{"invalid_low_surrogate", `DO E'BEGIN RAISE NOTICE ''\uD83D\u0041''; END';`},
{"unpaired_surrogate", `DO E'BEGIN RAISE NOTICE ''\uD83D''; END';`},
{"invalid_regclass", `DO $$ BEGIN PERFORM E'public.\u12'::pg_catalog.regclass; END $$;`},
} {
t.Run(tc.name, func(t *testing.T) {
_, rawErr := db.Exec(tc.body)
var pgErr *pgconn.PgError
if !errors.As(rawErr, &pgErr) {
t.Fatalf("PostgreSQL accepted invalid fixture: %v", rawErr)
}
for name, provider := range map[string]postgres.DesiredStateProvider{"embedded": sharedEmbeddedPG, "external": external} {
t.Run(name, func(t *testing.T) {
err := provider.ApplySchema(context.Background(), "public", `CREATE TABLE public.marker(v text); `+tc.body)
if err == nil || !strings.Contains(err.Error(), "failed to remap desired state SQL") {
t.Fatalf("missing desired-state remapping error: %v", err)
}
conn := db
if name == "embedded" {
conn = embeddedDB
}
var count int
if err := conn.QueryRow(`SELECT count(*) FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON n.oid=c.relnamespace WHERE n.nspname=$1 AND c.relname='marker'`, provider.GetSchemaName()).Scan(&count); err != nil {
t.Fatal(err)
}
if count != 0 {
t.Fatal("invalid desired SQL executed before remapping failed")
}
})
}
})
}
for _, escape := range []string{`\uD83D\uDE00`, `\U0000D83D\uDE00`, `\uD83D\U0000DE00`, `\U0001F600`} {
t.Run(escape, func(t *testing.T) {
const prefix = `CREATE TABLE public.marker(v text); `
body := `DO E'BEGIN EXECUTE pg_catalog.format(''INSERT INTO %s VALUES (%L)'', ''public.marker''::pg_catalog.regclass, ''` + escape + `''); END';`
if _, err := db.Exec(`DROP TABLE IF EXISTS public.marker; ` + prefix + body); err != nil {
t.Fatal(err)
}
var want string
if err := db.QueryRow(`SELECT v FROM public.marker`).Scan(&want); err != nil {
t.Fatal(err)
}
if want != "😀" {
t.Fatalf("unexpected native Unicode value %q", want)
}
for name, provider := range map[string]postgres.DesiredStateProvider{"embedded": sharedEmbeddedPG, "external": external} {
t.Run(name, func(t *testing.T) {
if name == "external" {
if _, err := db.Exec("DROP TABLE IF EXISTS " + ir.QuoteIdentifier(provider.GetSchemaName()) + ".marker"); err != nil {
t.Fatal(err)
}
}
if err := provider.ApplySchema(context.Background(), "public", prefix+body); err != nil {
t.Fatal(err)
}
conn := db
if name == "embedded" {
conn = embeddedDB
}
var got string
if err := conn.QueryRow("SELECT v FROM " + ir.QuoteIdentifier(provider.GetSchemaName()) + ".marker").Scan(&got); err != nil {
t.Fatal(err)
}
if got != want {
t.Fatalf("loader changed Unicode content: got %q, want %q", got, want)
}
})
}
})
}
}
87 changes: 87 additions & 0 deletions cmd/do_regclass_desired_state_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package cmd

import (
"context"
"database/sql"
"testing"

"github.com/pgplex/pgschema/internal/postgres"
"github.com/pgplex/pgschema/ir"
"github.com/pgplex/pgschema/testutil"
)

func TestDesiredStateDOAliasesAndRecords(t *testing.T) {
if testing.Short() {
t.Skip("embedded PostgreSQL integration")
}
pg := testutil.SetupPostgres(t)
defer pg.Stop()
db, h, p, d, u, w := testutil.ConnectToPostgres(t, pg)
defer db.Close()
major, err := testutil.GetMajorVersion(db)
if err != nil {
t.Fatal(err)
}
embeddedDB, _, _, _, _, _ := testutil.ConnectToPostgres(t, sharedEmbeddedPG)
defer embeddedDB.Close()
const prefix = `CREATE TABLE public.t(t int);
INSERT INTO t VALUES (7);
CREATE TABLE public.marker(v text);
`
for _, tc := range []struct{ name, body, want string }{
{"alias", `DO $$ BEGIN INSERT INTO marker SELECT public.t::text FROM t AS public; END $$;`, "7"},
{"quoted_alias", `DO $$ BEGIN INSERT INTO marker SELECT "public".t::text FROM t AS "public"; END $$;`, "7"},
{"record", `DO $$ DECLARE public record; BEGIN SELECT 9 AS t INTO public; INSERT INTO marker VALUES (public.t::text); END $$;`, "9"},
{"alias_and_regclass", `DO $$ BEGIN INSERT INTO marker SELECT public.t::text FROM t AS public WHERE 'public.t'::pg_catalog.regclass = 't'::regclass; END $$;`, "7"},
{"record_and_regclass", `DO $$ DECLARE public record; BEGIN SELECT 9 AS t INTO public; IF CAST('public.t' AS pg_catalog.regclass) = 't'::regclass THEN INSERT INTO marker VALUES (public.t::text); END IF; END $$;`, "9"},
{"nested_record", `DO $$ DECLARE public record; BEGIN
SELECT 11 AS t INTO public;
INSERT INTO marker VALUES (public.t::text);
DECLARE public record; BEGIN
SELECT 13 AS t INTO public;
INSERT INTO marker VALUES (public.t::text);
END;
INSERT INTO marker VALUES (public.t::text);
END $$;`, "11,11,13"},
} {
t.Run(tc.name, func(t *testing.T) {
fixture := prefix + tc.body
if _, err := db.Exec(`DROP SCHEMA public CASCADE; CREATE SCHEMA public; ` + fixture); err != nil {
t.Fatalf("independent PostgreSQL fixture failed: %v", err)
}
readMarker := func(conn *sql.DB, schema string) string {
t.Helper()
var got string
if err := conn.QueryRow("SELECT COALESCE(string_agg(v, ',' ORDER BY v), '') FROM " + ir.QuoteIdentifier(schema) + ".marker").Scan(&got); err != nil {
t.Fatal(err)
}
return got
}
if got := readMarker(db, "public"); got != tc.want {
t.Fatalf("native PostgreSQL result = %q, want %q", got, tc.want)
}
external, err := postgres.NewExternalDatabase(&postgres.ExternalDatabaseConfig{Host: h, Port: p, Database: d, Username: u, Password: w, SSLMode: "disable", TargetMajorVersion: major})
if err != nil {
t.Fatal(err)
}
defer external.Stop()
for name, provider := range map[string]postgres.DesiredStateProvider{"embedded": sharedEmbeddedPG, "external": external} {
t.Run(name, func(t *testing.T) {
if err := provider.ApplySchema(context.Background(), "public", fixture); err != nil {
t.Fatal(err)
}
conn := db
if name == "embedded" {
conn = embeddedDB
}
if got := readMarker(conn, provider.GetSchemaName()); got != tc.want {
t.Fatalf("loader result = %q, want %q", got, tc.want)
}
})
}
if got := readMarker(db, "public"); got != tc.want {
t.Fatalf("loader changed the original schema: %q", got)
}
})
}
}
23 changes: 18 additions & 5 deletions cmd/dump/partition_index_ignore_test.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package dump

import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"testing"

"github.com/pgplex/pgschema/internal/diff"
"github.com/pgplex/pgschema/internal/include"
"github.com/pgplex/pgschema/ir"
"github.com/pgplex/pgschema/testutil"
)

Expand Down Expand Up @@ -97,9 +100,13 @@ ALTER INDEX partial_lookup ATTACH PARTITION late_partial_lookup;
Schema: "public", SSLMode: "disable", MultiFile: multiFile, File: path,
})
if tc.child != "" {
want := "cannot dump index public." + tc.parent + ": attached index public." + tc.child + " is excluded"
if err == nil || !strings.Contains(err.Error(), want) || !strings.Contains(err.Error(), ".pgschemaignore") {
t.Fatalf("Expected actionable dependency error %q, got %v", want, err)
var dependencyErr *diff.PartitionIndexDependencyError
if !errors.As(err, &dependencyErr) {
t.Fatalf("Expected partition index dependency error, got %v", err)
}
if dependencyErr.ParentIndex != (ir.IndexRef{Schema: "public", Name: tc.parent}) ||
dependencyErr.AttachedIndex != (ir.IndexRef{Schema: "public", Name: tc.child}) || dependencyErr.ExcludedTable != nil {
t.Fatalf("Unexpected dependency context: %+v", dependencyErr)
}
if output != "" {
t.Fatal("Failed dump returned partial SQL")
Expand Down Expand Up @@ -185,8 +192,14 @@ CREATE TABLE event_links (bucket int, id int, FOREIGN KEY (bucket, id) REFERENCE
Host: host, Port: port, DB: dbname, User: user, Password: password,
Schema: "public", SSLMode: "disable", MultiFile: multiFile, File: path,
})
if err == nil || !strings.Contains(err.Error(), "event_unique") || !strings.Contains(err.Error(), "mid_unique") || !strings.Contains(err.Error(), "events_leaf") || !strings.Contains(err.Error(), ".pgschemaignore") {
t.Fatalf("Expected actionable nested dependency error, got %v", err)
var dependencyErr *diff.PartitionIndexDependencyError
if !errors.As(err, &dependencyErr) {
t.Fatalf("Expected nested partition index dependency error, got %v", err)
}
if dependencyErr.ParentIndex != (ir.IndexRef{Schema: "public", Name: "event_unique"}) ||
dependencyErr.AttachedIndex != (ir.IndexRef{Schema: "public", Name: "mid_unique"}) || dependencyErr.ExcludedTable == nil ||
dependencyErr.ExcludedTable.Schema != "public" || dependencyErr.ExcludedTable.Name != "events_leaf" {
t.Fatalf("Unexpected dependency context: %+v", dependencyErr)
}
if output != "" {
t.Fatal("Failed dump returned partial SQL")
Expand Down
Loading
Loading