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
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,15 @@ sql:
- "CopyUsers"
exclude:
- "DeleteUser"
insert_columns:
exclude:
- "id"
- "created_at"
- "updated_at"
update_columns:
exclude:
- "created_at"
- "updated_at"
```

Use `options.tables` to control which tables get query files. Entries may be
Expand All @@ -95,6 +104,17 @@ are query names (e.g. `GetUser`, `ListPostsByTitle`).
> list form (`queries: ["CopyUsers"]`) is no longer supported — move those
> entries under `queries.include`.

Use `options.insert_columns.exclude` to remove database-owned columns from
generated `INSERT`, batch insert, and `COPY` queries. Entries may be plain
column names (`id`), table-qualified names (`users.id`), or schema-qualified
names (`auth.users.id`). This is useful for columns with database defaults such
as generated IDs and timestamps.

Use `options.update_columns.exclude` to remove columns from generated `UPDATE`
queries. Entries support the same plain, table-qualified, and schema-qualified
forms. Generated update masks are cast to `text[]`, so sqlc emits strongly typed
`[]string` update masks for PostgreSQL targets.

### Default queries (always generated)

Primary key CRUD operations, List queries (including FK-index-based list
Expand Down
48 changes: 46 additions & 2 deletions internal/sqlc/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,10 @@ type Codegen struct {

// CodegenOptions holds plugin-specific options for the gen-queries plugin.
type CodegenOptions struct {
Queries QueryOptions `yaml:"queries,omitempty"`
Tables TableOptions `yaml:"tables,omitempty"`
Queries QueryOptions `yaml:"queries,omitempty"`
Tables TableOptions `yaml:"tables,omitempty"`
InsertColumns ColumnOptions `yaml:"insert_columns,omitempty"`
UpdateColumns ColumnOptions `yaml:"update_columns,omitempty"`
}

// QueryOptions holds query-level filtering options for the gen-queries plugin.
Expand All @@ -92,6 +94,11 @@ type TableOptions struct {
Exclude []string `yaml:"exclude,omitempty"`
}

// ColumnOptions holds column-level filtering options.
type ColumnOptions struct {
Exclude []string `yaml:"exclude,omitempty"`
}

// GetOptions returns the CodegenOptions for the gen-queries plugin.
// If no matching codegen entry is found, returns an empty CodegenOptions.
func (s *SQL) GetOptions() CodegenOptions {
Expand Down Expand Up @@ -148,6 +155,30 @@ func (s *SQL) GetExcludeSet() map[string]bool {
return excludeSet
}

// GetInsertColumnExcludeSet returns the deny-list of columns to skip in
// generated INSERT/COPY statements. Entries may be column names, table-qualified
// column names, or schema-qualified table column names.
func (s *SQL) GetInsertColumnExcludeSet() map[string]bool {
opts := s.GetOptions()
excludeSet := make(map[string]bool, len(opts.InsertColumns.Exclude))
for _, name := range opts.InsertColumns.Exclude {
excludeSet[name] = true
}
return excludeSet
}

// GetUpdateColumnExcludeSet returns the deny-list of columns to skip in
// generated UPDATE statements. Entries may be column names, table-qualified
// column names, or schema-qualified table column names.
func (s *SQL) GetUpdateColumnExcludeSet() map[string]bool {
opts := s.GetOptions()
excludeSet := make(map[string]bool, len(opts.UpdateColumns.Exclude))
for _, name := range opts.UpdateColumns.Exclude {
excludeSet[name] = true
}
return excludeSet
}

// tableSelected reports whether a table should have query files generated.
// Exclude always takes precedence over include; an empty include set matches
// every table. Both sets are checked against the unqualified table name and
Expand All @@ -162,3 +193,16 @@ func tableSelected(includeSet, excludeSet map[string]bool, schema, table string)
}
return includeSet[table] || includeSet[qualified]
}

func columnSelected(excludeSet map[string]bool, schema, table, column string) bool {
if excludeSet[column] {
return false
}
if excludeSet[table+"."+column] {
return false
}
if excludeSet[schema+"."+table+"."+column] {
return false
}
return true
}
72 changes: 72 additions & 0 deletions internal/sqlc/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ var _ = Describe("Config", func() {
Expect(opts.Queries.Include).To(HaveLen(2))
Expect(opts.Queries.Include).To(ContainElements("CopyUsers", "GetUserByEmail"))
Expect(opts.Tables.Exclude).To(ContainElement("posts"))
Expect(opts.InsertColumns.Exclude).To(ContainElements("id", "users.created_at", "public.users.updated_at"))
Expect(opts.UpdateColumns.Exclude).To(ContainElements("created_at", "users.updated_at", "public.users.deleted_at"))
})

When("the file does not exist", func() {
Expand Down Expand Up @@ -74,6 +76,12 @@ var _ = Describe("Config", func() {
Options: sqlc.CodegenOptions{
Queries: sqlc.QueryOptions{Include: []string{"ListUsers", "CopyUsers"}},
Tables: sqlc.TableOptions{Exclude: []string{"audit_logs"}},
InsertColumns: sqlc.ColumnOptions{
Exclude: []string{"id", "created_at", "updated_at"},
},
UpdateColumns: sqlc.ColumnOptions{
Exclude: []string{"created_at", "updated_at"},
},
},
},
},
Expand All @@ -82,6 +90,8 @@ var _ = Describe("Config", func() {
Expect(opts.Queries.Include).To(HaveLen(2))
Expect(opts.Queries.Include).To(ContainElements("ListUsers", "CopyUsers"))
Expect(opts.Tables.Exclude).To(ContainElement("audit_logs"))
Expect(opts.InsertColumns.Exclude).To(ContainElements("id", "created_at", "updated_at"))
Expect(opts.UpdateColumns.Exclude).To(ContainElements("created_at", "updated_at"))
})
})

Expand Down Expand Up @@ -242,4 +252,66 @@ var _ = Describe("Config", func() {
Expect(includeSet["posts"]).To(BeFalse())
})
})

Describe("SQL.GetInsertColumnExcludeSet", func() {
It("returns an empty map when codegen is nil", func() {
sql := sqlc.SQL{}
excludeSet := sql.GetInsertColumnExcludeSet()
Expect(excludeSet).NotTo(BeNil())
Expect(excludeSet).To(BeEmpty())
})

It("returns a map with excluded insert column names", func() {
sql := sqlc.SQL{
Codegen: []sqlc.Codegen{
{
Plugin: "gen-queries",
Out: "out",
Options: sqlc.CodegenOptions{
InsertColumns: sqlc.ColumnOptions{
Exclude: []string{"id", "todos.created_at", "public.todos.updated_at"},
},
},
},
},
}
excludeSet := sql.GetInsertColumnExcludeSet()
Expect(excludeSet).To(HaveLen(3))
Expect(excludeSet["id"]).To(BeTrue())
Expect(excludeSet["todos.created_at"]).To(BeTrue())
Expect(excludeSet["public.todos.updated_at"]).To(BeTrue())
Expect(excludeSet["title"]).To(BeFalse())
})
})

Describe("SQL.GetUpdateColumnExcludeSet", func() {
It("returns an empty map when codegen is nil", func() {
sql := sqlc.SQL{}
excludeSet := sql.GetUpdateColumnExcludeSet()
Expect(excludeSet).NotTo(BeNil())
Expect(excludeSet).To(BeEmpty())
})

It("returns a map with excluded update column names", func() {
sql := sqlc.SQL{
Codegen: []sqlc.Codegen{
{
Plugin: "gen-queries",
Out: "out",
Options: sqlc.CodegenOptions{
UpdateColumns: sqlc.ColumnOptions{
Exclude: []string{"created_at", "todos.updated_at", "public.todos.user_id"},
},
},
},
},
}
excludeSet := sql.GetUpdateColumnExcludeSet()
Expect(excludeSet).To(HaveLen(3))
Expect(excludeSet["created_at"]).To(BeTrue())
Expect(excludeSet["todos.updated_at"]).To(BeTrue())
Expect(excludeSet["public.todos.user_id"]).To(BeTrue())
Expect(excludeSet["title"]).To(BeFalse())
})
})
})
10 changes: 10 additions & 0 deletions internal/sqlc/config_test_exclude.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,13 @@ sql:
include:
- "CopyUsers"
- "GetUserByEmail"
insert_columns:
exclude:
- "id"
- "users.created_at"
- "public.users.updated_at"
update_columns:
exclude:
- "created_at"
- "users.updated_at"
- "public.users.deleted_at"
45 changes: 35 additions & 10 deletions internal/sqlc/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,13 @@ type Generator struct {
func (x *Generator) Generate() error {
// Context holds data for template execution
type Context struct {
Engine string
Schema string
Table *Table
QueryInclude map[string]bool
QueryExclude map[string]bool
Engine string
Schema string
Table *Table
QueryInclude map[string]bool
QueryExclude map[string]bool
InsertColumnExclude map[string]bool
UpdateColumnExclude map[string]bool
}

opts := map[string]any{
Expand Down Expand Up @@ -153,6 +155,25 @@ func (x *Generator) Generate() error {
}
return isDefault || ctx.QueryInclude[queryName]
},
"insert_columns": func(ctx Context) []Column {
columns := make([]Column, 0, len(ctx.Table.Columns))
for _, column := range ctx.Table.Columns {
if columnSelected(ctx.InsertColumnExclude, ctx.Schema, ctx.Table.Name, column.Name) {
columns = append(columns, column)
}
}
return columns
},
"update_columns": func(ctx Context) []Column {
tableColumns := ctx.Table.GetNonPrimaryKeyColumns()
columns := make([]Column, 0, len(tableColumns))
for _, column := range tableColumns {
if columnSelected(ctx.UpdateColumnExclude, ctx.Schema, ctx.Table.Name, column.Name) {
columns = append(columns, column)
}
}
return columns
},
}

// Open the template file
Expand All @@ -168,6 +189,8 @@ func (x *Generator) Generate() error {

queryInclude := config.GetQueryIncludeSet()
queryExclude := config.GetQueryExcludeSet()
insertColumnExclude := config.GetInsertColumnExcludeSet()
updateColumnExclude := config.GetUpdateColumnExcludeSet()
include := config.GetIncludeSet()
exclude := config.GetExcludeSet()

Expand All @@ -188,11 +211,13 @@ func (x *Generator) Generate() error {
defer file.Close()

ctx := Context{
Engine: config.Engine,
Schema: schema.Name,
Table: &table,
QueryInclude: queryInclude,
QueryExclude: queryExclude,
Engine: config.Engine,
Schema: schema.Name,
Table: &table,
QueryInclude: queryInclude,
QueryExclude: queryExclude,
InsertColumnExclude: insertColumnExclude,
UpdateColumnExclude: updateColumnExclude,
}
// Execute template into buffer, then squeeze blank lines
var buffer bytes.Buffer
Expand Down
74 changes: 74 additions & 0 deletions internal/sqlc/generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,80 @@ var _ = Describe("Generator", func() {
}
})

It("excludes configured columns from insert queries", func() {
dir := generator.Config.SQL[0].Queries
generator.Config.SQL[0].Codegen = []sqlc.Codegen{
{
Plugin: "gen-queries",
Out: dir,
Options: sqlc.CodegenOptions{
InsertColumns: sqlc.ColumnOptions{
Exclude: []string{"id"},
},
},
},
}

Expect(generator.Generate()).NotTo(HaveOccurred())

content, err := os.ReadFile(filepath.Join(dir, "users.sql"))
Expect(err).NotTo(HaveOccurred())

Expect(string(content)).To(ContainSubstring(`-- name: InsertUser :one
INSERT INTO users (
email,
name
) VALUES (
sqlc.arg(email),
sqlc.narg(name)
)
RETURNING *;`))
Expect(string(content)).To(ContainSubstring(`-- name: ExecInsertUser :exec
INSERT INTO users (
email,
name
) VALUES (
sqlc.arg(email),
sqlc.narg(name)
);`))
Expect(string(content)).NotTo(ContainSubstring(`-- name: InsertUser :one
INSERT INTO users (
id,`))
})

It("excludes configured columns from update queries", func() {
dir := generator.Config.SQL[0].Queries
generator.Config.SQL[0].Codegen = []sqlc.Codegen{
{
Plugin: "gen-queries",
Out: dir,
Options: sqlc.CodegenOptions{
UpdateColumns: sqlc.ColumnOptions{
Exclude: []string{"name"},
},
},
},
}

Expect(generator.Generate()).NotTo(HaveOccurred())

content, err := os.ReadFile(filepath.Join(dir, "users.sql"))
Expect(err).NotTo(HaveOccurred())

Expect(string(content)).To(ContainSubstring(`-- name: UpdateUser :one
UPDATE users
SET
email = CASE
WHEN 'email' = any(sqlc.arg(update_mask)::text[])
THEN sqlc.arg(email)
ELSE email
END
WHERE
id = sqlc.arg(id)
RETURNING *;`))
Expect(string(content)).NotTo(ContainSubstring("name = CASE"))
})

When("the queries directory does not exist", func() {
It("returns an error", func() {
for index := range generator.Config.SQL {
Expand Down
Loading