Skip to content
Merged
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
25 changes: 21 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ A CLI tool that generates [sqlc](https://sqlc.dev)-compatible SQL queries from y

- Generate CRUD queries (`SELECT`, `INSERT`, `UPDATE`, `DELETE`) from a database schema catalog
- Primary key CRUD and FK-based List operations generated by default — no configuration needed
- Opt-in to additional queries (Copy, FK joins, non-FK index queries) via `queries` allowlist
- Fine-grained control over which queries and tables are generated via `include`/`exclude` lists
- Configurable via YAML — shares the same `sqlc.yaml` configuration file
- Works as a standalone CLI or as part of a CI/CD pipeline
- Supports custom query templates
Expand Down Expand Up @@ -67,8 +67,10 @@ sql:
- "audit_logs"
- "auth.sessions"
queries:
- "CopyUsers"
- "GetUserWithPost"
include:
- "CopyUsers"
exclude:
- "DeleteUser"
```

Use `options.tables` to control which tables get query files. Entries may be
Expand All @@ -79,6 +81,20 @@ table names (`audit_logs`) or schema-qualified table names (`auth.sessions`).
- `exclude` is a deny-list that always takes precedence over `include`, so a
table present in both lists is skipped.

Use `options.queries` to control which queries are generated per table. Entries
are query names (e.g. `GetUser`, `ListPostsByTitle`).

- The default query set (below) is always generated unless a query is excluded.
- `include` adds opt-in queries (see [Opt-in queries](#opt-in-queries)) on top
of the default set.
- `exclude` removes queries from what would otherwise be generated — including
defaults (e.g. dropping `DeleteUser`) — and always takes precedence over
`include`.

> **Note:** `options.queries` is an object (`include`/`exclude`). The older flat
> list form (`queries: ["CopyUsers"]`) is no longer supported — move those
> entries under `queries.include`.

### Default queries (always generated)

Primary key CRUD operations, List queries (including FK-index-based list
Expand Down Expand Up @@ -106,7 +122,8 @@ table:

### Opt-in queries

These queries are only generated when explicitly listed in `options.queries`:
These queries are not part of the default set — they are only generated when
explicitly listed in `options.queries.include`:

| Query | Description |
| ------------------------------- | ---------------------------------------- |
Expand Down
36 changes: 28 additions & 8 deletions internal/sqlc/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,19 @@ type Codegen struct {

// CodegenOptions holds plugin-specific options for the gen-queries plugin.
type CodegenOptions struct {
Queries []string `yaml:"queries,omitempty"`
Queries QueryOptions `yaml:"queries,omitempty"`
Tables TableOptions `yaml:"tables,omitempty"`
}

// QueryOptions holds query-level filtering options for the gen-queries plugin.
// Include adds opt-in queries on top of the default query set. Exclude removes
// queries from what would otherwise be generated and always takes precedence
// over Include and the defaults.
type QueryOptions struct {
Include []string `yaml:"include,omitempty"`
Exclude []string `yaml:"exclude,omitempty"`
}

// TableOptions holds table-level filtering options for the gen-queries plugin.
// Include is an allow-list: when non-empty, only the listed tables are
// generated. Exclude is a deny-list that always takes precedence over Include.
Expand All @@ -94,15 +103,26 @@ func (s *SQL) GetOptions() CodegenOptions {
return CodegenOptions{}
}

// GetQueriesSet returns a set of opt-in query names for efficient lookup.
// The returned map allows O(1) lookup to check if a query is enabled.
func (s *SQL) GetQueriesSet() map[string]bool {
// GetQueryIncludeSet returns the set of opt-in query names to generate in
// addition to the default query set.
func (s *SQL) GetQueryIncludeSet() map[string]bool {
opts := s.GetOptions()
includeSet := make(map[string]bool, len(opts.Queries.Include))
for _, name := range opts.Queries.Include {
includeSet[name] = true
}
return includeSet
}

// GetQueryExcludeSet returns the deny-list of query names to skip. Excluded
// queries are never generated, even when they belong to the default set.
func (s *SQL) GetQueryExcludeSet() map[string]bool {
opts := s.GetOptions()
querySet := make(map[string]bool, len(opts.Queries))
for _, name := range opts.Queries {
querySet[name] = true
excludeSet := make(map[string]bool, len(opts.Queries.Exclude))
for _, name := range opts.Queries.Exclude {
excludeSet[name] = true
}
return querySet
return excludeSet
}

// GetIncludeSet returns the allow-list of table names for query generation.
Expand Down
51 changes: 30 additions & 21 deletions internal/sqlc/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ var _ = Describe("Config", func() {
Expect(config.SQL[0].Codegen[0].Plugin).To(Equal("gen-queries"))
Expect(config.SQL[0].Codegen[0].Out).To(Equal("ent/query"))
opts := config.SQL[0].GetOptions()
Expect(opts.Queries).To(HaveLen(2))
Expect(opts.Queries).To(ContainElements("CopyUsers", "GetUserByEmail"))
Expect(opts.Queries.Include).To(HaveLen(2))
Expect(opts.Queries.Include).To(ContainElements("CopyUsers", "GetUserByEmail"))
Expect(opts.Tables.Exclude).To(ContainElement("posts"))
})

Expand All @@ -50,7 +50,7 @@ var _ = Describe("Config", func() {
It("returns empty options when codegen is nil", func() {
sql := sqlc.SQL{}
opts := sql.GetOptions()
Expect(opts.Queries).To(BeNil())
Expect(opts.Queries.Include).To(BeNil())
Expect(opts.Tables.Exclude).To(BeNil())
})

Expand All @@ -61,7 +61,7 @@ var _ = Describe("Config", func() {
},
}
opts := sql.GetOptions()
Expect(opts.Queries).To(BeNil())
Expect(opts.Queries.Include).To(BeNil())
Expect(opts.Tables.Exclude).To(BeNil())
})

Expand All @@ -72,38 +72,38 @@ var _ = Describe("Config", func() {
Plugin: "gen-queries",
Out: "ent/query",
Options: sqlc.CodegenOptions{
Queries: []string{"ListUsers", "CopyUsers"},
Queries: sqlc.QueryOptions{Include: []string{"ListUsers", "CopyUsers"}},
Tables: sqlc.TableOptions{Exclude: []string{"audit_logs"}},
},
},
},
}
opts := sql.GetOptions()
Expect(opts.Queries).To(HaveLen(2))
Expect(opts.Queries).To(ContainElements("ListUsers", "CopyUsers"))
Expect(opts.Queries.Include).To(HaveLen(2))
Expect(opts.Queries.Include).To(ContainElements("ListUsers", "CopyUsers"))
Expect(opts.Tables.Exclude).To(ContainElement("audit_logs"))
})
})

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

It("returns an empty map when queries is empty", func() {
It("returns an empty map when include is empty", func() {
sql := sqlc.SQL{
Codegen: []sqlc.Codegen{
{
Plugin: "gen-queries",
Out: "ent/query",
Options: sqlc.CodegenOptions{Queries: []string{}},
Options: sqlc.CodegenOptions{Queries: sqlc.QueryOptions{Include: []string{}}},
},
},
}
querySet := sql.GetQueriesSet()
querySet := sql.GetQueryIncludeSet()
Expect(querySet).NotTo(BeNil())
Expect(querySet).To(BeEmpty())
})
Expand All @@ -115,36 +115,45 @@ var _ = Describe("Config", func() {
Plugin: "gen-queries",
Out: "ent/query",
Options: sqlc.CodegenOptions{
Queries: []string{"ListUsers", "CopyUsers", "GetUserByEmail"},
Queries: sqlc.QueryOptions{Include: []string{"ListUsers", "CopyUsers", "GetUserByEmail"}},
},
},
},
}
querySet := sql.GetQueriesSet()
querySet := sql.GetQueryIncludeSet()
Expect(querySet).To(HaveLen(3))
Expect(querySet["ListUsers"]).To(BeTrue())
Expect(querySet["CopyUsers"]).To(BeTrue())
Expect(querySet["GetUserByEmail"]).To(BeTrue())
Expect(querySet["OtherQuery"]).To(BeFalse())
})
})

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

It("provides O(1) lookup", func() {
It("returns a map with excluded query names", func() {
sql := sqlc.SQL{
Codegen: []sqlc.Codegen{
{
Plugin: "gen-queries",
Out: "ent/query",
Options: sqlc.CodegenOptions{
Queries: []string{"Query1", "Query2", "Query3"},
Queries: sqlc.QueryOptions{Exclude: []string{"DeleteUser", "BatchDeleteUsers"}},
},
},
},
}
querySet := sql.GetQueriesSet()
_, exists := querySet["Query2"]
Expect(exists).To(BeTrue())
_, exists = querySet["NonExistent"]
Expect(exists).To(BeFalse())
querySet := sql.GetQueryExcludeSet()
Expect(querySet).To(HaveLen(2))
Expect(querySet["DeleteUser"]).To(BeTrue())
Expect(querySet["BatchDeleteUsers"]).To(BeTrue())
Expect(querySet["GetUser"]).To(BeFalse())
})
})

Expand Down
5 changes: 3 additions & 2 deletions internal/sqlc/config_test_exclude.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@ sql:
exclude:
- "posts"
queries:
- "CopyUsers"
- "GetUserByEmail"
include:
- "CopyUsers"
- "GetUserByEmail"
31 changes: 19 additions & 12 deletions internal/sqlc/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,11 @@ type Generator struct {
func (x *Generator) Generate() error {
// Context holds data for template execution
type Context struct {
Engine string
Schema string
Table *Table
Queries map[string]bool
Engine string
Schema string
Table *Table
QueryInclude map[string]bool
QueryExclude map[string]bool
}

opts := map[string]any{
Expand Down Expand Up @@ -144,9 +145,13 @@ func (x *Generator) Generate() error {
"is_fk_index": func(table Table, index *Index) bool {
return table.IsForeignKeyIndex(index)
},
// Query include function for opt-in queries
"should_include": func(ctx Context, queryName string) bool {
return ctx.Queries[queryName]
// Query selection: a query renders when it belongs to the default set
// or is explicitly included, and never when excluded (exclude wins).
"should_generate": func(ctx Context, queryName string, isDefault bool) bool {
if ctx.QueryExclude[queryName] {
return false
}
return isDefault || ctx.QueryInclude[queryName]
},
}

Expand All @@ -161,7 +166,8 @@ func (x *Generator) Generate() error {
return err
}

queries := config.GetQueriesSet()
queryInclude := config.GetQueryIncludeSet()
queryExclude := config.GetQueryExcludeSet()
include := config.GetIncludeSet()
exclude := config.GetExcludeSet()

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

ctx := Context{
Engine: config.Engine,
Schema: schema.Name,
Table: &table,
Queries: queries,
Engine: config.Engine,
Schema: schema.Name,
Table: &table,
QueryInclude: queryInclude,
QueryExclude: queryExclude,
}
// Execute template into buffer, then squeeze blank lines
var buffer bytes.Buffer
Expand Down
Loading