diff --git a/internal/catalog/build.go b/internal/catalog/build.go index 2e88c95ab9..e2d02f7da9 100644 --- a/internal/catalog/build.go +++ b/internal/catalog/build.go @@ -69,6 +69,32 @@ func Update(c *pg.Catalog, stmt nodes.Node) error { } switch n := raw.Stmt.(type) { + case nodes.AlterObjectSchemaStmt: + switch n.ObjectType { + + case nodes.OBJECT_TABLE: + fqn, err := ParseRange(n.Relation) + if err != nil { + return err + } + from, exists := c.Schemas[fqn.Schema] + if !exists { + return pg.ErrorSchemaDoesNotExist(fqn.Schema) + } + table, exists := from.Tables[fqn.Rel] + if !exists { + return pg.ErrorRelationDoesNotExist(fqn.Rel) + } + to, exists := c.Schemas[*n.Newschema] + if !exists { + return pg.ErrorSchemaDoesNotExist(*n.Newschema) + } + // Move the table + delete(from.Tables, fqn.Rel) + to.Tables[fqn.Rel] = table + + } + case nodes.AlterTableStmt: fqn, err := ParseRange(n.Relation) if err != nil { @@ -86,57 +112,59 @@ func Update(c *pg.Catalog, stmt nodes.Node) error { for _, cmd := range n.Cmds.Items { switch cmd := cmd.(type) { case nodes.AlterTableCmd: - switch cmd.Subtype { + idx := -1 - case nodes.AT_AddColumn: - switch d := cmd.Def.(type) { - case nodes.ColumnDef: - for _, c := range table.Columns { - if c.Name == *d.Colname { - return pg.ErrorColumnAlreadyExists(table.Name, *d.Colname) - } - } - table.Columns = append(table.Columns, pg.Column{ - Name: *d.Colname, - DataType: join(d.TypeName.Names, "."), - NotNull: isNotNull(d), - }) - } - - case nodes.AT_DropColumn: - removed := false + // If cmd.Name is set, do a column lookup. + if cmd.Name != nil { for i, c := range table.Columns { if c.Name == *cmd.Name { - table.Columns = append(table.Columns[:i], table.Columns[i+1:]...) - removed = true + idx = i + break } } - if !removed { + if idx < 0 && !cmd.MissingOk { return pg.ErrorColumnDoesNotExist(table.Name, *cmd.Name) } + // If a missing column is allowed, skip this command + if idx < 0 && cmd.MissingOk { + continue + } + } + + switch cmd.Subtype { + + case nodes.AT_AddColumn: + d := cmd.Def.(nodes.ColumnDef) + for _, c := range table.Columns { + if c.Name == *d.Colname { + return pg.ErrorColumnAlreadyExists(table.Name, *d.Colname) + } + } + table.Columns = append(table.Columns, pg.Column{ + Name: *d.Colname, + DataType: join(d.TypeName.Names, "."), + NotNull: isNotNull(d), + }) + + case nodes.AT_AlterColumnType: + d := cmd.Def.(nodes.ColumnDef) + table.Columns[idx].DataType = join(d.TypeName.Names, ".") + + case nodes.AT_DropColumn: + table.Columns = append(table.Columns[:idx], table.Columns[idx+1:]...) + + case nodes.AT_DropNotNull: + table.Columns[idx].NotNull = false + + case nodes.AT_SetNotNull: + table.Columns[idx].NotNull = true + } schema.Tables[fqn.Rel] = table } } - case nodes.CreateEnumStmt: - fqn, err := ParseList(n.TypeName) - if err != nil { - return err - } - schema, exists := c.Schemas[fqn.Schema] - if !exists { - return pg.ErrorSchemaDoesNotExist(fqn.Schema) - } - if _, exists := schema.Enums[fqn.Rel]; exists { - return pg.ErrorTypeAlreadyExists(fqn.Rel) - } - schema.Enums[fqn.Rel] = pg.Enum{ - Name: fqn.Rel, - Vals: stringSlice(n.Vals), - } - case nodes.CreateStmt: fqn, err := ParseRange(n.Relation) if err != nil { @@ -165,44 +193,85 @@ func Update(c *pg.Catalog, stmt nodes.Node) error { } schema.Tables[fqn.Rel] = table + case nodes.CreateEnumStmt: + fqn, err := ParseList(n.TypeName) + if err != nil { + return err + } + schema, exists := c.Schemas[fqn.Schema] + if !exists { + return pg.ErrorSchemaDoesNotExist(fqn.Schema) + } + if _, exists := schema.Enums[fqn.Rel]; exists { + return pg.ErrorTypeAlreadyExists(fqn.Rel) + } + schema.Enums[fqn.Rel] = pg.Enum{ + Name: fqn.Rel, + Vals: stringSlice(n.Vals), + } + + case nodes.CreateSchemaStmt: + name := *n.Schemaname + if _, exists := c.Schemas[name]; exists { + return pg.ErrorSchemaAlreadyExists(name) + } + c.Schemas[name] = pg.NewSchema() + case nodes.DropStmt: for _, obj := range n.Objects.Items { - var fqn pg.FQN - var err error - - switch o := obj.(type) { - case nodes.List: - fqn, err = ParseList(o) - case nodes.TypeName: - fqn, err = ParseList(o.Names) - default: - return fmt.Errorf("nodes.DropStmt: unknown node in objects list: %T", o) - } - if err != nil { - return err - } + if n.RemoveType == nodes.OBJECT_TABLE || n.RemoveType == nodes.OBJECT_TYPE { + var fqn pg.FQN + var err error - schema, exists := c.Schemas[fqn.Schema] - if !exists { - return pg.ErrorSchemaDoesNotExist(fqn.Schema) - } + switch o := obj.(type) { + case nodes.List: + fqn, err = ParseList(o) + case nodes.TypeName: + fqn, err = ParseList(o.Names) + default: + return fmt.Errorf("nodes.DropStmt: unknown node in objects list: %T", o) + } + if err != nil { + return err + } - switch n.RemoveType { + schema, exists := c.Schemas[fqn.Schema] + if !exists { + return pg.ErrorSchemaDoesNotExist(fqn.Schema) + } + + switch n.RemoveType { + case nodes.OBJECT_TABLE: + if _, exists := schema.Tables[fqn.Rel]; exists { + delete(schema.Tables, fqn.Rel) + } else if !n.MissingOk { + return pg.ErrorRelationDoesNotExist(fqn.Rel) + } + + case nodes.OBJECT_TYPE: + if _, exists := schema.Enums[fqn.Rel]; exists { + delete(schema.Enums, fqn.Rel) + } else if !n.MissingOk { + return pg.ErrorTypeDoesNotExist(fqn.Rel) + } - case nodes.OBJECT_TABLE: - if _, exists := schema.Tables[fqn.Rel]; exists { - delete(schema.Tables, fqn.Rel) - } else if !n.MissingOk { - return pg.ErrorRelationDoesNotExist(fqn.Rel) } - case nodes.OBJECT_TYPE: - if _, exists := schema.Enums[fqn.Rel]; exists { - delete(schema.Enums, fqn.Rel) + } + + if n.RemoveType == nodes.OBJECT_SCHEMA { + var name string + switch o := obj.(type) { + case nodes.String: + name = o.Str + default: + return fmt.Errorf("nodes.DropStmt: unknown node in objects list: %T", o) + } + if _, exists := c.Schemas[name]; exists { + delete(c.Schemas, name) } else if !n.MissingOk { - return pg.ErrorTypeDoesNotExist(fqn.Rel) + return pg.ErrorSchemaDoesNotExist(name) } - } } diff --git a/internal/catalog/build_test.go b/internal/catalog/build_test.go index b64765945d..91a3589ea8 100644 --- a/internal/catalog/build_test.go +++ b/internal/catalog/build_test.go @@ -41,7 +41,6 @@ func TestUpdate(t *testing.T) { Vals: []string{"open", "closed"}, }, }, - Tables: map[string]pg.Table{}, }, }, }, @@ -51,7 +50,6 @@ func TestUpdate(t *testing.T) { pg.Catalog{ Schemas: map[string]pg.Schema{ "public": { - Enums: map[string]pg.Enum{}, Tables: map[string]pg.Table{ "venues": pg.Table{ Name: "venues", @@ -70,7 +68,6 @@ func TestUpdate(t *testing.T) { pg.Catalog{ Schemas: map[string]pg.Schema{ "public": { - Enums: map[string]pg.Enum{}, Tables: map[string]pg.Table{ "foo": pg.Table{ Name: "foo", @@ -82,45 +79,124 @@ func TestUpdate(t *testing.T) { }, { ` - CREATE TABLE venues (); - DROP TABLE venues; + CREATE TABLE foo (); + ALTER TABLE foo DROP COLUMN IF EXISTS bar; `, - pg.NewCatalog(), + pg.Catalog{ + Schemas: map[string]pg.Schema{ + "public": { + Tables: map[string]pg.Table{ + "foo": pg.Table{ + Name: "foo", + }, + }, + }, + }, + }, }, { ` - CREATE TYPE status AS ENUM ('open', 'closed'); - DROP TYPE status; + CREATE TABLE foo (bar text); + ALTER TABLE foo ALTER bar SET NOT NULL; `, - pg.NewCatalog(), + pg.Catalog{ + Schemas: map[string]pg.Schema{ + "public": { + Tables: map[string]pg.Table{ + "foo": pg.Table{ + Name: "foo", + Columns: []pg.Column{{Name: "bar", DataType: "text", NotNull: true}}, + }, + }, + }, + }, + }, }, { ` - CREATE TABLE venues (); - DROP TABLE IF EXISTS venues; - DROP TABLE IF EXISTS venues; + CREATE TABLE foo (bar text NOT NULL); + ALTER TABLE foo ALTER bar DROP NOT NULL; `, - pg.NewCatalog(), + pg.Catalog{ + Schemas: map[string]pg.Schema{ + "public": { + Tables: map[string]pg.Table{ + "foo": pg.Table{ + Name: "foo", + Columns: []pg.Column{{Name: "bar", DataType: "text"}}, + }, + }, + }, + }, + }, }, { ` - CREATE TYPE status AS ENUM ('open', 'closed'); - DROP TYPE IF EXISTS status; - DROP TYPE IF EXISTS status; + CREATE TABLE foo (bar text); + ALTER TABLE foo ALTER bar SET DATA TYPE bool; `, - pg.NewCatalog(), + pg.Catalog{ + Schemas: map[string]pg.Schema{ + "public": { + Tables: map[string]pg.Table{ + "foo": pg.Table{ + Name: "foo", + Columns: []pg.Column{{Name: "bar", DataType: "bool"}}, + }, + }, + }, + }, + }, + }, + { + ` + CREATE SCHEMA foo; + CREATE SCHEMA bar; + CREATE TABLE foo.baz (); + ALTER TABLE foo.baz SET SCHEMA bar; + `, + pg.Catalog{ + Schemas: map[string]pg.Schema{ + "public": {}, + "foo": {}, + "bar": { + Tables: map[string]pg.Table{ + "baz": pg.Table{ + Name: "baz", + }, + }, + }, + }, + }, + }, + { + "CREATE TYPE status AS ENUM ('open', 'closed');", + pg.Catalog{ + Schemas: map[string]pg.Schema{ + "public": { + Enums: map[string]pg.Enum{ + "status": pg.Enum{ + Name: "status", + Vals: []string{"open", "closed"}, + }, + }, + Tables: map[string]pg.Table{}, + }, + }, + }, }, { ` CREATE TABLE venues (); - DROP TABLE public.venues; + DROP TABLE venues; `, pg.NewCatalog(), }, { ` - CREATE TYPE status AS ENUM ('open', 'closed'); - DROP TYPE public.status; + CREATE TABLE venues (); + DROP TABLE IF EXISTS venues; + DROP TABLE IF EXISTS venues; `, pg.NewCatalog(), }, @@ -142,16 +218,65 @@ func TestUpdate(t *testing.T) { }, }, }, + { + ` + CREATE TYPE status AS ENUM ('open', 'closed'); + DROP TYPE status; + `, + pg.NewCatalog(), + }, + { + ` + CREATE TYPE status AS ENUM ('open', 'closed'); + DROP TYPE IF EXISTS status; + DROP TYPE IF EXISTS status; + `, + pg.NewCatalog(), + }, + { + ` + CREATE TABLE venues (); + DROP TABLE public.venues; + `, + pg.NewCatalog(), + }, + { + ` + CREATE TYPE status AS ENUM ('open', 'closed'); + DROP TYPE public.status; + `, + pg.NewCatalog(), + }, + { + ` + CREATE TYPE status AS ENUM ('open', 'closed'); + DROP TYPE public.status; + `, + pg.NewCatalog(), + }, + { + ` + CREATE SCHEMA foo; + DROP SCHEMA foo; + `, + pg.NewCatalog(), + }, + { + ` + DROP SCHEMA IF EXISTS foo; + `, + pg.NewCatalog(), + }, } { test := tc t.Run(strconv.Itoa(i), func(t *testing.T) { - t.Log(test.stmt) - c, err := buildCatalog(test.stmt) if err != nil { + t.Log(test.stmt) t.Fatal(err) } if diff := cmp.Diff(test.c, c, cmpopts.EquateEmpty()); diff != "" { + t.Log(test.stmt) t.Errorf("catalog mismatch:\n%s", diff) } }) @@ -218,21 +343,82 @@ func TestUpdateErrors(t *testing.T) { `, pg.Error{Code: "42703", Message: "column \"bar\" of relation \"foo\" does not exist"}, }, + { + ` + CREATE TABLE foo (); + ALTER TABLE foo ALTER COLUMN bar SET NOT NULL; + `, + pg.Error{Code: "42703", Message: "column \"bar\" of relation \"foo\" does not exist"}, + }, + { + ` + CREATE TABLE foo (); + ALTER TABLE foo ALTER COLUMN bar DROP NOT NULL; + `, + pg.Error{Code: "42703", Message: "column \"bar\" of relation \"foo\" does not exist"}, + }, + { + ` + CREATE TABLE foo (); + ALTER TABLE foo ALTER COLUMN bar DROP NOT NULL; + `, + pg.Error{Code: "42703", Message: "column \"bar\" of relation \"foo\" does not exist"}, + }, + { + ` + CREATE SCHEMA foo; + CREATE SCHEMA foo; + `, + pg.Error{Code: "42P06", Message: "schema \"foo\" already exists"}, + }, + { + ` + ALTER TABLE foo.baz SET SCHEMA bar; + `, + pg.Error{Code: "3F000", Message: "schema \"foo\" does not exist"}, + }, + { + ` + CREATE SCHEMA foo; + ALTER TABLE foo.baz SET SCHEMA bar; + `, + pg.Error{Code: "42P01", Message: "relation \"baz\" does not exist"}, + }, + { + ` + CREATE SCHEMA foo; + CREATE TABLE foo.baz (); + ALTER TABLE foo.baz SET SCHEMA bar; + `, + pg.Error{Code: "3F000", Message: "schema \"bar\" does not exist"}, + }, + { + ` + DROP SCHEMA bar; + `, + pg.Error{Code: "3F000", Message: "schema \"bar\" does not exist"}, + }, } { test := tc t.Run(strconv.Itoa(i), func(t *testing.T) { - t.Log(test.stmt) _, err := buildCatalog(test.stmt) if err == nil { + t.Log(test.stmt) t.Fatal("err was nil") } var actual pg.Error if err != nil { - actual = err.(pg.Error) + pge, ok := err.(pg.Error) + if !ok { + t.Log(test.stmt) + t.Fatal(err) + } + actual = pge } if diff := cmp.Diff(test.err, actual, cmpopts.EquateEmpty()); diff != "" { + t.Log(test.stmt) t.Errorf("error mismatch: \n%s", diff) } }) diff --git a/internal/pg/catalog.go b/internal/pg/catalog.go index 494e444932..87f80a40e7 100644 --- a/internal/pg/catalog.go +++ b/internal/pg/catalog.go @@ -3,14 +3,18 @@ package pg func NewCatalog() Catalog { return Catalog{ Schemas: map[string]Schema{ - "public": Schema{ - Tables: map[string]Table{}, - Enums: map[string]Enum{}, - }, + "public": NewSchema(), }, } } +func NewSchema() Schema { + return Schema{ + Tables: map[string]Table{}, + Enums: map[string]Enum{}, + } +} + type FQN struct { Catalog string Schema string diff --git a/internal/pg/errors.go b/internal/pg/errors.go index 6769412740..cb33269e4f 100644 --- a/internal/pg/errors.go +++ b/internal/pg/errors.go @@ -40,6 +40,13 @@ func ErrorRelationDoesNotExist(tbl string) Error { } } +func ErrorSchemaAlreadyExists(sch string) Error { + return Error{ + Code: "42P06", + Message: fmt.Sprintf("schema \"%s\" already exists", sch), + } +} + func ErrorSchemaDoesNotExist(sch string) Error { return Error{ Code: "3F000",