Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
e471171
core: evaluate how types are represented, and design full type expres…
claude Sep 10, 2026
96c085e
core: review each engine's own type catalog, and canonicalize types o…
claude Sep 11, 2026
9dcdbf7
core: PostgreSQL's canonical type names are format_type's
claude Sep 11, 2026
b4271e2
core: add a worked example of an array of arrays to the types note
claude Sep 11, 2026
895ed01
core: one type row per expression, canonicalized, with the analyzer c…
claude Sep 11, 2026
fedc4a1
postgresql: typmods, dimensions, interval fields, declared types and …
claude Sep 11, 2026
48a9f7a
mysql: unsigned families, typmods, enum and set members, and the type…
claude Sep 11, 2026
14c2a58
sqlite: a declared spelling stands on its affinity
claude Sep 11, 2026
efc83d3
clickhouse: casts, typed placeholders, aggregate-function idents, can…
claude Sep 11, 2026
4144f5d
duckdb, googlesql, mssql: nested types, parameters, MAX, defaults and…
claude Sep 11, 2026
b0e1cf0
docs: describe the types analyze reports and how the design landed
claude Sep 11, 2026
85b92aa
core: a type in another namespace resolves there and is reported qual…
claude Sep 11, 2026
271b10f
core: express what a dialect does to a type as data rather than regis…
claude Sep 11, 2026
c70c5d6
core: answer the review: canonical type field, label parsing, namespa…
claude Sep 11, 2026
33ee062
core: answer the second review: qualified keys, nested lookups, rewri…
claude Sep 11, 2026
fc7b02c
endtoend: regenerate the process plugin golden for the PostgreSQL rel…
claude Sep 11, 2026
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
22 changes: 16 additions & 6 deletions docs/howto/analyze.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ reports the result columns and parameters:
{
"name": "id",
"type": {
"name": "bigserial"
"name": "bigint"
},
"table": "authors"
},
Expand All @@ -97,7 +97,7 @@ reports the result columns and parameters:
"column": {
"name": "id",
"type": {
"name": "bigserial"
"name": "bigint"
},
"table": "authors"
}
Expand All @@ -109,10 +109,20 @@ reports the result columns and parameters:

A column's `type` is written as a call expression: a `name` applied to
`args`, each of which carries an optional `label` and exactly one of `type`,
`int`, `bool` or `string`, with `nullable` set at whatever depth it applies.
An array of text is `array` applied to `text`; a `Map(String, Nullable(UInt8))`
in ClickHouse is `map` applied to `string` and a nullable `uint8`. Names are
recorded as the engine reports them.
`int`, `bool`, `string` or `ident`, with `nullable` set at whatever depth it
applies. A `numeric(10,2)` column is `numeric` applied to `10` and `2`; an
array of text is `array` applied to `text`, and an array of arrays nests
one `array` per dimension; a `Map(String, Nullable(UInt8))` in ClickHouse is
`map` applied to `string` and a nullable `uint8`; a `STRUCT<a INT64>` in
GoogleSQL is `struct` applied to an `int64` labelled `a`; the `MAX` of SQL
Server's `nvarchar(max)` is the identifier `max`.

Types are reported the way the engine itself stores and reports them rather
than the way the schema spelled them: PostgreSQL's `int` and `bigserial` are
`integer` and `bigint`, as `format_type` prints them; MySQL's `BOOLEAN` is
`tinyint(1)`; ClickHouse's `Decimal32(4)` is `decimal(9, 4)`; DuckDB's
`TEXT` is `varchar`; SQL Server's `FLOAT(24)` is `real`. SQLite, which
keeps a declared type as written, is reported as written.

Pass `--ast` to also include each statement's parsed AST under an `ast` key. It
has the same shape as the output of [`parse`](parse.md), with every node tagged
Expand Down
12 changes: 6 additions & 6 deletions internal/codegen/golang/postgresql_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ func postgresType(req *plugin.GenerateRequest, options *opts.Options, col *plugi
}
return "sql.NullTime"

