Skip to content

Commit 8af5000

Browse files
committed
Add DuckDB support with PostgreSQL parser reuse
This commit adds DuckDB as a supported database engine to sqlc by reusing the PostgreSQL parser and catalog while implementing a custom analyzer that connects to an in-memory DuckDB instance. Key changes: - Add EngineDuckDB constant to config (internal/config/config.go) - Implement DuckDB analyzer using go-duckdb driver (internal/engine/duckdb/analyzer/) - Register DuckDB engine in compiler with PostgreSQL parser/catalog (internal/compiler/engine.go) - Add DuckDB support to vet command (internal/cmd/vet.go) - Add go-duckdb v1.8.5 dependency (go.mod) - Create comprehensive example with schema, queries, and documentation (examples/duckdb/) The DuckDB implementation leverages PostgreSQL-compatible SQL syntax while providing accurate type inference through live database analysis. The analyzer uses an in-memory DuckDB instance to extract column and parameter types. Features: - PostgreSQL-compatible SQL parsing - In-memory database analysis - Schema migration support - Type-safe Go code generation - Thread-safe connection management 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent b807fe9 commit 8af5000

File tree

10 files changed

+613
-0
lines changed

10 files changed

+613
-0
lines changed

examples/duckdb/README.md

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
# DuckDB Example
2+
3+
This example demonstrates how to use sqlc with DuckDB.
4+
5+
## Overview
6+
7+
DuckDB is an in-process analytical database that supports PostgreSQL-compatible SQL syntax. This integration reuses sqlc's PostgreSQL parser and catalog while providing a DuckDB-specific analyzer that connects to an in-memory DuckDB instance.
8+
9+
## Features
10+
11+
- **PostgreSQL-compatible SQL**: DuckDB uses PostgreSQL-compatible syntax, so you can use familiar SQL constructs
12+
- **In-memory database**: Perfect for testing and development
13+
- **Type-safe Go code**: sqlc generates type-safe Go code from your SQL queries
14+
- **Live database analysis**: The analyzer connects to a DuckDB instance to extract accurate column types
15+
16+
## Configuration
17+
18+
The `sqlc.yaml` file configures sqlc to use the DuckDB engine:
19+
20+
```yaml
21+
version: "2"
22+
sql:
23+
- name: "duckdb_example"
24+
engine: "duckdb" # Use DuckDB engine
25+
schema:
26+
- "schema.sql"
27+
queries:
28+
- "query.sql"
29+
database:
30+
managed: false
31+
uri: ":memory:" # Use in-memory database
32+
analyzer:
33+
database: true # Enable live database analysis
34+
gen:
35+
go:
36+
package: "db"
37+
out: "db"
38+
```
39+
40+
## Database URI
41+
42+
DuckDB supports several URI formats:
43+
44+
- `:memory:` - In-memory database (default if not specified)
45+
- `file.db` - File-based database
46+
- `/path/to/file.db` - Absolute path to database file
47+
48+
## Usage
49+
50+
1. Generate Go code:
51+
```bash
52+
sqlc generate
53+
```
54+
55+
2. Use the generated code in your application:
56+
```go
57+
package main
58+
59+
import (
60+
"context"
61+
"database/sql"
62+
"log"
63+
64+
_ "github.com/marcboeker/go-duckdb"
65+
"yourmodule/db"
66+
)
67+
68+
func main() {
69+
// Open DuckDB connection
70+
conn, err := sql.Open("duckdb", ":memory:")
71+
if err != nil {
72+
log.Fatal(err)
73+
}
74+
defer conn.Close()
75+
76+
// Create tables
77+
schema := `
78+
CREATE TABLE users (
79+
id INTEGER PRIMARY KEY,
80+
name VARCHAR NOT NULL,
81+
email VARCHAR UNIQUE NOT NULL,
82+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
83+
);
84+
`
85+
if _, err := conn.Exec(schema); err != nil {
86+
log.Fatal(err)
87+
}
88+
89+
// Use generated queries
90+
queries := db.New(conn)
91+
ctx := context.Background()
92+
93+
// Create a user
94+
user, err := queries.CreateUser(ctx, db.CreateUserParams{
95+
Name: "John Doe",
96+
Email: "john@example.com",
97+
})
98+
if err != nil {
99+
log.Fatal(err)
100+
}
101+
102+
log.Printf("Created user: %+v\n", user)
103+
104+
// Get the user
105+
fetchedUser, err := queries.GetUser(ctx, user.ID)
106+
if err != nil {
107+
log.Fatal(err)
108+
}
109+
110+
log.Printf("Fetched user: %+v\n", fetchedUser)
111+
}
112+
```
113+
114+
## Differences from PostgreSQL
115+
116+
While DuckDB supports PostgreSQL-compatible SQL, there are some differences:
117+
118+
1. **Data Types**: DuckDB has its own set of data types, though many are compatible with PostgreSQL
119+
2. **Functions**: Some PostgreSQL functions may not be available or may behave differently
120+
3. **Extensions**: DuckDB uses a different extension system than PostgreSQL
121+
122+
## Benefits of DuckDB
123+
124+
1. **Fast analytical queries**: Optimized for OLAP workloads
125+
2. **Embedded**: No separate server process needed
126+
3. **Portable**: Single file database
127+
4. **PostgreSQL-compatible**: Familiar SQL syntax
128+
129+
## Requirements
130+
131+
- Go 1.24.0 or later
132+
- `github.com/marcboeker/go-duckdb` driver
133+
134+
## Notes
135+
136+
- The DuckDB analyzer uses an in-memory instance to extract query metadata
137+
- Schema migrations are applied to the analyzer instance automatically
138+
- Type inference is done by preparing queries against the DuckDB instance

