diff --git a/CLAUDE.md b/CLAUDE.md index 9d637256a1..93d08ad0aa 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -189,6 +189,7 @@ POSTGRESQL_SERVER_URI="postgres://postgres:mysecretpassword@localhost:5432/postg - `/cmd/` - Main binaries (sqlc, sqlc-gen-json) - `/internal/cmd/` - Command implementations (vet, generate, etc.) - `/internal/engine/` - Database engine implementations + - `/clickhouse/` - ClickHouse parser and converter - `/postgresql/` - PostgreSQL parser and converter - `/dolphin/` - MySQL parser (uses TiDB parser) - `/sqlite/` - SQLite parser @@ -289,6 +290,55 @@ git push --force-with-lease origin - **CI Configuration:** `/.github/workflows/ci.yml` - **Docker Compose:** `/docker-compose.yml` +## ClickHouse Engine Support + +### Implementation Details + +The ClickHouse engine was added to support ClickHouse SQL parsing and code generation using the `github.com/AfterShip/clickhouse-sql-parser` library. + +**Files involved:** +- `internal/engine/clickhouse/parse.go` - Parser implementation +- `internal/engine/clickhouse/convert.go` - AST converter +- `internal/engine/clickhouse/catalog.go` - Catalog initialization +- `internal/config/config.go` - Engine constant registration +- `internal/compiler/engine.go` - Compiler integration +- `internal/compiler/expand.go` - Quote character handling +- `examples/clickhouse/` - Example project + +**Key features:** +- Parses ClickHouse SQL into sqlc's internal AST +- Supports comment syntax: `--` and `/* */` +- Uses backticks for identifier quoting (ClickHouse standard) +- Handles ClickHouse-specific keywords + +**Testing:** +```bash +# Test ClickHouse engine (unit tests) +go test ./internal/engine/clickhouse -v + +# Test compiler integration +go test ./internal/compiler -v -run TestNewCompilerClickHouse + +# Test code generation +cd examples/clickhouse && /path/to/sqlc generate && go mod init github.com/example/clickhouse && go mod tidy && go build ./gen +``` + +**Test Coverage:** +- Basic SELECT queries with WHERE, ORDER BY, LIMIT +- INSERT statements with VALUES +- Aggregate queries (COUNT, SUM, AVG) with GROUP BY and HAVING +- UNION and UNION ALL queries +- Subqueries and derived tables +- Multiple JOINs (INNER, LEFT, RIGHT, FULL) +- Window functions with OVER clause +- CAST expressions +- CASE expressions +- IS NULL / IS NOT NULL expressions +- Unary expressions (NOT, negation) +- Number literals (int, float, scientific notation) +- String literals and functions +- ClickHouse-specific features (PREWHERE, SAMPLE, array functions) + ## Recent Fixes & Improvements ### Fixed Issues @@ -296,13 +346,15 @@ git push --force-with-lease origin 1. **Typo in create_function_stmt.go** - Fixed "Undertand" → "Understand" 2. **Race condition in vet.go** - Fixed Client initialization using `sync.Once` 3. **Nil pointer dereference in parse.go** - Fixed unsafe type assertion in primary key parsing +4. **ClickHouse engine addition** - Added full ClickHouse SQL support with AST parsing These fixes demonstrate common patterns: - Using `sync.Once` for thread-safe lazy initialization - Using comma-ok idiom for safe type assertions: `if val, ok := x.(Type); ok { ... }` - Adding proper nil checks and defensive programming +- Registering new database engines following the established pattern --- -**Last Updated:** 2025-10-21 +**Last Updated:** 2025-11-19 **Maintainer:** Claude Code diff --git a/Makefile b/Makefile index b8745e57dc..78dfb4cea6 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ test: go test ./... test-managed: - MYSQL_SERVER_URI="invalid" POSTGRESQL_SERVER_URI="postgres://postgres:mysecretpassword@localhost:5432/postgres" go test -v ./... + MYSQL_SERVER_URI="invalid" POSTGRESQL_SERVER_URI="postgres://postgres:mysecretpassword@localhost:5432/postgres" CLICKHOUSE_SERVER_URI="localhost:9000" go test ./... vet: go vet ./... diff --git a/docker-compose.yml b/docker-compose.yml index f318d1ed93..b0cbdcde34 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,3 +19,10 @@ services: POSTGRES_DB: postgres POSTGRES_PASSWORD: mysecretpassword POSTGRES_USER: postgres + + clickhouse: + image: "clickhouse:lts" + ports: + - "9000:9000" + - "8123:8123" + restart: always diff --git a/docs/index.rst b/docs/index.rst index f914f3ec41..06964d1cbc 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -35,6 +35,7 @@ code ever again. tutorials/getting-started-mysql.md tutorials/getting-started-postgresql.md tutorials/getting-started-sqlite.md + tutorials/getting-started-clickhouse.md .. toctree:: :maxdepth: 2 diff --git a/docs/reference/config.md b/docs/reference/config.md index ff8bcd0890..c61c4e910e 100644 --- a/docs/reference/config.md +++ b/docs/reference/config.md @@ -37,7 +37,7 @@ Each mapping in the `sql` collection has the following keys: - `name`: - An human-friendly identifier for this query set. Optional. - `engine`: - - One of `postgresql`, `mysql` or `sqlite`. + - One of `postgresql`, `mysql`, `sqlite` or `clickhouse`. - `schema`: - Directory of SQL migrations or path to single SQL file; or a list of paths. - `queries`: @@ -134,7 +134,8 @@ The `gen` mapping supports the following keys: - `out`: - Output directory for generated code. - `sql_package`: - - Either `pgx/v4`, `pgx/v5` or `database/sql`. Defaults to `database/sql`. + - Either `pgx/v4`, `pgx/v5`, `database/sql` or `clickhouse/v2`. Defaults to `database/sql`. + - For ClickHouse, use `clickhouse/v2` for the native driver or omit for `database/sql` compatibility. - `sql_driver`: - Either `github.com/jackc/pgx/v4`, `github.com/jackc/pgx/v5`, `github.com/lib/pq` or `github.com/go-sql-driver/mysql`. No defaults. Required if query annotation `:copyfrom` is used. - `emit_db_tags`: @@ -158,7 +159,7 @@ The `gen` mapping supports the following keys: - `emit_methods_with_db_argument`: - If true, generated methods will accept a DBTX argument instead of storing a DBTX on the `*Queries` struct. Defaults to `false`. - `emit_pointers_for_null_types`: - - If true, generated types for nullable columns are emitted as pointers (ie. `*string`) instead of `database/sql` null types (ie. `NullString`). Currently only supported for PostgreSQL if `sql_package` is `pgx/v4` or `pgx/v5`, and for SQLite. Defaults to `false`. + - If true, generated types for nullable columns are emitted as pointers (ie. `*string`) instead of `database/sql` null types (ie. `NullString`). Currently only supported for PostgreSQL if `sql_package` is `pgx/v4` or `pgx/v5`, SQLite, and ClickHouse if `sql_package` is `clickhouse/v2`. Defaults to `false`. - `emit_enum_valid_method`: - If true, generate a Valid method on enum types, indicating whether a string is a valid enum value. diff --git a/docs/reference/datatypes.md b/docs/reference/datatypes.md index 14ceb42a3f..51efd172f4 100644 --- a/docs/reference/datatypes.md +++ b/docs/reference/datatypes.md @@ -8,8 +8,7 @@ If you're unsatisfied with the default, you can override any type using the ## Arrays -PostgreSQL [arrays](https://www.postgresql.org/docs/current/arrays.html) are -materialized as Go slices. +PostgreSQL [arrays](https://www.postgresql.org/docs/current/arrays.html) are materialized as Go slices. ```sql CREATE TABLE places ( @@ -27,6 +26,24 @@ type Place struct { } ``` +ClickHouse [array types](https://clickhouse.com/docs/en/sql-reference/data-types/array) are similarly mapped to Go slices: + +```sql +CREATE TABLE data ( + tags Array(String), + ids Array(UInt32) +); +``` + +```go +package db + +type Data struct { + Tags []string + IDs []uint32 +} +``` + ## Dates and times All date and time types are returned as `time.Time` structs. For @@ -60,6 +77,28 @@ type Author struct { } ``` +ClickHouse `DateTime` and `Date` types are also mapped to `time.Time`: + +```sql +CREATE TABLE events ( + created_at DateTime, + event_date Date +); +``` + +```go +package db + +import ( + "time" +) + +type Event struct { + CreatedAt time.Time + EventDate time.Time +} +``` + ## Enums PostgreSQL [enums](https://www.postgresql.org/docs/current/datatype-enum.html) are @@ -120,6 +159,46 @@ type Author struct { } ``` +ClickHouse uses `Nullable(T)` for nullable columns. When using the native `clickhouse/v2` +driver with `emit_pointers_for_null_types: true`, nullable fields are represented as +pointers. With `database/sql`, they use the standard `sql.Null*` types: + +```sql +CREATE TABLE articles ( + id UInt64, + title String, + bio Nullable(String) +); +``` + +With `clickhouse/v2` and `emit_pointers_for_null_types: true`: + +```go +package db + +type Article struct { + ID uint64 + Title string + Bio *string +} +``` + +With `database/sql`: + +```go +package db + +import ( + "database/sql" +) + +type Article struct { + ID uint64 + Title string + Bio sql.NullString +} +``` + ## UUIDs The Go standard library does not come with a `uuid` package. For UUID support, diff --git a/docs/reference/language-support.rst b/docs/reference/language-support.rst index 057a5ef65f..c619c9d83d 100644 --- a/docs/reference/language-support.rst +++ b/docs/reference/language-support.rst @@ -1,15 +1,15 @@ Database and language support ############################# -========== ======================= ============ ============ =============== -Language Plugin MySQL PostgreSQL SQLite -========== ======================= ============ ============ =============== -Go (built-in) Stable Stable Beta -Go `sqlc-gen-go`_ Stable Stable Beta -Kotlin `sqlc-gen-kotlin`_ Beta Beta Not implemented -Python `sqlc-gen-python`_ Beta Beta Not implemented -TypeScript `sqlc-gen-typescript`_ Beta Beta Not implemented -========== ======================= ============ ============ =============== +========== ======================= ============ ============ =============== =============== +Language Plugin MySQL PostgreSQL SQLite ClickHouse +========== ======================= ============ ============ =============== =============== +Go (built-in) Stable Stable Beta Beta +Go `sqlc-gen-go`_ Stable Stable Beta Beta +Kotlin `sqlc-gen-kotlin`_ Beta Beta Not implemented Not implemented +Python `sqlc-gen-python`_ Beta Beta Not implemented Not implemented +TypeScript `sqlc-gen-typescript`_ Beta Beta Not implemented Not implemented +========== ======================= ============ ============ =============== =============== Community language support ************************** diff --git a/docs/tutorials/getting-started-clickhouse.md b/docs/tutorials/getting-started-clickhouse.md new file mode 100644 index 0000000000..93c78c3f9a --- /dev/null +++ b/docs/tutorials/getting-started-clickhouse.md @@ -0,0 +1,184 @@ +# Getting started with ClickHouse + +This tutorial assumes that the latest version of sqlc is +[installed](../overview/install.md) and ready to use. + +We'll generate Go code here, but other +[language plugins](../reference/language-support.rst) are available. You'll +naturally need the Go toolchain if you want to build and run a program with the +code sqlc generates, but sqlc itself has no dependencies. + +## Setting up + +Create a new directory called `sqlc-tutorial` and open it up. + +Initialize a new Go module named `tutorial.sqlc.dev/app`: + +```shell +go mod init tutorial.sqlc.dev/app +``` + +sqlc looks for either a `sqlc.(yaml|yml)` or `sqlc.json` file in the current +directory. In our new directory, create a file named `sqlc.yaml` with the +following contents: + +```yaml +version: "2" +sql: + - engine: "clickhouse" + queries: "query.sql" + schema: "schema.sql" + gen: + go: + package: "tutorial" + out: "tutorial" + sql_package: "clickhouse/v2" +``` + +## Schema and queries + +sqlc needs to know your database schema and queries in order to generate code. +In the same directory, create a file named `schema.sql` with the following +content: + +```sql +CREATE TABLE authors ( + id UInt64, + name String, + bio String +) ENGINE = Memory; +``` + +Next, create a `query.sql` file with the following four queries: + +```sql +-- name: GetAuthor :one +SELECT * FROM authors +WHERE id = ? LIMIT 1; + +-- name: ListAuthors :many +SELECT * FROM authors +ORDER BY name; + +-- name: CreateAuthor :exec +INSERT INTO authors ( + id, name, bio +) VALUES ( + ?, ?, ? +); + +-- name: DeleteAuthor :exec +DELETE FROM authors +WHERE id = ?; +``` + +## Generating code + +You are now ready to generate code. You shouldn't see any output when you run +the `generate` subcommand, unless something goes wrong: + +```shell +sqlc generate +``` + +You should now have a `tutorial` subdirectory with three files containing Go +source code. These files comprise a Go package named `tutorial`: + +``` +├── go.mod +├── query.sql +├── schema.sql +├── sqlc.yaml +└── tutorial + ├── db.go + ├── models.go + └── query.sql.go +``` + +## Using generated code + +You can use your newly-generated `tutorial` package from any Go program. +Create a file named `tutorial.go` and add the following contents: + +```go +package main + +import ( + "context" + "log" + + "github.com/ClickHouse/clickhouse-go/v2" + + "tutorial.sqlc.dev/app/tutorial" +) + +func run() error { + ctx := context.Background() + + conn, err := clickhouse.Open(&clickhouse.Options{ + Addr: []string{"localhost:9000"}, + Auth: clickhouse.Auth{ + Database: "default", + Username: "default", + Password: "", + }, + }) + if err != nil { + return err + } + defer conn.Close() + + queries := tutorial.New(conn) + + // list all authors + authors, err := queries.ListAuthors(ctx) + if err != nil { + return err + } + log.Println(authors) + + // create an author + err = queries.CreateAuthor(ctx, tutorial.CreateAuthorParams{ + ID: 1, + Name: "Brian Kernighan", + Bio: "Co-author of The C Programming Language and The Go Programming Language", + }) + if err != nil { + return err + } + + // get the author we just inserted + fetchedAuthor, err := queries.GetAuthor(ctx, 1) + if err != nil { + return err + } + log.Println(fetchedAuthor) + + return nil +} + +func main() { + if err := run(); err != nil { + log.Fatal(err) + } +} +``` + +Before this code will compile you'll need to fetch the relevant ClickHouse driver: + +```shell +go get github.com/ClickHouse/clickhouse-go/v2 +go build ./... +``` + +The program should compile without errors. To make that possible, sqlc generates +readable, **idiomatic** Go code that you otherwise would've had to write +yourself. Take a look in `tutorial/query.sql.go`. + +Of course for this program to run successfully you'll need to compile after +replacing the database connection parameters in the call to `clickhouse.Open()` +with the correct parameters for your database. And your database must have the +`authors` table as defined in `schema.sql`. + +You should now have a working program using sqlc's generated Go source code, +and hopefully can see how you'd use sqlc in your own real-world applications. diff --git a/examples/clickhouse/README.md b/examples/clickhouse/README.md new file mode 100644 index 0000000000..84472899ad --- /dev/null +++ b/examples/clickhouse/README.md @@ -0,0 +1,146 @@ +# ClickHouse Example + +This example demonstrates using sqlc with ClickHouse for type-safe SQL query compilation and code generation. + +## Driver Support + +sqlc supports two modes for ClickHouse: + +1. **Native Driver** (`sql_package: "clickhouse/v2"`) - Uses the official ClickHouse Go driver with native protocol +2. **Database/SQL** (default) - Uses the standard library `database/sql` package + +This example uses the **native driver** for better performance and type support. See [USAGE.md](./USAGE.md) for detailed documentation on both modes. + +## Quick Start + +First, ensure you have sqlc installed: + +```bash +go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest +``` + +Then, from this directory, run: + +```bash +sqlc generate +``` + +This will generate Go code in the `gen/` directory based on the schema and queries. + +## Files + +- `schema.sql` - Table definitions for the example +- `queries.sql` - Named queries that sqlc will generate code for +- `sqlc.yaml` - Configuration file for sqlc +- `gen/` - Generated Go code (auto-generated by sqlc) + +## Schema + +The example schema includes: + +- `users` table - User information +- `posts` table - Posts created by users +- `comments` table - Comments on posts + +## Queries + +The example queries include: + +**Positional Parameters:** +- `GetUserByID` - Fetch a single user by ID +- `ListUsers` - List users with limit +- `InsertUser` - Insert a new user + +**Named Parameters (@name syntax):** +- `GetUserByIDNamed` - Fetch user using named parameter +- `ListUsersByStatusNamed` - List users with multiple named parameters +- `GetUserPostsForUser` - Get posts by user ID + +**Named Parameters (sqlc.arg() syntax):** +- `GetUserByEmail` - Fetch user by email +- `InsertUserNamed` - Insert with named parameters +- `GetUserWithPosts` - Get user with all their posts using JOIN + +**Advanced Features:** +- `InsertPost` - Insert post +- `GetCommentsForPost` - Get comments with ordering +- `GetPostsByOptionalStatus` - Nullable parameter handling with `sqlc.narg()` + +All queries demonstrate different parameter syntaxes and SQL features supported by ClickHouse in sqlc. + +## Parameter Syntax Support + +This example shows all three ways to use parameters with ClickHouse in sqlc: + +### 1. Positional Parameters (?) +```sql +SELECT * FROM users WHERE id = ?; +``` +Generated: `func (q *Queries) GetUserByID(ctx context.Context, dollar_1 interface{}) ...` + +### 2. Named Parameters (@name) +```sql +SELECT * FROM users WHERE id = @user_id AND status = @status; +``` +Generated: `func (q *Queries) GetUserByIDNamed(ctx context.Context, arg GetUserByIDNamedParams) ...` + +With struct: +```go +type GetUserByIDNamedParams struct { + UserID int32 + Status string +} +``` + +### 3. Function-Style Named Parameters +```sql +SELECT * FROM users WHERE email = sqlc.arg('email'); +SELECT * FROM users WHERE (sqlc.narg('status') IS NULL OR status = sqlc.narg('status')); +SELECT * FROM users WHERE id IN sqlc.slice('ids'); +``` + +**Recommendation:** Use named parameters (`@name` or `sqlc.arg()`) for better code readability and maintenance. + +## ClickHouse-Specific Features + +This example demonstrates several ClickHouse-specific SQL features: + +- **Table Engines**: Uses MergeTree family of table engines +- **ORDER BY**: Defines sort order for tables +- **Partitioning**: Tables are partitioned by primary key +- **Comments**: SQL-style (`--`) and C-style (`/* */`) comments +- **Named Parameters**: All three parameter syntaxes shown above + +## Type Resolution + +### Table Columns + +sqlc now properly resolves ClickHouse table column types to Go types: + +- `UInt32` → `uint32` (nullable: `sql.NullInt64`) +- `String` → `string` (nullable: `sql.NullString`) +- `DateTime` → `time.Time` (nullable: `sql.NullTime`) +- Other types are mapped following Go's `database/sql` conventions + +Generated table models include proper type information in struct fields. + +### Query Columns + +Query result columns are generated as `Column1`, `Column2`, etc. with `interface{}` types. This is a known limitation - proper column type resolution for SELECT queries requires either: + +1. Database-backed analysis with a ClickHouse connection +2. Complex semantic analysis of SELECT expressions + +To enable query column resolution in the future, you can: + +1. Run ClickHouse locally +2. Configure the analyzer in your `sqlc.yaml` +3. sqlc will then use `DESCRIBE` queries to resolve result column types + +## Notes + +- ClickHouse uses backticks for quoting identifiers +- ClickHouse supports `UInt32` and other specialized numeric types +- The `DateTime` type is used for timestamp columns +- MergeTree tables require an `ORDER BY` clause +- Columns are nullable by default - use database constraints if needed diff --git a/examples/clickhouse/USAGE.md b/examples/clickhouse/USAGE.md new file mode 100644 index 0000000000..4675b600f6 --- /dev/null +++ b/examples/clickhouse/USAGE.md @@ -0,0 +1,207 @@ +# ClickHouse Driver Usage + +The ClickHouse example demonstrates how to use sqlc with both the native ClickHouse driver and the standard `database/sql` package. + +## Driver Options + +### Native Driver (Recommended) + +The native `github.com/ClickHouse/clickhouse-go/v2` driver provides better performance and type support. + +**Configuration (`sqlc.yaml`):** +```yaml +version: "2" +sql: + - engine: "clickhouse" + queries: "queries.sql" + schema: "schema.sql" + gen: + go: + out: "gen" + package: "db" + sql_package: "clickhouse/v2" # Use native driver + emit_pointers_for_null_types: true # Use *T for nullable fields +``` + +**Usage:** +```go +package main + +import ( + "context" + "fmt" + "log" + + "github.com/ClickHouse/clickhouse-go/v2" + "your-project/gen" +) + +func main() { + conn, err := clickhouse.Open(&clickhouse.Options{ + Addr: []string{"localhost:9000"}, + Auth: clickhouse.Auth{ + Database: "default", + Username: "default", + Password: "", + }, + }) + if err != nil { + log.Fatal(err) + } + defer conn.Close() + + queries := db.New(conn) + + // Use the generated methods + ctx := context.Background() + user, err := queries.GetUser(ctx, 1) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("User: %s (%s)\n", *user.Name, *user.Email) +} +``` + +**Generated Code:** +```go +// db.go +type DBTX interface { + Exec(ctx context.Context, query string, args ...any) error + Query(ctx context.Context, query string, args ...any) (driver.Rows, error) + QueryRow(ctx context.Context, query string, args ...any) driver.Row +} + +// models.go +type SqlcExampleUser struct { + ID *uint32 // Pointer type for nullable field + Name *string + Email *string + CreatedAt *time.Time +} +``` + +### Database/SQL (Standard Library) + +Use the standard `database/sql` package for compatibility. + +**Configuration (`sqlc.yaml`):** +```yaml +version: "2" +sql: + - engine: "clickhouse" + queries: "queries.sql" + schema: "schema.sql" + gen: + go: + out: "gen" + package: "db" + # sql_package defaults to "database/sql" when not specified +``` + +**Usage:** +```go +package main + +import ( + "context" + "database/sql" + "fmt" + "log" + + _ "github.com/ClickHouse/clickhouse-go/v2" + "your-project/gen" +) + +func main() { + db, err := sql.Open("clickhouse", "clickhouse://localhost:9000/default") + if err != nil { + log.Fatal(err) + } + defer db.Close() + + queries := db.New(db) + + // Use the generated methods + ctx := context.Background() + user, err := queries.GetUser(ctx, 1) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("User: %s (%s)\n", user.Name.String, user.Email.String) +} +``` + +**Generated Code:** +```go +// db.go +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 +} + +// models.go +type SqlcExampleUser struct { + ID sql.NullInt64 // sql.Null* types for nullable fields + Name sql.NullString + Email sql.NullString + CreatedAt sql.NullTime +} +``` + +## Key Differences + +| Feature | Native Driver (`clickhouse/v2`) | Database/SQL | +|---------|--------------------------------|--------------| +| **Import** | `github.com/ClickHouse/clickhouse-go/v2` | `database/sql` | +| **Connection** | `clickhouse.Open()` | `sql.Open("clickhouse", dsn)` | +| **Method Signatures** | `Query(ctx, query, args...)` | `QueryContext(ctx, query, args...)` | +| **Null Types** | `*string`, `*int32` (with `emit_pointers_for_null_types`) | `sql.NullString`, `sql.NullInt32` | +| **Performance** | Better (native protocol) | Standard (uses driver internally) | +| **Type Safety** | Better ClickHouse type mapping | Generic sql.Null* types | + +## Recommendations + +- **Use native driver** (`clickhouse/v2`) for new projects - better performance and type support +- **Enable** `emit_pointers_for_null_types: true` for cleaner nullable field handling +- **Use database/sql** only if you need compatibility with generic SQL tooling + +## Example Schema + +```sql +CREATE TABLE IF NOT EXISTS sqlc_example.users ( + id UInt32, + name String, + email String, + created_at DateTime +) ENGINE = MergeTree() +ORDER BY id; +``` + +## Example Query + +```sql +-- name: GetUser :one +SELECT id, name, email, created_at +FROM sqlc_example.users +WHERE id = ?; + +-- name: CreateUser :exec +INSERT INTO sqlc_example.users (id, name, email, created_at) +VALUES (?, ?, ?, ?); +``` + +## Testing + +The generated code can be tested with the ClickHouse server: + +```bash +# Start ClickHouse (using Docker) +docker run -d --name clickhouse -p 9000:9000 clickhouse/clickhouse-server + +# Run your application +go run main.go +``` diff --git a/examples/clickhouse/gen/db.go b/examples/clickhouse/gen/db.go new file mode 100644 index 0000000000..4a3d37c44d --- /dev/null +++ b/examples/clickhouse/gen/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +import ( + "context" + + "github.com/ClickHouse/clickhouse-go/v2/lib/driver" +) + +type DBTX interface { + Exec(ctx context.Context, query string, args ...any) error + Query(ctx context.Context, query string, args ...any) (driver.Rows, error) + QueryRow(ctx context.Context, query string, args ...any) driver.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx driver.Conn) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/examples/clickhouse/gen/models.go b/examples/clickhouse/gen/models.go new file mode 100644 index 0000000000..39f3b11495 --- /dev/null +++ b/examples/clickhouse/gen/models.go @@ -0,0 +1,146 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +import ( + "net/netip" + "time" +) + +type SqlcExampleComment struct { + ID uint32 + PostID uint32 + UserID uint32 + Content string + CreatedAt time.Time +} + +type SqlcExampleConfigDatum struct { + ID uint32 + Settings map[string]string + Metrics map[string]float64 + Tags map[string][]string + CreatedAt time.Time +} + +type SqlcExampleEvent struct { + ID uint32 + Amount float64 + CreatedAt time.Time + Status string + PlatformID *string + JsonValue *string +} + +type SqlcExampleEventsWithProperty struct { + EventID uint32 + EventName string + Timestamp time.Time + Properties string +} + +type SqlcExampleFeatureUsage struct { + EventID uint32 + Timestamp time.Time + PlatformID uint32 + FeatureID string + UserCount uint32 + UsageCount uint64 +} + +type SqlcExampleMetric struct { + Category string + Value float64 + ValueX float64 + ValueY float64 + Timestamp time.Time +} + +type SqlcExampleNestedTable struct { + RecordID uint32 + NestedArray []string +} + +type SqlcExampleNetworkLog struct { + ID uint32 + SourceIp netip.Addr + DestIp netip.Addr + SourceIpv6 *netip.Addr + DestIpv6 *netip.Addr + Timestamp time.Time + BytesSent uint64 +} + +type SqlcExampleOrder struct { + OrderID uint32 + CustomerName string + Amount float64 + CreatedAt time.Time +} + +type SqlcExampleOrderMetric struct { + Status string + Amount float64 + Rating *float64 + CreatedAt time.Time +} + +type SqlcExamplePlatformCreatedEvent struct { + EventID uint32 + Timestamp time.Time + PlatformID uint32 + PlatformName string + Region string +} + +type SqlcExamplePlatformRenamedEvent struct { + EventID uint32 + Timestamp time.Time + PlatformID uint32 + OldName string + NewName string + Region string +} + +type SqlcExamplePost struct { + ID uint32 + UserID uint32 + Title string + Content string + Status *string + CreatedAt time.Time +} + +type SqlcExampleProduct struct { + ProductID uint32 + Name string + Categories []string +} + +type SqlcExampleShipment struct { + ShipmentID uint32 + OrderID uint32 + Address string + ShippedAt time.Time +} + +type SqlcExampleTimeseries struct { + Date time.Time + MetricValue float64 +} + +type SqlcExampleUser struct { + ID uint32 + Name string + Email string + Status string + CreatedAt time.Time +} + +type SqlcExampleUsersWithTag struct { + ID uint32 + Name string + Tags []string +} diff --git a/examples/clickhouse/gen/queries.sql.go b/examples/clickhouse/gen/queries.sql.go new file mode 100644 index 0000000000..b80be9ccb5 --- /dev/null +++ b/examples/clickhouse/gen/queries.sql.go @@ -0,0 +1,1264 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: queries.sql + +package db + +import ( + "context" + "net/netip" + "time" +) + +const analyzeArrayElements = `-- name: AnalyzeArrayElements :many +SELECT + product_id, + arrayJoin(categories) AS category, + COUNT(*) OVER (PARTITION BY category) as category_count +FROM sqlc_example.products +WHERE product_id = ? +GROUP BY product_id, category; +` + +type AnalyzeArrayElementsRow struct { + ProductID uint32 + Category string + CategoryCount uint64 +} + +func (q *Queries) AnalyzeArrayElements(ctx context.Context, productID uint32) ([]AnalyzeArrayElementsRow, error) { + rows, err := q.db.Query(ctx, analyzeArrayElements, productID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []AnalyzeArrayElementsRow + for rows.Next() { + var i AnalyzeArrayElementsRow + if err := rows.Scan(&i.ProductID, &i.Category, &i.CategoryCount); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const extractMetadataFromJSON = `-- name: ExtractMetadataFromJSON :many +SELECT + id, + arrayJoin(JSONExtract(json_value, 'Array(String)')) as metadata_value +FROM sqlc_example.events; +` + +type ExtractMetadataFromJSONRow struct { + ID uint32 + MetadataValue string +} + +func (q *Queries) ExtractMetadataFromJSON(ctx context.Context) ([]ExtractMetadataFromJSONRow, error) { + rows, err := q.db.Query(ctx, extractMetadataFromJSON) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ExtractMetadataFromJSONRow + for rows.Next() { + var i ExtractMetadataFromJSONRow + if err := rows.Scan(&i.ID, &i.MetadataValue); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const filterByIPRange = `-- name: FilterByIPRange :many +SELECT id, source_ip, dest_ip, timestamp, bytes_sent +FROM sqlc_example.network_logs +WHERE source_ip >= $1 AND source_ip <= $2 +ORDER BY timestamp DESC; + +` + +type FilterByIPRangeParams struct { + MinIp netip.Addr + MaxIp netip.Addr +} + +type FilterByIPRangeRow struct { + ID uint32 + SourceIp netip.Addr + DestIp netip.Addr + Timestamp time.Time + BytesSent uint64 +} + +// Complex CTE with UNION query (tests UNION inside unnamed subqueries) +func (q *Queries) FilterByIPRange(ctx context.Context, arg FilterByIPRangeParams) ([]FilterByIPRangeRow, error) { + rows, err := q.db.Query(ctx, filterByIPRange, arg.MinIp, arg.MaxIp) + if err != nil { + return nil, err + } + defer rows.Close() + var items []FilterByIPRangeRow + for rows.Next() { + var i FilterByIPRangeRow + if err := rows.Scan( + &i.ID, + &i.SourceIp, + &i.DestIp, + &i.Timestamp, + &i.BytesSent, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const filterUsersByIDAndStatus = `-- name: FilterUsersByIDAndStatus :many +SELECT id, name, email, status, created_at +FROM sqlc_example.users +WHERE id IN ($1) +AND status IN ('active', 'pending') +ORDER BY created_at DESC; + +` + +// ORDER BY with WITH FILL for time series +func (q *Queries) FilterUsersByIDAndStatus(ctx context.Context, userIds []uint32) ([]SqlcExampleUser, error) { + rows, err := q.db.Query(ctx, filterUsersByIDAndStatus, userIds) + if err != nil { + return nil, err + } + defer rows.Close() + var items []SqlcExampleUser + for rows.Next() { + var i SqlcExampleUser + if err := rows.Scan( + &i.ID, + &i.Name, + &i.Email, + &i.Status, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getCastedValues = `-- name: GetCastedValues :many +SELECT + id::String as id_text, + amount::Float32 as amount_float, + created_at::Date as date_only, + status::String as status_text +FROM sqlc_example.events +WHERE created_at::Date >= $1; + +` + +type GetCastedValuesRow struct { + IDText string + AmountFloat float32 + DateOnly time.Time + StatusText string +} + +// ARRAY JOIN examples +func (q *Queries) GetCastedValues(ctx context.Context, dateFilter time.Time) ([]GetCastedValuesRow, error) { + rows, err := q.db.Query(ctx, getCastedValues, dateFilter) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetCastedValuesRow + for rows.Next() { + var i GetCastedValuesRow + if err := rows.Scan( + &i.IDText, + &i.AmountFloat, + &i.DateOnly, + &i.StatusText, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getCommentAnalytics = `-- name: GetCommentAnalytics :many +SELECT + p.id as post_id, + p.title, + COUNT(*) as total_comments, + uniqExact(c.user_id) as unique_commenters, + countIf(c.created_at >= toDate(now()) - 7) as comments_last_week, + argMin(c.created_at, c.id) as first_comment_time, + argMax(c.created_at, c.id) as last_comment_time +FROM sqlc_example.posts p +LEFT JOIN sqlc_example.comments c ON p.id = c.post_id +WHERE p.user_id = $1 +GROUP BY p.id, p.title +ORDER BY total_comments DESC; + +` + +type GetCommentAnalyticsRow struct { + PostID uint32 + Title string + TotalComments uint64 + UniqueCommenters uint64 + CommentsLastWeek uint64 + FirstCommentTime time.Time + LastCommentTime time.Time +} + +// Statistical aggregate functions +func (q *Queries) GetCommentAnalytics(ctx context.Context, userID uint32) ([]GetCommentAnalyticsRow, error) { + rows, err := q.db.Query(ctx, getCommentAnalytics, userID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetCommentAnalyticsRow + for rows.Next() { + var i GetCommentAnalyticsRow + if err := rows.Scan( + &i.PostID, + &i.Title, + &i.TotalComments, + &i.UniqueCommenters, + &i.CommentsLastWeek, + &i.FirstCommentTime, + &i.LastCommentTime, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getCommentsForPost = `-- name: GetCommentsForPost :many +SELECT id, post_id, user_id, content, created_at +FROM sqlc_example.comments +WHERE post_id = $1 +ORDER BY created_at ASC; +` + +func (q *Queries) GetCommentsForPost(ctx context.Context, postID uint32) ([]SqlcExampleComment, error) { + rows, err := q.db.Query(ctx, getCommentsForPost, postID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []SqlcExampleComment + for rows.Next() { + var i SqlcExampleComment + if err := rows.Scan( + &i.ID, + &i.PostID, + &i.UserID, + &i.Content, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getConfigData = `-- name: GetConfigData :one +SELECT id, settings, metrics, tags, created_at +FROM sqlc_example.config_data +WHERE id = ?; +` + +func (q *Queries) GetConfigData(ctx context.Context, id uint32) (SqlcExampleConfigDatum, error) { + row := q.db.QueryRow(ctx, getConfigData, id) + var i SqlcExampleConfigDatum + err := row.Scan( + &i.ID, + &i.Settings, + &i.Metrics, + &i.Tags, + &i.CreatedAt, + ) + return i, err +} + +const getConfigSettings = `-- name: GetConfigSettings :many +SELECT id, settings, created_at +FROM sqlc_example.config_data +WHERE id > $1 +ORDER BY id; +` + +type GetConfigSettingsRow struct { + ID uint32 + Settings map[string]string + CreatedAt time.Time +} + +func (q *Queries) GetConfigSettings(ctx context.Context, minID uint32) ([]GetConfigSettingsRow, error) { + rows, err := q.db.Query(ctx, getConfigSettings, minID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetConfigSettingsRow + for rows.Next() { + var i GetConfigSettingsRow + if err := rows.Scan(&i.ID, &i.Settings, &i.CreatedAt); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getIPv6Connections = `-- name: GetIPv6Connections :many +SELECT id, source_ipv6, dest_ipv6, timestamp, bytes_sent +FROM sqlc_example.network_logs +WHERE source_ipv6 IS NOT NULL AND dest_ipv6 IS NOT NULL +ORDER BY timestamp DESC; +` + +type GetIPv6ConnectionsRow struct { + ID uint32 + SourceIpv6 *netip.Addr + DestIpv6 *netip.Addr + Timestamp time.Time + BytesSent uint64 +} + +func (q *Queries) GetIPv6Connections(ctx context.Context) ([]GetIPv6ConnectionsRow, error) { + rows, err := q.db.Query(ctx, getIPv6Connections) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetIPv6ConnectionsRow + for rows.Next() { + var i GetIPv6ConnectionsRow + if err := rows.Scan( + &i.ID, + &i.SourceIpv6, + &i.DestIpv6, + &i.Timestamp, + &i.BytesSent, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getMetricsStatistics = `-- name: GetMetricsStatistics :many +SELECT + category, + COUNT(*) as count, + varSamp(value) as variance_sample, + varPop(value) as variance_population, + stddevSamp(value) as stddev_sample, + stddevPop(value) as stddev_population, + corr(value_x, value_y) as correlation +FROM sqlc_example.metrics +WHERE timestamp >= $1 AND timestamp <= $2 +GROUP BY category +ORDER BY count DESC; + +` + +type GetMetricsStatisticsParams struct { + StartTime time.Time + EndTime time.Time +} + +type GetMetricsStatisticsRow struct { + Category string + Count uint64 + VarianceSample float64 + VariancePopulation float64 + StddevSample float64 + StddevPopulation float64 + Correlation float64 +} + +// Conditional aggregate variants +func (q *Queries) GetMetricsStatistics(ctx context.Context, arg GetMetricsStatisticsParams) ([]GetMetricsStatisticsRow, error) { + rows, err := q.db.Query(ctx, getMetricsStatistics, arg.StartTime, arg.EndTime) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetMetricsStatisticsRow + for rows.Next() { + var i GetMetricsStatisticsRow + if err := rows.Scan( + &i.Category, + &i.Count, + &i.VarianceSample, + &i.VariancePopulation, + &i.StddevSample, + &i.StddevPopulation, + &i.Correlation, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getNetworkLogBySourceIP = `-- name: GetNetworkLogBySourceIP :one +SELECT id, source_ip, dest_ip, source_ipv6, dest_ipv6, timestamp, bytes_sent +FROM sqlc_example.network_logs +WHERE source_ip = $1; +` + +func (q *Queries) GetNetworkLogBySourceIP(ctx context.Context, sourceIp netip.Addr) (SqlcExampleNetworkLog, error) { + row := q.db.QueryRow(ctx, getNetworkLogBySourceIP, sourceIp) + var i SqlcExampleNetworkLog + err := row.Scan( + &i.ID, + &i.SourceIp, + &i.DestIp, + &i.SourceIpv6, + &i.DestIpv6, + &i.Timestamp, + &i.BytesSent, + ) + return i, err +} + +const getNetworkLogsByDestIP = `-- name: GetNetworkLogsByDestIP :many +SELECT id, source_ip, dest_ip, timestamp, bytes_sent +FROM sqlc_example.network_logs +WHERE dest_ip = $1 +ORDER BY timestamp DESC; +` + +type GetNetworkLogsByDestIPRow struct { + ID uint32 + SourceIp netip.Addr + DestIp netip.Addr + Timestamp time.Time + BytesSent uint64 +} + +func (q *Queries) GetNetworkLogsByDestIP(ctx context.Context, destIp netip.Addr) ([]GetNetworkLogsByDestIPRow, error) { + rows, err := q.db.Query(ctx, getNetworkLogsByDestIP, destIp) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetNetworkLogsByDestIPRow + for rows.Next() { + var i GetNetworkLogsByDestIPRow + if err := rows.Scan( + &i.ID, + &i.SourceIp, + &i.DestIp, + &i.Timestamp, + &i.BytesSent, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getOrderMetrics = `-- name: GetOrderMetrics :many +SELECT + status, + COUNT(*) as total_orders, + minIf(amount, amount > 0) as min_positive_amount, + maxIf(amount, amount > 0) as max_positive_amount, + sumIf(amount, status = 'completed') as completed_revenue, + avgIf(rating, rating IS NOT NULL) as avg_valid_rating +FROM sqlc_example.order_metrics +WHERE created_at >= $1 +GROUP BY status +ORDER BY total_orders DESC; + +` + +type GetOrderMetricsRow struct { + Status string + TotalOrders uint64 + MinPositiveAmount float64 + MaxPositiveAmount float64 + CompletedRevenue float64 + AvgValidRating float64 +} + +// IN operator with multiple conditions +func (q *Queries) GetOrderMetrics(ctx context.Context, startDate time.Time) ([]GetOrderMetricsRow, error) { + rows, err := q.db.Query(ctx, getOrderMetrics, startDate) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetOrderMetricsRow + for rows.Next() { + var i GetOrderMetricsRow + if err := rows.Scan( + &i.Status, + &i.TotalOrders, + &i.MinPositiveAmount, + &i.MaxPositiveAmount, + &i.CompletedRevenue, + &i.AvgValidRating, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getOrdersWithShipments = `-- name: GetOrdersWithShipments :many +SELECT order_id, customer_name, amount, created_at, shipment_id, address, shipped_at +FROM sqlc_example.orders +LEFT JOIN sqlc_example.shipments USING (order_id) +ORDER BY created_at DESC; +` + +type GetOrdersWithShipmentsRow struct { + OrderID uint32 + CustomerName string + Amount float64 + CreatedAt time.Time + ShipmentID *uint32 + Address *string + ShippedAt *time.Time +} + +func (q *Queries) GetOrdersWithShipments(ctx context.Context) ([]GetOrdersWithShipmentsRow, error) { + rows, err := q.db.Query(ctx, getOrdersWithShipments) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetOrdersWithShipmentsRow + for rows.Next() { + var i GetOrdersWithShipmentsRow + if err := rows.Scan( + &i.OrderID, + &i.CustomerName, + &i.Amount, + &i.CreatedAt, + &i.ShipmentID, + &i.Address, + &i.ShippedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getPlatformHistory = `-- name: GetPlatformHistory :many +WITH platform_info AS ( + SELECT platform_id, + argMax(platform_name, timestamp) as platform_name, + argMax(region, timestamp) as region + FROM ( + SELECT timestamp, platform_id, platform_name, region + FROM sqlc_example.platform_created_event + + UNION ALL + + SELECT timestamp, platform_id, new_name as platform_name, region + FROM sqlc_example.platform_renamed_event + ) AS platform_events + GROUP BY platform_id +), usage_summary AS ( + SELECT feature_usage.platform_id, feature_id, + COUNT(*) AS update_count, + uniqExact(user_count) AS unique_user_counts, + SUM(usage_count) AS total_usage + FROM sqlc_example.feature_usage + WHERE feature_usage.platform_id = $1 + GROUP BY feature_usage.platform_id, feature_id + ORDER BY total_usage DESC +) + +SELECT + u.platform_id, + u.feature_id, + u.update_count, + u.unique_user_counts, + u.total_usage, + p.platform_name, + p.region +FROM usage_summary u +LEFT JOIN platform_info p ON u.platform_id = p.platform_id; + +` + +type GetPlatformHistoryRow struct { + PlatformID uint32 + FeatureID string + UpdateCount uint64 + UniqueUserCounts uint64 + TotalUsage uint64 + PlatformName *string + Region *string +} + +// LEFT JOIN USING clause test +// This ensures that USING clause returns only one column (not duplicated) +// ClickHouse naturally returns a single column for the shared key with USING +// After sqlc expansion, this should generate: order_id, customer_name, amount, created_at, shipment_id, address, shipped_at +// NOT: order_id, customer_name, amount, created_at, order_id, shipment_id, address, shipped_at (with order_id duplicated) +func (q *Queries) GetPlatformHistory(ctx context.Context, platformID uint32) ([]GetPlatformHistoryRow, error) { + rows, err := q.db.Query(ctx, getPlatformHistory, platformID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetPlatformHistoryRow + for rows.Next() { + var i GetPlatformHistoryRow + if err := rows.Scan( + &i.PlatformID, + &i.FeatureID, + &i.UpdateCount, + &i.UniqueUserCounts, + &i.TotalUsage, + &i.PlatformName, + &i.Region, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getPostsByOptionalStatus = `-- name: GetPostsByOptionalStatus :many +SELECT id, user_id, title, status, created_at +FROM sqlc_example.posts +WHERE ($1 IS NULL OR status = $1) +ORDER BY created_at DESC; + +` + +type GetPostsByOptionalStatusRow struct { + ID uint32 + UserID uint32 + Title string + Status *string + CreatedAt time.Time +} + +// ClickHouse-specific aggregate functions +func (q *Queries) GetPostsByOptionalStatus(ctx context.Context, status *string) ([]GetPostsByOptionalStatusRow, error) { + rows, err := q.db.Query(ctx, getPostsByOptionalStatus, status) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetPostsByOptionalStatusRow + for rows.Next() { + var i GetPostsByOptionalStatusRow + if err := rows.Scan( + &i.ID, + &i.UserID, + &i.Title, + &i.Status, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getTimeSeriesWithFill = `-- name: GetTimeSeriesWithFill :many +SELECT date, metric_value +FROM sqlc_example.timeseries +WHERE date >= $1 AND date <= $2 +ORDER BY date WITH FILL FROM sqlc.arg('start_date') TO sqlc.arg('end_date'); + +` + +type GetTimeSeriesWithFillParams struct { + StartDate time.Time + EndDate time.Time +} + +// Type casting examples +func (q *Queries) GetTimeSeriesWithFill(ctx context.Context, arg GetTimeSeriesWithFillParams) ([]SqlcExampleTimeseries, error) { + rows, err := q.db.Query(ctx, getTimeSeriesWithFill, arg.StartDate, arg.EndDate) + if err != nil { + return nil, err + } + defer rows.Close() + var items []SqlcExampleTimeseries + for rows.Next() { + var i SqlcExampleTimeseries + if err := rows.Scan(&i.Date, &i.MetricValue); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getUserAnalytics = `-- name: GetUserAnalytics :many +SELECT + u.id, + u.name, + COUNT(*) as total_posts, + uniqExact(p.id) as unique_posts, + countIf(p.created_at >= toDate(now()) - 30) as posts_last_30_days, + argMax(p.title, p.created_at) as latest_post_title, + argMaxIf(p.title, p.created_at, p.created_at >= toDate(now()) - 30) as latest_post_in_30_days +FROM sqlc_example.users u +LEFT JOIN sqlc_example.posts p ON u.id = p.user_id +GROUP BY u.id, u.name +HAVING COUNT(*) > 0 +ORDER BY total_posts DESC; +` + +type GetUserAnalyticsRow struct { + ID uint32 + Name string + TotalPosts uint64 + UniquePosts uint64 + PostsLast30Days uint64 + LatestPostTitle string + LatestPostIn30Days string +} + +func (q *Queries) GetUserAnalytics(ctx context.Context) ([]GetUserAnalyticsRow, error) { + rows, err := q.db.Query(ctx, getUserAnalytics) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetUserAnalyticsRow + for rows.Next() { + var i GetUserAnalyticsRow + if err := rows.Scan( + &i.ID, + &i.Name, + &i.TotalPosts, + &i.UniquePosts, + &i.PostsLast30Days, + &i.LatestPostTitle, + &i.LatestPostIn30Days, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getUserByEmail = `-- name: GetUserByEmail :one +SELECT id, name, email, created_at +FROM sqlc_example.users +WHERE email = $1; +` + +type GetUserByEmailRow struct { + ID uint32 + Name string + Email string + CreatedAt time.Time +} + +func (q *Queries) GetUserByEmail(ctx context.Context, email string) (GetUserByEmailRow, error) { + row := q.db.QueryRow(ctx, getUserByEmail, email) + var i GetUserByEmailRow + err := row.Scan( + &i.ID, + &i.Name, + &i.Email, + &i.CreatedAt, + ) + return i, err +} + +const getUserByID = `-- name: GetUserByID :one +SELECT id, name, email, created_at +FROM sqlc_example.users +WHERE id = ?; +` + +type GetUserByIDRow struct { + ID uint32 + Name string + Email string + CreatedAt time.Time +} + +func (q *Queries) GetUserByID(ctx context.Context, id uint32) (GetUserByIDRow, error) { + row := q.db.QueryRow(ctx, getUserByID, id) + var i GetUserByIDRow + err := row.Scan( + &i.ID, + &i.Name, + &i.Email, + &i.CreatedAt, + ) + return i, err +} + +const getUserPostsForUser = `-- name: GetUserPostsForUser :many +SELECT p.id, p.user_id, p.title, p.content, p.created_at +FROM sqlc_example.posts p +WHERE p.user_id = $1 +ORDER BY p.created_at DESC; +` + +type GetUserPostsForUserRow struct { + ID uint32 + UserID uint32 + Title string + Content string + CreatedAt time.Time +} + +func (q *Queries) GetUserPostsForUser(ctx context.Context, userID uint32) ([]GetUserPostsForUserRow, error) { + rows, err := q.db.Query(ctx, getUserPostsForUser, userID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetUserPostsForUserRow + for rows.Next() { + var i GetUserPostsForUserRow + if err := rows.Scan( + &i.ID, + &i.UserID, + &i.Title, + &i.Content, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getUserWithPosts = `-- name: GetUserWithPosts :many +SELECT u.id, u.name, u.email, u.created_at, p.id as post_id, p.title +FROM sqlc_example.users u +LEFT JOIN sqlc_example.posts p ON u.id = p.user_id +WHERE u.id = $1 +ORDER BY p.created_at DESC; + +` + +type GetUserWithPostsRow struct { + ID uint32 + Name string + Email string + CreatedAt time.Time + PostID *uint32 + Title *string +} + +// Named parameter with nullable values using sqlc.narg() +func (q *Queries) GetUserWithPosts(ctx context.Context, userID uint32) ([]GetUserWithPostsRow, error) { + rows, err := q.db.Query(ctx, getUserWithPosts, userID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetUserWithPostsRow + for rows.Next() { + var i GetUserWithPostsRow + if err := rows.Scan( + &i.ID, + &i.Name, + &i.Email, + &i.CreatedAt, + &i.PostID, + &i.Title, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const insertComment = `-- name: InsertComment :exec +INSERT INTO sqlc_example.comments (id, post_id, user_id, content, created_at) +VALUES (?, ?, ?, ?, ?); +` + +type InsertCommentParams struct { + ID uint32 + PostID uint32 + UserID uint32 + Content string + CreatedAt time.Time +} + +func (q *Queries) InsertComment(ctx context.Context, arg InsertCommentParams) error { + err := q.db.Exec(ctx, insertComment, + arg.ID, + arg.PostID, + arg.UserID, + arg.Content, + arg.CreatedAt, + ) + return err +} + +const insertConfigData = `-- name: InsertConfigData :exec +INSERT INTO sqlc_example.config_data (id, settings, metrics, tags, created_at) +VALUES (?, ?, ?, ?, ?); + +` + +type InsertConfigDataParams struct { + ID uint32 + Settings map[string]string + Metrics map[string]float64 + Tags map[string][]string + CreatedAt time.Time +} + +// IP address type examples +func (q *Queries) InsertConfigData(ctx context.Context, arg InsertConfigDataParams) error { + err := q.db.Exec(ctx, insertConfigData, + arg.ID, + arg.Settings, + arg.Metrics, + arg.Tags, + arg.CreatedAt, + ) + return err +} + +const insertNetworkLog = `-- name: InsertNetworkLog :exec +INSERT INTO sqlc_example.network_logs (id, source_ip, dest_ip, source_ipv6, dest_ipv6, timestamp, bytes_sent) +VALUES ($1, $2, $3, $4, $5, $6, $7); +` + +type InsertNetworkLogParams struct { + ID uint32 + SourceIp netip.Addr + DestIp netip.Addr + SourceIpv6 *netip.Addr + DestIpv6 *netip.Addr + Timestamp time.Time + BytesSent uint64 +} + +func (q *Queries) InsertNetworkLog(ctx context.Context, arg InsertNetworkLogParams) error { + err := q.db.Exec(ctx, insertNetworkLog, + arg.ID, + arg.SourceIp, + arg.DestIp, + arg.SourceIpv6, + arg.DestIpv6, + arg.Timestamp, + arg.BytesSent, + ) + return err +} + +const insertPost = `-- name: InsertPost :exec +INSERT INTO sqlc_example.posts (id, user_id, title, content, created_at) +VALUES ($1, $2, $3, $4, $5); +` + +type InsertPostParams struct { + ID uint32 + UserID uint32 + Title string + Content string + CreatedAt time.Time +} + +func (q *Queries) InsertPost(ctx context.Context, arg InsertPostParams) error { + err := q.db.Exec(ctx, insertPost, + arg.ID, + arg.UserID, + arg.Title, + arg.Content, + arg.CreatedAt, + ) + return err +} + +const insertUser = `-- name: InsertUser :exec +INSERT INTO sqlc_example.users (id, name, email, created_at) +VALUES (?, ?, ?, ?); + +` + +type InsertUserParams struct { + ID uint32 + Name string + Email string + CreatedAt time.Time +} + +// Named parameter examples using sqlc.arg() function +func (q *Queries) InsertUser(ctx context.Context, arg InsertUserParams) error { + err := q.db.Exec(ctx, insertUser, + arg.ID, + arg.Name, + arg.Email, + arg.CreatedAt, + ) + return err +} + +const insertUserNamed = `-- name: InsertUserNamed :exec +INSERT INTO sqlc_example.users (id, name, email, created_at) +VALUES ($1, $2, $3, $4); +` + +type InsertUserNamedParams struct { + ID uint32 + Name string + Email string + CreatedAt time.Time +} + +func (q *Queries) InsertUserNamed(ctx context.Context, arg InsertUserNamedParams) error { + err := q.db.Exec(ctx, insertUserNamed, + arg.ID, + arg.Name, + arg.Email, + arg.CreatedAt, + ) + return err +} + +const listUsers = `-- name: ListUsers :many +SELECT id, name, email, created_at +FROM sqlc_example.users +ORDER BY created_at DESC +LIMIT ?; +` + +type ListUsersRow struct { + ID uint32 + Name string + Email string + CreatedAt time.Time +} + +func (q *Queries) ListUsers(ctx context.Context, limit int64) ([]ListUsersRow, error) { + rows, err := q.db.Query(ctx, listUsers, limit) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListUsersRow + for rows.Next() { + var i ListUsersRow + if err := rows.Scan( + &i.ID, + &i.Name, + &i.Email, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const unfoldEventProperties = `-- name: UnfoldEventProperties :many +SELECT + e.event_id, + e.event_name, + e.timestamp, + prop_key, + prop_value +FROM sqlc_example.events_with_properties e +ARRAY JOIN e.properties.keys AS prop_key, e.properties.values AS prop_value +WHERE e.timestamp >= $1 +ORDER BY e.timestamp DESC; +` + +type UnfoldEventPropertiesRow struct { + EventID uint32 + EventName string + Timestamp time.Time + PropKey string + PropValue string +} + +func (q *Queries) UnfoldEventProperties(ctx context.Context, startTime time.Time) ([]UnfoldEventPropertiesRow, error) { + rows, err := q.db.Query(ctx, unfoldEventProperties, startTime) + if err != nil { + return nil, err + } + defer rows.Close() + var items []UnfoldEventPropertiesRow + for rows.Next() { + var i UnfoldEventPropertiesRow + if err := rows.Scan( + &i.EventID, + &i.EventName, + &i.Timestamp, + &i.PropKey, + &i.PropValue, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const unfoldNestedData = `-- name: UnfoldNestedData :many +SELECT + record_id, + nested_value +FROM sqlc_example.nested_table +ARRAY JOIN nested_array AS nested_value +WHERE record_id IN ($1); +` + +type UnfoldNestedDataRow struct { + RecordID uint32 + NestedValue string +} + +func (q *Queries) UnfoldNestedData(ctx context.Context, recordIds []uint32) ([]UnfoldNestedDataRow, error) { + rows, err := q.db.Query(ctx, unfoldNestedData, recordIds) + if err != nil { + return nil, err + } + defer rows.Close() + var items []UnfoldNestedDataRow + for rows.Next() { + var i UnfoldNestedDataRow + if err := rows.Scan(&i.RecordID, &i.NestedValue); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const unfoldUserTags = `-- name: UnfoldUserTags :many +SELECT + u.id as user_id, + u.name as user_name, + tag +FROM sqlc_example.users_with_tags u +ARRAY JOIN u.tags AS tag +WHERE u.id = $1 +ORDER BY tag; +` + +type UnfoldUserTagsRow struct { + UserID uint32 + UserName string + Tag string +} + +func (q *Queries) UnfoldUserTags(ctx context.Context, userID uint32) ([]UnfoldUserTagsRow, error) { + rows, err := q.db.Query(ctx, unfoldUserTags, userID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []UnfoldUserTagsRow + for rows.Next() { + var i UnfoldUserTagsRow + if err := rows.Scan(&i.UserID, &i.UserName, &i.Tag); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/examples/clickhouse/go.mod b/examples/clickhouse/go.mod new file mode 100644 index 0000000000..d291c4336e --- /dev/null +++ b/examples/clickhouse/go.mod @@ -0,0 +1,21 @@ +module github.com/example/clickhouse + +go 1.25.4 + +require github.com/ClickHouse/clickhouse-go/v2 v2.41.0 + +require ( + github.com/ClickHouse/ch-go v0.69.0 // indirect + github.com/go-faster/city v1.0.1 // indirect + github.com/go-faster/errors v0.7.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/paulmach/orb v0.12.0 // indirect + github.com/pierrec/lz4/v4 v4.1.22 // indirect + github.com/segmentio/asm v1.2.1 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/sys v0.38.0 // indirect +) diff --git a/examples/clickhouse/go.sum b/examples/clickhouse/go.sum new file mode 100644 index 0000000000..4fe3777fb2 --- /dev/null +++ b/examples/clickhouse/go.sum @@ -0,0 +1,106 @@ +github.com/ClickHouse/ch-go v0.69.0 h1:nO0OJkpxOlN/eaXFj0KzjTz5p7vwP1/y3GN4qc5z/iM= +github.com/ClickHouse/ch-go v0.69.0/go.mod h1:9XeZpSAT4S0kVjOpaJ5186b7PY/NH/hhF8R6u0WIjwg= +github.com/ClickHouse/clickhouse-go/v2 v2.41.0 h1:JbLKMXLEkW0NMalMgI+GYb6FVZtpaMVEzQa/HC1ZMRE= +github.com/ClickHouse/clickhouse-go/v2 v2.41.0/go.mod h1:/RoTHh4aDA4FOCIQggwsiOwO7Zq1+HxQ0inef0Au/7k= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw= +github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw= +github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg= +github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/paulmach/orb v0.12.0 h1:z+zOwjmG3MyEEqzv92UN49Lg1JFYx0L9GpGKNVDKk1s= +github.com/paulmach/orb v0.12.0/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU= +github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY= +github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= +github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= +github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= +github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/clickhouse/queries.sql b/examples/clickhouse/queries.sql new file mode 100644 index 0000000000..46367c88a9 --- /dev/null +++ b/examples/clickhouse/queries.sql @@ -0,0 +1,293 @@ +-- ClickHouse example queries +-- ClickHouse supports both positional (?) and named (sqlc.arg / sqlc.narg / sqlc.slice) parameters + +-- Positional parameter examples +-- name: GetUserByID :one +SELECT id, name, email, created_at +FROM sqlc_example.users +WHERE id = ?; + +-- name: ListUsers :many +SELECT id, name, email, created_at +FROM sqlc_example.users +ORDER BY created_at DESC +LIMIT ?; + +-- name: InsertUser :exec +INSERT INTO sqlc_example.users (id, name, email, created_at) +VALUES (?, ?, ?, ?); + +-- Named parameter examples using sqlc.arg() function +-- name: GetUserByEmail :one +SELECT id, name, email, created_at +FROM sqlc_example.users +WHERE email = sqlc.arg('email'); + +-- name: InsertUserNamed :exec +INSERT INTO sqlc_example.users (id, name, email, created_at) +VALUES (sqlc.arg('id'), sqlc.arg('name'), sqlc.arg('email'), sqlc.arg('created_at')); + +-- name: GetUserPostsForUser :many +SELECT p.id, p.user_id, p.title, p.content, p.created_at +FROM sqlc_example.posts p +WHERE p.user_id = sqlc.arg('user_id') +ORDER BY p.created_at DESC; + +-- name: InsertPost :exec +INSERT INTO sqlc_example.posts (id, user_id, title, content, created_at) +VALUES (sqlc.arg('id'), sqlc.arg('user_id'), sqlc.arg('title'), sqlc.arg('content'), sqlc.arg('created_at')); + +-- name: GetCommentsForPost :many +SELECT id, post_id, user_id, content, created_at +FROM sqlc_example.comments +WHERE post_id = sqlc.arg('post_id') +ORDER BY created_at ASC; + +-- name: InsertComment :exec +INSERT INTO sqlc_example.comments (id, post_id, user_id, content, created_at) +VALUES (?, ?, ?, ?, ?); + +-- name: GetUserWithPosts :many +SELECT u.id, u.name, u.email, u.created_at, p.id as post_id, p.title +FROM sqlc_example.users u +LEFT JOIN sqlc_example.posts p ON u.id = p.user_id +WHERE u.id = sqlc.arg('user_id') +ORDER BY p.created_at DESC; + +-- Named parameter with nullable values using sqlc.narg() +-- name: GetPostsByOptionalStatus :many +SELECT id, user_id, title, status, created_at +FROM sqlc_example.posts +WHERE (sqlc.narg('status') IS NULL OR status = sqlc.narg('status')) +ORDER BY created_at DESC; + +-- ClickHouse-specific aggregate functions + +-- name: GetUserAnalytics :many +SELECT + u.id, + u.name, + COUNT(*) as total_posts, + uniqExact(p.id) as unique_posts, + countIf(p.created_at >= toDate(now()) - 30) as posts_last_30_days, + argMax(p.title, p.created_at) as latest_post_title, + argMaxIf(p.title, p.created_at, p.created_at >= toDate(now()) - 30) as latest_post_in_30_days +FROM sqlc_example.users u +LEFT JOIN sqlc_example.posts p ON u.id = p.user_id +GROUP BY u.id, u.name +HAVING COUNT(*) > 0 +ORDER BY total_posts DESC; + +-- name: GetCommentAnalytics :many +SELECT + p.id as post_id, + p.title, + COUNT(*) as total_comments, + uniqExact(c.user_id) as unique_commenters, + countIf(c.created_at >= toDate(now()) - 7) as comments_last_week, + argMin(c.created_at, c.id) as first_comment_time, + argMax(c.created_at, c.id) as last_comment_time +FROM sqlc_example.posts p +LEFT JOIN sqlc_example.comments c ON p.id = c.post_id +WHERE p.user_id = sqlc.arg('user_id') +GROUP BY p.id, p.title +ORDER BY total_comments DESC; + +-- Statistical aggregate functions + +-- name: GetMetricsStatistics :many +SELECT + category, + COUNT(*) as count, + varSamp(value) as variance_sample, + varPop(value) as variance_population, + stddevSamp(value) as stddev_sample, + stddevPop(value) as stddev_population, + corr(value_x, value_y) as correlation +FROM sqlc_example.metrics +WHERE timestamp >= sqlc.arg('start_time') AND timestamp <= sqlc.arg('end_time') +GROUP BY category +ORDER BY count DESC; + +-- Conditional aggregate variants + +-- name: GetOrderMetrics :many +SELECT + status, + COUNT(*) as total_orders, + minIf(amount, amount > 0) as min_positive_amount, + maxIf(amount, amount > 0) as max_positive_amount, + sumIf(amount, status = 'completed') as completed_revenue, + avgIf(rating, rating IS NOT NULL) as avg_valid_rating +FROM sqlc_example.order_metrics +WHERE created_at >= sqlc.arg('start_date') +GROUP BY status +ORDER BY total_orders DESC; + +-- IN operator with multiple conditions + +-- name: FilterUsersByIDAndStatus :many +SELECT id, name, email, status, created_at +FROM sqlc_example.users +WHERE id IN (sqlc.slice('user_ids')) +AND status IN ('active', 'pending') +ORDER BY created_at DESC; + +-- ORDER BY with WITH FILL for time series + +-- name: GetTimeSeriesWithFill :many +SELECT date, metric_value +FROM sqlc_example.timeseries +WHERE date >= sqlc.arg('start_date') AND date <= sqlc.arg('end_date') +ORDER BY date WITH FILL FROM sqlc.arg('start_date') TO sqlc.arg('end_date'); + +-- Type casting examples + +-- name: GetCastedValues :many +SELECT + id::String as id_text, + amount::Float32 as amount_float, + created_at::Date as date_only, + status::String as status_text +FROM sqlc_example.events +WHERE created_at::Date >= sqlc.arg('date_filter'); + +-- ARRAY JOIN examples + +-- name: UnfoldUserTags :many +SELECT + u.id as user_id, + u.name as user_name, + tag +FROM sqlc_example.users_with_tags u +ARRAY JOIN u.tags AS tag +WHERE u.id = sqlc.arg('user_id') +ORDER BY tag; + +-- name: UnfoldEventProperties :many +SELECT + e.event_id, + e.event_name, + e.timestamp, + prop_key, + prop_value +FROM sqlc_example.events_with_properties e +ARRAY JOIN e.properties.keys AS prop_key, e.properties.values AS prop_value +WHERE e.timestamp >= sqlc.arg('start_time') +ORDER BY e.timestamp DESC; + +-- name: UnfoldNestedData :many +SELECT + record_id, + nested_value +FROM sqlc_example.nested_table +ARRAY JOIN nested_array AS nested_value +WHERE record_id IN (sqlc.slice('record_ids')); + +-- name: AnalyzeArrayElements :many +SELECT + product_id, + arrayJoin(categories) AS category, + COUNT(*) OVER (PARTITION BY category) as category_count +FROM sqlc_example.products +WHERE product_id = ? +GROUP BY product_id, category; + +-- name: ExtractMetadataFromJSON :many +SELECT + id, + arrayJoin(JSONExtract(json_value, 'Array(String)')) as metadata_value +FROM sqlc_example.events; + +-- name: GetConfigData :one +SELECT id, settings, metrics, tags, created_at +FROM sqlc_example.config_data +WHERE id = ?; + +-- name: GetConfigSettings :many +SELECT id, settings, created_at +FROM sqlc_example.config_data +WHERE id > sqlc.arg('min_id') +ORDER BY id; + +-- name: InsertConfigData :exec +INSERT INTO sqlc_example.config_data (id, settings, metrics, tags, created_at) +VALUES (?, ?, ?, ?, ?); + +-- IP address type examples + +-- name: GetNetworkLogBySourceIP :one +SELECT id, source_ip, dest_ip, source_ipv6, dest_ipv6, timestamp, bytes_sent +FROM sqlc_example.network_logs +WHERE source_ip = sqlc.arg('source_ip'); + +-- name: GetNetworkLogsByDestIP :many +SELECT id, source_ip, dest_ip, timestamp, bytes_sent +FROM sqlc_example.network_logs +WHERE dest_ip = sqlc.arg('dest_ip') +ORDER BY timestamp DESC; + +-- name: GetIPv6Connections :many +SELECT id, source_ipv6, dest_ipv6, timestamp, bytes_sent +FROM sqlc_example.network_logs +WHERE source_ipv6 IS NOT NULL AND dest_ipv6 IS NOT NULL +ORDER BY timestamp DESC; + +-- name: InsertNetworkLog :exec +INSERT INTO sqlc_example.network_logs (id, source_ip, dest_ip, source_ipv6, dest_ipv6, timestamp, bytes_sent) +VALUES (sqlc.arg('id'), sqlc.arg('source_ip'), sqlc.arg('dest_ip'), sqlc.arg('source_ipv6'), sqlc.arg('dest_ipv6'), sqlc.arg('timestamp'), sqlc.arg('bytes_sent')); + +-- name: FilterByIPRange :many +SELECT id, source_ip, dest_ip, timestamp, bytes_sent +FROM sqlc_example.network_logs +WHERE source_ip >= sqlc.arg('min_ip') AND source_ip <= sqlc.arg('max_ip') +ORDER BY timestamp DESC; + +-- Complex CTE with UNION query (tests UNION inside unnamed subqueries) +-- name: GetPlatformHistory :many +WITH platform_info AS ( + SELECT platform_id, + argMax(platform_name, timestamp) as platform_name, + argMax(region, timestamp) as region + FROM ( + SELECT timestamp, platform_id, platform_name, region + FROM sqlc_example.platform_created_event + + UNION ALL + + SELECT timestamp, platform_id, new_name as platform_name, region + FROM sqlc_example.platform_renamed_event + ) AS platform_events + GROUP BY platform_id +), usage_summary AS ( + SELECT feature_usage.platform_id, feature_id, + COUNT(*) AS update_count, + uniqExact(user_count) AS unique_user_counts, + SUM(usage_count) AS total_usage + FROM sqlc_example.feature_usage + WHERE feature_usage.platform_id = sqlc.arg('platform_id') + GROUP BY feature_usage.platform_id, feature_id + ORDER BY total_usage DESC +) + +SELECT + u.platform_id, + u.feature_id, + u.update_count, + u.unique_user_counts, + u.total_usage, + p.platform_name, + p.region +FROM usage_summary u +LEFT JOIN platform_info p ON u.platform_id = p.platform_id; + +-- LEFT JOIN USING clause test +-- This ensures that USING clause returns only one column (not duplicated) +-- ClickHouse naturally returns a single column for the shared key with USING +-- After sqlc expansion, this should generate: order_id, customer_name, amount, created_at, shipment_id, address, shipped_at +-- NOT: order_id, customer_name, amount, created_at, order_id, shipment_id, address, shipped_at (with order_id duplicated) +-- name: GetOrdersWithShipments :many +SELECT * +FROM sqlc_example.orders +LEFT JOIN sqlc_example.shipments USING (order_id) +ORDER BY created_at DESC; diff --git a/examples/clickhouse/schema.sql b/examples/clickhouse/schema.sql new file mode 100644 index 0000000000..b78c3ad7e7 --- /dev/null +++ b/examples/clickhouse/schema.sql @@ -0,0 +1,208 @@ +-- ClickHouse example schema + +CREATE DATABASE IF NOT EXISTS sqlc_example; + +-- Basic CRUD tables +CREATE TABLE IF NOT EXISTS sqlc_example.users +( + id UInt32, + name String, + email String, + status String, + created_at DateTime +) +ENGINE = MergeTree() +ORDER BY id; + +CREATE TABLE IF NOT EXISTS sqlc_example.posts +( + id UInt32, + user_id UInt32, + title String, + content String, + status Nullable(String), + created_at DateTime +) +ENGINE = MergeTree() +ORDER BY (id, user_id); + +CREATE TABLE IF NOT EXISTS sqlc_example.comments +( + id UInt32, + post_id UInt32, + user_id UInt32, + content String, + created_at DateTime +) +ENGINE = MergeTree() +ORDER BY (id, post_id, user_id); + +-- Tables with array columns for ARRAY JOIN examples +CREATE TABLE IF NOT EXISTS sqlc_example.users_with_tags +( + id UInt32, + name String, + tags Array(String) +) +ENGINE = MergeTree() +ORDER BY id; + +CREATE TABLE IF NOT EXISTS sqlc_example.events_with_properties +( + event_id UInt32, + event_name String, + timestamp DateTime, + properties Nested( + keys String, + values String + ) +) +ENGINE = MergeTree() +ORDER BY event_id; + +CREATE TABLE IF NOT EXISTS sqlc_example.nested_table +( + record_id UInt32, + nested_array Array(String) +) +ENGINE = MergeTree() +ORDER BY record_id; + +CREATE TABLE IF NOT EXISTS sqlc_example.products +( + product_id UInt32, + name String, + categories Array(String) +) +ENGINE = MergeTree() +ORDER BY product_id; + +-- Metrics and analytics tables +CREATE TABLE IF NOT EXISTS sqlc_example.metrics +( + category String, + value Float64, + value_x Float64, + value_y Float64, + timestamp DateTime +) +ENGINE = MergeTree() +ORDER BY timestamp; + + + +CREATE TABLE IF NOT EXISTS sqlc_example.order_metrics +( + status String, + amount Float64, + rating Nullable(Float64), + created_at DateTime +) +ENGINE = MergeTree() +ORDER BY created_at; + +CREATE TABLE IF NOT EXISTS sqlc_example.timeseries +( + date Date, + metric_value Float64 +) +ENGINE = MergeTree() +ORDER BY date; + +CREATE TABLE IF NOT EXISTS sqlc_example.events +( + id UInt32, + amount Float64, + created_at DateTime, + status String, + platform_id Nullable(String), + json_value Nullable(String) +) +ENGINE = MergeTree() +ORDER BY id; + +-- Table with Map type columns +CREATE TABLE IF NOT EXISTS sqlc_example.config_data +( + id UInt32, + settings Map(String, String), + metrics Map(String, Float64), + tags Map(String, Array(String)), + created_at DateTime +) +ENGINE = MergeTree() +ORDER BY id; + +-- Table with IP address columns +CREATE TABLE IF NOT EXISTS sqlc_example.network_logs +( + id UInt32, + source_ip IPv4, + dest_ip IPv4, + source_ipv6 Nullable(IPv6), + dest_ipv6 Nullable(IPv6), + timestamp DateTime, + bytes_sent UInt64 +) +ENGINE = MergeTree() +ORDER BY (timestamp, source_ip); + +-- Event tracking tables for complex UNION/CTE queries +CREATE TABLE IF NOT EXISTS sqlc_example.platform_created_event +( + event_id UInt32, + timestamp DateTime, + platform_id UInt32, + platform_name String, + region String +) +ENGINE = MergeTree() +PARTITION BY toYYYYMM(timestamp) +ORDER BY (timestamp, platform_id); + +CREATE TABLE IF NOT EXISTS sqlc_example.platform_renamed_event +( + event_id UInt32, + timestamp DateTime, + platform_id UInt32, + old_name String, + new_name String, + region String +) +ENGINE = MergeTree() +PARTITION BY toYYYYMM(timestamp) +ORDER BY (timestamp, platform_id); + +CREATE TABLE IF NOT EXISTS sqlc_example.feature_usage +( + event_id UInt32, + timestamp DateTime, + platform_id UInt32, + feature_id String, + user_count UInt32, + usage_count UInt64 +) +ENGINE = MergeTree() +PARTITION BY toYYYYMM(timestamp) +ORDER BY (timestamp, platform_id, feature_id); + +-- Tables for LEFT JOIN USING clause test +CREATE TABLE IF NOT EXISTS sqlc_example.orders +( + order_id UInt32, + customer_name String, + amount Float64, + created_at DateTime +) +ENGINE = MergeTree() +ORDER BY order_id; + +CREATE TABLE IF NOT EXISTS sqlc_example.shipments +( + shipment_id UInt32, + order_id UInt32, + address String, + shipped_at DateTime +) +ENGINE = MergeTree() +ORDER BY (order_id, shipment_id); diff --git a/examples/clickhouse/sqlc.yaml b/examples/clickhouse/sqlc.yaml new file mode 100644 index 0000000000..4edd94910b --- /dev/null +++ b/examples/clickhouse/sqlc.yaml @@ -0,0 +1,13 @@ +version: "2" +sql: + - engine: "clickhouse" + queries: "queries.sql" + schema: "schema.sql" + strict_order_by: false + strict_function_checks: false + gen: + go: + out: "gen" + package: "db" + sql_package: "clickhouse/v2" + emit_pointers_for_null_types: true diff --git a/go.mod b/go.mod index 630795248e..36dba5eb74 100644 --- a/go.mod +++ b/go.mod @@ -34,6 +34,14 @@ require ( require ( cel.dev/expr v0.24.0 // indirect filippo.io/edwards25519 v1.1.0 // indirect + github.com/AfterShip/clickhouse-sql-parser v0.4.16 // indirect + github.com/ClickHouse/ch-go v0.68.0 // indirect + github.com/ClickHouse/clickhouse-go/v2 v2.40.3 // indirect + github.com/andybalholm/brotli v1.2.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/go-faster/city v1.0.1 // indirect + github.com/go-faster/errors v0.7.1 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect github.com/jackc/pgconn v1.14.3 // indirect @@ -43,18 +51,29 @@ require ( github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgtype v1.14.0 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/paulmach/orb v0.11.1 // indirect + github.com/pierrec/lz4/v4 v4.1.22 // indirect github.com/ncruces/julianday v1.0.0 // indirect github.com/pingcap/errors v0.11.5-0.20240311024730-e056997136bb // indirect github.com/pingcap/failpoint v0.0.0-20240528011301-b51a646c7c86 // indirect github.com/pingcap/log v1.1.0 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect + github.com/segmentio/asm v1.2.0 // indirect + github.com/shopspring/decimal v1.4.0 // indirect github.com/stoewer/go-strcase v1.2.0 // indirect github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect golang.org/x/crypto v0.45.0 // indirect golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect golang.org/x/net v0.47.0 // indirect diff --git a/go.sum b/go.sum index 002020f15c..44a0692f4a 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,16 @@ cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/AfterShip/clickhouse-sql-parser v0.4.16 h1:gpl+wXclYUKT0p4+gBq22XeRYWwEoZ9f35vogqMvkLQ= +github.com/AfterShip/clickhouse-sql-parser v0.4.16/go.mod h1:W0Z82wJWkJxz2RVun/RMwxue3g7ut47Xxl+SFqdJGus= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/ClickHouse/ch-go v0.68.0 h1:zd2VD8l2aVYnXFRyhTyKCrxvhSz1AaY4wBUXu/f0GiU= +github.com/ClickHouse/ch-go v0.68.0/go.mod h1:C89Fsm7oyck9hr6rRo5gqqiVtaIY6AjdD0WFMyNRQ5s= +github.com/ClickHouse/clickhouse-go/v2 v2.40.3 h1:46jB4kKwVDUOnECpStKMVXxvR0Cg9zeV9vdbPjtn6po= +github.com/ClickHouse/clickhouse-go/v2 v2.40.3/go.mod h1:qO0HwvjCnTB4BPL/k6EE3l4d9f/uF+aoimAhJX70eKA= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= +github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= @@ -20,6 +28,10 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= +github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw= +github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw= +github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg= +github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= @@ -29,11 +41,14 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/cel-go v0.26.1 h1:iPbVVEdkhTX++hpe3lzSk7D3G3QSYqLGoHOcEio+UXQ= github.com/google/cel-go v0.26.1/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= @@ -95,7 +110,11 @@ github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -117,12 +136,22 @@ github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/paulmach/orb v0.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU= +github.com/paulmach/orb v0.11.1/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU= +github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY= github.com/ncruces/go-sqlite3 v0.30.2 h1:1GVbHAkKAOwjJd3JYl8ldrYROudfZUOah7oXPD7VZbQ= github.com/ncruces/go-sqlite3 v0.30.2/go.mod h1:AxKu9sRxkludimFocbktlY6LiYSkxiI5gTA8r+os/Nw= github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M= github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g= github.com/pganalyze/pg_query_go/v6 v6.1.0 h1:jG5ZLhcVgL1FAw4C/0VNQaVmX1SUJx71wBGdtTtBvls= github.com/pganalyze/pg_query_go/v6 v6.1.0/go.mod h1:nvTHIuoud6e1SfrUaFwHqT0i4b5Nr+1rPWVds3B5+50= +github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= +github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pingcap/errors v0.11.0/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pingcap/errors v0.11.5-0.20240311024730-e056997136bb h1:3pSi4EDG6hg0orE1ndHkXvX6Qdq2cZn8gAPir8ymKZk= github.com/pingcap/errors v0.11.5-0.20240311024730-e056997136bb/go.mod h1:X2r9ueLEUZgtx2cIogM0v4Zj5uvvzhuuiu7Pn8HzMPg= @@ -147,9 +176,13 @@ github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OK github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= +github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= @@ -168,22 +201,49 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= +github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tetratelabs/wazero v1.10.1 h1:2DugeJf6VVk58KTPszlNfeeN8AhhpwcZqkJj2wwFuH8= github.com/tetratelabs/wazero v1.10.1/go.mod h1:DRm5twOQ5Gr1AoEdSi0CLjDQF1J9ZAuyqFIjl1KKfQU= github.com/wasilibs/go-pgquery v0.0.0-20250409022910-10ac41983c07 h1:mJdDDPblDfPe7z7go8Dvv1AJQDI3eQ/5xith3q2mFlo= github.com/wasilibs/go-pgquery v0.0.0-20250409022910-10ac41983c07/go.mod h1:Ak17IJ037caFp4jpCw/iQQ7/W74Sqpb1YuKJU6HTKfM= github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 h1:OvLBa8SqJnZ6P+mjlzc2K7PM22rRUPE1x32G9DTPrC4= github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52/go.mod h1:jMeV4Vpbi8osrE/pKUxRZkVaA0EX7NZN0A9/oRzgpgY= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= +github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= +go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= @@ -221,6 +281,8 @@ go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -230,6 +292,11 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= +golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= +golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= @@ -237,11 +304,28 @@ golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAf golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= +golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= +golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= +golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -257,7 +341,9 @@ golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= @@ -268,6 +354,11 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -278,7 +369,12 @@ golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -294,6 +390,7 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go. google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= diff --git a/internal/codegen/golang/clickhouse_type.go b/internal/codegen/golang/clickhouse_type.go new file mode 100644 index 0000000000..85bdd18cbf --- /dev/null +++ b/internal/codegen/golang/clickhouse_type.go @@ -0,0 +1,342 @@ +package golang + +import ( + "strings" + + "github.com/sqlc-dev/sqlc/internal/codegen/golang/opts" + "github.com/sqlc-dev/sqlc/internal/codegen/sdk" + "github.com/sqlc-dev/sqlc/internal/plugin" +) + +func clickhouseType(req *plugin.GenerateRequest, options *opts.Options, col *plugin.Column) string { + columnType := sdk.DataType(col.Type) + notNull := col.NotNull || col.IsArray + + // Check if we're using the native ClickHouse driver + driver := parseDriver(options.SqlPackage) + useNativeDriver := driver.IsClickHouse() + + switch columnType { + + // String types + case "string", "varchar", "text", "char", "fixedstring": + if useNativeDriver { + // Native driver uses *string for nullable + if notNull { + return "string" + } + if options.EmitPointersForNullTypes { + return "*string" + } + return "sql.NullString" + } + if notNull { + return "string" + } + return "sql.NullString" + + // Integer types - UInt variants (unsigned) + case "uint8": + if useNativeDriver { + if notNull { + return "uint8" + } + if options.EmitPointersForNullTypes { + return "*uint8" + } + return "sql.NullInt16" + } + if notNull { + return "uint8" + } + return "sql.NullInt16" // database/sql doesn't have NullUint8 + + case "uint16": + if useNativeDriver { + if notNull { + return "uint16" + } + if options.EmitPointersForNullTypes { + return "*uint16" + } + return "sql.NullInt32" + } + if notNull { + return "uint16" + } + return "sql.NullInt32" // database/sql doesn't have NullUint16 + + case "uint32": + if useNativeDriver { + if notNull { + return "uint32" + } + if options.EmitPointersForNullTypes { + return "*uint32" + } + return "sql.NullInt64" + } + if notNull { + return "uint32" + } + return "sql.NullInt64" // database/sql doesn't have NullUint32 + + case "uint64": + if useNativeDriver { + if notNull { + return "uint64" + } + if options.EmitPointersForNullTypes { + return "*uint64" + } + return "sql.NullInt64" + } + if notNull { + return "uint64" + } + return "string" // uint64 can overflow, use string for large values + + // Integer types - Int variants (signed) + case "int8": + if useNativeDriver { + if notNull { + return "int8" + } + if options.EmitPointersForNullTypes { + return "*int8" + } + return "sql.NullInt16" + } + if notNull { + return "int8" + } + return "sql.NullInt16" + + case "int16": + if useNativeDriver { + if notNull { + return "int16" + } + if options.EmitPointersForNullTypes { + return "*int16" + } + return "sql.NullInt16" + } + if notNull { + return "int16" + } + return "sql.NullInt16" + + case "int32": + if useNativeDriver { + if notNull { + return "int32" + } + if options.EmitPointersForNullTypes { + return "*int32" + } + return "sql.NullInt32" + } + if notNull { + return "int32" + } + return "sql.NullInt32" + + case "int64": + if useNativeDriver { + if notNull { + return "int64" + } + if options.EmitPointersForNullTypes { + return "*int64" + } + return "sql.NullInt64" + } + if notNull { + return "int64" + } + return "sql.NullInt64" + + // Generic "integer" type (used for LIMIT/OFFSET parameters and other integer values) + case "integer": + if useNativeDriver { + if notNull { + return "int64" + } + if options.EmitPointersForNullTypes { + return "*int64" + } + return "sql.NullInt64" + } + if notNull { + return "int64" + } + return "sql.NullInt64" + + // Large integer types + case "int128", "int256", "uint128", "uint256": + // These are too large for standard Go integers, use string + if notNull { + return "string" + } + return "sql.NullString" + + // Floating point types + case "float32", "real": + if useNativeDriver { + if notNull { + return "float32" + } + if options.EmitPointersForNullTypes { + return "*float32" + } + return "sql.NullFloat64" + } + if notNull { + return "float32" + } + return "sql.NullFloat64" // database/sql doesn't have NullFloat32 + + case "float64", "double precision", "double": + if useNativeDriver { + if notNull { + return "float64" + } + if options.EmitPointersForNullTypes { + return "*float64" + } + return "sql.NullFloat64" + } + if notNull { + return "float64" + } + return "sql.NullFloat64" + + // Decimal types + case "decimal": + if notNull { + return "string" + } + return "sql.NullString" + + // Date and time types + case "date", "date32": + if useNativeDriver { + if notNull { + return "time.Time" + } + if options.EmitPointersForNullTypes { + return "*time.Time" + } + return "sql.NullTime" + } + if notNull { + return "time.Time" + } + return "sql.NullTime" + + case "datetime", "datetime64", "timestamp": + if useNativeDriver { + if notNull { + return "time.Time" + } + if options.EmitPointersForNullTypes { + return "*time.Time" + } + return "sql.NullTime" + } + if notNull { + return "time.Time" + } + return "sql.NullTime" + + // Boolean + case "boolean", "bool": + if useNativeDriver { + if notNull { + return "bool" + } + if options.EmitPointersForNullTypes { + return "*bool" + } + return "sql.NullBool" + } + if notNull { + return "bool" + } + return "sql.NullBool" + + // UUID + case "uuid": + if notNull { + return "string" + } + return "sql.NullString" + + // IP address types + case "ipv4", "ipv6": + if notNull { + return "netip.Addr" + } + if options.EmitPointersForNullTypes { + return "*netip.Addr" + } + // Use a custom SQL null type for nullable IP addresses + // For now, use pointer since netip.Addr doesn't have a nullable variant + return "*netip.Addr" + + // JSON types + case "json": + return "json.RawMessage" + + // Arrays - ClickHouse array types + case "array": + if useNativeDriver { + // Native driver has better array support + // For now, still use generic until we have element type info + return "[]interface{}" + } + return "[]interface{}" // Generic array type + + // Any/Unknown type + case "any": + return "interface{}" + + default: + // Check if this is a map type (starts with "map[") + // Map types come from the engine layer with full type information (e.g., "map[string]int64") + if strings.HasPrefix(columnType, "map[") { + if notNull { + return columnType + } + // For nullable map types, wrap in pointer + if options.EmitPointersForNullTypes { + return "*" + columnType + } + // Otherwise treat as interface{} for nullable + return "interface{}" + } + + // Check for custom types (enums, etc.) + for _, schema := range req.Catalog.Schemas { + for _, enum := range schema.Enums { + if enum.Name == columnType { + if notNull { + if schema.Name == req.Catalog.DefaultSchema { + return StructName(enum.Name, options) + } + return StructName(schema.Name+"_"+enum.Name, options) + } else { + if schema.Name == req.Catalog.DefaultSchema { + return "Null" + StructName(enum.Name, options) + } + return "Null" + StructName(schema.Name+"_"+enum.Name, options) + } + } + } + } + + // Default fallback for unknown types + return "interface{}" + } +} diff --git a/internal/codegen/golang/driver.go b/internal/codegen/golang/driver.go index 5e3a533dcc..2728d943d2 100644 --- a/internal/codegen/golang/driver.go +++ b/internal/codegen/golang/driver.go @@ -8,6 +8,8 @@ func parseDriver(sqlPackage string) opts.SQLDriver { return opts.SQLDriverPGXV4 case opts.SQLPackagePGXV5: return opts.SQLDriverPGXV5 + case opts.SQLPackageClickHouseV2: + return opts.SQLDriverClickHouseV2 default: return opts.SQLDriverLibPQ } diff --git a/internal/codegen/golang/go_type.go b/internal/codegen/golang/go_type.go index c4aac84dd6..f258ef78ee 100644 --- a/internal/codegen/golang/go_type.go +++ b/internal/codegen/golang/go_type.go @@ -89,6 +89,8 @@ func goInnerType(req *plugin.GenerateRequest, options *opts.Options, col *plugin return postgresType(req, options, col) case "sqlite": return sqliteType(req, options, col) + case "clickhouse": + return clickhouseType(req, options, col) default: return "interface{}" } diff --git a/internal/codegen/golang/imports.go b/internal/codegen/golang/imports.go index ccca4f603c..3ebf1070d3 100644 --- a/internal/codegen/golang/imports.go +++ b/internal/codegen/golang/imports.go @@ -132,6 +132,8 @@ func (i *importer) dbImports() fileImports { case opts.SQLDriverPGXV5: pkg = append(pkg, ImportSpec{Path: "github.com/jackc/pgx/v5/pgconn"}) pkg = append(pkg, ImportSpec{Path: "github.com/jackc/pgx/v5"}) + case opts.SQLDriverClickHouseV2: + pkg = append(pkg, ImportSpec{Path: "github.com/ClickHouse/clickhouse-go/v2/lib/driver"}) default: std = append(std, ImportSpec{Path: "database/sql"}) if i.Options.EmitPreparedQueries { @@ -395,7 +397,7 @@ func (i *importer) queryImports(filename string) fileImports { } sqlpkg := parseDriver(i.Options.SqlPackage) - if sqlcSliceScan() && !sqlpkg.IsPGX() { + if sqlcSliceScan() && !sqlpkg.IsPGX() && !sqlpkg.IsClickHouse() { std["strings"] = struct{}{} } if sliceScan() && !sqlpkg.IsPGX() { diff --git a/internal/codegen/golang/imports_test.go b/internal/codegen/golang/imports_test.go new file mode 100644 index 0000000000..4e2782e3ed --- /dev/null +++ b/internal/codegen/golang/imports_test.go @@ -0,0 +1,28 @@ +package golang + +import ( + "testing" + + "github.com/sqlc-dev/sqlc/internal/codegen/golang/opts" +) + +func TestClickHouseDriver_NotPGX(t *testing.T) { + driver := opts.SQLDriver(opts.SQLDriverClickHouseV2) + if driver.IsPGX() { + t.Error("ClickHouse driver should not identify as PGX") + } +} + +func TestClickHouseDriver_IsClickHouse(t *testing.T) { + driver := opts.SQLDriver(opts.SQLDriverClickHouseV2) + if !driver.IsClickHouse() { + t.Error("ClickHouse driver should identify as ClickHouse") + } +} + +func TestStandardDriver_NotClickHouse(t *testing.T) { + driver := opts.SQLDriver(opts.SQLDriverLibPQ) + if driver.IsClickHouse() { + t.Error("Standard driver should not identify as ClickHouse") + } +} diff --git a/internal/codegen/golang/opts/enum.go b/internal/codegen/golang/opts/enum.go index 40457d040a..c4c78dbef6 100644 --- a/internal/codegen/golang/opts/enum.go +++ b/internal/codegen/golang/opts/enum.go @@ -5,15 +5,17 @@ import "fmt" type SQLDriver string const ( - SQLPackagePGXV4 string = "pgx/v4" - SQLPackagePGXV5 string = "pgx/v5" - SQLPackageStandard string = "database/sql" + SQLPackagePGXV4 string = "pgx/v4" + SQLPackagePGXV5 string = "pgx/v5" + SQLPackageClickHouseV2 string = "clickhouse/v2" + SQLPackageStandard string = "database/sql" ) var validPackages = map[string]struct{}{ - string(SQLPackagePGXV4): {}, - string(SQLPackagePGXV5): {}, - string(SQLPackageStandard): {}, + string(SQLPackagePGXV4): {}, + string(SQLPackagePGXV5): {}, + string(SQLPackageClickHouseV2): {}, + string(SQLPackageStandard): {}, } func validatePackage(sqlPackage string) error { @@ -28,6 +30,7 @@ const ( SQLDriverPGXV5 = "github.com/jackc/pgx/v5" SQLDriverLibPQ = "github.com/lib/pq" SQLDriverGoSQLDriverMySQL = "github.com/go-sql-driver/mysql" + SQLDriverClickHouseV2 = "github.com/ClickHouse/clickhouse-go/v2" ) var validDrivers = map[string]struct{}{ @@ -35,6 +38,7 @@ var validDrivers = map[string]struct{}{ string(SQLDriverPGXV5): {}, string(SQLDriverLibPQ): {}, string(SQLDriverGoSQLDriverMySQL): {}, + string(SQLDriverClickHouseV2): {}, } func validateDriver(sqlDriver string) error { @@ -52,12 +56,18 @@ func (d SQLDriver) IsGoSQLDriverMySQL() bool { return d == SQLDriverGoSQLDriverMySQL } +func (d SQLDriver) IsClickHouse() bool { + return d == SQLDriverClickHouseV2 +} + func (d SQLDriver) Package() string { switch d { case SQLDriverPGXV4: return SQLPackagePGXV4 case SQLDriverPGXV5: return SQLPackagePGXV5 + case SQLDriverClickHouseV2: + return SQLPackageClickHouseV2 default: return SQLPackageStandard } diff --git a/internal/codegen/golang/templates/clickhouse/dbCode.tmpl b/internal/codegen/golang/templates/clickhouse/dbCode.tmpl new file mode 100644 index 0000000000..71b81094b6 --- /dev/null +++ b/internal/codegen/golang/templates/clickhouse/dbCode.tmpl @@ -0,0 +1,34 @@ +{{define "dbCodeTemplateClickHouse"}} + +type DBTX interface { + Exec(ctx context.Context, query string, args ...any) error + Query(ctx context.Context, query string, args ...any) (driver.Rows, error) + QueryRow(ctx context.Context, query string, args ...any) driver.Row +{{- if .UsesBatch }} + PrepareBatch(ctx context.Context, query string) (driver.Batch, error) +{{- end }} +} + +{{ if .EmitMethodsWithDBArgument}} +func New() *Queries { + return &Queries{} +{{- else -}} +func New(db DBTX) *Queries { + return &Queries{db: db} +{{- end}} +} + +type Queries struct { + {{if not .EmitMethodsWithDBArgument}} + db DBTX + {{end}} +} + +{{if not .EmitMethodsWithDBArgument}} +func (q *Queries) WithTx(tx driver.Conn) *Queries { + return &Queries{ + db: tx, + } +} +{{end}} +{{end}} diff --git a/internal/codegen/golang/templates/clickhouse/interfaceCode.tmpl b/internal/codegen/golang/templates/clickhouse/interfaceCode.tmpl new file mode 100644 index 0000000000..4304e9eacf --- /dev/null +++ b/internal/codegen/golang/templates/clickhouse/interfaceCode.tmpl @@ -0,0 +1,16 @@ +{{define "interfaceCodeClickHouse"}} +type Querier interface { {{- range .GoQueries}} + {{- if ne (hasPrefix .Cmd ":batch") true}} + {{range .Comments}}//{{.}} + {{end -}} + {{- if $.EmitMethodsWithDBArgument -}} + {{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}) ({{.Ret.EmitResultType}}, error) + {{- else -}} + {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) ({{.Ret.EmitResultType}}, error) + {{- end -}} + {{- end}} +{{- end}} +} + +var _ Querier = (*Queries)(nil) +{{end}} diff --git a/internal/codegen/golang/templates/clickhouse/queryCode.tmpl b/internal/codegen/golang/templates/clickhouse/queryCode.tmpl new file mode 100644 index 0000000000..91d255647f --- /dev/null +++ b/internal/codegen/golang/templates/clickhouse/queryCode.tmpl @@ -0,0 +1,165 @@ +{{define "queryCodeClickHouse"}} +{{range .GoQueries}} +{{if $.OutputQuery .SourceName}} +{{if and (ne .Cmd ":copyfrom") (ne (hasPrefix .Cmd ":batch") true)}} +const {{.ConstantName}} = {{$.Q}}-- name: {{.MethodName}} {{.Cmd}} +{{escape .SQL}} +{{$.Q}} +{{end}} + +{{if ne (hasPrefix .Cmd ":batch") true}} +{{if .Arg.EmitStruct}} +type {{.Arg.Type}} struct { {{- range .Arg.Struct.Fields}} + {{.Name}} {{.Type}} {{if .Tag}}{{$.Q}}{{.Tag}}{{$.Q}}{{end}} + {{- end}} +} +{{end}} + +{{if .Ret.EmitStruct}} +type {{.Ret.Type}} struct { {{- range .Ret.Struct.Fields}} + {{.Name}} {{.Type}} {{if .Tag}}{{$.Q}}{{.Tag}}{{$.Q}}{{end}} + {{- end}} +} +{{end}} +{{end}} + +{{if eq .Cmd ":one"}} +{{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}}) +{{- end}} + {{- if or (ne .Arg.Pair .Ret.Pair) (ne .Arg.DefineType .Ret.DefineType) }} + var {{.Ret.Name}} {{.Ret.Type}} + {{- end}} + err := row.Scan({{.Ret.Scan}}) + {{- if $.WrapErrors}} + if err != nil { + err = fmt.Errorf("query {{.MethodName}}: %w", err) + } + {{- end}} + return {{.Ret.ReturnName}}, err +} +{{end}} + +{{if eq .Cmd ":many"}} +{{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}}) +{{- end}} + if err != nil { + return nil, {{if $.WrapErrors}}fmt.Errorf("query {{.MethodName}}: %w", err){{else}}err{{end}} + } + defer rows.Close() + {{- if $.EmitEmptySlices}} + items := []{{.Ret.DefineType}}{} + {{else}} + var items []{{.Ret.DefineType}} + {{end -}} + for rows.Next() { + var {{.Ret.Name}} {{.Ret.Type}} + if err := rows.Scan({{.Ret.Scan}}); err != nil { + return nil, {{if $.WrapErrors}}fmt.Errorf("query {{.MethodName}}: %w", err){{else}}err{{end}} + } + items = append(items, {{.Ret.ReturnName}}) + } + if err := rows.Err(); err != nil { + return nil, {{if $.WrapErrors}}fmt.Errorf("query {{.MethodName}}: %w", err){{else}}err{{end}} + } + return items, nil +} +{{end}} + +{{if eq .Cmd ":exec"}} +{{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}}) +{{- end}} + {{- if $.WrapErrors}} + if err != nil { + return fmt.Errorf("exec {{.MethodName}}: %w", err) + } + {{- end}} + return err +} +{{end}} + +{{if eq .Cmd ":execrows"}} +{{range .Comments}}//{{.}} +{{end -}} +{{- if $.EmitMethodsWithDBArgument -}} +func (q *Queries) {{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}) (int64, error) { + // ClickHouse doesn't support RowsAffected, returning error + return 0, fmt.Errorf(":execrows not supported by ClickHouse driver") +} +{{- else -}} +func (q *Queries) {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) (int64, error) { + // ClickHouse doesn't support RowsAffected, returning error + return 0, fmt.Errorf(":execrows not supported by ClickHouse driver") +} +{{- end}} +{{end}} + +{{if eq .Cmd ":execlastid"}} +{{range .Comments}}//{{.}} +{{end -}} +{{- if $.EmitMethodsWithDBArgument -}} +func (q *Queries) {{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}) (int64, error) { + // ClickHouse doesn't support LastInsertId, returning error + return 0, fmt.Errorf(":execlastid not supported by ClickHouse driver") +} +{{- else -}} +func (q *Queries) {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) (int64, error) { + // ClickHouse doesn't support LastInsertId, returning error + return 0, fmt.Errorf(":execlastid not supported by ClickHouse driver") +} +{{- end}} +{{end}} + +{{if eq .Cmd ":execresult"}} +{{range .Comments}}//{{.}} +{{end -}} +{{- if $.EmitMethodsWithDBArgument -}} +func (q *Queries) {{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}) error { + {{- if $.WrapErrors}} + err := db.Exec(ctx, {{.ConstantName}}, {{.Arg.Params}}) + if err != nil { + return fmt.Errorf("exec {{.MethodName}}: %w", err) + } + return nil + {{- else}} + return db.Exec(ctx, {{.ConstantName}}, {{.Arg.Params}}) + {{- end}} +} +{{- else -}} +func (q *Queries) {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) error { + {{- if $.WrapErrors}} + err := q.db.Exec(ctx, {{.ConstantName}}, {{.Arg.Params}}) + if err != nil { + return fmt.Errorf("exec {{.MethodName}}: %w", err) + } + return nil + {{- else}} + return q.db.Exec(ctx, {{.ConstantName}}, {{.Arg.Params}}) + {{- end}} +} +{{- end}} +{{end}} + +{{end}} +{{end}} +{{end}} diff --git a/internal/codegen/golang/templates/template.tmpl b/internal/codegen/golang/templates/template.tmpl index afd50c01ac..a4c1291e2f 100644 --- a/internal/codegen/golang/templates/template.tmpl +++ b/internal/codegen/golang/templates/template.tmpl @@ -25,6 +25,8 @@ import ( {{if .SQLDriver.IsPGX }} {{- template "dbCodeTemplatePgx" .}} +{{else if .SQLDriver.IsClickHouse }} + {{- template "dbCodeTemplateClickHouse" .}} {{else}} {{- template "dbCodeTemplateStd" .}} {{end}} @@ -57,6 +59,8 @@ import ( {{define "interfaceCode"}} {{if .SQLDriver.IsPGX }} {{- template "interfaceCodePgx" .}} + {{else if .SQLDriver.IsClickHouse }} + {{- template "interfaceCodeClickHouse" .}} {{else}} {{- template "interfaceCodeStd" .}} {{end}} @@ -188,6 +192,8 @@ import ( {{define "queryCode"}} {{if .SQLDriver.IsPGX }} {{- template "queryCodePgx" .}} +{{else if .SQLDriver.IsClickHouse }} + {{- template "queryCodeClickHouse" .}} {{else}} {{- template "queryCodeStd" .}} {{end}} diff --git a/internal/compiler/clickhouse_join_test.go b/internal/compiler/clickhouse_join_test.go new file mode 100644 index 0000000000..6fa6a8767e --- /dev/null +++ b/internal/compiler/clickhouse_join_test.go @@ -0,0 +1,196 @@ +package compiler + +import ( + "strings" + "testing" + + "github.com/sqlc-dev/sqlc/internal/config" + "github.com/sqlc-dev/sqlc/internal/engine/clickhouse" + "github.com/sqlc-dev/sqlc/internal/sql/ast" +) + +// TestClickHouseJoinColumnResolution tests that column names are properly resolved +// in JOIN queries now that JoinExpr is correctly converted +func TestClickHouseJoinColumnResolution(t *testing.T) { + parser := clickhouse.NewParser() + cat := clickhouse.NewCatalog() + + // Create database and tables + schemaSQL := `CREATE DATABASE IF NOT EXISTS test_db; +CREATE TABLE test_db.users ( + id UInt32, + name String, + email String +); +CREATE TABLE test_db.posts ( + id UInt32, + user_id UInt32, + title String, + content String +)` + + stmts, err := parser.Parse(strings.NewReader(schemaSQL)) + if err != nil { + t.Fatalf("Parse schema failed: %v", err) + } + + for _, stmt := range stmts { + if err := cat.Update(stmt, nil); err != nil { + t.Fatalf("Update catalog failed: %v", err) + } + } + + // Create compiler + conf := config.SQL{ + Engine: config.EngineClickHouse, + } + combo := config.CombinedSettings{ + Global: config.Config{}, + } + + c, err := NewCompiler(conf, combo) + if err != nil { + t.Fatalf("Failed to create compiler: %v", err) + } + + // Replace catalog + c.catalog = cat + + // Parse a JOIN query + querySQL := "SELECT u.id, u.name, p.id as post_id, p.title FROM test_db.users u LEFT JOIN test_db.posts p ON u.id = p.user_id WHERE u.id = 1" + queryStmts, err := parser.Parse(strings.NewReader(querySQL)) + if err != nil { + t.Fatalf("Parse query failed: %v", err) + } + + if len(queryStmts) == 0 { + t.Fatal("No queries parsed") + } + + selectStmt := queryStmts[0].Raw.Stmt + if selectStmt == nil { + t.Fatal("Select statement is nil") + } + + selectAst, ok := selectStmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", selectStmt) + } + + // Build query catalog and get output columns + qc, err := c.buildQueryCatalog(c.catalog, selectAst, nil) + if err != nil { + t.Fatalf("Failed to build query catalog: %v", err) + } + + cols, err := c.outputColumns(qc, selectAst) + if err != nil { + t.Fatalf("Failed to get output columns: %v", err) + } + + if len(cols) != 4 { + t.Errorf("Expected 4 columns, got %d", len(cols)) + } + + expectedNames := []string{"id", "name", "post_id", "title"} + for i, expected := range expectedNames { + if i < len(cols) { + if cols[i].Name != expected { + t.Errorf("Column %d: expected name %q, got %q", i, expected, cols[i].Name) + } + } + } +} + +// TestClickHouseLeftJoinNullability tests that LEFT JOIN correctly marks right-side columns as nullable +// In ClickHouse, columns are non-nullable by default unless wrapped in Nullable(T) +func TestClickHouseLeftJoinNullability(t *testing.T) { + parser := clickhouse.NewParser() + cat := clickhouse.NewCatalog() + + schemaSQL := `CREATE TABLE orders ( + order_id UInt32, + customer_name String, + amount Float64, + created_at DateTime + ); + CREATE TABLE shipments ( + shipment_id UInt32, + order_id UInt32, + address String, + shipped_at DateTime + )` + + stmts, err := parser.Parse(strings.NewReader(schemaSQL)) + if err != nil { + t.Fatalf("Parse schema failed: %v", err) + } + + for _, stmt := range stmts { + if err := cat.Update(stmt, nil); err != nil { + t.Fatalf("Update catalog failed: %v", err) + } + } + + conf := config.SQL{ + Engine: config.EngineClickHouse, + } + combo := config.CombinedSettings{ + Global: config.Config{}, + } + + c, err := NewCompiler(conf, combo) + if err != nil { + t.Fatalf("Failed to create compiler: %v", err) + } + c.catalog = cat + + querySQL := "SELECT o.order_id, o.customer_name, o.amount, o.created_at, s.shipment_id, s.address, s.shipped_at FROM orders o LEFT JOIN shipments s ON o.order_id = s.order_id ORDER BY o.created_at DESC" + queryStmts, err := parser.Parse(strings.NewReader(querySQL)) + if err != nil { + t.Fatalf("Parse query failed: %v", err) + } + + selectAst := queryStmts[0].Raw.Stmt.(*ast.SelectStmt) + qc, err := c.buildQueryCatalog(c.catalog, selectAst, nil) + if err != nil { + t.Fatalf("Failed to build query catalog: %v", err) + } + + cols, err := c.outputColumns(qc, selectAst) + if err != nil { + t.Fatalf("Failed to get output columns: %v", err) + } + + if len(cols) != 7 { + t.Errorf("Expected 7 columns, got %d", len(cols)) + } + + // Left table columns should be non-nullable + leftTableNonNull := map[string]bool{ + "order_id": true, + "customer_name": true, + "amount": true, + "created_at": true, + } + + // Right table columns should be nullable (because of LEFT JOIN) + rightTableNullable := map[string]bool{ + "shipment_id": true, + "address": true, + "shipped_at": true, + } + + for _, col := range cols { + if expected, ok := leftTableNonNull[col.Name]; ok { + if col.NotNull != expected { + t.Errorf("Column %q: expected NotNull=%v, got %v", col.Name, expected, col.NotNull) + } + } + if expected, ok := rightTableNullable[col.Name]; ok { + if col.NotNull == expected { + t.Errorf("Column %q: expected NotNull=%v, got %v", col.Name, !expected, col.NotNull) + } + } + } +} diff --git a/internal/compiler/engine.go b/internal/compiler/engine.go index 75749cd6df..68da4f0f05 100644 --- a/internal/compiler/engine.go +++ b/internal/compiler/engine.go @@ -7,15 +7,19 @@ import ( "github.com/sqlc-dev/sqlc/internal/analyzer" "github.com/sqlc-dev/sqlc/internal/config" "github.com/sqlc-dev/sqlc/internal/dbmanager" + "github.com/sqlc-dev/sqlc/internal/engine/clickhouse" "github.com/sqlc-dev/sqlc/internal/engine/dolphin" "github.com/sqlc-dev/sqlc/internal/engine/postgresql" pganalyze "github.com/sqlc-dev/sqlc/internal/engine/postgresql/analyzer" "github.com/sqlc-dev/sqlc/internal/engine/sqlite" sqliteanalyze "github.com/sqlc-dev/sqlc/internal/engine/sqlite/analyzer" "github.com/sqlc-dev/sqlc/internal/opts" + "github.com/sqlc-dev/sqlc/internal/sql/ast" "github.com/sqlc-dev/sqlc/internal/sql/catalog" ) +type ResolveTypeFunc func(call *ast.FuncCall, fun *catalog.Function, resolve func(n ast.Node) (*catalog.Column, error)) *ast.TypeName + type Compiler struct { conf config.SQL combo config.CombinedSettings @@ -26,7 +30,8 @@ type Compiler struct { client dbmanager.Client selector selector - schema []string + schema []string + TypeResolver ResolveTypeFunc } func NewCompiler(conf config.SQL, combo config.CombinedSettings) (*Compiler, error) { @@ -38,6 +43,11 @@ func NewCompiler(conf config.SQL, combo config.CombinedSettings) (*Compiler, err } switch conf.Engine { + case config.EngineClickHouse: + c.parser = clickhouse.NewParser() + c.catalog = clickhouse.NewCatalog() + c.selector = newDefaultSelector() + c.TypeResolver = clickhouse.TypeResolver case config.EngineSQLite: c.parser = sqlite.NewParser() c.catalog = sqlite.NewCatalog() @@ -79,7 +89,15 @@ func (c *Compiler) Catalog() *catalog.Catalog { } func (c *Compiler) ParseCatalog(schema []string) error { - return c.parseCatalog(schema) + err := c.parseCatalog(schema) + if err == nil && c.conf.Engine == config.EngineClickHouse { + // Set the catalog on the ClickHouse parser so it can register + // context-dependent functions during query parsing + if chParser, ok := c.parser.(*clickhouse.Parser); ok { + chParser.Catalog = c.catalog + } + } + return err } func (c *Compiler) ParseQueries(queries []string, o opts.Parser) error { diff --git a/internal/compiler/expand.go b/internal/compiler/expand.go index c60b7618b2..2047edc88d 100644 --- a/internal/compiler/expand.go +++ b/internal/compiler/expand.go @@ -71,6 +71,8 @@ func (c *Compiler) quoteIdent(ident string) string { func (c *Compiler) quote(x string) string { switch c.conf.Engine { + case config.EngineClickHouse: + return "`" + x + "`" case config.EngineMySQL: return "`" + x + "`" default: @@ -84,6 +86,9 @@ func (c *Compiler) expandStmt(qc *QueryCatalog, raw *ast.RawStmt, node ast.Node) return nil, err } + // Track USING columns to avoid duplicating them in SELECT * expansion + usingMap := getJoinUsingMap(node) + var targets *ast.List switch n := node.(type) { case *ast.DeleteStmt: @@ -126,8 +131,13 @@ func (c *Compiler) expandStmt(qc *QueryCatalog, raw *ast.RawStmt, node ast.Node) counts := map[string]int{} if scope == "" { for _, t := range tables { - for _, c := range t.Columns { - counts[c.Name] += 1 + for _, col := range t.Columns { + // Don't count columns that are in USING clause for this table + // since they won't be included in the expansion + if usingInfo, ok := usingMap[t.Rel.Name]; ok && usingInfo.HasColumn(col.Name) { + continue + } + counts[col.Name] += 1 } } } @@ -138,6 +148,12 @@ func (c *Compiler) expandStmt(qc *QueryCatalog, raw *ast.RawStmt, node ast.Node) tableName := c.quoteIdent(t.Rel.Name) scopeName := c.quoteIdent(scope) for _, column := range t.Columns { + // Skip columns that are in USING clause for this table + // to avoid duplication (USING naturally returns only one column) + if usingInfo, ok := usingMap[t.Rel.Name]; ok && usingInfo.HasColumn(column.Name) { + continue + } + cname := column.Name if res.Name != nil { cname = *res.Name diff --git a/internal/compiler/output_columns.go b/internal/compiler/output_columns.go index dbd486359a..56499b387a 100644 --- a/internal/compiler/output_columns.go +++ b/internal/compiler/output_columns.go @@ -3,7 +3,9 @@ package compiler import ( "errors" "fmt" + "log" + "github.com/sqlc-dev/sqlc/internal/debug" "github.com/sqlc-dev/sqlc/internal/sql/ast" "github.com/sqlc-dev/sqlc/internal/sql/astutils" "github.com/sqlc-dev/sqlc/internal/sql/catalog" @@ -125,268 +127,11 @@ func (c *Compiler) outputColumns(qc *QueryCatalog, node ast.Node) ([]*Column, er if !ok { continue } - switch n := res.Val.(type) { - - case *ast.A_Const: - name := "" - if res.Name != nil { - name = *res.Name - } - switch n.Val.(type) { - case *ast.String: - cols = append(cols, &Column{Name: name, DataType: "text", NotNull: true}) - case *ast.Integer: - cols = append(cols, &Column{Name: name, DataType: "int", NotNull: true}) - case *ast.Float: - cols = append(cols, &Column{Name: name, DataType: "float", NotNull: true}) - case *ast.Boolean: - cols = append(cols, &Column{Name: name, DataType: "bool", NotNull: true}) - default: - cols = append(cols, &Column{Name: name, DataType: "any", NotNull: false}) - } - - case *ast.A_Expr: - name := "" - if res.Name != nil { - name = *res.Name - } - switch op := astutils.Join(n.Name, ""); { - case lang.IsComparisonOperator(op): - // TODO: Generate a name for these operations - cols = append(cols, &Column{Name: name, DataType: "bool", NotNull: true}) - case lang.IsMathematicalOperator(op): - cols = append(cols, &Column{Name: name, DataType: "int", NotNull: true}) - default: - cols = append(cols, &Column{Name: name, DataType: "any", NotNull: false}) - } - - case *ast.BoolExpr: - name := "" - if res.Name != nil { - name = *res.Name - } - notNull := false - if len(n.Args.Items) == 1 { - switch n.Boolop { - case ast.BoolExprTypeIsNull, ast.BoolExprTypeIsNotNull: - notNull = true - case ast.BoolExprTypeNot: - sublink, ok := n.Args.Items[0].(*ast.SubLink) - if ok && sublink.SubLinkType == ast.EXISTS_SUBLINK { - notNull = true - if name == "" { - name = "not_exists" - } - } - } - } - cols = append(cols, &Column{Name: name, DataType: "bool", NotNull: notNull}) - - case *ast.CaseExpr: - name := "" - if res.Name != nil { - name = *res.Name - } - // TODO: The TypeCase and A_Const code has been copied from below. Instead, we - // need a recurse function to get the type of a node. - if tc, ok := n.Defresult.(*ast.TypeCast); ok { - if tc.TypeName == nil { - return nil, errors.New("no type name type cast") - } - name := "" - if ref, ok := tc.Arg.(*ast.ColumnRef); ok { - name = astutils.Join(ref.Fields, "_") - } - if res.Name != nil { - name = *res.Name - } - // TODO Validate column names - col := toColumn(tc.TypeName) - col.Name = name - cols = append(cols, col) - } else if aconst, ok := n.Defresult.(*ast.A_Const); ok { - switch aconst.Val.(type) { - case *ast.String: - cols = append(cols, &Column{Name: name, DataType: "text", NotNull: true}) - case *ast.Integer: - cols = append(cols, &Column{Name: name, DataType: "int", NotNull: true}) - case *ast.Float: - cols = append(cols, &Column{Name: name, DataType: "float", NotNull: true}) - case *ast.Boolean: - cols = append(cols, &Column{Name: name, DataType: "bool", NotNull: true}) - default: - cols = append(cols, &Column{Name: name, DataType: "any", NotNull: false}) - } - } else { - cols = append(cols, &Column{Name: name, DataType: "any", NotNull: false}) - } - - case *ast.CoalesceExpr: - name := "coalesce" - if res.Name != nil { - name = *res.Name - } - var firstColumn *Column - var shouldNotBeNull bool - for _, arg := range n.Args.Items { - if _, ok := arg.(*ast.A_Const); ok { - shouldNotBeNull = true - continue - } - if ref, ok := arg.(*ast.ColumnRef); ok { - columns, err := outputColumnRefs(res, tables, ref) - if err != nil { - return nil, err - } - for _, c := range columns { - if firstColumn == nil { - firstColumn = c - } - shouldNotBeNull = shouldNotBeNull || c.NotNull - } - } - } - if firstColumn != nil { - firstColumn.NotNull = shouldNotBeNull - firstColumn.skipTableRequiredCheck = true - cols = append(cols, firstColumn) - } else { - cols = append(cols, &Column{Name: name, DataType: "any", NotNull: false}) - } - - case *ast.ColumnRef: - if hasStarRef(n) { - - // add a column with a reference to an embedded table - if embed, ok := qc.embeds.Find(n); ok { - cols = append(cols, &Column{ - Name: embed.Table.Name, - EmbedTable: embed.Table, - }) - continue - } - - // TODO: This code is copied in func expand() - for _, t := range tables { - scope := astutils.Join(n.Fields, ".") - if scope != "" && scope != t.Rel.Name { - continue - } - for _, c := range t.Columns { - cname := c.Name - if res.Name != nil { - cname = *res.Name - } - cols = append(cols, &Column{ - Name: cname, - OriginalName: c.Name, - Type: c.Type, - Scope: scope, - Table: c.Table, - TableAlias: t.Rel.Name, - DataType: c.DataType, - NotNull: c.NotNull, - Unsigned: c.Unsigned, - IsArray: c.IsArray, - ArrayDims: c.ArrayDims, - Length: c.Length, - }) - } - } - continue - } - - columns, err := outputColumnRefs(res, tables, n) - if err != nil { - return nil, err - } - cols = append(cols, columns...) - - case *ast.FuncCall: - rel := n.Func - name := rel.Name - if res.Name != nil { - name = *res.Name - } - fun, err := qc.catalog.ResolveFuncCall(n) - if err == nil { - cols = append(cols, &Column{ - Name: name, - DataType: dataType(fun.ReturnType), - NotNull: !fun.ReturnTypeNullable, - IsFuncCall: true, - }) - } else { - cols = append(cols, &Column{ - Name: name, - DataType: "any", - IsFuncCall: true, - }) - } - - case *ast.SubLink: - name := "exists" - if res.Name != nil { - name = *res.Name - } - switch n.SubLinkType { - case ast.EXISTS_SUBLINK: - cols = append(cols, &Column{Name: name, DataType: "bool", NotNull: true}) - case ast.EXPR_SUBLINK: - subcols, err := c.outputColumns(qc, n.Subselect) - if err != nil { - return nil, err - } - first := subcols[0] - if res.Name != nil { - first.Name = *res.Name - } - cols = append(cols, first) - default: - cols = append(cols, &Column{Name: name, DataType: "any", NotNull: false}) - } - - case *ast.TypeCast: - if n.TypeName == nil { - return nil, errors.New("no type name type cast") - } - name := "" - if ref, ok := n.Arg.(*ast.ColumnRef); ok { - name = astutils.Join(ref.Fields, "_") - } - if res.Name != nil { - name = *res.Name - } - // TODO Validate column names - col := toColumn(n.TypeName) - col.Name = name - // TODO Add correct, real type inference - if constant, ok := n.Arg.(*ast.A_Const); ok { - if _, ok := constant.Val.(*ast.Null); ok { - col.NotNull = false - } - } - cols = append(cols, col) - - case *ast.SelectStmt: - subcols, err := c.outputColumns(qc, n) - if err != nil { - return nil, err - } - first := subcols[0] - if res.Name != nil { - first.Name = *res.Name - } - cols = append(cols, first) - - default: - name := "" - if res.Name != nil { - name = *res.Name - } - cols = append(cols, &Column{Name: name, DataType: "any", NotNull: false}) - + columns, err := c.resolveValue(res, tables, qc, node) + if err != nil { + return nil, err } + cols = append(cols, columns...) } if n, ok := node.(*ast.SelectStmt); ok { @@ -394,12 +139,16 @@ func (c *Compiler) outputColumns(qc *QueryCatalog, node ast.Node) ([]*Column, er if !col.NotNull || col.Table == nil || col.skipTableRequiredCheck { continue } - for _, f := range n.FromClause.Items { - res := isTableRequired(f, col, tableRequired) - if res != tableNotFound { - col.NotNull = res == tableRequired - break + if n.FromClause != nil { + for _, f := range n.FromClause.Items { + res := isTableRequired(f, col, tableRequired) + if res != tableNotFound { + col.NotNull = res == tableRequired + break + } } + } else if debug.Active { + log.Printf("compiler: SelectStmt has nil FromClause while processing column %q from table %q\n", col.Name, col.Table.Name) } } } @@ -496,7 +245,11 @@ func (c *Compiler) sourceTables(qc *QueryCatalog, node ast.Node) ([]*Table, erro } case *ast.SelectStmt: var tv tableVisitor - astutils.Walk(&tv, n.FromClause) + if n.FromClause != nil { + astutils.Walk(&tv, n.FromClause) + } else if debug.Active { + log.Printf("compiler: SelectStmt has nil FromClause in sourceTables\n") + } list = &tv.list case *ast.TruncateStmt: list = astutils.Search(n.Relations, func(node ast.Node) bool { @@ -777,3 +530,324 @@ func findColumnForRef(ref *ast.ColumnRef, tables []*Table, targetList *ast.List) return nil } + +func (c *Compiler) resolveValue(res *ast.ResTarget, tables []*Table, qc *QueryCatalog, node ast.Node) ([]*Column, error) { + var cols []*Column + + switch n := res.Val.(type) { + + case *ast.A_Const: + name := "" + if res.Name != nil { + name = *res.Name + } + switch n.Val.(type) { + case *ast.String: + cols = append(cols, &Column{Name: name, DataType: "text", NotNull: true}) + case *ast.Integer: + cols = append(cols, &Column{Name: name, DataType: "int", NotNull: true}) + case *ast.Float: + cols = append(cols, &Column{Name: name, DataType: "float", NotNull: true}) + case *ast.Boolean: + cols = append(cols, &Column{Name: name, DataType: "bool", NotNull: true}) + default: + cols = append(cols, &Column{Name: name, DataType: "any", NotNull: false}) + } + + case *ast.A_Expr: + name := "" + if res.Name != nil { + name = *res.Name + } + switch op := astutils.Join(n.Name, ""); { + case lang.IsComparisonOperator(op): + // TODO: Generate a name for these operations + cols = append(cols, &Column{Name: name, DataType: "bool", NotNull: true}) + case lang.IsMathematicalOperator(op): + cols = append(cols, &Column{Name: name, DataType: "int", NotNull: true}) + default: + cols = append(cols, &Column{Name: name, DataType: "any", NotNull: false}) + } + + case *ast.BoolExpr: + name := "" + if res.Name != nil { + name = *res.Name + } + notNull := false + if len(n.Args.Items) == 1 { + switch n.Boolop { + case ast.BoolExprTypeIsNull, ast.BoolExprTypeIsNotNull: + notNull = true + case ast.BoolExprTypeNot: + sublink, ok := n.Args.Items[0].(*ast.SubLink) + if ok && sublink.SubLinkType == ast.EXISTS_SUBLINK { + notNull = true + if name == "" { + name = "not_exists" + } + } + } + } + cols = append(cols, &Column{Name: name, DataType: "bool", NotNull: notNull}) + + case *ast.CaseExpr: + name := "" + if res.Name != nil { + name = *res.Name + } + // TODO: The TypeCase and A_Const code has been copied from below. Instead, we + // need a recurse function to get the type of a node. + if tc, ok := n.Defresult.(*ast.TypeCast); ok { + if tc.TypeName == nil { + return nil, errors.New("no type name type cast") + } + name := "" + if ref, ok := tc.Arg.(*ast.ColumnRef); ok { + name = astutils.Join(ref.Fields, "_") + } + if res.Name != nil { + name = *res.Name + } + // TODO Validate column names + col := toColumn(tc.TypeName) + col.Name = name + cols = append(cols, col) + } else if aconst, ok := n.Defresult.(*ast.A_Const); ok { + switch aconst.Val.(type) { + case *ast.String: + cols = append(cols, &Column{Name: name, DataType: "text", NotNull: true}) + case *ast.Integer: + cols = append(cols, &Column{Name: name, DataType: "int", NotNull: true}) + case *ast.Float: + cols = append(cols, &Column{Name: name, DataType: "float", NotNull: true}) + case *ast.Boolean: + cols = append(cols, &Column{Name: name, DataType: "bool", NotNull: true}) + default: + cols = append(cols, &Column{Name: name, DataType: "any", NotNull: false}) + } + } else { + cols = append(cols, &Column{Name: name, DataType: "any", NotNull: false}) + } + + case *ast.CoalesceExpr: + name := "coalesce" + if res.Name != nil { + name = *res.Name + } + var firstColumn *Column + var shouldNotBeNull bool + for _, arg := range n.Args.Items { + if _, ok := arg.(*ast.A_Const); ok { + shouldNotBeNull = true + continue + } + if ref, ok := arg.(*ast.ColumnRef); ok { + columns, err := outputColumnRefs(res, tables, ref) + if err != nil { + return nil, err + } + for _, c := range columns { + if firstColumn == nil { + firstColumn = c + } + shouldNotBeNull = shouldNotBeNull || c.NotNull + } + } + } + if firstColumn != nil { + firstColumn.NotNull = shouldNotBeNull + firstColumn.skipTableRequiredCheck = true + cols = append(cols, firstColumn) + } else { + cols = append(cols, &Column{Name: name, DataType: "any", NotNull: false}) + } + + case *ast.ColumnRef: + if hasStarRef(n) { + + // add a column with a reference to an embedded table + if embed, ok := qc.embeds.Find(n); ok { + cols = append(cols, &Column{ + Name: embed.Table.Name, + EmbedTable: embed.Table, + }) + return cols, nil + } + + // Extract USING columns from the query to avoid duplication + usingMap := getJoinUsingMap(node) + + // TODO: This code is copied in func expand() + for _, t := range tables { + scope := astutils.Join(n.Fields, ".") + if scope != "" && scope != t.Rel.Name { + continue + } + for _, c := range t.Columns { + // Skip columns that are in USING clause for this table + // to avoid duplication (USING naturally returns only one column) + if usingInfo, ok := usingMap[t.Rel.Name]; ok && usingInfo.HasColumn(c.Name) { + continue + } + + cname := c.Name + if res.Name != nil { + cname = *res.Name + } + cols = append(cols, &Column{ + Name: cname, + OriginalName: c.Name, + Type: c.Type, + Scope: scope, + Table: c.Table, + TableAlias: t.Rel.Name, + DataType: c.DataType, + NotNull: c.NotNull, + Unsigned: c.Unsigned, + IsArray: c.IsArray, + ArrayDims: c.ArrayDims, + Length: c.Length, + }) + } + } + return cols, nil + } + + columns, err := outputColumnRefs(res, tables, n) + if err != nil { + return nil, err + } + cols = append(cols, columns...) + + case *ast.FuncCall: + rel := n.Func + name := rel.Name + if res.Name != nil { + name = *res.Name + } + + fun, err := qc.catalog.ResolveFuncCall(n) + if err == nil { + var returnType *ast.TypeName + var resolved bool + if c.TypeResolver != nil { + returnType = c.TypeResolver(n, fun, func(node ast.Node) (*catalog.Column, error) { + res := &ast.ResTarget{Val: node} + cols, err := c.resolveValue(res, tables, qc, node) + if err != nil { + return nil, err + } + if len(cols) == 0 { + return nil, fmt.Errorf("no columns returned") + } + col := cols[0] + var typeName ast.TypeName + if col.Type != nil { + typeName = *col.Type + } else { + typeName = ast.TypeName{Name: col.DataType} + } + return &catalog.Column{ + Name: col.Name, + Type: typeName, + IsNotNull: col.NotNull, + IsUnsigned: col.Unsigned, + IsArray: col.IsArray, + ArrayDims: col.ArrayDims, + Length: col.Length, + }, nil + }) + if returnType != nil { + resolved = true + } + } + if returnType == nil { + returnType = fun.ReturnType + } + + col := &Column{ + Name: name, + DataType: dataType(returnType), + NotNull: !fun.ReturnTypeNullable, + IsFuncCall: true, + } + if resolved { + col.Type = returnType + col.IsArray = returnType != nil && returnType.ArrayBounds != nil + col.ArrayDims = arrayDims(returnType) + } + cols = append(cols, col) + } else { + cols = append(cols, &Column{ + Name: name, + DataType: "any", + IsFuncCall: true, + }) + } + + case *ast.SubLink: + name := "exists" + if res.Name != nil { + name = *res.Name + } + switch n.SubLinkType { + case ast.EXISTS_SUBLINK: + cols = append(cols, &Column{Name: name, DataType: "bool", NotNull: true}) + case ast.EXPR_SUBLINK: + subcols, err := c.outputColumns(qc, n.Subselect) + if err != nil { + return nil, err + } + first := subcols[0] + if res.Name != nil { + first.Name = *res.Name + } + cols = append(cols, first) + default: + cols = append(cols, &Column{Name: name, DataType: "any", NotNull: false}) + } + + case *ast.TypeCast: + if n.TypeName == nil { + return nil, errors.New("no type name type cast") + } + name := "" + if ref, ok := n.Arg.(*ast.ColumnRef); ok { + name = astutils.Join(ref.Fields, "_") + } + if res.Name != nil { + name = *res.Name + } + // TODO Validate column names + col := toColumn(n.TypeName) + col.Name = name + // TODO Add correct, real type inference + if constant, ok := n.Arg.(*ast.A_Const); ok { + if _, ok := constant.Val.(*ast.Null); ok { + col.NotNull = false + } + } + cols = append(cols, col) + + case *ast.SelectStmt: + subcols, err := c.outputColumns(qc, n) + if err != nil { + return nil, err + } + first := subcols[0] + if res.Name != nil { + first.Name = *res.Name + } + cols = append(cols, first) + + default: + name := "" + if res.Name != nil { + name = *res.Name + } + cols = append(cols, &Column{Name: name, DataType: "any", NotNull: false}) + + } + return cols, nil +} diff --git a/internal/compiler/output_columns_test.go b/internal/compiler/output_columns_test.go new file mode 100644 index 0000000000..c0dbc3d6e3 --- /dev/null +++ b/internal/compiler/output_columns_test.go @@ -0,0 +1,122 @@ +package compiler + +import ( + "strings" + "testing" + + "github.com/sqlc-dev/sqlc/internal/config" + "github.com/sqlc-dev/sqlc/internal/engine/clickhouse" + "github.com/sqlc-dev/sqlc/internal/sql/ast" +) + +// TestClickHouseColumnNameResolution tests that column names are properly resolved +// from the catalog when processing SELECT statements with ClickHouse tables. +// This is a regression test for a bug where identifiers were being converted to +// string literals instead of ColumnRef nodes, preventing proper column name lookups. +func TestClickHouseColumnNameResolution(t *testing.T) { + parser := clickhouse.NewParser() + cat := clickhouse.NewCatalog() + + // Parse and add schema - CREATE DATABASE first, then table + schemaSQL := `CREATE DATABASE IF NOT EXISTS sqlc_example; +CREATE TABLE sqlc_example.users ( + id UInt32, + name String, + email String, + created_at DateTime +)` + + stmts, err := parser.Parse(strings.NewReader(schemaSQL)) + if err != nil { + t.Fatalf("Parse schema failed: %v", err) + } + + for _, stmt := range stmts { + if err := cat.Update(stmt, nil); err != nil { + t.Fatalf("Update catalog failed: %v", err) + } + } + + // Verify catalog is populated + t.Logf("Catalog schemas: %d", len(cat.Schemas)) + found := false + for _, schema := range cat.Schemas { + if schema.Name == "sqlc_example" { + found = true + t.Logf("Found sqlc_example schema with %d tables", len(schema.Tables)) + if len(schema.Tables) > 0 { + tbl := schema.Tables[0] + t.Logf(" Table: %s.%s with %d columns", schema.Name, tbl.Rel.Name, len(tbl.Columns)) + for _, col := range tbl.Columns { + t.Logf(" Column: %s (type: %v)", col.Name, col.Type) + } + } + } + } + if !found { + t.Fatal("sqlc_example schema not found in catalog") + } + + // Create compiler + conf := config.SQL{ + Engine: config.EngineClickHouse, + } + combo := config.CombinedSettings{ + Global: config.Config{}, + } + + c, err := NewCompiler(conf, combo) + if err != nil { + t.Fatalf("Failed to create compiler: %v", err) + } + + // Replace the catalog with our populated one + c.catalog = cat + + // Parse a SELECT query + querySQL := "SELECT id, name, email FROM sqlc_example.users WHERE id = 1;" + queryStmts, err := parser.Parse(strings.NewReader(querySQL)) + if err != nil { + t.Fatalf("Parse query failed: %v", err) + } + + if len(queryStmts) == 0 { + t.Fatal("No queries parsed") + } + + selectStmt := queryStmts[0].Raw.Stmt + if selectStmt == nil { + t.Fatal("Select statement is nil") + } + + selectAst, ok := selectStmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", selectStmt) + } + + // Build the query catalog first + qc, err := c.buildQueryCatalog(c.catalog, selectAst, nil) + if err != nil { + t.Fatalf("Failed to build query catalog: %v", err) + } + + // Get output columns + cols, err := c.outputColumns(qc, selectAst) + if err != nil { + t.Fatalf("Failed to get output columns: %v", err) + } + + // Check if names are properly resolved + if len(cols) != 3 { + t.Errorf("Expected 3 columns, got %d", len(cols)) + } + + expectedNames := []string{"id", "name", "email"} + for i, expected := range expectedNames { + if i < len(cols) { + if cols[i].Name != expected { + t.Errorf("Column %d: expected name %q, got %q", i, expected, cols[i].Name) + } + } + } +} diff --git a/internal/compiler/type_resolution_test.go b/internal/compiler/type_resolution_test.go new file mode 100644 index 0000000000..14e7099092 --- /dev/null +++ b/internal/compiler/type_resolution_test.go @@ -0,0 +1,538 @@ +package compiler + +import ( + "strings" + "testing" + + "github.com/sqlc-dev/sqlc/internal/config" + "github.com/sqlc-dev/sqlc/internal/opts" +) + +func TestNewCompilerClickHouse(t *testing.T) { + conf := config.SQL{ + Engine: config.EngineClickHouse, + } + + combo := config.CombinedSettings{ + Global: config.Config{}, + } + + c, err := NewCompiler(conf, combo) + if err != nil { + t.Fatalf("unexpected error creating ClickHouse compiler: %v", err) + } + + if c.parser == nil { + t.Error("expected parser to be set") + } + + if c.catalog == nil { + t.Error("expected catalog to be set") + } + + if c.parser.CommentSyntax().Dash == false { + t.Error("expected ClickHouse parser to support dash comments") + } + + if c.parser.CommentSyntax().SlashStar == false { + t.Error("expected ClickHouse parser to support slash-star comments") + } +} + +func TestClickHouseTypeResolver(t *testing.T) { + schema := ` +CREATE TABLE events ( + id UInt64, + tags Array(String), + scores Array(UInt32) +) ENGINE = Memory; +` + query := ` +-- name: TestArrayJoin :many +SELECT arrayJoin(tags) as tag FROM events; + +-- name: TestCount :one +SELECT count(*) FROM events; + +-- name: TestArgMin :one +SELECT argMin(id, id) FROM events; + +-- name: TestAny :one +SELECT any(tags) FROM events; +` + + conf := config.SQL{ + Engine: config.EngineClickHouse, + } + + c, err := NewCompiler(conf, config.CombinedSettings{}) + if err != nil { + t.Fatal(err) + } + + // Manually update catalog with schema + c.schema = append(c.schema, schema) + stmts, err := c.parser.Parse(strings.NewReader(schema)) + if err != nil { + t.Fatal(err) + } + for _, stmt := range stmts { + if err := c.catalog.Update(stmt, c); err != nil { + t.Fatal(err) + } + } + + // Parse queries + queryStmts, err := c.parser.Parse(strings.NewReader(query)) + if err != nil { + t.Fatal(err) + } + + for _, stmt := range queryStmts { + q, err := c.parseQuery(stmt.Raw, query, opts.Parser{}) + if err != nil { + t.Fatal(err) + } + if q == nil { + continue + } + + // Verify types + switch q.Metadata.Name { + case "TestArrayJoin": + if len(q.Columns) != 1 { + t.Errorf("TestArrayJoin: expected 1 column, got %d", len(q.Columns)) + } + col := q.Columns[0] + if col.DataType != "text" { // String maps to text + t.Errorf("TestArrayJoin: expected text, got %s", col.DataType) + } + if col.IsArray { + t.Errorf("TestArrayJoin: expected not array") + } + + case "TestCount": + if len(q.Columns) != 1 { + t.Errorf("TestCount: expected 1 column, got %d", len(q.Columns)) + } + col := q.Columns[0] + if col.DataType != "uint64" { + t.Errorf("TestCount: expected uint64, got %s", col.DataType) + } + + case "TestArgMin": + if len(q.Columns) != 1 { + t.Errorf("TestArgMin: expected 1 column, got %d", len(q.Columns)) + } + col := q.Columns[0] + if col.DataType != "uint64" { + t.Errorf("TestArgMin: expected uint64, got %s", col.DataType) + } + + case "TestAny": + if len(q.Columns) != 1 { + t.Errorf("TestAny: expected 1 column, got %d", len(q.Columns)) + } + col := q.Columns[0] + if col.DataType != "text" { // text[] in sqlc usually maps to text with IsArray=true + t.Errorf("TestAny: expected text, got %s", col.DataType) + } + if !col.IsArray { + t.Errorf("TestAny: expected array") + } + } + } +} + +func TestClickHouseLimitParameterType(t *testing.T) { + schema := ` +CREATE TABLE users ( + id UInt32, + name String, + email String, + created_at DateTime +) ENGINE = Memory; +` + query := ` +-- name: ListUsers :many +SELECT id, name, email, created_at +FROM users +LIMIT ?; +` + + conf := config.SQL{ + Engine: config.EngineClickHouse, + } + + c, err := NewCompiler(conf, config.CombinedSettings{}) + if err != nil { + t.Fatal(err) + } + + // Manually update catalog with schema + c.schema = append(c.schema, schema) + stmts, err := c.parser.Parse(strings.NewReader(schema)) + if err != nil { + t.Fatal(err) + } + for _, stmt := range stmts { + if err := c.catalog.Update(stmt, c); err != nil { + t.Fatal(err) + } + } + + // Parse queries + queryStmts, err := c.parser.Parse(strings.NewReader(query)) + if err != nil { + t.Fatal(err) + } + + for _, stmt := range queryStmts { + q, err := c.parseQuery(stmt.Raw, query, opts.Parser{}) + if err != nil { + t.Fatal(err) + } + if q == nil { + continue + } + + // Check parameters + if len(q.Params) != 1 { + t.Errorf("Expected 1 parameter, got %d", len(q.Params)) + } + + param := q.Params[0] + t.Logf("Parameter: Name=%s, DataType=%s, Number=%d", param.Column.Name, param.Column.DataType, param.Number) + + if param.Column.DataType != "integer" { + t.Errorf("Expected integer type for LIMIT parameter, got %s", param.Column.DataType) + } + } +} + +func TestClickHouseLowCardinality(t *testing.T) { + schema := ` +CREATE TABLE products ( + id UInt32, + category LowCardinality(String), + status LowCardinality(String), + priority LowCardinality(UInt8) +) ENGINE = Memory; +` + query := ` +-- name: GetProductsByCategory :many +SELECT id, category, status, priority FROM products WHERE category = ?; +` + + conf := config.SQL{ + Engine: config.EngineClickHouse, + } + + c, err := NewCompiler(conf, config.CombinedSettings{}) + if err != nil { + t.Fatal(err) + } + + // Manually update catalog with schema + c.schema = append(c.schema, schema) + stmts, err := c.parser.Parse(strings.NewReader(schema)) + if err != nil { + t.Fatal(err) + } + for _, stmt := range stmts { + if err := c.catalog.Update(stmt, c); err != nil { + t.Fatal(err) + } + } + + // Parse queries + queryStmts, err := c.parser.Parse(strings.NewReader(query)) + if err != nil { + t.Fatal(err) + } + + for _, stmt := range queryStmts { + q, err := c.parseQuery(stmt.Raw, query, opts.Parser{}) + if err != nil { + t.Fatal(err) + } + if q == nil { + continue + } + + // Check that LowCardinality columns are correctly resolved + if len(q.Columns) < 4 { + t.Fatalf("Expected at least 4 columns, got %d", len(q.Columns)) + } + + tests := []struct { + colIndex int + name string + dataType string + }{ + {0, "id", "uint32"}, + {1, "category", "text"}, // LowCardinality(String) -> String -> text + {2, "status", "text"}, // LowCardinality(String) -> String -> text + {3, "priority", "uint8"}, // LowCardinality(UInt8) -> UInt8 -> uint8 + } + + for _, test := range tests { + if test.colIndex >= len(q.Columns) { + t.Errorf("Column index %d out of bounds", test.colIndex) + continue + } + + col := q.Columns[test.colIndex] + if col.Name != test.name { + t.Errorf("Column %d: expected name %q, got %q", test.colIndex, test.name, col.Name) + } + if col.DataType != test.dataType { + t.Errorf("Column %q: expected type %q, got %q", col.Name, test.dataType, col.DataType) + } + } + + // Check parameter (the WHERE clause) + if len(q.Params) != 1 { + t.Errorf("Expected 1 parameter, got %d", len(q.Params)) + } else { + param := q.Params[0] + if param.Column.DataType != "text" { + t.Errorf("Expected text type for category parameter, got %s", param.Column.DataType) + } + } + } +} + +func TestClickHouseIPAddressTypes(t *testing.T) { + schema := ` +CREATE TABLE network_data ( + id UInt32, + source_ip IPv4, + dest_ip IPv4, + ipv6_addr IPv6, + nullable_ip Nullable(IPv4) +) ENGINE = Memory; +` + query := ` +-- name: GetNetworkData :one +SELECT id, source_ip, dest_ip, ipv6_addr, nullable_ip FROM network_data WHERE id = ?; + +-- name: FilterByIPv4 :many +SELECT id, source_ip FROM network_data WHERE source_ip = ?; + +-- name: FilterByIPv6 :many +SELECT id, ipv6_addr FROM network_data WHERE ipv6_addr = ?; +` + + conf := config.SQL{ + Engine: config.EngineClickHouse, + } + + c, err := NewCompiler(conf, config.CombinedSettings{}) + if err != nil { + t.Fatal(err) + } + + // Manually update catalog with schema + c.schema = append(c.schema, schema) + stmts, err := c.parser.Parse(strings.NewReader(schema)) + if err != nil { + t.Fatal(err) + } + for _, stmt := range stmts { + if err := c.catalog.Update(stmt, c); err != nil { + t.Fatal(err) + } + } + + // Parse queries + queryStmts, err := c.parser.Parse(strings.NewReader(query)) + if err != nil { + t.Fatal(err) + } + + for _, stmt := range queryStmts { + q, err := c.parseQuery(stmt.Raw, query, opts.Parser{}) + if err != nil { + t.Fatal(err) + } + if q == nil { + continue + } + + switch q.Metadata.Name { + case "GetNetworkData": + if len(q.Columns) != 5 { + t.Errorf("GetNetworkData: expected 5 columns, got %d", len(q.Columns)) + } + + tests := []struct { + colIndex int + name string + dataType string + }{ + {0, "id", "uint32"}, + {1, "source_ip", "ipv4"}, + {2, "dest_ip", "ipv4"}, + {3, "ipv6_addr", "ipv6"}, + {4, "nullable_ip", "ipv4"}, + } + + for _, test := range tests { + if test.colIndex >= len(q.Columns) { + t.Errorf("Column index %d out of bounds", test.colIndex) + continue + } + + col := q.Columns[test.colIndex] + if col.Name != test.name { + t.Errorf("Column %d: expected name %q, got %q", test.colIndex, test.name, col.Name) + } + if col.DataType != test.dataType { + t.Errorf("Column %q: expected type %q, got %q", col.Name, test.dataType, col.DataType) + } + } + + // Check parameter (id filter) + if len(q.Params) != 1 { + t.Errorf("Expected 1 parameter, got %d", len(q.Params)) + } else { + param := q.Params[0] + if param.Column.DataType != "uint32" { + t.Errorf("Expected uint32 type for id parameter, got %s", param.Column.DataType) + } + } + + case "FilterByIPv4": + if len(q.Columns) != 2 { + t.Errorf("FilterByIPv4: expected 2 columns, got %d", len(q.Columns)) + } + + col := q.Columns[1] + if col.Name != "source_ip" { + t.Errorf("Expected column name source_ip, got %q", col.Name) + } + if col.DataType != "ipv4" { + t.Errorf("Expected ipv4 type, got %s", col.DataType) + } + + // Check parameter (IPv4 filter) + if len(q.Params) != 1 { + t.Errorf("Expected 1 parameter, got %d", len(q.Params)) + } else { + param := q.Params[0] + if param.Column.DataType != "ipv4" { + t.Errorf("Expected ipv4 type for IP parameter, got %s", param.Column.DataType) + } + } + + case "FilterByIPv6": + if len(q.Columns) != 2 { + t.Errorf("FilterByIPv6: expected 2 columns, got %d", len(q.Columns)) + } + + col := q.Columns[1] + if col.Name != "ipv6_addr" { + t.Errorf("Expected column name ipv6_addr, got %q", col.Name) + } + if col.DataType != "ipv6" { + t.Errorf("Expected ipv6 type, got %s", col.DataType) + } + + // Check parameter (IPv6 filter) + if len(q.Params) != 1 { + t.Errorf("Expected 1 parameter, got %d", len(q.Params)) + } else { + param := q.Params[0] + if param.Column.DataType != "ipv6" { + t.Errorf("Expected ipv6 type for IP parameter, got %s", param.Column.DataType) + } + } + } + } +} + +func TestClickHouseMapType(t *testing.T) { + schema := ` +CREATE TABLE config ( + id UInt32, + settings Map(String, String), + metrics Map(String, UInt64), + nested_data Map(String, Array(String)), + invalid_key Map(Array(String), String) +) ENGINE = Memory; +` + query := ` +-- name: GetConfig :one +SELECT id, settings, metrics, nested_data, invalid_key FROM config WHERE id = ?; +` + + conf := config.SQL{ + Engine: config.EngineClickHouse, + } + + c, err := NewCompiler(conf, config.CombinedSettings{}) + if err != nil { + t.Fatal(err) + } + + // Manually update catalog with schema + c.schema = append(c.schema, schema) + stmts, err := c.parser.Parse(strings.NewReader(schema)) + if err != nil { + t.Fatal(err) + } + for _, stmt := range stmts { + if err := c.catalog.Update(stmt, c); err != nil { + t.Fatal(err) + } + } + + // Parse queries + queryStmts, err := c.parser.Parse(strings.NewReader(query)) + if err != nil { + t.Fatal(err) + } + + for _, stmt := range queryStmts { + q, err := c.parseQuery(stmt.Raw, query, opts.Parser{}) + if err != nil { + t.Fatal(err) + } + if q == nil { + continue + } + + // Check that Map columns are correctly resolved + if len(q.Columns) < 5 { + t.Fatalf("Expected at least 5 columns, got %d", len(q.Columns)) + } + + tests := []struct { + colIndex int + name string + dataType string + }{ + {0, "id", "uint32"}, + {1, "settings", "map[string]string"}, // Map(String, String) -> map[string]string + {2, "metrics", "map[string]uint64"}, // Map(String, UInt64) -> map[string]uint64 + {3, "nested_data", "map[string][]string"}, // Map(String, Array(String)) -> map[string][]string + {4, "invalid_key", "map[string]interface{}"}, // Map(Array(String), String) -> falls back due to invalid key + } + + for _, test := range tests { + if test.colIndex >= len(q.Columns) { + t.Errorf("Column index %d out of bounds", test.colIndex) + continue + } + + col := q.Columns[test.colIndex] + if col.Name != test.name { + t.Errorf("Column %d: expected name %q, got %q", test.colIndex, test.name, col.Name) + } + if col.DataType != test.dataType { + t.Errorf("Column %q: expected type %q, got %q", col.Name, test.dataType, col.DataType) + } + } + } +} diff --git a/internal/compiler/using.go b/internal/compiler/using.go new file mode 100644 index 0000000000..c9b24df79b --- /dev/null +++ b/internal/compiler/using.go @@ -0,0 +1,73 @@ +package compiler + +import ( + "github.com/sqlc-dev/sqlc/internal/sql/ast" + "github.com/sqlc-dev/sqlc/internal/sql/astutils" +) + +// UsingInfo tracks USING columns for a specific join to avoid duplication +// when expanding SELECT * across multiple tables +type UsingInfo struct { + // columns is a set of column names that appear in the USING clause + columns map[string]bool +} + +// NewUsingInfo creates a new UsingInfo from a JoinExpr's UsingClause +func NewUsingInfo(joinExpr *ast.JoinExpr) *UsingInfo { + ui := &UsingInfo{ + columns: make(map[string]bool), + } + + if joinExpr == nil || joinExpr.UsingClause == nil { + return ui + } + + // Extract column names from the USING clause + for _, item := range joinExpr.UsingClause.Items { + if str, ok := item.(*ast.String); ok { + ui.columns[str.Str] = true + } + } + + return ui +} + +// HasColumn checks if a column name is in the USING clause +func (ui *UsingInfo) HasColumn(colName string) bool { + return ui.columns[colName] +} + +// getJoinUsingMap builds a map of table names to their USING columns +// This helps identify which columns should not be duplicated when expanding * +func getJoinUsingMap(node ast.Node) map[string]*UsingInfo { + usingMap := make(map[string]*UsingInfo) + + // Find all JoinExpr nodes in the query and extract USING information + visitor := &joinVisitor{ + usingMap: usingMap, + } + astutils.Walk(visitor, node) + + return usingMap +} + +// joinVisitor traverses the AST to find and track USING information in joins +type joinVisitor struct { + usingMap map[string]*UsingInfo +} + +func (v *joinVisitor) Visit(node ast.Node) astutils.Visitor { + if join, ok := node.(*ast.JoinExpr); ok { + // Create UsingInfo for this join + // The right argument of the join is the table being joined in + if rarg, ok := join.Rarg.(*ast.RangeVar); ok { + if rarg.Relname != nil { + usingInfo := NewUsingInfo(join) + if len(usingInfo.columns) > 0 { + v.usingMap[*rarg.Relname] = usingInfo + } + } + } + } + return v +} diff --git a/internal/compiler/using_test.go b/internal/compiler/using_test.go new file mode 100644 index 0000000000..6e4c51846a --- /dev/null +++ b/internal/compiler/using_test.go @@ -0,0 +1,66 @@ +package compiler + +import ( + "testing" + + "github.com/sqlc-dev/sqlc/internal/sql/ast" +) + +func TestGetJoinUsingMap(t *testing.T) { + // Create a mock JoinExpr with USING clause + joinExpr := &ast.JoinExpr{ + UsingClause: &ast.List{ + Items: []ast.Node{ + &ast.String{Str: "order_id"}, + }, + }, + Rarg: &ast.RangeVar{ + Relname: strPtr("shipments"), + }, + } + + selectStmt := &ast.SelectStmt{ + FromClause: &ast.List{ + Items: []ast.Node{ + joinExpr, + }, + }, + } + + usingMap := getJoinUsingMap(selectStmt) + + if info, ok := usingMap["shipments"]; ok { + if !info.HasColumn("order_id") { + t.Errorf("Expected order_id to be in USING clause for shipments") + } + } else { + t.Errorf("Expected shipments to be in using map") + } +} + +func TestNewUsingInfo(t *testing.T) { + joinExpr := &ast.JoinExpr{ + UsingClause: &ast.List{ + Items: []ast.Node{ + &ast.String{Str: "id"}, + &ast.String{Str: "type_id"}, + }, + }, + } + + info := NewUsingInfo(joinExpr) + + if !info.HasColumn("id") { + t.Errorf("Expected id to be in USING columns") + } + if !info.HasColumn("type_id") { + t.Errorf("Expected type_id to be in USING columns") + } + if info.HasColumn("other_id") { + t.Errorf("Did not expect other_id to be in USING columns") + } +} + +func strPtr(s string) *string { + return &s +} diff --git a/internal/config/config.go b/internal/config/config.go index 0ff805fccd..5880803567 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -51,6 +51,7 @@ func (p *Paths) UnmarshalYAML(unmarshal func(interface{}) error) error { } const ( + EngineClickHouse Engine = "clickhouse" EngineMySQL Engine = "mysql" EnginePostgreSQL Engine = "postgresql" EngineSQLite Engine = "sqlite" diff --git a/internal/config/v_one.json b/internal/config/v_one.json index a0667a7c9c..09e3c2204a 100644 --- a/internal/config/v_one.json +++ b/internal/config/v_one.json @@ -33,6 +33,7 @@ "properties": { "engine": { "enum": [ + "clickhouse", "postgresql", "mysql", "sqlite" @@ -192,6 +193,7 @@ }, "engine": { "enum": [ + "clickhouse", "postgresql", "mysql", "sqlite" @@ -300,6 +302,7 @@ }, "engine": { "enum": [ + "clickhouse", "postgresql", "mysql", "sqlite" diff --git a/internal/config/v_two.json b/internal/config/v_two.json index acf914997d..96770bdbb9 100644 --- a/internal/config/v_two.json +++ b/internal/config/v_two.json @@ -36,6 +36,7 @@ }, "engine": { "enum": [ + "clickhouse", "postgresql", "mysql", "sqlite" @@ -201,6 +202,7 @@ }, "engine": { "enum": [ + "clickhouse", "postgresql", "mysql", "sqlite" @@ -358,6 +360,7 @@ }, "engine": { "enum": [ + "clickhouse", "postgresql", "mysql", "sqlite" diff --git a/internal/endtoend/ddl_test.go b/internal/endtoend/ddl_test.go index bed9333743..7a49f7a923 100644 --- a/internal/endtoend/ddl_test.go +++ b/internal/endtoend/ddl_test.go @@ -54,6 +54,8 @@ func TestValidSchema(t *testing.T) { local.PostgreSQL(t, schema) case config.EngineMySQL: local.MySQL(t, schema) + case config.EngineClickHouse: + local.ClickHouse(t, schema) } }) } diff --git a/internal/endtoend/endtoend_test.go b/internal/endtoend/endtoend_test.go index 537307e453..f26c0250e8 100644 --- a/internal/endtoend/endtoend_test.go +++ b/internal/endtoend/endtoend_test.go @@ -112,7 +112,7 @@ func TestReplay(t *testing.T) { // t.Parallel() ctx := context.Background() - var mysqlURI, postgresURI string + var mysqlURI, postgresURI, clickhouseURI string if err := docker.Installed(); err == nil { { host, err := docker.StartPostgreSQLServer(ctx) @@ -128,6 +128,13 @@ func TestReplay(t *testing.T) { } mysqlURI = host } + { + host, err := docker.StartClickHouseServer(ctx) + if err != nil { + t.Fatalf("starting clickhouse failed: %s", err) + } + clickhouseURI = host + } } contexts := map[string]textContext{ @@ -150,6 +157,12 @@ func TestReplay(t *testing.T) { Engine: config.EngineMySQL, URI: mysqlURI, }, + + { + Name: "clickhouse", + Engine: config.EngineClickHouse, + URI: clickhouseURI, + }, } for i := range c.SQL { switch c.SQL[i].Engine { @@ -161,6 +174,10 @@ func TestReplay(t *testing.T) { c.SQL[i].Database = &config.Database{ Managed: true, } + case config.EngineClickHouse: + c.SQL[i].Database = &config.Database{ + Managed: true, + } case config.EngineSQLite: c.SQL[i].Database = &config.Database{ Managed: true, diff --git a/internal/endtoend/testdata/clickhouse_advanced/go/db.go b/internal/endtoend/testdata/clickhouse_advanced/go/db.go new file mode 100644 index 0000000000..cd5bbb8e08 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_advanced/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +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/clickhouse_advanced/go/models.go b/internal/endtoend/testdata/clickhouse_advanced/go/models.go new file mode 100644 index 0000000000..1605ea1f7d --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_advanced/go/models.go @@ -0,0 +1,24 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +import ( + "time" +) + +type Activity struct { + ID uint32 + UserID uint32 + Action string + Timestamp time.Time +} + +type Transaction struct { + ID uint32 + UserID uint32 + Amount float64 + Category string + CreatedAt time.Time +} diff --git a/internal/endtoend/testdata/clickhouse_advanced/go/query.sql.go b/internal/endtoend/testdata/clickhouse_advanced/go/query.sql.go new file mode 100644 index 0000000000..f04270a8f2 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_advanced/go/query.sql.go @@ -0,0 +1,112 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: query.sql + +package db + +import ( + "context" + "time" +) + +const getTransactionClassification = `-- name: GetTransactionClassification :many +SELECT + id, + user_id, + amount, + CASE + WHEN amount > 1000 THEN 'high' + WHEN amount > 100 THEN 'medium' + ELSE 'low' + END as classification, + category +FROM transactions +WHERE created_at >= ? +ORDER BY amount DESC; + +` + +type GetTransactionClassificationRow struct { + ID uint32 + UserID uint32 + Amount float64 + Classification string + Category string +} + +// IN operator with subquery style +func (q *Queries) GetTransactionClassification(ctx context.Context, createdAt time.Time) ([]GetTransactionClassificationRow, error) { + rows, err := q.db.QueryContext(ctx, getTransactionClassification, createdAt) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetTransactionClassificationRow + for rows.Next() { + var i GetTransactionClassificationRow + if err := rows.Scan( + &i.ID, + &i.UserID, + &i.Amount, + &i.Classification, + &i.Category, + ); 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 +} + +const getTransactionsByCategory = `-- name: GetTransactionsByCategory :many +SELECT + id, + user_id, + amount, + category +FROM transactions +WHERE category IN ('groceries', 'utilities', 'transportation') +ORDER BY created_at DESC; +` + +type GetTransactionsByCategoryRow struct { + ID uint32 + UserID uint32 + Amount float64 + Category string +} + +func (q *Queries) GetTransactionsByCategory(ctx context.Context) ([]GetTransactionsByCategoryRow, error) { + rows, err := q.db.QueryContext(ctx, getTransactionsByCategory) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetTransactionsByCategoryRow + for rows.Next() { + var i GetTransactionsByCategoryRow + if err := rows.Scan( + &i.ID, + &i.UserID, + &i.Amount, + &i.Category, + ); 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/clickhouse_advanced/query.sql b/internal/endtoend/testdata/clickhouse_advanced/query.sql new file mode 100644 index 0000000000..c56e97ad91 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_advanced/query.sql @@ -0,0 +1,26 @@ +-- CASE expression +-- name: GetTransactionClassification :many +SELECT + id, + user_id, + amount, + CASE + WHEN amount > 1000 THEN 'high' + WHEN amount > 100 THEN 'medium' + ELSE 'low' + END as classification, + category +FROM transactions +WHERE created_at >= ? +ORDER BY amount DESC; + +-- IN operator with subquery style +-- name: GetTransactionsByCategory :many +SELECT + id, + user_id, + amount, + category +FROM transactions +WHERE category IN ('groceries', 'utilities', 'transportation') +ORDER BY created_at DESC; diff --git a/internal/endtoend/testdata/clickhouse_advanced/schema.sql b/internal/endtoend/testdata/clickhouse_advanced/schema.sql new file mode 100644 index 0000000000..907e6650f8 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_advanced/schema.sql @@ -0,0 +1,20 @@ +CREATE TABLE IF NOT EXISTS transactions +( + id UInt32, + user_id UInt32, + amount Float64, + category String, + created_at DateTime +) +ENGINE = MergeTree() +ORDER BY (id, created_at); + +CREATE TABLE IF NOT EXISTS activities +( + id UInt32, + user_id UInt32, + action String, + timestamp DateTime +) +ENGINE = MergeTree() +ORDER BY (id, timestamp); diff --git a/internal/endtoend/testdata/clickhouse_advanced/sqlc.json b/internal/endtoend/testdata/clickhouse_advanced/sqlc.json new file mode 100644 index 0000000000..439f37f401 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_advanced/sqlc.json @@ -0,0 +1,16 @@ +{ + "version": "2", + "sql": [ + { + "schema": "schema.sql", + "queries": "query.sql", + "engine": "clickhouse", + "gen": { + "go": { + "out": "go", + "package": "db" + } + } + } + ] +} diff --git a/internal/endtoend/testdata/clickhouse_aggregates/go/db.go b/internal/endtoend/testdata/clickhouse_aggregates/go/db.go new file mode 100644 index 0000000000..cd5bbb8e08 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_aggregates/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +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/clickhouse_aggregates/go/models.go b/internal/endtoend/testdata/clickhouse_aggregates/go/models.go new file mode 100644 index 0000000000..b1c0d19059 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_aggregates/go/models.go @@ -0,0 +1,18 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +import ( + "time" +) + +type Sale struct { + ID uint32 + ProductID uint32 + Category string + Amount float64 + Quantity uint32 + CreatedAt time.Time +} diff --git a/internal/endtoend/testdata/clickhouse_aggregates/go/query.sql.go b/internal/endtoend/testdata/clickhouse_aggregates/go/query.sql.go new file mode 100644 index 0000000000..8518c88c98 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_aggregates/go/query.sql.go @@ -0,0 +1,206 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: query.sql + +package db + +import ( + "context" + "time" +) + +const getCategoryStats = `-- name: GetCategoryStats :many +SELECT + category, + COUNT(*) as all_sales, + countIf(amount > 100) as high_value_sales, + sumIf(amount, quantity > 5) as revenue_bulk_orders, + avgIf(amount, amount > 50) as avg_high_value +FROM sales +WHERE created_at >= ? +GROUP BY category; + +` + +type GetCategoryStatsRow struct { + Category string + AllSales uint64 + HighValueSales uint64 + RevenueBulkOrders float64 + AvgHighValue float64 +} + +// HAVING clause +func (q *Queries) GetCategoryStats(ctx context.Context, createdAt time.Time) ([]GetCategoryStatsRow, error) { + rows, err := q.db.QueryContext(ctx, getCategoryStats, createdAt) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetCategoryStatsRow + for rows.Next() { + var i GetCategoryStatsRow + if err := rows.Scan( + &i.Category, + &i.AllSales, + &i.HighValueSales, + &i.RevenueBulkOrders, + &i.AvgHighValue, + ); 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 +} + +const getProductCategoryStats = `-- name: GetProductCategoryStats :many +SELECT + product_id, + category, + COUNT(*) as count, + SUM(amount) as total +FROM sales +GROUP BY product_id, category +ORDER BY product_id, total DESC; +` + +type GetProductCategoryStatsRow struct { + ProductID uint32 + Category string + Count uint64 + Total float64 +} + +func (q *Queries) GetProductCategoryStats(ctx context.Context) ([]GetProductCategoryStatsRow, error) { + rows, err := q.db.QueryContext(ctx, getProductCategoryStats) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetProductCategoryStatsRow + for rows.Next() { + var i GetProductCategoryStatsRow + if err := rows.Scan( + &i.ProductID, + &i.Category, + &i.Count, + &i.Total, + ); 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 +} + +const getSalesStatistics = `-- name: GetSalesStatistics :many +SELECT + category, + COUNT(*) as total_sales, + SUM(amount) as total_revenue, + AVG(amount) as avg_amount, + MIN(amount) as min_amount, + MAX(amount) as max_amount, + SUM(quantity) as total_quantity +FROM sales +GROUP BY category +ORDER BY total_revenue DESC; + +` + +type GetSalesStatisticsRow struct { + Category string + TotalSales uint64 + TotalRevenue float64 + AvgAmount float64 + MinAmount float64 + MaxAmount float64 + TotalQuantity uint32 +} + +// Conditional aggregates +func (q *Queries) GetSalesStatistics(ctx context.Context) ([]GetSalesStatisticsRow, error) { + rows, err := q.db.QueryContext(ctx, getSalesStatistics) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetSalesStatisticsRow + for rows.Next() { + var i GetSalesStatisticsRow + if err := rows.Scan( + &i.Category, + &i.TotalSales, + &i.TotalRevenue, + &i.AvgAmount, + &i.MinAmount, + &i.MaxAmount, + &i.TotalQuantity, + ); 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 +} + +const getTopCategories = `-- name: GetTopCategories :many +SELECT + category, + COUNT(*) as sale_count, + SUM(amount) as total_amount +FROM sales +GROUP BY category +ORDER BY total_amount DESC; + +` + +type GetTopCategoriesRow struct { + Category string + SaleCount uint64 + TotalAmount float64 +} + +// Multiple GROUP BY columns +func (q *Queries) GetTopCategories(ctx context.Context) ([]GetTopCategoriesRow, error) { + rows, err := q.db.QueryContext(ctx, getTopCategories) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetTopCategoriesRow + for rows.Next() { + var i GetTopCategoriesRow + if err := rows.Scan(&i.Category, &i.SaleCount, &i.TotalAmount); 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/clickhouse_aggregates/query.sql b/internal/endtoend/testdata/clickhouse_aggregates/query.sql new file mode 100644 index 0000000000..094d7a215a --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_aggregates/query.sql @@ -0,0 +1,46 @@ +-- Basic aggregates +-- name: GetSalesStatistics :many +SELECT + category, + COUNT(*) as total_sales, + SUM(amount) as total_revenue, + AVG(amount) as avg_amount, + MIN(amount) as min_amount, + MAX(amount) as max_amount, + SUM(quantity) as total_quantity +FROM sales +GROUP BY category +ORDER BY total_revenue DESC; + +-- Conditional aggregates +-- name: GetCategoryStats :many +SELECT + category, + COUNT(*) as all_sales, + countIf(amount > 100) as high_value_sales, + sumIf(amount, quantity > 5) as revenue_bulk_orders, + avgIf(amount, amount > 50) as avg_high_value +FROM sales +WHERE created_at >= ? +GROUP BY category; + +-- HAVING clause +-- name: GetTopCategories :many +SELECT + category, + COUNT(*) as sale_count, + SUM(amount) as total_amount +FROM sales +GROUP BY category +ORDER BY total_amount DESC; + +-- Multiple GROUP BY columns +-- name: GetProductCategoryStats :many +SELECT + product_id, + category, + COUNT(*) as count, + SUM(amount) as total +FROM sales +GROUP BY product_id, category +ORDER BY product_id, total DESC; diff --git a/internal/endtoend/testdata/clickhouse_aggregates/schema.sql b/internal/endtoend/testdata/clickhouse_aggregates/schema.sql new file mode 100644 index 0000000000..ea7087bc41 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_aggregates/schema.sql @@ -0,0 +1,11 @@ +CREATE TABLE IF NOT EXISTS sales +( + id UInt32, + product_id UInt32, + category String, + amount Float64, + quantity UInt32, + created_at DateTime +) +ENGINE = MergeTree() +ORDER BY (id, created_at); diff --git a/internal/endtoend/testdata/clickhouse_aggregates/sqlc.json b/internal/endtoend/testdata/clickhouse_aggregates/sqlc.json new file mode 100644 index 0000000000..439f37f401 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_aggregates/sqlc.json @@ -0,0 +1,16 @@ +{ + "version": "2", + "sql": [ + { + "schema": "schema.sql", + "queries": "query.sql", + "engine": "clickhouse", + "gen": { + "go": { + "out": "go", + "package": "db" + } + } + } + ] +} diff --git a/internal/endtoend/testdata/clickhouse_arrays/go/db.go b/internal/endtoend/testdata/clickhouse_arrays/go/db.go new file mode 100644 index 0000000000..cd5bbb8e08 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_arrays/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +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/clickhouse_arrays/go/models.go b/internal/endtoend/testdata/clickhouse_arrays/go/models.go new file mode 100644 index 0000000000..7b86b1f661 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_arrays/go/models.go @@ -0,0 +1,23 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +import ( + "time" +) + +type Event struct { + ID uint32 + Name string + Timestamp time.Time + Properties string +} + +type Product struct { + ID uint32 + Name string + Tags []string + Ratings []string +} diff --git a/internal/endtoend/testdata/clickhouse_arrays/go/query.sql.go b/internal/endtoend/testdata/clickhouse_arrays/go/query.sql.go new file mode 100644 index 0000000000..07075b945f --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_arrays/go/query.sql.go @@ -0,0 +1,96 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: query.sql + +package db + +import ( + "context" +) + +const getProductTagsArray = `-- name: GetProductTagsArray :many +SELECT + id, + name, + arrayJoin(tags) as tag +FROM products +WHERE id = ? +ORDER BY tag; + +` + +type GetProductTagsArrayRow struct { + ID uint32 + Name string + Tag string +} + +// Array functions +func (q *Queries) GetProductTagsArray(ctx context.Context, id uint32) ([]GetProductTagsArrayRow, error) { + rows, err := q.db.QueryContext(ctx, getProductTagsArray, id) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetProductTagsArrayRow + for rows.Next() { + var i GetProductTagsArrayRow + if err := rows.Scan(&i.ID, &i.Name, &i.Tag); 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 +} + +const getProductsWithArrayFunctions = `-- name: GetProductsWithArrayFunctions :many +SELECT + id, + name, + length(tags) as tag_count, + length(ratings) as rating_count +FROM products +ORDER BY tag_count DESC; +` + +type GetProductsWithArrayFunctionsRow struct { + ID uint32 + Name string + TagCount uint64 + RatingCount uint64 +} + +func (q *Queries) GetProductsWithArrayFunctions(ctx context.Context) ([]GetProductsWithArrayFunctionsRow, error) { + rows, err := q.db.QueryContext(ctx, getProductsWithArrayFunctions) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetProductsWithArrayFunctionsRow + for rows.Next() { + var i GetProductsWithArrayFunctionsRow + if err := rows.Scan( + &i.ID, + &i.Name, + &i.TagCount, + &i.RatingCount, + ); 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/clickhouse_arrays/query.sql b/internal/endtoend/testdata/clickhouse_arrays/query.sql new file mode 100644 index 0000000000..2405e34819 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_arrays/query.sql @@ -0,0 +1,19 @@ +-- ARRAY JOIN with arrays +-- name: GetProductTagsArray :many +SELECT + id, + name, + arrayJoin(tags) as tag +FROM products +WHERE id = ? +ORDER BY tag; + +-- Array functions +-- name: GetProductsWithArrayFunctions :many +SELECT + id, + name, + length(tags) as tag_count, + length(ratings) as rating_count +FROM products +ORDER BY tag_count DESC; diff --git a/internal/endtoend/testdata/clickhouse_arrays/schema.sql b/internal/endtoend/testdata/clickhouse_arrays/schema.sql new file mode 100644 index 0000000000..8356ff87a7 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_arrays/schema.sql @@ -0,0 +1,22 @@ +CREATE TABLE IF NOT EXISTS products +( + id UInt32, + name String, + tags Array(String), + ratings Array(UInt8) +) +ENGINE = MergeTree() +ORDER BY id; + +CREATE TABLE IF NOT EXISTS events +( + id UInt32, + name String, + timestamp DateTime, + properties Nested( + key String, + value String + ) +) +ENGINE = MergeTree() +ORDER BY (id, timestamp); diff --git a/internal/endtoend/testdata/clickhouse_arrays/sqlc.json b/internal/endtoend/testdata/clickhouse_arrays/sqlc.json new file mode 100644 index 0000000000..439f37f401 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_arrays/sqlc.json @@ -0,0 +1,16 @@ +{ + "version": "2", + "sql": [ + { + "schema": "schema.sql", + "queries": "query.sql", + "engine": "clickhouse", + "gen": { + "go": { + "out": "go", + "package": "db" + } + } + } + ] +} diff --git a/internal/endtoend/testdata/clickhouse_cast/go/db.go b/internal/endtoend/testdata/clickhouse_cast/go/db.go new file mode 100644 index 0000000000..cd5bbb8e08 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_cast/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +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/clickhouse_cast/go/models.go b/internal/endtoend/testdata/clickhouse_cast/go/models.go new file mode 100644 index 0000000000..a276b3d559 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_cast/go/models.go @@ -0,0 +1,12 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +type Datum struct { + ID uint32 + Amount string + Quantity string + CreatedDate string +} diff --git a/internal/endtoend/testdata/clickhouse_cast/go/query.sql.go b/internal/endtoend/testdata/clickhouse_cast/go/query.sql.go new file mode 100644 index 0000000000..98b7e9c930 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_cast/go/query.sql.go @@ -0,0 +1,140 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: query.sql + +package db + +import ( + "context" + "time" +) + +const getCastToDate = `-- name: GetCastToDate :many +SELECT id, CAST(created_date AS Date) AS date FROM data; +` + +type GetCastToDateRow struct { + ID uint32 + Date time.Time +} + +func (q *Queries) GetCastToDate(ctx context.Context) ([]GetCastToDateRow, error) { + rows, err := q.db.QueryContext(ctx, getCastToDate) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetCastToDateRow + for rows.Next() { + var i GetCastToDateRow + if err := rows.Scan(&i.ID, &i.Date); 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 +} + +const getCastToFloat = `-- name: GetCastToFloat :many +SELECT id, CAST(amount AS Float32) AS amount FROM data; +` + +type GetCastToFloatRow struct { + ID uint32 + Amount float32 +} + +func (q *Queries) GetCastToFloat(ctx context.Context) ([]GetCastToFloatRow, error) { + rows, err := q.db.QueryContext(ctx, getCastToFloat) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetCastToFloatRow + for rows.Next() { + var i GetCastToFloatRow + if err := rows.Scan(&i.ID, &i.Amount); 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 +} + +const getCastToInt = `-- name: GetCastToInt :many +SELECT id, CAST(quantity AS UInt32) AS quantity FROM data; +` + +type GetCastToIntRow struct { + ID uint32 + Quantity uint32 +} + +func (q *Queries) GetCastToInt(ctx context.Context) ([]GetCastToIntRow, error) { + rows, err := q.db.QueryContext(ctx, getCastToInt) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetCastToIntRow + for rows.Next() { + var i GetCastToIntRow + if err := rows.Scan(&i.ID, &i.Quantity); 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 +} + +const getMultipleCasts = `-- name: GetMultipleCasts :many +SELECT id, CAST(amount AS Float32) AS amount_float, CAST(quantity AS UInt32) AS quantity_int FROM data; +` + +type GetMultipleCastsRow struct { + ID uint32 + AmountFloat float32 + QuantityInt uint32 +} + +func (q *Queries) GetMultipleCasts(ctx context.Context) ([]GetMultipleCastsRow, error) { + rows, err := q.db.QueryContext(ctx, getMultipleCasts) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetMultipleCastsRow + for rows.Next() { + var i GetMultipleCastsRow + if err := rows.Scan(&i.ID, &i.AmountFloat, &i.QuantityInt); 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/clickhouse_cast/query.sql b/internal/endtoend/testdata/clickhouse_cast/query.sql new file mode 100644 index 0000000000..57640735db --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_cast/query.sql @@ -0,0 +1,11 @@ +-- name: GetCastToFloat :many +SELECT id, CAST(amount AS Float32) AS amount FROM data; + +-- name: GetCastToInt :many +SELECT id, CAST(quantity AS UInt32) AS quantity FROM data; + +-- name: GetCastToDate :many +SELECT id, CAST(created_date AS Date) AS date FROM data; + +-- name: GetMultipleCasts :many +SELECT id, CAST(amount AS Float32) AS amount_float, CAST(quantity AS UInt32) AS quantity_int FROM data; diff --git a/internal/endtoend/testdata/clickhouse_cast/schema.sql b/internal/endtoend/testdata/clickhouse_cast/schema.sql new file mode 100644 index 0000000000..a08432d55b --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_cast/schema.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS data +( + id UInt32, + amount String, + quantity String, + created_date String +) +ENGINE = MergeTree() +ORDER BY id; diff --git a/internal/endtoend/testdata/clickhouse_cast/sqlc.json b/internal/endtoend/testdata/clickhouse_cast/sqlc.json new file mode 100644 index 0000000000..8c3f26ae8c --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_cast/sqlc.json @@ -0,0 +1,16 @@ +{ + "version": "2", + "sql": [ + { + "engine": "clickhouse", + "queries": "query.sql", + "schema": "schema.sql", + "gen": { + "go": { + "out": "go", + "package": "db" + } + } + } + ] +} diff --git a/internal/endtoend/testdata/clickhouse_cte/go/db.go b/internal/endtoend/testdata/clickhouse_cte/go/db.go new file mode 100644 index 0000000000..cd5bbb8e08 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_cte/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +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/clickhouse_cte/go/models.go b/internal/endtoend/testdata/clickhouse_cte/go/models.go new file mode 100644 index 0000000000..57b3c42a30 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_cte/go/models.go @@ -0,0 +1,16 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +import ( + "database/sql" +) + +type Employee struct { + ID uint32 + Name string + ManagerID sql.NullInt64 + Salary uint32 +} diff --git a/internal/endtoend/testdata/clickhouse_cte/go/query.sql.go b/internal/endtoend/testdata/clickhouse_cte/go/query.sql.go new file mode 100644 index 0000000000..fff09e87ef --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_cte/go/query.sql.go @@ -0,0 +1,84 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: query.sql + +package db + +import ( + "context" +) + +const getHighEarners = `-- name: GetHighEarners :many +WITH high_earners AS ( + SELECT id, name, salary FROM employees WHERE salary > ? +) +SELECT id, name, salary FROM high_earners ORDER BY salary DESC; +` + +type GetHighEarnersRow struct { + ID uint32 + Name string + Salary uint32 +} + +func (q *Queries) GetHighEarners(ctx context.Context, salary uint32) ([]GetHighEarnersRow, error) { + rows, err := q.db.QueryContext(ctx, getHighEarners, salary) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetHighEarnersRow + for rows.Next() { + var i GetHighEarnersRow + if err := rows.Scan(&i.ID, &i.Name, &i.Salary); 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 +} + +const getMultipleCTEs = `-- name: GetMultipleCTEs :many +WITH emp_data AS ( + SELECT id, name, salary FROM employees +), +filtered AS ( + SELECT id, name, salary FROM emp_data WHERE salary > ? +) +SELECT id, name FROM filtered; +` + +type GetMultipleCTEsRow struct { + ID uint32 + Name string +} + +func (q *Queries) GetMultipleCTEs(ctx context.Context, salary uint32) ([]GetMultipleCTEsRow, error) { + rows, err := q.db.QueryContext(ctx, getMultipleCTEs, salary) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetMultipleCTEsRow + for rows.Next() { + var i GetMultipleCTEsRow + if err := rows.Scan(&i.ID, &i.Name); 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/clickhouse_cte/query.sql b/internal/endtoend/testdata/clickhouse_cte/query.sql new file mode 100644 index 0000000000..271318560e --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_cte/query.sql @@ -0,0 +1,14 @@ +-- name: GetHighEarners :many +WITH high_earners AS ( + SELECT id, name, salary FROM employees WHERE salary > ? +) +SELECT id, name, salary FROM high_earners ORDER BY salary DESC; + +-- name: GetMultipleCTEs :many +WITH emp_data AS ( + SELECT id, name, salary FROM employees +), +filtered AS ( + SELECT id, name, salary FROM emp_data WHERE salary > ? +) +SELECT id, name FROM filtered; diff --git a/internal/endtoend/testdata/clickhouse_cte/schema.sql b/internal/endtoend/testdata/clickhouse_cte/schema.sql new file mode 100644 index 0000000000..215bcec36f --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_cte/schema.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS employees +( + id UInt32, + name String, + manager_id Nullable(UInt32), + salary UInt32 +) +ENGINE = MergeTree() +ORDER BY id; diff --git a/internal/endtoend/testdata/clickhouse_cte/sqlc.json b/internal/endtoend/testdata/clickhouse_cte/sqlc.json new file mode 100644 index 0000000000..8c3f26ae8c --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_cte/sqlc.json @@ -0,0 +1,16 @@ +{ + "version": "2", + "sql": [ + { + "engine": "clickhouse", + "queries": "query.sql", + "schema": "schema.sql", + "gen": { + "go": { + "out": "go", + "package": "db" + } + } + } + ] +} diff --git a/internal/endtoend/testdata/clickhouse_delete_update/go/db.go b/internal/endtoend/testdata/clickhouse_delete_update/go/db.go new file mode 100644 index 0000000000..cd5bbb8e08 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_delete_update/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +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/clickhouse_delete_update/go/models.go b/internal/endtoend/testdata/clickhouse_delete_update/go/models.go new file mode 100644 index 0000000000..76d767290c --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_delete_update/go/models.go @@ -0,0 +1,16 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +import ( + "time" +) + +type Log struct { + ID uint32 + Level string + Message string + CreatedAt time.Time +} diff --git a/internal/endtoend/testdata/clickhouse_delete_update/go/query.sql.go b/internal/endtoend/testdata/clickhouse_delete_update/go/query.sql.go new file mode 100644 index 0000000000..637871e77a --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_delete_update/go/query.sql.go @@ -0,0 +1,37 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: query.sql + +package db + +import ( + "context" +) + +const deleteErrorLogs = `-- name: DeleteErrorLogs :exec +DELETE FROM logs WHERE level = 'ERROR'; +` + +func (q *Queries) DeleteErrorLogs(ctx context.Context) error { + _, err := q.db.ExecContext(ctx, deleteErrorLogs) + return err +} + +const deleteOldLogs = `-- name: DeleteOldLogs :exec +DELETE FROM logs WHERE created_at < ?; +` + +func (q *Queries) DeleteOldLogs(ctx context.Context) error { + _, err := q.db.ExecContext(ctx, deleteOldLogs) + return err +} + +const updateLogLevel = `-- name: UpdateLogLevel :exec +ALTER TABLE logs UPDATE level = ? WHERE id = ?; +` + +func (q *Queries) UpdateLogLevel(ctx context.Context) error { + _, err := q.db.ExecContext(ctx, updateLogLevel) + return err +} diff --git a/internal/endtoend/testdata/clickhouse_delete_update/query.sql b/internal/endtoend/testdata/clickhouse_delete_update/query.sql new file mode 100644 index 0000000000..acc59438f6 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_delete_update/query.sql @@ -0,0 +1,8 @@ +-- name: DeleteOldLogs :exec +DELETE FROM logs WHERE created_at < ?; + +-- name: DeleteErrorLogs :exec +DELETE FROM logs WHERE level = 'ERROR'; + +-- name: UpdateLogLevel :exec +ALTER TABLE logs UPDATE level = ? WHERE id = ?; diff --git a/internal/endtoend/testdata/clickhouse_delete_update/schema.sql b/internal/endtoend/testdata/clickhouse_delete_update/schema.sql new file mode 100644 index 0000000000..cb37160eb3 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_delete_update/schema.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS logs +( + id UInt32, + level String, + message String, + created_at DateTime +) +ENGINE = MergeTree() +ORDER BY id; diff --git a/internal/endtoend/testdata/clickhouse_delete_update/sqlc.json b/internal/endtoend/testdata/clickhouse_delete_update/sqlc.json new file mode 100644 index 0000000000..8c3f26ae8c --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_delete_update/sqlc.json @@ -0,0 +1,16 @@ +{ + "version": "2", + "sql": [ + { + "engine": "clickhouse", + "queries": "query.sql", + "schema": "schema.sql", + "gen": { + "go": { + "out": "go", + "package": "db" + } + } + } + ] +} diff --git a/internal/endtoend/testdata/clickhouse_distinct/go/db.go b/internal/endtoend/testdata/clickhouse_distinct/go/db.go new file mode 100644 index 0000000000..cd5bbb8e08 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_distinct/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +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/clickhouse_distinct/go/models.go b/internal/endtoend/testdata/clickhouse_distinct/go/models.go new file mode 100644 index 0000000000..6a12380a6f --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_distinct/go/models.go @@ -0,0 +1,12 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +type User struct { + ID uint32 + Name string + Department string + Salary uint32 +} diff --git a/internal/endtoend/testdata/clickhouse_distinct/go/query.sql.go b/internal/endtoend/testdata/clickhouse_distinct/go/query.sql.go new file mode 100644 index 0000000000..1d3a55c7b5 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_distinct/go/query.sql.go @@ -0,0 +1,69 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: query.sql + +package db + +import ( + "context" +) + +const distinctDepartments = `-- name: DistinctDepartments :many +SELECT DISTINCT department FROM users; +` + +func (q *Queries) DistinctDepartments(ctx context.Context) ([]string, error) { + rows, err := q.db.QueryContext(ctx, distinctDepartments) + if err != nil { + return nil, err + } + defer rows.Close() + var items []string + for rows.Next() { + var department string + if err := rows.Scan(&department); err != nil { + return nil, err + } + items = append(items, department) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const distinctMultipleColumns = `-- name: DistinctMultipleColumns :many +SELECT DISTINCT department, salary FROM users ORDER BY department; +` + +type DistinctMultipleColumnsRow struct { + Department string + Salary uint32 +} + +func (q *Queries) DistinctMultipleColumns(ctx context.Context) ([]DistinctMultipleColumnsRow, error) { + rows, err := q.db.QueryContext(ctx, distinctMultipleColumns) + if err != nil { + return nil, err + } + defer rows.Close() + var items []DistinctMultipleColumnsRow + for rows.Next() { + var i DistinctMultipleColumnsRow + if err := rows.Scan(&i.Department, &i.Salary); 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/clickhouse_distinct/query.sql b/internal/endtoend/testdata/clickhouse_distinct/query.sql new file mode 100644 index 0000000000..888e1db468 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_distinct/query.sql @@ -0,0 +1,5 @@ +-- name: DistinctDepartments :many +SELECT DISTINCT department FROM users; + +-- name: DistinctMultipleColumns :many +SELECT DISTINCT department, salary FROM users ORDER BY department; diff --git a/internal/endtoend/testdata/clickhouse_distinct/schema.sql b/internal/endtoend/testdata/clickhouse_distinct/schema.sql new file mode 100644 index 0000000000..ebdcb09ac6 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_distinct/schema.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS users +( + id UInt32, + name String, + department String, + salary UInt32 +) +ENGINE = MergeTree() +ORDER BY id; diff --git a/internal/endtoend/testdata/clickhouse_distinct/sqlc.json b/internal/endtoend/testdata/clickhouse_distinct/sqlc.json new file mode 100644 index 0000000000..8c3f26ae8c --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_distinct/sqlc.json @@ -0,0 +1,16 @@ +{ + "version": "2", + "sql": [ + { + "engine": "clickhouse", + "queries": "query.sql", + "schema": "schema.sql", + "gen": { + "go": { + "out": "go", + "package": "db" + } + } + } + ] +} diff --git a/internal/endtoend/testdata/clickhouse_dml/go/db.go b/internal/endtoend/testdata/clickhouse_dml/go/db.go new file mode 100644 index 0000000000..cd5bbb8e08 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_dml/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +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/clickhouse_dml/go/models.go b/internal/endtoend/testdata/clickhouse_dml/go/models.go new file mode 100644 index 0000000000..33a2c4225d --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_dml/go/models.go @@ -0,0 +1,25 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +import ( + "time" +) + +type Log struct { + ID uint32 + Level string + Message string + Timestamp time.Time + Source string +} + +type Notification struct { + ID uint32 + UserID uint32 + Message string + ReadStatus uint8 + CreatedAt time.Time +} diff --git a/internal/endtoend/testdata/clickhouse_dml/go/query.sql.go b/internal/endtoend/testdata/clickhouse_dml/go/query.sql.go new file mode 100644 index 0000000000..e58fc32fd7 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_dml/go/query.sql.go @@ -0,0 +1,273 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: query.sql + +package db + +import ( + "context" + "time" +) + +const countUnreadNotifications = `-- name: CountUnreadNotifications :one +SELECT COUNT(*) as unread_count +FROM notifications +WHERE user_id = ? AND read_status = 0; +` + +func (q *Queries) CountUnreadNotifications(ctx context.Context, userID uint32) (uint64, error) { + row := q.db.QueryRowContext(ctx, countUnreadNotifications, userID) + var unread_count uint64 + err := row.Scan(&unread_count) + return unread_count, err +} + +const getLogs = `-- name: GetLogs :many +SELECT id, level, message, timestamp, source +FROM logs +WHERE timestamp >= ? +ORDER BY timestamp DESC +LIMIT ?; +` + +type GetLogsParams struct { + Timestamp time.Time + Limit int64 +} + +func (q *Queries) GetLogs(ctx context.Context, arg GetLogsParams) ([]Log, error) { + rows, err := q.db.QueryContext(ctx, getLogs, arg.Timestamp, arg.Limit) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Log + for rows.Next() { + var i Log + if err := rows.Scan( + &i.ID, + &i.Level, + &i.Message, + &i.Timestamp, + &i.Source, + ); 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 +} + +const getLogsByLevel = `-- name: GetLogsByLevel :many +SELECT id, level, message, timestamp, source +FROM logs +WHERE level = ? +ORDER BY timestamp DESC; +` + +func (q *Queries) GetLogsByLevel(ctx context.Context, level string) ([]Log, error) { + rows, err := q.db.QueryContext(ctx, getLogsByLevel, level) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Log + for rows.Next() { + var i Log + if err := rows.Scan( + &i.ID, + &i.Level, + &i.Message, + &i.Timestamp, + &i.Source, + ); 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 +} + +const getNotificationSummary = `-- name: GetNotificationSummary :many +SELECT + level, + COUNT(*) as count +FROM logs +WHERE timestamp >= ? +GROUP BY level +ORDER BY count DESC; +` + +type GetNotificationSummaryRow struct { + Level string + Count uint64 +} + +func (q *Queries) GetNotificationSummary(ctx context.Context, timestamp time.Time) ([]GetNotificationSummaryRow, error) { + rows, err := q.db.QueryContext(ctx, getNotificationSummary, timestamp) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetNotificationSummaryRow + for rows.Next() { + var i GetNotificationSummaryRow + if err := rows.Scan(&i.Level, &i.Count); 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 +} + +const getNotifications = `-- name: GetNotifications :many +SELECT id, user_id, message, read_status, created_at +FROM notifications +WHERE user_id = ? +ORDER BY created_at DESC; +` + +func (q *Queries) GetNotifications(ctx context.Context, userID uint32) ([]Notification, error) { + rows, err := q.db.QueryContext(ctx, getNotifications, userID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Notification + for rows.Next() { + var i Notification + if err := rows.Scan( + &i.ID, + &i.UserID, + &i.Message, + &i.ReadStatus, + &i.CreatedAt, + ); 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 +} + +const insertLog = `-- name: InsertLog :exec +INSERT INTO logs (id, level, message, timestamp, source) +VALUES (?, ?, ?, ?, ?); +` + +type InsertLogParams struct { + ID uint32 + Level string + Message string + Timestamp time.Time + Source string +} + +func (q *Queries) InsertLog(ctx context.Context, arg InsertLogParams) error { + _, err := q.db.ExecContext(ctx, insertLog, + arg.ID, + arg.Level, + arg.Message, + arg.Timestamp, + arg.Source, + ) + return err +} + +const insertMultipleLogs = `-- name: InsertMultipleLogs :exec +INSERT INTO logs (id, level, message, timestamp, source) +VALUES + (?, ?, ?, ?, ?), + (?, ?, ?, ?, ?), + (?, ?, ?, ?, ?); +` + +type InsertMultipleLogsParams struct { + ID uint32 + Level string + Message string + Timestamp time.Time + Source string + ID_2 uint32 + Level_2 string + Message_2 string + Timestamp_2 time.Time + Source_2 string + ID_3 uint32 + Level_3 string + Message_3 string + Timestamp_3 time.Time + Source_3 string +} + +func (q *Queries) InsertMultipleLogs(ctx context.Context, arg InsertMultipleLogsParams) error { + _, err := q.db.ExecContext(ctx, insertMultipleLogs, + arg.ID, + arg.Level, + arg.Message, + arg.Timestamp, + arg.Source, + arg.ID_2, + arg.Level_2, + arg.Message_2, + arg.Timestamp_2, + arg.Source_2, + arg.ID_3, + arg.Level_3, + arg.Message_3, + arg.Timestamp_3, + arg.Source_3, + ) + return err +} + +const insertNotification = `-- name: InsertNotification :exec +INSERT INTO notifications (id, user_id, message, read_status, created_at) +VALUES (?, ?, ?, ?, ?); + +` + +type InsertNotificationParams struct { + ID uint32 + UserID uint32 + Message string + ReadStatus uint8 + CreatedAt time.Time +} + +// Select for use in tests +func (q *Queries) InsertNotification(ctx context.Context, arg InsertNotificationParams) error { + _, err := q.db.ExecContext(ctx, insertNotification, + arg.ID, + arg.UserID, + arg.Message, + arg.ReadStatus, + arg.CreatedAt, + ) + return err +} diff --git a/internal/endtoend/testdata/clickhouse_dml/query.sql b/internal/endtoend/testdata/clickhouse_dml/query.sql new file mode 100644 index 0000000000..43e79b12a7 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_dml/query.sql @@ -0,0 +1,49 @@ +-- Insert operations +-- name: InsertLog :exec +INSERT INTO logs (id, level, message, timestamp, source) +VALUES (?, ?, ?, ?, ?); + +-- name: InsertMultipleLogs :exec +INSERT INTO logs (id, level, message, timestamp, source) +VALUES + (?, ?, ?, ?, ?), + (?, ?, ?, ?, ?), + (?, ?, ?, ?, ?); + +-- name: InsertNotification :exec +INSERT INTO notifications (id, user_id, message, read_status, created_at) +VALUES (?, ?, ?, ?, ?); + +-- Select for use in tests +-- name: GetLogs :many +SELECT id, level, message, timestamp, source +FROM logs +WHERE timestamp >= ? +ORDER BY timestamp DESC +LIMIT ?; + +-- name: GetLogsByLevel :many +SELECT id, level, message, timestamp, source +FROM logs +WHERE level = ? +ORDER BY timestamp DESC; + +-- name: GetNotifications :many +SELECT id, user_id, message, read_status, created_at +FROM notifications +WHERE user_id = ? +ORDER BY created_at DESC; + +-- name: CountUnreadNotifications :one +SELECT COUNT(*) as unread_count +FROM notifications +WHERE user_id = ? AND read_status = 0; + +-- name: GetNotificationSummary :many +SELECT + level, + COUNT(*) as count +FROM logs +WHERE timestamp >= ? +GROUP BY level +ORDER BY count DESC; diff --git a/internal/endtoend/testdata/clickhouse_dml/schema.sql b/internal/endtoend/testdata/clickhouse_dml/schema.sql new file mode 100644 index 0000000000..9732e24d69 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_dml/schema.sql @@ -0,0 +1,21 @@ +CREATE TABLE IF NOT EXISTS logs +( + id UInt32, + level String, + message String, + timestamp DateTime, + source String +) +ENGINE = MergeTree() +ORDER BY (timestamp, id); + +CREATE TABLE IF NOT EXISTS notifications +( + id UInt32, + user_id UInt32, + message String, + read_status UInt8, + created_at DateTime +) +ENGINE = MergeTree() +ORDER BY (user_id, created_at); diff --git a/internal/endtoend/testdata/clickhouse_dml/sqlc.json b/internal/endtoend/testdata/clickhouse_dml/sqlc.json new file mode 100644 index 0000000000..439f37f401 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_dml/sqlc.json @@ -0,0 +1,16 @@ +{ + "version": "2", + "sql": [ + { + "schema": "schema.sql", + "queries": "query.sql", + "engine": "clickhouse", + "gen": { + "go": { + "out": "go", + "package": "db" + } + } + } + ] +} diff --git a/internal/endtoend/testdata/clickhouse_exists/go/db.go b/internal/endtoend/testdata/clickhouse_exists/go/db.go new file mode 100644 index 0000000000..cd5bbb8e08 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_exists/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +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/clickhouse_exists/go/models.go b/internal/endtoend/testdata/clickhouse_exists/go/models.go new file mode 100644 index 0000000000..f0af642fa7 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_exists/go/models.go @@ -0,0 +1,15 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +type Profile struct { + UserID uint32 + Bio string +} + +type User struct { + ID uint32 + Name string +} diff --git a/internal/endtoend/testdata/clickhouse_exists/go/query.sql.go b/internal/endtoend/testdata/clickhouse_exists/go/query.sql.go new file mode 100644 index 0000000000..736f27db01 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_exists/go/query.sql.go @@ -0,0 +1,64 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: query.sql + +package db + +import ( + "context" +) + +const getUsersWithProfiles = `-- name: GetUsersWithProfiles :many +SELECT id, name FROM users WHERE EXISTS (SELECT 1 FROM profiles WHERE profiles.user_id = users.id); +` + +func (q *Queries) GetUsersWithProfiles(ctx context.Context) ([]User, error) { + rows, err := q.db.QueryContext(ctx, getUsersWithProfiles) + if err != nil { + return nil, err + } + defer rows.Close() + var items []User + for rows.Next() { + var i User + if err := rows.Scan(&i.ID, &i.Name); 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 +} + +const getUsersWithoutProfiles = `-- name: GetUsersWithoutProfiles :many +SELECT id, name FROM users WHERE NOT EXISTS (SELECT 1 FROM profiles WHERE profiles.user_id = users.id); +` + +func (q *Queries) GetUsersWithoutProfiles(ctx context.Context) ([]User, error) { + rows, err := q.db.QueryContext(ctx, getUsersWithoutProfiles) + if err != nil { + return nil, err + } + defer rows.Close() + var items []User + for rows.Next() { + var i User + if err := rows.Scan(&i.ID, &i.Name); 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/clickhouse_exists/query.sql b/internal/endtoend/testdata/clickhouse_exists/query.sql new file mode 100644 index 0000000000..fea429dbb1 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_exists/query.sql @@ -0,0 +1,5 @@ +-- name: GetUsersWithProfiles :many +SELECT id, name FROM users WHERE EXISTS (SELECT 1 FROM profiles WHERE profiles.user_id = users.id); + +-- name: GetUsersWithoutProfiles :many +SELECT id, name FROM users WHERE NOT EXISTS (SELECT 1 FROM profiles WHERE profiles.user_id = users.id); diff --git a/internal/endtoend/testdata/clickhouse_exists/schema.sql b/internal/endtoend/testdata/clickhouse_exists/schema.sql new file mode 100644 index 0000000000..06ccf5d830 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_exists/schema.sql @@ -0,0 +1,15 @@ +CREATE TABLE IF NOT EXISTS users +( + id UInt32, + name String +) +ENGINE = MergeTree() +ORDER BY id; + +CREATE TABLE IF NOT EXISTS profiles +( + user_id UInt32, + bio String +) +ENGINE = MergeTree() +ORDER BY user_id; diff --git a/internal/endtoend/testdata/clickhouse_exists/sqlc.json b/internal/endtoend/testdata/clickhouse_exists/sqlc.json new file mode 100644 index 0000000000..8c3f26ae8c --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_exists/sqlc.json @@ -0,0 +1,16 @@ +{ + "version": "2", + "sql": [ + { + "engine": "clickhouse", + "queries": "query.sql", + "schema": "schema.sql", + "gen": { + "go": { + "out": "go", + "package": "db" + } + } + } + ] +} diff --git a/internal/endtoend/testdata/clickhouse_functions/go/db.go b/internal/endtoend/testdata/clickhouse_functions/go/db.go new file mode 100644 index 0000000000..cd5bbb8e08 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_functions/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +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/clickhouse_functions/go/models.go b/internal/endtoend/testdata/clickhouse_functions/go/models.go new file mode 100644 index 0000000000..0f7b5a2f03 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_functions/go/models.go @@ -0,0 +1,27 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +import ( + "net/netip" + "time" +) + +type Metric struct { + Name string + Value float64 + Tags map[string]string + CreatedAt time.Time +} + +type WebLog struct { + ID uint32 + IpAddress netip.Addr + Timestamp time.Time + Url string + ResponseTimeMs uint32 + StatusCode uint16 + UserAgent string +} diff --git a/internal/endtoend/testdata/clickhouse_functions/go/query.sql.go b/internal/endtoend/testdata/clickhouse_functions/go/query.sql.go new file mode 100644 index 0000000000..d2aaf05d1b --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_functions/go/query.sql.go @@ -0,0 +1,392 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: query.sql + +package db + +import ( + "context" + "net/netip" + "time" +) + +const getHttpStatusCodes = `-- name: GetHttpStatusCodes :many +SELECT + status_code, + COUNT(*) as count, + AVG(response_time_ms) as avg_response_time +FROM web_logs +WHERE timestamp >= ? +GROUP BY status_code +ORDER BY count DESC; + +` + +type GetHttpStatusCodesRow struct { + StatusCode uint16 + Count uint64 + AvgResponseTime float64 +} + +// Conditional expressions +func (q *Queries) GetHttpStatusCodes(ctx context.Context, timestamp time.Time) ([]GetHttpStatusCodesRow, error) { + rows, err := q.db.QueryContext(ctx, getHttpStatusCodes, timestamp) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetHttpStatusCodesRow + for rows.Next() { + var i GetHttpStatusCodesRow + if err := rows.Scan(&i.StatusCode, &i.Count, &i.AvgResponseTime); 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 +} + +const getLogsForDateRange = `-- name: GetLogsForDateRange :many +SELECT + id, + ip_address, + timestamp, + url +FROM web_logs +WHERE toDate(timestamp) >= ? AND toDate(timestamp) <= ? +ORDER BY timestamp DESC; +` + +type GetLogsForDateRangeParams struct { + Timestamp time.Time + Timestamp_2 time.Time +} + +type GetLogsForDateRangeRow struct { + ID uint32 + IpAddress netip.Addr + Timestamp time.Time + Url string +} + +func (q *Queries) GetLogsForDateRange(ctx context.Context, arg GetLogsForDateRangeParams) ([]GetLogsForDateRangeRow, error) { + rows, err := q.db.QueryContext(ctx, getLogsForDateRange, arg.Timestamp, arg.Timestamp_2) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetLogsForDateRangeRow + for rows.Next() { + var i GetLogsForDateRangeRow + if err := rows.Scan( + &i.ID, + &i.IpAddress, + &i.Timestamp, + &i.Url, + ); 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 +} + +const getLogsGroupedByHour = `-- name: GetLogsGroupedByHour :many +SELECT + toStartOfHour(timestamp) as hour, + COUNT(*) as request_count, + AVG(response_time_ms) as avg_response_time +FROM web_logs +WHERE timestamp >= ? +GROUP BY hour +ORDER BY hour DESC; + +` + +type GetLogsGroupedByHourRow struct { + Hour interface{} + RequestCount uint64 + AvgResponseTime float64 +} + +// String functions +func (q *Queries) GetLogsGroupedByHour(ctx context.Context, timestamp time.Time) ([]GetLogsGroupedByHourRow, error) { + rows, err := q.db.QueryContext(ctx, getLogsGroupedByHour, timestamp) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetLogsGroupedByHourRow + for rows.Next() { + var i GetLogsGroupedByHourRow + if err := rows.Scan(&i.Hour, &i.RequestCount, &i.AvgResponseTime); 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 +} + +const getLogsSummary = `-- name: GetLogsSummary :many +SELECT + ip_address, + COUNT(*) as request_count, + CAST(AVG(response_time_ms) AS UInt32) as avg_time, + CAST(MIN(timestamp) AS Date) as first_request, + CAST(MAX(timestamp) AS Date) as last_request +FROM web_logs +WHERE timestamp >= ? +GROUP BY ip_address +ORDER BY request_count DESC; + +` + +type GetLogsSummaryRow struct { + IpAddress netip.Addr + RequestCount uint64 + AvgTime uint32 + FirstRequest time.Time + LastRequest time.Time +} + +// Math functions +func (q *Queries) GetLogsSummary(ctx context.Context, timestamp time.Time) ([]GetLogsSummaryRow, error) { + rows, err := q.db.QueryContext(ctx, getLogsSummary, timestamp) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetLogsSummaryRow + for rows.Next() { + var i GetLogsSummaryRow + if err := rows.Scan( + &i.IpAddress, + &i.RequestCount, + &i.AvgTime, + &i.FirstRequest, + &i.LastRequest, + ); 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 +} + +const getMetricsByTag = `-- name: GetMetricsByTag :many +SELECT + name, + value, + tags, + created_at +FROM metrics +WHERE tags['environment'] = ? +ORDER BY created_at DESC; +` + +func (q *Queries) GetMetricsByTag(ctx context.Context, dollar_1 interface{}) ([]Metric, error) { + rows, err := q.db.QueryContext(ctx, getMetricsByTag, dollar_1) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Metric + for rows.Next() { + var i Metric + if err := rows.Scan( + &i.Name, + &i.Value, + &i.Tags, + &i.CreatedAt, + ); 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 +} + +const getResponseTimeStats = `-- name: GetResponseTimeStats :many +SELECT + status_code, + COUNT(*) as count, + round(AVG(response_time_ms), 2) as avg_response_time, + sqrt(varPop(response_time_ms)) as stddev +FROM web_logs +WHERE timestamp >= ? +GROUP BY status_code +ORDER BY avg_response_time DESC; + +` + +type GetResponseTimeStatsRow struct { + StatusCode uint16 + Count uint64 + AvgResponseTime interface{} + Stddev interface{} +} + +// Map type operations +func (q *Queries) GetResponseTimeStats(ctx context.Context, timestamp time.Time) ([]GetResponseTimeStatsRow, error) { + rows, err := q.db.QueryContext(ctx, getResponseTimeStats, timestamp) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetResponseTimeStatsRow + for rows.Next() { + var i GetResponseTimeStatsRow + if err := rows.Scan( + &i.StatusCode, + &i.Count, + &i.AvgResponseTime, + &i.Stddev, + ); 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 +} + +const getSlowRequests = `-- name: GetSlowRequests :many +SELECT + id, + url, + response_time_ms, + CASE + WHEN response_time_ms > 1000 THEN 'very_slow' + WHEN response_time_ms > 500 THEN 'slow' + WHEN response_time_ms > 100 THEN 'medium' + ELSE 'fast' + END as performance +FROM web_logs +WHERE response_time_ms > ? +ORDER BY response_time_ms DESC; + +` + +type GetSlowRequestsRow struct { + ID uint32 + Url string + ResponseTimeMs uint32 + Performance string +} + +// Type casting +func (q *Queries) GetSlowRequests(ctx context.Context, responseTimeMs uint32) ([]GetSlowRequestsRow, error) { + rows, err := q.db.QueryContext(ctx, getSlowRequests, responseTimeMs) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetSlowRequestsRow + for rows.Next() { + var i GetSlowRequestsRow + if err := rows.Scan( + &i.ID, + &i.Url, + &i.ResponseTimeMs, + &i.Performance, + ); 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 +} + +const searchLogsByUrl = `-- name: SearchLogsByUrl :many +SELECT + id, + ip_address, + url, + response_time_ms +FROM web_logs +WHERE url LIKE ? +ORDER BY timestamp DESC +LIMIT ?; +` + +type SearchLogsByUrlParams struct { + Url string + Limit int64 +} + +type SearchLogsByUrlRow struct { + ID uint32 + IpAddress netip.Addr + Url string + ResponseTimeMs uint32 +} + +func (q *Queries) SearchLogsByUrl(ctx context.Context, arg SearchLogsByUrlParams) ([]SearchLogsByUrlRow, error) { + rows, err := q.db.QueryContext(ctx, searchLogsByUrl, arg.Url, arg.Limit) + if err != nil { + return nil, err + } + defer rows.Close() + var items []SearchLogsByUrlRow + for rows.Next() { + var i SearchLogsByUrlRow + if err := rows.Scan( + &i.ID, + &i.IpAddress, + &i.Url, + &i.ResponseTimeMs, + ); 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/clickhouse_functions/query.sql b/internal/endtoend/testdata/clickhouse_functions/query.sql new file mode 100644 index 0000000000..ef8590c395 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_functions/query.sql @@ -0,0 +1,94 @@ +-- Date/Time functions +-- name: GetLogsForDateRange :many +SELECT + id, + ip_address, + timestamp, + url +FROM web_logs +WHERE toDate(timestamp) >= ? AND toDate(timestamp) <= ? +ORDER BY timestamp DESC; + +-- name: GetLogsGroupedByHour :many +SELECT + toStartOfHour(timestamp) as hour, + COUNT(*) as request_count, + AVG(response_time_ms) as avg_response_time +FROM web_logs +WHERE timestamp >= ? +GROUP BY hour +ORDER BY hour DESC; + +-- String functions +-- name: SearchLogsByUrl :many +SELECT + id, + ip_address, + url, + response_time_ms +FROM web_logs +WHERE url LIKE ? +ORDER BY timestamp DESC +LIMIT ?; + +-- name: GetHttpStatusCodes :many +SELECT + status_code, + COUNT(*) as count, + AVG(response_time_ms) as avg_response_time +FROM web_logs +WHERE timestamp >= ? +GROUP BY status_code +ORDER BY count DESC; + +-- Conditional expressions +-- name: GetSlowRequests :many +SELECT + id, + url, + response_time_ms, + CASE + WHEN response_time_ms > 1000 THEN 'very_slow' + WHEN response_time_ms > 500 THEN 'slow' + WHEN response_time_ms > 100 THEN 'medium' + ELSE 'fast' + END as performance +FROM web_logs +WHERE response_time_ms > ? +ORDER BY response_time_ms DESC; + +-- Type casting +-- name: GetLogsSummary :many +SELECT + ip_address, + COUNT(*) as request_count, + CAST(AVG(response_time_ms) AS UInt32) as avg_time, + CAST(MIN(timestamp) AS Date) as first_request, + CAST(MAX(timestamp) AS Date) as last_request +FROM web_logs +WHERE timestamp >= ? +GROUP BY ip_address +ORDER BY request_count DESC; + +-- Math functions +-- name: GetResponseTimeStats :many +SELECT + status_code, + COUNT(*) as count, + round(AVG(response_time_ms), 2) as avg_response_time, + sqrt(varPop(response_time_ms)) as stddev +FROM web_logs +WHERE timestamp >= ? +GROUP BY status_code +ORDER BY avg_response_time DESC; + +-- Map type operations +-- name: GetMetricsByTag :many +SELECT + name, + value, + tags, + created_at +FROM metrics +WHERE tags['environment'] = ? +ORDER BY created_at DESC; diff --git a/internal/endtoend/testdata/clickhouse_functions/schema.sql b/internal/endtoend/testdata/clickhouse_functions/schema.sql new file mode 100644 index 0000000000..8a6ee7a269 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_functions/schema.sql @@ -0,0 +1,22 @@ +CREATE TABLE IF NOT EXISTS web_logs +( + id UInt32, + ip_address IPv4, + timestamp DateTime, + url String, + response_time_ms UInt32, + status_code UInt16, + user_agent String +) +ENGINE = MergeTree() +ORDER BY (timestamp, ip_address); + +CREATE TABLE IF NOT EXISTS metrics +( + name String, + value Float64, + tags Map(String, String), + created_at DateTime +) +ENGINE = MergeTree() +ORDER BY (created_at, name); diff --git a/internal/endtoend/testdata/clickhouse_functions/sqlc.json b/internal/endtoend/testdata/clickhouse_functions/sqlc.json new file mode 100644 index 0000000000..439f37f401 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_functions/sqlc.json @@ -0,0 +1,16 @@ +{ + "version": "2", + "sql": [ + { + "schema": "schema.sql", + "queries": "query.sql", + "engine": "clickhouse", + "gen": { + "go": { + "out": "go", + "package": "db" + } + } + } + ] +} diff --git a/internal/endtoend/testdata/clickhouse_having/go/db.go b/internal/endtoend/testdata/clickhouse_having/go/db.go new file mode 100644 index 0000000000..cd5bbb8e08 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_having/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +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/clickhouse_having/go/models.go b/internal/endtoend/testdata/clickhouse_having/go/models.go new file mode 100644 index 0000000000..bd7b67bc3b --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_having/go/models.go @@ -0,0 +1,12 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +type Sale struct { + ID uint32 + Region string + Amount float32 + Year uint32 +} diff --git a/internal/endtoend/testdata/clickhouse_having/go/query.sql.go b/internal/endtoend/testdata/clickhouse_having/go/query.sql.go new file mode 100644 index 0000000000..50ff6fe16b --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_having/go/query.sql.go @@ -0,0 +1,106 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: query.sql + +package db + +import ( + "context" +) + +const getAverageSalesByRegion = `-- name: GetAverageSalesByRegion :many +SELECT region, avg(amount) AS avg_sale FROM sales GROUP BY region HAVING avg(amount) > ?; +` + +type GetAverageSalesByRegionRow struct { + Region string + AvgSale float64 +} + +func (q *Queries) GetAverageSalesByRegion(ctx context.Context, amount float32) ([]GetAverageSalesByRegionRow, error) { + rows, err := q.db.QueryContext(ctx, getAverageSalesByRegion, amount) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetAverageSalesByRegionRow + for rows.Next() { + var i GetAverageSalesByRegionRow + if err := rows.Scan(&i.Region, &i.AvgSale); 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 +} + +const getRegionSalesAboveThreshold = `-- name: GetRegionSalesAboveThreshold :many +SELECT region, sum(amount) AS total_sales FROM sales GROUP BY region HAVING sum(amount) > ?; +` + +type GetRegionSalesAboveThresholdRow struct { + Region string + TotalSales float32 +} + +func (q *Queries) GetRegionSalesAboveThreshold(ctx context.Context, amount float32) ([]GetRegionSalesAboveThresholdRow, error) { + rows, err := q.db.QueryContext(ctx, getRegionSalesAboveThreshold, amount) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetRegionSalesAboveThresholdRow + for rows.Next() { + var i GetRegionSalesAboveThresholdRow + if err := rows.Scan(&i.Region, &i.TotalSales); 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 +} + +const getRegionSalesCount = `-- name: GetRegionSalesCount :many +SELECT region, count(id) AS transaction_count FROM sales GROUP BY region HAVING count(id) > ?; +` + +type GetRegionSalesCountRow struct { + Region string + TransactionCount uint64 +} + +func (q *Queries) GetRegionSalesCount(ctx context.Context, id uint32) ([]GetRegionSalesCountRow, error) { + rows, err := q.db.QueryContext(ctx, getRegionSalesCount, id) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetRegionSalesCountRow + for rows.Next() { + var i GetRegionSalesCountRow + if err := rows.Scan(&i.Region, &i.TransactionCount); 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/clickhouse_having/query.sql b/internal/endtoend/testdata/clickhouse_having/query.sql new file mode 100644 index 0000000000..529b8b8c52 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_having/query.sql @@ -0,0 +1,8 @@ +-- name: GetRegionSalesAboveThreshold :many +SELECT region, sum(amount) AS total_sales FROM sales GROUP BY region HAVING sum(amount) > ?; + +-- name: GetRegionSalesCount :many +SELECT region, count(id) AS transaction_count FROM sales GROUP BY region HAVING count(id) > ?; + +-- name: GetAverageSalesByRegion :many +SELECT region, avg(amount) AS avg_sale FROM sales GROUP BY region HAVING avg(amount) > ?; diff --git a/internal/endtoend/testdata/clickhouse_having/schema.sql b/internal/endtoend/testdata/clickhouse_having/schema.sql new file mode 100644 index 0000000000..9cd47b84d1 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_having/schema.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS sales +( + id UInt32, + region String, + amount Float32, + year UInt32 +) +ENGINE = MergeTree() +ORDER BY id; diff --git a/internal/endtoend/testdata/clickhouse_having/sqlc.json b/internal/endtoend/testdata/clickhouse_having/sqlc.json new file mode 100644 index 0000000000..8c3f26ae8c --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_having/sqlc.json @@ -0,0 +1,16 @@ +{ + "version": "2", + "sql": [ + { + "engine": "clickhouse", + "queries": "query.sql", + "schema": "schema.sql", + "gen": { + "go": { + "out": "go", + "package": "db" + } + } + } + ] +} diff --git a/internal/endtoend/testdata/clickhouse_implicit_join/go/db.go b/internal/endtoend/testdata/clickhouse_implicit_join/go/db.go new file mode 100644 index 0000000000..cd5bbb8e08 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_implicit_join/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +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/clickhouse_implicit_join/go/models.go b/internal/endtoend/testdata/clickhouse_implicit_join/go/models.go new file mode 100644 index 0000000000..c57f89488d --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_implicit_join/go/models.go @@ -0,0 +1,17 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +type Customer struct { + ID uint32 + Name string + City string +} + +type Order struct { + ID uint32 + CustomerID uint32 + Amount float32 +} diff --git a/internal/endtoend/testdata/clickhouse_implicit_join/go/query.sql.go b/internal/endtoend/testdata/clickhouse_implicit_join/go/query.sql.go new file mode 100644 index 0000000000..dd25a47cf7 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_implicit_join/go/query.sql.go @@ -0,0 +1,75 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: query.sql + +package db + +import ( + "context" +) + +const getCustomerOrders = `-- name: GetCustomerOrders :many +SELECT c.name, o.amount FROM customers c, orders o WHERE c.id = o.customer_id; +` + +type GetCustomerOrdersRow struct { + Name string + Amount float32 +} + +func (q *Queries) GetCustomerOrders(ctx context.Context) ([]GetCustomerOrdersRow, error) { + rows, err := q.db.QueryContext(ctx, getCustomerOrders) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetCustomerOrdersRow + for rows.Next() { + var i GetCustomerOrdersRow + if err := rows.Scan(&i.Name, &i.Amount); 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 +} + +const getCustomerOrdersByCity = `-- name: GetCustomerOrdersByCity :many +SELECT c.name, c.city, o.amount FROM customers c, orders o WHERE c.id = o.customer_id AND c.city = ?; +` + +type GetCustomerOrdersByCityRow struct { + Name string + City string + Amount float32 +} + +func (q *Queries) GetCustomerOrdersByCity(ctx context.Context, city string) ([]GetCustomerOrdersByCityRow, error) { + rows, err := q.db.QueryContext(ctx, getCustomerOrdersByCity, city) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetCustomerOrdersByCityRow + for rows.Next() { + var i GetCustomerOrdersByCityRow + if err := rows.Scan(&i.Name, &i.City, &i.Amount); 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/clickhouse_implicit_join/query.sql b/internal/endtoend/testdata/clickhouse_implicit_join/query.sql new file mode 100644 index 0000000000..907e8475e5 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_implicit_join/query.sql @@ -0,0 +1,5 @@ +-- name: GetCustomerOrders :many +SELECT c.name, o.amount FROM customers c, orders o WHERE c.id = o.customer_id; + +-- name: GetCustomerOrdersByCity :many +SELECT c.name, c.city, o.amount FROM customers c, orders o WHERE c.id = o.customer_id AND c.city = ?; diff --git a/internal/endtoend/testdata/clickhouse_implicit_join/schema.sql b/internal/endtoend/testdata/clickhouse_implicit_join/schema.sql new file mode 100644 index 0000000000..1c7b19eee1 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_implicit_join/schema.sql @@ -0,0 +1,17 @@ +CREATE TABLE IF NOT EXISTS customers +( + id UInt32, + name String, + city String +) +ENGINE = MergeTree() +ORDER BY id; + +CREATE TABLE IF NOT EXISTS orders +( + id UInt32, + customer_id UInt32, + amount Float32 +) +ENGINE = MergeTree() +ORDER BY id; diff --git a/internal/endtoend/testdata/clickhouse_implicit_join/sqlc.json b/internal/endtoend/testdata/clickhouse_implicit_join/sqlc.json new file mode 100644 index 0000000000..8c3f26ae8c --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_implicit_join/sqlc.json @@ -0,0 +1,16 @@ +{ + "version": "2", + "sql": [ + { + "engine": "clickhouse", + "queries": "query.sql", + "schema": "schema.sql", + "gen": { + "go": { + "out": "go", + "package": "db" + } + } + } + ] +} diff --git a/internal/endtoend/testdata/clickhouse_in_subquery/go/db.go b/internal/endtoend/testdata/clickhouse_in_subquery/go/db.go new file mode 100644 index 0000000000..cd5bbb8e08 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_in_subquery/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +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/clickhouse_in_subquery/go/models.go b/internal/endtoend/testdata/clickhouse_in_subquery/go/models.go new file mode 100644 index 0000000000..acf614e0ad --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_in_subquery/go/models.go @@ -0,0 +1,16 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +type Department struct { + ID uint32 + Name string +} + +type User struct { + ID uint32 + Name string + DepartmentID uint32 +} diff --git a/internal/endtoend/testdata/clickhouse_in_subquery/go/query.sql.go b/internal/endtoend/testdata/clickhouse_in_subquery/go/query.sql.go new file mode 100644 index 0000000000..340d8fec8d --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_in_subquery/go/query.sql.go @@ -0,0 +1,112 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: query.sql + +package db + +import ( + "context" +) + +const getUsersInDepartments = `-- name: GetUsersInDepartments :many +SELECT id, name FROM users WHERE department_id IN (SELECT id FROM departments WHERE name = ?); +` + +type GetUsersInDepartmentsRow struct { + ID uint32 + Name string +} + +func (q *Queries) GetUsersInDepartments(ctx context.Context) ([]GetUsersInDepartmentsRow, error) { + rows, err := q.db.QueryContext(ctx, getUsersInDepartments) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetUsersInDepartmentsRow + for rows.Next() { + var i GetUsersInDepartmentsRow + if err := rows.Scan(&i.ID, &i.Name); 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 +} + +const getUsersNotInDepartments = `-- name: GetUsersNotInDepartments :many +SELECT id, name FROM users WHERE department_id NOT IN (SELECT id FROM departments WHERE name IN (?, ?)); +` + +type GetUsersNotInDepartmentsRow struct { + ID uint32 + Name string +} + +func (q *Queries) GetUsersNotInDepartments(ctx context.Context) ([]GetUsersNotInDepartmentsRow, error) { + rows, err := q.db.QueryContext(ctx, getUsersNotInDepartments) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetUsersNotInDepartmentsRow + for rows.Next() { + var i GetUsersNotInDepartmentsRow + if err := rows.Scan(&i.ID, &i.Name); 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 +} + +const getUsersWithIds = `-- name: GetUsersWithIds :many +SELECT id, name FROM users WHERE id IN (?, ?, ?); +` + +type GetUsersWithIdsParams struct { + ID uint32 + ID_2 uint32 + ID_3 uint32 +} + +type GetUsersWithIdsRow struct { + ID uint32 + Name string +} + +func (q *Queries) GetUsersWithIds(ctx context.Context, arg GetUsersWithIdsParams) ([]GetUsersWithIdsRow, error) { + rows, err := q.db.QueryContext(ctx, getUsersWithIds, arg.ID, arg.ID_2, arg.ID_3) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetUsersWithIdsRow + for rows.Next() { + var i GetUsersWithIdsRow + if err := rows.Scan(&i.ID, &i.Name); 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/clickhouse_in_subquery/query.sql b/internal/endtoend/testdata/clickhouse_in_subquery/query.sql new file mode 100644 index 0000000000..4dd806adc7 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_in_subquery/query.sql @@ -0,0 +1,8 @@ +-- name: GetUsersInDepartments :many +SELECT id, name FROM users WHERE department_id IN (SELECT id FROM departments WHERE name = ?); + +-- name: GetUsersNotInDepartments :many +SELECT id, name FROM users WHERE department_id NOT IN (SELECT id FROM departments WHERE name IN (?, ?)); + +-- name: GetUsersWithIds :many +SELECT id, name FROM users WHERE id IN (?, ?, ?); diff --git a/internal/endtoend/testdata/clickhouse_in_subquery/schema.sql b/internal/endtoend/testdata/clickhouse_in_subquery/schema.sql new file mode 100644 index 0000000000..151937ffbe --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_in_subquery/schema.sql @@ -0,0 +1,16 @@ +CREATE TABLE IF NOT EXISTS users +( + id UInt32, + name String, + department_id UInt32 +) +ENGINE = MergeTree() +ORDER BY id; + +CREATE TABLE IF NOT EXISTS departments +( + id UInt32, + name String +) +ENGINE = MergeTree() +ORDER BY id; diff --git a/internal/endtoend/testdata/clickhouse_in_subquery/sqlc.json b/internal/endtoend/testdata/clickhouse_in_subquery/sqlc.json new file mode 100644 index 0000000000..8c3f26ae8c --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_in_subquery/sqlc.json @@ -0,0 +1,16 @@ +{ + "version": "2", + "sql": [ + { + "engine": "clickhouse", + "queries": "query.sql", + "schema": "schema.sql", + "gen": { + "go": { + "out": "go", + "package": "db" + } + } + } + ] +} diff --git a/internal/endtoend/testdata/clickhouse_joins/go/db.go b/internal/endtoend/testdata/clickhouse_joins/go/db.go new file mode 100644 index 0000000000..cd5bbb8e08 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_joins/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +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/clickhouse_joins/go/models.go b/internal/endtoend/testdata/clickhouse_joins/go/models.go new file mode 100644 index 0000000000..6e89eccdb7 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_joins/go/models.go @@ -0,0 +1,28 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +import ( + "time" +) + +type Department struct { + ID uint32 + Name string + Location string +} + +type Order struct { + ID uint32 + UserID uint32 + Amount float64 + CreatedAt time.Time +} + +type User struct { + ID uint32 + Name string + DepartmentID uint32 +} diff --git a/internal/endtoend/testdata/clickhouse_joins/go/query.sql.go b/internal/endtoend/testdata/clickhouse_joins/go/query.sql.go new file mode 100644 index 0000000000..749fe79148 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_joins/go/query.sql.go @@ -0,0 +1,187 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: query.sql + +package db + +import ( + "context" + "database/sql" +) + +const getCompleteOrderInfo = `-- name: GetCompleteOrderInfo :one +SELECT + o.id, + o.amount, + u.id as user_id, + u.name as user_name, + d.id as department_id, + d.name as department_name +FROM orders o +INNER JOIN users u ON o.user_id = u.id +INNER JOIN departments d ON u.department_id = d.id +WHERE o.id = ?; + +` + +type GetCompleteOrderInfoRow struct { + ID uint32 + Amount float64 + UserID uint32 + UserName string + DepartmentID uint32 + DepartmentName string +} + +// RIGHT JOIN +func (q *Queries) GetCompleteOrderInfo(ctx context.Context, id uint32) (GetCompleteOrderInfoRow, error) { + row := q.db.QueryRowContext(ctx, getCompleteOrderInfo, id) + var i GetCompleteOrderInfoRow + err := row.Scan( + &i.ID, + &i.Amount, + &i.UserID, + &i.UserName, + &i.DepartmentID, + &i.DepartmentName, + ) + return i, err +} + +const getDepartmentsWithUsers = `-- name: GetDepartmentsWithUsers :many +SELECT + d.id, + d.name, + COUNT(u.id) as user_count +FROM departments d +RIGHT JOIN users u ON d.id = u.department_id +GROUP BY d.id, d.name +ORDER BY user_count DESC; +` + +type GetDepartmentsWithUsersRow struct { + ID sql.NullInt64 + Name sql.NullString + UserCount uint64 +} + +func (q *Queries) GetDepartmentsWithUsers(ctx context.Context) ([]GetDepartmentsWithUsersRow, error) { + rows, err := q.db.QueryContext(ctx, getDepartmentsWithUsers) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetDepartmentsWithUsersRow + for rows.Next() { + var i GetDepartmentsWithUsersRow + if err := rows.Scan(&i.ID, &i.Name, &i.UserCount); 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 +} + +const getUserOrders = `-- name: GetUserOrders :many +SELECT + u.id, + u.name, + o.id as order_id, + o.amount +FROM users u +LEFT JOIN orders o ON u.id = o.user_id +WHERE u.id = ? +ORDER BY o.created_at DESC; + +` + +type GetUserOrdersRow struct { + ID uint32 + Name string + OrderID sql.NullInt64 + Amount sql.NullFloat64 +} + +// Multiple JOINs +func (q *Queries) GetUserOrders(ctx context.Context, id uint32) ([]GetUserOrdersRow, error) { + rows, err := q.db.QueryContext(ctx, getUserOrders, id) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetUserOrdersRow + for rows.Next() { + var i GetUserOrdersRow + if err := rows.Scan( + &i.ID, + &i.Name, + &i.OrderID, + &i.Amount, + ); 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 +} + +const getUserWithDepartment = `-- name: GetUserWithDepartment :many +SELECT + u.id, + u.name, + d.id as department_id, + d.name as department_name +FROM users u +INNER JOIN departments d ON u.department_id = d.id +ORDER BY u.id; + +` + +type GetUserWithDepartmentRow struct { + ID uint32 + Name string + DepartmentID uint32 + DepartmentName string +} + +// LEFT JOIN +func (q *Queries) GetUserWithDepartment(ctx context.Context) ([]GetUserWithDepartmentRow, error) { + rows, err := q.db.QueryContext(ctx, getUserWithDepartment) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetUserWithDepartmentRow + for rows.Next() { + var i GetUserWithDepartmentRow + if err := rows.Scan( + &i.ID, + &i.Name, + &i.DepartmentID, + &i.DepartmentName, + ); 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/clickhouse_joins/query.sql b/internal/endtoend/testdata/clickhouse_joins/query.sql new file mode 100644 index 0000000000..e078a63927 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_joins/query.sql @@ -0,0 +1,47 @@ +-- INNER JOIN +-- name: GetUserWithDepartment :many +SELECT + u.id, + u.name, + d.id as department_id, + d.name as department_name +FROM users u +INNER JOIN departments d ON u.department_id = d.id +ORDER BY u.id; + +-- LEFT JOIN +-- name: GetUserOrders :many +SELECT + u.id, + u.name, + o.id as order_id, + o.amount +FROM users u +LEFT JOIN orders o ON u.id = o.user_id +WHERE u.id = ? +ORDER BY o.created_at DESC; + +-- Multiple JOINs +-- name: GetCompleteOrderInfo :one +SELECT + o.id, + o.amount, + u.id as user_id, + u.name as user_name, + d.id as department_id, + d.name as department_name +FROM orders o +INNER JOIN users u ON o.user_id = u.id +INNER JOIN departments d ON u.department_id = d.id +WHERE o.id = ?; + +-- RIGHT JOIN +-- name: GetDepartmentsWithUsers :many +SELECT + d.id, + d.name, + COUNT(u.id) as user_count +FROM departments d +RIGHT JOIN users u ON d.id = u.department_id +GROUP BY d.id, d.name +ORDER BY user_count DESC; diff --git a/internal/endtoend/testdata/clickhouse_joins/schema.sql b/internal/endtoend/testdata/clickhouse_joins/schema.sql new file mode 100644 index 0000000000..e3d5c350a0 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_joins/schema.sql @@ -0,0 +1,27 @@ +CREATE TABLE IF NOT EXISTS users +( + id UInt32, + name String, + department_id UInt32 +) +ENGINE = MergeTree() +ORDER BY id; + +CREATE TABLE IF NOT EXISTS departments +( + id UInt32, + name String, + location String +) +ENGINE = MergeTree() +ORDER BY id; + +CREATE TABLE IF NOT EXISTS orders +( + id UInt32, + user_id UInt32, + amount Float64, + created_at DateTime +) +ENGINE = MergeTree() +ORDER BY (id, user_id); diff --git a/internal/endtoend/testdata/clickhouse_joins/sqlc.json b/internal/endtoend/testdata/clickhouse_joins/sqlc.json new file mode 100644 index 0000000000..439f37f401 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_joins/sqlc.json @@ -0,0 +1,16 @@ +{ + "version": "2", + "sql": [ + { + "schema": "schema.sql", + "queries": "query.sql", + "engine": "clickhouse", + "gen": { + "go": { + "out": "go", + "package": "db" + } + } + } + ] +} diff --git a/internal/endtoend/testdata/clickhouse_named_params/go/db.go b/internal/endtoend/testdata/clickhouse_named_params/go/db.go new file mode 100644 index 0000000000..cd5bbb8e08 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_named_params/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +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/clickhouse_named_params/go/models.go b/internal/endtoend/testdata/clickhouse_named_params/go/models.go new file mode 100644 index 0000000000..4eec6c305f --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_named_params/go/models.go @@ -0,0 +1,11 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +type User struct { + ID uint32 + Name string + Email string +} diff --git a/internal/endtoend/testdata/clickhouse_named_params/go/query.sql.go b/internal/endtoend/testdata/clickhouse_named_params/go/query.sql.go new file mode 100644 index 0000000000..e78e09c646 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_named_params/go/query.sql.go @@ -0,0 +1,74 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: query.sql + +package db + +import ( + "context" + "strings" +) + +const filterUsersByIDs = `-- name: FilterUsersByIDs :many +SELECT id, name, email FROM users WHERE id IN ($1); +` + +func (q *Queries) FilterUsersByIDs(ctx context.Context, ids []uint32) ([]User, error) { + query := filterUsersByIDs + var queryParams []interface{} + if len(ids) > 0 { + for _, v := range ids { + queryParams = append(queryParams, v) + } + query = strings.Replace(query, "/*SLICE:ids*/?", strings.Repeat(",?", len(ids))[1:], 1) + } else { + query = strings.Replace(query, "/*SLICE:ids*/?", "NULL", 1) + } + rows, err := q.db.QueryContext(ctx, query, queryParams...) + if err != nil { + return nil, err + } + defer rows.Close() + var items []User + for rows.Next() { + var i User + if err := rows.Scan(&i.ID, &i.Name, &i.Email); 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 +} + +const getUserByEmail = `-- name: GetUserByEmail :one +SELECT id, name, email FROM users WHERE email = $1; +` + +func (q *Queries) GetUserByEmail(ctx context.Context, email string) (User, error) { + row := q.db.QueryRowContext(ctx, getUserByEmail, email) + var i User + err := row.Scan(&i.ID, &i.Name, &i.Email) + return i, err +} + +const insertUser = `-- name: InsertUser :exec +INSERT INTO users (id, name, email) VALUES ($1, $2, $3); +` + +type InsertUserParams struct { + ID uint32 + Name string + Email string +} + +func (q *Queries) InsertUser(ctx context.Context, arg InsertUserParams) error { + _, err := q.db.ExecContext(ctx, insertUser, arg.ID, arg.Name, arg.Email) + return err +} diff --git a/internal/endtoend/testdata/clickhouse_named_params/query.sql b/internal/endtoend/testdata/clickhouse_named_params/query.sql new file mode 100644 index 0000000000..e22f18db4c --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_named_params/query.sql @@ -0,0 +1,8 @@ +-- name: GetUserByEmail :one +SELECT id, name, email FROM users WHERE email = sqlc.arg('email'); + +-- name: InsertUser :exec +INSERT INTO users (id, name, email) VALUES (sqlc.arg('id'), sqlc.arg('name'), sqlc.arg('email')); + +-- name: FilterUsersByIDs :many +SELECT id, name, email FROM users WHERE id IN (sqlc.slice('ids')); diff --git a/internal/endtoend/testdata/clickhouse_named_params/schema.sql b/internal/endtoend/testdata/clickhouse_named_params/schema.sql new file mode 100644 index 0000000000..4cebdd7396 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_named_params/schema.sql @@ -0,0 +1,8 @@ +CREATE TABLE users +( + id UInt32, + name String, + email String +) +ENGINE = MergeTree() +ORDER BY id; diff --git a/internal/endtoend/testdata/clickhouse_named_params/sqlc.json b/internal/endtoend/testdata/clickhouse_named_params/sqlc.json new file mode 100644 index 0000000000..8c3f26ae8c --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_named_params/sqlc.json @@ -0,0 +1,16 @@ +{ + "version": "2", + "sql": [ + { + "engine": "clickhouse", + "queries": "query.sql", + "schema": "schema.sql", + "gen": { + "go": { + "out": "go", + "package": "db" + } + } + } + ] +} diff --git a/internal/endtoend/testdata/clickhouse_nulls/go/db.go b/internal/endtoend/testdata/clickhouse_nulls/go/db.go new file mode 100644 index 0000000000..cd5bbb8e08 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_nulls/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +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/clickhouse_nulls/go/models.go b/internal/endtoend/testdata/clickhouse_nulls/go/models.go new file mode 100644 index 0000000000..b4259b3299 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_nulls/go/models.go @@ -0,0 +1,17 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +import ( + "database/sql" +) + +type Product struct { + ID uint32 + Name string + Description sql.NullString + Discount sql.NullFloat64 + Category string +} diff --git a/internal/endtoend/testdata/clickhouse_nulls/go/query.sql.go b/internal/endtoend/testdata/clickhouse_nulls/go/query.sql.go new file mode 100644 index 0000000000..c53553cbb4 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_nulls/go/query.sql.go @@ -0,0 +1,142 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: query.sql + +package db + +import ( + "context" + "database/sql" +) + +const getProductsWithDefault = `-- name: GetProductsWithDefault :many +SELECT id, name, ifNull(category, 'Uncategorized') AS category FROM products; +` + +type GetProductsWithDefaultRow struct { + ID uint32 + Name string + Category interface{} +} + +func (q *Queries) GetProductsWithDefault(ctx context.Context) ([]GetProductsWithDefaultRow, error) { + rows, err := q.db.QueryContext(ctx, getProductsWithDefault) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetProductsWithDefaultRow + for rows.Next() { + var i GetProductsWithDefaultRow + if err := rows.Scan(&i.ID, &i.Name, &i.Category); 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 +} + +const getProductsWithDescription = `-- name: GetProductsWithDescription :many +SELECT id, name, description FROM products WHERE description IS NOT NULL; +` + +type GetProductsWithDescriptionRow struct { + ID uint32 + Name string + Description sql.NullString +} + +func (q *Queries) GetProductsWithDescription(ctx context.Context) ([]GetProductsWithDescriptionRow, error) { + rows, err := q.db.QueryContext(ctx, getProductsWithDescription) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetProductsWithDescriptionRow + for rows.Next() { + var i GetProductsWithDescriptionRow + if err := rows.Scan(&i.ID, &i.Name, &i.Description); 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 +} + +const getProductsWithDiscount = `-- name: GetProductsWithDiscount :many +SELECT id, name, coalesce(discount, 0) AS discount FROM products; +` + +type GetProductsWithDiscountRow struct { + ID uint32 + Name string + Discount interface{} +} + +func (q *Queries) GetProductsWithDiscount(ctx context.Context) ([]GetProductsWithDiscountRow, error) { + rows, err := q.db.QueryContext(ctx, getProductsWithDiscount) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetProductsWithDiscountRow + for rows.Next() { + var i GetProductsWithDiscountRow + if err := rows.Scan(&i.ID, &i.Name, &i.Discount); 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 +} + +const getProductsWithoutDescription = `-- name: GetProductsWithoutDescription :many +SELECT id, name FROM products WHERE description IS NULL; +` + +type GetProductsWithoutDescriptionRow struct { + ID uint32 + Name string +} + +func (q *Queries) GetProductsWithoutDescription(ctx context.Context) ([]GetProductsWithoutDescriptionRow, error) { + rows, err := q.db.QueryContext(ctx, getProductsWithoutDescription) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetProductsWithoutDescriptionRow + for rows.Next() { + var i GetProductsWithoutDescriptionRow + if err := rows.Scan(&i.ID, &i.Name); 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/clickhouse_nulls/query.sql b/internal/endtoend/testdata/clickhouse_nulls/query.sql new file mode 100644 index 0000000000..fc86c09e2b --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_nulls/query.sql @@ -0,0 +1,11 @@ +-- name: GetProductsWithoutDescription :many +SELECT id, name FROM products WHERE description IS NULL; + +-- name: GetProductsWithDescription :many +SELECT id, name, description FROM products WHERE description IS NOT NULL; + +-- name: GetProductsWithDiscount :many +SELECT id, name, coalesce(discount, 0) AS discount FROM products; + +-- name: GetProductsWithDefault :many +SELECT id, name, ifNull(category, 'Uncategorized') AS category FROM products; diff --git a/internal/endtoend/testdata/clickhouse_nulls/schema.sql b/internal/endtoend/testdata/clickhouse_nulls/schema.sql new file mode 100644 index 0000000000..067a5d6d39 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_nulls/schema.sql @@ -0,0 +1,10 @@ +CREATE TABLE IF NOT EXISTS products +( + id UInt32, + name String, + description Nullable(String), + discount Nullable(Float32), + category String +) +ENGINE = MergeTree() +ORDER BY id; diff --git a/internal/endtoend/testdata/clickhouse_nulls/sqlc.json b/internal/endtoend/testdata/clickhouse_nulls/sqlc.json new file mode 100644 index 0000000000..8c3f26ae8c --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_nulls/sqlc.json @@ -0,0 +1,16 @@ +{ + "version": "2", + "sql": [ + { + "engine": "clickhouse", + "queries": "query.sql", + "schema": "schema.sql", + "gen": { + "go": { + "out": "go", + "package": "db" + } + } + } + ] +} diff --git a/internal/endtoend/testdata/clickhouse_ordering/go/db.go b/internal/endtoend/testdata/clickhouse_ordering/go/db.go new file mode 100644 index 0000000000..cd5bbb8e08 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_ordering/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +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/clickhouse_ordering/go/models.go b/internal/endtoend/testdata/clickhouse_ordering/go/models.go new file mode 100644 index 0000000000..e555f80ae4 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_ordering/go/models.go @@ -0,0 +1,16 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +import ( + "time" +) + +type Order struct { + ID uint32 + CustomerName string + Total float32 + CreatedAt time.Time +} diff --git a/internal/endtoend/testdata/clickhouse_ordering/go/query.sql.go b/internal/endtoend/testdata/clickhouse_ordering/go/query.sql.go new file mode 100644 index 0000000000..5f064a18ac --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_ordering/go/query.sql.go @@ -0,0 +1,109 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: query.sql + +package db + +import ( + "context" + "time" +) + +const listOrdersAscending = `-- name: ListOrdersAscending :many +SELECT id, total FROM orders ORDER BY total ASC; +` + +type ListOrdersAscendingRow struct { + ID uint32 + Total float32 +} + +func (q *Queries) ListOrdersAscending(ctx context.Context) ([]ListOrdersAscendingRow, error) { + rows, err := q.db.QueryContext(ctx, listOrdersAscending) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListOrdersAscendingRow + for rows.Next() { + var i ListOrdersAscendingRow + if err := rows.Scan(&i.ID, &i.Total); 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 +} + +const listOrdersByCustomerAndDate = `-- name: ListOrdersByCustomerAndDate :many +SELECT id, customer_name, created_at FROM orders ORDER BY customer_name, created_at DESC; +` + +type ListOrdersByCustomerAndDateRow struct { + ID uint32 + CustomerName string + CreatedAt time.Time +} + +func (q *Queries) ListOrdersByCustomerAndDate(ctx context.Context) ([]ListOrdersByCustomerAndDateRow, error) { + rows, err := q.db.QueryContext(ctx, listOrdersByCustomerAndDate) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListOrdersByCustomerAndDateRow + for rows.Next() { + var i ListOrdersByCustomerAndDateRow + if err := rows.Scan(&i.ID, &i.CustomerName, &i.CreatedAt); 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 +} + +const listOrdersByTotal = `-- name: ListOrdersByTotal :many +SELECT id, customer_name, total FROM orders ORDER BY total DESC; +` + +type ListOrdersByTotalRow struct { + ID uint32 + CustomerName string + Total float32 +} + +func (q *Queries) ListOrdersByTotal(ctx context.Context) ([]ListOrdersByTotalRow, error) { + rows, err := q.db.QueryContext(ctx, listOrdersByTotal) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListOrdersByTotalRow + for rows.Next() { + var i ListOrdersByTotalRow + if err := rows.Scan(&i.ID, &i.CustomerName, &i.Total); 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/clickhouse_ordering/query.sql b/internal/endtoend/testdata/clickhouse_ordering/query.sql new file mode 100644 index 0000000000..cd111c3682 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_ordering/query.sql @@ -0,0 +1,8 @@ +-- name: ListOrdersByTotal :many +SELECT id, customer_name, total FROM orders ORDER BY total DESC; + +-- name: ListOrdersByCustomerAndDate :many +SELECT id, customer_name, created_at FROM orders ORDER BY customer_name, created_at DESC; + +-- name: ListOrdersAscending :many +SELECT id, total FROM orders ORDER BY total ASC; diff --git a/internal/endtoend/testdata/clickhouse_ordering/schema.sql b/internal/endtoend/testdata/clickhouse_ordering/schema.sql new file mode 100644 index 0000000000..cb6f46ba2a --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_ordering/schema.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS orders +( + id UInt32, + customer_name String, + total Float32, + created_at DateTime +) +ENGINE = MergeTree() +ORDER BY id; diff --git a/internal/endtoend/testdata/clickhouse_ordering/sqlc.json b/internal/endtoend/testdata/clickhouse_ordering/sqlc.json new file mode 100644 index 0000000000..8c3f26ae8c --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_ordering/sqlc.json @@ -0,0 +1,16 @@ +{ + "version": "2", + "sql": [ + { + "engine": "clickhouse", + "queries": "query.sql", + "schema": "schema.sql", + "gen": { + "go": { + "out": "go", + "package": "db" + } + } + } + ] +} diff --git a/internal/endtoend/testdata/clickhouse_pagination/go/db.go b/internal/endtoend/testdata/clickhouse_pagination/go/db.go new file mode 100644 index 0000000000..cd5bbb8e08 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_pagination/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +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/clickhouse_pagination/go/models.go b/internal/endtoend/testdata/clickhouse_pagination/go/models.go new file mode 100644 index 0000000000..eb9a3ea2bb --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_pagination/go/models.go @@ -0,0 +1,12 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +type Product struct { + ID uint32 + Name string + Price float32 + Stock uint32 +} diff --git a/internal/endtoend/testdata/clickhouse_pagination/go/query.sql.go b/internal/endtoend/testdata/clickhouse_pagination/go/query.sql.go new file mode 100644 index 0000000000..2cfd85f48b --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_pagination/go/query.sql.go @@ -0,0 +1,114 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: query.sql + +package db + +import ( + "context" +) + +const getFirstNProducts = `-- name: GetFirstNProducts :many +SELECT id, name, price FROM products LIMIT ?; +` + +type GetFirstNProductsRow struct { + ID uint32 + Name string + Price float32 +} + +func (q *Queries) GetFirstNProducts(ctx context.Context, limit int64) ([]GetFirstNProductsRow, error) { + rows, err := q.db.QueryContext(ctx, getFirstNProducts, limit) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetFirstNProductsRow + for rows.Next() { + var i GetFirstNProductsRow + if err := rows.Scan(&i.ID, &i.Name, &i.Price); 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 +} + +const getProductsWithOffset = `-- name: GetProductsWithOffset :many +SELECT id, name, price FROM products ORDER BY id LIMIT 10 OFFSET ?; +` + +type GetProductsWithOffsetRow struct { + ID uint32 + Name string + Price float32 +} + +func (q *Queries) GetProductsWithOffset(ctx context.Context, offset int64) ([]GetProductsWithOffsetRow, error) { + rows, err := q.db.QueryContext(ctx, getProductsWithOffset, offset) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetProductsWithOffsetRow + for rows.Next() { + var i GetProductsWithOffsetRow + if err := rows.Scan(&i.ID, &i.Name, &i.Price); 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 +} + +const listProductsPage = `-- name: ListProductsPage :many +SELECT id, name, price FROM products ORDER BY id LIMIT ? OFFSET ?; +` + +type ListProductsPageParams struct { + Limit int64 + Offset int64 +} + +type ListProductsPageRow struct { + ID uint32 + Name string + Price float32 +} + +func (q *Queries) ListProductsPage(ctx context.Context, arg ListProductsPageParams) ([]ListProductsPageRow, error) { + rows, err := q.db.QueryContext(ctx, listProductsPage, arg.Limit, arg.Offset) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListProductsPageRow + for rows.Next() { + var i ListProductsPageRow + if err := rows.Scan(&i.ID, &i.Name, &i.Price); 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/clickhouse_pagination/query.sql b/internal/endtoend/testdata/clickhouse_pagination/query.sql new file mode 100644 index 0000000000..b61d1ad3b3 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_pagination/query.sql @@ -0,0 +1,8 @@ +-- name: ListProductsPage :many +SELECT id, name, price FROM products ORDER BY id LIMIT ? OFFSET ?; + +-- name: GetFirstNProducts :many +SELECT id, name, price FROM products LIMIT ?; + +-- name: GetProductsWithOffset :many +SELECT id, name, price FROM products ORDER BY id LIMIT 10 OFFSET ?; diff --git a/internal/endtoend/testdata/clickhouse_pagination/schema.sql b/internal/endtoend/testdata/clickhouse_pagination/schema.sql new file mode 100644 index 0000000000..bf182a05c0 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_pagination/schema.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS products +( + id UInt32, + name String, + price Float32, + stock UInt32 +) +ENGINE = MergeTree() +ORDER BY id; diff --git a/internal/endtoend/testdata/clickhouse_pagination/sqlc.json b/internal/endtoend/testdata/clickhouse_pagination/sqlc.json new file mode 100644 index 0000000000..8c3f26ae8c --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_pagination/sqlc.json @@ -0,0 +1,16 @@ +{ + "version": "2", + "sql": [ + { + "engine": "clickhouse", + "queries": "query.sql", + "schema": "schema.sql", + "gen": { + "go": { + "out": "go", + "package": "db" + } + } + } + ] +} diff --git a/internal/endtoend/testdata/clickhouse_select/go/db.go b/internal/endtoend/testdata/clickhouse_select/go/db.go new file mode 100644 index 0000000000..cd5bbb8e08 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_select/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +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/clickhouse_select/go/models.go b/internal/endtoend/testdata/clickhouse_select/go/models.go new file mode 100644 index 0000000000..4eec6c305f --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_select/go/models.go @@ -0,0 +1,11 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +type User struct { + ID uint32 + Name string + Email string +} diff --git a/internal/endtoend/testdata/clickhouse_select/go/query.sql.go b/internal/endtoend/testdata/clickhouse_select/go/query.sql.go new file mode 100644 index 0000000000..7bc55dc954 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_select/go/query.sql.go @@ -0,0 +1,48 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: query.sql + +package db + +import ( + "context" +) + +const getUser = `-- name: GetUser :one +SELECT id, name, email FROM users WHERE id = ?; +` + +func (q *Queries) GetUser(ctx context.Context, id uint32) (User, error) { + row := q.db.QueryRowContext(ctx, getUser, id) + var i User + err := row.Scan(&i.ID, &i.Name, &i.Email) + return i, err +} + +const listUsers = `-- name: ListUsers :many +SELECT id, name, email FROM users ORDER BY name; +` + +func (q *Queries) ListUsers(ctx context.Context) ([]User, error) { + rows, err := q.db.QueryContext(ctx, listUsers) + if err != nil { + return nil, err + } + defer rows.Close() + var items []User + for rows.Next() { + var i User + if err := rows.Scan(&i.ID, &i.Name, &i.Email); 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/clickhouse_select/query.sql b/internal/endtoend/testdata/clickhouse_select/query.sql new file mode 100644 index 0000000000..32dbc22fff --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_select/query.sql @@ -0,0 +1,5 @@ +-- name: GetUser :one +SELECT id, name, email FROM users WHERE id = ?; + +-- name: ListUsers :many +SELECT id, name, email FROM users ORDER BY name; diff --git a/internal/endtoend/testdata/clickhouse_select/schema.sql b/internal/endtoend/testdata/clickhouse_select/schema.sql new file mode 100644 index 0000000000..eb2ae68d06 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_select/schema.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS users +( + id UInt32, + name String, + email String +) +ENGINE = MergeTree() +ORDER BY id; diff --git a/internal/endtoend/testdata/clickhouse_select/sqlc.json b/internal/endtoend/testdata/clickhouse_select/sqlc.json new file mode 100644 index 0000000000..8c3f26ae8c --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_select/sqlc.json @@ -0,0 +1,16 @@ +{ + "version": "2", + "sql": [ + { + "engine": "clickhouse", + "queries": "query.sql", + "schema": "schema.sql", + "gen": { + "go": { + "out": "go", + "package": "db" + } + } + } + ] +} diff --git a/internal/endtoend/testdata/clickhouse_strings/go/db.go b/internal/endtoend/testdata/clickhouse_strings/go/db.go new file mode 100644 index 0000000000..cd5bbb8e08 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_strings/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +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/clickhouse_strings/go/models.go b/internal/endtoend/testdata/clickhouse_strings/go/models.go new file mode 100644 index 0000000000..3ab8a05b32 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_strings/go/models.go @@ -0,0 +1,13 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +type Employee struct { + ID uint32 + FirstName string + LastName string + Email string + Bio string +} diff --git a/internal/endtoend/testdata/clickhouse_strings/go/query.sql.go b/internal/endtoend/testdata/clickhouse_strings/go/query.sql.go new file mode 100644 index 0000000000..c897d60f16 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_strings/go/query.sql.go @@ -0,0 +1,140 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: query.sql + +package db + +import ( + "context" +) + +const getEmailDomain = `-- name: GetEmailDomain :many +SELECT id, email, substring(email, position(email, '@') + 1) AS domain FROM employees; +` + +type GetEmailDomainRow struct { + ID uint32 + Email string + Domain string +} + +func (q *Queries) GetEmailDomain(ctx context.Context) ([]GetEmailDomainRow, error) { + rows, err := q.db.QueryContext(ctx, getEmailDomain) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetEmailDomainRow + for rows.Next() { + var i GetEmailDomainRow + if err := rows.Scan(&i.ID, &i.Email, &i.Domain); 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 +} + +const getFullName = `-- name: GetFullName :many +SELECT id, concat(first_name, ' ', last_name) AS full_name FROM employees; +` + +type GetFullNameRow struct { + ID uint32 + FullName string +} + +func (q *Queries) GetFullName(ctx context.Context) ([]GetFullNameRow, error) { + rows, err := q.db.QueryContext(ctx, getFullName) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetFullNameRow + for rows.Next() { + var i GetFullNameRow + if err := rows.Scan(&i.ID, &i.FullName); 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 +} + +const getUppercaseNames = `-- name: GetUppercaseNames :many +SELECT id, upper(first_name) AS first_name_upper, lower(last_name) AS last_name_lower FROM employees; +` + +type GetUppercaseNamesRow struct { + ID uint32 + FirstNameUpper string + LastNameLower string +} + +func (q *Queries) GetUppercaseNames(ctx context.Context) ([]GetUppercaseNamesRow, error) { + rows, err := q.db.QueryContext(ctx, getUppercaseNames) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetUppercaseNamesRow + for rows.Next() { + var i GetUppercaseNamesRow + if err := rows.Scan(&i.ID, &i.FirstNameUpper, &i.LastNameLower); 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 +} + +const trimWhitespace = `-- name: TrimWhitespace :many +SELECT id, trim(bio) AS bio FROM employees; +` + +type TrimWhitespaceRow struct { + ID uint32 + Bio string +} + +func (q *Queries) TrimWhitespace(ctx context.Context) ([]TrimWhitespaceRow, error) { + rows, err := q.db.QueryContext(ctx, trimWhitespace) + if err != nil { + return nil, err + } + defer rows.Close() + var items []TrimWhitespaceRow + for rows.Next() { + var i TrimWhitespaceRow + if err := rows.Scan(&i.ID, &i.Bio); 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/clickhouse_strings/query.sql b/internal/endtoend/testdata/clickhouse_strings/query.sql new file mode 100644 index 0000000000..1e80eafc4f --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_strings/query.sql @@ -0,0 +1,11 @@ +-- name: GetFullName :many +SELECT id, concat(first_name, ' ', last_name) AS full_name FROM employees; + +-- name: GetUppercaseNames :many +SELECT id, upper(first_name) AS first_name_upper, lower(last_name) AS last_name_lower FROM employees; + +-- name: GetEmailDomain :many +SELECT id, email, substring(email, position(email, '@') + 1) AS domain FROM employees; + +-- name: TrimWhitespace :many +SELECT id, trim(bio) AS bio FROM employees; diff --git a/internal/endtoend/testdata/clickhouse_strings/schema.sql b/internal/endtoend/testdata/clickhouse_strings/schema.sql new file mode 100644 index 0000000000..42f01ccb83 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_strings/schema.sql @@ -0,0 +1,10 @@ +CREATE TABLE IF NOT EXISTS employees +( + id UInt32, + first_name String, + last_name String, + email String, + bio String +) +ENGINE = MergeTree() +ORDER BY id; diff --git a/internal/endtoend/testdata/clickhouse_strings/sqlc.json b/internal/endtoend/testdata/clickhouse_strings/sqlc.json new file mode 100644 index 0000000000..8c3f26ae8c --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_strings/sqlc.json @@ -0,0 +1,16 @@ +{ + "version": "2", + "sql": [ + { + "engine": "clickhouse", + "queries": "query.sql", + "schema": "schema.sql", + "gen": { + "go": { + "out": "go", + "package": "db" + } + } + } + ] +} diff --git a/internal/endtoend/testdata/clickhouse_subqueries/go/db.go b/internal/endtoend/testdata/clickhouse_subqueries/go/db.go new file mode 100644 index 0000000000..cd5bbb8e08 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_subqueries/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +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/clickhouse_subqueries/go/models.go b/internal/endtoend/testdata/clickhouse_subqueries/go/models.go new file mode 100644 index 0000000000..c0288e5ebc --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_subqueries/go/models.go @@ -0,0 +1,24 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package db + +import ( + "time" +) + +type Employee struct { + ID uint32 + Name string + Salary float64 + Department string + HireDate time.Time +} + +type SalariesHistory struct { + ID uint32 + EmployeeID uint32 + Salary float64 + EffectiveDate time.Time +} diff --git a/internal/endtoend/testdata/clickhouse_subqueries/go/query.sql.go b/internal/endtoend/testdata/clickhouse_subqueries/go/query.sql.go new file mode 100644 index 0000000000..bb46e8a290 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_subqueries/go/query.sql.go @@ -0,0 +1,94 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: query.sql + +package db + +import ( + "context" +) + +const getAllEmployees = `-- name: GetAllEmployees :many +SELECT + id, + name, + salary, + department +FROM employees +ORDER BY id; +` + +type GetAllEmployeesRow struct { + ID uint32 + Name string + Salary float64 + Department string +} + +func (q *Queries) GetAllEmployees(ctx context.Context) ([]GetAllEmployeesRow, error) { + rows, err := q.db.QueryContext(ctx, getAllEmployees) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetAllEmployeesRow + for rows.Next() { + var i GetAllEmployeesRow + if err := rows.Scan( + &i.ID, + &i.Name, + &i.Salary, + &i.Department, + ); 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 +} + +const getEmployeesByDepartment = `-- name: GetEmployeesByDepartment :many +SELECT + id, + name, + salary +FROM employees +WHERE department = ? +ORDER BY salary DESC; +` + +type GetEmployeesByDepartmentRow struct { + ID uint32 + Name string + Salary float64 +} + +func (q *Queries) GetEmployeesByDepartment(ctx context.Context, department string) ([]GetEmployeesByDepartmentRow, error) { + rows, err := q.db.QueryContext(ctx, getEmployeesByDepartment, department) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetEmployeesByDepartmentRow + for rows.Next() { + var i GetEmployeesByDepartmentRow + if err := rows.Scan(&i.ID, &i.Name, &i.Salary); 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/clickhouse_subqueries/query.sql b/internal/endtoend/testdata/clickhouse_subqueries/query.sql new file mode 100644 index 0000000000..33aa438306 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_subqueries/query.sql @@ -0,0 +1,18 @@ +-- Simple queries on employees table +-- name: GetAllEmployees :many +SELECT + id, + name, + salary, + department +FROM employees +ORDER BY id; + +-- name: GetEmployeesByDepartment :many +SELECT + id, + name, + salary +FROM employees +WHERE department = ? +ORDER BY salary DESC; diff --git a/internal/endtoend/testdata/clickhouse_subqueries/schema.sql b/internal/endtoend/testdata/clickhouse_subqueries/schema.sql new file mode 100644 index 0000000000..1a24c2ce2f --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_subqueries/schema.sql @@ -0,0 +1,20 @@ +CREATE TABLE IF NOT EXISTS employees +( + id UInt32, + name String, + salary Float64, + department String, + hire_date DateTime +) +ENGINE = MergeTree() +ORDER BY id; + +CREATE TABLE IF NOT EXISTS salaries_history +( + id UInt32, + employee_id UInt32, + salary Float64, + effective_date DateTime +) +ENGINE = MergeTree() +ORDER BY (id, employee_id, effective_date); diff --git a/internal/endtoend/testdata/clickhouse_subqueries/sqlc.json b/internal/endtoend/testdata/clickhouse_subqueries/sqlc.json new file mode 100644 index 0000000000..439f37f401 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_subqueries/sqlc.json @@ -0,0 +1,16 @@ +{ + "version": "2", + "sql": [ + { + "schema": "schema.sql", + "queries": "query.sql", + "engine": "clickhouse", + "gen": { + "go": { + "out": "go", + "package": "db" + } + } + } + ] +} diff --git a/internal/endtoend/testdata/join_using_single/postgresql/go/db.go b/internal/endtoend/testdata/join_using_single/postgresql/go/db.go new file mode 100644 index 0000000000..3b320aa168 --- /dev/null +++ b/internal/endtoend/testdata/join_using_single/postgresql/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +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/join_using_single/postgresql/go/models.go b/internal/endtoend/testdata/join_using_single/postgresql/go/models.go new file mode 100644 index 0000000000..2f4cf01268 --- /dev/null +++ b/internal/endtoend/testdata/join_using_single/postgresql/go/models.go @@ -0,0 +1,21 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package querytest + +import ( + "database/sql" +) + +type Order struct { + OrderID int32 + CustomerName sql.NullString + Amount sql.NullString +} + +type Shipment struct { + ShipmentID int32 + OrderID sql.NullInt32 + Address sql.NullString +} diff --git a/internal/endtoend/testdata/join_using_single/postgresql/go/query.sql.go b/internal/endtoend/testdata/join_using_single/postgresql/go/query.sql.go new file mode 100644 index 0000000000..5ad1ce538a --- /dev/null +++ b/internal/endtoend/testdata/join_using_single/postgresql/go/query.sql.go @@ -0,0 +1,54 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: query.sql + +package querytest + +import ( + "context" + "database/sql" +) + +const getOrdersWithShipments = `-- name: GetOrdersWithShipments :many +SELECT order_id, customer_name, amount, shipment_id, address +FROM orders +LEFT JOIN shipments USING (order_id) +` + +type GetOrdersWithShipmentsRow struct { + OrderID int32 + CustomerName sql.NullString + Amount sql.NullString + ShipmentID sql.NullInt32 + Address sql.NullString +} + +func (q *Queries) GetOrdersWithShipments(ctx context.Context) ([]GetOrdersWithShipmentsRow, error) { + rows, err := q.db.QueryContext(ctx, getOrdersWithShipments) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetOrdersWithShipmentsRow + for rows.Next() { + var i GetOrdersWithShipmentsRow + if err := rows.Scan( + &i.OrderID, + &i.CustomerName, + &i.Amount, + &i.ShipmentID, + &i.Address, + ); 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/join_using_single/query.sql b/internal/endtoend/testdata/join_using_single/query.sql new file mode 100644 index 0000000000..1faeb0de06 --- /dev/null +++ b/internal/endtoend/testdata/join_using_single/query.sql @@ -0,0 +1,4 @@ +-- name: GetOrdersWithShipments :many +SELECT * +FROM orders +LEFT JOIN shipments USING (order_id); diff --git a/internal/endtoend/testdata/join_using_single/schema.sql b/internal/endtoend/testdata/join_using_single/schema.sql new file mode 100644 index 0000000000..a5aebc63f1 --- /dev/null +++ b/internal/endtoend/testdata/join_using_single/schema.sql @@ -0,0 +1,11 @@ +CREATE TABLE orders ( + order_id SERIAL PRIMARY KEY, + customer_name TEXT, + amount DECIMAL(10, 2) +); + +CREATE TABLE shipments ( + shipment_id SERIAL PRIMARY KEY, + order_id INT REFERENCES orders(order_id), + address TEXT +); diff --git a/internal/endtoend/testdata/join_using_single/sqlc.json b/internal/endtoend/testdata/join_using_single/sqlc.json new file mode 100644 index 0000000000..7e80c9d144 --- /dev/null +++ b/internal/endtoend/testdata/join_using_single/sqlc.json @@ -0,0 +1,12 @@ +{ + "version": "1", + "packages": [ + { + "path": "postgresql/go", + "engine": "postgresql", + "name": "querytest", + "schema": "schema.sql", + "queries": "query.sql" + } + ] +} diff --git a/internal/engine/clickhouse/array_join_columns_test.go b/internal/engine/clickhouse/array_join_columns_test.go new file mode 100644 index 0000000000..82e5558423 --- /dev/null +++ b/internal/engine/clickhouse/array_join_columns_test.go @@ -0,0 +1,128 @@ +package clickhouse + +import ( + "strings" + "testing" + + "github.com/sqlc-dev/sqlc/internal/sql/ast" +) + +// TestArrayJoinColumnAliases validates that ARRAY JOIN creates properly aliased columns +// These columns should be available for reference in the SELECT list +func TestArrayJoinColumnAliases(t *testing.T) { + parser := NewParser() + + tests := []struct { + name string + query string + expectedColnames []string // column names from ARRAY JOIN + wantErr bool + }{ + { + name: "simple array join with alias", + query: ` + SELECT id, tag + FROM users + ARRAY JOIN tags AS tag + `, + expectedColnames: []string{"tag"}, + wantErr: false, + }, + { + name: "single array join with table alias and qualified name", + query: ` + SELECT u.id, u.name, tag + FROM users u + ARRAY JOIN u.tags AS tag + `, + expectedColnames: []string{"tag"}, + wantErr: false, + }, + { + name: "multiple array joins with aliases", + query: ` + SELECT event_id, event_name, prop_key, prop_value + FROM events + ARRAY JOIN properties.keys AS prop_key, properties.values AS prop_value + `, + expectedColnames: []string{"prop_key", "prop_value"}, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + stmts, err := parser.Parse(strings.NewReader(tt.query)) + if (err != nil) != tt.wantErr { + t.Fatalf("Parse error: %v, wantErr %v", err, tt.wantErr) + } + + if len(stmts) == 0 { + t.Fatal("No statements parsed") + } + + selectStmt, ok := stmts[0].Raw.Stmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", stmts[0].Raw.Stmt) + } + + // Check that the FROM clause contains the ARRAY JOIN as a RangeSubselect + if selectStmt.FromClause == nil || len(selectStmt.FromClause.Items) == 0 { + t.Fatal("No FROM clause items found") + } + + // Find the RangeSubselect that represents the ARRAY JOIN + var arrayJoinRangeSubselect *ast.RangeSubselect + for _, item := range selectStmt.FromClause.Items { + if rs, ok := item.(*ast.RangeSubselect); ok { + arrayJoinRangeSubselect = rs + break + } + } + + if arrayJoinRangeSubselect == nil { + t.Fatal("No RangeSubselect found for ARRAY JOIN") + } + + // Verify that the RangeSubselect has a Subquery (synthetic SELECT statement) + if arrayJoinRangeSubselect.Subquery == nil { + t.Error("ARRAY JOIN RangeSubselect has no Subquery") + return + } + + syntheticSelect, ok := arrayJoinRangeSubselect.Subquery.(*ast.SelectStmt) + if !ok { + t.Errorf("Expected SelectStmt subquery, got %T", arrayJoinRangeSubselect.Subquery) + return + } + + // Verify the target list has the expected column names + if syntheticSelect.TargetList == nil || len(syntheticSelect.TargetList.Items) == 0 { + t.Error("Synthetic SELECT has no target list") + return + } + + if len(syntheticSelect.TargetList.Items) != len(tt.expectedColnames) { + t.Errorf("Expected %d targets, got %d", len(tt.expectedColnames), len(syntheticSelect.TargetList.Items)) + return + } + + // Verify the target values (which should be ResTargets with Name set) + for i, expected := range tt.expectedColnames { + target, ok := syntheticSelect.TargetList.Items[i].(*ast.ResTarget) + if !ok { + t.Errorf("Target %d is not a ResTarget: %T", i, syntheticSelect.TargetList.Items[i]) + continue + } + + if target.Name == nil || *target.Name != expected { + var name string + if target.Name != nil { + name = *target.Name + } + t.Errorf("Target %d: expected name %q, got %q", i, expected, name) + } + } + }) + } +} diff --git a/internal/engine/clickhouse/case_sensitivity_test.go b/internal/engine/clickhouse/case_sensitivity_test.go new file mode 100644 index 0000000000..19b947ecf7 --- /dev/null +++ b/internal/engine/clickhouse/case_sensitivity_test.go @@ -0,0 +1,165 @@ +package clickhouse + +import ( + "strings" + "testing" + + "github.com/sqlc-dev/sqlc/internal/sql/ast" +) + +func TestCaseSensitiveColumns(t *testing.T) { + // ClickHouse is case-sensitive for identifiers + // This test demonstrates the issue where columns with different cases + // are incorrectly treated as the same column + + sql := ` +CREATE TABLE test_table +( + UserId UInt32, + userName String, + EMAIL String +) +ENGINE = MergeTree() +ORDER BY UserId; +` + + p := NewParser() + stmts, err := p.Parse(strings.NewReader(sql)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(stmts) != 1 { + t.Fatalf("Expected 1 statement, got %d", len(stmts)) + } + + createStmt, ok := stmts[0].Raw.Stmt.(*ast.CreateTableStmt) + if !ok { + t.Fatalf("Expected CreateTableStmt, got %T", stmts[0].Raw.Stmt) + } + + // Check that column names preserve their case + expectedColumns := map[string]bool{ + "UserId": true, + "userName": true, + "EMAIL": true, + } + + actualColumns := make(map[string]bool) + for _, col := range createStmt.Cols { + actualColumns[col.Colname] = true + } + + if len(actualColumns) != len(expectedColumns) { + t.Errorf("Expected %d distinct columns, got %d", len(expectedColumns), len(actualColumns)) + } + + for expected := range expectedColumns { + if !actualColumns[expected] { + t.Errorf("Column '%s' not found. Found columns: %v", expected, actualColumns) + } + } +} + +func TestCaseSensitiveColumnReference(t *testing.T) { + // Test that column references preserve case in SELECT statements + sql := "SELECT UserId, userName, EMAIL FROM test_table;" + + p := NewParser() + stmts, err := p.Parse(strings.NewReader(sql)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(stmts) != 1 { + t.Fatalf("Expected 1 statement, got %d", len(stmts)) + } + + selectStmt, ok := stmts[0].Raw.Stmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", stmts[0].Raw.Stmt) + } + + expectedColRefs := []string{"UserId", "userName", "EMAIL"} + if len(selectStmt.TargetList.Items) != len(expectedColRefs) { + t.Fatalf("Expected %d target items, got %d", len(expectedColRefs), len(selectStmt.TargetList.Items)) + } + + for i, expected := range expectedColRefs { + target, ok := selectStmt.TargetList.Items[i].(*ast.ResTarget) + if !ok { + t.Fatalf("Item %d is not a ResTarget: %T", i, selectStmt.TargetList.Items[i]) + } + + // Check if Name is set (for aliased columns) or extract from ColumnRef + var got string + if target.Name != nil && *target.Name != "" { + got = *target.Name + } else if colRef, ok := target.Val.(*ast.ColumnRef); ok && colRef != nil && colRef.Fields != nil && len(colRef.Fields.Items) > 0 { + // Extract the column name from the ColumnRef + if s, ok := colRef.Fields.Items[len(colRef.Fields.Items)-1].(*ast.String); ok { + got = s.Str + } + } + + if got != expected { + t.Errorf("Column %d: expected '%s', got '%s'", i, expected, got) + } + } +} + +func TestCaseSensitiveWhereClauses(t *testing.T) { + // Test that WHERE clauses with case-sensitive column names work correctly + sql := "SELECT * FROM users WHERE UserId = 123 AND userName = 'John';" + + p := NewParser() + stmts, err := p.Parse(strings.NewReader(sql)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + selectStmt, ok := stmts[0].Raw.Stmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", stmts[0].Raw.Stmt) + } + + // Verify WHERE clause references preserve case + if selectStmt.WhereClause == nil { + t.Fatal("WHERE clause is nil") + } + + // The WHERE clause should contain column references with preserved case + // This is a simple check - we'd need to traverse the AST to verify + // that column names in the WHERE clause preserve their case + whereStr := astToString(selectStmt.WhereClause) + + // Check that the case is preserved in the where clause + if !strings.Contains(whereStr, "UserId") || !strings.Contains(whereStr, "userName") { + t.Errorf("WHERE clause should preserve column name case. Got: %s", whereStr) + } +} + +// astToString converts AST nodes to a string representation for testing +func astToString(node ast.Node) string { + if node == nil { + return "" + } + + switch n := node.(type) { + case *ast.A_Expr: + left := astToString(n.Lexpr) + right := astToString(n.Rexpr) + return left + " " + right + case *ast.ColumnRef: + if n.Fields != nil && len(n.Fields.Items) > 0 { + if s, ok := n.Fields.Items[len(n.Fields.Items)-1].(*ast.String); ok { + return s.Str + } + } + case *ast.A_Const: + if s, ok := n.Val.(*ast.String); ok { + return s.Str + } + } + return "" +} diff --git a/internal/engine/clickhouse/catalog.go b/internal/engine/clickhouse/catalog.go new file mode 100644 index 0000000000..8cc5339363 --- /dev/null +++ b/internal/engine/clickhouse/catalog.go @@ -0,0 +1,355 @@ +package clickhouse + +import ( + "github.com/sqlc-dev/sqlc/internal/sql/ast" + "github.com/sqlc-dev/sqlc/internal/sql/catalog" +) + +// NewCatalog creates a new ClickHouse catalog with default settings +func NewCatalog() *catalog.Catalog { + // ClickHouse uses "default" as the default database + defaultSchemaName := "default" + + cat := &catalog.Catalog{ + DefaultSchema: defaultSchemaName, + Schemas: []*catalog.Schema{ + newDefaultSchema(defaultSchemaName), + }, + Extensions: map[string]struct{}{}, + } + + // Register ClickHouse built-in functions with fixed return types + registerBuiltinFunctions(cat) + + return cat +} + +// newDefaultSchema creates the default ClickHouse schema +func newDefaultSchema(name string) *catalog.Schema { + return &catalog.Schema{ + Name: name, + Tables: make([]*catalog.Table, 0), + Funcs: make([]*catalog.Function, 0), + } +} + +// registerBuiltinFunctions registers ClickHouse built-in functions in the default schema +func registerBuiltinFunctions(cat *catalog.Catalog) { + // Find the default schema + var schema *catalog.Schema + for _, s := range cat.Schemas { + if s.Name == cat.DefaultSchema { + schema = s + break + } + } + if schema == nil { + return + } + + if schema.Funcs == nil { + schema.Funcs = make([]*catalog.Function, 0) + } + + // Aggregate functions that always return uint64 + uint64Type := &ast.TypeName{Name: "uint64"} + int64Type := &ast.TypeName{Name: "int64"} + anyType := &ast.TypeName{Name: "any"} + for _, name := range []string{"count", "uniqexact", "countif", "uniq", "uniqcombined", "uniquehll12"} { + schema.Funcs = append(schema.Funcs, &catalog.Function{ + Name: name, + ReturnType: uint64Type, + Args: []*catalog.Argument{ + { + Name: "arg", + Type: anyType, + Mode: ast.FuncParamVariadic, + }, + }, + }) + } + + // Statistical aggregate functions + float64Type := &ast.TypeName{Name: "float64"} + for _, name := range []string{"varsamp", "varpop", "stddevsamp", "stddevpop", "corr", "covariance", "avg", "avgif"} { + schema.Funcs = append(schema.Funcs, &catalog.Function{ + Name: name, + ReturnType: float64Type, + Args: []*catalog.Argument{ + { + Name: "arg", + Type: anyType, + Mode: ast.FuncParamVariadic, + }, + }, + }) + } + + // Date/Time functions + dateType := &ast.TypeName{Name: "date"} + timeType := &ast.TypeName{Name: "timestamp"} + + // Functions returning Date + for _, name := range []string{"todate", "todate32", "today", "yesterday"} { + schema.Funcs = append(schema.Funcs, &catalog.Function{ + Name: name, + ReturnType: dateType, + Args: []*catalog.Argument{ + {Name: "arg", Type: anyType, Mode: ast.FuncParamVariadic}, + }, + }) + } + + // Functions returning Timestamp (DateTime/DateTime64) + for _, name := range []string{"now", "todatetime", "todatetime64", "parseDateTime", "parseDateTimeBestEffort", "parseDateTime64BestEffort"} { + schema.Funcs = append(schema.Funcs, &catalog.Function{ + Name: name, + ReturnType: timeType, + Args: []*catalog.Argument{ + {Name: "arg", Type: anyType, Mode: ast.FuncParamVariadic}, + }, + }) + } + + // Functions returning integers (components) + int32Type := &ast.TypeName{Name: "int32"} + for _, name := range []string{"toyear", "tomonth", "todayofmonth", "tohour", "tominute", "tosecond", "tounixtimestamp"} { + schema.Funcs = append(schema.Funcs, &catalog.Function{ + Name: name, + ReturnType: int32Type, + Args: []*catalog.Argument{ + {Name: "arg", Type: anyType, Mode: ast.FuncParamIn}, + }, + }) + } + + // String functions + stringType := &ast.TypeName{Name: "text"} + for _, name := range []string{ + "concat", "substring", "lower", "upper", "trim", "ltrim", "rtrim", "reverse", + "replace", "replaceall", "replaceregexpone", "replaceregexpall", + "format", "tostring", "base64encode", "base64decode", "hex", "unhex", + "extract", "extractall", "splitbychar", "splitbystring", "splitbyregexp", + "domain", "domainwithoutwww", "topleveldomain", "protocol", "path", + "cutquerystring", "cutfragment", "cutwww", "cutquerystringandfragment", + } { + schema.Funcs = append(schema.Funcs, &catalog.Function{ + Name: name, + ReturnType: stringType, + Args: []*catalog.Argument{ + { + Name: "arg", + Type: anyType, + Mode: ast.FuncParamVariadic, + }, + }, + }) + } + + // Hashing functions (returning strings usually, or numbers) + for _, name := range []string{"md5", "sha1", "sha224", "sha256", "halfmd5"} { + schema.Funcs = append(schema.Funcs, &catalog.Function{ + Name: name, + ReturnType: stringType, + Args: []*catalog.Argument{{Name: "arg", Type: anyType, Mode: ast.FuncParamVariadic}}, + }) + } + for _, name := range []string{"siphash64", "siphash128", "cityhash64", "inthash32", "inthash64", "farmhash64", "metrohash64"} { + schema.Funcs = append(schema.Funcs, &catalog.Function{ + Name: name, + ReturnType: uint64Type, // hashes are often uint64 + Args: []*catalog.Argument{{Name: "arg", Type: anyType, Mode: ast.FuncParamVariadic}}, + }) + } + + // JSON Functions + // JSONExtract is special (handled by type resolver), but we register it with anyType fallback + // JSONHas, JSONLength, JSONType, JSONExtractString, etc. + + // JSON functions returning generic/string + for _, name := range []string{"jsonextract", "jsonextractstring", "jsonextractraw", "tojsonstring"} { + schema.Funcs = append(schema.Funcs, &catalog.Function{ + Name: name, + ReturnType: stringType, // Fallback to string/text + Args: []*catalog.Argument{{Name: "arg", Type: anyType, Mode: ast.FuncParamVariadic}}, + }) + } + + // JSON functions returning bool + boolType := &ast.TypeName{Name: "bool"} + for _, name := range []string{"jsonhas", "jsonextractbool", "isvalidjson"} { + schema.Funcs = append(schema.Funcs, &catalog.Function{ + Name: name, + ReturnType: boolType, + Args: []*catalog.Argument{{Name: "arg", Type: anyType, Mode: ast.FuncParamVariadic}}, + }) + } + + // JSON functions returning int/float + for _, name := range []string{"jsonlength", "jsonextractint", "jsonextractuint"} { + schema.Funcs = append(schema.Funcs, &catalog.Function{ + Name: name, + ReturnType: int64Type, + Args: []*catalog.Argument{{Name: "arg", Type: anyType, Mode: ast.FuncParamVariadic}}, + }) + } + for _, name := range []string{"jsonextractfloat"} { + schema.Funcs = append(schema.Funcs, &catalog.Function{ + Name: name, + ReturnType: float64Type, + Args: []*catalog.Argument{{Name: "arg", Type: anyType, Mode: ast.FuncParamVariadic}}, + }) + } + + // Array Functions (generic return) + // Many array functions return arrays, which sqlc handles as generic "any" mostly unless resolved + for _, name := range []string{ + "array", "arrayconcat", "arrayslice", "arraypushback", "arraypushfront", + "arraypopback", "arraypopfront", "arrayresize", "arrayfilter", "arraymap", + "arrayreverse", "arraysort", "arraydistinct", "arrayuniq", "arrayjoin", + "arrayenumerate", "arrayenumerateuniq", "arrayflatten", "arraycompact", + "arrayzip", "arrayreduce", "arrayfold", + } { + schema.Funcs = append(schema.Funcs, &catalog.Function{ + Name: name, + ReturnType: anyType, + Args: []*catalog.Argument{{Name: "arg", Type: anyType, Mode: ast.FuncParamVariadic}}, + }) + } + // Array functions returning bool + for _, name := range []string{"has", "hasall", "hasany", "hassubstr", "arrayexists", "arrayall"} { + schema.Funcs = append(schema.Funcs, &catalog.Function{ + Name: name, + ReturnType: boolType, + Args: []*catalog.Argument{{Name: "arg", Type: anyType, Mode: ast.FuncParamVariadic}}, + }) + } + // Array functions returning int/uint + for _, name := range []string{"length", "empty", "notempty", "arraycount", "indexof"} { + schema.Funcs = append(schema.Funcs, &catalog.Function{ + Name: name, + ReturnType: uint64Type, + Args: []*catalog.Argument{{Name: "arg", Type: anyType, Mode: ast.FuncParamVariadic}}, + }) + } + + // Type Conversion + // toInt*, toUInt*, toFloat*, toDecimal* + for _, name := range []string{"toint8", "toint16", "toint32", "toint64", "toint128", "toint256"} { + // Simplified to int64 for now for sqlc mapping purposes, though could be specific + schema.Funcs = append(schema.Funcs, &catalog.Function{ + Name: name, + ReturnType: int64Type, + Args: []*catalog.Argument{{Name: "arg", Type: anyType, Mode: ast.FuncParamIn}}, + }) + } + for _, name := range []string{"touint8", "touint16", "touint32", "touint64", "touint128", "touint256"} { + schema.Funcs = append(schema.Funcs, &catalog.Function{ + Name: name, + ReturnType: uint64Type, + Args: []*catalog.Argument{{Name: "arg", Type: anyType, Mode: ast.FuncParamIn}}, + }) + } + for _, name := range []string{"tofloat32", "tofloat64"} { + schema.Funcs = append(schema.Funcs, &catalog.Function{ + Name: name, + ReturnType: float64Type, + Args: []*catalog.Argument{{Name: "arg", Type: anyType, Mode: ast.FuncParamIn}}, + }) + } + + // UUID + schema.Funcs = append(schema.Funcs, &catalog.Function{ + Name: "generateuuidv4", + ReturnType: &ast.TypeName{Name: "uuid"}, + }) + + // IP + schema.Funcs = append(schema.Funcs, &catalog.Function{ + Name: "ipv4stringtonum", + ReturnType: uint64Type, + Args: []*catalog.Argument{{Name: "arg", Type: anyType, Mode: ast.FuncParamIn}}, + }) + schema.Funcs = append(schema.Funcs, &catalog.Function{ + Name: "ipv4numtostring", + ReturnType: stringType, + Args: []*catalog.Argument{{Name: "arg", Type: anyType, Mode: ast.FuncParamIn}}, + }) + + // Functions with context-dependent return types + // These are registered with a placeholder return type and will be handled specially + // by the compiler when analyzing query output columns + + // arrayJoin(Array(T)) returns T + schema.Funcs = append(schema.Funcs, &catalog.Function{ + Name: "arrayjoin", + ReturnType: anyType, + Args: []*catalog.Argument{ + { + Name: "arr", + Type: anyType, + Mode: ast.FuncParamIn, + }, + }, + }) + + // argMin and argMax return the type of their first argument + for _, name := range []string{"argmin", "argmax"} { + schema.Funcs = append(schema.Funcs, &catalog.Function{ + Name: name, + ReturnType: anyType, + Args: []*catalog.Argument{ + {Name: "val", Type: anyType, Mode: ast.FuncParamIn}, + {Name: "arg", Type: anyType, Mode: ast.FuncParamIn}, + }, + }) + } + + // argMinIf and argMaxIf return the type of their first argument + for _, name := range []string{"argminif", "argmaxif"} { + schema.Funcs = append(schema.Funcs, &catalog.Function{ + Name: name, + ReturnType: anyType, + Args: []*catalog.Argument{ + {Name: "val", Type: anyType, Mode: ast.FuncParamIn}, + {Name: "arg", Type: anyType, Mode: ast.FuncParamIn}, + {Name: "cond", Type: anyType, Mode: ast.FuncParamIn}, + }, + }) + } + + // any, anyLast, anyHeavy return the type of their argument + for _, name := range []string{"any", "anylast", "anyheavy", "min", "max", "sum"} { + schema.Funcs = append(schema.Funcs, &catalog.Function{ + Name: name, + ReturnType: anyType, + Args: []*catalog.Argument{ + { + Name: "arg", + Type: anyType, + Mode: ast.FuncParamIn, + }, + }, + }) + } + + // anyIf, anyLastIf, anyHeavyIf, minIf, maxIf, sumIf return the type of their argument + for _, name := range []string{"anyif", "anylastif", "anyheavyif", "minif", "maxif", "sumif"} { + schema.Funcs = append(schema.Funcs, &catalog.Function{ + Name: name, + ReturnType: anyType, + Args: []*catalog.Argument{ + { + Name: "arg", + Type: anyType, + Mode: ast.FuncParamIn, + }, + { + Name: "cond", + Type: anyType, + Mode: ast.FuncParamIn, + }, + }, + }) + } +} diff --git a/internal/engine/clickhouse/catalog_test.go b/internal/engine/clickhouse/catalog_test.go new file mode 100644 index 0000000000..20191df25b --- /dev/null +++ b/internal/engine/clickhouse/catalog_test.go @@ -0,0 +1,83 @@ +package clickhouse + +import ( + "strings" + "testing" + + "github.com/sqlc-dev/sqlc/internal/sql/ast" +) + +func TestCatalogIntegration(t *testing.T) { + schema := ` +CREATE TABLE IF NOT EXISTS users +( + id UInt32, + name String, + email String +) +ENGINE = MergeTree() +ORDER BY id; +` + + p := NewParser() + stmts, err := p.Parse(strings.NewReader(schema)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(stmts) != 1 { + t.Fatalf("Expected 1 statement, got %d", len(stmts)) + } + + // Debug: check what's in the statement + if stmts[0].Raw != nil && stmts[0].Raw.Stmt != nil { + if createStmt, ok := stmts[0].Raw.Stmt.(*ast.CreateTableStmt); ok { + t.Logf("CreateTableStmt: Schema='%s', Table='%s'", createStmt.Name.Schema, createStmt.Name.Name) + t.Logf("CreateTableStmt: Cols count=%d", len(createStmt.Cols)) + } else { + t.Logf("Statement type: %T", stmts[0].Raw.Stmt) + } + } + + cat := NewCatalog() + if cat.DefaultSchema != "default" { + t.Errorf("Expected default schema 'default', got '%s'", cat.DefaultSchema) + } + + // Try to update catalog with the CREATE TABLE + t.Logf("Calling catalog.Update()...") + err = cat.Update(stmts[0], nil) + if err != nil { + t.Fatalf("Catalog update failed: %v", err) + } + t.Logf("Catalog update succeeded") + + // Check if table was added + t.Logf("Catalog has %d schemas", len(cat.Schemas)) + for i, schema := range cat.Schemas { + t.Logf("Schema[%d]: Name='%s', Tables=%d", i, schema.Name, len(schema.Tables)) + } + + if len(cat.Schemas) == 0 { + t.Fatal("No schemas in catalog") + } + + defaultSchema := cat.Schemas[0] + if len(defaultSchema.Tables) == 0 { + t.Fatal("No tables in default schema") + } + + table := defaultSchema.Tables[0] + if table.Rel.Name != "users" { + t.Errorf("Expected table name 'users', got '%s'", table.Rel.Name) + } + + if len(table.Columns) != 3 { + t.Errorf("Expected 3 columns, got %d", len(table.Columns)) + } + + // Log column types for debugging + for i, col := range table.Columns { + t.Logf("Column[%d]: Name='%s', Type.Name='%s', NotNull=%v", i, col.Name, col.Type.Name, col.IsNotNull) + } +} diff --git a/internal/engine/clickhouse/convert.go b/internal/engine/clickhouse/convert.go new file mode 100644 index 0000000000..940e084a75 --- /dev/null +++ b/internal/engine/clickhouse/convert.go @@ -0,0 +1,2430 @@ +package clickhouse + +import ( + "log" + "strconv" + "strings" + + chparser "github.com/AfterShip/clickhouse-sql-parser/parser" + + "github.com/sqlc-dev/sqlc/internal/debug" + "github.com/sqlc-dev/sqlc/internal/sql/ast" + "github.com/sqlc-dev/sqlc/internal/sql/catalog" +) + +type cc struct { + paramCount int + subqueryCount int + catalog interface{} // *catalog.Catalog - using interface{} to avoid circular imports +} + +func todo(n chparser.Expr) *ast.TODO { + if debug.Active { + log.Printf("clickhouse.convert: Unsupported AST node type %T\n", n) + log.Printf("clickhouse.convert: This node type may not be fully supported yet. Consider using different query syntax or filing an issue.\n") + } + return &ast.TODO{} +} + +// identifier preserves the case of identifiers as ClickHouse is case-sensitive +// for table, column, and schema names +func identifier(id string) string { + return id +} + +// normalizeFunctionName normalizes function names to lowercase for comparison +// ClickHouse function names are case-insensitive, so we normalize them for lookups +func normalizeFunctionName(name string) string { + return strings.ToLower(name) +} + +func NewIdentifier(t string) *ast.String { + return &ast.String{Str: identifier(t)} +} + +// getCatalog safely casts the interface{} catalog to a *catalog.Catalog +func (c *cc) getCatalog() *catalog.Catalog { + if c.catalog == nil { + return nil + } + cat, ok := c.catalog.(*catalog.Catalog) + if !ok { + return nil + } + return cat +} + +// registerFunctionInCatalog registers or updates a function in the catalog with the given return type +func (c *cc) registerFunctionInCatalog(funcName string, returnType *ast.TypeName) { + cat := c.getCatalog() + if cat == nil { + return + } + + // Find the default schema + var schema *catalog.Schema + for _, s := range cat.Schemas { + if s.Name == cat.DefaultSchema { + schema = s + break + } + } + if schema == nil { + return + } + + // Check if function already exists + for i, f := range schema.Funcs { + if strings.ToLower(f.Name) == strings.ToLower(funcName) { + // Update existing function + schema.Funcs[i].ReturnType = returnType + return + } + } + + // Add new function + schema.Funcs = append(schema.Funcs, &catalog.Function{ + Name: strings.ToLower(funcName), + ReturnType: returnType, + }) +} + +// findColumnTypeInCatalog searches all tables in the catalog for a column and returns its type +// This is used for unqualified column references (no table prefix) +// Returns the type of the first matching column found, or empty string if not found +// If multiple tables have the same column name, this is ambiguous but we return the first match +// (relying on the query to be syntactically valid from ClickHouse's perspective) +// Column names are case-sensitive in ClickHouse +func (c *cc) findColumnTypeInCatalog(columnName string) string { + cat := c.getCatalog() + if cat == nil { + return "" + } + + // Search all schemas + for _, schema := range cat.Schemas { + if schema == nil || schema.Tables == nil { + continue + } + // Search all tables in this schema + for _, table := range schema.Tables { + if table == nil || table.Columns == nil { + continue + } + // Search all columns in this table + // Column names are case-sensitive + for _, col := range table.Columns { + if col.Name == columnName { + return col.Type.Name + } + } + } + } + + return "" +} + +// extractTypeFromColumnRef extracts the type of a column reference from the catalog +// Returns empty string if the column cannot be resolved +// Column names are case-sensitive in ClickHouse +func (c *cc) extractTypeFromColumnRef(colRef *ast.ColumnRef) string { + if colRef == nil || colRef.Fields == nil || len(colRef.Fields.Items) == 0 { + return "" + } + + cat := c.getCatalog() + if cat == nil { + return "" + } + + // Extract the parts of the column reference + var parts []string + for _, item := range colRef.Fields.Items { + if s, ok := item.(*ast.String); ok { + parts = append(parts, s.Str) + } + } + + if len(parts) == 0 { + return "" + } + + // Try to resolve: table.column or just column + var tableName, columnName string + if len(parts) == 2 { + tableName = parts[0] + columnName = parts[1] + } else if len(parts) == 1 { + columnName = parts[0] + // Unqualified column - search all tables in all schemas + return c.findColumnTypeInCatalog(columnName) + } else { + return "" + } + + // Qualified table.column - look up the specific table + tableRef := &ast.TableName{Name: tableName} + table, err := cat.GetTable(tableRef) + if err != nil { + return "" + } + + // Find the column - case-sensitive comparison + for _, col := range table.Columns { + if col.Name == columnName { + return col.Type.Name + } + } + + return "" +} + +// convert converts a ClickHouse AST node to a sqlc AST node +func (c *cc) convert(node chparser.Expr) ast.Node { + if node == nil { + return nil + } + + switch n := node.(type) { + case *chparser.SelectQuery: + result := c.convertSelectQuery(n) + if debug.Active { + if stmt, ok := result.(*ast.SelectStmt); ok && stmt != nil && stmt.TargetList != nil { + isUnion := len(stmt.TargetList.Items) == 0 && stmt.Larg != nil + log.Printf("[DEBUG] clickhouse.convert: SelectQuery converted, isUnion=%v, targets=%d", isUnion, len(stmt.TargetList.Items)) + } + } + return result + case *chparser.InsertStmt: + return c.convertInsertStmt(n) + case *chparser.AlterTable: + return c.convertAlterTable(n) + case *chparser.CreateTable: + return c.convertCreateTable(n) + case *chparser.CreateDatabase: + return c.convertCreateDatabase(n) + case *chparser.CreateView: + return c.convertCreateView(n) + case *chparser.CreateMaterializedView: + return c.convertCreateMaterializedView(n) + case *chparser.DropStmt: + return c.convertDropStmt(n) + case *chparser.OptimizeStmt: + return c.convertOptimizeStmt(n) + case *chparser.DescribeStmt: + return c.convertDescribeStmt(n) + case *chparser.ExplainStmt: + return c.convertExplainStmt(n) + case *chparser.ShowStmt: + return c.convertShowStmt(n) + case *chparser.TruncateTable: + return c.convertTruncateTable(n) + + // Expression nodes + case *chparser.Ident: + return c.convertIdent(n) + case *chparser.Path: + return c.convertPath(n) + case *chparser.ColumnExpr: + return c.convertColumnExpr(n) + case *chparser.FunctionExpr: + return c.convertFunctionExpr(n) + case *chparser.BinaryOperation: + return c.convertBinaryOperation(n) + case *chparser.NumberLiteral: + return c.convertNumberLiteral(n) + case *chparser.StringLiteral: + return c.convertStringLiteral(n) + case *chparser.QueryParam: + return c.convertQueryParam(n) + case *chparser.NestedIdentifier: + return c.convertNestedIdentifier(n) + case *chparser.OrderExpr: + return c.convertOrderExpr(n) + case *chparser.PlaceHolder: + return c.convertPlaceHolder(n) + case *chparser.JoinTableExpr: + return c.convertJoinTableExpr(n) + + // Additional expression nodes + case *chparser.CastExpr: + return c.convertCastExpr(n) + case *chparser.CaseExpr: + return c.convertCaseExpr(n) + case *chparser.WindowFunctionExpr: + return c.convertWindowFunctionExpr(n) + case *chparser.IsNullExpr: + return c.convertIsNullExpr(n) + case *chparser.IsNotNullExpr: + return c.convertIsNotNullExpr(n) + case *chparser.UnaryExpr: + return c.convertUnaryExpr(n) + case *chparser.MapLiteral: + return c.convertMapLiteral(n) + case *chparser.ParamExprList: + return c.convertParamExprList(n) + case *chparser.IndexOperation: + return c.convertIndexOperation(n) + case *chparser.ArrayParamList: + return c.convertArrayParamList(n) + case *chparser.TableFunctionExpr: + return c.convertTableFunctionExpr(n) + case *chparser.TernaryOperation: + return c.convertTernaryOperation(n) + + case *chparser.UsingClause: + return c.convertUsingClause(n) + + default: + // Return TODO for unsupported node types + return todo(n) + } +} + +func (c *cc) convertSelectQuery(stmt *chparser.SelectQuery) ast.Node { + selectStmt := &ast.SelectStmt{ + TargetList: c.convertSelectItems(stmt.SelectItems), + FromClause: c.convertFromClause(stmt.From), + WhereClause: c.convertWhereClause(stmt.Where), + GroupClause: c.convertGroupByClause(stmt.GroupBy), + HavingClause: c.convertHavingClause(stmt.Having), + SortClause: c.convertOrderByClause(stmt.OrderBy), + WithClause: c.convertWithClause(stmt.With), + } + + // Handle ARRAY JOIN by integrating it into the FROM clause + if stmt.ArrayJoin != nil { + selectStmt.FromClause = c.mergeArrayJoinIntoFrom(selectStmt.FromClause, stmt.ArrayJoin) + } + + // Handle DISTINCT + if stmt.HasDistinct { + selectStmt.DistinctClause = &ast.List{Items: []ast.Node{}} + } + + // Handle LIMIT + if stmt.Limit != nil { + selectStmt.LimitCount = c.convertLimitClause(stmt.Limit) + if stmt.Limit.Offset != nil { + selectStmt.LimitOffset = c.convert(stmt.Limit.Offset) + } + } + + // Handle UNION/EXCEPT + if stmt.UnionAll != nil || stmt.UnionDistinct != nil || stmt.Except != nil { + // For UNION/EXCEPT queries, create a wrapper SelectStmt with no targets + // The Larg points to the left SELECT, Rarg points to the right SELECT + wrapperStmt := &ast.SelectStmt{ + TargetList: &ast.List{}, // Empty list, not nil + FromClause: &ast.List{}, // Empty list, not nil + } + + // Set the left SELECT (current selectStmt with all its targets and clauses) + wrapperStmt.Larg = selectStmt + + // Determine the operation and set the right SELECT + if stmt.UnionAll != nil { + wrapperStmt.Op = ast.Union + wrapperStmt.All = true + wrapperStmt.Rarg = c.convertSelectQuery(stmt.UnionAll).(*ast.SelectStmt) + } else if stmt.UnionDistinct != nil { + wrapperStmt.Op = ast.Union + wrapperStmt.All = false + wrapperStmt.Rarg = c.convertSelectQuery(stmt.UnionDistinct).(*ast.SelectStmt) + } else if stmt.Except != nil { + wrapperStmt.Op = ast.Except + wrapperStmt.Rarg = c.convertSelectQuery(stmt.Except).(*ast.SelectStmt) + } + + return wrapperStmt + } + + return selectStmt +} + +func (c *cc) convertSelectItems(items []*chparser.SelectItem) *ast.List { + list := &ast.List{Items: []ast.Node{}} + for _, item := range items { + list.Items = append(list.Items, c.convertSelectItem(item)) + } + return list +} + +func (c *cc) convertSelectItem(item *chparser.SelectItem) *ast.ResTarget { + var name *string + if item.Alias != nil { + aliasName := identifier(item.Alias.Name) + name = &aliasName + } else { + // If no explicit alias, try to extract a default name from the expression + // For Path expressions like u.id, use the last part as the name + if path, ok := item.Expr.(*chparser.Path); ok && path != nil && len(path.Fields) > 0 { + lastName := identifier(path.Fields[len(path.Fields)-1].Name) + name = &lastName + } + } + + return &ast.ResTarget{ + Name: name, + Val: c.convert(item.Expr), + Location: int(item.Pos()), + } +} + +func (c *cc) convertFromClause(from *chparser.FromClause) *ast.List { + if from == nil { + return &ast.List{} + } + + list := &ast.List{Items: []ast.Node{}} + + // From.Expr can be a TableExpr, JoinExpr, or other expression + if from.Expr != nil { + list.Items = append(list.Items, c.convertFromExpr(from.Expr)) + } + + return list +} + +func (c *cc) convertFromExpr(expr chparser.Expr) ast.Node { + if expr == nil { + return &ast.TODO{} + } + + switch e := expr.(type) { + case *chparser.TableExpr: + return c.convertTableExpr(e) + case *chparser.JoinTableExpr: + // JoinTableExpr wraps a table with optional FINAL and SAMPLE clauses + // The Table field contains the actual table reference + if e.Table != nil { + return c.convertTableExpr(e.Table) + } + return &ast.TODO{} + case *chparser.JoinExpr: + return c.convertJoinExpr(e) + default: + return c.convert(expr) + } +} + +func (c *cc) convertTableExpr(expr *chparser.TableExpr) ast.Node { + if expr == nil { + return &ast.TODO{} + } + + if debug.Active { + log.Printf("[DEBUG] convertTableExpr called with expr type: %T", expr.Expr) + } + + // The Expr field contains the actual table reference + var baseNode ast.Node + var alias *string + + // Handle AliasExpr which wraps the actual table reference with an alias + exprToProcess := expr.Expr + if aliasExpr, ok := expr.Expr.(*chparser.AliasExpr); ok { + // Extract the alias name + if aliasExpr.Alias != nil { + if aliasIdent, ok := aliasExpr.Alias.(*chparser.Ident); ok { + aliasName := identifier(aliasIdent.Name) + alias = &aliasName + } + } + // Process the underlying expression + exprToProcess = aliasExpr.Expr + } + + if tableIdent, ok := exprToProcess.(*chparser.TableIdentifier); ok { + baseNode = c.convertTableIdentifier(tableIdent) + // Apply alias if we found one + if alias != nil { + if rangeVar, ok := baseNode.(*ast.RangeVar); ok { + rangeVar.Alias = &ast.Alias{ + Aliasname: alias, + } + } + } + } else if selectQuery, ok := exprToProcess.(*chparser.SelectQuery); ok { + // Subquery (SelectQuery) + convertedSubquery := c.convert(selectQuery) + if debug.Active { + if stmt, ok := convertedSubquery.(*ast.SelectStmt); ok && stmt != nil && stmt.TargetList != nil { + isUnion := len(stmt.TargetList.Items) == 0 && stmt.Larg != nil + log.Printf("[DEBUG] convertTableExpr: SelectQuery converted, isUnion=%v, targets=%d", isUnion, len(stmt.TargetList.Items)) + } + } + rangeSubselect := &ast.RangeSubselect{ + Subquery: convertedSubquery, + } + if alias != nil { + rangeSubselect.Alias = &ast.Alias{ + Aliasname: alias, + } + } else if expr.Alias != nil { + if aliasIdent, ok := expr.Alias.Alias.(*chparser.Ident); ok { + rangeSubselect.Alias = &ast.Alias{ + Aliasname: &aliasIdent.Name, + } + } + } else { + // Generate a synthetic alias for subqueries without explicit aliases + // This is necessary for the compiler to resolve columns from the subquery + c.subqueryCount++ + syntheticAlias := "sq_" + strconv.Itoa(c.subqueryCount) + // IMPORTANT: Copy the string to ensure the pointer persists + aliasCopy := syntheticAlias + rangeSubselect.Alias = &ast.Alias{ + Aliasname: &aliasCopy, + } + } + return rangeSubselect + } else if subQuery, ok := exprToProcess.(*chparser.SubQuery); ok { + // Subquery (SubQuery with Select field) + if subQuery.Select == nil { + return &ast.TODO{} + } + convertedSubquery := c.convert(subQuery.Select) + if debug.Active { + if stmt, ok := convertedSubquery.(*ast.SelectStmt); ok && stmt != nil && stmt.TargetList != nil { + isUnion := len(stmt.TargetList.Items) == 0 && stmt.Larg != nil + log.Printf("[DEBUG] convertTableExpr: SubQuery.Select converted, isUnion=%v, targets=%d", isUnion, len(stmt.TargetList.Items)) + } + } + rangeSubselect := &ast.RangeSubselect{ + Subquery: convertedSubquery, + } + if alias != nil { + rangeSubselect.Alias = &ast.Alias{ + Aliasname: alias, + } + } else if expr.Alias != nil { + if aliasIdent, ok := expr.Alias.Alias.(*chparser.Ident); ok { + rangeSubselect.Alias = &ast.Alias{ + Aliasname: &aliasIdent.Name, + } + } + } else { + // Generate a synthetic alias for subqueries without explicit aliases + // This is necessary for the compiler to resolve columns from the subquery + c.subqueryCount++ + syntheticAlias := "sq_" + strconv.Itoa(c.subqueryCount) + // IMPORTANT: Copy the string to ensure the pointer persists + aliasCopy := syntheticAlias + rangeSubselect.Alias = &ast.Alias{ + Aliasname: &aliasCopy, + } + } + return rangeSubselect + } else { + baseNode = c.convert(exprToProcess) + } + + return baseNode +} + +func (c *cc) convertTableIdentifier(ident *chparser.TableIdentifier) *ast.RangeVar { + var schema *string + var table *string + + if ident.Database != nil { + dbName := identifier(ident.Database.Name) + schema = &dbName + } + + if ident.Table != nil { + tableName := identifier(ident.Table.Name) + table = &tableName + } + + rangeVar := &ast.RangeVar{ + Schemaname: schema, + Relname: table, + Inh: true, + Location: int(ident.Pos()), + } + + return rangeVar +} + +func (c *cc) convertJoinExpr(join *chparser.JoinExpr) ast.Node { + // JoinExpr represents JOIN operations + // Left and Right are the expressions being joined + // Modifiers contains things like "LEFT", "RIGHT", "INNER", etc. + // Constraints contains either an ON clause expression or a USING clause + // + // Note: ClickHouse's parser sometimes creates nested JoinExpr structures: + // JoinExpr{Left: table1, Right: JoinExpr{Left: table2, Right: nil, Constraints: USING}} + // We normalize this to match PostgreSQL's flat structure during conversion. + + // Check if Right is a nested JoinExpr with USING clause and no Right itself + // This is a ClickHouse-specific pattern that we flatten to PostgreSQL-style + var rarg chparser.Expr = join.Right + var constraints chparser.Expr = join.Constraints + + if nestedJoin, ok := join.Right.(*chparser.JoinExpr); ok { + // If the nested join has no Right child and has USING constraints on it, + // we're looking at a ClickHouse nested structure that should be flattened + if nestedJoin.Right == nil && nestedJoin.Constraints != nil { + // Pull the table from the nested join's Left + rarg = nestedJoin.Left + // Pull the constraints from the nested join + constraints = nestedJoin.Constraints + + // Copy modifiers from nested join if the top-level has none + if len(join.Modifiers) == 0 && len(nestedJoin.Modifiers) > 0 { + join.Modifiers = nestedJoin.Modifiers + } + } + } + + joinNode := &ast.JoinExpr{ + Larg: c.convertFromExpr(join.Left), + Rarg: c.convertFromExpr(rarg), + } + + // Determine join type from modifiers + joinType := "JOIN" + for _, mod := range join.Modifiers { + modUpper := strings.ToUpper(mod) + if modUpper == "LEFT" || modUpper == "RIGHT" || modUpper == "FULL" || modUpper == "INNER" { + joinType = modUpper + " " + joinType + } + } + joinNode.Jointype = c.parseJoinType(joinType) + + // Handle constraints: either ON clause or USING clause + if constraints != nil { + // Check if this is a USING clause + if usingClause, ok := constraints.(*chparser.UsingClause); ok { + // Convert USING clause to ast.JoinExpr.UsingClause + joinNode.UsingClause = c.convertUsingClauseToList(usingClause) + } else { + // Handle ON clause (regular expression) + joinNode.Quals = c.convert(constraints) + } + } + + return joinNode +} + +func (c *cc) parseJoinType(joinType string) ast.JoinType { + upperType := strings.ToUpper(joinType) + switch { + case strings.Contains(upperType, "LEFT"): + return ast.JoinTypeLeft + case strings.Contains(upperType, "RIGHT"): + return ast.JoinTypeRight + case strings.Contains(upperType, "FULL"): + return ast.JoinTypeFull + case strings.Contains(upperType, "INNER"): + return ast.JoinTypeInner + default: + return ast.JoinTypeInner + } +} + +// convertUsingClause converts a ClickHouse UsingClause to an ast.List of String nodes +// This creates a representation compatible with PostgreSQL-style USING clauses +func (c *cc) convertUsingClause(using *chparser.UsingClause) ast.Node { + if using == nil || using.Using == nil { + return nil + } + return c.convertUsingClauseToList(using) +} + +// convertUsingClauseToList converts a ClickHouse UsingClause to an ast.List of String nodes +// representing the column names in the USING clause +func (c *cc) convertUsingClauseToList(using *chparser.UsingClause) *ast.List { + if using == nil || using.Using == nil || len(using.Using.Items) == 0 { + return nil + } + + list := &ast.List{Items: []ast.Node{}} + for _, item := range using.Using.Items { + // Each item should be a ColumnExpr wrapping an Ident + colExpr, ok := item.(*chparser.ColumnExpr) + if !ok { + continue + } + + // Get the column name from the ColumnExpr + if ident, ok := colExpr.Expr.(*chparser.Ident); ok { + colName := identifier(ident.Name) + list.Items = append(list.Items, &ast.String{Str: colName}) + } + } + + return list +} + +func (c *cc) convertWhereClause(where *chparser.WhereClause) ast.Node { + if where == nil { + return nil + } + return c.convert(where.Expr) +} + +func (c *cc) convertGroupByClause(groupBy *chparser.GroupByClause) *ast.List { + if groupBy == nil { + return &ast.List{} + } + + list := &ast.List{Items: []ast.Node{}} + // GroupBy.Expr is a single expression which might be a comma-separated list + if groupBy.Expr != nil { + // Just convert the expression as-is + // The parser should handle comma-separated lists internally + list.Items = append(list.Items, c.convert(groupBy.Expr)) + } + return list +} + +func (c *cc) convertHavingClause(having *chparser.HavingClause) ast.Node { + if having == nil { + return nil + } + return c.convert(having.Expr) +} + +func (c *cc) convertOrderByClause(orderBy *chparser.OrderByClause) *ast.List { + if orderBy == nil { + return &ast.List{} + } + + list := &ast.List{Items: []ast.Node{}} + + // OrderBy.Items is a slice of Expr + // For now, just convert each item directly + for _, item := range orderBy.Items { + list.Items = append(list.Items, c.convert(item)) + } + + return list +} + +func (c *cc) convertLimitClause(limit *chparser.LimitClause) ast.Node { + if limit == nil || limit.Limit == nil { + return nil + } + return c.convert(limit.Limit) +} + +func (c *cc) convertWithClause(with *chparser.WithClause) *ast.WithClause { + if with == nil { + return nil + } + + list := &ast.List{Items: []ast.Node{}} + for _, cte := range with.CTEs { + list.Items = append(list.Items, c.convertCTE(cte)) + } + + return &ast.WithClause{ + Ctes: list, + Location: int(with.Pos()), + } +} + +func (c *cc) convertCTE(cte *chparser.CTEStmt) *ast.CommonTableExpr { + if cte == nil { + return nil + } + + // Extract CTE name from Expr (should be an Ident) + var cteName *string + if ident, ok := cte.Expr.(*chparser.Ident); ok { + name := identifier(ident.Name) + cteName = &name + } + + return &ast.CommonTableExpr{ + Ctename: cteName, + Ctequery: c.convert(cte.Alias), + Location: int(cte.Pos()), + } +} + +func (c *cc) convertInsertStmt(stmt *chparser.InsertStmt) ast.Node { + insert := &ast.InsertStmt{ + Relation: c.convertTableExprToRangeVar(stmt.Table), + Cols: c.convertColumnNames(stmt.ColumnNames), + ReturningList: &ast.List{}, + } + + // Handle VALUES + if len(stmt.Values) > 0 { + insert.SelectStmt = &ast.SelectStmt{ + FromClause: &ast.List{}, + TargetList: &ast.List{}, + ValuesLists: c.convertValues(stmt.Values), + } + } + + // Handle INSERT INTO ... SELECT + if stmt.SelectExpr != nil { + insert.SelectStmt = c.convert(stmt.SelectExpr) + } + + return insert +} + +func (c *cc) convertTableExprToRangeVar(expr chparser.Expr) *ast.RangeVar { + if tableIdent, ok := expr.(*chparser.TableIdentifier); ok { + return c.convertTableIdentifier(tableIdent) + } + if ident, ok := expr.(*chparser.Ident); ok { + name := identifier(ident.Name) + return &ast.RangeVar{ + Relname: &name, + Location: int(ident.Pos()), + } + } + return &ast.RangeVar{} +} + +func (c *cc) convertColumnNames(colNames *chparser.ColumnNamesExpr) *ast.List { + if colNames == nil { + return &ast.List{} + } + + list := &ast.List{Items: []ast.Node{}} + for _, col := range colNames.ColumnNames { + // ColumnNames contains NestedIdentifier which has pointers + // Convert to ResTarget with ColumnRef so the compiler can resolve types properly + var colName string + if col.Ident != nil { + colName = identifier(col.Ident.Name) + } else if col.DotIdent != nil { + colName = identifier(col.DotIdent.Name) + } + + if colName != "" { + // Create a ResTarget with a ColumnRef that the compiler can resolve + // This allows type inference to work properly for INSERT parameters + resTarget := &ast.ResTarget{ + Name: &colName, + Val: &ast.ColumnRef{ + Fields: &ast.List{ + Items: []ast.Node{ + &ast.String{Str: colName}, + }, + }, + }, + } + list.Items = append(list.Items, resTarget) + } + } + return list +} + +func (c *cc) convertValues(values []*chparser.AssignmentValues) *ast.List { + list := &ast.List{Items: []ast.Node{}} + for _, valueSet := range values { + inner := &ast.List{Items: []ast.Node{}} + for _, val := range valueSet.Values { + inner.Items = append(inner.Items, c.convert(val)) + } + list.Items = append(list.Items, inner) + } + return list +} + +func (c *cc) convertCreateTable(stmt *chparser.CreateTable) ast.Node { + if stmt == nil { + return &ast.TODO{} + } + + // Extract table name + var schema *string + var table *string + if stmt.Name != nil { + if stmt.Name.Database != nil { + dbName := identifier(stmt.Name.Database.Name) + schema = &dbName + } + if stmt.Name.Table != nil { + tableName := identifier(stmt.Name.Table.Name) + table = &tableName + } + } + + // If no schema/database specified, the table name might be in Name.Table or Name.Database + // In ClickHouse parser, a simple "users" goes into Database field, not Table + if table == nil && stmt.Name != nil && stmt.Name.Database != nil { + tableName := identifier(stmt.Name.Database.Name) + table = &tableName + schema = nil // No schema specified, will use default + } + + // Build TableName for CreateTableStmt + tableName := &ast.TableName{} + if schema != nil { + tableName.Schema = *schema + } + if table != nil { + tableName.Name = *table + } + + createStmt := &ast.CreateTableStmt{ + Name: tableName, + IfNotExists: stmt.IfNotExists, + } + + // Convert columns from TableSchema + if stmt.TableSchema != nil && len(stmt.TableSchema.Columns) > 0 { + cols := []*ast.ColumnDef{} + for _, col := range stmt.TableSchema.Columns { + if colDef, ok := col.(*chparser.ColumnDef); ok { + if converted, ok := c.convertColumnDef(colDef).(*ast.ColumnDef); ok { + cols = append(cols, converted) + } + } + } + createStmt.Cols = cols + } + + // Note: ClickHouse-specific features like ENGINE, ORDER BY, PARTITION BY, and SETTINGS + // are not stored in sqlc's CreateTableStmt as it's designed for PostgreSQL compatibility. + // These features are parsed but not preserved in the AST for now. + // In a full ClickHouse implementation, we might extend CreateTableStmt or create + // ClickHouse-specific statement types. + + return createStmt +} + +func (c *cc) convertCreateDatabase(stmt *chparser.CreateDatabase) ast.Node { + if stmt == nil { + return &ast.TODO{} + } + + var schemaName string + if stmt.Name != nil { + // Name is usually an Ident + if ident, ok := stmt.Name.(*chparser.Ident); ok { + schemaName = identifier(ident.Name) + } + } + + return &ast.CreateSchemaStmt{ + Name: &schemaName, + IfNotExists: stmt.IfNotExists, + } +} + +func (c *cc) convertDropStmt(stmt *chparser.DropStmt) ast.Node { + if stmt == nil { + return &ast.TODO{} + } + + // ClickHouse DROP statements are mostly structural (DROP TABLE, DROP DATABASE) + // sqlc doesn't have a dedicated DropStmt, so return TODO + // This is expected - DROP is a DDL statement not typically used in application queries + return &ast.TODO{} +} + +func (c *cc) convertAlterTable(stmt *chparser.AlterTable) ast.Node { + if stmt == nil { + return &ast.TODO{} + } + + // ClickHouse uses ALTER TABLE for modifications that would be UPDATE/DELETE in other DBs + // sqlc doesn't have dedicated support for ALTER TABLE modifications + // This is expected - ALTER TABLE is DDL, not typically used in application queries + return &ast.TODO{} +} + +func (c *cc) convertOptimizeStmt(stmt *chparser.OptimizeStmt) ast.Node { + if stmt == nil { + return &ast.TODO{} + } + + // OPTIMIZE is a ClickHouse-specific statement for maintenance + // Not a query statement that generates application code + return &ast.TODO{} +} + +func (c *cc) convertDescribeStmt(stmt *chparser.DescribeStmt) ast.Node { + if stmt == nil { + return &ast.TODO{} + } + + // DESCRIBE/DESC is a metadata query - useful for introspection but not + // typically used in application code generation workflows + return &ast.TODO{} +} + +func (c *cc) convertExplainStmt(stmt *chparser.ExplainStmt) ast.Node { + if stmt == nil { + return &ast.TODO{} + } + + // EXPLAIN is for query analysis, not application code + return &ast.TODO{} +} + +func (c *cc) convertShowStmt(stmt *chparser.ShowStmt) ast.Node { + if stmt == nil { + return &ast.TODO{} + } + + // SHOW is an introspection statement for metadata queries + // While it returns result sets, it's not typically code-generated + // Treating as TODO for now as it's not a primary use case + return &ast.TODO{} +} + +func (c *cc) convertTruncateTable(stmt *chparser.TruncateTable) ast.Node { + if stmt == nil { + return &ast.TODO{} + } + + // TRUNCATE is a DDL statement for deleting all rows from a table + // While executable, it's not typically generated as application code + // Treating as TODO for now as it's a maintenance operation + return &ast.TODO{} +} + +func (c *cc) convertIdent(id *chparser.Ident) ast.Node { + // Convert identifier to a ColumnRef (represents a column reference) + // An identifier in a SELECT or WHERE clause refers to a column, not a string literal + identName := identifier(id.Name) + + // Special case: * is represented as A_Star in sqlc AST + if identName == "*" { + return &ast.ColumnRef{ + Fields: &ast.List{ + Items: []ast.Node{ + &ast.A_Star{}, + }, + }, + Location: int(id.Pos()), + } + } + + return &ast.ColumnRef{ + Fields: &ast.List{ + Items: []ast.Node{ + &ast.String{Str: identName}, + }, + }, + Location: int(id.Pos()), + } +} + +func (c *cc) convertPath(path *chparser.Path) ast.Node { + // Path represents a qualified identifier like "table.column" or "schema.table" + // Convert it to a ColumnRef with multiple fields + if path == nil || len(path.Fields) == 0 { + return &ast.TODO{} + } + + fields := &ast.List{Items: []ast.Node{}} + for _, field := range path.Fields { + if field != nil { + fieldName := identifier(field.Name) + if fieldName == "*" { + fields.Items = append(fields.Items, &ast.A_Star{}) + } else { + fields.Items = append(fields.Items, &ast.String{Str: fieldName}) + } + } + } + + return &ast.ColumnRef{ + Fields: fields, + Location: int(path.Pos()), + } +} + +func (c *cc) convertColumnExpr(col *chparser.ColumnExpr) ast.Node { + // ColumnExpr wraps an expression (could be Ident, NestedIdentifier, etc.) + // Just convert the underlying expression + return c.convert(col.Expr) +} + +func (c *cc) convertFunctionExpr(fn *chparser.FunctionExpr) ast.Node { + // Convert function calls like COUNT(*), SUM(column), etc. + // Normalize function names to lowercase since ClickHouse function names are case-insensitive + originalFuncName := identifier(fn.Name.Name) + funcNameLower := normalizeFunctionName(originalFuncName) + + // Handle sqlc_* functions (converted from sqlc.* during preprocessing) + // Normalize back to sqlc.* schema.function format for proper AST representation + var schema string + var baseFuncName string + + if strings.HasPrefix(funcNameLower, "sqlc_") { + schema = "sqlc" + baseFuncName = strings.TrimPrefix(funcNameLower, "sqlc_") + } else { + baseFuncName = funcNameLower + } + + args := &ast.List{Items: []ast.Node{}} + var chArgs []*chparser.Expr // Keep original ClickHouse args for type analysis + if fn.Params != nil { + if fn.Params.Items != nil { + for _, item := range fn.Params.Items.Items { + chArgs = append(chArgs, &item) + args.Items = append(args.Items, c.convert(item)) + } + } + } + + // Handle special context-dependent ClickHouse functions + // For these functions, try to register them with the correct return type + c.handleSpecialFunctionTypes(funcNameLower, fn, chArgs) + + return &ast.FuncCall{ + Func: &ast.FuncName{ + Schema: schema, + Name: baseFuncName, + }, + Funcname: &ast.List{ + Items: []ast.Node{ + &ast.String{Str: funcNameLower}, + }, + }, + Args: args, + Location: int(fn.Pos()), + } +} + +// handleSpecialFunctionTypes handles ClickHouse functions with context-dependent return types +// funcName should already be normalized to lowercase via normalizeFunctionName +func (c *cc) handleSpecialFunctionTypes(funcName string, fn *chparser.FunctionExpr, chArgs []*chparser.Expr) { + switch funcName { + case "arrayjoin": + // arrayJoin(Array(T)) returns T (the element type) + if len(chArgs) > 0 { + // Try to extract element type from the array argument + elemType := c.extractArrayElementType(*chArgs[0]) + if elemType != "" { + c.registerFunctionInCatalog("arrayjoin", &ast.TypeName{Name: elemType}) + } + } + + case "argmin", "argmax": + // argMin/argMax return the type of their first argument + if len(chArgs) > 0 { + // Try to extract type from the first argument (the value being tracked) + argType := c.extractTypeFromChExpr(*chArgs[0]) + if argType != "" { + c.registerFunctionInCatalog(funcName, &ast.TypeName{Name: argType}) + } + } + } +} + +// extractTypeFromChExpr extracts a type from a ClickHouse expression +func (c *cc) extractTypeFromChExpr(expr chparser.Expr) string { + if expr == nil { + return "" + } + + switch e := expr.(type) { + case *chparser.ColumnExpr: + // ColumnExpr wraps another expression - extract from the inner expression + if e.Expr != nil { + return c.extractTypeFromChExpr(e.Expr) + } + + case *chparser.Path: + // Path like u.id + if len(e.Fields) >= 2 { + colRef := &ast.ColumnRef{ + Fields: &ast.List{ + Items: []ast.Node{ + &ast.String{Str: identifier(e.Fields[0].Name)}, + &ast.String{Str: identifier(e.Fields[1].Name)}, + }, + }, + } + return c.extractTypeFromColumnRef(colRef) + } else if len(e.Fields) == 1 { + // Single field - just the column name + colRef := &ast.ColumnRef{ + Fields: &ast.List{ + Items: []ast.Node{ + &ast.String{Str: identifier(e.Fields[0].Name)}, + }, + }, + } + return c.extractTypeFromColumnRef(colRef) + } + + case *chparser.Ident: + // Just an identifier + colRef := &ast.ColumnRef{ + Fields: &ast.List{ + Items: []ast.Node{ + &ast.String{Str: identifier(e.Name)}, + }, + }, + } + return c.extractTypeFromColumnRef(colRef) + + case *chparser.FunctionExpr: + // Handle function calls like Array(String), CAST(x AS String), etc. + return c.extractTypeFromFunctionCall(e) + + case *chparser.CastExpr: + // Handle CAST(expr AS Type) + if e.AsType != nil { + // AsType can be a StringLiteral or ColumnType + if stringLit, ok := e.AsType.(*chparser.StringLiteral); ok { + return mapClickHouseType(strings.ToLower(stringLit.Literal)) + } else if colType, ok := e.AsType.(chparser.ColumnType); ok { + return mapClickHouseType(strings.ToLower(colType.Type())) + } + } + + case *chparser.BinaryOperation: + // For :: operator (PostgreSQL-style cast), extract from right side + if string(e.Operation) == "::" { + if ident, ok := e.RightExpr.(*chparser.Ident); ok { + return mapClickHouseType(identifier(ident.Name)) + } + } + } + + return "" +} + +// extractTypeFromFunctionCall extracts the return type from a function call expression +// Handles Array(ElementType), CAST(expr AS Type), and other function patterns +func (c *cc) extractTypeFromFunctionCall(fn *chparser.FunctionExpr) string { + if fn == nil || fn.Name == nil { + return "" + } + + funcName := strings.ToLower(identifier(fn.Name.Name)) + + // Handle Array(ElementType) - returns Array of that element type + if funcName == "array" { + if fn.Params != nil && fn.Params.Items != nil && len(fn.Params.Items.Items) > 0 { + // Get the first parameter which should be the element type + elemType := c.extractTypeFromChExpr(fn.Params.Items.Items[0]) + if elemType != "" { + // Return as array type + return elemType + "[]" + } + } + } + + // Handle CAST(expr AS Type) or similar casting functions + if strings.Contains(funcName, "cast") { + if fn.Params != nil && fn.Params.Items != nil && len(fn.Params.Items.Items) > 0 { + // Last parameter might be the type, but this is complex + // Return empty to be safe + } + } + + return "" +} + +// extractArrayElementType extracts the element type from an array type or array expression +// For Array(T), returns T. For columns of Array(T) type, returns T. +func (c *cc) extractArrayElementType(expr chparser.Expr) string { + if expr == nil { + return "" + } + + // Use the general type extractor which now handles all these cases + colType := c.extractTypeFromChExpr(expr) + if colType != "" { + // If it's an array type, extract the element type + if strings.HasSuffix(colType, "[]") { + return strings.TrimSuffix(colType, "[]") + } + // Otherwise return as-is (might not be an array, but the caller will handle it) + return colType + } + + return "" +} + +func (c *cc) convertBinaryOperation(op *chparser.BinaryOperation) ast.Node { + // Special handling for :: (type cast) operator (PostgreSQL-style ClickHouse casting) + if string(op.Operation) == "::" { + // Extract the type from the right side + var typeName *ast.TypeName + if ident, ok := op.RightExpr.(*chparser.Ident); ok { + // The right side should be an identifier representing the type + typeStr := identifier(ident.Name) + mappedType := mapClickHouseType(typeStr) + // Strip trailing [] since we'll use ArrayBounds if needed + if strings.HasSuffix(mappedType, "[]") { + mappedType = strings.TrimSuffix(mappedType, "[]") + } + typeName = &ast.TypeName{ + Name: mappedType, + Names: &ast.List{Items: []ast.Node{NewIdentifier(mappedType)}}, + } + } else { + // Fallback to text if we can't determine the type + typeName = &ast.TypeName{ + Name: "text", + Names: &ast.List{Items: []ast.Node{NewIdentifier("text")}}, + } + } + + return &ast.TypeCast{ + Arg: c.convert(op.LeftExpr), + TypeName: typeName, + Location: int(op.Pos()), + } + } + + // Convert binary operations like =, !=, <, >, AND, OR, etc. + return &ast.A_Expr{ + Kind: ast.A_Expr_Kind(0), // Default kind + Name: &ast.List{ + Items: []ast.Node{ + &ast.String{Str: string(op.Operation)}, + }, + }, + Lexpr: c.convert(op.LeftExpr), + Rexpr: c.convert(op.RightExpr), + Location: int(op.Pos()), + } +} + +func (c *cc) convertNumberLiteral(num *chparser.NumberLiteral) ast.Node { + if num == nil || num.Literal == "" { + return &ast.A_Const{ + Val: &ast.Integer{Ival: 0}, + Location: 0, + } + } + + numStr := num.Literal + + // Try to parse as integer first + if !strings.ContainsAny(numStr, ".eE") { + // Integer literal + if ival, err := strconv.ParseInt(numStr, 10, 64); err == nil { + return &ast.A_Const{ + Val: &ast.Integer{Ival: ival}, + Location: int(num.Pos()), + } + } + } + + // Try to parse as float + if _, err := strconv.ParseFloat(numStr, 64); err == nil { + return &ast.A_Const{ + Val: &ast.Float{Str: numStr}, + Location: int(num.Pos()), + } + } + + // Fallback to integer 0 if parsing fails + return &ast.A_Const{ + Val: &ast.Integer{Ival: 0}, + Location: int(num.Pos()), + } +} + +func (c *cc) convertStringLiteral(str *chparser.StringLiteral) ast.Node { + // The ClickHouse parser's StringLiteral.Pos() returns the position of the first + // character after the opening quote. We need to adjust it to point to the opening + // quote itself for correct location tracking in rewrite.NamedParameters, which uses + // args[0].Pos() - 1 to find the opening paren position. + pos := int(str.Pos()) + if pos > 0 { + pos-- // Move from first char inside quote to the opening quote + } + return &ast.A_Const{ + Val: &ast.String{ + Str: str.Literal, + }, + Location: pos, + } +} + +func (c *cc) convertQueryParam(param *chparser.QueryParam) ast.Node { + // ClickHouse uses ? for parameters + c.paramCount += 1 + return &ast.ParamRef{ + Number: c.paramCount, + Location: int(param.Pos()), + Dollar: false, // ClickHouse uses ? notation, not $1 + } +} + +func (c *cc) convertNestedIdentifier(nested *chparser.NestedIdentifier) ast.Node { + // NestedIdentifier represents things like "database.table" or "table.column" + fields := &ast.List{Items: []ast.Node{}} + + if nested.Ident != nil { + fieldName := identifier(nested.Ident.Name) + if fieldName == "*" { + fields.Items = append(fields.Items, &ast.A_Star{}) + } else { + fields.Items = append(fields.Items, &ast.String{Str: fieldName}) + } + } + if nested.DotIdent != nil { + fieldName := identifier(nested.DotIdent.Name) + if fieldName == "*" { + fields.Items = append(fields.Items, &ast.A_Star{}) + } else { + fields.Items = append(fields.Items, &ast.String{Str: fieldName}) + } + } + + return &ast.ColumnRef{ + Fields: fields, + Location: int(nested.Pos()), + } +} + +// isClickHouseTypeNullable checks if a ClickHouse column type is nullable +// In ClickHouse, columns are non-nullable by default unless wrapped in Nullable(T) +func isClickHouseTypeNullable(colType chparser.ColumnType) bool { + if colType == nil { + return false + } + + // Check if it's a ComplexType (like Nullable(T), Array(T), etc.) + if ct, ok := colType.(*chparser.ComplexType); ok { + if ct.Name != nil { + typeName := ct.Name.String() + if strings.EqualFold(typeName, "Nullable") { + return true + } + } + } + + return false +} + +func (c *cc) convertColumnDef(col *chparser.ColumnDef) ast.Node { + if col == nil { + return &ast.TODO{} + } + + // Extract column name + var colName string + if col.Name != nil { + if col.Name.Ident != nil { + colName = identifier(col.Name.Ident.Name) + } else if col.Name.DotIdent != nil { + colName = identifier(col.Name.DotIdent.Name) + } + } + + // Convert column type + var typeName *ast.TypeName + if col.Type != nil { + typeName = c.convertColumnType(col.Type) + } + + // Extract array information from TypeName + arrayDims := 0 + if typeName != nil && typeName.ArrayBounds != nil { + arrayDims = len(typeName.ArrayBounds.Items) + } + + // In ClickHouse, columns are non-nullable by default. + // They become nullable only if explicitly wrapped in Nullable(T). + // The Nullable wrapper is unwrapped in convertColumnType(), so we need to + // check if the original type was Nullable to determine nullability. + isNullable := isClickHouseTypeNullable(col.Type) + + columnDef := &ast.ColumnDef{ + Colname: colName, + TypeName: typeName, + IsNotNull: !isNullable, + IsArray: arrayDims > 0, + ArrayDims: arrayDims, + } + + return columnDef +} + +func (c *cc) convertColumnType(colType chparser.ColumnType) *ast.TypeName { + if colType == nil { + return &ast.TypeName{ + Name: "text", + Names: &ast.List{Items: []ast.Node{NewIdentifier("text")}}, + } + } + + // Extract type name - ColumnType is an interface, get the string representation + typeName := colType.Type() + + // Handle ComplexType (e.g., LowCardinality(T), Array(T), Map(K,V), Nullable(T), etc.) + // For LowCardinality(T), extract T directly and discard the wrapper + // LowCardinality is a ClickHouse-specific optimization hint that doesn't affect + // the semantic type of the data, so we unwrap it at the engine level before + // it reaches the codegen layer. This prevents LowCardinality from leaking into + // sqlc's type system where it would have no meaning. + if complexType, ok := colType.(*chparser.ComplexType); ok { + if strings.EqualFold(typeName, "LowCardinality") && len(complexType.Params) > 0 { + innerColType := complexType.Params[0] + return c.convertColumnType(innerColType) + } + + // Handle Nullable(T) - unwrap and return inner type + // Nullability is tracked via the NotNull flag in ColumnDef, not the type itself + if strings.EqualFold(typeName, "Nullable") && len(complexType.Params) > 0 { + innerColType := complexType.Params[0] + return c.convertColumnType(innerColType) + } + + // Handle Map(K, V) types + if strings.EqualFold(typeName, "Map") && len(complexType.Params) >= 2 { + keyColType := complexType.Params[0] + valueColType := complexType.Params[1] + + // Get mapped type names + keyTypeName := keyColType.Type() + keyMappedType := mapClickHouseType(keyTypeName) + + valueTypeName := valueColType.Type() + valueMappedType := mapClickHouseType(valueTypeName) + + // Check if the key type is valid for a Go map + if !isValidMapKeyType(keyMappedType) { + // If key type is not valid, fall back to map[string]interface{} + return &ast.TypeName{ + Name: "map[string]interface{}", + Names: &ast.List{Items: []ast.Node{NewIdentifier("map[string]interface{}")}}, + } + } + + // Convert database type names to valid Go type names for map syntax + keyGoType := databaseTypeToGoType(keyMappedType) + valueGoType := databaseTypeToGoType(valueMappedType) + + // Return map[K]V representation + mapType := "map[" + keyGoType + "]" + valueGoType + return &ast.TypeName{ + Name: mapType, + Names: &ast.List{Items: []ast.Node{NewIdentifier(mapType)}}, + } + } + } + + // Check if this is an array type + lowerTypeName := strings.ToLower(typeName) + var arrayBounds *ast.List + if strings.HasPrefix(lowerTypeName, "array") { + // Array types need ArrayBounds to be set for proper array dimension handling + // Each array level adds one item to ArrayBounds + arrayBounds = &ast.List{ + Items: []ast.Node{&ast.A_Const{}}, // One item for 1D array + } + } + + // Map ClickHouse types to PostgreSQL-compatible types for sqlc + mappedType := mapClickHouseType(typeName) + + // Strip trailing [] from mappedType since we'll use ArrayBounds instead + if strings.HasSuffix(mappedType, "[]") { + mappedType = strings.TrimSuffix(mappedType, "[]") + } + + return &ast.TypeName{ + Name: mappedType, + Names: &ast.List{Items: []ast.Node{NewIdentifier(mappedType)}}, + ArrayBounds: arrayBounds, + } +} + +// extractArrayElementType extracts the element type from Array(ElementType) +// e.g., "array(string)" -> "string", "array(uint32)" -> "uint32" +func extractArrayElementType(chType string) string { + chType = strings.ToLower(chType) + // Find the content within parentheses + start := strings.Index(chType, "(") + end := strings.LastIndex(chType, ")") + if start != -1 && end != -1 && end > start { + // Extract content and handle nested types + content := chType[start+1 : end] + // Remove any extra whitespace and handle nested parentheses + return strings.TrimSpace(content) + } + // Fallback to "string" if we can't extract the type + return "string" +} + +// databaseTypeToGoType converts database type names to valid Go syntax type names +// This is used for generating map types which need valid Go type syntax +// e.g., "double precision" -> "float64", "text" -> "string" +func databaseTypeToGoType(dbType string) string { + // Strip any trailing [] for now and re-add later + isArray := strings.HasSuffix(dbType, "[]") + typeBase := dbType + if isArray { + typeBase = strings.TrimSuffix(dbType, "[]") + } + + // Map database types to Go types + goType := "" + switch typeBase { + // Numeric types + case "int8": + goType = "int8" + case "int16": + goType = "int16" + case "int32": + goType = "int32" + case "int64": + goType = "int64" + case "uint8": + goType = "uint8" + case "uint16": + goType = "uint16" + case "uint32": + goType = "uint32" + case "uint64": + goType = "uint64" + case "float32", "real": + goType = "float32" + case "float64", "double precision": + goType = "float64" + case "numeric": + goType = "string" // Decimals use string + // String types + case "text", "varchar", "char", "string": + goType = "string" + // Boolean + case "bool", "boolean": + goType = "bool" + // Date/Time + case "date", "date32", "datetime", "datetime64", "timestamp": + goType = "time.Time" + // UUID + case "uuid": + goType = "string" + // JSON + case "jsonb", "json": + goType = "[]byte" // JSON types as raw bytes + // Default + default: + goType = typeBase // Fall back to the original + } + + // Re-add the array suffix if present + if isArray { + return "[]" + goType + } + return goType +} + +// isValidMapKeyType checks if a Go type is valid as a map key +// In Go, valid map key types are technically arrays (comparable), but in ClickHouse +// context, we restrict map keys to practical, simple types only. +// We explicitly forbid: +// - Arrays/slices (too complex as keys) +// - Maps/nested maps (invalid in Go) +// - Unknown/complex types +func isValidMapKeyType(goType string) bool { + // Explicitly allow common scalar types + switch goType { + case "int8", "int16", "int32", "int64", + "uint8", "uint16", "uint32", "uint64", + "float32", "float64", "real", "double precision", + "bool", "boolean", + "text", "string", "varchar", "char", + "uuid", + "numeric": // decimal types + return true + } + + // Date/time types are comparable + switch goType { + case "date", "date32", "datetime", "datetime64", "timestamp": + return true + } + + // Any pointer type to a scalar is valid + if strings.HasPrefix(goType, "*") && !strings.HasPrefix(goType, "*[]") && !strings.HasPrefix(goType, "*map") { + return true + } + + // interface{} is technically valid (matches anything) + if goType == "interface{}" { + return true + } + + // Explicitly forbid arrays, slices, and maps - too complex as map keys + // Arrays with [] suffix + if strings.HasSuffix(goType, "[]") { + return false + } + + // Maps + if strings.HasPrefix(goType, "map[") { + return false + } + + // Default: assume invalid (unknown types) + return false +} + +// mapClickHouseType maps ClickHouse data types to PostgreSQL-compatible types +// that sqlc understands for Go code generation +func mapClickHouseType(chType string) string { + chType = strings.ToLower(chType) + + switch { + // Integer types (UInt variants - unsigned) + case strings.HasPrefix(chType, "uint8"): + return "uint8" + case strings.HasPrefix(chType, "uint16"): + return "uint16" + case strings.HasPrefix(chType, "uint32"): + return "uint32" + case strings.HasPrefix(chType, "uint64"): + return "uint64" + // Integer types (Int variants - signed) + case strings.HasPrefix(chType, "int8"): + return "int8" + case strings.HasPrefix(chType, "int16"): + return "int16" + case strings.HasPrefix(chType, "int32"): + return "int32" + case strings.HasPrefix(chType, "int64"): + return "int64" + case strings.HasPrefix(chType, "int128"): + return "numeric" + case strings.HasPrefix(chType, "int256"): + return "numeric" + + // Float types + case strings.HasPrefix(chType, "float32"): + return "real" + case strings.HasPrefix(chType, "float64"): + return "double precision" + + // Decimal types + case strings.HasPrefix(chType, "decimal"): + return "numeric" + + // String types + case chType == "string": + return "text" + case strings.HasPrefix(chType, "fixedstring"): + return "varchar" + + // Date/Time types + case chType == "date": + return "date" + case chType == "date32": + return "date" + case chType == "datetime": + return "timestamp" + case chType == "datetime64": + return "timestamp" + + // Boolean + case chType == "bool": + return "boolean" + + // UUID + case chType == "uuid": + return "uuid" + + // IP address types + case chType == "ipv4": + return "ipv4" + case chType == "ipv6": + return "ipv6" + + // Array types + case strings.HasPrefix(chType, "array"): + // Extract element type from Array(ElementType) + // e.g., "array(string)" -> extract "string" + elementType := extractArrayElementType(chType) + mappedElementType := mapClickHouseType(elementType) + return mappedElementType + "[]" + + // JSON types + case strings.Contains(chType, "json"): + return "jsonb" + + // Default fallback + default: + return "text" + } +} + +func (c *cc) convertOrderExpr(order *chparser.OrderExpr) ast.Node { + if order == nil { + return &ast.TODO{} + } + + sortBy := &ast.SortBy{ + Node: c.convert(order.Expr), + Location: int(order.Pos()), + } + + // Handle sort direction + switch order.Direction { + case "DESC": + sortBy.SortbyDir = ast.SortByDirDesc + case "ASC": + sortBy.SortbyDir = ast.SortByDirAsc + default: + sortBy.SortbyDir = ast.SortByDirDefault + } + + return sortBy +} + +func (c *cc) convertPlaceHolder(ph *chparser.PlaceHolder) ast.Node { + // PlaceHolder is ClickHouse's ? parameter + c.paramCount += 1 + return &ast.ParamRef{ + Number: c.paramCount, + Location: int(ph.Pos()), + Dollar: false, // ClickHouse uses ? notation, not $1 + } +} + +func (c *cc) convertJoinTableExpr(jte *chparser.JoinTableExpr) ast.Node { + if jte == nil || jte.Table == nil { + return &ast.TODO{} + } + // JoinTableExpr is a wrapper around TableExpr with optional modifiers + // Just extract the underlying table expression + return c.convertTableExpr(jte.Table) +} + +// convertCastExpr converts CAST expressions like CAST(column AS type) +func (c *cc) convertCastExpr(castExpr *chparser.CastExpr) ast.Node { + if castExpr == nil { + return &ast.TODO{} + } + + // Convert the expression to be cast + expr := c.convert(castExpr.Expr) + + // Convert the target type - AsType is an Expr, need to extract type information + var typeName *ast.TypeName + if castExpr.AsType != nil { + // The AsType can be: ColumnType, Ident, or StringLiteral + if stringLit, ok := castExpr.AsType.(*chparser.StringLiteral); ok { + // CAST(x AS 'String') - extract type from string literal + typeStr := strings.ToLower(stringLit.Literal) + mappedType := mapClickHouseType(typeStr) + // Strip trailing [] since we'll use ArrayBounds if needed + if strings.HasSuffix(mappedType, "[]") { + mappedType = strings.TrimSuffix(mappedType, "[]") + } + typeName = &ast.TypeName{ + Name: mappedType, + Names: &ast.List{Items: []ast.Node{NewIdentifier(mappedType)}}, + } + } else if colType, ok := castExpr.AsType.(chparser.ColumnType); ok { + // CAST(x AS ColumnType) - standard form + typeName = c.convertColumnType(colType) + } else if ident, ok := castExpr.AsType.(*chparser.Ident); ok { + // Fallback: treat the identifier as a type name + typeStr := identifier(ident.Name) + mappedType := mapClickHouseType(typeStr) + // Strip trailing [] since we'll use ArrayBounds if needed + if strings.HasSuffix(mappedType, "[]") { + mappedType = strings.TrimSuffix(mappedType, "[]") + } + typeName = &ast.TypeName{ + Name: mappedType, + Names: &ast.List{Items: []ast.Node{NewIdentifier(mappedType)}}, + } + } else { + // Unknown type, default to text + typeName = &ast.TypeName{ + Name: "text", + Names: &ast.List{Items: []ast.Node{NewIdentifier("text")}}, + } + } + } + + return &ast.TypeCast{ + Arg: expr, + TypeName: typeName, + Location: int(castExpr.Pos()), + } +} + +// convertCaseExpr converts CASE expressions +func (c *cc) convertCaseExpr(caseExpr *chparser.CaseExpr) ast.Node { + if caseExpr == nil { + return &ast.TODO{} + } + + // Convert CASE input expression (if present) + var arg ast.Node + if caseExpr.Expr != nil { + arg = c.convert(caseExpr.Expr) + } + + // Convert WHEN clauses + args := &ast.List{Items: []ast.Node{}} + + for _, when := range caseExpr.Whens { + if when != nil { + // Convert WHEN condition + whenExpr := c.convert(when.When) + args.Items = append(args.Items, whenExpr) + + // Convert THEN result + thenExpr := c.convert(when.Then) + args.Items = append(args.Items, thenExpr) + } + } + + // Convert ELSE clause (if present) + var elseExpr ast.Node + if caseExpr.Else != nil { + elseExpr = c.convert(caseExpr.Else) + } + + return &ast.CaseExpr{ + Arg: arg, + Args: args, + Defresult: elseExpr, + Location: int(caseExpr.Pos()), + } +} + +// convertWindowFunctionExpr converts window function expressions +func (c *cc) convertWindowFunctionExpr(winExpr *chparser.WindowFunctionExpr) ast.Node { + if winExpr == nil { + return &ast.TODO{} + } + + // Convert the underlying function + funcCall := c.convertFunctionExpr(winExpr.Function) + + // Convert OVER clause (OverExpr contains the window specification) + var overClause *ast.WindowDef + if winExpr.OverExpr != nil { + // OverExpr might be a WindowExpr or other expression + if winDef, ok := winExpr.OverExpr.(*chparser.WindowExpr); ok { + overClause = c.convertWindowDef(winDef) + } + } + + // Wrap the function call in a window context + if funcCall, ok := funcCall.(*ast.FuncCall); ok { + funcCall.Over = overClause + return funcCall + } + + return funcCall +} + +// convertWindowDef converts window definition +func (c *cc) convertWindowDef(winDef *chparser.WindowExpr) *ast.WindowDef { + if winDef == nil { + return nil + } + + windowDef := &ast.WindowDef{ + Location: int(winDef.Pos()), + } + + // Convert PARTITION BY + if winDef.PartitionBy != nil && winDef.PartitionBy.Expr != nil { + windowDef.PartitionClause = &ast.List{Items: []ast.Node{}} + windowDef.PartitionClause.Items = append(windowDef.PartitionClause.Items, c.convert(winDef.PartitionBy.Expr)) + } + + // Convert ORDER BY + if winDef.OrderBy != nil { + windowDef.OrderClause = c.convertOrderByClause(winDef.OrderBy) + } + + return windowDef +} + +// convertIsNullExpr converts IS NULL expressions +func (c *cc) convertIsNullExpr(isNull *chparser.IsNullExpr) ast.Node { + if isNull == nil { + return &ast.TODO{} + } + + return &ast.NullTest{ + Arg: c.convert(isNull.Expr), + Nulltesttype: ast.NullTestType(0), // IS_NULL = 0 + Location: int(isNull.Pos()), + } +} + +// convertIsNotNullExpr converts IS NOT NULL expressions +func (c *cc) convertIsNotNullExpr(isNotNull *chparser.IsNotNullExpr) ast.Node { + if isNotNull == nil { + return &ast.TODO{} + } + + return &ast.NullTest{ + Arg: c.convert(isNotNull.Expr), + Nulltesttype: ast.NullTestType(1), // IS_NOT_NULL = 1 + Location: int(isNotNull.Pos()), + } +} + +// convertUnaryExpr converts unary expressions (like NOT, negation) +func (c *cc) convertUnaryExpr(unary *chparser.UnaryExpr) ast.Node { + if unary == nil { + return &ast.TODO{} + } + + // Kind is a TokenKind (string) + kindStr := string(unary.Kind) + + return &ast.A_Expr{ + Kind: ast.A_Expr_Kind(1), // AEXPR_OP_ANY or AEXPR_OP + Name: &ast.List{ + Items: []ast.Node{ + &ast.String{Str: kindStr}, + }, + }, + Rexpr: c.convert(unary.Expr), + Location: int(unary.Pos()), + } +} + +// convertMapLiteral converts map/dictionary literals +func (c *cc) convertMapLiteral(mapLit *chparser.MapLiteral) ast.Node { + if mapLit == nil { + return &ast.TODO{} + } + + // ClickHouse uses map literals like {'key': value, 'key2': value2} + // Convert to a list of key-value pairs + items := &ast.List{Items: []ast.Node{}} + + for _, kv := range mapLit.KeyValues { + // Key is a StringLiteral value, need to convert it to a pointer + keyLit := &kv.Key + // Add key + items.Items = append(items.Items, c.convert(keyLit)) + // Add value + if kv.Value != nil { + items.Items = append(items.Items, c.convert(kv.Value)) + } + } + + // Return as a generic constant list (maps aren't directly supported in sqlc AST) + return &ast.A_Const{ + Val: items, + Location: int(mapLit.Pos()), + } +} + +// convertParamExprList converts a parenthesized expression list to its content +// ParamExprList represents (expr1, expr2, ...) or (expr) +// We convert it by extracting and converting the items +func (c *cc) convertParamExprList(paramList *chparser.ParamExprList) ast.Node { + if paramList == nil || paramList.Items == nil { + return &ast.TODO{} + } + + // If there's only one item, return that directly (unwrap the parens) + if len(paramList.Items.Items) == 1 { + return c.convert(paramList.Items.Items[0]) + } + + // If there are multiple items, convert them all and wrap in a list + // This shouldn't normally happen in a WHERE clause, but handle it just in case + items := &ast.List{Items: []ast.Node{}} + for _, item := range paramList.Items.Items { + if colExpr, ok := item.(*chparser.ColumnExpr); ok { + items.Items = append(items.Items, c.convert(colExpr.Expr)) + } else { + items.Items = append(items.Items, c.convert(item)) + } + } + return items +} + +// mergeArrayJoinIntoFrom integrates ARRAY JOIN into the FROM clause as a special join +// ClickHouse's ARRAY JOIN is unique - it "unfolds" arrays into rows +// We represent it as a cross join with special handling +func (c *cc) mergeArrayJoinIntoFrom(fromClause *ast.List, arrayJoin *chparser.ArrayJoinClause) *ast.List { + if fromClause == nil { + fromClause = &ast.List{Items: []ast.Node{}} + } + + // Convert the ARRAY JOIN expression to a join node + arrayJoinNode := c.convertArrayJoinClause(arrayJoin) + + // Add the ARRAY JOIN to the FROM clause + if arrayJoinNode != nil { + fromClause.Items = append(fromClause.Items, arrayJoinNode) + } + + return fromClause +} + +// convertArrayJoinClause converts ClickHouse ARRAY JOIN to sqlc AST +// ARRAY JOIN unfolds arrays into rows - we represent it as a RangeSubselect (derived table) +// This creates a synthetic SELECT that the compiler can understand without special handling +func (c *cc) convertArrayJoinClause(arrayJoin *chparser.ArrayJoinClause) ast.Node { + if arrayJoin == nil { + return nil + } + + // The Expr field contains the array expression(s) to unfold + // It can be: + // - A single column reference (e.g., "tags") + // - A list of expressions with aliases (e.g., "ParsedParams AS pp" or "a AS x, b AS y") + + // Check if it's a ColumnExprList (multiple array expressions) + if exprList, ok := arrayJoin.Expr.(*chparser.ColumnExprList); ok && len(exprList.Items) > 0 { + // Multiple array expressions - create synthetic SELECT for each + colnames := c.collectArrayJoinColnames(exprList.Items) + if len(colnames) == 0 { + return nil + } + return c.createArrayJoinSubquery(colnames) + } + + // Single expression + colnames := c.extractArrayJoinColname(arrayJoin.Expr) + if colnames == nil { + return nil + } + return c.createArrayJoinSubquery([]ast.Node{colnames}) +} + +// collectArrayJoinColnames extracts all column names from ARRAY JOIN expressions +// Only adds non-nil colnames to the returned list +func (c *cc) collectArrayJoinColnames(items []chparser.Expr) []ast.Node { + var colnames []ast.Node + for _, expr := range items { + colname := c.extractArrayJoinColname(expr) + // Only add non-nil colnames + if colname != nil { + colnames = append(colnames, colname) + } + } + return colnames +} + +// extractArrayJoinColname extracts the column name from an ARRAY JOIN item +// Returns a String AST node representing the column name +func (c *cc) extractArrayJoinColname(expr chparser.Expr) ast.Node { + if expr == nil { + return nil + } + + // Handle ColumnExpr (most common case) + if colExpr, ok := expr.(*chparser.ColumnExpr); ok { + if colExpr.Alias != nil { + // Use the explicit alias + return &ast.String{Str: identifier(colExpr.Alias.Name)} + } + // Extract name from the expression itself + if colExpr.Expr != nil { + return c.extractNameFromExpr(colExpr.Expr) + } + } + + // Handle other expression types + return c.extractNameFromExpr(expr) +} + +// extractNameFromExpr extracts a name from an arbitrary expression +func (c *cc) extractNameFromExpr(expr chparser.Expr) ast.Node { + if expr == nil { + return nil + } + + // Path expression (e.g., u.tags) + if path, ok := expr.(*chparser.Path); ok && len(path.Fields) > 0 { + lastName := path.Fields[len(path.Fields)-1].Name + return &ast.String{Str: identifier(lastName)} + } + + // Simple identifier + if ident, ok := expr.(*chparser.Ident); ok { + return &ast.String{Str: identifier(ident.Name)} + } + + return nil +} + +// createArrayJoinSubquery creates a synthetic RangeSubselect representing ARRAY JOIN output +// We create a synthetic SelectStmt with ResTargets that have the column names +// The compiler will evaluate this SelectStmt normally via outputColumns logic +func (c *cc) createArrayJoinSubquery(colnames []ast.Node) ast.Node { + // Filter out any nil column names + validColnames := []ast.Node{} + for _, colname := range colnames { + if colname != nil { + validColnames = append(validColnames, colname) + } + } + + if len(validColnames) == 0 { + return nil + } + + // Create a synthetic SELECT statement with the column names + // SELECT colname1, colname2, ... + // This allows the compiler's existing outputColumns logic to extract the columns + targetList := &ast.List{Items: []ast.Node{}} + for _, colname := range validColnames { + if strNode, ok := colname.(*ast.String); ok { + // Create a ResTarget for each column name + // Use A_Const with a String value - this doesn't require table lookups + colName := strNode.Str + targetList.Items = append(targetList.Items, &ast.ResTarget{ + Name: &colName, + Val: &ast.A_Const{ + Val: &ast.String{Str: colName}, + }, + }) + } + } + + // Create synthetic SelectStmt for this ARRAY JOIN + // Initialize with empty Lists to avoid nil pointer dereferences + syntheticSelect := &ast.SelectStmt{ + TargetList: targetList, + FromClause: &ast.List{}, + GroupClause: &ast.List{}, + WindowClause: &ast.List{}, + SortClause: &ast.List{}, + } + + // Wrap in RangeSubselect (derived table) + // The compiler will call outputColumns on this subquery to get the columns + return &ast.RangeSubselect{ + Lateral: false, // ARRAY JOIN is not a lateral subquery + Subquery: syntheticSelect, + Alias: nil, // No need for Colnames since we have a proper Subquery + } +} + +// convertArrayJoinItemToFunc converts a single ARRAY JOIN item to a FuncCall and optional colnames +// Returns the FuncCall and a list of column name nodes (StringNodes) for the alias(es) +func (c *cc) convertArrayJoinItemToFunc(expr chparser.Expr) (*ast.FuncCall, []ast.Node) { + if expr == nil { + return nil, nil + } + + var arrayExpr ast.Node + var colnames []ast.Node + + // Handle ColumnExpr (which can have an alias) - this is what ARRAY JOIN produces + if colExpr, ok := expr.(*chparser.ColumnExpr); ok { + // Extract the expression and alias + arrayExpr = c.convert(colExpr.Expr) + + if colExpr.Alias != nil { + columnName := identifier(colExpr.Alias.Name) + colnames = append(colnames, &ast.String{Str: columnName}) + } + } else if selectItem, ok := expr.(*chparser.SelectItem); ok { + // Also handle SelectItem for compatibility + arrayExpr = c.convert(selectItem.Expr) + + if selectItem.Alias != nil { + columnName := identifier(selectItem.Alias.Name) + colnames = append(colnames, &ast.String{Str: columnName}) + } + } else { + // Direct column reference without alias + arrayExpr = c.convert(expr) + } + + if arrayExpr == nil { + return nil, nil + } + + // Create a function call representing the array unnesting + // We use a special function name "arrayjoin" to indicate this is an ARRAY JOIN + funcCall := &ast.FuncCall{ + Func: &ast.FuncName{ + Name: "arrayjoin", + }, + Args: &ast.List{ + Items: []ast.Node{arrayExpr}, + }, + } + + return funcCall, colnames +} + +// convertIndexOperation converts array/tuple indexing like arr[1] or tuple[2] +func (c *cc) convertIndexOperation(idxOp *chparser.IndexOperation) ast.Node { + if idxOp == nil { + return &ast.TODO{} + } + + // Convert the index expression + idx := c.convert(idxOp.Index) + + // Create an A_Indices node representing array/tuple indexing + // IsSlice is false for single-element access like arr[1] + // It would be true for range access like arr[1:5] (if supported) + return &ast.A_Indices{ + IsSlice: false, + Lidx: idx, + Uidx: nil, // No upper index for single-element access + } +} + +// convertArrayParamList converts array literals like [1, 2, 3] or ['a', 'b'] +func (c *cc) convertArrayParamList(arrList *chparser.ArrayParamList) ast.Node { + if arrList == nil || arrList.Items == nil { + return &ast.TODO{} + } + + // Convert each item in the array + items := &ast.List{Items: []ast.Node{}} + for _, item := range arrList.Items.Items { + // Each item is a ColumnExpr, extract the underlying expression + converted := c.convert(item) + items.Items = append(items.Items, converted) + } + + // Return an A_ArrayExpr representing the array literal + return &ast.A_ArrayExpr{ + Elements: items, + Location: int(arrList.Pos()), + } +} + +// convertTableFunctionExpr converts table functions like SELECT * FROM numbers(10) +// These are ClickHouse-specific functions that return table-like results +func (c *cc) convertTableFunctionExpr(tfn *chparser.TableFunctionExpr) ast.Node { + if tfn == nil { + return &ast.TODO{} + } + + // TableFunctionExpr has a Name (which is an Expr) and Args (TableArgListExpr) + // We convert it to a RangeFunction to represent a function in FROM clause context + + // Get the function name by converting the Name expression + // Usually it's a simple Ident, but could be more complex + var funcName string + if tfn.Name != nil { + if ident, ok := tfn.Name.(*chparser.Ident); ok { + funcName = identifier(ident.Name) + } else { + funcName = "table_function" + } + } else { + funcName = "unknown" + } + + // Convert arguments if present + args := &ast.List{Items: []ast.Node{}} + if tfn.Args != nil && tfn.Args.Args != nil { + for _, arg := range tfn.Args.Args { + args.Items = append(args.Items, c.convert(arg)) + } + } + + // Create a FuncCall representing the table function + funcCall := &ast.FuncCall{ + Func: &ast.FuncName{ + Name: funcName, + }, + Args: args, + } + + // Wrap in a RangeFunction to represent it in FROM clause context + return &ast.RangeFunction{ + Functions: &ast.List{ + Items: []ast.Node{funcCall}, + }, + } +} + +// convertTernaryOperation converts ternary conditional expressions +// These are similar to CASE expressions but use a different structure +func (c *cc) convertTernaryOperation(ternary *chparser.TernaryOperation) ast.Node { + if ternary == nil { + return &ast.TODO{} + } + + // Convert to a CaseExpr structure for consistency with sqlc AST + // A ternary operation is: condition ? true_expr : false_expr + // This maps to: CASE WHEN condition THEN true_expr ELSE false_expr END + + // Convert the condition and expressions + condition := c.convert(ternary.Condition) + trueExpr := c.convert(ternary.TrueExpr) + falseExpr := c.convert(ternary.FalseExpr) + + return &ast.CaseExpr{ + Arg: nil, // No CASE expr, just WHEN conditions + Args: &ast.List{ + Items: []ast.Node{ + condition, + trueExpr, + }, + }, + Defresult: falseExpr, // ELSE clause + Location: int(ternary.Pos()), + } +} + +// convertCreateView converts CREATE VIEW statements +// ClickHouse views are similar to other SQL databases but may have specific features +func (c *cc) convertCreateView(view *chparser.CreateView) ast.Node { + if view == nil { + return &ast.TODO{} + } + + // Extract view name from TableIdentifier + var viewName string + if view.Name != nil { + if view.Name.Table != nil { + viewName = identifier(view.Name.Table.Name) + } + } + + // Convert the SELECT query from SubQuery + var selectStmt ast.Node + if view.SubQuery != nil && view.SubQuery.Select != nil { + selectStmt = c.convert(view.SubQuery.Select) + } + + // For now, return a TODO since sqlc AST doesn't have a specific View representation + // The SelectStmt is converted for reference + _ = selectStmt + _ = viewName + + return &ast.TODO{} +} + +// convertCreateMaterializedView converts CREATE MATERIALIZED VIEW statements +// These are ClickHouse-specific materialized views +func (c *cc) convertCreateMaterializedView(matView *chparser.CreateMaterializedView) ast.Node { + if matView == nil { + return &ast.TODO{} + } + + // Extract view name from TableIdentifier + var viewName string + if matView.Name != nil { + if matView.Name.Table != nil { + viewName = identifier(matView.Name.Table.Name) + } + } + + // Convert the SELECT query from SubQuery + var selectStmt ast.Node + if matView.SubQuery != nil && matView.SubQuery.Select != nil { + selectStmt = c.convert(matView.SubQuery.Select) + } + + // For now, return a TODO since sqlc AST doesn't have a specific MaterializedView representation + // The SelectStmt is converted for reference + _ = selectStmt + _ = viewName + + return &ast.TODO{} +} diff --git a/internal/engine/clickhouse/ident_test.go b/internal/engine/clickhouse/ident_test.go new file mode 100644 index 0000000000..ff475ddba4 --- /dev/null +++ b/internal/engine/clickhouse/ident_test.go @@ -0,0 +1,171 @@ +package clickhouse + +import ( + "strings" + "testing" + + "github.com/sqlc-dev/sqlc/internal/sql/ast" +) + +// TestConvertIdentAsColumnRef tests that identifiers are correctly converted +// to ColumnRef nodes instead of String literals. +// +// This is important because identifiers in SELECT, WHERE, and other clauses +// refer to columns, not literal strings. The compiler's column resolution logic +// depends on finding ColumnRef nodes to properly match columns against the catalog. +func TestConvertIdentAsColumnRef(t *testing.T) { + parser := NewParser() + + tests := []struct { + name string + query string + wantVal func(ast.Node) bool // checks that Val is a ColumnRef + }{ + { + name: "select single column", + query: "SELECT id FROM table1", + wantVal: func(n ast.Node) bool { + colRef, ok := n.(*ast.ColumnRef) + if !ok { + return false + } + if len(colRef.Fields.Items) != 1 { + return false + } + str, ok := colRef.Fields.Items[0].(*ast.String) + return ok && str.Str == "id" + }, + }, + { + name: "select multiple columns", + query: "SELECT id, name, email FROM table1", + wantVal: func(n ast.Node) bool { + _, ok := n.(*ast.ColumnRef) + return ok + }, + }, + { + name: "where clause with column reference", + query: "SELECT * FROM table1 WHERE id = 1", + wantVal: func(n ast.Node) bool { + // The WHERE clause should have a ColumnRef for 'id' + // This is a simple smoke test that the query parses + return true + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + stmts, err := parser.Parse(strings.NewReader(tt.query)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(stmts) == 0 { + t.Fatal("No statements parsed") + } + + selectStmt, ok := stmts[0].Raw.Stmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", stmts[0].Raw.Stmt) + } + + if len(selectStmt.TargetList.Items) == 0 { + t.Fatal("No targets in select") + } + + // Check the first target + resTarget := selectStmt.TargetList.Items[0].(*ast.ResTarget) + if resTarget == nil { + t.Fatal("First target is not a ResTarget") + } + + if !tt.wantVal(resTarget.Val) { + t.Errorf("Val check failed. Got type %T: %+v", resTarget.Val, resTarget.Val) + } + }) + } +} + +// TestIdentifierInWhereClause tests that identifiers in WHERE clauses are +// converted to ColumnRef, not String literals. +func TestIdentifierInWhereClause(t *testing.T) { + parser := NewParser() + + query := "SELECT * FROM users WHERE status = 'active' AND age > 18" + stmts, err := parser.Parse(strings.NewReader(query)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + selectStmt := stmts[0].Raw.Stmt.(*ast.SelectStmt) + + // The WHERE clause should contain a BoolExpr with column references for 'status' and 'age' + // This test ensures the parser correctly identifies column references in conditions + if selectStmt.WhereClause == nil { + t.Fatal("WHERE clause is nil") + } + + // Just verify it parses without error and has a where clause + // The detailed structure is tested in the ClickHouse parser tests + t.Logf("WHERE clause type: %T", selectStmt.WhereClause) +} + +// TestIdentifierResolution tests that identifiers are properly resolved +// when matching against catalog columns. +func TestIdentifierResolution(t *testing.T) { + parser := NewParser() + cat := NewCatalog() + + // Create a table with specific columns + schemaSQL := `CREATE TABLE users ( + user_id UInt32, + user_name String, + user_email String + )` + + stmts, err := parser.Parse(strings.NewReader(schemaSQL)) + if err != nil { + t.Fatalf("Parse schema failed: %v", err) + } + + for _, stmt := range stmts { + if err := cat.Update(stmt, nil); err != nil { + t.Fatalf("Update catalog failed: %v", err) + } + } + + // Parse a query selecting these columns by name + querySQL := "SELECT user_id, user_name FROM users" + queryStmts, err := parser.Parse(strings.NewReader(querySQL)) + if err != nil { + t.Fatalf("Parse query failed: %v", err) + } + + selectStmt := queryStmts[0].Raw.Stmt.(*ast.SelectStmt) + + // Verify that targets are ColumnRefs + for i, target := range selectStmt.TargetList.Items { + resTarget, ok := target.(*ast.ResTarget) + if !ok { + t.Fatalf("Target %d is not ResTarget", i) + } + + colRef, ok := resTarget.Val.(*ast.ColumnRef) + if !ok { + t.Fatalf("Target %d Val is not ColumnRef, got %T", i, resTarget.Val) + } + + if len(colRef.Fields.Items) == 0 { + t.Fatalf("Target %d ColumnRef has no fields", i) + } + + colName, ok := colRef.Fields.Items[0].(*ast.String) + if !ok { + t.Fatalf("Target %d field is not String", i) + } + + t.Logf("Column %d: %s", i, colName.Str) + } +} diff --git a/internal/engine/clickhouse/integration_test.go b/internal/engine/clickhouse/integration_test.go new file mode 100644 index 0000000000..3eb21ed71f --- /dev/null +++ b/internal/engine/clickhouse/integration_test.go @@ -0,0 +1,253 @@ +//go:build integration +// +build integration + +package clickhouse + +import ( + "context" + "database/sql" + "os" + "strings" + "testing" + + _ "github.com/ClickHouse/clickhouse-go/v2" +) + +// TestArrayJoinIntegration tests ARRAY JOIN against a live ClickHouse database +// Run with: go test -tags=integration -run TestArrayJoinIntegration ./internal/engine/clickhouse +// +// Prerequisites: +// - ClickHouse server running (docker run -p 9000:9000 -p 8123:8123 clickhouse/clickhouse-server) +// - Or use docker-compose up clickhouse +func TestArrayJoinIntegration(t *testing.T) { + // Skip if no ClickHouse connection info + clickhouseURL := os.Getenv("CLICKHOUSE_URL") + if clickhouseURL == "" { + clickhouseURL = "clickhouse://localhost:9000/default" + } + + db, err := sql.Open("clickhouse", clickhouseURL) + if err != nil { + t.Skip("ClickHouse not available:", err) + return + } + defer db.Close() + + ctx := context.Background() + + // Test connection + if err := db.PingContext(ctx); err != nil { + t.Skip("ClickHouse not reachable:", err) + return + } + + // Create test database + _, err = db.ExecContext(ctx, "CREATE DATABASE IF NOT EXISTS sqlc_test") + if err != nil { + t.Fatalf("Failed to create database: %v", err) + } + + // Clean up function + defer func() { + db.ExecContext(ctx, "DROP DATABASE IF EXISTS sqlc_test") + }() + + t.Run("BasicArrayJoin", func(t *testing.T) { + // Create table with array column + _, err := db.ExecContext(ctx, ` + CREATE TABLE IF NOT EXISTS sqlc_test.users_with_tags ( + id UInt32, + name String, + tags Array(String) + ) ENGINE = MergeTree() + ORDER BY id + `) + if err != nil { + t.Fatalf("Failed to create table: %v", err) + } + defer db.ExecContext(ctx, "DROP TABLE IF EXISTS sqlc_test.users_with_tags") + + // Insert test data + _, err = db.ExecContext(ctx, ` + INSERT INTO sqlc_test.users_with_tags VALUES + (1, 'Alice', ['developer', 'admin']), + (2, 'Bob', ['designer', 'user']), + (3, 'Charlie', ['manager']) + `) + if err != nil { + t.Fatalf("Failed to insert data: %v", err) + } + + // Test ARRAY JOIN query + rows, err := db.QueryContext(ctx, ` + SELECT id, name, tag + FROM sqlc_test.users_with_tags + ARRAY JOIN tags AS tag + ORDER BY id, tag + `) + if err != nil { + t.Fatalf("ARRAY JOIN query failed: %v", err) + } + defer rows.Close() + + // Verify results + expectedResults := []struct { + id uint32 + name string + tag string + }{ + {1, "Alice", "admin"}, + {1, "Alice", "developer"}, + {2, "Bob", "designer"}, + {2, "Bob", "user"}, + {3, "Charlie", "manager"}, + } + + resultCount := 0 + for rows.Next() { + var id uint32 + var name, tag string + if err := rows.Scan(&id, &name, &tag); err != nil { + t.Fatalf("Failed to scan row: %v", err) + } + + if resultCount >= len(expectedResults) { + t.Fatalf("More results than expected, got row: id=%d, name=%s, tag=%s", id, name, tag) + } + + expected := expectedResults[resultCount] + if id != expected.id || name != expected.name || tag != expected.tag { + t.Errorf("Row %d mismatch: got (%d, %s, %s), want (%d, %s, %s)", + resultCount, id, name, tag, expected.id, expected.name, expected.tag) + } + resultCount++ + } + + if resultCount != len(expectedResults) { + t.Errorf("Expected %d rows, got %d", len(expectedResults), resultCount) + } + }) + + t.Run("ArrayJoinWithFilter", func(t *testing.T) { + // Create table with nested arrays + _, err := db.ExecContext(ctx, ` + CREATE TABLE IF NOT EXISTS sqlc_test.events_with_props ( + event_id UInt32, + event_name String, + properties Array(String) + ) ENGINE = MergeTree() + ORDER BY event_id + `) + if err != nil { + t.Fatalf("Failed to create table: %v", err) + } + defer db.ExecContext(ctx, "DROP TABLE IF EXISTS sqlc_test.events_with_props") + + // Insert test data + _, err = db.ExecContext(ctx, ` + INSERT INTO sqlc_test.events_with_props VALUES + (1, 'click', ['button', 'header', 'link']), + (2, 'view', ['page', 'section']), + (3, 'submit', ['form', 'button']) + `) + if err != nil { + t.Fatalf("Failed to insert data: %v", err) + } + + // Test ARRAY JOIN with WHERE clause + rows, err := db.QueryContext(ctx, ` + SELECT event_id, event_name, prop + FROM sqlc_test.events_with_props + ARRAY JOIN properties AS prop + WHERE event_id >= 2 + ORDER BY event_id, prop + `) + if err != nil { + t.Fatalf("ARRAY JOIN with WHERE failed: %v", err) + } + defer rows.Close() + + resultCount := 0 + for rows.Next() { + var eventID uint32 + var eventName, prop string + if err := rows.Scan(&eventID, &eventName, &prop); err != nil { + t.Fatalf("Failed to scan row: %v", err) + } + + // Verify event_id is >= 2 + if eventID < 2 { + t.Errorf("Expected event_id >= 2, got %d", eventID) + } + resultCount++ + } + + // Should have 4 rows (2: page, section; 3: button, form) + if resultCount != 4 { + t.Errorf("Expected 4 rows, got %d", resultCount) + } + }) + + t.Run("ArrayJoinFunction", func(t *testing.T) { + // Test arrayJoin() function (different from ARRAY JOIN clause) + rows, err := db.QueryContext(ctx, ` + SELECT arrayJoin(['a', 'b', 'c']) AS element + `) + if err != nil { + t.Fatalf("arrayJoin() function failed: %v", err) + } + defer rows.Close() + + elements := []string{} + for rows.Next() { + var elem string + if err := rows.Scan(&elem); err != nil { + t.Fatalf("Failed to scan row: %v", err) + } + elements = append(elements, elem) + } + + expected := []string{"a", "b", "c"} + if len(elements) != len(expected) { + t.Errorf("Expected %d elements, got %d", len(expected), len(elements)) + } + + for i, elem := range elements { + if elem != expected[i] { + t.Errorf("Element %d: expected %s, got %s", i, expected[i], elem) + } + } + }) + + t.Run("ParseAndExecute", func(t *testing.T) { + // Test that our parser can parse the ARRAY JOIN query + // and the generated AST is correct + sql := ` + SELECT id, name, tag + FROM sqlc_test.users_with_tags + ARRAY JOIN tags AS tag + WHERE id = 1 + ORDER BY tag + ` + + p := NewParser() + stmts, err := p.Parse(strings.NewReader(sql)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(stmts) != 1 { + t.Fatalf("Expected 1 statement, got %d", len(stmts)) + } + + // The parsing succeeded - this validates our converter worked + t.Log("Successfully parsed ARRAY JOIN query") + }) +} + +// TestArrayJoinCodeGeneration tests the full pipeline: parse -> generate -> execute +func TestArrayJoinCodeGeneration(t *testing.T) { + // This would require the full sqlc pipeline + // For now, we just test that the queries in queries.sql can be parsed + t.Skip("Full code generation test requires sqlc generate - run manually") +} diff --git a/internal/engine/clickhouse/join_test.go b/internal/engine/clickhouse/join_test.go new file mode 100644 index 0000000000..369495935b --- /dev/null +++ b/internal/engine/clickhouse/join_test.go @@ -0,0 +1,115 @@ +package clickhouse + +import ( + "fmt" + "strings" + "testing" + + "github.com/sqlc-dev/sqlc/internal/sql/ast" +) + +// TestConvertJoinExpr tests that JOIN expressions are properly converted +// to sqlc AST with RangeVar nodes instead of TODO nodes +func TestConvertJoinExpr(t *testing.T) { + parser := NewParser() + + tests := []struct { + name string + query string + wantErr bool + check func(*ast.SelectStmt) error + }{ + { + name: "simple left join", + query: "SELECT u.id, u.name, p.id as post_id FROM users u LEFT JOIN posts p ON u.id = p.user_id", + wantErr: false, + check: func(selectStmt *ast.SelectStmt) error { + // Check that FROM clause contains a JoinExpr + if selectStmt.FromClause == nil { + return errorf("FromClause is nil") + } + if len(selectStmt.FromClause.Items) == 0 { + return errorf("FromClause items is empty") + } + + // The first item in FromClause should be a JoinExpr + fromItem := selectStmt.FromClause.Items[0] + joinExpr, ok := fromItem.(*ast.JoinExpr) + if !ok { + return errorf("Expected JoinExpr, got %T", fromItem) + } + + // Larg should be a RangeVar for the left table + if joinExpr.Larg == nil { + return errorf("JoinExpr.Larg is nil") + } + larg, ok := joinExpr.Larg.(*ast.RangeVar) + if !ok { + return errorf("Expected RangeVar for Larg, got %T", joinExpr.Larg) + } + if larg.Relname == nil || *larg.Relname != "users" { + return errorf("Expected left table to be 'users', got %v", larg.Relname) + } + + // Rarg should be a RangeVar for the right table (after normalization) + // ClickHouse join structures are normalized to PostgreSQL style at conversion time + if joinExpr.Rarg == nil { + return errorf("JoinExpr.Rarg is nil") + } + rarg, ok := joinExpr.Rarg.(*ast.RangeVar) + if !ok { + return errorf("Expected RangeVar for Rarg (normalized from ClickHouse structure), got %T", joinExpr.Rarg) + } + if rarg.Relname == nil || *rarg.Relname != "posts" { + return errorf("Expected right table to be 'posts', got %v", rarg.Relname) + } + + return nil + }, + }, + { + name: "join without aliases", + query: "SELECT * FROM users INNER JOIN posts ON users.id = posts.user_id", + wantErr: false, + check: func(selectStmt *ast.SelectStmt) error { + if len(selectStmt.FromClause.Items) == 0 { + return errorf("FromClause items is empty") + } + joinExpr, ok := selectStmt.FromClause.Items[0].(*ast.JoinExpr) + if !ok { + return errorf("Expected JoinExpr, got %T", selectStmt.FromClause.Items[0]) + } + if joinExpr.Jointype != ast.JoinTypeInner { + return errorf("Expected INNER join, got %v", joinExpr.Jointype) + } + return nil + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + stmts, err := parser.Parse(strings.NewReader(tt.query)) + if (err != nil) != tt.wantErr { + t.Fatalf("Parse error: %v, wantErr %v", err, tt.wantErr) + } + + if len(stmts) == 0 { + t.Fatal("No statements parsed") + } + + selectStmt, ok := stmts[0].Raw.Stmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", stmts[0].Raw.Stmt) + } + + if err := tt.check(selectStmt); err != nil { + t.Fatal(err) + } + }) + } +} + +func errorf(format string, args ...interface{}) error { + return fmt.Errorf(format, args...) +} diff --git a/internal/engine/clickhouse/new_conversions_test.go b/internal/engine/clickhouse/new_conversions_test.go new file mode 100644 index 0000000000..a059c5ac98 --- /dev/null +++ b/internal/engine/clickhouse/new_conversions_test.go @@ -0,0 +1,210 @@ +package clickhouse + +import ( + "strings" + "testing" + + "github.com/sqlc-dev/sqlc/internal/sql/ast" +) + +// TestArrayParamListConversion tests array literal conversion +func TestArrayParamListConversion(t *testing.T) { + parser := NewParser() + + tests := []struct { + name string + query string + wantErr bool + }{ + { + name: "simple array literal", + query: "SELECT [1, 2, 3] as arr", + wantErr: false, + }, + { + name: "string array literal", + query: "SELECT ['a', 'b', 'c'] as strs", + wantErr: false, + }, + { + name: "array in IN clause", + query: "SELECT * FROM table1 WHERE x IN [1, 2, 3]", + wantErr: false, + }, + { + name: "array in function", + query: "SELECT arrayMap(x -> x * 2, [1, 2, 3]) as doubled", + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + stmts, err := parser.Parse(strings.NewReader(tt.query)) + if (err != nil) != tt.wantErr { + t.Fatalf("Parse error: %v, wantErr %v", err, tt.wantErr) + } + + if len(stmts) > 0 { + selectStmt, ok := stmts[0].Raw.Stmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", stmts[0].Raw.Stmt) + } + + // Verify it parses without TODO nodes (or minimal TODOs) + _ = selectStmt + } + }) + } +} + +// TestTableFunctionConversion tests table function conversion +func TestTableFunctionConversion(t *testing.T) { + parser := NewParser() + + tests := []struct { + name string + query string + wantErr bool + }{ + { + name: "numbers table function", + query: "SELECT * FROM numbers(10)", + wantErr: false, + }, + { + name: "numbers with range", + query: "SELECT * FROM numbers(1, 5)", + wantErr: false, + }, + { + name: "numbers in join", + query: "SELECT t.number FROM numbers(5) t", + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + stmts, err := parser.Parse(strings.NewReader(tt.query)) + if (err != nil) != tt.wantErr { + t.Fatalf("Parse error: %v, wantErr %v", err, tt.wantErr) + } + + if len(stmts) > 0 { + selectStmt, ok := stmts[0].Raw.Stmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", stmts[0].Raw.Stmt) + } + + // Verify FromClause exists and has items + if selectStmt.FromClause != nil && len(selectStmt.FromClause.Items) > 0 { + // Should be a RangeFunction for table functions + fromItem := selectStmt.FromClause.Items[0] + _, isRangeFunc := fromItem.(*ast.RangeFunction) + if !isRangeFunc && !tt.wantErr { + // Could also be RangeVar if not a pure table function context + // This is acceptable + } + } + } + }) + } +} + +// TestIndexOperationConversion tests array/tuple indexing conversion +func TestIndexOperationConversion(t *testing.T) { + parser := NewParser() + + tests := []struct { + name string + query string + wantErr bool + }{ + { + name: "array indexing", + query: "SELECT arr[1] FROM table1", + wantErr: false, + }, + { + name: "tuple indexing", + query: "SELECT (1, 2, 3)[2] as second_elem", + wantErr: false, + }, + { + name: "nested array indexing", + query: "SELECT nested_arr[1][2] FROM table1", + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + stmts, err := parser.Parse(strings.NewReader(tt.query)) + if (err != nil) != tt.wantErr { + t.Fatalf("Parse error: %v, wantErr %v", err, tt.wantErr) + } + + if len(stmts) > 0 { + selectStmt, ok := stmts[0].Raw.Stmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", stmts[0].Raw.Stmt) + } + + _ = selectStmt + } + }) + } +} + +// TestTernaryOperationConversion tests ternary operator conversion +func TestTernaryOperationConversion(t *testing.T) { + parser := NewParser() + + tests := []struct { + name string + query string + wantErr bool + }{ + { + name: "simple ternary", + query: "SELECT x > 0 ? 'positive' : 'non-positive' as sign FROM table1", + wantErr: false, + }, + { + name: "nested ternary", + query: "SELECT x > 0 ? 'positive' : x < 0 ? 'negative' : 'zero' as sign FROM table1", + wantErr: false, + }, + { + name: "ternary in expression", + query: "SELECT (a > b ? a : b) as max_val FROM table1", + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + stmts, err := parser.Parse(strings.NewReader(tt.query)) + if (err != nil) != tt.wantErr { + t.Fatalf("Parse error: %v, wantErr %v", err, tt.wantErr) + } + + if len(stmts) > 0 { + selectStmt, ok := stmts[0].Raw.Stmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", stmts[0].Raw.Stmt) + } + + // Should have a CaseExpr in the TargetList + if len(selectStmt.TargetList.Items) > 0 { + resTarget := selectStmt.TargetList.Items[0].(*ast.ResTarget) + _, isCaseExpr := resTarget.Val.(*ast.CaseExpr) + if !isCaseExpr { + // Might be wrapped in other expressions, which is ok + } + } + } + }) + } +} diff --git a/internal/engine/clickhouse/parse.go b/internal/engine/clickhouse/parse.go new file mode 100644 index 0000000000..1ee9050b50 --- /dev/null +++ b/internal/engine/clickhouse/parse.go @@ -0,0 +1,402 @@ +package clickhouse + +import ( + "errors" + "fmt" + "io" + "regexp" + "strings" + + chparser "github.com/AfterShip/clickhouse-sql-parser/parser" + + "github.com/sqlc-dev/sqlc/internal/source" + "github.com/sqlc-dev/sqlc/internal/sql/ast" + "github.com/sqlc-dev/sqlc/internal/sql/catalog" + "github.com/sqlc-dev/sqlc/internal/sql/sqlerr" +) + +func NewParser() *Parser { + return &Parser{} +} + +type Parser struct { + // catalog is set by the Compiler after schema parsing is complete + // It allows the parser to register context-dependent functions + Catalog interface{} +} + +// preprocessNamedParameters converts sqlc named parameter syntax to valid ClickHouse syntax +// that the parser can handle. This allows sqlc.arg(), sqlc.narg(), and sqlc.slice() syntax +// to be used in queries by converting the dot notation to underscore notation, which the +// ClickHouse parser can recognize as a function call. +// +// Conversions: +// - sqlc.arg('name') → sqlc_arg('name') +// - sqlc.narg('name') → sqlc_narg('name') +// - sqlc.slice('name') → sqlc_slice('name') +// +// The original SQL is preserved in the compiler, so rewrite.NamedParameters can still +// find and process the original named parameter syntax. The converter normalizes +// sqlc_* function names back to sqlc.* schema.function format in the AST. +func preprocessNamedParameters(sql string) string { + // Convert sqlc.arg/narg/slice to sqlc_arg/narg/slice + // This makes them valid function names in ClickHouse parser + // Using same-length replacement (sqlc. = 5 chars, sqlc_ = 5 chars) preserves positions + funcPattern := regexp.MustCompile(`sqlc\.(arg|narg|slice)`) + sql = funcPattern.ReplaceAllString(sql, "sqlc_$1") + + return sql +} + +func (p *Parser) Parse(r io.Reader) ([]ast.Statement, error) { + blob, err := io.ReadAll(r) + if err != nil { + return nil, err + } + + originalSQL := string(blob) + + // Preprocess to replace named parameter syntax with valid ClickHouse syntax + processedSQL := preprocessNamedParameters(originalSQL) + + chp := chparser.NewParser(processedSQL) + stmtNodes, err := chp.ParseStmts() + if err != nil { + return nil, normalizeErr(err) + } + + // Find all -- name: comments in the original SQL + nameCommentPositions := findNameComments(originalSQL) + + // Clone the catalog for this parse operation to isolate function registrations + // (like arrayJoin, argMin, etc.) from other queries + var clonedCatalog *catalog.Catalog + if p.Catalog != nil { + cat, ok := p.Catalog.(*catalog.Catalog) + if !ok { + return nil, fmt.Errorf("invalid catalog type: expected *catalog.Catalog, got %T", p.Catalog) + } + clonedCatalog = cat.Clone() + } + + var stmts []ast.Statement + for i := range stmtNodes { + converter := &cc{catalog: clonedCatalog} + out := converter.convert(stmtNodes[i]) + + var statementStart, statementEnd int + + // Check if we're processing a file with -- name: comments (queries.sql) + // or without them (schema.sql) + if len(nameCommentPositions) == len(stmtNodes) { + // We have a -- name: comment for each statement (queries file) + statementStart = nameCommentPositions[i] + + if i+1 < len(nameCommentPositions) { + statementEnd = nameCommentPositions[i+1] + } else { + statementEnd = len(originalSQL) + } + } else { + // No name comments, or mismatch (schema file or mixed) + // Use the parser's positions, but try to find better boundaries + processedStmtPos := int(stmtNodes[i].Pos()) + if processedStmtPos > 0 { + processedStmtPos -= 1 + } + + originalStmtPos := findOriginalPosition(originalSQL, processedSQL, processedStmtPos) + statementStart = findStatementStart(originalSQL, originalStmtPos) + + if i+1 < len(stmtNodes) { + nextProcessedStmtPos := int(stmtNodes[i+1].Pos()) + if nextProcessedStmtPos > 0 { + nextProcessedStmtPos -= 1 + } + nextOriginalStmtPos := findOriginalPosition(originalSQL, processedSQL, nextProcessedStmtPos) + statementEnd = findStatementStart(originalSQL, nextOriginalStmtPos) + } else { + statementEnd = len(originalSQL) + } + } + + // Bounds check + if statementStart < 0 || statementStart >= len(originalSQL) || statementEnd > len(originalSQL) || statementStart >= statementEnd { + continue + } + + segment := originalSQL[statementStart:statementEnd] + // Trim trailing whitespace but preserve the content + segment = strings.TrimRight(segment, " \t\r\n") + + stmts = append(stmts, ast.Statement{ + Raw: &ast.RawStmt{ + Stmt: out, + StmtLocation: statementStart, + StmtLen: len(segment), + }, + }) + } + + return stmts, nil +} + +// findNameComments finds all positions of -- name: comments in the SQL +// Returns a slice of positions where each -- name: comment starts +func findNameComments(sql string) []int { + var positions []int + lines := strings.Split(sql, "\n") + currentPos := 0 + + for _, line := range lines { + trimmed := strings.TrimSpace(line) + // Check if this line contains a -- name: comment + if strings.HasPrefix(trimmed, "--") && strings.Contains(trimmed, "name:") { + // Find the actual position of the start of this line (not trimmed) + lineStart := currentPos + // Walk backwards to find any leading whitespace + for lineStart < currentPos+len(line) && (sql[lineStart] == ' ' || sql[lineStart] == '\t') { + lineStart++ + } + // Actually, we want the start of the line including whitespace + positions = append(positions, currentPos) + } + // Move to next line (including the \n character) + currentPos += len(line) + 1 + } + + return positions +} + +// findOriginalPosition maps a position in processedSQL back to the original SQL +// Preprocessing only changes sqlc.arg to sqlc_arg (same length), so positions are mostly 1:1 +func findOriginalPosition(originalSQL, processedSQL string, processedPos int) int { + // Since sqlc. → sqlc_ is same length (5 chars), positions are virtually identical + // Just ensure we don't go out of bounds + if processedPos >= len(originalSQL) { + return len(originalSQL) + } + return processedPos +} + +// findStatementStart finds the start of a statement in SQL, including preceding -- name: annotation +func findStatementStart(sql string, stmtPos int) int { + if stmtPos <= 0 { + return 0 + } + + // Walk backwards through lines to find the -- name: annotation + // The stmtPos usually points to the SELECT/INSERT/etc line, but we need to include + // the -- name: comment that precedes it. + + currentPos := stmtPos + + // Keep walking backwards through lines + for currentPos > 0 { + // Find the start of the current line + lineStart := currentPos - 1 + for lineStart >= 0 && sql[lineStart] != '\n' { + lineStart-- + } + lineStart++ // Move past the newline (or stay at 0) + + // Extract the line content, skipping leading whitespace + checkPos := lineStart + for checkPos < len(sql) && (sql[checkPos] == ' ' || sql[checkPos] == '\t') { + checkPos++ + } + + // Find the end of this line + lineEnd := checkPos + for lineEnd < len(sql) && sql[lineEnd] != '\n' { + lineEnd++ + } + + if checkPos < lineEnd { + lineText := sql[checkPos:lineEnd] + + // Check if this line is a -- name: annotation + if strings.HasPrefix(lineText, "--") && strings.Contains(lineText, "name:") { + // Found it! Return the start of this line + return lineStart + } + + // Check if this is a non-empty, non-comment line + // If we find actual SQL before finding the -- name: comment, stop + if len(strings.TrimSpace(lineText)) > 0 && !strings.HasPrefix(lineText, "--") { + // This is SQL content, stop searching backwards + break + } + } + + // Move to the previous line + if lineStart == 0 { + break + } + currentPos = lineStart - 1 + } + + // Didn't find a -- name: annotation, return the original position + return stmtPos +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} + +// normalizeErr converts ClickHouse parser errors to sqlc error format +func normalizeErr(err error) error { + if err == nil { + return err + } + + // For now, wrap the error as a generic syntax error + // ClickHouse parser may provide better error information in future versions + return &sqlerr.Error{ + Message: "syntax error", + Err: errors.New(err.Error()), + Line: 1, + Column: 1, + } +} + +// CommentSyntax returns the comment syntax for ClickHouse +// ClickHouse supports: +// - Line comments: -- (with optional space after) +// - Block comments: /* */ +func (p *Parser) CommentSyntax() source.CommentSyntax { + return source.CommentSyntax{ + Dash: true, + SlashStar: true, + } +} + +// IsReservedKeyword checks if a string is a ClickHouse reserved keyword +func (p *Parser) IsReservedKeyword(s string) bool { + return isReservedKeyword(strings.ToUpper(s)) +} + +// ClickHouse reserved keywords +// Based on https://clickhouse.com/docs/sql-reference/syntax#keywords +var reservedKeywords = map[string]bool{ + "ALTER": true, + "AND": true, + "ARRAY": true, + "AS": true, + "ASCENDING": true, + "ASOF": true, + "BETWEEN": true, + "BY": true, + "CASE": true, + "CAST": true, + "CHECK": true, + "CLUSTER": true, + "CODEC": true, + "COLLATE": true, + "COLUMN": true, + "CONSTRAINT": true, + "CREATE": true, + "CROSS": true, + "CUBE": true, + "CURRENT": true, + "DATABASE": true, + "DAY": true, + "DEDUPLICATE": true, + "DEFAULT": true, + "DEFINER": true, + "DELETE": true, + "DESC": true, + "DESCENDING": true, + "DESCRIBE": true, + "DISTINCT": true, + "DROP": true, + "ELSE": true, + "END": true, + "ESCAPING": true, + "EXCEPT": true, + "EXCHANGE": true, + "EXPLAIN": true, + "FETCH": true, + "FILL": true, + "FINAL": true, + "FIRST": true, + "FOR": true, + "FOREGROUND": true, + "FROM": true, + "FULL": true, + "FUNCTION": true, + "GLOBAL": true, + "GRANT": true, + "GROUP": true, + "HAVING": true, + "HOUR": true, + "IF": true, + "ILIKE": true, + "IN": true, + "INNER": true, + "INTERSECT": true, + "INTO": true, + "IS": true, + "ISNULL": true, + "JOIN": true, + "KEY": true, + "KILL": true, + "LAST": true, + "LATERAL": true, + "LEFT": true, + "LIKE": true, + "LIMIT": true, + "LOCAL": true, + "NATURAL": true, + "NOT": true, + "NOTNULL": true, + "NULL": true, + "OFFSET": true, + "ON": true, + "OR": true, + "ORDER": true, + "OUTER": true, + "PARTITION": true, + "PREWHERE": true, + "PRIMARY": true, + "REVOKE": true, + "RIGHT": true, + "ROLLUP": true, + "ROW": true, + "ROWS": true, + "SAMPLE": true, + "SELECT": true, + "SEMI": true, + "SET": true, + "SETTINGS": true, + "SHOW": true, + "SOME": true, + "SUBJECT": true, + "TABLE": true, + "THEN": true, + "TIES": true, + "TRUNCATE": true, + "UNION": true, + "UPDATE": true, + "USING": true, + "VIEW": true, + "WHEN": true, + "WHERE": true, + "WINDOW": true, + "WITH": true, + "YEAR": true, +} + +func isReservedKeyword(s string) bool { + return reservedKeywords[s] +} diff --git a/internal/engine/clickhouse/parse_actual_queries_test.go b/internal/engine/clickhouse/parse_actual_queries_test.go new file mode 100644 index 0000000000..c43d3551f5 --- /dev/null +++ b/internal/engine/clickhouse/parse_actual_queries_test.go @@ -0,0 +1,123 @@ +package clickhouse + +import ( + "strings" + "testing" +) + +// TestActualQueryBoundaries tests with the actual queries that are having issues +func TestActualQueryBoundaries(t *testing.T) { + // These are the actual queries from examples/clickhouse/queries.sql that show the bug + input := `-- name: UnfoldNestedData :many +SELECT + record_id, + nested_value +FROM sqlc_example.nested_table +ARRAY JOIN nested_array AS nested_value +WHERE record_id IN (sqlc.slice('record_ids')); + +-- name: AnalyzeArrayElements :many +SELECT + product_id, + arrayJoin(categories) AS category, + COUNT(*) OVER (PARTITION BY category) as category_count +FROM sqlc_example.products +WHERE product_id = ? +GROUP BY product_id, category; + +-- name: ExtractMetadataFromJSON :many +SELECT + MetadataPlatformId, + arrayJoin(JSONExtract(JsonValue, 'Array(String)')) as self_help_id +FROM sqlc_example.events;` + + parser := NewParser() + stmts, err := parser.Parse(strings.NewReader(input)) + if err != nil { + t.Fatalf("failed to parse: %v", err) + } + + if len(stmts) != 3 { + t.Fatalf("expected 3 statements, got %d", len(stmts)) + } + + // Define expected queries + expectedQueries := []string{ + `-- name: UnfoldNestedData :many +SELECT + record_id, + nested_value +FROM sqlc_example.nested_table +ARRAY JOIN nested_array AS nested_value +WHERE record_id IN (sqlc.slice('record_ids'));`, + `-- name: AnalyzeArrayElements :many +SELECT + product_id, + arrayJoin(categories) AS category, + COUNT(*) OVER (PARTITION BY category) as category_count +FROM sqlc_example.products +WHERE product_id = ? +GROUP BY product_id, category;`, + `-- name: ExtractMetadataFromJSON :many +SELECT + MetadataPlatformId, + arrayJoin(JSONExtract(JsonValue, 'Array(String)')) as self_help_id +FROM sqlc_example.events;`, + } + + for i, stmt := range stmts { + raw := stmt.Raw + if raw == nil { + t.Fatalf("statement %d has no RawStmt", i) + } + + // Extract the SQL text + location := raw.StmtLocation + length := raw.StmtLen + + t.Logf("Statement %d: location=%d, length=%d", i, location, length) + + if location < 0 || location >= len(input) { + t.Errorf("Statement %d: invalid location %d (input length: %d)", i, location, len(input)) + continue + } + + if location+length > len(input) { + t.Errorf("Statement %d: location+length (%d) exceeds input length (%d)", + i, location+length, len(input)) + continue + } + + extracted := input[location : location+length] + + // Normalize whitespace for comparison + extracted = strings.TrimSpace(extracted) + expected := strings.TrimSpace(expectedQueries[i]) + + if extracted != expected { + t.Errorf("Query %d boundary mismatch:\n\n=== EXPECTED ===\n%s\n\n=== GOT ===\n%s\n\n=== DIFF ===", + i, expected, extracted) + + // Show first difference + minLen := len(extracted) + if len(expected) < minLen { + minLen = len(expected) + } + for j := 0; j < minLen; j++ { + if extracted[j] != expected[j] { + start := j - 20 + if start < 0 { + start = 0 + } + end := j + 20 + if end > minLen { + end = minLen + } + t.Errorf("First difference at position %d:\nExpected: %q\nGot: %q", + j, expected[start:end], extracted[start:end]) + break + } + } + } + } +} diff --git a/internal/engine/clickhouse/parse_boundary_test.go b/internal/engine/clickhouse/parse_boundary_test.go new file mode 100644 index 0000000000..905552b182 --- /dev/null +++ b/internal/engine/clickhouse/parse_boundary_test.go @@ -0,0 +1,142 @@ +package clickhouse + +import ( + "strings" + "testing" +) + +// TestQueryBoundaryDetection tests that the parser correctly identifies +// statement boundaries including the -- name: annotation +func TestQueryBoundaryDetection(t *testing.T) { + input := `-- name: QueryOne :one +SELECT id, name FROM table1 +WHERE id = ?; + +-- name: QueryTwo :many +SELECT id, value FROM table2 +WHERE status = sqlc.arg('status') +ORDER BY id; + +-- name: QueryThree :exec +INSERT INTO table3 (id, data) +VALUES (?, ?);` + + parser := NewParser() + stmts, err := parser.Parse(strings.NewReader(input)) + if err != nil { + t.Fatalf("failed to parse: %v", err) + } + + if len(stmts) != 3 { + t.Fatalf("expected 3 statements, got %d", len(stmts)) + } + + // Extract the raw SQL for each statement + type extractedQuery struct { + name string + expected string + } + + queries := []extractedQuery{ + { + name: "QueryOne", + expected: `-- name: QueryOne :one +SELECT id, name FROM table1 +WHERE id = ?;`, + }, + { + name: "QueryTwo", + expected: `-- name: QueryTwo :many +SELECT id, value FROM table2 +WHERE status = sqlc.arg('status') +ORDER BY id;`, + }, + { + name: "QueryThree", + expected: `-- name: QueryThree :exec +INSERT INTO table3 (id, data) +VALUES (?, ?);`, + }, + } + + for i, stmt := range stmts { + raw := stmt.Raw + if raw == nil { + t.Fatalf("statement %d has no RawStmt", i) + } + + // Extract the SQL text using the same logic as the compiler + location := raw.StmtLocation + length := raw.StmtLen + extracted := input[location : location+length] + + // Normalize whitespace for comparison + extracted = strings.TrimSpace(extracted) + expected := strings.TrimSpace(queries[i].expected) + + if extracted != expected { + t.Errorf("Query %d (%s) boundary mismatch:\nExpected:\n%s\n\nGot:\n%s", + i, queries[i].name, expected, extracted) + } + } +} + +// TestComplexQueryBoundaries tests boundary detection with more complex queries +func TestComplexQueryBoundaries(t *testing.T) { + input := `-- name: GetUserByID :one +SELECT id, name, email, created_at +FROM users +WHERE id = ?; + +-- name: ListUsers :many +SELECT id, name, email, created_at +FROM users +ORDER BY created_at DESC +LIMIT ?; + +-- name: InsertUser :exec +INSERT INTO users (id, name, email, created_at) +VALUES (?, ?, ?, ?);` + + parser := NewParser() + stmts, err := parser.Parse(strings.NewReader(input)) + if err != nil { + t.Fatalf("failed to parse: %v", err) + } + + if len(stmts) != 3 { + t.Fatalf("expected 3 statements, got %d", len(stmts)) + } + + // Verify each extracted query + for i, stmt := range stmts { + raw := stmt.Raw + location := raw.StmtLocation + length := raw.StmtLen + extracted := input[location : location+length] + + // Check that it starts with "-- name:" + if !strings.HasPrefix(strings.TrimSpace(extracted), "-- name:") { + t.Errorf("Query %d doesn't start with '-- name:': %q", i, extracted[:min(50, len(extracted))]) + } + + // Check that it ends with a semicolon + trimmed := strings.TrimSpace(extracted) + if !strings.HasSuffix(trimmed, ";") { + t.Errorf("Query %d doesn't end with ';': %q", i, trimmed[max(0, len(trimmed)-50):]) + } + + // Check that it doesn't contain text from adjacent queries + lines := strings.Split(extracted, "\n") + nameCommentCount := 0 + for _, line := range lines { + if strings.Contains(line, "-- name:") { + nameCommentCount++ + } + } + if nameCommentCount != 1 { + t.Errorf("Query %d contains %d '-- name:' comments, expected 1:\n%s", + i, nameCommentCount, extracted) + } + } +} diff --git a/internal/engine/clickhouse/parse_real_file_test.go b/internal/engine/clickhouse/parse_real_file_test.go new file mode 100644 index 0000000000..30fb0fbb4a --- /dev/null +++ b/internal/engine/clickhouse/parse_real_file_test.go @@ -0,0 +1,112 @@ +package clickhouse + +import ( + "os" + "strings" + "testing" +) + +// TestRealQueryFile tests parsing the actual queries.sql file +func TestRealQueryFile(t *testing.T) { + // Read the actual queries file + queriesPath := "../../../examples/clickhouse/queries.sql" + content, err := os.ReadFile(queriesPath) + if err != nil { + t.Skipf("Could not read queries file: %v", err) + } + + input := string(content) + + parser := NewParser() + stmts, err := parser.Parse(strings.NewReader(input)) + if err != nil { + t.Fatalf("failed to parse: %v", err) + } + + t.Logf("Parsed %d statements", len(stmts)) + + // Find statements we know are having issues + problemQueries := map[string]struct { + expectedStart string + expectedEnd string + }{ + "UnfoldNestedData": { + expectedStart: "-- name: UnfoldNestedData :many", + expectedEnd: "WHERE record_id IN (sqlc.slice('record_ids'));", + }, + "AnalyzeArrayElements": { + expectedStart: "-- name: AnalyzeArrayElements :many", + expectedEnd: "GROUP BY product_id, category;", + }, + "ExtractMetadataFromJSON": { + expectedStart: "-- name: ExtractMetadataFromJSON :many", + expectedEnd: "FROM sqlc_example.events;", + }, + } + + // Check each statement + for i, stmt := range stmts { + raw := stmt.Raw + if raw == nil { + continue + } + + location := raw.StmtLocation + length := raw.StmtLen + + if location < 0 || location >= len(input) { + t.Errorf("Statement %d: invalid location %d (input length: %d)", i, location, len(input)) + continue + } + + if location+length > len(input) { + t.Errorf("Statement %d: location+length (%d) exceeds input length (%d)", + i, location+length, len(input)) + continue + } + + extracted := input[location : location+length] + extracted = strings.TrimSpace(extracted) + + // Look for the query name in the extracted text + if strings.Contains(extracted, "-- name:") { + // Extract the query name + lines := strings.Split(extracted, "\n") + if len(lines) > 0 { + firstLine := strings.TrimSpace(lines[0]) + if strings.HasPrefix(firstLine, "-- name:") { + parts := strings.Fields(firstLine) + if len(parts) >= 3 { + queryName := parts[2] + + if check, ok := problemQueries[queryName]; ok { + // Check if it starts correctly + if !strings.HasPrefix(extracted, check.expectedStart) { + t.Errorf("Query %s: doesn't start correctly\nExpected start: %q\nGot: %q", + queryName, check.expectedStart, extracted[:min(len(check.expectedStart)+20, len(extracted))]) + } + + // Check if it ends correctly + if !strings.HasSuffix(extracted, check.expectedEnd) { + t.Errorf("Query %s: doesn't end correctly\nExpected end: %q\nGot: %q", + queryName, check.expectedEnd, extracted[max(0, len(extracted)-len(check.expectedEnd)-20):]) + } + + // Check for contamination from other queries + nameCommentCount := 0 + for _, line := range lines { + if strings.Contains(line, "-- name:") { + nameCommentCount++ + } + } + if nameCommentCount > 1 { + t.Errorf("Query %s contains %d '-- name:' comments (expected 1)", + queryName, nameCommentCount) + } + } + } + } + } + } + } +} diff --git a/internal/engine/clickhouse/parse_test.go b/internal/engine/clickhouse/parse_test.go new file mode 100644 index 0000000000..7d5e631d27 --- /dev/null +++ b/internal/engine/clickhouse/parse_test.go @@ -0,0 +1,2255 @@ +package clickhouse + +import ( + "strings" + "testing" + + "github.com/sqlc-dev/sqlc/internal/sql/ast" + "github.com/sqlc-dev/sqlc/internal/sql/catalog" +) + +func TestParseCreateTable(t *testing.T) { + sql := ` +CREATE TABLE IF NOT EXISTS users +( + id UInt32, + name String, + email String +) +ENGINE = MergeTree() +ORDER BY id; +` + + p := NewParser() + stmts, err := p.Parse(strings.NewReader(sql)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(stmts) != 1 { + t.Fatalf("Expected 1 statement, got %d", len(stmts)) + } + + stmt := stmts[0] + if stmt.Raw == nil || stmt.Raw.Stmt == nil { + t.Fatal("Statement or Raw.Stmt is nil") + } + + createStmt, ok := stmt.Raw.Stmt.(*ast.CreateTableStmt) + if !ok { + t.Fatalf("Expected CreateTableStmt, got %T", stmt.Raw.Stmt) + } + + if createStmt.Name == nil || createStmt.Name.Name == "" { + t.Fatal("Table name is missing") + } + + if createStmt.Name.Name != "users" { + t.Errorf("Expected table name 'users', got '%s'", createStmt.Name.Name) + } + + if createStmt.Cols == nil || len(createStmt.Cols) == 0 { + t.Fatal("Table columns are missing") + } + + if len(createStmt.Cols) != 3 { + t.Errorf("Expected 3 columns, got %d", len(createStmt.Cols)) + } + + // Check first column + col0 := createStmt.Cols[0] + if col0.Colname != "id" { + t.Errorf("Expected column name 'id', got '%s'", col0.Colname) + } +} + +func TestParseNumberLiterals(t *testing.T) { + tests := []struct { + name string + sql string + wantInt bool + wantVal int64 + wantFloat bool + }{ + { + name: "Integer literal", + sql: "SELECT 42;", + wantInt: true, + wantVal: 42, + }, + { + name: "Large integer literal", + sql: "SELECT 9223372036854775807;", + wantInt: true, + wantVal: 9223372036854775807, + }, + { + name: "Float literal", + sql: "SELECT 3.14;", + wantFloat: true, + }, + { + name: "Scientific notation", + sql: "SELECT 1.5e2;", + wantFloat: true, + }, + { + name: "Zero", + sql: "SELECT 0;", + wantInt: true, + wantVal: 0, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + p := NewParser() + stmts, err := p.Parse(strings.NewReader(test.sql)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(stmts) != 1 { + t.Fatalf("Expected 1 statement, got %d", len(stmts)) + } + + selectStmt, ok := stmts[0].Raw.Stmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", stmts[0].Raw.Stmt) + } + + if selectStmt.TargetList == nil || len(selectStmt.TargetList.Items) == 0 { + t.Fatal("Expected at least one target") + } + + target, ok := selectStmt.TargetList.Items[0].(*ast.ResTarget) + if !ok { + t.Fatalf("Expected ResTarget, got %T", selectStmt.TargetList.Items[0]) + } + + if test.wantInt { + constNode, ok := target.Val.(*ast.A_Const) + if !ok { + t.Fatalf("Expected A_Const, got %T", target.Val) + } + + intVal, ok := constNode.Val.(*ast.Integer) + if !ok { + t.Fatalf("Expected Integer, got %T", constNode.Val) + } + + if intVal.Ival != test.wantVal { + t.Errorf("Expected value %d, got %d", test.wantVal, intVal.Ival) + } + } + + if test.wantFloat { + constNode, ok := target.Val.(*ast.A_Const) + if !ok { + t.Fatalf("Expected A_Const, got %T", target.Val) + } + + _, ok = constNode.Val.(*ast.Float) + if !ok { + t.Fatalf("Expected Float, got %T", constNode.Val) + } + } + }) + } +} + +func TestParseWindowFunctions(t *testing.T) { + sql := ` + SELECT + id, + COUNT(*) OVER (PARTITION BY department ORDER BY salary DESC) as rank + FROM employees; + ` + + p := NewParser() + stmts, err := p.Parse(strings.NewReader(sql)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(stmts) != 1 { + t.Fatalf("Expected 1 statement, got %d", len(stmts)) + } + + selectStmt, ok := stmts[0].Raw.Stmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", stmts[0].Raw.Stmt) + } + + // Window functions should be parsed in TargetList + if selectStmt.TargetList == nil || len(selectStmt.TargetList.Items) < 2 { + t.Fatalf("Expected at least 2 targets, got %d", len(selectStmt.TargetList.Items)) + } +} + +func TestParseCastExpression(t *testing.T) { + sql := "SELECT CAST(id AS String), CAST(value AS Float32) FROM table1;" + + p := NewParser() + stmts, err := p.Parse(strings.NewReader(sql)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(stmts) != 1 { + t.Fatalf("Expected 1 statement, got %d", len(stmts)) + } + + selectStmt, ok := stmts[0].Raw.Stmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", stmts[0].Raw.Stmt) + } + + if selectStmt.TargetList == nil || len(selectStmt.TargetList.Items) < 2 { + t.Fatalf("Expected at least 2 targets, got %d", len(selectStmt.TargetList.Items)) + } +} + +func TestParseCaseExpression(t *testing.T) { + sql := ` + SELECT + id, + CASE + WHEN status = 'active' THEN 1 + WHEN status = 'inactive' THEN 0 + ELSE -1 + END as status_code + FROM users; + ` + + p := NewParser() + stmts, err := p.Parse(strings.NewReader(sql)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(stmts) != 1 { + t.Fatalf("Expected 1 statement, got %d", len(stmts)) + } + + selectStmt, ok := stmts[0].Raw.Stmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", stmts[0].Raw.Stmt) + } + + if selectStmt.TargetList == nil || len(selectStmt.TargetList.Items) < 2 { + t.Fatalf("Expected at least 2 targets, got %d", len(selectStmt.TargetList.Items)) + } +} + +func TestParseAggregateQuery(t *testing.T) { + sql := ` + SELECT + department, + COUNT(*) as count, + SUM(salary) as total_salary, + AVG(salary) as avg_salary + FROM employees + GROUP BY department + HAVING COUNT(*) > 10 + ORDER BY total_salary DESC; + ` + + p := NewParser() + stmts, err := p.Parse(strings.NewReader(sql)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(stmts) != 1 { + t.Fatalf("Expected 1 statement, got %d", len(stmts)) + } + + selectStmt, ok := stmts[0].Raw.Stmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", stmts[0].Raw.Stmt) + } + + if selectStmt.GroupClause == nil || len(selectStmt.GroupClause.Items) == 0 { + t.Fatal("GROUP BY clause not parsed") + } + + if selectStmt.HavingClause == nil { + t.Fatal("HAVING clause not parsed") + } + + if selectStmt.SortClause == nil || len(selectStmt.SortClause.Items) == 0 { + t.Fatal("ORDER BY clause not parsed") + } +} + +func TestParseUnionQueries(t *testing.T) { + sql := ` + SELECT id, name FROM users + UNION ALL + SELECT id, name FROM archived_users; + ` + + p := NewParser() + stmts, err := p.Parse(strings.NewReader(sql)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(stmts) != 1 { + t.Fatalf("Expected 1 statement, got %d", len(stmts)) + } + + selectStmt, ok := stmts[0].Raw.Stmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", stmts[0].Raw.Stmt) + } + + if selectStmt.Op != ast.Union { + t.Fatalf("Expected UNION operation, got %v", selectStmt.Op) + } + + if !selectStmt.All { + t.Fatal("Expected UNION ALL (All=true)") + } +} + +func TestParseSubquery(t *testing.T) { + sql := ` + SELECT * FROM ( + SELECT id, name FROM users WHERE id > 100 + ) as filtered_users; + ` + + p := NewParser() + stmts, err := p.Parse(strings.NewReader(sql)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(stmts) != 1 { + t.Fatalf("Expected 1 statement, got %d", len(stmts)) + } + + selectStmt, ok := stmts[0].Raw.Stmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", stmts[0].Raw.Stmt) + } + + if selectStmt.FromClause == nil || len(selectStmt.FromClause.Items) == 0 { + t.Fatal("FROM clause not parsed") + } +} + +func TestParseIsNullExpressions(t *testing.T) { + sql := `SELECT * FROM users WHERE name IS NULL AND email IS NOT NULL;` + + p := NewParser() + stmts, err := p.Parse(strings.NewReader(sql)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(stmts) != 1 { + t.Fatalf("Expected 1 statement, got %d", len(stmts)) + } + + selectStmt, ok := stmts[0].Raw.Stmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", stmts[0].Raw.Stmt) + } + + if selectStmt.WhereClause == nil { + t.Fatal("WHERE clause not parsed") + } +} + +func TestParseMultipleJoins(t *testing.T) { + sql := ` + SELECT + u.id, u.name, p.title, c.content + FROM users u + INNER JOIN posts p ON u.id = p.user_id + LEFT JOIN comments c ON p.id = c.post_id + WHERE u.active = 1 + ORDER BY p.created_at DESC; + ` + + p := NewParser() + stmts, err := p.Parse(strings.NewReader(sql)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(stmts) != 1 { + t.Fatalf("Expected 1 statement, got %d", len(stmts)) + } + + selectStmt, ok := stmts[0].Raw.Stmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", stmts[0].Raw.Stmt) + } + + if selectStmt.FromClause == nil || len(selectStmt.FromClause.Items) == 0 { + t.Fatal("FROM clause not parsed") + } +} + +// TestParseClickHousePrewhere tests PREWHERE support +// PREWHERE is a ClickHouse optimization - executes before WHERE for better performance +func TestParseClickHousePrewhere(t *testing.T) { + sql := `SELECT * FROM events PREWHERE event_type = 'click' WHERE user_id = 123;` + + p := NewParser() + stmts, err := p.Parse(strings.NewReader(sql)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(stmts) != 1 { + t.Fatalf("Expected 1 statement, got %d", len(stmts)) + } + + // If PREWHERE is parsed as a separate clause, it would be in a different field + // For now, it might be part of WHERE or treated as a TODO + selectStmt, ok := stmts[0].Raw.Stmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", stmts[0].Raw.Stmt) + } + + // Just verify parsing succeeded + if selectStmt.FromClause == nil { + t.Fatal("FROM clause not parsed") + } +} + +// TestParseClickHouseSample tests SAMPLE support +// SAMPLE is a ClickHouse optimization to read only a portion of data +func TestParseClickHouseSample(t *testing.T) { + sql := `SELECT * FROM events SAMPLE 1/10 WHERE created_at > now() - INTERVAL 1 DAY;` + + p := NewParser() + stmts, err := p.Parse(strings.NewReader(sql)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(stmts) != 1 { + t.Fatalf("Expected 1 statement, got %d", len(stmts)) + } + + selectStmt, ok := stmts[0].Raw.Stmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", stmts[0].Raw.Stmt) + } + + if selectStmt.FromClause == nil { + t.Fatal("FROM clause not parsed") + } +} + +// TestParseClickHouseArrayFunctions tests array function support +// ClickHouse has built-in array operations +func TestParseClickHouseArrayFunctions(t *testing.T) { + sql := `SELECT arrayLength(tags) as tag_count FROM articles WHERE arrayExists(x -> x > 5, scores);` + + p := NewParser() + stmts, err := p.Parse(strings.NewReader(sql)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(stmts) != 1 { + t.Fatalf("Expected 1 statement, got %d", len(stmts)) + } + + selectStmt, ok := stmts[0].Raw.Stmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", stmts[0].Raw.Stmt) + } + + if selectStmt.TargetList == nil || len(selectStmt.TargetList.Items) == 0 { + t.Fatal("SELECT list not parsed") + } +} + +// TestParseStringOperations tests string operations and functions +func TestParseStringOperations(t *testing.T) { + sql := ` + SELECT + concat(first_name, ' ', last_name) as full_name, + length(email) as email_length, + upper(name) as name_upper + FROM users + WHERE email LIKE '%@example.com'; + ` + + p := NewParser() + stmts, err := p.Parse(strings.NewReader(sql)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(stmts) != 1 { + t.Fatalf("Expected 1 statement, got %d", len(stmts)) + } + + selectStmt, ok := stmts[0].Raw.Stmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", stmts[0].Raw.Stmt) + } + + if selectStmt.TargetList == nil || len(selectStmt.TargetList.Items) < 3 { + t.Fatalf("Expected 3+ targets, got %d", len(selectStmt.TargetList.Items)) + } +} + +// TestParsePositionalParameter tests positional parameters (?) +func TestParsePositionalParameter(t *testing.T) { + sql := "SELECT * FROM users WHERE id = ? AND name = ?;" + + p := NewParser() + stmts, err := p.Parse(strings.NewReader(sql)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(stmts) != 1 { + t.Fatalf("Expected 1 statement, got %d", len(stmts)) + } + + selectStmt, ok := stmts[0].Raw.Stmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", stmts[0].Raw.Stmt) + } + + if selectStmt.WhereClause == nil { + t.Fatal("WHERE clause not parsed") + } + + // Check that we have at least one ParamRef in the WHERE clause + paramRefs := findParamRefs(selectStmt.WhereClause) + if len(paramRefs) == 0 { + t.Fatal("Expected ParamRef nodes in WHERE clause") + } +} + +// Helper to find all ParamRef nodes in an AST +func findParamRefs(node ast.Node) []*ast.ParamRef { + var refs []*ast.ParamRef + var walkNode func(ast.Node) + walkNode = func(n ast.Node) { + if pr, ok := n.(*ast.ParamRef); ok { + refs = append(refs, pr) + } + switch v := n.(type) { + case *ast.A_Expr: + if v.Lexpr != nil { + walkNode(v.Lexpr) + } + if v.Rexpr != nil { + walkNode(v.Rexpr) + } + case *ast.List: + if v != nil { + for _, item := range v.Items { + walkNode(item) + } + } + } + } + walkNode(node) + return refs +} + +// findSqlcFunctionCalls finds all sqlc.* function calls with the given function name +func findSqlcFunctionCalls(node ast.Node, funcName string) []*ast.FuncCall { + var calls []*ast.FuncCall + var walkNode func(ast.Node) + walkNode = func(n ast.Node) { + if fc, ok := n.(*ast.FuncCall); ok { + // Check if this is a sqlc.* function call + if fc.Func != nil && fc.Func.Schema == "sqlc" && fc.Func.Name == funcName { + calls = append(calls, fc) + } + } + switch v := n.(type) { + case *ast.A_Expr: + if v.Lexpr != nil { + walkNode(v.Lexpr) + } + if v.Rexpr != nil { + walkNode(v.Rexpr) + } + case *ast.List: + if v != nil { + for _, item := range v.Items { + walkNode(item) + } + } + } + } + walkNode(node) + return calls +} + +// TestInsertIntoSelect tests INSERT INTO ... SELECT +func TestInsertIntoSelect(t *testing.T) { + sql := ` + INSERT INTO analytics.summary (date, count) + SELECT toDate(timestamp) as date, COUNT(*) as count + FROM events + GROUP BY toDate(timestamp); + ` + + p := NewParser() + stmts, err := p.Parse(strings.NewReader(sql)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(stmts) != 1 { + t.Fatalf("Expected 1 statement, got %d", len(stmts)) + } + + insertStmt, ok := stmts[0].Raw.Stmt.(*ast.InsertStmt) + if !ok { + t.Fatalf("Expected InsertStmt, got %T", stmts[0].Raw.Stmt) + } + + if insertStmt.Relation == nil { + t.Fatal("INSERT target table not parsed") + } + + if insertStmt.SelectStmt == nil { + t.Fatal("INSERT SELECT statement not parsed") + } +} + +// TestParseDistinct tests DISTINCT clause +func TestParseDistinct(t *testing.T) { + sql := `SELECT DISTINCT country FROM users ORDER BY country;` + + p := NewParser() + stmts, err := p.Parse(strings.NewReader(sql)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(stmts) != 1 { + t.Fatalf("Expected 1 statement, got %d", len(stmts)) + } + + selectStmt, ok := stmts[0].Raw.Stmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", stmts[0].Raw.Stmt) + } + + if selectStmt.DistinctClause == nil { + t.Fatal("DISTINCT clause not parsed") + } +} + +// TestParseWithCTE tests Common Table Expressions (CTEs) +func TestParseWithCTE(t *testing.T) { + sql := ` + WITH recent_events AS ( + SELECT id, user_id, event_type, timestamp + FROM events + WHERE timestamp > now() - INTERVAL 7 DAY + ) + SELECT user_id, COUNT(*) as event_count + FROM recent_events + GROUP BY user_id; + ` + + p := NewParser() + stmts, err := p.Parse(strings.NewReader(sql)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(stmts) != 1 { + t.Fatalf("Expected 1 statement, got %d", len(stmts)) + } + + selectStmt, ok := stmts[0].Raw.Stmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", stmts[0].Raw.Stmt) + } + + if selectStmt.WithClause == nil { + t.Fatal("WITH clause not parsed") + } +} + +// TestParseNamedParameterSqlcArg tests sqlc.arg() function syntax +// sqlc.arg() is converted to sqlc_arg() during preprocessing, then converted +// back to sqlc.arg in the AST with proper schema/function name separation +func TestParseNamedParameterSqlcArg(t *testing.T) { + sql := "SELECT * FROM users WHERE id = sqlc.arg('user_id') AND name = sqlc.arg('name');" + + p := NewParser() + stmts, err := p.Parse(strings.NewReader(sql)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(stmts) != 1 { + t.Fatalf("Expected 1 statement, got %d", len(stmts)) + } + + selectStmt, ok := stmts[0].Raw.Stmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", stmts[0].Raw.Stmt) + } + + if selectStmt.WhereClause == nil { + t.Fatal("WHERE clause not parsed") + } + + // Should find sqlc.arg() function calls in the WHERE clause + funcCalls := findSqlcFunctionCalls(selectStmt.WhereClause, "arg") + if len(funcCalls) != 2 { + t.Fatalf("Expected 2 sqlc.arg() calls, found %d", len(funcCalls)) + } +} + +// TestParseNamedParameterSqlcNarg tests sqlc.narg() function syntax +// sqlc.narg() is converted to sqlc_narg() during preprocessing, then converted +// back to sqlc.narg in the AST with proper schema/function name separation +func TestParseNamedParameterSqlcNarg(t *testing.T) { + sql := "SELECT * FROM users WHERE status = sqlc.narg('optional_status');" + + p := NewParser() + stmts, err := p.Parse(strings.NewReader(sql)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(stmts) != 1 { + t.Fatalf("Expected 1 statement, got %d", len(stmts)) + } + + selectStmt, ok := stmts[0].Raw.Stmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", stmts[0].Raw.Stmt) + } + + if selectStmt.WhereClause == nil { + t.Fatal("WHERE clause not parsed") + } + + // Should find sqlc.narg() function call + funcCalls := findSqlcFunctionCalls(selectStmt.WhereClause, "narg") + if len(funcCalls) != 1 { + t.Fatalf("Expected 1 sqlc.narg() call, found %d", len(funcCalls)) + } +} + +// TestParseNamedParameterSqlcSlice tests sqlc.slice() function syntax +// sqlc.slice() is converted to sqlc_slice() during preprocessing, then converted +// back to sqlc.slice in the AST with proper schema/function name separation +func TestParseNamedParameterSqlcSlice(t *testing.T) { + sql := "SELECT * FROM users WHERE status IN sqlc.slice('statuses');" + + p := NewParser() + stmts, err := p.Parse(strings.NewReader(sql)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(stmts) != 1 { + t.Fatalf("Expected 1 statement, got %d", len(stmts)) + } + + selectStmt, ok := stmts[0].Raw.Stmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", stmts[0].Raw.Stmt) + } + + if selectStmt.WhereClause == nil { + t.Fatal("WHERE clause not parsed") + } + + // Should find sqlc.slice() function call + funcCalls := findSqlcFunctionCalls(selectStmt.WhereClause, "slice") + if len(funcCalls) != 1 { + t.Fatalf("Expected 1 sqlc.slice() call, found %d", len(funcCalls)) + } +} + +// TestParseNamedParameterMultipleFunctions tests using multiple sqlc.* functions +func TestParseNamedParameterMultipleFunctions(t *testing.T) { + sql := ` + SELECT u.id, u.name, p.title + FROM users u + LEFT JOIN posts p ON u.id = p.user_id + WHERE u.id = sqlc.arg('user_id') AND u.status = sqlc.narg('status') + AND p.category IN sqlc.slice('categories') + ORDER BY p.created_at DESC; + ` + + p := NewParser() + stmts, err := p.Parse(strings.NewReader(sql)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(stmts) != 1 { + t.Fatalf("Expected 1 statement, got %d", len(stmts)) + } + + selectStmt, ok := stmts[0].Raw.Stmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", stmts[0].Raw.Stmt) + } + + if selectStmt.WhereClause == nil { + t.Fatal("WHERE clause not parsed") + } + + // Should find all three types of sqlc functions + argCalls := findSqlcFunctionCalls(selectStmt.WhereClause, "arg") + nargCalls := findSqlcFunctionCalls(selectStmt.WhereClause, "narg") + sliceCalls := findSqlcFunctionCalls(selectStmt.WhereClause, "slice") + + if len(argCalls) != 1 { + t.Fatalf("Expected 1 sqlc.arg() call, found %d", len(argCalls)) + } + if len(nargCalls) != 1 { + t.Fatalf("Expected 1 sqlc.narg() call, found %d", len(nargCalls)) + } + if len(sliceCalls) != 1 { + t.Fatalf("Expected 1 sqlc.slice() call, found %d", len(sliceCalls)) + } +} + +// TestParseShow tests SHOW statements +func TestParseShow(t *testing.T) { + sql := "SHOW TABLES;" + + p := NewParser() + stmts, err := p.Parse(strings.NewReader(sql)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(stmts) != 1 { + t.Fatalf("Expected 1 statement, got %d", len(stmts)) + } + + // SHOW statements return TODO as they're introspection queries + if stmts[0].Raw == nil || stmts[0].Raw.Stmt == nil { + t.Fatal("Statement or Raw.Stmt is nil") + } +} + +// TestParseTruncate tests TRUNCATE statements +func TestParseTruncate(t *testing.T) { + sql := "TRUNCATE TABLE users;" + + p := NewParser() + stmts, err := p.Parse(strings.NewReader(sql)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(stmts) != 1 { + t.Fatalf("Expected 1 statement, got %d", len(stmts)) + } + + // TRUNCATE statements return TODO as they're maintenance operations + if stmts[0].Raw == nil || stmts[0].Raw.Stmt == nil { + t.Fatal("Statement or Raw.Stmt is nil") + } +} + +// TestPreprocessNamedParameters tests the preprocessing function directly +// Preprocessing converts sqlc.* to sqlc_* (same length) so ClickHouse parser recognizes them as functions +func TestPreprocessNamedParameters(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "sqlc.arg with single quotes", + input: "WHERE id = sqlc.arg('user_id')", + expected: "WHERE id = sqlc_arg('user_id')", + }, + { + name: "sqlc.arg with double quotes", + input: `WHERE id = sqlc.arg("user_id")`, + expected: `WHERE id = sqlc_arg("user_id")`, + }, + { + name: "sqlc.narg", + input: "WHERE status = sqlc.narg('status')", + expected: "WHERE status = sqlc_narg('status')", + }, + { + name: "sqlc.slice", + input: "WHERE id IN sqlc.slice('ids')", + expected: "WHERE id IN sqlc_slice('ids')", + }, + { + name: "Multiple sqlc functions", + input: "WHERE id = sqlc.arg('id') AND status = sqlc.narg('status')", + expected: "WHERE id = sqlc_arg('id') AND status = sqlc_narg('status')", + }, + { + name: "With whitespace", + input: "WHERE id = sqlc.arg ( 'user_id' )", + expected: "WHERE id = sqlc_arg ( 'user_id' )", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result := preprocessNamedParameters(test.input) + if result != test.expected { + t.Errorf("Expected %q, got %q", test.expected, result) + } + }) + } +} + +// TestParseUniqExact tests uniqExact aggregate function +func TestParseUniqExact(t *testing.T) { + sql := ` + SELECT + user_id, + uniqExact(event_id) as unique_events + FROM events + GROUP BY user_id; + ` + + p := NewParser() + stmts, err := p.Parse(strings.NewReader(sql)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(stmts) != 1 { + t.Fatalf("Expected 1 statement, got %d", len(stmts)) + } + + selectStmt, ok := stmts[0].Raw.Stmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", stmts[0].Raw.Stmt) + } + + if selectStmt.TargetList == nil || len(selectStmt.TargetList.Items) < 2 { + t.Fatalf("Expected at least 2 targets, got %d", len(selectStmt.TargetList.Items)) + } + + // Check that the uniqExact function is in the target list + hasUniqExact := false + for _, item := range selectStmt.TargetList.Items { + if target, ok := item.(*ast.ResTarget); ok { + if funcCall, ok := target.Val.(*ast.FuncCall); ok { + if funcCall.Func.Name == "uniqexact" { + hasUniqExact = true + break + } + } + } + } + + if !hasUniqExact { + t.Fatal("Expected uniqExact function in target list") + } +} + +// TestParseUniqExactIf tests uniqExactIf conditional aggregate function +func TestParseUniqExactIf(t *testing.T) { + sql := ` + SELECT + user_id, + uniqExactIf(event_id, event_type = 'click') as unique_clicks + FROM events + GROUP BY user_id; + ` + + p := NewParser() + stmts, err := p.Parse(strings.NewReader(sql)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(stmts) != 1 { + t.Fatalf("Expected 1 statement, got %d", len(stmts)) + } + + selectStmt, ok := stmts[0].Raw.Stmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", stmts[0].Raw.Stmt) + } + + if selectStmt.TargetList == nil || len(selectStmt.TargetList.Items) < 2 { + t.Fatalf("Expected at least 2 targets, got %d", len(selectStmt.TargetList.Items)) + } + + // Check that the uniqExactIf function is in the target list with 2 arguments + hasUniqExactIf := false + for _, item := range selectStmt.TargetList.Items { + if target, ok := item.(*ast.ResTarget); ok { + if funcCall, ok := target.Val.(*ast.FuncCall); ok { + if funcCall.Func.Name == "uniqexactif" && len(funcCall.Args.Items) == 2 { + hasUniqExactIf = true + break + } + } + } + } + + if !hasUniqExactIf { + t.Fatal("Expected uniqExactIf function with 2 arguments in target list") + } +} + +// TestParseArgMax tests argMax aggregate function +func TestParseArgMax(t *testing.T) { + sql := ` + SELECT + user_id, + argMax(event_name, timestamp) as latest_event + FROM events + GROUP BY user_id; + ` + + p := NewParser() + stmts, err := p.Parse(strings.NewReader(sql)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(stmts) != 1 { + t.Fatalf("Expected 1 statement, got %d", len(stmts)) + } + + selectStmt, ok := stmts[0].Raw.Stmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", stmts[0].Raw.Stmt) + } + + if selectStmt.TargetList == nil || len(selectStmt.TargetList.Items) < 2 { + t.Fatalf("Expected at least 2 targets, got %d", len(selectStmt.TargetList.Items)) + } + + // Check that argMax function with 2 arguments is present + hasArgMax := false + for _, item := range selectStmt.TargetList.Items { + if target, ok := item.(*ast.ResTarget); ok { + if funcCall, ok := target.Val.(*ast.FuncCall); ok { + if funcCall.Func.Name == "argmax" && len(funcCall.Args.Items) == 2 { + hasArgMax = true + break + } + } + } + } + + if !hasArgMax { + t.Fatal("Expected argMax function with 2 arguments in target list") + } +} + +// TestParseArgMaxIf tests argMaxIf conditional aggregate function +func TestParseArgMaxIf(t *testing.T) { + sql := ` + SELECT + user_id, + argMaxIf(event_name, timestamp, event_type = 'purchase') as latest_purchase + FROM events + GROUP BY user_id; + ` + + p := NewParser() + stmts, err := p.Parse(strings.NewReader(sql)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(stmts) != 1 { + t.Fatalf("Expected 1 statement, got %d", len(stmts)) + } + + selectStmt, ok := stmts[0].Raw.Stmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", stmts[0].Raw.Stmt) + } + + if selectStmt.TargetList == nil || len(selectStmt.TargetList.Items) < 2 { + t.Fatalf("Expected at least 2 targets, got %d", len(selectStmt.TargetList.Items)) + } + + // Check that argMaxIf function with 3 arguments is present + hasArgMaxIf := false + for _, item := range selectStmt.TargetList.Items { + if target, ok := item.(*ast.ResTarget); ok { + if funcCall, ok := target.Val.(*ast.FuncCall); ok { + if funcCall.Func.Name == "argmaxif" && len(funcCall.Args.Items) == 3 { + hasArgMaxIf = true + break + } + } + } + } + + if !hasArgMaxIf { + t.Fatal("Expected argMaxIf function with 3 arguments in target list") + } +} + +// TestParseCountIf tests countIf conditional aggregate function +func TestParseCountIf(t *testing.T) { + sql := ` + SELECT + user_id, + count() as total_events, + countIf(event_type = 'click') as click_count, + countIf(event_type = 'view') as view_count + FROM events + GROUP BY user_id; + ` + + p := NewParser() + stmts, err := p.Parse(strings.NewReader(sql)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(stmts) != 1 { + t.Fatalf("Expected 1 statement, got %d", len(stmts)) + } + + selectStmt, ok := stmts[0].Raw.Stmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", stmts[0].Raw.Stmt) + } + + if selectStmt.TargetList == nil || len(selectStmt.TargetList.Items) < 4 { + t.Fatalf("Expected at least 4 targets, got %d", len(selectStmt.TargetList.Items)) + } + + // Count countIf functions + countIfCount := 0 + for _, item := range selectStmt.TargetList.Items { + if target, ok := item.(*ast.ResTarget); ok { + if funcCall, ok := target.Val.(*ast.FuncCall); ok { + if funcCall.Func.Name == "countif" { + countIfCount++ + } + } + } + } + + if countIfCount != 2 { + t.Errorf("Expected 2 countIf functions, got %d", countIfCount) + } +} + +// TestParseMultipleAggregatesFunctions tests multiple aggregate functions together +func TestParseMultipleAggregateFunctions(t *testing.T) { + sql := ` + SELECT + category, + COUNT(*) as count, + SUM(amount) as total, + AVG(amount) as average, + MIN(amount) as min_amount, + MAX(amount) as max_amount, + uniqExact(customer_id) as unique_customers, + countIf(status = 'completed') as completed_orders, + argMax(product_name, amount) as top_product + FROM orders + WHERE created_at >= sqlc.arg('start_date') + GROUP BY category + HAVING COUNT(*) > sqlc.arg('min_orders') + ORDER BY total DESC; + ` + + p := NewParser() + stmts, err := p.Parse(strings.NewReader(sql)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(stmts) != 1 { + t.Fatalf("Expected 1 statement, got %d", len(stmts)) + } + + selectStmt, ok := stmts[0].Raw.Stmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", stmts[0].Raw.Stmt) + } + + if selectStmt.TargetList == nil || len(selectStmt.TargetList.Items) < 9 { + t.Fatalf("Expected at least 9 targets, got %d", len(selectStmt.TargetList.Items)) + } + + // Verify all expected functions are present + functionNames := make(map[string]int) + for _, item := range selectStmt.TargetList.Items { + if target, ok := item.(*ast.ResTarget); ok { + if funcCall, ok := target.Val.(*ast.FuncCall); ok { + functionNames[funcCall.Func.Name]++ + } + } + } + + expectedFunctions := map[string]int{ + "count": 1, + "sum": 1, + "avg": 1, + "min": 1, + "max": 1, + "uniqexact": 1, + "countif": 1, + "argmax": 1, + } + + for funcName, expectedCount := range expectedFunctions { + if count, ok := functionNames[funcName]; !ok || count < expectedCount { + t.Errorf("Expected function %s with count >= %d, got %d", funcName, expectedCount, count) + } + } + + // Verify WHERE clause with named parameters + if selectStmt.WhereClause == nil { + t.Fatal("Expected WHERE clause") + } + + // Verify GROUP BY + if selectStmt.GroupClause == nil { + t.Fatal("Expected GROUP BY clause") + } + + // Verify HAVING + if selectStmt.HavingClause == nil { + t.Fatal("Expected HAVING clause") + } + + // Verify ORDER BY + if selectStmt.SortClause == nil { + t.Fatal("Expected ORDER BY clause") + } +} + +// TestParseAggregatesWithWindow tests mixing aggregate and window functions +func TestParseAggregatesWithWindow(t *testing.T) { + sql := ` + SELECT + user_id, + event_count, + ROW_NUMBER() OVER (ORDER BY event_count DESC) as rank, + uniqExact(session_id) as unique_sessions, + SUM(event_count) OVER (PARTITION BY user_type ORDER BY event_count) as running_total + FROM ( + SELECT + user_id, + user_type, + COUNT(*) as event_count, + countIf(event_type = 'purchase') as purchase_count + FROM events + GROUP BY user_id, user_type + ) + ORDER BY event_count DESC; + ` + + p := NewParser() + stmts, err := p.Parse(strings.NewReader(sql)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(stmts) != 1 { + t.Fatalf("Expected 1 statement, got %d", len(stmts)) + } + + selectStmt, ok := stmts[0].Raw.Stmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", stmts[0].Raw.Stmt) + } + + // Verify main target list + if selectStmt.TargetList == nil || len(selectStmt.TargetList.Items) < 5 { + t.Fatalf("Expected at least 5 targets, got %d", len(selectStmt.TargetList.Items)) + } + + // Check for window functions + hasWindowFunc := false + for _, item := range selectStmt.TargetList.Items { + if target, ok := item.(*ast.ResTarget); ok { + if funcCall, ok := target.Val.(*ast.FuncCall); ok { + if funcCall.Over != nil { + hasWindowFunc = true + break + } + } + } + } + + if !hasWindowFunc { + t.Fatal("Expected at least one window function (OVER clause)") + } + + // Verify FROM clause is a subquery + if selectStmt.FromClause == nil || len(selectStmt.FromClause.Items) == 0 { + t.Fatal("Expected FROM clause with subquery") + } +} + +// TestParseArgMinArgMax tests both argMin and argMax together +func TestParseArgMinArgMax(t *testing.T) { + sql := ` + SELECT + product_id, + argMin(price, timestamp) as min_price_time, + argMax(price, timestamp) as max_price_time, + argMinIf(price, timestamp, status = 'active') as min_active_price, + argMaxIf(price, timestamp, status = 'active') as max_active_price + FROM price_history + GROUP BY product_id; + ` + + p := NewParser() + stmts, err := p.Parse(strings.NewReader(sql)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(stmts) != 1 { + t.Fatalf("Expected 1 statement, got %d", len(stmts)) + } + + selectStmt, ok := stmts[0].Raw.Stmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", stmts[0].Raw.Stmt) + } + + if selectStmt.TargetList == nil || len(selectStmt.TargetList.Items) < 5 { + t.Fatalf("Expected at least 5 targets, got %d", len(selectStmt.TargetList.Items)) + } + + // Count each function type + functionCounts := make(map[string]int) + for _, item := range selectStmt.TargetList.Items { + if target, ok := item.(*ast.ResTarget); ok { + if funcCall, ok := target.Val.(*ast.FuncCall); ok { + functionCounts[funcCall.Func.Name]++ + } + } + } + + expectedFunctions := []string{"argmin", "argmax", "argminif", "argmaxif"} + for _, funcName := range expectedFunctions { + if count, ok := functionCounts[funcName]; !ok || count == 0 { + t.Errorf("Expected function %s to be present", funcName) + } + } +} + +// TestParseUniqWithModifiers tests uniq functions with different modifiers +func TestParseUniqWithModifiers(t *testing.T) { + sql := ` + SELECT + date, + uniq(user_id) as unique_users, + uniqIf(user_id, user_type = 'premium') as premium_users, + uniqHLL12(user_id) as approx_unique_users, + uniqExact(user_id) as exact_unique_users + FROM events + WHERE date >= sqlc.arg('start_date') AND date <= sqlc.arg('end_date') + GROUP BY date + ORDER BY date; + ` + + p := NewParser() + stmts, err := p.Parse(strings.NewReader(sql)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(stmts) != 1 { + t.Fatalf("Expected 1 statement, got %d", len(stmts)) + } + + selectStmt, ok := stmts[0].Raw.Stmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", stmts[0].Raw.Stmt) + } + + if selectStmt.TargetList == nil || len(selectStmt.TargetList.Items) < 5 { + t.Fatalf("Expected at least 5 targets, got %d", len(selectStmt.TargetList.Items)) + } + + // Count uniq variants + uniqVariants := make(map[string]int) + for _, item := range selectStmt.TargetList.Items { + if target, ok := item.(*ast.ResTarget); ok { + if funcCall, ok := target.Val.(*ast.FuncCall); ok { + if strings.HasPrefix(strings.ToLower(funcCall.Func.Name), "uniq") { + uniqVariants[funcCall.Func.Name]++ + } + } + } + } + + expectedVariants := []string{"uniq", "uniqif", "uniqhll12", "uniqexact"} + for _, variant := range expectedVariants { + if count, ok := uniqVariants[variant]; !ok || count == 0 { + t.Errorf("Expected uniq variant %s to be present", variant) + } + } +} + +// TestParseStatisticalAggregates tests statistical aggregate functions +func TestParseStatisticalAggregates(t *testing.T) { + sql := ` + SELECT + varSamp(value) as variance_sample, + varPop(value) as variance_population, + stddevSamp(value) as stddev_sample, + stddevPop(value) as stddev_population, + covarSamp(x, y) as covariance_sample, + covarPop(x, y) as covariance_population, + corr(x, y) as correlation + FROM metrics; + ` + + p := NewParser() + stmts, err := p.Parse(strings.NewReader(sql)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(stmts) != 1 { + t.Fatalf("Expected 1 statement, got %d", len(stmts)) + } + + selectStmt, ok := stmts[0].Raw.Stmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", stmts[0].Raw.Stmt) + } + + if selectStmt.TargetList == nil || len(selectStmt.TargetList.Items) < 7 { + t.Fatalf("Expected at least 7 targets, got %d", len(selectStmt.TargetList.Items)) + } + + // Count statistical functions + statFunctions := make(map[string]int) + for _, item := range selectStmt.TargetList.Items { + if target, ok := item.(*ast.ResTarget); ok { + if funcCall, ok := target.Val.(*ast.FuncCall); ok { + funcName := strings.ToLower(funcCall.Func.Name) + statFunctions[funcName]++ + } + } + } + + expectedFunctions := map[string]int{ + "varsamp": 1, + "varpop": 1, + "stddevsamp": 1, + "stddevpop": 1, + "covarsamp": 1, + "covarpop": 1, + "corr": 1, + } + + for funcName, expectedCount := range expectedFunctions { + if count, ok := statFunctions[funcName]; !ok || count < expectedCount { + t.Errorf("Expected function %s with count >= %d, got %d", funcName, expectedCount, count) + } + } +} + +// TestParseConditionalAggregatesVariants tests minIf and other conditional variants +func TestParseConditionalAggregatesVariants(t *testing.T) { + sql := ` + SELECT + minIf(price, status = 'active') as min_active_price, + maxIf(price, status = 'active') as max_active_price, + sumIf(amount, quantity > 0) as positive_amount, + avgIf(value, value IS NOT NULL) as avg_non_null + FROM orders + GROUP BY category; + ` + + p := NewParser() + stmts, err := p.Parse(strings.NewReader(sql)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(stmts) != 1 { + t.Fatalf("Expected 1 statement, got %d", len(stmts)) + } + + selectStmt, ok := stmts[0].Raw.Stmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", stmts[0].Raw.Stmt) + } + + if selectStmt.TargetList == nil || len(selectStmt.TargetList.Items) < 4 { + t.Fatalf("Expected at least 4 targets, got %d", len(selectStmt.TargetList.Items)) + } + + // Verify conditional aggregates are present + conditionalFunctions := make(map[string]int) + for _, item := range selectStmt.TargetList.Items { + if target, ok := item.(*ast.ResTarget); ok { + if funcCall, ok := target.Val.(*ast.FuncCall); ok { + funcName := strings.ToLower(funcCall.Func.Name) + if strings.HasSuffix(funcName, "if") { + conditionalFunctions[funcName]++ + } + } + } + } + + expectedConditionals := []string{"minif", "maxif", "sumif", "avgif"} + for _, funcName := range expectedConditionals { + if count, ok := conditionalFunctions[funcName]; !ok || count == 0 { + t.Errorf("Expected function %s to be present", funcName) + } + } +} + +// TestParseInOperator tests IN operator with value lists +func TestParseInOperator(t *testing.T) { + sql := ` + SELECT id, name, status + FROM users + WHERE id IN (1, 2, 3, 4, 5) + AND status IN ('active', 'pending') + ORDER BY id; + ` + + p := NewParser() + stmts, err := p.Parse(strings.NewReader(sql)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(stmts) != 1 { + t.Fatalf("Expected 1 statement, got %d", len(stmts)) + } + + selectStmt, ok := stmts[0].Raw.Stmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", stmts[0].Raw.Stmt) + } + + // Verify WHERE clause exists + if selectStmt.WhereClause == nil { + t.Fatal("Expected WHERE clause") + } + + // Verify ORDER BY exists + if selectStmt.SortClause == nil { + t.Fatal("Expected ORDER BY clause") + } +} + +// TestParseTOPClause tests TOP clause (ClickHouse LIMIT alternative) +func TestParseTOPClause(t *testing.T) { + sql := "SELECT TOP 10 my_column FROM tableName;" + + p := NewParser() + stmts, err := p.Parse(strings.NewReader(sql)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(stmts) != 1 { + t.Fatalf("Expected 1 statement, got %d", len(stmts)) + } + + selectStmt, ok := stmts[0].Raw.Stmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", stmts[0].Raw.Stmt) + } + + // TOP clause is a valid ClickHouse syntax - parser should handle it + // It may be stored in TargetList or as a special node depending on parser + if selectStmt.TargetList == nil || len(selectStmt.TargetList.Items) == 0 { + t.Fatal("Expected target list") + } +} + +// TestParseOrderByWithFill tests ORDER BY ... WITH FILL time series feature +func TestParseOrderByWithFill(t *testing.T) { + tests := []struct { + name string + sql string + }{ + { + name: "Basic WITH FILL", + sql: ` + SELECT n FROM data + ORDER BY n WITH FILL; + `, + }, + { + name: "WITH FILL FROM TO", + sql: ` + SELECT date, value FROM timeseries + ORDER BY date WITH FILL FROM '2024-01-01' TO '2024-01-10'; + `, + }, + { + name: "WITH FILL numeric STEP", + sql: ` + SELECT day, metric FROM series + ORDER BY day WITH FILL STEP 1; + `, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := NewParser() + stmts, err := p.Parse(strings.NewReader(tt.sql)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(stmts) != 1 { + t.Fatalf("Expected 1 statement, got %d", len(stmts)) + } + + selectStmt, ok := stmts[0].Raw.Stmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", stmts[0].Raw.Stmt) + } + + // Verify ORDER BY clause exists (WITH FILL is part of ORDER BY) + if selectStmt.SortClause == nil { + t.Fatal("Expected ORDER BY clause with FILL") + } + }) + } +} + +// TestParseTypeCastOperator tests :: operator for type casting +func TestParseTypeCastOperator(t *testing.T) { + sql := ` + SELECT + timestamp_col::DateTime, + amount::Float32, + id::String, + flag::Boolean + FROM data + WHERE created_at::Date >= '2024-01-01'; + ` + + p := NewParser() + stmts, err := p.Parse(strings.NewReader(sql)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(stmts) != 1 { + t.Fatalf("Expected 1 statement, got %d", len(stmts)) + } + + selectStmt, ok := stmts[0].Raw.Stmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", stmts[0].Raw.Stmt) + } + + // Verify target list + if selectStmt.TargetList == nil || len(selectStmt.TargetList.Items) < 4 { + t.Fatalf("Expected at least 4 targets, got %d", + len(selectStmt.TargetList.Items)) + } + + // Verify WHERE clause + if selectStmt.WhereClause == nil { + t.Fatal("Expected WHERE clause") + } +} + +// TestParseArrayJoin tests ARRAY JOIN clause for unfolding arrays into rows +func TestParseArrayJoin(t *testing.T) { + tests := []struct { + name string + sql string + expectJoinType string + }{ + { + name: "Basic ARRAY JOIN", + sql: ` + SELECT id, tag + FROM users + ARRAY JOIN tags AS tag; + `, + expectJoinType: "", + }, + // Note: LEFT ARRAY JOIN is not properly supported by clickhouse-sql-parser v0.4.16 + // The parser returns nil for ArrayJoin when LEFT is specified + // This is a known limitation of the parser library + /* + { + name: "LEFT ARRAY JOIN", + sql: ` + SELECT id, tag + FROM users + LEFT ARRAY JOIN tags AS tag; + `, + expectJoinType: "LEFT", + }, + */ + { + name: "ARRAY JOIN with WHERE and ORDER BY", + sql: ` + SELECT user_id, param_key, param_value + FROM events + ARRAY JOIN params AS param + WHERE user_id = ? + ORDER BY param_key; + `, + expectJoinType: "", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + p := NewParser() + stmts, err := p.Parse(strings.NewReader(test.sql)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(stmts) != 1 { + t.Fatalf("Expected 1 statement, got %d", len(stmts)) + } + + selectStmt, ok := stmts[0].Raw.Stmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", stmts[0].Raw.Stmt) + } + + // ARRAY JOIN should be integrated into FROM clause + if selectStmt.FromClause == nil || len(selectStmt.FromClause.Items) == 0 { + t.Fatal("Expected FROM clause with ARRAY JOIN") + } + + // Check for RangeSubselect (our representation of ARRAY JOIN) + hasArrayJoin := false + for _, item := range selectStmt.FromClause.Items { + if rangeSubselect, ok := item.(*ast.RangeSubselect); ok { + hasArrayJoin = true + + // Verify the RangeSubselect has a Subquery (synthetic SELECT statement) + if rangeSubselect.Subquery == nil { + t.Error("Expected RangeSubselect to have a Subquery") + continue + } + + syntheticSelect, ok := rangeSubselect.Subquery.(*ast.SelectStmt) + if !ok { + t.Errorf("Expected SelectStmt subquery, got %T", rangeSubselect.Subquery) + continue + } + + if syntheticSelect.TargetList == nil || len(syntheticSelect.TargetList.Items) == 0 { + t.Error("Expected synthetic SELECT to have target list") + } + } + } + + if !hasArrayJoin { + t.Error("Expected ARRAY JOIN to be present in FROM clause") + } + }) + } +} + +// TestParseArrayJoinWithNamedParameters tests ARRAY JOIN with named parameters +func TestParseArrayJoinWithNamedParameters(t *testing.T) { + sql := ` + SELECT + user_id, + event_name, + property_key, + property_value + FROM events + ARRAY JOIN properties AS prop + WHERE user_id = sqlc.arg('user_id') + AND event_date >= sqlc.arg('start_date') + ORDER BY event_time DESC; + ` + + p := NewParser() + stmts, err := p.Parse(strings.NewReader(sql)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(stmts) != 1 { + t.Fatalf("Expected 1 statement, got %d", len(stmts)) + } + + selectStmt, ok := stmts[0].Raw.Stmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", stmts[0].Raw.Stmt) + } + + // Verify ARRAY JOIN in FROM clause + if selectStmt.FromClause == nil || len(selectStmt.FromClause.Items) == 0 { + t.Fatal("Expected FROM clause with ARRAY JOIN") + } + + // Verify WHERE clause exists (contains named parameters) + if selectStmt.WhereClause == nil { + t.Fatal("Expected WHERE clause") + } + + // Verify ORDER BY exists + if selectStmt.SortClause == nil { + t.Fatal("Expected ORDER BY clause") + } +} + +// TestParseArrayJoinMultiple tests ARRAY JOIN with multiple array columns +func TestParseArrayJoinMultiple(t *testing.T) { + sql := ` + SELECT + id, + nested_value + FROM table_with_nested + ARRAY JOIN nested.field1, nested.field2 + WHERE id > 0; + ` + + p := NewParser() + stmts, err := p.Parse(strings.NewReader(sql)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(stmts) != 1 { + t.Fatalf("Expected 1 statement, got %d", len(stmts)) + } + + selectStmt, ok := stmts[0].Raw.Stmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", stmts[0].Raw.Stmt) + } + + // Verify FROM clause contains ARRAY JOIN + if selectStmt.FromClause == nil || len(selectStmt.FromClause.Items) == 0 { + t.Fatal("Expected FROM clause with ARRAY JOIN") + } +} + +// TestParseComplexAggregationWithNamedParams combines statistical functions with named parameters +func TestParseComplexAggregationWithNamedParams(t *testing.T) { + sql := ` + SELECT + date_col, + COUNT(*) as count, + varSamp(metric_value) as variance, + corr(value_x, value_y) as correlation, + countIf(status = 'success') as successes, + maxIf(score, score IS NOT NULL) as max_valid_score + FROM events + WHERE date_col >= sqlc.arg('start_date') AND date_col <= sqlc.arg('end_date') + GROUP BY date_col + HAVING COUNT(*) > sqlc.arg('min_events') + ORDER BY date_col DESC; + ` + + p := NewParser() + stmts, err := p.Parse(strings.NewReader(sql)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(stmts) != 1 { + t.Fatalf("Expected 1 statement, got %d", len(stmts)) + } + + selectStmt, ok := stmts[0].Raw.Stmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", stmts[0].Raw.Stmt) + } + + // Verify all clauses present + if selectStmt.TargetList == nil || len(selectStmt.TargetList.Items) < 6 { + t.Fatalf("Expected at least 6 targets") + } + if selectStmt.WhereClause == nil { + t.Fatal("Expected WHERE clause") + } + if selectStmt.GroupClause == nil { + t.Fatal("Expected GROUP BY clause") + } + if selectStmt.HavingClause == nil { + t.Fatal("Expected HAVING clause") + } + if selectStmt.SortClause == nil { + t.Fatal("Expected ORDER BY clause") + } +} + +// TestParseArrayJoinFunction tests arrayJoin() as a table function in SELECT list +func TestParseArrayJoinFunction(t *testing.T) { + tests := []struct { + name string + sql string + }{ + { + name: "arrayJoin() in SELECT", + sql: ` + SELECT arrayJoin(categories) AS category + FROM products; + `, + }, + { + name: "arrayJoin() with nested function", + sql: ` + SELECT + product_id, + arrayJoin(JSONExtract(metadata, 'Array(String)')) as tag + FROM products + WHERE product_id = ?; + `, + }, + { + name: "arrayJoin() with window function", + sql: ` + SELECT + product_id, + arrayJoin(categories) AS category, + COUNT(*) OVER (PARTITION BY category) as category_count + FROM products + WHERE product_id = ? + GROUP BY product_id, category; + `, + }, + { + name: "arrayJoin() with named parameters", + sql: ` + SELECT + user_id, + arrayJoin(tags) AS tag + FROM users + WHERE user_id = sqlc.arg('user_id') + ORDER BY tag; + `, + }, + { + name: "Multiple columns with arrayJoin()", + sql: ` + SELECT + id, + name, + arrayJoin(items) AS item + FROM orders; + `, + }, + { + name: "arrayJoin() with JSONExtract", + sql: ` + SELECT + MetadataPlatformId, + arrayJoin(JSONExtract(JsonValue, 'Array(String)')) as self_help_id + FROM events; + `, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + p := NewParser() + stmts, err := p.Parse(strings.NewReader(test.sql)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(stmts) != 1 { + t.Fatalf("Expected 1 statement, got %d", len(stmts)) + } + + selectStmt, ok := stmts[0].Raw.Stmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", stmts[0].Raw.Stmt) + } + + // Verify arrayJoin function is in target list + if selectStmt.TargetList == nil || len(selectStmt.TargetList.Items) == 0 { + t.Fatal("Expected at least one target in SELECT list") + } + + // Look for arrayJoin function call in the targets + hasArrayJoinFunc := false + for _, item := range selectStmt.TargetList.Items { + if target, ok := item.(*ast.ResTarget); ok { + if funcCall, ok := target.Val.(*ast.FuncCall); ok { + if funcCall.Func.Name == "arrayjoin" { + hasArrayJoinFunc = true + break + } + } + } + } + + if !hasArrayJoinFunc { + t.Error("Expected arrayJoin() function call in SELECT list") + } + }) + } +} + +func TestLocationIndexing(t *testing.T) { + // Test to verify Location indexing is 0-based or 1-based + sql := "SELECT sqlc_arg('test')" + // Position map: + // 0-indexed: S=0, E=1, L=2, E=3, C=4, T=5, space=6, s=7, q=8, l=9, c=10, _=11, a=12, r=13, g=14, (=15 + + p := NewParser() + stmts, err := p.Parse(strings.NewReader(sql)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(stmts) != 1 { + t.Fatalf("Expected 1 statement, got %d", len(stmts)) + } + + stmt := stmts[0] + if stmt.Raw == nil || stmt.Raw.Stmt == nil { + t.Fatal("Statement or Raw.Stmt is nil") + } + + selectStmt, ok := stmt.Raw.Stmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", stmt.Raw.Stmt) + } + + if selectStmt.TargetList == nil || len(selectStmt.TargetList.Items) == 0 { + t.Fatal("No target items") + } + + // Get the FuncCall node + resTarget := selectStmt.TargetList.Items[0].(*ast.ResTarget) + funcCall, ok := resTarget.Val.(*ast.FuncCall) + if !ok { + t.Fatalf("Expected FuncCall, got %T", resTarget.Val) + } + + // The Location should point to 'sqlc_arg' in the parsed SQL + // In "SELECT sqlc_arg('test')", 's' of 'sqlc_arg' is at 0-indexed position 7 + t.Logf("FuncCall.Location: %d", funcCall.Location) + t.Logf("SQL: \"%s\"", sql) + t.Logf("Expected location: 7 (0-indexed position of 's' in 'sqlc_arg')") + + // Extract substring at that location + if funcCall.Location >= 0 && funcCall.Location < len(sql) { + t.Logf("Character at Location: %c (expecting 's')", sql[funcCall.Location]) + } +} + +// TestImprovedTypeInference verifies that unqualified column references +// in function arguments can be resolved from the catalog +func TestImprovedTypeInference(t *testing.T) { + tests := []struct { + name string + sql string + }{ + { + name: "arrayJoin with unqualified column reference", + sql: `SELECT arrayJoin(categories) AS category FROM products`, + }, + { + name: "argMin with unqualified column reference", + sql: `SELECT argMin(price, id) AS min_price FROM products`, + }, + { + name: "argMax with unqualified column reference", + sql: `SELECT argMax(name, timestamp) AS max_name FROM products`, + }, + { + name: "Array() literal in function", + sql: `SELECT arrayJoin(Array('a', 'b', 'c')) AS element`, + }, + { + name: "CAST expression in function", + sql: `SELECT CAST(price AS String) FROM products`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := NewParser() + stmts, err := p.Parse(strings.NewReader(tt.sql)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(stmts) == 0 { + t.Fatal("Expected at least 1 statement") + } + + // Just verify the query parses without TODO nodes + // The type inference happens during conversion + stmt := stmts[0] + if stmt.Raw.Stmt == nil { + t.Fatal("Expected non-nil statement") + } + }) + } +} + +// TestCountStar verifies that COUNT(*) parses correctly +func TestCountStar(t *testing.T) { + sql := `SELECT COUNT(*) FROM products` + + p := NewParser() + stmts, err := p.Parse(strings.NewReader(sql)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(stmts) == 0 { + t.Fatal("Expected at least 1 statement") + } + + stmt := stmts[0] + if stmt.Raw.Stmt == nil { + t.Fatal("Expected non-nil statement") + } + + selectStmt, ok := stmt.Raw.Stmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", stmt.Raw.Stmt) + } + + if len(selectStmt.TargetList.Items) == 0 { + t.Fatal("Expected target list items") + } + + resTarget, ok := selectStmt.TargetList.Items[0].(*ast.ResTarget) + if !ok { + t.Fatalf("Expected ResTarget, got %T", selectStmt.TargetList.Items[0]) + } + + funcCall, ok := resTarget.Val.(*ast.FuncCall) + if !ok { + t.Fatalf("Expected FuncCall, got %T", resTarget.Val) + } + + if funcCall.Func.Name != "count" { + t.Fatalf("Expected function name 'count', got '%s'", funcCall.Func.Name) + } + + t.Logf("COUNT(*) parsed successfully with %d arguments", len(funcCall.Args.Items)) + for i, arg := range funcCall.Args.Items { + t.Logf(" Arg %d: %T", i, arg) + if colRef, ok := arg.(*ast.ColumnRef); ok { + t.Logf(" ColumnRef with %d fields", len(colRef.Fields.Items)) + if len(colRef.Fields.Items) > 0 { + t.Logf(" Field 0 type: %T", colRef.Fields.Items[0]) + if _, ok := colRef.Fields.Items[0].(*ast.A_Star); ok { + t.Logf(" -> Contains A_Star") + } + if str, ok := colRef.Fields.Items[0].(*ast.String); ok { + t.Logf(" -> String: '%s'", str.Str) + } + } + } + } +} + +// TestCatalogHasCountFunction verifies COUNT is registered in the catalog +func TestCatalogHasCountFunction(t *testing.T) { + cat := NewCatalog() + + // Try to find the COUNT function + funcs, err := cat.ListFuncsByName(&ast.FuncName{Name: "count"}) + if err != nil { + t.Fatalf("ListFuncsByName failed: %v", err) + } + + if len(funcs) == 0 { + t.Fatal("COUNT function not found in catalog") + } + + count := funcs[0] + t.Logf("Found COUNT function: %+v", count) + t.Logf(" Name: %s", count.Name) + t.Logf(" Return type: %+v", count.ReturnType) + t.Logf(" Args: %v", len(count.Args)) + + if len(count.Args) > 0 { + arg := count.Args[0] + t.Logf(" Arg 0: %+v", arg) + t.Logf(" Name: %s", arg.Name) + t.Logf(" Type: %+v", arg.Type) + t.Logf(" Mode: %v", arg.Mode) + t.Logf(" HasDefault: %v", arg.HasDefault) + } +} + +// TestCatalogIsolationBetweenQueries verifies that function registrations in one query +// don't affect other queries. This tests the catalog cloning mechanism. +func TestCatalogIsolationBetweenQueries(t *testing.T) { + // Create a catalog with a test table + cat := catalog.New("default") + // Access the default schema directly + schema := cat.Schemas[0] + schema.Tables = append(schema.Tables, &catalog.Table{ + Rel: &ast.TableName{Name: "test_table"}, + Columns: []*catalog.Column{ + {Name: "id", Type: ast.TypeName{Name: "int32"}}, + {Name: "values", Type: ast.TypeName{Name: "array"}}, + }, + }) + + // Create a parser and set the catalog + parser := NewParser() + parser.Catalog = cat + + // Query 1: arrayJoin should register with a specific type in the cloned catalog + query1 := "SELECT arrayJoin(values) AS item FROM test_table" + stmts1, err := parser.Parse(strings.NewReader(query1)) + if err != nil { + t.Fatalf("Query 1 parse failed: %v", err) + } + if len(stmts1) != 1 { + t.Fatalf("Query 1: expected 1 statement, got %d", len(stmts1)) + } + + // Query 2: Same arrayJoin call - should use fresh cloned catalog + query2 := "SELECT arrayJoin(values) AS item FROM test_table" + stmts2, err := parser.Parse(strings.NewReader(query2)) + if err != nil { + t.Fatalf("Query 2 parse failed: %v", err) + } + if len(stmts2) != 1 { + t.Fatalf("Query 2: expected 1 statement, got %d", len(stmts2)) + } + + // Verify that the original catalog was not mutated + // Count arrayJoin functions registered in the original catalog + var arrayJoinFuncs []*catalog.Function + for _, fn := range schema.Funcs { + if strings.ToLower(fn.Name) == "arrayjoin" { + arrayJoinFuncs = append(arrayJoinFuncs, fn) + } + } + + // Should be no arrayJoin functions registered in the original catalog + // since cloning happens per Parse() call + if len(arrayJoinFuncs) > 0 { + t.Fatalf("Original catalog was mutated: found %d arrayJoin functions, expected 0", len(arrayJoinFuncs)) + } + + t.Log("✓ Catalog isolation verified: functions registered during parsing don't affect original catalog") +} diff --git a/internal/engine/clickhouse/qualified_col_test.go b/internal/engine/clickhouse/qualified_col_test.go new file mode 100644 index 0000000000..cce5179a42 --- /dev/null +++ b/internal/engine/clickhouse/qualified_col_test.go @@ -0,0 +1,76 @@ +package clickhouse + +import ( + "strings" + "testing" + + "github.com/sqlc-dev/sqlc/internal/sql/ast" +) + +// TestQualifiedColumnNameExtraction tests that qualified column references +// like u.id without explicit aliases get the column name extracted as the default name +func TestQualifiedColumnNameExtraction(t *testing.T) { + parser := NewParser() + + tests := []struct { + name string + query string + expectedNames []string + }{ + { + name: "qualified columns without alias", + query: "SELECT u.id, u.name FROM users u", + expectedNames: []string{"id", "name"}, + }, + { + name: "qualified columns with mixed aliases", + query: "SELECT u.id, u.name as user_name, u.email FROM users u", + expectedNames: []string{"id", "user_name", "email"}, + }, + { + name: "join with qualified columns", + query: "SELECT u.id, u.name, p.title FROM users u LEFT JOIN posts p ON u.id = p.user_id", + expectedNames: []string{"id", "name", "title"}, + }, + { + name: "nested qualified columns", + query: "SELECT t.a, t.b, t.c FROM table1 t", + expectedNames: []string{"a", "b", "c"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + stmts, err := parser.Parse(strings.NewReader(tt.query)) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(stmts) == 0 { + t.Fatal("No statements parsed") + } + + selectStmt, ok := stmts[0].Raw.Stmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", stmts[0].Raw.Stmt) + } + + if len(selectStmt.TargetList.Items) != len(tt.expectedNames) { + t.Fatalf("Expected %d targets, got %d", len(tt.expectedNames), len(selectStmt.TargetList.Items)) + } + + for i, expected := range tt.expectedNames { + resTarget, ok := selectStmt.TargetList.Items[i].(*ast.ResTarget) + if !ok { + t.Fatalf("Target %d is not ResTarget", i) + } + + if resTarget.Name == nil { + t.Errorf("Target %d has no name (expected %q)", i, expected) + } else if *resTarget.Name != expected { + t.Errorf("Target %d: expected name %q, got %q", i, expected, *resTarget.Name) + } + } + }) + } +} diff --git a/internal/engine/clickhouse/type_resolver.go b/internal/engine/clickhouse/type_resolver.go new file mode 100644 index 0000000000..2271e64bb0 --- /dev/null +++ b/internal/engine/clickhouse/type_resolver.go @@ -0,0 +1,95 @@ +package clickhouse + +import ( + "strings" + + "github.com/sqlc-dev/sqlc/internal/sql/ast" + "github.com/sqlc-dev/sqlc/internal/sql/catalog" +) + +func TypeResolver(call *ast.FuncCall, fun *catalog.Function, resolve func(n ast.Node) (*catalog.Column, error)) *ast.TypeName { + funcName := strings.ToLower(fun.Name) + + switch funcName { + case "arrayjoin": + if len(call.Args.Items) != 1 { + return nil + } + // arrayJoin(arr) -> returns element type of arr + col, err := resolve(call.Args.Items[0]) + if err != nil { + return nil + } + // If the argument is an array, return the element type + // In sqlc, arrays are often represented by the IsArray flag on the column + if col.IsArray { + // Create a new type name based on the column's type + // We need to "unwrap" the array. + typeName := col.Type.Name + if strings.HasSuffix(typeName, "[]") { + typeName = strings.TrimSuffix(typeName, "[]") + } + return &ast.TypeName{ + Name: typeName, + } + } + // If it's not marked as IsArray, it might still be an array type string (e.g. "Array(Int32)") + // TODO: Parsing ClickHouse type strings might be needed here if DataType is "Array(T)" + return nil + + case "argmin", "argmax", "any", "anylast", "anyheavy", + "argminif", "argmaxif", "anyif", "anylastif", "anyheavyif", + "min", "max", "sum", "minif", "maxif", "sumif": + if len(call.Args.Items) < 1 { + return nil + } + // These functions return the type of their first argument + col, err := resolve(call.Args.Items[0]) + if err != nil { + return nil + } + typeName := col.Type + return &typeName + + case "count", "countif", "uniq", "uniqexact": + // ClickHouse count returns UInt64 + return &ast.TypeName{Name: "uint64"} + + case "jsonextract": + // JSONExtract(json, indices_or_keys..., return_type) + // The last argument is usually the type + if len(call.Args.Items) < 2 { + return nil + } + lastArg := call.Args.Items[len(call.Args.Items)-1] + // Check if it's a string literal + if constVal, ok := lastArg.(*ast.A_Const); ok { + if strVal, ok := constVal.Val.(*ast.String); ok { + typeStr := strVal.Str + // Map ClickHouse type string to sqlc type + mappedType := mapClickHouseType(typeStr) + // If it's an array type, we need to handle it + if strings.HasSuffix(mappedType, "[]") { + elemType := strings.TrimSuffix(mappedType, "[]") + return &ast.TypeName{ + Name: elemType, + ArrayBounds: &ast.List{ + Items: []ast.Node{&ast.A_Const{}}, + }, + } + } + return &ast.TypeName{Name: mappedType} + } + } + return nil + + case "jsonextractkeysandvalues": + // JSONExtractKeysAndValues(json, 'ValueType') -> Array(Tuple(String, ValueType)) + // We map this to just "any" or a complex type if possible. + // In sqlc, we might represent Array(Tuple(String, T)) as... complex. + // For now, let's return any, or maybe just handle the array part. + return &ast.TypeName{Name: "any"} + } + + return nil +} diff --git a/internal/engine/clickhouse/unhandled_types_test.go b/internal/engine/clickhouse/unhandled_types_test.go new file mode 100644 index 0000000000..ded021d9d0 --- /dev/null +++ b/internal/engine/clickhouse/unhandled_types_test.go @@ -0,0 +1,139 @@ +package clickhouse + +import ( + "strings" + "testing" + + "github.com/sqlc-dev/sqlc/internal/sql/ast" +) + +// TestUnhandledNodeTypes tests queries that use ClickHouse AST node types +// that we haven't implemented converters for yet. +// These tests identify which unhandled types actually matter in practice. +func TestUnhandledNodeTypes(t *testing.T) { + parser := NewParser() + + tests := []struct { + name string + query string + wantErr bool + hasTodo bool // whether we expect TODO nodes in output + checkFunc func(*ast.SelectStmt) error + }{ + { + name: "array indexing", + query: "SELECT arr[1] FROM table1", + wantErr: false, + hasTodo: true, // IndexOperation not handled yet + }, + { + name: "array literal", + query: "SELECT [1, 2, 3] as arr FROM table1", + wantErr: false, + hasTodo: false, // ArrayParamList now handled + }, + { + name: "tuple literal", + query: "SELECT (1, 2, 3) as t FROM table1", + wantErr: false, + hasTodo: false, // ParamExprList handles this + }, + { + name: "table function", + query: "SELECT * FROM numbers(10)", + wantErr: false, + hasTodo: false, // TableFunctionExpr now handled + }, + { + name: "array comparison", + query: "SELECT * FROM table1 WHERE x IN [1, 2, 3]", + wantErr: false, + hasTodo: false, // ArrayParamList now handled + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + stmts, err := parser.Parse(strings.NewReader(tt.query)) + if (err != nil) != tt.wantErr { + t.Fatalf("Parse error: %v, wantErr %v", err, tt.wantErr) + } + + if len(stmts) == 0 { + t.Fatal("No statements parsed") + } + + selectStmt, ok := stmts[0].Raw.Stmt.(*ast.SelectStmt) + if !ok { + t.Fatalf("Expected SelectStmt, got %T", stmts[0].Raw.Stmt) + } + + hasTodoNode := containsTODO(selectStmt) + if hasTodoNode != tt.hasTodo { + t.Errorf("Expected hasTodo=%v, but got %v", tt.hasTodo, hasTodoNode) + } + + t.Logf("Query parses: has TODO nodes=%v", hasTodoNode) + }) + } +} + +// containsTODO recursively checks if an AST node tree contains any TODO nodes +func containsTODO(node interface{}) bool { + if node == nil { + return false + } + + switch n := node.(type) { + case *ast.TODO: + if n != nil { + return true + } + case *ast.SelectStmt: + if n != nil { + if containsTODO(n.TargetList) { + return true + } + if containsTODO(n.FromClause) { + return true + } + if containsTODO(n.WhereClause) { + return true + } + return containsTODO(n.Larg) || containsTODO(n.Rarg) + } + case *ast.List: + if n != nil && n.Items != nil { + for _, item := range n.Items { + if item != nil && containsTODO(item) { + return true + } + } + } + case *ast.ResTarget: + if n != nil && containsTODO(n.Val) { + return true + } + case *ast.RangeVar: + // RangeVar doesn't have a Subquery field in this version + return false + case *ast.A_Expr: + if n != nil { + return containsTODO(n.Lexpr) || containsTODO(n.Rexpr) + } + case *ast.FuncCall: + if n != nil && containsTODO(n.Args) { + return true + } + case *ast.JoinExpr: + if n != nil { + return containsTODO(n.Larg) || containsTODO(n.Rarg) || containsTODO(n.Quals) + } + case *ast.ColumnRef: + if n != nil { + return containsTODO(n.Fields) + } + } + + return false +} diff --git a/internal/engine/clickhouse/using_test.go b/internal/engine/clickhouse/using_test.go new file mode 100644 index 0000000000..81934dde68 --- /dev/null +++ b/internal/engine/clickhouse/using_test.go @@ -0,0 +1,54 @@ +package clickhouse + +import ( + "strings" + "testing" + + "github.com/sqlc-dev/sqlc/internal/sql/ast" +) + +// TestClickHouseUsingMapStructure verifies the structure of the converted JoinExpr +func TestClickHouseUsingMapStructure(t *testing.T) { + sql := "SELECT * FROM orders LEFT JOIN shipments USING (order_id)" + parser := NewParser() + + stmts, err := parser.Parse(strings.NewReader(sql)) + if err != nil { + t.Fatalf("Parse error: %v", err) + } + + selectStmt := stmts[0].Raw.Stmt.(*ast.SelectStmt) + if selectStmt.FromClause == nil || len(selectStmt.FromClause.Items) == 0 { + t.Fatal("No FROM clause") + } + + fromItem := selectStmt.FromClause.Items[0] + topJoin, ok := fromItem.(*ast.JoinExpr) + if !ok { + t.Fatalf("Expected JoinExpr, got %T", fromItem) + } + + t.Logf("Top JoinExpr:") + t.Logf(" Larg: %T", topJoin.Larg) + t.Logf(" Rarg: %T", topJoin.Rarg) + t.Logf(" UsingClause: %v", topJoin.UsingClause) + + // Check nested join + if nestedJoin, ok := topJoin.Rarg.(*ast.JoinExpr); ok { + t.Logf("Nested JoinExpr (Rarg):") + t.Logf(" Larg: %T", nestedJoin.Larg) + if rvar, ok := nestedJoin.Larg.(*ast.RangeVar); ok { + t.Logf(" Relname: %v", rvar.Relname) + } + t.Logf(" Rarg: %T", nestedJoin.Rarg) + t.Logf(" UsingClause: %v", nestedJoin.UsingClause) + if nestedJoin.UsingClause != nil { + t.Logf(" Items: %d", len(nestedJoin.UsingClause.Items)) + for i, item := range nestedJoin.UsingClause.Items { + if str, ok := item.(*ast.String); ok { + t.Logf(" %d: %s", i, str.Str) + } + } + } + } +} diff --git a/internal/sql/catalog/catalog.go b/internal/sql/catalog/catalog.go index 278ea8797d..1ead170eac 100644 --- a/internal/sql/catalog/catalog.go +++ b/internal/sql/catalog/catalog.go @@ -33,6 +33,155 @@ func New(defaultSchema string) *Catalog { return newCatalog } +// Clone creates a deep copy of the catalog, preserving all schemas, tables, types, and functions. +// This is used to create isolated copies for query parsing where functions might be registered +// with context-dependent types without affecting the original catalog. +func (c *Catalog) Clone() *Catalog { + if c == nil { + return nil + } + + cloned := &Catalog{ + Comment: c.Comment, + DefaultSchema: c.DefaultSchema, + Name: c.Name, + Schemas: make([]*Schema, 0, len(c.Schemas)), + SearchPath: make([]string, len(c.SearchPath)), + LoadExtension: c.LoadExtension, + Extensions: make(map[string]struct{}), + } + + // Copy search path + copy(cloned.SearchPath, c.SearchPath) + + // Copy extensions + for k, v := range c.Extensions { + cloned.Extensions[k] = v + } + + // Clone schemas + for _, schema := range c.Schemas { + cloned.Schemas = append(cloned.Schemas, cloneSchema(schema)) + } + + return cloned +} + +func cloneSchema(s *Schema) *Schema { + if s == nil { + return nil + } + + cloned := &Schema{ + Name: s.Name, + Comment: s.Comment, + Tables: make([]*Table, len(s.Tables)), + Types: make([]Type, len(s.Types)), + Funcs: make([]*Function, len(s.Funcs)), + } + + // Clone tables + for i, table := range s.Tables { + cloned.Tables[i] = cloneTable(table) + } + + // Clone types + for i, t := range s.Types { + cloned.Types[i] = cloneType(t) + } + + // Clone functions + for i, fn := range s.Funcs { + cloned.Funcs[i] = cloneFunction(fn) + } + + return cloned +} + +func cloneFunction(f *Function) *Function { + if f == nil { + return nil + } + + cloned := &Function{ + Name: f.Name, + Comment: f.Comment, + Desc: f.Desc, + ReturnType: f.ReturnType, // ast.TypeName is immutable for our purposes + ReturnTypeNullable: f.ReturnTypeNullable, + Args: make([]*Argument, len(f.Args)), + } + + for i, arg := range f.Args { + if arg != nil { + cloned.Args[i] = &Argument{ + Name: arg.Name, + Type: arg.Type, // ast.TypeName is immutable + HasDefault: arg.HasDefault, + Mode: arg.Mode, + } + } + } + + return cloned +} + +func cloneTable(t *Table) *Table { + if t == nil { + return nil + } + + cloned := &Table{ + Rel: t.Rel, + Comment: t.Comment, + Columns: make([]*Column, len(t.Columns)), + } + + for i, col := range t.Columns { + if col != nil { + colClone := &Column{ + Name: col.Name, + Type: col.Type, + IsNotNull: col.IsNotNull, + IsUnsigned: col.IsUnsigned, + IsArray: col.IsArray, + ArrayDims: col.ArrayDims, + Comment: col.Comment, + linkedType: col.linkedType, + } + if col.Length != nil { + length := *col.Length + colClone.Length = &length + } + cloned.Columns[i] = colClone + } + } + + return cloned +} + +func cloneType(t Type) Type { + if t == nil { + return nil + } + + switch typ := t.(type) { + case *Enum: + return &Enum{ + Name: typ.Name, + Vals: append([]string{}, typ.Vals...), + Comment: typ.Comment, + } + case *CompositeType: + return &CompositeType{ + Name: typ.Name, + Comment: typ.Comment, + } + default: + return t + } +} + func (c *Catalog) Build(stmts []ast.Statement) error { for i := range stmts { if err := c.Update(stmts[i], nil); err != nil { diff --git a/internal/sqltest/docker/clickhouse.go b/internal/sqltest/docker/clickhouse.go new file mode 100644 index 0000000000..fa95b3a83e --- /dev/null +++ b/internal/sqltest/docker/clickhouse.go @@ -0,0 +1,109 @@ +package docker + +import ( + "context" + "database/sql" + "fmt" + "log/slog" + "os/exec" + "strings" + "time" + + _ "github.com/ClickHouse/clickhouse-go/v2" +) + +var clickhouseHost string + +func StartClickHouseServer(c context.Context) (string, error) { + if err := Installed(); err != nil { + return "", err + } + if clickhouseHost != "" { + return clickhouseHost, nil + } + value, err, _ := flight.Do("clickhouse", func() (interface{}, error) { + host, err := startClickHouseServer(c) + if err != nil { + return "", err + } + clickhouseHost = host + return host, err + }) + if err != nil { + return "", err + } + data, ok := value.(string) + if !ok { + return "", fmt.Errorf("returned value was not a string") + } + return data, nil +} + +func startClickHouseServer(c context.Context) (string, error) { + { + _, err := exec.Command("docker", "pull", "clickhouse:lts").CombinedOutput() + if err != nil { + return "", fmt.Errorf("docker pull: clickhouse:lts %w", err) + } + } + + var exists bool + { + cmd := exec.Command("docker", "container", "inspect", "sqlc_sqltest_docker_clickhouse") + // This means we've already started the container + exists = cmd.Run() == nil + } + + if !exists { + cmd := exec.Command("docker", "run", + "--name", "sqlc_sqltest_docker_clickhouse", + "-p", "9000:9000", + "-p", "8123:8123", + "-e", "CLICKHOUSE_DB=default", + "-e", "CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT=1", + "-d", + "clickhouse:lts", + ) + + output, err := cmd.CombinedOutput() + fmt.Println(string(output)) + + msg := `Conflict. The container name "/sqlc_sqltest_docker_clickhouse" is already in use by container` + if !strings.Contains(string(output), msg) && err != nil { + return "", err + } + } + + ctx, cancel := context.WithTimeout(c, 10*time.Second) + defer cancel() + + // Create a ticker that fires every 10ms + ticker := time.NewTicker(10 * time.Millisecond) + defer ticker.Stop() + + // ClickHouse DSN format: clickhouse://host:port + dsn := "clickhouse://localhost:9000/default" + + for { + select { + case <-ctx.Done(): + return "", fmt.Errorf("timeout reached: %w", ctx.Err()) + + case <-ticker.C: + db, err := sql.Open("clickhouse", dsn) + if err != nil { + slog.Debug("sqltest", "open", err) + continue + } + + if err := db.PingContext(ctx); err != nil { + slog.Debug("sqltest", "ping", err) + db.Close() + continue + } + + db.Close() + return dsn, nil + } + } +} diff --git a/internal/sqltest/local/clickhouse.go b/internal/sqltest/local/clickhouse.go new file mode 100644 index 0000000000..b4c9090aa9 --- /dev/null +++ b/internal/sqltest/local/clickhouse.go @@ -0,0 +1,99 @@ +package local + +import ( + "context" + "database/sql" + "fmt" + "hash/fnv" + "os" + "strings" + "testing" + + migrate "github.com/sqlc-dev/sqlc/internal/migrations" + "github.com/sqlc-dev/sqlc/internal/sql/sqlpath" + "github.com/sqlc-dev/sqlc/internal/sqltest/docker" + + _ "github.com/ClickHouse/clickhouse-go/v2" +) + +func ClickHouse(t *testing.T, migrations []string) string { + ctx := context.Background() + t.Helper() + + dburi := os.Getenv("CLICKHOUSE_SERVER_URI") + if dburi == "" { + if ierr := docker.Installed(); ierr == nil { + u, err := docker.StartClickHouseServer(ctx) + if err != nil { + t.Fatal(err) + } + dburi = u + } else { + t.Skip("CLICKHOUSE_SERVER_URI is empty") + } + } + + // Open connection to ClickHouse + db, err := sql.Open("clickhouse", dburi) + if err != nil { + t.Fatalf("ClickHouse connection failed: %s", err) + } + defer db.Close() + + var seed []string + files, err := sqlpath.Glob(migrations) + if err != nil { + t.Fatal(err) + } + + h := fnv.New64() + for _, f := range files { + blob, err := os.ReadFile(f) + if err != nil { + t.Fatal(err) + } + h.Write(blob) + seed = append(seed, migrate.RemoveRollbackStatements(string(blob))) + } + + // Create unique database name + name := fmt.Sprintf("sqlc_test_%x", h.Sum(nil)) + + // Drop database if it exists (ClickHouse style) + dropQuery := fmt.Sprintf(`DROP DATABASE IF EXISTS %s`, name) + if _, err := db.ExecContext(ctx, dropQuery); err != nil { + t.Logf("could not drop database (may not exist): %s", err) + } + + // Create new database + createQuery := fmt.Sprintf(`CREATE DATABASE IF NOT EXISTS %s`, name) + if _, err := db.ExecContext(ctx, createQuery); err != nil { + t.Fatalf("failed to create database: %s", err) + } + + // Execute migration scripts + dbWithDatabase := fmt.Sprintf("%s?database=%s", dburi, name) + dbConn, err := sql.Open("clickhouse", dbWithDatabase) + if err != nil { + t.Fatalf("ClickHouse connection to new database failed: %s", err) + } + defer dbConn.Close() + + for _, q := range seed { + if len(strings.TrimSpace(q)) == 0 { + continue + } + if _, err := dbConn.ExecContext(ctx, q); err != nil { + t.Fatalf("migration failed: %s: %s", q, err) + } + } + + // Register cleanup + t.Cleanup(func() { + if _, err := db.ExecContext(ctx, dropQuery); err != nil { + t.Logf("failed cleaning up database: %s", err) + } + }) + + return dbWithDatabase +}