case "pg_catalog.time":
case "pg_catalog.time", "time", "time without time zone":
if driver == opts.SQLDriverPGXV5 {
return "pgtype.Time"
}
Expand All @@ -228,7 +228,7 @@ func postgresType(req *plugin.GenerateRequest, options *opts.Options, col *plugi
}
return "sql.NullTime"

case "pg_catalog.timetz":
case "pg_catalog.timetz", "timetz", "time with time zone":
if notNull {
return "time.Time"
}
Expand All @@ -237,7 +237,7 @@ func postgresType(req *plugin.GenerateRequest, options *opts.Options, col *plugi
}
return "sql.NullTime"

case "pg_catalog.timestamp", "timestamp":
case "pg_catalog.timestamp", "timestamp", "timestamp without time zone":
if driver == opts.SQLDriverPGXV5 {
return "pgtype.Timestamp"
}
Expand All @@ -249,7 +249,7 @@ func postgresType(req *plugin.GenerateRequest, options *opts.Options, col *plugi
}
return "sql.NullTime"

case "pg_catalog.timestamptz", "timestamptz":
case "pg_catalog.timestamptz", "timestamptz", "timestamp with time zone":
if driver == opts.SQLDriverPGXV5 {
return "pgtype.Timestamptz"
}
Expand All @@ -261,7 +261,7 @@ func postgresType(req *plugin.GenerateRequest, options *opts.Options, col *plugi
}
return "sql.NullTime"

case "text", "pg_catalog.varchar", "pg_catalog.bpchar", "string", "citext", "name":
case "text", "pg_catalog.varchar", "varchar", "character varying", "pg_catalog.bpchar", "bpchar", "character", "string", "citext", "name":
if notNull {
return "string"
}
Expand Down Expand Up @@ -470,7 +470,7 @@ func postgresType(req *plugin.GenerateRequest, options *opts.Options, col *plugi
}
return "any"