examples/duckdb/query.sql

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
-- name: GetUser :one
2+
SELECT id, name, email, created_at
3+
FROM users
4+
WHERE id = $1;
5+
6+
-- name: ListUsers :many
7+
SELECT id, name, email, created_at
8+
FROM users
9+
ORDER BY name;
10+
11+
-- name: CreateUser :one
12+
INSERT INTO users (name, email)
13+
VALUES ($1, $2)
14+
RETURNING id, name, email, created_at;
15+
16+
-- name: GetUserPosts :many
17+
SELECT p.id, p.title, p.content, p.published, p.created_at
18+
FROM posts p
19+
WHERE p.user_id = $1
20+
ORDER BY p.created_at DESC;
21+
22+
-- name: CreatePost :one
23+
INSERT INTO posts (user_id, title, content, published)
24+
VALUES ($1, $2, $3, $4)
25+
RETURNING id, user_id, title, content, published, created_at;

examples/duckdb/schema.sql

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
-- Example DuckDB schema
2+
CREATE TABLE users (
3+
id INTEGER PRIMARY KEY,
4+
name VARCHAR NOT NULL,
5+
email VARCHAR UNIQUE NOT NULL,
6+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
7+
);
8+
9+
CREATE TABLE posts (
10+
id INTEGER PRIMARY KEY,
11+
user_id INTEGER NOT NULL,
12+
title VARCHAR NOT NULL,
13+
content TEXT,
14+
published BOOLEAN DEFAULT false,
15+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
16+
FOREIGN KEY (user_id) REFERENCES users(id)
17+
);

examples/duckdb/sqlc.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
version: "2"
2+
sql:
3+
- name: "duckdb_example"
4+
engine: "duckdb"
5+
schema:
6+
- "schema.sql"
7+
queries:
8+
- "query.sql"
9+
database:
10+
managed: false
11+
uri: ":memory:"
12+
analyzer:
13+
database: true
14+
gen:
15+
go:
16+
package: "db"
17+
out: "db"
18+
sql_package: "database/sql"

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ require (
1111
github.com/fatih/structtag v1.2.0
1212
github.com/go-sql-driver/mysql v1.9.3
1313
github.com/google/cel-go v0.26.1
14+
github.com/marcboeker/go-duckdb v1.8.5
1415
github.com/google/go-cmp v0.7.0
1516
github.com/jackc/pgx/v4 v4.18.3
1617
github.com/jackc/pgx/v5 v5.7.6

internal/cmd/vet.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"github.com/google/cel-go/cel"
2222
"github.com/google/cel-go/ext"
2323
"github.com/jackc/pgx/v5"
24+
_ "github.com/marcboeker/go-duckdb"
2425
"github.com/spf13/cobra"
2526
"google.golang.org/protobuf/encoding/protojson"
2627

@@ -529,6 +530,18 @@ func (c *checker) checkSQL(ctx context.Context, s config.SQL) error {
529530
// SQLite really doesn't want us to depend on the output of EXPLAIN
530531
// QUERY PLAN: https://www.sqlite.org/eqp.html
531532
expl = nil
533+
case config.EngineDuckDB:
534+
db, err := sql.Open("duckdb", dburl)
535+
if err != nil {
536+
return fmt.Errorf("database: connection error: %s", err)
537+
}
538+
if err := db.PingContext(ctx); err != nil {
539+
return fmt.Errorf("database: connection error: %s", err)
540+
}
541+
defer db.Close()
542+
prep = &dbPreparer{db}
543+
// DuckDB supports EXPLAIN
544+
expl = nil
532545
default:
533546
return fmt.Errorf("unsupported database uri: %s", s.Engine)
534547
}

internal/compiler/engine.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/sqlc-dev/sqlc/internal/config"
99
"github.com/sqlc-dev/sqlc/internal/dbmanager"
1010
"github.com/sqlc-dev/sqlc/internal/engine/dolphin"
11+
duckdbanalyze "github.com/sqlc-dev/sqlc/internal/engine/duckdb/analyzer"
1112
"github.com/sqlc-dev/sqlc/internal/engine/postgresql"
1213
pganalyze "github.com/sqlc-dev/sqlc/internal/engine/postgresql/analyzer"
1314
"github.com/sqlc-dev/sqlc/internal/engine/sqlite"
@@ -58,6 +59,20 @@ func NewCompiler(conf config.SQL, combo config.CombinedSettings) (*Compiler, err
5859
)
5960
}
6061
}
62+
case config.EngineDuckDB:
63+
// DuckDB uses PostgreSQL-compatible SQL, so we reuse the PostgreSQL parser and catalog
64+
c.parser = postgresql.NewParser()
65+
c.catalog = postgresql.NewCatalog()
66+
c.selector = newDefaultSelector()
67+
if conf.Database != nil {
68+
if conf.Analyzer.Database == nil || *conf.Analyzer.Database {
69+
c.analyzer = analyzer.Cached(
70+
duckdbanalyze.New(c.client, *conf.Database),
71+
combo.Global,
72+
*conf.Database,
73+
)
74+
}
75+
}
6176
default:
6277
return nil, fmt.Errorf("unknown engine: %s", conf.Engine)
6378
}

internal/config/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ const (
5454
EngineMySQL Engine = "mysql"
5555
EnginePostgreSQL Engine = "postgresql"
5656
EngineSQLite Engine = "sqlite"
57+
EngineDuckDB Engine = "duckdb"
5758
)
5859

5960
type Config struct {

0 commit comments

Comments
 (0)