From b3054cf451b14a405d13f2710dfe0e76c4c55c76 Mon Sep 17 00:00:00 2001 From: Matthew Crenshaw Date: Thu, 2 Jul 2026 12:46:18 -0400 Subject: [PATCH] feat(config): allow generating models from information_schema tables --- docs/reference/config.md | 5 ++ internal/codegen/golang/opts/options.go | 11 +++ internal/codegen/golang/result.go | 4 +- internal/codegen/golang/result_test.go | 65 +++++++++++++++++ internal/config/v_one.go | 2 + internal/config/v_one.json | 3 + internal/config/v_two.json | 3 + .../postgresql/stdlib/go/db.go | 31 +++++++++ .../postgresql/stdlib/go/models.go | 16 +++++ .../postgresql/stdlib/go/query.sql.go | 69 +++++++++++++++++++ .../postgresql/stdlib/query.sql | 5 ++ .../postgresql/stdlib/schema.sql | 1 + .../postgresql/stdlib/sqlc.json | 14 ++++ 13 files changed, 227 insertions(+), 2 deletions(-) create mode 100644 internal/endtoend/testdata/omit_catalog_schema/postgresql/stdlib/go/db.go create mode 100644 internal/endtoend/testdata/omit_catalog_schema/postgresql/stdlib/go/models.go create mode 100644 internal/endtoend/testdata/omit_catalog_schema/postgresql/stdlib/go/query.sql.go create mode 100644 internal/endtoend/testdata/omit_catalog_schema/postgresql/stdlib/query.sql create mode 100644 internal/endtoend/testdata/omit_catalog_schema/postgresql/stdlib/schema.sql create mode 100644 internal/endtoend/testdata/omit_catalog_schema/postgresql/stdlib/sqlc.json diff --git a/docs/reference/config.md b/docs/reference/config.md index c88d41d963..c04983c8dc 100644 --- a/docs/reference/config.md +++ b/docs/reference/config.md @@ -179,6 +179,8 @@ The `gen` mapping supports the following keys: - `camel` for camelCase, `pascal` for PascalCase, `snake` for snake_case or `none` to use the column name in the DB. Defaults to `none`. - `omit_unused_structs`: - If `true`, sqlc won't generate table and enum structs that aren't used in queries for a given package. Defaults to `false`. +- `omit_catalog_schema`: + - If `true`, sqlc won't generate structs for tables and enums in the `pg_catalog` and `information_schema` catalog schemas. Set to `false` to generate models from these schemas. Defaults to `true`. - `output_batch_file_name`: - Customize the name of the batch file. Defaults to `batch.go`. - `output_db_file_name`: @@ -396,6 +398,7 @@ packages: build_tags: "some_tag" json_tags_case_style: "camel" omit_unused_structs: false + omit_catalog_schema: true output_batch_file_name: "batch.go" output_db_file_name: "db.go" output_models_file_name: "models.go" @@ -458,6 +461,8 @@ Each mapping in the `packages` collection has the following keys: - `camel` for camelCase, `pascal` for PascalCase, `snake` for snake_case or `none` to use the column name in the DB. Defaults to `none`. - `omit_unused_structs`: - If `true`, sqlc won't generate table and enum structs that aren't used in queries for a given package. Defaults to `false`. +- `omit_catalog_schema`: + - If `true`, sqlc won't generate structs for tables and enums in the `pg_catalog` and `information_schema` catalog schemas. Set to `false` to generate models from these schemas. Defaults to `true`. - `output_batch_file_name`: - Customize the name of the batch file. Defaults to `batch.go`. - `output_db_file_name`: diff --git a/internal/codegen/golang/opts/options.go b/internal/codegen/golang/opts/options.go index 646bf1e066..50fcf2c28f 100644 --- a/internal/codegen/golang/opts/options.go +++ b/internal/codegen/golang/opts/options.go @@ -49,6 +49,7 @@ type Options struct { QueryParameterLimit *int32 `json:"query_parameter_limit,omitempty" yaml:"query_parameter_limit"` OmitSqlcVersion bool `json:"omit_sqlc_version,omitempty" yaml:"omit_sqlc_version"` OmitUnusedStructs bool `json:"omit_unused_structs,omitempty" yaml:"omit_unused_structs"` + OmitCatalogSchema *bool `json:"omit_catalog_schema,omitempty" yaml:"omit_catalog_schema"` BuildTags string `json:"build_tags,omitempty" yaml:"build_tags"` Initialisms *[]string `json:"initialisms,omitempty" yaml:"initialisms"` @@ -185,6 +186,16 @@ func (o *Options) ModelsEmitEnabled() bool { return *o.OutputModelsEmit } +// OmitCatalogSchemaEnabled reports whether the pg_catalog and +// information_schema catalog schemas should be excluded when generating +// models. Defaults to true when the option is unset. +func (o *Options) OmitCatalogSchemaEnabled() bool { + if o.OmitCatalogSchema == nil { + return true + } + return *o.OmitCatalogSchema +} + // ModelsImportAlias is the fixed Go import alias used for the models // package in query files. Using a constant alias keeps the type qualifier // consistent regardless of how the user names the actual package. diff --git a/internal/codegen/golang/result.go b/internal/codegen/golang/result.go index c5126602da..df5a5e3f05 100644 --- a/internal/codegen/golang/result.go +++ b/internal/codegen/golang/result.go @@ -16,7 +16,7 @@ import ( func buildEnums(req *plugin.GenerateRequest, options *opts.Options) []Enum { var enums []Enum for _, schema := range req.Catalog.Schemas { - if schema.Name == "pg_catalog" || schema.Name == "information_schema" { + if options.OmitCatalogSchemaEnabled() && (schema.Name == "pg_catalog" || schema.Name == "information_schema") { continue } for _, enum := range schema.Enums { @@ -63,7 +63,7 @@ func buildEnums(req *plugin.GenerateRequest, options *opts.Options) []Enum { func buildStructs(req *plugin.GenerateRequest, options *opts.Options) []Struct { var structs []Struct for _, schema := range req.Catalog.Schemas { - if schema.Name == "pg_catalog" || schema.Name == "information_schema" { + if options.OmitCatalogSchemaEnabled() && (schema.Name == "pg_catalog" || schema.Name == "information_schema") { continue } for _, table := range schema.Tables { diff --git a/internal/codegen/golang/result_test.go b/internal/codegen/golang/result_test.go index 0c58525ec3..520caaaa8b 100644 --- a/internal/codegen/golang/result_test.go +++ b/internal/codegen/golang/result_test.go @@ -3,10 +3,75 @@ package golang import ( "testing" + "github.com/sqlc-dev/sqlc/internal/codegen/golang/opts" "github.com/sqlc-dev/sqlc/internal/metadata" "github.com/sqlc-dev/sqlc/internal/plugin" ) +func boolPtr(b bool) *bool { return &b } + +func catalogSchemaRequest() *plugin.GenerateRequest { + col := func(name, typ string) *plugin.Column { + return &plugin.Column{Name: name, Type: &plugin.Identifier{Name: typ}} + } + table := func(schema, name string, cols ...*plugin.Column) *plugin.Schema { + return &plugin.Schema{ + Name: schema, + Tables: []*plugin.Table{ + {Rel: &plugin.Identifier{Schema: schema, Name: name}, Columns: cols}, + }, + } + } + return &plugin.GenerateRequest{ + Settings: &plugin.Settings{Engine: "postgresql"}, + Catalog: &plugin.Catalog{ + DefaultSchema: "public", + Schemas: []*plugin.Schema{ + table("pg_catalog", "pg_class", col("oid", "oid")), + table("information_schema", "tables", col("table_name", "text")), + table("public", "users", col("id", "int4")), + }, + }, + } +} + +func TestBuildStructs_OmitCatalogSchema(t *testing.T) { + req := catalogSchemaRequest() + + t.Run("unset (default) skips pg_catalog and information_schema", func(t *testing.T) { + structs := buildStructs(req, &opts.Options{}) + for _, s := range structs { + if s.Table != nil && (s.Table.Schema == "pg_catalog" || s.Table.Schema == "information_schema") { + t.Errorf("unexpected catalog struct: %s.%s", s.Table.Schema, s.Table.Name) + } + } + }) + + t.Run("omit=true skips pg_catalog and information_schema", func(t *testing.T) { + structs := buildStructs(req, &opts.Options{OmitCatalogSchema: boolPtr(true)}) + for _, s := range structs { + if s.Table != nil && (s.Table.Schema == "pg_catalog" || s.Table.Schema == "information_schema") { + t.Errorf("unexpected catalog struct: %s.%s", s.Table.Schema, s.Table.Name) + } + } + }) + + t.Run("omit=false includes pg_catalog and information_schema", func(t *testing.T) { + structs := buildStructs(req, &opts.Options{OmitCatalogSchema: boolPtr(false)}) + schemas := make(map[string]bool) + for _, s := range structs { + if s.Table != nil { + schemas[s.Table.Schema] = true + } + } + for _, want := range []string{"pg_catalog", "information_schema", "public"} { + if !schemas[want] { + t.Errorf("expected structs for schema %q, got none", want) + } + } + }) +} + func TestPutOutColumns_ForZeroColumns(t *testing.T) { tests := []struct { cmd string diff --git a/internal/config/v_one.go b/internal/config/v_one.go index fa1a4d8d28..5be042859f 100644 --- a/internal/config/v_one.go +++ b/internal/config/v_one.go @@ -62,6 +62,7 @@ type v1PackageSettings struct { QueryParameterLimit *int32 `json:"query_parameter_limit,omitempty" yaml:"query_parameter_limit"` OmitSqlcVersion bool `json:"omit_sqlc_version,omitempty" yaml:"omit_sqlc_version"` OmitUnusedStructs bool `json:"omit_unused_structs,omitempty" yaml:"omit_unused_structs"` + OmitCatalogSchema *bool `json:"omit_catalog_schema,omitempty" yaml:"omit_catalog_schema"` Rules []string `json:"rules" yaml:"rules"` BuildTags string `json:"build_tags,omitempty" yaml:"build_tags"` } @@ -177,6 +178,7 @@ func (c *V1GenerateSettings) Translate() Config { QueryParameterLimit: pkg.QueryParameterLimit, OmitSqlcVersion: pkg.OmitSqlcVersion, OmitUnusedStructs: pkg.OmitUnusedStructs, + OmitCatalogSchema: pkg.OmitCatalogSchema, BuildTags: pkg.BuildTags, }, }, diff --git a/internal/config/v_one.json b/internal/config/v_one.json index d4ea6b6304..0bcf674934 100644 --- a/internal/config/v_one.json +++ b/internal/config/v_one.json @@ -263,6 +263,9 @@ "omit_unused_structs": { "type": "boolean" }, + "omit_catalog_schema": { + "type": "boolean" + }, "rules": { "type": "array", "items": { diff --git a/internal/config/v_two.json b/internal/config/v_two.json index f4cfb875c0..bb095e3f18 100644 --- a/internal/config/v_two.json +++ b/internal/config/v_two.json @@ -280,6 +280,9 @@ }, "omit_unused_structs": { "type": "boolean" + }, + "omit_catalog_schema": { + "type": "boolean" } }, "json": { diff --git a/internal/endtoend/testdata/omit_catalog_schema/postgresql/stdlib/go/db.go b/internal/endtoend/testdata/omit_catalog_schema/postgresql/stdlib/go/db.go new file mode 100644 index 0000000000..80dd6ab1f6 --- /dev/null +++ b/internal/endtoend/testdata/omit_catalog_schema/postgresql/stdlib/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +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/omit_catalog_schema/postgresql/stdlib/go/models.go b/internal/endtoend/testdata/omit_catalog_schema/postgresql/stdlib/go/models.go new file mode 100644 index 0000000000..fed963fb98 --- /dev/null +++ b/internal/endtoend/testdata/omit_catalog_schema/postgresql/stdlib/go/models.go @@ -0,0 +1,16 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package querytest + +import ( + "database/sql" +) + +type PgCatalogPgTimezoneName struct { + Name sql.NullString + Abbrev sql.NullString + UtcOffset sql.NullInt64 + IsDst sql.NullBool +} diff --git a/internal/endtoend/testdata/omit_catalog_schema/postgresql/stdlib/go/query.sql.go b/internal/endtoend/testdata/omit_catalog_schema/postgresql/stdlib/go/query.sql.go new file mode 100644 index 0000000000..96ee412770 --- /dev/null +++ b/internal/endtoend/testdata/omit_catalog_schema/postgresql/stdlib/go/query.sql.go @@ -0,0 +1,69 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: query.sql + +package querytest + +import ( + "context" +) + +const getTables = `-- name: GetTables :many +SELECT table_name::text FROM information_schema.tables +` + +func (q *Queries) GetTables(ctx context.Context) ([]string, error) { + rows, err := q.db.QueryContext(ctx, getTables) + if err != nil { + return nil, err + } + defer rows.Close() + var items []string + for rows.Next() { + var table_name string + if err := rows.Scan(&table_name); err != nil { + return nil, err + } + items = append(items, table_name) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getTimezones = `-- name: GetTimezones :many +SELECT name, abbrev, utc_offset, is_dst FROM pg_catalog.pg_timezone_names +` + +func (q *Queries) GetTimezones(ctx context.Context) ([]PgCatalogPgTimezoneName, error) { + rows, err := q.db.QueryContext(ctx, getTimezones) + if err != nil { + return nil, err + } + defer rows.Close() + var items []PgCatalogPgTimezoneName + for rows.Next() { + var i PgCatalogPgTimezoneName + if err := rows.Scan( + &i.Name, + &i.Abbrev, + &i.UtcOffset, + &i.IsDst, + ); 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/omit_catalog_schema/postgresql/stdlib/query.sql b/internal/endtoend/testdata/omit_catalog_schema/postgresql/stdlib/query.sql new file mode 100644 index 0000000000..cde14d915f --- /dev/null +++ b/internal/endtoend/testdata/omit_catalog_schema/postgresql/stdlib/query.sql @@ -0,0 +1,5 @@ +-- name: GetTimezones :many +SELECT * FROM pg_catalog.pg_timezone_names; + +-- name: GetTables :many +SELECT table_name::text FROM information_schema.tables; diff --git a/internal/endtoend/testdata/omit_catalog_schema/postgresql/stdlib/schema.sql b/internal/endtoend/testdata/omit_catalog_schema/postgresql/stdlib/schema.sql new file mode 100644 index 0000000000..e0ac49d1ec --- /dev/null +++ b/internal/endtoend/testdata/omit_catalog_schema/postgresql/stdlib/schema.sql @@ -0,0 +1 @@ +SELECT 1; diff --git a/internal/endtoend/testdata/omit_catalog_schema/postgresql/stdlib/sqlc.json b/internal/endtoend/testdata/omit_catalog_schema/postgresql/stdlib/sqlc.json new file mode 100644 index 0000000000..5e8a6e3c9c --- /dev/null +++ b/internal/endtoend/testdata/omit_catalog_schema/postgresql/stdlib/sqlc.json @@ -0,0 +1,14 @@ +{ + "version": "1", + "packages": [ + { + "path": "go", + "name": "querytest", + "engine": "postgresql", + "schema": "schema.sql", + "queries": "query.sql", + "omit_catalog_schema": false, + "omit_unused_structs": true + } + ] +}