From ee38aa3c8264fbbce5270a47aa45b97eed96de11 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 28 May 2025 05:38:54 +0000 Subject: [PATCH] feat: Introduce sqlc.optional for dynamic query generation This commit introduces the `sqlc.optional` feature, allowing conditional inclusion of SQL query fragments at runtime. Key changes: 1. **Parser Enhancement**: The SQL parser now recognizes `sqlc.optional('ConditionKey', 'SQLFragment')` syntax within query files. This information is stored in the query's metadata. 2. **Code Generation**: - Go code generation logic has been updated to process these `OptionalBlocks`. - Generated Go functions now include new parameters (typed as `interface{}`) corresponding to each `ConditionKey`. - Templates (`stdlib/queryCode.tmpl`, `pgx/queryCode.tmpl`) were modified to dynamically build the SQL query string and its arguments at runtime. If an optional Go parameter is non-nil, its associated SQL fragment is included in the final query, and its value is added to the list of database arguments. 3. **Parameter Handling**: `$N` placeholders in all SQL fragments (base or optional) consistently refer to the Nth parameter in the generated Go function's signature. 4. **Documentation**: Added comprehensive documentation for `sqlc.optional` in `docs/reference/query-annotations.md`, covering syntax, behavior, parameter numbering, and examples. 5. **Examples**: A new runnable example has been added to `examples/dynamic_query/postgresql/` to demonstrate practical usage. 6. **Tests**: New end-to-end tests were added in `internal/endtoend/testdata/dynamic_query/` for both `stdlib` and `pgx` drivers, ensuring the correctness of the generated code. --- docs/reference/query-annotations.md | 174 ++++++++++++++ examples/dynamic_query/postgresql/db.go | 29 +++ examples/dynamic_query/postgresql/models.go | 15 ++ examples/dynamic_query/postgresql/querier.go | 12 + examples/dynamic_query/postgresql/query.sql | 11 + .../dynamic_query/postgresql/query.sql.go | 106 +++++++++ .../dynamic_query/postgresql/query_test.go | 220 ++++++++++++++++++ examples/dynamic_query/postgresql/schema.sql | 8 + examples/dynamic_query/sqlc.yaml | 12 + internal/codegen/golang/gen.go | 50 +++- internal/codegen/golang/query.go | 5 +- .../golang/templates/pgx/queryCode.tmpl | 83 ++++--- .../golang/templates/stdlib/queryCode.tmpl | 158 +++++++++---- internal/compiler/parse.go | 37 +++ .../postgresql_pgx/gend/db.go.expected | 32 +++ .../postgresql_pgx/gend/models.go.expected | 10 + .../postgresql_pgx/gend/querier.go.expected | 12 + .../postgresql_pgx/gend/query.sql.go.expected | 130 +++++++++++ .../dynamic_query/postgresql_pgx/query.sql | 12 + .../dynamic_query/postgresql_pgx/schema.sql | 6 + .../dynamic_query/postgresql_pgx/sqlc.yaml | 22 ++ .../postgresql_stdlib/gend/db.go.expected | 27 +++ .../postgresql_stdlib/gend/models.go.expected | 10 + .../gend/querier.go.expected | 13 ++ .../gend/query.sql.go.expected | 135 +++++++++++ .../dynamic_query/postgresql_stdlib/query.sql | 12 + .../postgresql_stdlib/schema.sql | 6 + .../dynamic_query/postgresql_stdlib/sqlc.yaml | 22 ++ internal/metadata/meta.go | 8 + 29 files changed, 1301 insertions(+), 76 deletions(-) create mode 100644 examples/dynamic_query/postgresql/db.go create mode 100644 examples/dynamic_query/postgresql/models.go create mode 100644 examples/dynamic_query/postgresql/querier.go create mode 100644 examples/dynamic_query/postgresql/query.sql create mode 100644 examples/dynamic_query/postgresql/query.sql.go create mode 100644 examples/dynamic_query/postgresql/query_test.go create mode 100644 examples/dynamic_query/postgresql/schema.sql create mode 100644 examples/dynamic_query/sqlc.yaml create mode 100644 internal/endtoend/testdata/dynamic_query/postgresql_pgx/gend/db.go.expected create mode 100644 internal/endtoend/testdata/dynamic_query/postgresql_pgx/gend/models.go.expected create mode 100644 internal/endtoend/testdata/dynamic_query/postgresql_pgx/gend/querier.go.expected create mode 100644 internal/endtoend/testdata/dynamic_query/postgresql_pgx/gend/query.sql.go.expected create mode 100644 internal/endtoend/testdata/dynamic_query/postgresql_pgx/query.sql create mode 100644 internal/endtoend/testdata/dynamic_query/postgresql_pgx/schema.sql create mode 100644 internal/endtoend/testdata/dynamic_query/postgresql_pgx/sqlc.yaml create mode 100644 internal/endtoend/testdata/dynamic_query/postgresql_stdlib/gend/db.go.expected create mode 100644 internal/endtoend/testdata/dynamic_query/postgresql_stdlib/gend/models.go.expected create mode 100644 internal/endtoend/testdata/dynamic_query/postgresql_stdlib/gend/querier.go.expected create mode 100644 internal/endtoend/testdata/dynamic_query/postgresql_stdlib/gend/query.sql.go.expected create mode 100644 internal/endtoend/testdata/dynamic_query/postgresql_stdlib/query.sql create mode 100644 internal/endtoend/testdata/dynamic_query/postgresql_stdlib/schema.sql create mode 100644 internal/endtoend/testdata/dynamic_query/postgresql_stdlib/sqlc.yaml diff --git a/docs/reference/query-annotations.md b/docs/reference/query-annotations.md index 4fabe05aae..247cc27d68 100644 --- a/docs/reference/query-annotations.md +++ b/docs/reference/query-annotations.md @@ -113,6 +113,180 @@ func (q *Queries) GetAuthor(ctx context.Context, id int64) (Author, error) { } ``` +## Conditional SQL with `sqlc.optional` + +The `sqlc.optional` annotation allows for parts of a SQL query to be conditionally included at runtime. This is useful for building queries with optional filters or other dynamic components. + +### Purpose + +`sqlc.optional` provides a way to construct dynamic SQL queries where certain SQL fragments are only appended to the base query if a corresponding Go parameter is non-`nil`. This avoids the need for complex string manipulation or multiple similar queries for different filtering scenarios. + +### Syntax + +You include `sqlc.optional` calls directly in your SQL query comments, after the main query body. Each call specifies a key (which becomes part of the Go function parameter name) and the SQL fragment to include. + +```sql +-- name: GetItemsByOwner :many +SELECT * FROM items +WHERE owner_id = $1 -- Base condition for mandatory parameter +sqlc.optional('NameFilter', 'AND name LIKE $2') +sqlc.optional('ActiveOnly', 'AND is_active = $3'); +``` + +### Generated Function Signature + +For each `sqlc.optional('Key', 'SQLFragment')` annotation, a new parameter is added to the generated Go function. The parameter name is derived from `Key` (converted to lowerCamelCase, e.g., `nameFilter`, `activeOnly`), and its type is `interface{}`. + +Given the SQL example above, the generated Go function signature would be: + +```go +func (q *Queries) GetItemsByOwner(ctx context.Context, ownerID int64, nameFilter interface{}, activeOnly interface{}) ([]Item, error) +``` + +Here, `ownerID int64` is the standard parameter corresponding to `$1`. `nameFilter interface{}` and `activeOnly interface{}` are the optional parameters generated due to `sqlc.optional`. + +### Runtime Behavior + +- The SQL fragment associated with an `sqlc.optional` directive is appended to the main query (with a preceding space) if the corresponding Go parameter in the generated function is **not `nil`**. +- If the parameter is `nil`, the fragment is ignored. +- The database driver receives the fully constructed SQL string and only the parameters that are active (standard parameters + non-`nil` optional parameters). + +### Parameter Numbering + +The `$N` placeholders in *any* SQL fragment (whether part of the base query or an `sqlc.optional` fragment) **must** correspond to the position of the argument in the generated Go function's parameter list. + +- Standard (non-optional) parameters are numbered first, based on their order in the SQL query. +- Optional parameters are numbered subsequently, based on the order of their `sqlc.optional` appearance in the SQL query. + +**Example:** + +For the query: +```sql +-- name: GetItemsByOwner :many +SELECT * FROM items +WHERE owner_id = $1 -- owner_id is the 1st parameter +sqlc.optional('NameFilter', 'AND name LIKE $2') -- nameFilter is the 2nd parameter +sqlc.optional('ActiveOnly', 'AND is_active = $3'); -- activeOnly is the 3rd parameter +``` + +The generated Go function is: +`func (q *Queries) GetItemsByOwner(ctx context.Context, ownerID int64, nameFilter interface{}, activeOnly interface{})` + +- In the base query, `$1` refers to `ownerID`. +- In the `NameFilter` fragment, `$2` refers to `nameFilter`. +- In the `ActiveOnly` fragment, `$3` refers to `activeOnly`. + +If `nameFilter` is `nil` and `activeOnly` is provided, the final SQL sent to the driver might look like: +`SELECT * FROM items WHERE owner_id = $1 AND is_active = $2` +And the parameters passed to the driver would be `ownerID` and the value of `activeOnly`. The database driver sees a query with parameters re-numbered sequentially from `$1`. sqlc handles this re-numbering automatically when constructing the query for the driver. + +### Complete Example + +**SQL (`query.sql`):** +```sql +-- name: ListUsers :many +SELECT id, name, status FROM users +WHERE 1=1 -- Base condition (can be any valid SQL expression) +sqlc.optional('NameParam', 'AND name LIKE $1') +sqlc.optional('StatusParam', 'AND status = $2'); +``` +*(For this specific example, if `NameParam` is active, it's `$1`. If `StatusParam` is active, it's `$2`. If both are active, `NameParam` is `$1` and `StatusParam` is `$2` in their respective fragments, but they become `$1` and `$2` overall if no mandatory params precede them. The parameter numbering in fragments refers to their final position in the argument list passed to the database driver, which sqlc constructs based on active parameters.)* + +**Correction to the above parenthetical note, aligning with the "Parameter Numbering" section:** +The `$N` in the SQL fragments refers to the Go function signature's parameter order. +- `NameParam` (if not nil) corresponds to `$1`. +- `StatusParam` (if not nil) corresponds to `$2`. + +If `NameParam` is `John%` and `StatusParam` is `active`, the effective SQL is: +`SELECT id, name, status FROM users WHERE 1=1 AND name LIKE $1 AND status = $2` +And the parameters passed to the driver are `John%` and `active`. + +If `NameParam` is `nil` and `StatusParam` is `active`, the effective SQL is: +`SELECT id, name, status FROM users WHERE 1=1 AND status = $1` +And the parameter passed to the driver is `active`. sqlc handles mapping the Go parameters to the correct positional placeholders for the final SQL. + + +**Generated Go (`query.sql.go`):** +```go +func (q *Queries) ListUsers(ctx context.Context, nameParam interface{}, statusParam interface{}) ([]User, error) { + // ... implementation using strings.Builder ... +} +``` + +**Example Usage (Go):** +```go +package main + +import ( + "context" + "database/sql" + "fmt" + "log" + + // assume models and queries are in package "db" + "yourmodule/db" // Adjust to your actual module path +) + +func main() { + ctx := context.Background() + // Assume dbConn is an initialized *sql.DB + var dbConn *sql.DB + // dbConn, err := sql.Open("driver-name", "connection-string") + // if err != nil { + // log.Fatal(err) + // } + // defer dbConn.Close() + + queries := db.New(dbConn) + + // Example 1: Get all users (both optional parameters are nil) + fmt.Println("Fetching all users...") + allUsers, err := queries.ListUsers(ctx, nil, nil) + if err != nil { + log.Fatalf("Failed to list all users: %v", err) + } + for _, user := range allUsers { + fmt.Printf("User: ID=%d, Name=%s, Status=%s\n", user.ID, user.Name, user.Status) + } + + fmt.Println("\nFetching users with name starting with 'J':") + // Example 2: Get users with name starting with "J" + nameFilter := "J%" + jUsers, err := queries.ListUsers(ctx, nameFilter, nil) + if err != nil { + log.Fatalf("Failed to list J-users: %v", err) + } + for _, user := range jUsers { + fmt.Printf("User: ID=%d, Name=%s, Status=%s\n", user.ID, user.Name, user.Status) + } + + fmt.Println("\nFetching 'active' users:") + // Example 3: Get 'active' users + statusFilter := "active" + activeUsers, err := queries.ListUsers(ctx, nil, statusFilter) + if err != nil { + log.Fatalf("Failed to list active users: %v", err) + } + for _, user := range activeUsers { + fmt.Printf("User: ID=%d, Name=%s, Status=%s\n", user.ID, user.Name, user.Status) + } + + fmt.Println("\nFetching 'inactive' users with name 'Jane Doe':") + // Example 4: Get 'inactive' users with name 'Jane Doe' + nameFilterSpecific := "Jane Doe" + statusFilterSpecific := "inactive" + janeUsers, err := queries.ListUsers(ctx, nameFilterSpecific, statusFilterSpecific) + if err != nil { + log.Fatalf("Failed to list specific Jane users: %v", err) + } + for _, user := range janeUsers { + fmt.Printf("User: ID=%d, Name=%s, Status=%s\n", user.ID, user.Name, user.Status) + } +} +``` + +This feature provides a powerful way to reduce boilerplate and manage complex queries with multiple optional conditions directly within your SQL files. + ## `:batchexec` __NOTE: This command only works with PostgreSQL using the `pgx/v4` and `pgx/v5` drivers and outputting Go code.__ diff --git a/examples/dynamic_query/postgresql/db.go b/examples/dynamic_query/postgresql/db.go new file mode 100644 index 0000000000..e32c8cef6b --- /dev/null +++ b/examples/dynamic_query/postgresql/db.go @@ -0,0 +1,29 @@ +package postgresql + +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 + Begin(context.Context) (pgx.Tx, error) +} + +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/examples/dynamic_query/postgresql/models.go b/examples/dynamic_query/postgresql/models.go new file mode 100644 index 0000000000..88efb94ef8 --- /dev/null +++ b/examples/dynamic_query/postgresql/models.go @@ -0,0 +1,15 @@ +package postgresql + +import ( + "database/sql" + "time" +) + +type Product struct { + ID int32 `json:"id"` + Name string `json:"name"` + Category string `json:"category"` + Price int32 `json:"price"` + IsAvailable sql.NullBool `json:"is_available"` + CreatedAt sql.NullTime `json:"created_at"` +} diff --git a/examples/dynamic_query/postgresql/querier.go b/examples/dynamic_query/postgresql/querier.go new file mode 100644 index 0000000000..a7659bc5ed --- /dev/null +++ b/examples/dynamic_query/postgresql/querier.go @@ -0,0 +1,12 @@ +package postgresql + +import ( + "context" +) + +type Querier interface { + GetProducts(ctx context.Context, category interface{}, minPrice interface{}, isAvailable interface{}) ([]Product, error) + AddProduct(ctx context.Context, arg AddProductParams) (Product, error) +} + +var _ Querier = (*Queries)(nil) diff --git a/examples/dynamic_query/postgresql/query.sql b/examples/dynamic_query/postgresql/query.sql new file mode 100644 index 0000000000..ab8c015fab --- /dev/null +++ b/examples/dynamic_query/postgresql/query.sql @@ -0,0 +1,11 @@ +-- name: GetProducts :many +SELECT * FROM products +WHERE 1=1 +sqlc.optional('Category', 'AND category = $1') +sqlc.optional('MinPrice', 'AND price >= $2') +sqlc.optional('IsAvailable', 'AND is_available = $3'); + +-- name: AddProduct :one +INSERT INTO products (name, category, price, is_available) +VALUES ($1, $2, $3, $4) +RETURNING *; diff --git a/examples/dynamic_query/postgresql/query.sql.go b/examples/dynamic_query/postgresql/query.sql.go new file mode 100644 index 0000000000..575fb32b04 --- /dev/null +++ b/examples/dynamic_query/postgresql/query.sql.go @@ -0,0 +1,106 @@ +package postgresql + +import ( + "context" + "fmt" // Ensure fmt is imported + "strings" +) + +const getProducts = `-- name: GetProducts :many +SELECT id, name, category, price, is_available, created_at FROM products +WHERE 1=1 +` + +// GetProductsParams is a placeholder as the function takes optional params directly. +// It's not used by the generated GetProducts function itself but might be useful +// for users if they wanted to wrap the call. +type GetProductsParams struct { + Category interface{} `json:"category"` + MinPrice interface{} `json:"min_price"` + IsAvailable interface{} `json:"is_available"` +} + +func (q *Queries) GetProducts(ctx context.Context, category interface{}, minPrice interface{}, isAvailable interface{}) ([]Product, error) { + var sqlBuilder strings.Builder + sqlBuilder.WriteString(getProducts) // Base query + + var queryParams []interface{} + + // Optional 'Category' + if category != nil { + sqlBuilder.WriteString(" AND category = $") + queryParams = append(queryParams, category) + sqlBuilder.WriteString(fmt.Sprintf("%d", len(queryParams))) + } + + // Optional 'MinPrice' + if minPrice != nil { + sqlBuilder.WriteString(" AND price >= $") + queryParams = append(queryParams, minPrice) + sqlBuilder.WriteString(fmt.Sprintf("%d", len(queryParams))) + } + + // Optional 'IsAvailable' + if isAvailable != nil { + sqlBuilder.WriteString(" AND is_available = $") + queryParams = append(queryParams, isAvailable) + sqlBuilder.WriteString(fmt.Sprintf("%d", len(queryParams))) + } + + rows, err := q.db.Query(ctx, sqlBuilder.String(), queryParams...) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Product + for rows.Next() { + var i Product + if err := rows.Scan( + &i.ID, + &i.Name, + &i.Category, + &i.Price, + &i.IsAvailable, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const addProduct = `-- name: AddProduct :one +INSERT INTO products (name, category, price, is_available) +VALUES ($1, $2, $3, $4) +RETURNING id, name, category, price, is_available, created_at +` + +type AddProductParams struct { + Name string `json:"name"` + Category string `json:"category"` + Price int32 `json:"price"` + IsAvailable bool `json:"is_available"` +} + +func (q *Queries) AddProduct(ctx context.Context, arg AddProductParams) (Product, error) { + row := q.db.QueryRow(ctx, addProduct, + arg.Name, + arg.Category, + arg.Price, + arg.IsAvailable, + ) + var i Product + err := row.Scan( + &i.ID, + &i.Name, + &i.Category, + &i.Price, + &i.IsAvailable, + &i.CreatedAt, + ) + return i, err +} diff --git a/examples/dynamic_query/postgresql/query_test.go b/examples/dynamic_query/postgresql/query_test.go new file mode 100644 index 0000000000..349f7c934b --- /dev/null +++ b/examples/dynamic_query/postgresql/query_test.go @@ -0,0 +1,220 @@ +package postgresql + +import ( + "context" + "database/sql" + "fmt" + "log" + "os" + "testing" + + "github.com/jackc/pgx/v5/pgxpool" + _ "github.com/jackc/pgx/v5/stdlib" // Import the pgx stdlib driver +) + +var testQueries *Queries + +// TestMain sets up the database connection and runs the tests. +// For a real test suite, you'd use a dedicated test database and potentially migrations. +func TestMain(m *testing.M) { + ctx := context.Background() + dbURL := os.Getenv("DATABASE_URL") + if dbURL == "" { + // Provide a default for local testing if DATABASE_URL is not set. + // Adjust if your local PostgreSQL instance is different. + dbURL = "postgres://user:password@localhost:5432/testdb?sslmode=disable" + log.Printf("DATABASE_URL not set, using default: %s\n", dbURL) + } + + // For pgx/v5, it's common to use pgxpool directly. + // However, sqlc can also generate code for database/sql, which might be simpler for some examples. + // The sqlc.yaml specified pgx/v5, so we'll use pgxpool here. + pool, err := pgxpool.New(ctx, dbURL) + if err != nil { + log.Fatalf("Unable to connect to database: %v\n", err) + } + defer pool.Close() + + // Or, if using database/sql with pgx (e.g., if sql_package was "database/sql" and driver was "pgx") + // stdDB, err := sql.Open("pgx", dbURL) + // if err != nil { + // log.Fatalf("Unable to connect to database using database/sql: %v\n", err) + // } + // defer stdDB.Close() + // testQueries = New(stdDB) // If using database/sql adapter + + testQueries = New(pool) // New takes DBTX, which pgxpool.Pool implements + + // Minimal schema setup - in a real test, use migrations. + // This is a simplified version and might fail if the table already exists. + // Consider dropping and recreating for idempotency in real tests. + _, err = pool.Exec(ctx, ` + DROP TABLE IF EXISTS products; + CREATE TABLE products ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL, + category TEXT NOT NULL, + price INT NOT NULL, + is_available BOOLEAN DEFAULT TRUE, + created_at TIMESTAMPTZ DEFAULT NOW() + ); + `) + if err != nil { + log.Fatalf("Failed to create schema: %v\n", err) + } + + // Insert some initial data + initialProducts := []struct { + Name string + Category string + Price int32 + IsAvailable bool + }{ + {"Laptop Pro", "electronics", 1200, true}, + {"Coffee Maker", "appliances", 80, true}, + {"Gaming Mouse", "electronics", 75, true}, + {"Desk Chair", "furniture", 150, false}, + {"Laptop Lite", "electronics", 800, true}, + } + + for _, p := range initialProducts { + _, err := testQueries.AddProduct(ctx, AddProductParams{ + Name: p.Name, + Category: p.Category, + Price: p.Price, + IsAvailable: p.IsAvailable, + }) + if err != nil { + log.Fatalf("Failed to insert initial product %s: %v", p.Name, err) + } + } + + log.Println("Test database setup complete.") + exitCode := m.Run() + os.Exit(exitCode) +} + +func TestGetProducts(t *testing.T) { + ctx := context.Background() + + if testQueries == nil { + t.Fatal("testQueries not initialized. DB setup might have failed.") + } + + // Example 1: Get all products (all optional params nil) + t.Run("GetAllProducts", func(t *testing.T) { + products, err := testQueries.GetProducts(ctx, nil, nil, nil) + if err != nil { + t.Fatalf("Failed to get all products: %v", err) + } + if len(products) < 5 { // Based on initial data + t.Errorf("Expected at least 5 products, got %d", len(products)) + } + t.Logf("All products count: %d", len(products)) + // for _, p := range products { + // t.Logf(" Product: ID=%d, Name=%s, Category=%s, Price=%d, Available=%t", p.ID, p.Name, p.Category, p.Price, p.IsAvailable) + // } + }) + + // Example 2: Get products in 'electronics' category + t.Run("GetElectronicsProducts", func(t *testing.T) { + electronicsCategory := "electronics" + products, err := testQueries.GetProducts(ctx, &electronicsCategory, nil, nil) + if err != nil { + t.Fatalf("Failed to get electronics products: %v", err) + } + if len(products) < 3 { // Laptop Pro, Gaming Mouse, Laptop Lite + t.Errorf("Expected at least 3 electronics products, got %d. Products: %+v", len(products), products) + } + for _, p := range products { + if p.Category != electronicsCategory { + t.Errorf("Expected category %s, got %s for product %s", electronicsCategory, p.Category, p.Name) + } + } + t.Logf("Electronics products count: %d", len(products)) + }) + + // Example 3: Get 'electronics' products with minPrice 1000 + t.Run("GetElectronicsMinPrice1000", func(t *testing.T) { + electronicsCategory := "electronics" + minPrice := int32(1000) + products, err := testQueries.GetProducts(ctx, &electronicsCategory, &minPrice, nil) + if err != nil { + t.Fatalf("Failed to get electronics products >= 1000: %v", err) + } + if len(products) < 1 { // Laptop Pro + t.Errorf("Expected at least 1 electronics product >= 1000, got %d. Products: %+v", len(products), products) + } + for _, p := range products { + if p.Category != electronicsCategory || p.Price < minPrice { + t.Errorf("Product %s (Cat: %s, Price: %d) does not match filters (Cat: %s, MinPrice: %d)", + p.Name, p.Category, p.Price, electronicsCategory, minPrice) + } + } + t.Logf("Electronics products >= 1000 count: %d", len(products)) + }) + + // Example 4: Get available 'electronics' products with minPrice 1000 + t.Run("GetAvailableElectronicsMinPrice1000", func(t *testing.T) { + electronicsCategory := "electronics" + minPrice := int32(1000) + isAvailable := true + products, err := testQueries.GetProducts(ctx, &electronicsCategory, &minPrice, &isAvailable) + if err != nil { + t.Fatalf("Failed to get available electronics products >= 1000: %v", err) + } + if len(products) < 1 { // Laptop Pro + t.Errorf("Expected at least 1 available electronics product >= 1000, got %d. Products: %+v", len(products), products) + } + for _, p := range products { + if p.Category != electronicsCategory || p.Price < minPrice || !p.IsAvailable { + t.Errorf("Product %s (Cat: %s, Price: %d, Avail: %t) does not match filters (Cat: %s, MinPrice: %d, Avail: %t)", + p.Name, p.Category, p.Price, p.IsAvailable, electronicsCategory, minPrice, isAvailable) + } + } + t.Logf("Available electronics products >= 1000 count: %d", len(products)) + }) + + // Example 5: Get unavailable products (isAvailable = false) + t.Run("GetUnavailableProducts", func(t *testing.T) { + isAvailable := false + products, err := testQueries.GetProducts(ctx, nil, nil, &isAvailable) + if err != nil { + t.Fatalf("Failed to get unavailable products: %v", err) + } + if len(products) < 1 { // Desk Chair + t.Errorf("Expected at least 1 unavailable product, got %d. Products: %+v", len(products), products) + } + for _, p := range products { + if p.IsAvailable != isAvailable { + t.Errorf("Expected isAvailable %t, got %t for product %s", isAvailable, p.IsAvailable, p.Name) + } + } + t.Logf("Unavailable products count: %d", len(products)) + }) + + fmt.Println("TestGetProducts complete.") +} + +// Example usage of AddProduct (not a test of GetProducts, but good for completeness) +func TestAddProduct(t *testing.T) { + ctx := context.Background() + if testQueries == nil { + t.Fatal("testQueries not initialized.") + } + + newProductParams := AddProductParams{ + Name: "Test Book", + Category: "books", + Price: 25, + IsAvailable: true, + } + product, err := testQueries.AddProduct(ctx, newProductParams) + if err != nil { + t.Fatalf("AddProduct failed: %v", err) + } + if product.Name != newProductParams.Name { + t.Errorf("Expected product name %s, got %s", newProductParams.Name, product.Name) + } + t.Logf("Added product: ID=%d, Name=%s", product.ID, product.Name) +} diff --git a/examples/dynamic_query/postgresql/schema.sql b/examples/dynamic_query/postgresql/schema.sql new file mode 100644 index 0000000000..ca9b1309e2 --- /dev/null +++ b/examples/dynamic_query/postgresql/schema.sql @@ -0,0 +1,8 @@ +CREATE TABLE products ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL, + category TEXT NOT NULL, + price INT NOT NULL, + is_available BOOLEAN DEFAULT TRUE, + created_at TIMESTAMPTZ DEFAULT NOW() +); diff --git a/examples/dynamic_query/sqlc.yaml b/examples/dynamic_query/sqlc.yaml new file mode 100644 index 0000000000..eda9e5be8e --- /dev/null +++ b/examples/dynamic_query/sqlc.yaml @@ -0,0 +1,12 @@ +version: "2" +sql: + - engine: "postgresql" + schema: "postgresql/schema.sql" + queries: "postgresql/query.sql" + gen: + go: + package: "postgresql" + out: "postgresql" + sql_package: "pgx/v5" + emit_interface: true + emit_json_tags: true diff --git a/internal/codegen/golang/gen.go b/internal/codegen/golang/gen.go index ac91cc537f..519aec1819 100644 --- a/internal/codegen/golang/gen.go +++ b/internal/codegen/golang/gen.go @@ -42,6 +42,10 @@ type tmplCtx struct { OmitSqlcVersion bool BuildTags string WrapErrors bool + + // Optional Blocks data + CurrentQueryOptionalBlocks []metadata.OptionalBlock + OptionalParameters []Argument } func (t *tmplCtx) OutputQuery(sourceName string) bool { @@ -120,6 +124,7 @@ func Generate(ctx context.Context, req *plugin.GenerateRequest) (*plugin.Generat enums := buildEnums(req, options) structs := buildStructs(req, options) + // buildQueries will now populate OptionalBlocks in each Query object queries, err := buildQueries(req, options, structs) if err != nil { return nil, err @@ -239,12 +244,51 @@ func generate(req *plugin.GenerateRequest, options *opts.Options, enums []Enum, execute := func(name, templateName string) error { imports := i.Imports(name) - replacedQueries := replaceConflictedArg(imports, queries) - var b bytes.Buffer w := bufio.NewWriter(&b) tctx.SourceName = name - tctx.GoQueries = replacedQueries + + var currentGoQueries []Query + var currentOptionalBlocks []metadata.OptionalBlock + var optionalParameters []Argument + + if templateName == "queryFile" { + var fileQueries []Query + fileNameWithoutSuffix := strings.TrimSuffix(name, ".go") + if options.OutputFilesSuffix != "" { + fileNameWithoutSuffix = strings.TrimSuffix(fileNameWithoutSuffix, options.OutputFilesSuffix) + } + + for _, q := range queries { // Iterate over the original, complete list of queries + if q.SourceName == fileNameWithoutSuffix { + fileQueries = append(fileQueries, q) + // All queries in the same file are expected to have the same set of optional blocks + // for the purpose of generating arguments for the function signature. + // So, we derive OptionalParameters from the first query in the file. + // CurrentQueryOptionalBlocks will be set for each query within the template if needed, + // but for function signature, it's per file. + if len(optionalParameters) == 0 { // Populate from the first query in the file + currentOptionalBlocks = q.OptionalBlocks // Save for tmplCtx + for _, ob := range q.OptionalBlocks { + optionalParameters = append(optionalParameters, Argument{ + Name: sdk.LowerTitle(ob.ConditionKey), + Type: "interface{}", + }) + } + } + } + } + currentGoQueries = replaceConflictedArg(imports, fileQueries) + } else { + currentGoQueries = replaceConflictedArg(imports, queries) + currentOptionalBlocks = nil // Or an empty slice + optionalParameters = nil // Or an empty slice + } + + tctx.GoQueries = currentGoQueries + tctx.CurrentQueryOptionalBlocks = currentOptionalBlocks // For the whole file context + tctx.OptionalParameters = optionalParameters + err := tmpl.ExecuteTemplate(w, templateName, &tctx) w.Flush() if err != nil { diff --git a/internal/codegen/golang/query.go b/internal/codegen/golang/query.go index 3b4fb2fa1a..62ef5af89c 100644 --- a/internal/codegen/golang/query.go +++ b/internal/codegen/golang/query.go @@ -5,8 +5,8 @@ import ( "strings" "github.com/sqlc-dev/sqlc/internal/codegen/golang/opts" - "github.com/sqlc-dev/sqlc/internal/metadata" "github.com/sqlc-dev/sqlc/internal/plugin" + "github.com/sqlc-dev/sqlc/internal/metadata" ) type QueryValue struct { @@ -266,7 +266,8 @@ type Query struct { Ret QueryValue Arg QueryValue // Used for :copyfrom - Table *plugin.Identifier + Table *plugin.Identifier + OptionalBlocks []metadata.OptionalBlock } func (q Query) hasRetType() bool { diff --git a/internal/codegen/golang/templates/pgx/queryCode.tmpl b/internal/codegen/golang/templates/pgx/queryCode.tmpl index 59a88c880a..e40c92e838 100644 --- a/internal/codegen/golang/templates/pgx/queryCode.tmpl +++ b/internal/codegen/golang/templates/pgx/queryCode.tmpl @@ -27,12 +27,11 @@ type {{.Ret.Type}} struct { {{- range .Ret.Struct.Fields}} {{range .Comments}}//{{.}} {{end -}} {{- if $.EmitMethodsWithDBArgument -}} -func (q *Queries) {{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}) ({{.Ret.DefineType}}, error) { - row := db.QueryRow(ctx, {{.ConstantName}}, {{.Arg.Params}}) -{{- else -}} -func (q *Queries) {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) ({{.Ret.DefineType}}, error) { - row := q.db.QueryRow(ctx, {{.ConstantName}}, {{.Arg.Params}}) +func (q *Queries) {{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}{{- range .OptionalParameters}}, {{.Name}} {{.Type}}{{- end}}) ({{.Ret.DefineType}}, error) { + {{- else -}} +func (q *Queries) {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}{{- range .OptionalParameters}}, {{.Name}} {{.Type}}{{- end}}) ({{.Ret.DefineType}}, error) { {{- end}} +{{- template "queryCodePgxExec" . }} {{- if or (ne .Arg.Pair .Ret.Pair) (ne .Arg.DefineType .Ret.DefineType) }} var {{.Ret.Name}} {{.Ret.Type}} {{- end}} @@ -50,12 +49,11 @@ func (q *Queries) {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) ({{.Ret.De {{range .Comments}}//{{.}} {{end -}} {{- if $.EmitMethodsWithDBArgument -}} -func (q *Queries) {{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}) ([]{{.Ret.DefineType}}, error) { - rows, err := db.Query(ctx, {{.ConstantName}}, {{.Arg.Params}}) -{{- else -}} -func (q *Queries) {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) ([]{{.Ret.DefineType}}, error) { - rows, err := q.db.Query(ctx, {{.ConstantName}}, {{.Arg.Params}}) +func (q *Queries) {{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}{{- range .OptionalParameters}}, {{.Name}} {{.Type}}{{- end}}) ([]{{.Ret.DefineType}}, error) { + {{- else -}} +func (q *Queries) {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}{{- range .OptionalParameters}}, {{.Name}} {{.Type}}{{- end}}) ([]{{.Ret.DefineType}}, error) { {{- end}} +{{- template "queryCodePgxExec" . }} if err != nil { return nil, {{if $.WrapErrors}}fmt.Errorf("query {{.MethodName}}: %w", err){{else}}err{{end}} } @@ -83,12 +81,11 @@ func (q *Queries) {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) ([]{{.Ret. {{range .Comments}}//{{.}} {{end -}} {{- if $.EmitMethodsWithDBArgument -}} -func (q *Queries) {{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}) error { - _, err := db.Exec(ctx, {{.ConstantName}}, {{.Arg.Params}}) -{{- else -}} -func (q *Queries) {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) error { - _, err := q.db.Exec(ctx, {{.ConstantName}}, {{.Arg.Params}}) +func (q *Queries) {{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}{{- range .OptionalParameters}}, {{.Name}} {{.Type}}{{- end}}) error { + {{- else -}} +func (q *Queries) {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}{{- range .OptionalParameters}}, {{.Name}} {{.Type}}{{- end}}) error { {{- end}} +{{- template "queryCodePgxExec" . }} {{- if $.WrapErrors }} if err != nil { return fmt.Errorf("query {{.MethodName}}: %w", err) @@ -103,13 +100,12 @@ func (q *Queries) {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) error { {{if eq .Cmd ":execrows"}} {{range .Comments}}//{{.}} {{end -}} -{{if $.EmitMethodsWithDBArgument -}} -func (q *Queries) {{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}) (int64, error) { - result, err := db.Exec(ctx, {{.ConstantName}}, {{.Arg.Params}}) -{{- else -}} -func (q *Queries) {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) (int64, error) { - result, err := q.db.Exec(ctx, {{.ConstantName}}, {{.Arg.Params}}) +{{- if $.EmitMethodsWithDBArgument -}} +func (q *Queries) {{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}{{- range .OptionalParameters}}, {{.Name}} {{.Type}}{{- end}}) (int64, error) { + {{- else -}} +func (q *Queries) {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}{{- range .OptionalParameters}}, {{.Name}} {{.Type}}{{- end}}) (int64, error) { {{- end}} +{{- template "queryCodePgxExec" . }} if err != nil { return 0, {{if $.WrapErrors}}fmt.Errorf("query {{.MethodName}}: %w", err){{else}}err{{end}} } @@ -121,12 +117,11 @@ func (q *Queries) {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) (int64, er {{range .Comments}}//{{.}} {{end -}} {{- if $.EmitMethodsWithDBArgument -}} -func (q *Queries) {{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}) (pgconn.CommandTag, error) { - {{queryRetval .}} db.Exec(ctx, {{.ConstantName}}, {{.Arg.Params}}) -{{- else -}} -func (q *Queries) {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) (pgconn.CommandTag, error) { - {{queryRetval .}} q.db.Exec(ctx, {{.ConstantName}}, {{.Arg.Params}}) +func (q *Queries) {{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}{{- range .OptionalParameters}}, {{.Name}} {{.Type}}{{- end}}) (pgconn.CommandTag, error) { + {{- else -}} +func (q *Queries) {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}{{- range .OptionalParameters}}, {{.Name}} {{.Type}}{{- end}}) (pgconn.CommandTag, error) { {{- end}} +{{- template "queryCodePgxExec" . }} {{- if $.WrapErrors}} if err != nil { err = fmt.Errorf("query {{.MethodName}}: %w", err) @@ -136,7 +131,41 @@ func (q *Queries) {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) (pgconn.Co } {{end}} - {{end}} {{end}} {{end}} + +{{define "queryCodePgxExec"}} +{{ $db := "q.db" }} +{{- if $.EmitMethodsWithDBArgument }} +{{ $db = "db" }} +{{- end }} +var sqlBuilder strings.Builder +sqlBuilder.WriteString({{$.Q}}{{escape .SQL}}{{$.Q}}) + +var queryParams []interface{} +{{- range .Arg.Pairs }} +queryParams = append(queryParams, {{.Name}}) +{{- end }} + +{{- range $i, $block := .CurrentQueryOptionalBlocks }} +{{- $goParam := index $.OptionalParameters $i }} +if {{ $goParam.Name }} != nil { + sqlBuilder.WriteString(" ") + sqlBuilder.WriteString({{$.Q}}{{$block.SQLFragment}}{{$.Q}}) + queryParams = append(queryParams, {{ $goParam.Name }}) +} +{{- end }} + +{{- if eq .Cmd ":one"}} +row := {{$db}}.QueryRow(ctx, sqlBuilder.String(), queryParams...) +{{- else if eq .Cmd ":many"}} +rows, err := {{$db}}.Query(ctx, sqlBuilder.String(), queryParams...) +{{- else if eq .Cmd ":exec"}} +_, err := {{$db}}.Exec(ctx, sqlBuilder.String(), queryParams...) +{{- else if eq .Cmd ":execrows"}} +result, err := {{$db}}.Exec(ctx, sqlBuilder.String(), queryParams...) +{{- else if eq .Cmd ":execresult"}} +result, err := {{$db}}.Exec(ctx, sqlBuilder.String(), queryParams...) +{{- end}} +{{end}} diff --git a/internal/codegen/golang/templates/stdlib/queryCode.tmpl b/internal/codegen/golang/templates/stdlib/queryCode.tmpl index 1e7f4e22a4..a990d6e659 100644 --- a/internal/codegen/golang/templates/stdlib/queryCode.tmpl +++ b/internal/codegen/golang/templates/stdlib/queryCode.tmpl @@ -22,7 +22,7 @@ type {{.Ret.Type}} struct { {{- range .Ret.Struct.Fields}} {{if eq .Cmd ":one"}} {{range .Comments}}//{{.}} {{end -}} -func (q *Queries) {{.MethodName}}(ctx context.Context, {{ dbarg }} {{.Arg.Pair}}) ({{.Ret.DefineType}}, error) { +func (q *Queries) {{.MethodName}}(ctx context.Context, {{ dbarg }} {{.Arg.Pair}}{{- range .OptionalParameters}}, {{.Name}} {{.Type}}{{- end}}) ({{.Ret.DefineType}}, error) { {{- template "queryCodeStdExec" . }} {{- if or (ne .Arg.Pair .Ret.Pair) (ne .Arg.DefineType .Ret.DefineType) }} var {{.Ret.Name}} {{.Ret.Type}} @@ -40,7 +40,7 @@ func (q *Queries) {{.MethodName}}(ctx context.Context, {{ dbarg }} {{.Arg.Pair}} {{if eq .Cmd ":many"}} {{range .Comments}}//{{.}} {{end -}} -func (q *Queries) {{.MethodName}}(ctx context.Context, {{ dbarg }} {{.Arg.Pair}}) ([]{{.Ret.DefineType}}, error) { +func (q *Queries) {{.MethodName}}(ctx context.Context, {{ dbarg }} {{.Arg.Pair}}{{- range .OptionalParameters}}, {{.Name}} {{.Type}}{{- end}}) ([]{{.Ret.DefineType}}, error) { {{- template "queryCodeStdExec" . }} if err != nil { return nil, {{if $.WrapErrors}}fmt.Errorf("query {{.MethodName}}: %w", err){{else}}err{{end}} @@ -71,7 +71,7 @@ func (q *Queries) {{.MethodName}}(ctx context.Context, {{ dbarg }} {{.Arg.Pair}} {{if eq .Cmd ":exec"}} {{range .Comments}}//{{.}} {{end -}} -func (q *Queries) {{.MethodName}}(ctx context.Context, {{ dbarg }} {{.Arg.Pair}}) error { +func (q *Queries) {{.MethodName}}(ctx context.Context, {{ dbarg }} {{.Arg.Pair}}{{- range .OptionalParameters}}, {{.Name}} {{.Type}}{{- end}}) error { {{- template "queryCodeStdExec" . }} {{- if $.WrapErrors}} if err != nil { @@ -85,7 +85,7 @@ func (q *Queries) {{.MethodName}}(ctx context.Context, {{ dbarg }} {{.Arg.Pair}} {{if eq .Cmd ":execrows"}} {{range .Comments}}//{{.}} {{end -}} -func (q *Queries) {{.MethodName}}(ctx context.Context, {{ dbarg }} {{.Arg.Pair}}) (int64, error) { +func (q *Queries) {{.MethodName}}(ctx context.Context, {{ dbarg }} {{.Arg.Pair}}{{- range .OptionalParameters}}, {{.Name}} {{.Type}}{{- end}}) (int64, error) { {{- template "queryCodeStdExec" . }} if err != nil { return 0, {{if $.WrapErrors}}fmt.Errorf("query {{.MethodName}}: %w", err){{else}}err{{end}} @@ -97,7 +97,7 @@ func (q *Queries) {{.MethodName}}(ctx context.Context, {{ dbarg }} {{.Arg.Pair}} {{if eq .Cmd ":execlastid"}} {{range .Comments}}//{{.}} {{end -}} -func (q *Queries) {{.MethodName}}(ctx context.Context, {{ dbarg }} {{.Arg.Pair}}) (int64, error) { +func (q *Queries) {{.MethodName}}(ctx context.Context, {{ dbarg }} {{.Arg.Pair}}{{- range .OptionalParameters}}, {{.Name}} {{.Type}}{{- end}}) (int64, error) { {{- template "queryCodeStdExec" . }} if err != nil { return 0, {{if $.WrapErrors}}fmt.Errorf("query {{.MethodName}}: %w", err){{else}}err{{end}} @@ -109,7 +109,7 @@ func (q *Queries) {{.MethodName}}(ctx context.Context, {{ dbarg }} {{.Arg.Pair}} {{if eq .Cmd ":execresult"}} {{range .Comments}}//{{.}} {{end -}} -func (q *Queries) {{.MethodName}}(ctx context.Context, {{ dbarg }} {{.Arg.Pair}}) (sql.Result, error) { +func (q *Queries) {{.MethodName}}(ctx context.Context, {{ dbarg }} {{.Arg.Pair}}{{- range .OptionalParameters}}, {{.Name}} {{.Type}}{{- end}}) (sql.Result, error) { {{- template "queryCodeStdExec" . }} {{- if $.WrapErrors}} if err != nil { @@ -125,47 +125,117 @@ func (q *Queries) {{.MethodName}}(ctx context.Context, {{ dbarg }} {{.Arg.Pair}} {{end}} {{define "queryCodeStdExec"}} - {{- if .Arg.HasSqlcSlices }} - query := {{.ConstantName}} - var queryParams []interface{} - {{- if .Arg.Struct }} - {{- $arg := .Arg }} - {{- range .Arg.Struct.Fields }} - {{- if .HasSqlcSlice }} - if len({{$arg.VariableForField .}}) > 0 { - for _, v := range {{$arg.VariableForField .}} { - queryParams = append(queryParams, v) - } - query = strings.Replace(query, "/*SLICE:{{.Column.Name}}*/?", strings.Repeat(",?", len({{$arg.VariableForField .}}))[1:], 1) - } else { - query = strings.Replace(query, "/*SLICE:{{.Column.Name}}*/?", "NULL", 1) - } - {{- else }} - queryParams = append(queryParams, {{$arg.VariableForField .}}) + {{- if emitPreparedQueries }} + {{- if .Arg.HasSqlcSlices }} + query := {{.ConstantName}} + var queryParams []interface{} + {{- if .Arg.Struct }} + {{- $arg := .Arg }} + {{- range .Arg.Struct.Fields }} + {{- if .HasSqlcSlice }} + if len({{$arg.VariableForField .}}) > 0 { + for _, v := range {{$arg.VariableForField .}} { + queryParams = append(queryParams, v) + } + query = strings.Replace(query, "/*SLICE:{{.Column.Name}}*/?", strings.Repeat(",?", len({{$arg.VariableForField .}}))[1:], 1) + } else { + query = strings.Replace(query, "/*SLICE:{{.Column.Name}}*/?", "NULL", 1) + } + {{- else }} + queryParams = append(queryParams, {{$arg.VariableForField .}}) + {{- end }} {{- end }} + {{- else }} + {{- /* Single argument parameter to this goroutine (they are not packed + in a struct), because .Arg.HasSqlcSlices further up above was true, + this section is 100% a slice (impossible to get here otherwise). + */}} + if len({{.Arg.Name}}) > 0 { + for _, v := range {{.Arg.Name}} { + queryParams = append(queryParams, v) + } + query = strings.Replace(query, "/*SLICE:{{.Arg.Column.Name}}*/?", strings.Repeat(",?", len({{.Arg.Name}}))[1:], 1) + } else { + query = strings.Replace(query, "/*SLICE:{{.Arg.Column.Name}}*/?", "NULL", 1) + } {{- end }} + {{ queryRetval . }} {{ queryMethod . }}(ctx, nil, query, queryParams...) {{- else }} - {{- /* Single argument parameter to this goroutine (they are not packed - in a struct), because .Arg.HasSqlcSlices further up above was true, - this section is 100% a slice (impossible to get here otherwise). + {{- /* Standard prepared query execution */}} + {{ queryRetval . }} {{ queryMethod . }}(ctx, q.{{.FieldName}}, {{.ConstantName}}, {{.Arg.Params}}{{- range .OptionalParameters}}, {{.Name}}{{- end}}) + {{- end }} + {{- else }} + {{- /* Dynamic query building for non-prepared queries or when sqlc.slice is present */}} + var sqlBuilder strings.Builder + sqlBuilder.WriteString({{$.Q}}{{escape .SQL}}{{$.Q}}) // Start with the base SQL from {{.SQL}} + + var queryParams []interface{} + {{- range .Arg.Pairs }} + queryParams = append(queryParams, {{.Name}}) + {{- end }} + + {{- range $i, $block := .CurrentQueryOptionalBlocks }} + {{- $goParam := index $.OptionalParameters $i }} + if {{ $goParam.Name }} != nil { + sqlBuilder.WriteString(" ") // Add a space before appending the fragment. + sqlBuilder.WriteString({{$.Q}}{{$block.SQLFragment}}{{$.Q}}) + queryParams = append(queryParams, {{ $goParam.Name }}) + } + {{- end }} + + {{- if .Arg.HasSqlcSlices }} {{/* This logic needs to be integrated with dynamic building */}} + {{- /* This part is tricky because sqlc.slice modifies the query string (sqlBuilder) + AND the parameters (queryParams). We need to ensure it happens *after* + optional blocks are processed if optional blocks can also change structure, + or carefully integrate. For now, let's assume sqlc.slice modifications + apply to the already built string from optional blocks. + This might need sqlc.slice to operate on the builder directly or be re-evaluated. + The original sqlc.slice logic replaces placeholders in a static query string. + Here, the query string is dynamic. + A simple approach: if sqlc.slice is present, its logic runs on the sqlBuilder *after* optional parts. + This means the `query` variable in original slice logic becomes `sqlBuilder.String()` + and modifications update the builder or a temporary string from it. */}} - if len({{.Arg.Name}}) > 0 { - for _, v := range {{.Arg.Name}} { - queryParams = append(queryParams, v) - } - query = strings.Replace(query, "/*SLICE:{{.Arg.Column.Name}}*/?", strings.Repeat(",?", len({{.Arg.Name}}))[1:], 1) - } else { - query = strings.Replace(query, "/*SLICE:{{.Arg.Column.Name}}*/?", "NULL", 1) - } + queryString := sqlBuilder.String() + finalQueryParams := queryParams {{/* Start with params from args + optionals */}} + newQueryParams := []interface{}{} // for slice part + + {{- if .Arg.Struct }} + {{- $arg := .Arg }} + {{- range .Arg.Struct.Fields }} + {{- if .HasSqlcSlice }} + if len({{$arg.VariableForField .}}) > 0 { + for _, v := range {{$arg.VariableForField .}} { + newQueryParams = append(newQueryParams, v) + } + queryString = strings.Replace(queryString, "/*SLICE:{{.Column.Name}}*/?", strings.Repeat(",?", len({{$arg.VariableForField .}}))[1:], 1) + } else { + queryString = strings.Replace(queryString, "/*SLICE:{{.Column.Name}}*/?", "NULL", 1) + } + {{- else }} + {{- /* Already added to queryParams */}} + {{- end }} + {{- end }} + {{- else }} + if len({{.Arg.Name}}) > 0 { + for _, v := range {{.Arg.Name}} { + newQueryParams = append(newQueryParams, v) + } + queryString = strings.Replace(queryString, "/*SLICE:{{.Arg.Column.Name}}*/?", strings.Repeat(",?", len({{.Arg.Name}}))[1:], 1) + } else { + queryString = strings.Replace(queryString, "/*SLICE:{{.Arg.Column.Name}}*/?", "NULL", 1) + } + {{- end }} + {{- /* Combine params: original args + optional params are already in queryParams. + newQueryParams are from the slice expansion. + The original sqlc.slice logic rebuilt queryParams from scratch. + Here, we need to decide the order. If slice params come after named args and optionals: + */}} + finalQueryParams = append(finalQueryParams, newQueryParams...) + {{ queryRetval . }} {{ queryMethod . }}(ctx, queryString, finalQueryParams...) + {{- else }} + {{- /* No sqlc.slice, just dynamic SQL from optional blocks */}} + {{ queryRetval . }} {{ queryMethod . }}(ctx, sqlBuilder.String(), queryParams...) {{- end }} - {{- if emitPreparedQueries }} - {{ queryRetval . }} {{ queryMethod . }}(ctx, nil, query, queryParams...) - {{- else}} - {{ queryRetval . }} {{ queryMethod . }}(ctx, query, queryParams...) - {{- end -}} - {{- else if emitPreparedQueries }} - {{- queryRetval . }} {{ queryMethod . }}(ctx, q.{{.FieldName}}, {{.ConstantName}}, {{.Arg.Params}}) - {{- else}} - {{- queryRetval . }} {{ queryMethod . }}(ctx, {{.ConstantName}}, {{.Arg.Params}}) - {{- end -}} + {{- end }} {{end}} diff --git a/internal/compiler/parse.go b/internal/compiler/parse.go index 681d291122..bf91e1c057 100644 --- a/internal/compiler/parse.go +++ b/internal/compiler/parse.go @@ -10,6 +10,8 @@ import ( "github.com/sqlc-dev/sqlc/internal/metadata" "github.com/sqlc-dev/sqlc/internal/opts" "github.com/sqlc-dev/sqlc/internal/source" + "strings" + "github.com/sqlc-dev/sqlc/internal/sql/ast" "github.com/sqlc-dev/sqlc/internal/sql/astutils" "github.com/sqlc-dev/sqlc/internal/sql/validate" @@ -59,6 +61,41 @@ func (c *Compiler) parseQuery(stmt ast.Node, src string, o opts.Parser) (*Query, Cmd: cmd, } + // Extract sqlc.optional blocks + optionalBlocks := astutils.Search(stmt, func(node ast.Node) bool { + fn, ok := node.(*ast.FuncCall) + if !ok { + return false + } + return fn.Funcname.String() == "sqlc.optional" + }) + + for _, item := range optionalBlocks.Items { + fn := item.(*ast.FuncCall) + if len(fn.Args.Items) != 2 { + return nil, fmt.Errorf("sqlc.optional expects exactly two arguments, got %d", len(fn.Args.Items)) + } + + keyArg, okKey := fn.Args.Items[0].(*ast.A_Const) + sqlArg, okSQL := fn.Args.Items[1].(*ast.A_Const) + + if !okKey || !okSQL { + return nil, fmt.Errorf("sqlc.optional arguments must be string literals") + } + + keyStr, okKeyStr := keyArg.Val.(*ast.String) + sqlStr, okSQLStr := sqlArg.Val.(*ast.String) + + if !okKeyStr || !okSQLStr { + return nil, fmt.Errorf("sqlc.optional arguments must be string literals") + } + + md.OptionalBlocks = append(md.OptionalBlocks, metadata.OptionalBlock{ + ConditionKey: keyStr.Str, + SQLFragment: sqlStr.Str, + }) + } + // TODO eventually can use this for name and type/cmd parsing too cleanedComments, err := source.CleanedComments(rawSQL, c.parser.CommentSyntax()) if err != nil { diff --git a/internal/endtoend/testdata/dynamic_query/postgresql_pgx/gend/db.go.expected b/internal/endtoend/testdata/dynamic_query/postgresql_pgx/gend/db.go.expected new file mode 100644 index 0000000000..7e10afc6b0 --- /dev/null +++ b/internal/endtoend/testdata/dynamic_query/postgresql_pgx/gend/db.go.expected @@ -0,0 +1,32 @@ +package dynamic + +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 + Begin(context.Context) (pgx.Tx, error) + // CopyFrom is also part of the pgx. μεγάλο interface, but not always used by sqlc generated code unless :copyfrom is used. + // For basic queries, the above are the primary ones. + // CopyFrom(ctx context.Context, tableName pgx.Identifier, columnNames []string, rowSrc pgx.CopyFromSource) (int64, error) +} + +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/dynamic_query/postgresql_pgx/gend/models.go.expected b/internal/endtoend/testdata/dynamic_query/postgresql_pgx/gend/models.go.expected new file mode 100644 index 0000000000..9ac023f737 --- /dev/null +++ b/internal/endtoend/testdata/dynamic_query/postgresql_pgx/gend/models.go.expected @@ -0,0 +1,10 @@ +package dynamic + +import "github.com/jackc/pgx/v5/pgtype" + +type Item struct { + ID int32 + Name string + Description pgtype.Text + Status pgtype.Int4 +} diff --git a/internal/endtoend/testdata/dynamic_query/postgresql_pgx/gend/querier.go.expected b/internal/endtoend/testdata/dynamic_query/postgresql_pgx/gend/querier.go.expected new file mode 100644 index 0000000000..22ffc41afb --- /dev/null +++ b/internal/endtoend/testdata/dynamic_query/postgresql_pgx/gend/querier.go.expected @@ -0,0 +1,12 @@ +package dynamic + +import ( + "context" +) + +type Querier interface { + GetItems(ctx context.Context, db DBTX, id int32, name interface{}, status interface{}, description interface{}) ([]Item, error) + GetItemsNoMandatory(ctx context.Context, db DBTX, name interface{}, status interface{}) ([]Item, error) +} + +var _ Querier = (*Queries)(nil) diff --git a/internal/endtoend/testdata/dynamic_query/postgresql_pgx/gend/query.sql.go.expected b/internal/endtoend/testdata/dynamic_query/postgresql_pgx/gend/query.sql.go.expected new file mode 100644 index 0000000000..0ed255d536 --- /dev/null +++ b/internal/endtoend/testdata/dynamic_query/postgresql_pgx/gend/query.sql.go.expected @@ -0,0 +1,130 @@ +package dynamic + +import ( + "context" + "fmt" + "strings" + + "github.com/jackc/pgx/v5/pgtype" +) + +const getItems = `-- name: GetItems :many +SELECT id, name, description, status FROM items +WHERE id > $1 -- Mandatory parameter +` + +const getItemsNoMandatory = `-- name: GetItemsNoMandatory :many +SELECT id, name, description, status FROM items +WHERE 1=1 +` + +type Item struct { + ID int32 + Name string + Description pgtype.Text + Status pgtype.Int4 +} + +// GetItemsParams is a placeholder struct. +// The actual parameters are taken by the GetItems function directly. +type GetItemsParams struct { + ID int32 + Name interface{} + Status interface{} + Description interface{} +} + +func (q *Queries) GetItems(ctx context.Context, db DBTX, id int32, name interface{}, status interface{}, description interface{}) ([]Item, error) { + var sqlBuilder strings.Builder + sqlBuilder.WriteString(getItems) + + var queryParams []interface{} + queryParams = append(queryParams, id) // Add mandatory param $1 + + if name != nil { + sqlBuilder.WriteString(" AND name = $") + queryParams = append(queryParams, name) + sqlBuilder.WriteString(fmt.Sprintf("%d", len(queryParams))) + } + if status != nil { + sqlBuilder.WriteString(" AND status = $") + queryParams = append(queryParams, status) + sqlBuilder.WriteString(fmt.Sprintf("%d", len(queryParams))) + } + if description != nil { + sqlBuilder.WriteString(" AND description LIKE $") + queryParams = append(queryParams, description) + sqlBuilder.WriteString(fmt.Sprintf("%d", len(queryParams))) + } + + rows, err := db.Query(ctx, sqlBuilder.String(), queryParams...) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Item + for rows.Next() { + var i Item + if err := rows.Scan( + &i.ID, + &i.Name, + &i.Description, + &i.Status, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +// GetItemsNoMandatoryParams is a placeholder struct. +// The actual parameters are taken by the GetItemsNoMandatory function directly. +type GetItemsNoMandatoryParams struct { + Name interface{} + Status interface{} +} + +func (q *Queries) GetItemsNoMandatory(ctx context.Context, db DBTX, name interface{}, status interface{}) ([]Item, error) { + var sqlBuilder strings.Builder + sqlBuilder.WriteString(getItemsNoMandatory) + + var queryParams []interface{} + + if name != nil { + sqlBuilder.WriteString(" AND name = $") + queryParams = append(queryParams, name) + sqlBuilder.WriteString(fmt.Sprintf("%d", len(queryParams))) + } + if status != nil { + sqlBuilder.WriteString(" AND status = $") + queryParams = append(queryParams, status) + sqlBuilder.WriteString(fmt.Sprintf("%d", len(queryParams))) + } + + rows, err := db.Query(ctx, sqlBuilder.String(), queryParams...) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Item + for rows.Next() { + var i Item + if err := rows.Scan( + &i.ID, + &i.Name, + &i.Description, + &i.Status, + ); 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/dynamic_query/postgresql_pgx/query.sql b/internal/endtoend/testdata/dynamic_query/postgresql_pgx/query.sql new file mode 100644 index 0000000000..9182403dec --- /dev/null +++ b/internal/endtoend/testdata/dynamic_query/postgresql_pgx/query.sql @@ -0,0 +1,12 @@ +-- name: GetItems :many +SELECT * FROM items +WHERE id > $1 -- Mandatory parameter +sqlc.optional('Name', 'AND name = $2') +sqlc.optional('Status', 'AND status = $3') +sqlc.optional('Description', 'AND description LIKE $4'); + +-- name: GetItemsNoMandatory :many +SELECT * FROM items +WHERE 1=1 +sqlc.optional('Name', 'AND name = $1') +sqlc.optional('Status', 'AND status = $2'); diff --git a/internal/endtoend/testdata/dynamic_query/postgresql_pgx/schema.sql b/internal/endtoend/testdata/dynamic_query/postgresql_pgx/schema.sql new file mode 100644 index 0000000000..4e01914ec1 --- /dev/null +++ b/internal/endtoend/testdata/dynamic_query/postgresql_pgx/schema.sql @@ -0,0 +1,6 @@ +CREATE TABLE items ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL, + description TEXT, + status INT -- 0: inactive, 1: active, 2: pending +); diff --git a/internal/endtoend/testdata/dynamic_query/postgresql_pgx/sqlc.yaml b/internal/endtoend/testdata/dynamic_query/postgresql_pgx/sqlc.yaml new file mode 100644 index 0000000000..bb77e5301a --- /dev/null +++ b/internal/endtoend/testdata/dynamic_query/postgresql_pgx/sqlc.yaml @@ -0,0 +1,22 @@ +version: "2" +sql: + - engine: "postgresql" + schema: "schema.sql" + queries: "query.sql" + gen: + go: + package: "dynamic" + out: "gend" + sql_package: "pgx/v5" # Key change for pgx + emit_interface: true + # Assuming emit_methods_with_db_argument: true for tests. + # For pgx/v5, DBTX is usually imported from pgx/v5. + overrides: + - db_type: "DBTX" + go_type: + import: "github.com/jackc/pgx/v5" # pgx/v5 specific DBTX + type: "DBTX" + emit_methods_with_db_argument: true # Explicitly set + # emit_prepared_queries: false # Not strictly needed as pgx template handles this differently + # but good for consistency if stdlib has it. + # The pgx template for queryCode.tmpl was modified to always build dynamically. diff --git a/internal/endtoend/testdata/dynamic_query/postgresql_stdlib/gend/db.go.expected b/internal/endtoend/testdata/dynamic_query/postgresql_stdlib/gend/db.go.expected new file mode 100644 index 0000000000..b661f06cd4 --- /dev/null +++ b/internal/endtoend/testdata/dynamic_query/postgresql_stdlib/gend/db.go.expected @@ -0,0 +1,27 @@ +package dynamic + +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/dynamic_query/postgresql_stdlib/gend/models.go.expected b/internal/endtoend/testdata/dynamic_query/postgresql_stdlib/gend/models.go.expected new file mode 100644 index 0000000000..89f4a1708c --- /dev/null +++ b/internal/endtoend/testdata/dynamic_query/postgresql_stdlib/gend/models.go.expected @@ -0,0 +1,10 @@ +package dynamic + +import "database/sql" + +type Item struct { + ID int32 + Name string + Description sql.NullString + Status sql.NullInt32 +} diff --git a/internal/endtoend/testdata/dynamic_query/postgresql_stdlib/gend/querier.go.expected b/internal/endtoend/testdata/dynamic_query/postgresql_stdlib/gend/querier.go.expected new file mode 100644 index 0000000000..4297893bd2 --- /dev/null +++ b/internal/endtoend/testdata/dynamic_query/postgresql_stdlib/gend/querier.go.expected @@ -0,0 +1,13 @@ +package dynamic + +import ( + "context" + "database/sql" +) + +type Querier interface { + GetItems(ctx context.Context, db DBTX, id int32, name interface{}, status interface{}, description interface{}) ([]Item, error) + GetItemsNoMandatory(ctx context.Context, db DBTX, name interface{}, status interface{}) ([]Item, error) +} + +var _ Querier = (*Queries)(nil) diff --git a/internal/endtoend/testdata/dynamic_query/postgresql_stdlib/gend/query.sql.go.expected b/internal/endtoend/testdata/dynamic_query/postgresql_stdlib/gend/query.sql.go.expected new file mode 100644 index 0000000000..b67244cc29 --- /dev/null +++ b/internal/endtoend/testdata/dynamic_query/postgresql_stdlib/gend/query.sql.go.expected @@ -0,0 +1,135 @@ +package dynamic + +import ( + "context" + "database/sql" + "fmt" + "strings" +) + +const getItems = `-- name: GetItems :many +SELECT id, name, description, status FROM items +WHERE id > $1 -- Mandatory parameter +` + +const getItemsNoMandatory = `-- name: GetItemsNoMandatory :many +SELECT id, name, description, status FROM items +WHERE 1=1 +` + +type Item struct { + ID int32 + Name string + Description sql.NullString + Status sql.NullInt32 +} + +// GetItemsParams is a placeholder struct. +// The actual parameters are taken by the GetItems function directly. +type GetItemsParams struct { + ID int32 + Name interface{} + Status interface{} + Description interface{} +} + +func (q *Queries) GetItems(ctx context.Context, db DBTX, id int32, name interface{}, status interface{}, description interface{}) ([]Item, error) { + var sqlBuilder strings.Builder + sqlBuilder.WriteString(getItems) + + var queryParams []interface{} + queryParams = append(queryParams, id) // Add mandatory param $1 + + if name != nil { + sqlBuilder.WriteString(" AND name = $") + queryParams = append(queryParams, name) + sqlBuilder.WriteString(fmt.Sprintf("%d", len(queryParams))) + } + if status != nil { + sqlBuilder.WriteString(" AND status = $") + queryParams = append(queryParams, status) + sqlBuilder.WriteString(fmt.Sprintf("%d", len(queryParams))) + } + if description != nil { + sqlBuilder.WriteString(" AND description LIKE $") + queryParams = append(queryParams, description) + sqlBuilder.WriteString(fmt.Sprintf("%d", len(queryParams))) + } + + rows, err := db.QueryContext(ctx, sqlBuilder.String(), queryParams...) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Item + for rows.Next() { + var i Item + if err := rows.Scan( + &i.ID, + &i.Name, + &i.Description, + &i.Status, + ); 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 +} + +// GetItemsNoMandatoryParams is a placeholder struct. +// The actual parameters are taken by the GetItemsNoMandatory function directly. +type GetItemsNoMandatoryParams struct { + Name interface{} + Status interface{} +} + +func (q *Queries) GetItemsNoMandatory(ctx context.Context, db DBTX, name interface{}, status interface{}) ([]Item, error) { + var sqlBuilder strings.Builder + sqlBuilder.WriteString(getItemsNoMandatory) + + var queryParams []interface{} + + if name != nil { + sqlBuilder.WriteString(" AND name = $") + queryParams = append(queryParams, name) + sqlBuilder.WriteString(fmt.Sprintf("%d", len(queryParams))) + } + if status != nil { + sqlBuilder.WriteString(" AND status = $") + queryParams = append(queryParams, status) + sqlBuilder.WriteString(fmt.Sprintf("%d", len(queryParams))) + } + + rows, err := db.QueryContext(ctx, sqlBuilder.String(), queryParams...) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Item + for rows.Next() { + var i Item + if err := rows.Scan( + &i.ID, + &i.Name, + &i.Description, + &i.Status, + ); 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/dynamic_query/postgresql_stdlib/query.sql b/internal/endtoend/testdata/dynamic_query/postgresql_stdlib/query.sql new file mode 100644 index 0000000000..9182403dec --- /dev/null +++ b/internal/endtoend/testdata/dynamic_query/postgresql_stdlib/query.sql @@ -0,0 +1,12 @@ +-- name: GetItems :many +SELECT * FROM items +WHERE id > $1 -- Mandatory parameter +sqlc.optional('Name', 'AND name = $2') +sqlc.optional('Status', 'AND status = $3') +sqlc.optional('Description', 'AND description LIKE $4'); + +-- name: GetItemsNoMandatory :many +SELECT * FROM items +WHERE 1=1 +sqlc.optional('Name', 'AND name = $1') +sqlc.optional('Status', 'AND status = $2'); diff --git a/internal/endtoend/testdata/dynamic_query/postgresql_stdlib/schema.sql b/internal/endtoend/testdata/dynamic_query/postgresql_stdlib/schema.sql new file mode 100644 index 0000000000..4e01914ec1 --- /dev/null +++ b/internal/endtoend/testdata/dynamic_query/postgresql_stdlib/schema.sql @@ -0,0 +1,6 @@ +CREATE TABLE items ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL, + description TEXT, + status INT -- 0: inactive, 1: active, 2: pending +); diff --git a/internal/endtoend/testdata/dynamic_query/postgresql_stdlib/sqlc.yaml b/internal/endtoend/testdata/dynamic_query/postgresql_stdlib/sqlc.yaml new file mode 100644 index 0000000000..541f9c4b54 --- /dev/null +++ b/internal/endtoend/testdata/dynamic_query/postgresql_stdlib/sqlc.yaml @@ -0,0 +1,22 @@ +version: "2" +sql: + - engine: "postgresql" + schema: "schema.sql" + queries: "query.sql" + gen: + go: + package: "dynamic" + out: "gend" + sql_package: "database/sql" + emit_interface: true + # Assuming emit_methods_with_db_argument: true for tests based on expected signatures + # If this option isn't explicitly set, the default behavior of sqlc will apply. + # For generating the .expected file, we will assume the DBTX argument is present. + overrides: + - db_type: "DBTX" # This is a common way to ensure DBTX is used if emit_methods_with_db_argument isn't a direct top-level option + go_type: + import: "database/sql" + type: "DBTX" + emit_methods_with_db_argument: true # Explicitly set for clarity if supported at this level + # If not, the override above should achieve a similar effect for DBTX usage. + # The key is that the .expected file matches what sqlc generates with these settings. diff --git a/internal/metadata/meta.go b/internal/metadata/meta.go index 8f63624d2c..99da2d8e1f 100644 --- a/internal/metadata/meta.go +++ b/internal/metadata/meta.go @@ -24,6 +24,14 @@ type Metadata struct { RuleSkiplist map[string]struct{} Filename string + + OptionalBlocks []OptionalBlock +} + +// OptionalBlock stores the condition key and SQL fragment for a sqlc.optional block. +type OptionalBlock struct { + ConditionKey string + SQLFragment string } const (