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
6 changes: 6 additions & 0 deletions docs/howto/overrides.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ sql:
go_type: "time.Time"
```

A column typed with a domain (PostgreSQL's `CREATE DOMAIN`, SQL Server's
`CREATE TYPE ... FROM`) is generated as the domain's base type. A `db_type`
override naming the domain itself, qualified by its schema when it is not in
the default one (`billing.ksuid`), takes precedence and applies to every
column of the domain.

> [!TIP]
> A single `db_type` override configuration applies to either nullable or non-nullable
> columns, but not both. If you want the same Go type to override regardless of
Expand Down
9 changes: 9 additions & 0 deletions internal/cmd/shim.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ func pluginCatalog(c *catalog.Catalog) *plugin.Catalog {
for _, s := range c.Schemas {
var enums []*plugin.Enum
var cts []*plugin.CompositeType
var domains []*plugin.Domain
for _, typ := range s.Types {
switch typ := typ.(type) {
case *catalog.Enum:
Expand All @@ -78,6 +79,13 @@ func pluginCatalog(c *catalog.Catalog) *plugin.Catalog {
Name: typ.Name,
Comment: typ.Comment,
})
case *catalog.Domain:
domains = append(domains, &plugin.Domain{
Name: typ.Name,
Base: pluginTypeExpr(typ.Base),
NotNull: typ.NotNull,
Comment: typ.Comment,
})
}
}
var tables []*plugin.Table
Expand Down Expand Up @@ -125,6 +133,7 @@ func pluginCatalog(c *catalog.Catalog) *plugin.Catalog {
Tables: tables,
Enums: enums,
CompositeTypes: cts,
Domains: domains,
})
}
return &plugin.Catalog{
Expand Down
119 changes: 119 additions & 0 deletions internal/codegen/golang/domain_type.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package golang

import (
"slices"
"strings"

"google.golang.org/protobuf/proto"

"github.com/sqlc-dev/sqlc/internal/plugin"
)

// domainColumn is col with every domain its type names replaced by the
// domain's base, or nil when its type names none.
func domainColumn(req *plugin.GenerateRequest, col *plugin.Column) *plugin.Column {
if col.TypeExpr != nil {
t, ok := resolveDomains(req, col.TypeExpr)
if !ok {
return nil
}
out := proto.Clone(col).(*plugin.Column)
out.TypeExpr = t
name, dims := flatType(t)
out.Type = &plugin.Identifier{Name: name}
out.IsArray, out.ArrayDims = dims > 0, dims
return out
}
d := findDomain(req, col.Type.GetSchema(), col.Type.GetName())
if baseless(req, d) {
return nil
}
// A domain over an array adds its dimensions to the column's own.
out := proto.Clone(col).(*plugin.Column)
name, dims := flatType(d.Base)
out.Type = &plugin.Identifier{Name: name}
if dims > 0 {
out.IsArray, out.ArrayDims = true, out.ArrayDims+dims
}
return out
}

// flatType is t as the flat description reads it, the way
// coreResultCatalog writes one: the innermost element's name, with one
// dimension per array around it.
func flatType(t *plugin.TypeExpr) (string, int32) {
var dims int32
for typeExprName(t) == "array" && typeExprArg(t, 0) != nil {
t = typeExprArg(t, 0)
dims++
}
return t.GetName(), dims
}

// resolveDomains replaces each domain in t with its base, keeping the
// nullability t says each position has.
func resolveDomains(req *plugin.GenerateRequest, t *plugin.TypeExpr) (*plugin.TypeExpr, bool) {
if d := findDomain(req, "", t.GetName()); !baseless(req, d) {
// One domain at a time, so an override on a domain under it applies.
base := proto.Clone(d.Base).(*plugin.TypeExpr)
base.Nullable = t.Nullable
return base, true
}
var out *plugin.TypeExpr
for i, arg := range t.GetArgs() {
inner, ok := resolveDomains(req, arg.GetType())
if arg.GetType() == nil || !ok {
continue
}
if out == nil {
out = proto.Clone(t).(*plugin.TypeExpr)
}
out.Args[i].Value = &plugin.TypeArg_Type{Type: inner}
}
return out, out != nil
}

// baseless reports whether d is missing or has no other type to resolve to:
// public.text over text reports its base as text, which a bare lookup
// finds as the domain again rather than the built-in it is.
func baseless(req *plugin.GenerateRequest, d *plugin.Domain) bool {
return d == nil || d.Base == nil || findDomain(req, "", d.Base.GetName()) == d
}

// findDomain looks a domain up by the name a column's type reports: a
// qualified name in its schema, and a bare one only in the namespaces a
// type is reported from unqualified, since one elsewhere is reported
// qualified and so is a different type.
func findDomain(req *plugin.GenerateRequest, schema, name string) *plugin.Domain {
if name == "" {
return nil
}
if schema == "" {
if i := strings.LastIndex(name, "."); i >= 0 {
schema, name = name[:i], name[i+1:]
}
}
schemas := []string{schema}
if schema == "" {
schemas = []string{req.GetCatalog().GetDefaultSchema(), "pg_catalog", engineDefaultSchema[req.GetSettings().GetEngine()]}
}
for _, s := range req.GetCatalog().GetSchemas() {
if !slices.Contains(schemas, s.Name) {
continue
}
for _, d := range s.GetDomains() {
if d.Name == name {
return d
}
}
}
return nil
}

// engineDefaultSchema is the schema an engine puts an unqualified object
// in when it is not the catalog's default, as its dialect seed names it.
var engineDefaultSchema = map[string]string{
engineClickHouse: "default",
engineDuckDB: "main",
engineMSSQL: "dbo",
}
26 changes: 26 additions & 0 deletions internal/codegen/golang/go_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import (
)

func addExtraGoStructTags(tags map[string]string, req *plugin.GenerateRequest, options *opts.Options, col *plugin.Column) {
// A domain generated as its base takes the base's tags, under its own.
if base := domainColumn(req, col); base != nil && !typeOverridden(options, col) {
addExtraGoStructTags(tags, req, options, base)
}
for _, override := range options.Overrides {
oride := override.ShimOverride
if oride.GoType.StructTags == nil {
Expand All @@ -36,6 +40,17 @@ func addExtraGoStructTags(tags map[string]string, req *plugin.GenerateRequest, o
}
}

// typeOverridden reports whether an override names col's type, which
// goInnerType then generates it as.
func typeOverridden(options *opts.Options, col *plugin.Column) bool {
for _, override := range options.Overrides {
if override.ShimOverride.GoType.TypeName != "" && override.MatchesColumn(col) {
return true
}
}
return false
}

// goType is the Go type of a column's value: what a query's row scans
// into, or what a table's model holds.
func goType(req *plugin.GenerateRequest, options *opts.Options, col *plugin.Column) string {
Expand Down Expand Up @@ -95,6 +110,17 @@ func goInnerType(req *plugin.GenerateRequest, options *opts.Options, col *plugin
}
}

// A domain is generated as the type it stands on, unless an override
// above named the domain itself. The base says how many dimensions the
// column has, since a domain may stand on an array.
if base := domainColumn(req, col); base != nil {
typ, complete := goInnerType(req, options, base, param)
if base.IsArray && !complete {
typ = strings.Repeat("[]", int(base.ArrayDims)) + typ
}
return typ, true
}

// TODO: Extend the engine interface to handle types
switch req.Settings.Engine {
case engineMySQL:
Expand Down
5 changes: 2 additions & 3 deletions internal/codegen/golang/mssql_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,8 @@ import (
// come back as their decimal text, so they map to string the way
// PostgreSQL's numeric does under database/sql. A uniqueidentifier maps to
// the driver's own type, which keeps SQL Server's byte order straight. An
// alias type created with CREATE TYPE ... FROM is reported by its own name,
// which the catalog does not yet resolve to its base, so it maps to any
// until it does.
// alias type created with CREATE TYPE ... FROM never reaches here: it is
// mapped as its base.
func mssqlType(req *plugin.GenerateRequest, options *opts.Options, col *plugin.Column) string {
t, nullable := columnType(col)
null := func(typ, nullTyp string) string {
Expand Down
17 changes: 14 additions & 3 deletions internal/compiler/catalog_core.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import (

// coreResultCatalog dumps the core catalog into the legacy catalog shape a
// Result carries, so codegen sees the same table models either way a query
// set was analyzed. Only relations make the trip: codegen reads tables and
// their columns to build models, and none of the types, functions or
// operators the core catalog also holds.
// set was analyzed. Codegen reads tables and their columns to build models,
// and the domains those columns may be typed with; none of the other types,
// functions or operators the core catalog also holds.
func coreResultCatalog(c *core.Catalog) (*catalog.Catalog, error) {
cat := catalog.New("public")
namespaces, err := c.Namespaces()
Expand Down Expand Up @@ -60,6 +60,17 @@ func coreResultCatalog(c *core.Catalog) (*catalog.Catalog, error) {
}
schema.Tables = append(schema.Tables, t)
}
domains, err := c.DomainsInNamespace(ns.OID)
if err != nil {
return nil, err
}
for _, d := range domains {
base, err := c.TypeExprOf(d.BaseOID)
if err != nil {
return nil, err
}
schema.Types = append(schema.Types, &catalog.Domain{Name: d.Name, Base: base, NotNull: d.NotNull})
}
cat.Schemas = append(cat.Schemas, schema)
}
return cat, nil
Expand Down
29 changes: 29 additions & 0 deletions internal/core/catalogdb/query.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions internal/core/catalogdef/query.sql
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,11 @@ SELECT oid, name FROM sql_class
WHERE namespace_oid = ? AND kind = 'r'
ORDER BY oid;

-- name: ListDomainsInNamespace :many
SELECT oid FROM sql_type
WHERE namespace_oid = ? AND typtype = 'd'
ORDER BY oid;

-- name: DeleteClass :exec
DELETE FROM sql_class WHERE oid = ?;

Expand Down
17 changes: 17 additions & 0 deletions internal/core/class.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,23 @@ func (c *Catalog) RenameClass(classOID int64, name string) error {
return nil
}

// DomainsInNamespace lists the domains and alias types a namespace holds.
func (c *Catalog) DomainsInNamespace(namespaceOID int64) ([]TypeInfo, error) {
oids, err := c.q.ListDomainsInNamespace(context.Background(), namespaceOID)
if err != nil {
return nil, fmt.Errorf("list domains in namespace %d: %w", namespaceOID, err)
}
out := make([]TypeInfo, 0, len(oids))
for _, oid := range oids {
info, err := c.LookupType(oid)
if err != nil {
return nil, err
}
out = append(out, info)
}
return out, nil
}

type ClassInfo struct {
OID int64
Name string
Expand Down
12 changes: 8 additions & 4 deletions internal/endtoend/testdata/codegen_json/gen/codegen.json
Original file line number Diff line number Diff line change
Expand Up @@ -119,14 +119,16 @@
}
],
"enums": [],
"composite_types": []
"composite_types": [],
"domains": []
},
{
"comment": "",
"name": "pg_temp",
"tables": [],
"enums": [],
"composite_types": []
"composite_types": [],
"domains": []
},
{
"comment": "",
Expand Down Expand Up @@ -48568,7 +48570,8 @@
}
],
"enums": [],
"composite_types": []
"composite_types": [],
"domains": []
},
{
"comment": "",
Expand Down Expand Up @@ -68733,7 +68736,8 @@
}
],
"enums": [],
"composite_types": []
"composite_types": [],
"domains": []
}
]
},
Expand Down
3 changes: 3 additions & 0 deletions internal/endtoend/testdata/codegen_json_domains/exec.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"contexts": ["base"]
}
Loading
Loading