diff --git a/internal/catalog/build.go b/internal/catalog/build.go index 20215d5f90..9f4b7d0dba 100644 --- a/internal/catalog/build.go +++ b/internal/catalog/build.go @@ -225,6 +225,29 @@ func Update(c *pg.Catalog, stmt nodes.Node) error { } } + case nodes.CompositeTypeStmt: + fqn, err := ParseRange(n.Typevar) + if err != nil { + return err + } + schema, exists := c.Schemas[fqn.Schema] + if !exists { + return wrap(pg.ErrorSchemaDoesNotExist(fqn.Schema), raw.StmtLocation) + } + // Because tables have associated data types, the type name must also + // be distinct from the name of any existing table in the same + // schema. + // https://www.postgresql.org/docs/current/sql-createtype.html + if _, exists := schema.Tables[fqn.Rel]; exists { + return wrap(pg.ErrorRelationAlreadyExists(fqn.Rel), raw.StmtLocation) + } + if _, exists := schema.Types[fqn.Rel]; exists { + return wrap(pg.ErrorRelationAlreadyExists(fqn.Rel), raw.StmtLocation) + } + schema.Types[fqn.Rel] = pg.CompositeType{ + Name: fqn.Rel, + } + case nodes.CreateStmt: fqn, err := ParseRange(n.Relation) if err != nil { @@ -264,10 +287,17 @@ func Update(c *pg.Catalog, stmt nodes.Node) error { if !exists { return wrap(pg.ErrorSchemaDoesNotExist(fqn.Schema), raw.StmtLocation) } - if _, exists := schema.Enums[fqn.Rel]; exists { + // Because tables have associated data types, the type name must also + // be distinct from the name of any existing table in the same + // schema. + // https://www.postgresql.org/docs/current/sql-createtype.html + if _, exists := schema.Tables[fqn.Rel]; exists { + return wrap(pg.ErrorRelationAlreadyExists(fqn.Rel), raw.StmtLocation) + } + if _, exists := schema.Types[fqn.Rel]; exists { return wrap(pg.ErrorTypeAlreadyExists(fqn.Rel), raw.StmtLocation) } - schema.Enums[fqn.Rel] = pg.Enum{ + schema.Types[fqn.Rel] = pg.Enum{ Name: fqn.Rel, Vals: stringSlice(n.Vals), } @@ -311,8 +341,8 @@ func Update(c *pg.Catalog, stmt nodes.Node) error { } case nodes.OBJECT_TYPE: - if _, exists := schema.Enums[fqn.Rel]; exists { - delete(schema.Enums, fqn.Rel) + if _, exists := schema.Types[fqn.Rel]; exists { + delete(schema.Types, fqn.Rel) } else if !n.MissingOk { return wrap(pg.ErrorTypeDoesNotExist(fqn.Rel), raw.StmtLocation) } @@ -507,16 +537,19 @@ func Update(c *pg.Catalog, stmt nodes.Node) error { if !exists { return wrap(pg.ErrorSchemaDoesNotExist(fqn.Schema), raw.StmtLocation) } - enum, exists := schema.Enums[fqn.Rel] + typ, exists := schema.Types[fqn.Rel] if !exists { return wrap(pg.ErrorRelationDoesNotExist(fqn.Rel), raw.StmtLocation) } - if n.Comment != nil { - enum.Comment = *n.Comment - } else { - enum.Comment = "" + switch t := typ.(type) { + case pg.Enum: + if n.Comment != nil { + t.Comment = *n.Comment + } else { + t.Comment = "" + } + schema.Types[fqn.Rel] = t } - schema.Enums[fqn.Rel] = enum } diff --git a/internal/catalog/build_test.go b/internal/catalog/build_test.go index d4ee09ce7c..f2df039d7f 100644 --- a/internal/catalog/build_test.go +++ b/internal/catalog/build_test.go @@ -35,7 +35,7 @@ func TestUpdate(t *testing.T) { pg.Catalog{ Schemas: map[string]pg.Schema{ "public": { - Enums: map[string]pg.Enum{ + Types: map[string]pg.Type{ "status": pg.Enum{ Name: "status", Vals: []string{"open", "closed"}, @@ -229,7 +229,7 @@ func TestUpdate(t *testing.T) { pg.Catalog{ Schemas: map[string]pg.Schema{ "public": { - Enums: map[string]pg.Enum{ + Types: map[string]pg.Type{ "status": pg.Enum{ Name: "status", Vals: []string{"open", "closed"}, @@ -263,7 +263,7 @@ func TestUpdate(t *testing.T) { pg.Catalog{ Schemas: map[string]pg.Schema{ "public": { - Enums: map[string]pg.Enum{}, + Types: map[string]pg.Type{}, Tables: map[string]pg.Table{ "arenas": pg.Table{ Name: "arenas", @@ -337,7 +337,7 @@ func TestUpdate(t *testing.T) { pg.Catalog{ Schemas: map[string]pg.Schema{ "public": { - Enums: map[string]pg.Enum{}, + Types: map[string]pg.Type{}, Tables: map[string]pg.Table{ "venues": pg.Table{ Name: "venues", @@ -530,7 +530,7 @@ func TestUpdate(t *testing.T) { }, }, }, - Enums: map[string]pg.Enum{"bat": {Comment: "Enum comment", Name: "bat", Vals: []string{"bat"}}}, + Types: map[string]pg.Type{"bat": pg.Enum{Comment: "Enum comment", Name: "bat", Vals: []string{"bat"}}}, Funcs: map[string][]pg.Function{}, }, }, @@ -561,7 +561,7 @@ func TestUpdate(t *testing.T) { }, }, }, - Enums: map[string]pg.Enum{"bat": {Comment: "Enum comment", Name: "bat", Vals: []string{"bat"}}}, + Types: map[string]pg.Type{"bat": pg.Enum{Comment: "Enum comment", Name: "bat", Vals: []string{"bat"}}}, Funcs: map[string][]pg.Function{}, }, }, diff --git a/internal/dinosql/gen.go b/internal/dinosql/gen.go index b3f7634434..a8c88f55b1 100644 --- a/internal/dinosql/gen.go +++ b/internal/dinosql/gen.go @@ -495,7 +495,7 @@ func (r Result) Enums(settings CombinedSettings) []GoEnum { if name == "pg_catalog" { continue } - for _, enum := range schema.Enums { + for _, enum := range schema.Enums() { var enumName string if name == "public" { enumName = enum.Name @@ -711,12 +711,20 @@ func (r Result) goInnerType(col core.Column, settings CombinedSettings) string { if name == "pg_catalog" { continue } - for _, enum := range schema.Enums { - if fqn.Rel == enum.Name && fqn.Schema == name { - if name == "public" { - return StructName(enum.Name, settings) + for _, typ := range schema.Types { + switch t := typ.(type) { + case core.Enum: + if fqn.Rel == t.Name && fqn.Schema == name { + if name == "public" { + return StructName(t.Name, settings) + } + return StructName(name+"_"+t.Name, settings) + } + case core.CompositeType: + if notNull { + return "string" } - return StructName(name+"_"+enum.Name, settings) + return "sql.NullString" } } } diff --git a/internal/endtoend/testdata/composite_type/go/db.go b/internal/endtoend/testdata/composite_type/go/db.go new file mode 100644 index 0000000000..6a99519302 --- /dev/null +++ b/internal/endtoend/testdata/composite_type/go/db.go @@ -0,0 +1,29 @@ +// Code generated by sqlc. DO NOT EDIT. + +package querytest + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/composite_type/go/models.go b/internal/endtoend/testdata/composite_type/go/models.go new file mode 100644 index 0000000000..f2ebcd2576 --- /dev/null +++ b/internal/endtoend/testdata/composite_type/go/models.go @@ -0,0 +1,12 @@ +// Code generated by sqlc. DO NOT EDIT. + +package querytest + +import ( + "database/sql" +) + +type FooPath struct { + PointOne sql.NullString + PointTwo sql.NullString +} diff --git a/internal/endtoend/testdata/composite_type/go/query.sql.go b/internal/endtoend/testdata/composite_type/go/query.sql.go new file mode 100644 index 0000000000..fdc5cf1771 --- /dev/null +++ b/internal/endtoend/testdata/composite_type/go/query.sql.go @@ -0,0 +1,35 @@ +// Code generated by sqlc. DO NOT EDIT. +// source: query.sql + +package querytest + +import ( + "context" +) + +const listPaths = `-- name: ListPaths :many +SELECT point_one, point_two FROM foo.paths +` + +func (q *Queries) ListPaths(ctx context.Context) ([]FooPath, error) { + rows, err := q.db.QueryContext(ctx, listPaths) + if err != nil { + return nil, err + } + defer rows.Close() + var items []FooPath + for rows.Next() { + var i FooPath + if err := rows.Scan(&i.PointOne, &i.PointTwo); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/endtoend/testdata/composite_type/query.sql b/internal/endtoend/testdata/composite_type/query.sql new file mode 100644 index 0000000000..8e053a61a2 --- /dev/null +++ b/internal/endtoend/testdata/composite_type/query.sql @@ -0,0 +1,19 @@ +CREATE SCHEMA foo; + +CREATE TYPE point_type AS ( + x integer, + y integer +); + +CREATE TYPE foo.point_type AS ( + x integer, + y integer +); + +CREATE TABLE foo.paths ( + point_one point_type, + point_two foo.point_type +); + +-- name: ListPaths :many +SELECT * FROM foo.paths; diff --git a/internal/endtoend/testdata/composite_type/sqlc.json b/internal/endtoend/testdata/composite_type/sqlc.json new file mode 100644 index 0000000000..1161aac713 --- /dev/null +++ b/internal/endtoend/testdata/composite_type/sqlc.json @@ -0,0 +1,9 @@ +{ + "version": "1", + "packages": [{ + "path": "go", + "name": "querytest", + "schema": "query.sql", + "queries": "query.sql" + }] +} diff --git a/internal/pg/catalog.go b/internal/pg/catalog.go index f4eec2d23a..bcd2543f76 100644 --- a/internal/pg/catalog.go +++ b/internal/pg/catalog.go @@ -21,7 +21,7 @@ func NewCatalog() Catalog { func NewSchema() Schema { return Schema{ Tables: map[string]Table{}, - Enums: map[string]Enum{}, + Types: map[string]Type{}, Funcs: map[string][]Function{}, } } @@ -93,11 +93,21 @@ func (c Catalog) LookupFunctionN(fqn FQN, argn int) (Function, error) { type Schema struct { Name string Tables map[string]Table - Enums map[string]Enum + Types map[string]Type Funcs map[string][]Function Comment string } +func (s Schema) Enums() []Enum { + var enums []Enum + for _, typ := range s.Types { + if enum, ok := typ.(Enum); ok { + enums = append(enums, enum) + } + } + return enums +} + type Table struct { ID FQN Name string @@ -117,12 +127,26 @@ type Column struct { Table FQN } +type Type interface { + isType() +} + type Enum struct { Name string Vals []string Comment string } +func (e Enum) isType() { +} + +type CompositeType struct { + Name string +} + +func (e CompositeType) isType() { +} + type Function struct { Name string ArgN int