Skip to content

Commit a237740

Browse files
luongs3claude
andcommitted
fix(catalog): synthesize Postgres system columns (xmin, ctid, ...) (#3742)
Problem ------- sqlc errored with "column does not exist" when a PostgreSQL query referenced any of the six system columns (tableoid, xmin, cmin, xmax, cmax, ctid) unless `database.managed: true` was configured. The 2023 fix (#2871 / #1745) only added the columns inside the PostgreSQL analyzer, which is initialized only when a live database is attached. Code-only pipelines that build the catalog purely from schema files still hit the same "column \"xmin\" does not exist" failure 17 months later (#3742). Fix --- Synthesize the six PostgreSQL system columns inside `compiler.QueryCatalog.GetTable` whenever the engine is `postgresql`. The catalog itself is left untouched (so MySQL/SQLite tables are not polluted), and the synthesized columns are marked with a new `Column.IsSystem` flag so `SELECT *` / `RETURNING *` expansion skips them — matching PostgreSQL's own behavior. Explicit references like `SELECT xmin, ctid FROM foo` and aliased forms like `a.xmin` resolve to the correct types (oid/xid/cid -> pgtype.Uint32, tid -> pgtype.TID). Changed files ------------- - internal/compiler/query.go: add `Column.IsSystem`. - internal/compiler/query_catalog.go: track engine on QueryCatalog; append `pgSystemColumns` in GetTable when engine == postgresql. - internal/compiler/expand.go: skip IsSystem columns in `*` expansion. - internal/compiler/output_columns.go: skip IsSystem columns in the output-columns `*` branch (mirror of expand.go). - internal/endtoend/testdata/system_columns_3742/postgresql/pgx/v5/: new fixture exercising all six system columns, a table alias, and SELECT * (which must omit system columns). Golden files committed. Tests ----- - `go test ./internal/compiler/... ./internal/sql/... ./internal/engine/... ./internal/codegen/...` — all pass. - `go test -run TestExamples ./internal/endtoend/` — passes. - `go vet` + `gofmt -l` clean on touched files. Local verification ------------------ Built `/tmp/sqlc-dev-3742` from this branch and ran it against the exact schema + query in the issue. Before fix: `EXIT=1, column "xmin" does not exist`. After fix: `EXIT=0`, generates `pgtype.Uint32` for xmin, `pgtype.TID` for ctid. Confirmed `SELECT *` still expands to only the user columns (id, name, bio) — system columns are not included. What does NOT change -------------------- - MySQL / SQLite catalogs are untouched. - The managed-database analyzer path (#2871) is untouched and still works for users on `database.managed: true`. - `SELECT *` continues to omit system columns (PG-compatible). - The existing `select_system` endtoend fixture (managed-db) is left alone; the new fixture covers the non-managed code path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a3b0cfd commit a237740

10 files changed

Lines changed: 206 additions & 1 deletion

File tree

internal/compiler/expand.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,12 @@ func (c *Compiler) expandStmt(qc *QueryCatalog, raw *ast.RawStmt, node ast.Node)
138138
tableName := c.quoteIdent(t.Rel.Name)
139139
scopeName := c.quoteIdent(scope)
140140
for _, column := range t.Columns {
141+
// Skip PostgreSQL system columns (xmin, ctid, ...) when expanding *.
142+
// This matches PostgreSQL's own behavior — they must be referenced
143+
// explicitly. See issue #3742.
144+
if column.IsSystem {
145+
continue
146+
}
141147
cname := column.Name
142148
if res.Name != nil {
143149
cname = *res.Name

internal/compiler/output_columns.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,11 @@ func (c *Compiler) outputColumns(qc *QueryCatalog, node ast.Node) ([]*Column, er
273273
continue
274274
}
275275
for _, c := range t.Columns {
276+
// Skip PostgreSQL system columns on SELECT * to match PG behavior.
277+
// See issue #3742.
278+
if c.IsSystem {
279+
continue
280+
}
276281
cname := c.Name
277282
if res.Name != nil {
278283
cname = *res.Name

internal/compiler/query.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ type Column struct {
3939

4040
IsSqlcSlice bool // is this sqlc.slice()
4141

42+
// IsSystem indicates this is a PostgreSQL system column synthesized by
43+
// QueryCatalog.GetTable (tableoid, xmin, cmin, xmax, cmax, ctid). System
44+
// columns are excluded from SELECT * / RETURNING * expansion to match
45+
// PostgreSQL's own behavior.
46+
IsSystem bool
47+
4248
skipTableRequiredCheck bool
4349
}
4450

internal/compiler/query_catalog.go

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package compiler
33
import (
44
"fmt"
55

6+
"github.com/sqlc-dev/sqlc/internal/config"
67
"github.com/sqlc-dev/sqlc/internal/sql/ast"
78
"github.com/sqlc-dev/sqlc/internal/sql/catalog"
89
"github.com/sqlc-dev/sqlc/internal/sql/rewrite"
@@ -12,6 +13,7 @@ type QueryCatalog struct {
1213
catalog *catalog.Catalog
1314
ctes map[string]*Table
1415
embeds rewrite.EmbedSet
16+
engine config.Engine
1517
}
1618

1719
func (comp *Compiler) buildQueryCatalog(c *catalog.Catalog, node ast.Node, embeds rewrite.EmbedSet) (*QueryCatalog, error) {
@@ -28,7 +30,7 @@ func (comp *Compiler) buildQueryCatalog(c *catalog.Catalog, node ast.Node, embed
2830
default:
2931
with = nil
3032
}
31-
qc := &QueryCatalog{catalog: c, ctes: map[string]*Table{}, embeds: embeds}
33+
qc := &QueryCatalog{catalog: c, ctes: map[string]*Table{}, embeds: embeds, engine: comp.conf.Engine}
3234
if with != nil {
3335
for _, item := range with.Ctes.Items {
3436
if cte, ok := item.(*ast.CommonTableExpr); ok {
@@ -90,9 +92,43 @@ func (qc QueryCatalog) GetTable(rel *ast.TableName) (*Table, error) {
9092
for _, c := range src.Columns {
9193
cols = append(cols, ConvertColumn(rel, c))
9294
}
95+
// PostgreSQL exposes six system columns on every user table
96+
// (tableoid, xmin, cmin, xmax, cmax, ctid). They are not part of the
97+
// CREATE TABLE definition, so the catalog has no record of them — but
98+
// queries are allowed to reference them by name. Synthesize them here
99+
// so the compiler can resolve refs like `SELECT xmin, ctid FROM foo`.
100+
// They are marked IsSystem so SELECT * / RETURNING * skip them, which
101+
// matches PostgreSQL's own behavior. See issues #1745, #3742.
102+
if qc.engine == config.EnginePostgreSQL {
103+
cols = append(cols, pgSystemColumns(rel)...)
104+
}
93105
return &Table{Rel: rel, Columns: cols}, nil
94106
}
95107

108+
// pgSystemColumns returns the six PostgreSQL system columns synthesized for
109+
// every user table. See https://www.postgresql.org/docs/current/ddl-system-columns.html
110+
func pgSystemColumns(rel *ast.TableName) []*Column {
111+
mk := func(name, typ string) *Column {
112+
t := &ast.TypeName{Name: typ}
113+
return &Column{
114+
Name: name,
115+
DataType: typ,
116+
NotNull: true,
117+
Table: rel,
118+
Type: t,
119+
IsSystem: true,
120+
}
121+
}
122+
return []*Column{
123+
mk("tableoid", "oid"),
124+
mk("xmin", "xid"),
125+
mk("cmin", "cid"),
126+
mk("xmax", "xid"),
127+
mk("cmax", "cid"),
128+
mk("ctid", "tid"),
129+
}
130+
}
131+
96132
func (qc QueryCatalog) GetFunc(rel *ast.FuncName) (*Function, error) {
97133
funcs, err := qc.catalog.ListFuncsByName(rel)
98134
if err != nil {

internal/endtoend/testdata/system_columns_3742/postgresql/pgx/v5/go/db.go

Lines changed: 32 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/endtoend/testdata/system_columns_3742/postgresql/pgx/v5/go/models.go

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/endtoend/testdata/system_columns_3742/postgresql/pgx/v5/go/query.sql.go

Lines changed: 79 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
-- name: GetSystemColumns :one
2+
SELECT xmin, cmin, xmax, cmax, ctid, tableoid FROM authors LIMIT 1;
3+
4+
-- name: GetSystemColumnsAliased :one
5+
SELECT a.xmin, a.ctid FROM authors a LIMIT 1;
6+
7+
-- name: SelectStar :many
8+
SELECT * FROM authors;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
CREATE TABLE authors (
2+
id BIGSERIAL PRIMARY KEY,
3+
name text NOT NULL,
4+
bio text
5+
);
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"version": "1",
3+
"packages": [
4+
{
5+
"path": "go",
6+
"engine": "postgresql",
7+
"sql_package": "pgx/v5",
8+
"name": "querytest",
9+
"schema": "schema.sql",
10+
"queries": "query.sql"
11+
}
12+
]
13+
}

0 commit comments

Comments
 (0)