case "bit", "varbit", "pg_catalog.bit", "pg_catalog.varbit":
case "bit", "varbit", "bit varying", "pg_catalog.bit", "pg_catalog.varbit":
if driver == opts.SQLDriverPGXV5 {
return "pgtype.Bits"
}
Expand Down
28 changes: 17 additions & 11 deletions internal/compiler/catalog_core.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,25 @@ func coreResultCatalog(c *core.Catalog) (*catalog.Catalog, error) {
}
t := &catalog.Table{Rel: &ast.TableName{Schema: ns.Name, Name: table.Name}}
for _, col := range cols {
// The catalog names an array type after its element with the
// suffix appended, which is codegen's data type and array
// flag in one string. The core catalog holds one dimension,
// and codegen renders a "[]" per dimension.
dataType, isArray := strings.CutSuffix(col.TypeName, core.ArraySuffix)
// Codegen reads a data type and an array flag, and renders
// a "[]" per dimension, so an array of arrays of integers
// is the type integer with two dimensions.
expr, err := c.TypeExprOf(col.TypeOID)
if err != nil {
return nil, err
}
inner := expr.Innermost()
column := &catalog.Column{
Name: col.Name,
Type: ast.TypeName{Name: dataType},
IsNotNull: col.NotNull,
IsArray: isArray,
Name: col.Name,
Type: ast.TypeName{Name: strings.TrimSuffix(inner.Name, " unsigned")},
IsNotNull: col.NotNull,
IsArray: expr.IsArray(),
ArrayDims: expr.ArrayDims(),
IsUnsigned: strings.HasSuffix(inner.Name, " unsigned"),
}
if isArray {
column.ArrayDims = 1
if len(inner.Args) > 0 && inner.Args[0].Int != nil {
l := int(*inner.Args[0].Int)
column.Length = &l
}
t.Columns = append(t.Columns, column)
}
Expand Down
32 changes: 21 additions & 11 deletions internal/compiler/parse_core.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,21 +107,33 @@ func coreColumn(c core.Column) *Column {
IsArray: c.IsArray,
TypeExpr: c.Type,
}
// The core reports arrays without dimensions, and codegen renders one
// "[]" per dimension.
if c.IsArray {
col.ArrayDims = 1
}
describeType(col, c.Type)
if c.Source != nil && c.Source.Table != "" {
col.Table = &ast.TableName{Schema: c.Source.Schema, Name: c.Source.Table}
col.TableAlias = c.Source.TableAlias
col.OriginalName = c.Source.Column
}
if c.TypeLength > 0 {
l := c.TypeLength
return col
}

// describeType fills in what codegen reads about a type from its
// expression: one array dimension per nesting, the length that is the
// innermost type's first integer argument (which is how a MySQL tinyint(1)
// is told from a tinyint), and whether the innermost type is unsigned.
func describeType(col *Column, t *core.TypeExpr) {
if t == nil {
if col.IsArray {
col.ArrayDims = 1
}
return
}
col.ArrayDims = t.ArrayDims()
inner := t.Innermost()
if len(inner.Args) > 0 && inner.Args[0].Int != nil {
l := int(*inner.Args[0].Int)
col.Length = &l
}
return col
col.Unsigned = strings.HasSuffix(inner.Name, " unsigned")
}

func coreParamColumn(p core.Parameter, params *named.ParamSet) *Column {
Expand All @@ -132,9 +144,7 @@ func coreParamColumn(p core.Parameter, params *named.ParamSet) *Column {
IsArray: p.IsArray,
TypeExpr: p.Type,
}
if p.IsArray {
col.ArrayDims = 1
}
describeType(col, p.Type)
if p.Source != nil && p.Source.Table != "" {
col.Table = &ast.TableName{Schema: p.Source.Schema, Name: p.Source.Table}
col.OriginalName = p.Source.Column
Expand Down
2 changes: 0 additions & 2 deletions internal/core/analysis.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,6 @@ type Column struct {
SourceAttributeOID int64 `json:"source_attribute_oid,omitempty"`
Source *ColumnSource `json:"source,omitempty"`
DeclType string `json:"decl_type,omitempty"`
TypeLength int `json:"type_length,omitempty"`
TypeScale int `json:"type_scale,omitempty"`
IsPrimaryKey bool `json:"is_primary_key,omitempty"`
IsUnique bool `json:"is_unique,omitempty"`
IsAutoIncrement bool `json:"is_auto_increment,omitempty"`
Expand Down
5 changes: 3 additions & 2 deletions internal/core/analyzer/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ func derivedRel(alias string, cols []core.Column) scopeRel {
AttOID: col.SourceAttributeOID,
Name: col.Name,
TypeOID: col.TypeOID,
Type: col.Type.WithNullable(false),
NotNull: col.NotNull,
})
}
Expand All @@ -123,12 +124,12 @@ func (a *analyzer) result() core.PrepareResult {
// the dialect has such a type.
if oid, ok := a.cat.UntypedTypeOID(); ok {
for n, p := range a.params {
if p.TypeOID == 0 && p.DataType == "" {
if p.TypeOID == 0 && p.Type == nil {
t := exprType{typeOID: oid, nullable: true}
p.TypeOID = oid
p.DataType, p.IsArray = a.typeNameOf(t)
p.NotNull = false
p.Type = a.typeExprOf(t, "")
p.Type = a.typeExprOf(t)
a.params[n] = p
}
}
Expand Down
1 change: 1 addition & 0 deletions internal/core/analyzer/dml.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ func findColumn(rel scopeRel, name string) (core.ClassColumn, bool) {
func columnType(rel scopeRel, col core.ClassColumn) exprType {
return exprType{
typeOID: col.TypeOID,
expr: col.Type,
nullable: !col.NotNull,
sourceClassOID: rel.classOID,
sourceAttributeOID: col.AttOID,
Expand Down
Loading
Loading