diff --git a/internal/compiler/expand.go b/internal/compiler/expand.go index c60b7618b2..06e82670f8 100644 --- a/internal/compiler/expand.go +++ b/internal/compiler/expand.go @@ -138,6 +138,12 @@ func (c *Compiler) expandStmt(qc *QueryCatalog, raw *ast.RawStmt, node ast.Node) tableName := c.quoteIdent(t.Rel.Name) scopeName := c.quoteIdent(scope) for _, column := range t.Columns { + // Skip PostgreSQL system columns (xmin, ctid, ...) when expanding *. + // This matches PostgreSQL's own behavior — they must be referenced + // explicitly. See issue #3742. + if column.IsSystem { + continue + } cname := column.Name if res.Name != nil { cname = *res.Name diff --git a/internal/compiler/output_columns.go b/internal/compiler/output_columns.go index dbd486359a..d4d473607a 100644 --- a/internal/compiler/output_columns.go +++ b/internal/compiler/output_columns.go @@ -273,6 +273,11 @@ func (c *Compiler) outputColumns(qc *QueryCatalog, node ast.Node) ([]*Column, er continue } for _, c := range t.Columns { + // Skip PostgreSQL system columns on SELECT * to match PG behavior. + // See issue #3742. + if c.IsSystem { + continue + } cname := c.Name if res.Name != nil { cname = *res.Name diff --git a/internal/compiler/query.go b/internal/compiler/query.go index b3cf9d6154..871c8beb7f 100644 --- a/internal/compiler/query.go +++ b/internal/compiler/query.go @@ -39,6 +39,12 @@ type Column struct { IsSqlcSlice bool // is this sqlc.slice() + // IsSystem indicates this is a PostgreSQL system column synthesized by + // QueryCatalog.GetTable (tableoid, xmin, cmin, xmax, cmax, ctid). System + // columns are excluded from SELECT * / RETURNING * expansion to match + // PostgreSQL's own behavior. + IsSystem bool + skipTableRequiredCheck bool } diff --git a/internal/compiler/query_catalog.go b/internal/compiler/query_catalog.go index 80b59d876c..d36eca51b0 100644 --- a/internal/compiler/query_catalog.go +++ b/internal/compiler/query_catalog.go @@ -3,6 +3,7 @@ package compiler import ( "fmt" + "github.com/sqlc-dev/sqlc/internal/config" "github.com/sqlc-dev/sqlc/internal/sql/ast" "github.com/sqlc-dev/sqlc/internal/sql/catalog" "github.com/sqlc-dev/sqlc/internal/sql/rewrite" @@ -12,6 +13,7 @@ type QueryCatalog struct { catalog *catalog.Catalog ctes map[string]*Table embeds rewrite.EmbedSet + engine config.Engine } 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 default: with = nil } - qc := &QueryCatalog{catalog: c, ctes: map[string]*Table{}, embeds: embeds} + qc := &QueryCatalog{catalog: c, ctes: map[string]*Table{}, embeds: embeds, engine: comp.conf.Engine} if with != nil { for _, item := range with.Ctes.Items { if cte, ok := item.(*ast.CommonTableExpr); ok { @@ -90,9 +92,43 @@ func (qc QueryCatalog) GetTable(rel *ast.TableName) (*Table, error) { for _, c := range src.Columns { cols = append(cols, ConvertColumn(rel, c)) } + // PostgreSQL exposes six system columns on every user table + // (tableoid, xmin, cmin, xmax, cmax, ctid). They are not part of the + // CREATE TABLE definition, so the catalog has no record of them — but + // queries are allowed to reference them by name. Synthesize them here + // so the compiler can resolve refs like `SELECT xmin, ctid FROM foo`. + // They are marked IsSystem so SELECT * / RETURNING * skip them, which + // matches PostgreSQL's own behavior. See issues #1745, #3742. + if qc.engine == config.EnginePostgreSQL { + cols = append(cols, pgSystemColumns(rel)...) + } return &Table{Rel: rel, Columns: cols}, nil } +// pgSystemColumns returns the six PostgreSQL system columns synthesized for +// every user table. See https://www.postgresql.org/docs/current/ddl-system-columns.html +func pgSystemColumns(rel *ast.TableName) []*Column { + mk := func(name, typ string) *Column { + t := &ast.TypeName{Name: typ} + return &Column{ + Name: name, + DataType: typ, + NotNull: true, + Table: rel, + Type: t, + IsSystem: true, + } + } + return []*Column{ + mk("tableoid", "oid"), + mk("xmin", "xid"), + mk("cmin", "cid"), + mk("xmax", "xid"), + mk("cmax", "cid"), + mk("ctid", "tid"), + } +} + func (qc QueryCatalog) GetFunc(rel *ast.FuncName) (*Function, error) { funcs, err := qc.catalog.ListFuncsByName(rel) if err != nil { diff --git a/internal/endtoend/testdata/system_columns_3742/postgresql/pgx/v5/go/db.go b/internal/endtoend/testdata/system_columns_3742/postgresql/pgx/v5/go/db.go new file mode 100644 index 0000000000..0057c62319 --- /dev/null +++ b/internal/endtoend/testdata/system_columns_3742/postgresql/pgx/v5/go/db.go @@ -0,0 +1,32 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "context" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" +) + +type DBTX interface { + Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error) + Query(context.Context, string, ...interface{}) (pgx.Rows, error) + QueryRow(context.Context, string, ...interface{}) pgx.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx pgx.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/system_columns_3742/postgresql/pgx/v5/go/models.go b/internal/endtoend/testdata/system_columns_3742/postgresql/pgx/v5/go/models.go new file mode 100644 index 0000000000..4e97f66a99 --- /dev/null +++ b/internal/endtoend/testdata/system_columns_3742/postgresql/pgx/v5/go/models.go @@ -0,0 +1,15 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "github.com/jackc/pgx/v5/pgtype" +) + +type Author struct { + ID int64 + Name string + Bio pgtype.Text +} diff --git a/internal/endtoend/testdata/system_columns_3742/postgresql/pgx/v5/go/query.sql.go b/internal/endtoend/testdata/system_columns_3742/postgresql/pgx/v5/go/query.sql.go new file mode 100644 index 0000000000..e142287e1d --- /dev/null +++ b/internal/endtoend/testdata/system_columns_3742/postgresql/pgx/v5/go/query.sql.go @@ -0,0 +1,79 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: query.sql + +package querytest + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const getSystemColumns = `-- name: GetSystemColumns :one +SELECT xmin, cmin, xmax, cmax, ctid, tableoid FROM authors LIMIT 1 +` + +type GetSystemColumnsRow struct { + Xmin pgtype.Uint32 + Cmin pgtype.Uint32 + Xmax pgtype.Uint32 + Cmax pgtype.Uint32 + Ctid pgtype.TID + Tableoid pgtype.Uint32 +} + +func (q *Queries) GetSystemColumns(ctx context.Context) (GetSystemColumnsRow, error) { + row := q.db.QueryRow(ctx, getSystemColumns) + var i GetSystemColumnsRow + err := row.Scan( + &i.Xmin, + &i.Cmin, + &i.Xmax, + &i.Cmax, + &i.Ctid, + &i.Tableoid, + ) + return i, err +} + +const getSystemColumnsAliased = `-- name: GetSystemColumnsAliased :one +SELECT a.xmin, a.ctid FROM authors a LIMIT 1 +` + +type GetSystemColumnsAliasedRow struct { + Xmin pgtype.Uint32 + Ctid pgtype.TID +} + +func (q *Queries) GetSystemColumnsAliased(ctx context.Context) (GetSystemColumnsAliasedRow, error) { + row := q.db.QueryRow(ctx, getSystemColumnsAliased) + var i GetSystemColumnsAliasedRow + err := row.Scan(&i.Xmin, &i.Ctid) + return i, err +} + +const selectStar = `-- name: SelectStar :many +SELECT id, name, bio FROM authors +` + +func (q *Queries) SelectStar(ctx context.Context) ([]Author, error) { + rows, err := q.db.Query(ctx, selectStar) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Author + for rows.Next() { + var i Author + if err := rows.Scan(&i.ID, &i.Name, &i.Bio); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/endtoend/testdata/system_columns_3742/postgresql/pgx/v5/query.sql b/internal/endtoend/testdata/system_columns_3742/postgresql/pgx/v5/query.sql new file mode 100644 index 0000000000..1f13368a59 --- /dev/null +++ b/internal/endtoend/testdata/system_columns_3742/postgresql/pgx/v5/query.sql @@ -0,0 +1,8 @@ +-- name: GetSystemColumns :one +SELECT xmin, cmin, xmax, cmax, ctid, tableoid FROM authors LIMIT 1; + +-- name: GetSystemColumnsAliased :one +SELECT a.xmin, a.ctid FROM authors a LIMIT 1; + +-- name: SelectStar :many +SELECT * FROM authors; diff --git a/internal/endtoend/testdata/system_columns_3742/postgresql/pgx/v5/schema.sql b/internal/endtoend/testdata/system_columns_3742/postgresql/pgx/v5/schema.sql new file mode 100644 index 0000000000..69b607d902 --- /dev/null +++ b/internal/endtoend/testdata/system_columns_3742/postgresql/pgx/v5/schema.sql @@ -0,0 +1,5 @@ +CREATE TABLE authors ( + id BIGSERIAL PRIMARY KEY, + name text NOT NULL, + bio text +); diff --git a/internal/endtoend/testdata/system_columns_3742/postgresql/pgx/v5/sqlc.json b/internal/endtoend/testdata/system_columns_3742/postgresql/pgx/v5/sqlc.json new file mode 100644 index 0000000000..32ede07158 --- /dev/null +++ b/internal/endtoend/testdata/system_columns_3742/postgresql/pgx/v5/sqlc.json @@ -0,0 +1,13 @@ +{ + "version": "1", + "packages": [ + { + "path": "go", + "engine": "postgresql", + "sql_package": "pgx/v5", + "name": "querytest", + "schema": "schema.sql", + "queries": "query.sql" + } + ] +}