Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,18 @@ Thumbs.db
*.test
*.out
coverage.html

# Local config files with credentials
duckgres_r2_test.yaml
duckgres_*.yaml
!duckgres.yaml.example

# Local convenience scripts
run-duckgres.sh

# Local Docker setup
docker-compose-local.yml
Dockerfile

# Local docs (may contain project-specific notes)
docs/
10 changes: 7 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,14 @@ Three-tier configuration (highest to lowest priority):
## Testing

```bash
# Build
go build -o duckgres .
# Build and run with DuckLake config (kills existing, tails logs)
./run-duckgres.sh

# Build and run with limited tables for faster Metabase/Grafana testing
./run-duckgres.sh 5 # Only show 5 tables (DUCKGRES_TABLE_LIMIT=5)

# Run on non-standard port
# Manual build and run
go build -o duckgres .
./duckgres --port 35437

# Connect with psql
Expand Down
42 changes: 42 additions & 0 deletions duckgres.yaml.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Duckgres Configuration Example
# Copy this file to duckgres.yaml and fill in your values

host: "0.0.0.0"
port: 5432

# Directory for DuckDB database files
data_dir: "./data"

# User credentials (username: password)
users:
postgres: "your_password_here"

# Optional: TLS certificates (auto-generated if not specified)
# tls_cert: "./certs/server.crt"
# tls_key: "./certs/server.key"

# Optional: DuckDB extensions to load
extensions:
- ducklake
# - spatial
# - httpfs

# Optional: DuckLake configuration for cloud-native lakehouse
# ducklake:
# # PostgreSQL metadata store connection string
# metadata_store: "postgres:host=your-host.com port=5432 dbname=ducklake user=your_user password=YOUR_PASSWORD sslmode=require"
#
# # Object store URL (S3, R2, GCS, etc.)
# object_store: "s3://your-bucket/"
#
# # For AWS S3:
# # s3_region: "us-east-1"
# # s3_access_key: "YOUR_ACCESS_KEY"
# # s3_secret_key: "YOUR_SECRET_KEY"
# # s3_endpoint: "" # Leave empty for AWS, set for S3-compatible services
# # s3_use_ssl: true
#
# # For Cloudflare R2:
# # r2_account_id: "YOUR_ACCOUNT_ID"
# # r2_access_key: "YOUR_ACCESS_KEY"
# # r2_secret_key: "YOUR_SECRET_KEY"
28 changes: 27 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ type RateLimitFileConfig struct {

type DuckLakeFileConfig struct {
MetadataStore string `yaml:"metadata_store"` // e.g., "postgres:host=localhost user=ducklake password=secret dbname=ducklake"
ObjectStore string `yaml:"object_store"` // e.g., "s3://bucket/path/" for S3/MinIO storage
ObjectStore string `yaml:"object_store"` // e.g., "s3://bucket/path/" for S3/MinIO or "r2://bucket/path/" for R2

// S3 credential provider: "config" (explicit) or "credential_chain" (AWS SDK)
S3Provider string `yaml:"s3_provider"`
Expand All @@ -56,6 +56,11 @@ type DuckLakeFileConfig struct {
// Credential chain provider settings (AWS SDK credential chain)
S3Chain string `yaml:"s3_chain"` // e.g., "env;config" - which credential sources to check
S3Profile string `yaml:"s3_profile"` // AWS profile name for config chain

// R2 (Cloudflare) settings for r2:// URLs
R2AccountID string `yaml:"r2_account_id"` // Cloudflare account ID
R2AccessKey string `yaml:"r2_access_key"` // R2 access key ID
R2SecretKey string `yaml:"r2_secret_key"` // R2 secret access key
}

// loadConfigFile loads configuration from a YAML file
Expand Down Expand Up @@ -221,6 +226,17 @@ func main() {
if fileCfg.DuckLake.S3Profile != "" {
cfg.DuckLake.S3Profile = fileCfg.DuckLake.S3Profile
}

// Apply R2 (Cloudflare) config
if fileCfg.DuckLake.R2AccountID != "" {
cfg.DuckLake.R2AccountID = fileCfg.DuckLake.R2AccountID
}
if fileCfg.DuckLake.R2AccessKey != "" {
cfg.DuckLake.R2AccessKey = fileCfg.DuckLake.R2AccessKey
}
if fileCfg.DuckLake.R2SecretKey != "" {
cfg.DuckLake.R2SecretKey = fileCfg.DuckLake.R2SecretKey
}
}

// Apply environment variables (override config file)
Expand Down Expand Up @@ -274,6 +290,16 @@ func main() {
if v := os.Getenv("DUCKGRES_DUCKLAKE_S3_PROFILE"); v != "" {
cfg.DuckLake.S3Profile = v
}
// R2 (Cloudflare) environment variables
if v := os.Getenv("DUCKGRES_DUCKLAKE_R2_ACCOUNT_ID"); v != "" {
cfg.DuckLake.R2AccountID = v
}
if v := os.Getenv("DUCKGRES_DUCKLAKE_R2_ACCESS_KEY"); v != "" {
cfg.DuckLake.R2AccessKey = v
}
if v := os.Getenv("DUCKGRES_DUCKLAKE_R2_SECRET_KEY"); v != "" {
cfg.DuckLake.R2SecretKey = v
}

// Apply CLI flags (highest priority)
if *host != "" {
Expand Down
1,229 changes: 1,053 additions & 176 deletions server/catalog.go

Large diffs are not rendered by default.

19 changes: 18 additions & 1 deletion server/conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ func (c *clientConn) handleStartup() error {

func (c *clientConn) sendInitialParams() {
params := map[string]string{
"server_version": "15.0 (Duckgres)",
"server_version": "15.0",
"server_encoding": "UTF8",
"client_encoding": "UTF8",
"DateStyle": "ISO, MDY",
Expand Down Expand Up @@ -357,6 +357,10 @@ func (c *clientConn) handleQuery(body []byte) error {

log.Printf("[%s] Query: %s", c.username, query)

// Fix JDBC driver escaping of regex operators (e.g., \!~ -> !~)
// PostgreSQL JDBC sometimes sends escaped operators which pg_query can't parse
query = fixJDBCEscapedOperators(query)

// Transpile PostgreSQL SQL to DuckDB-compatible SQL
tr := c.newTranspiler(false)
result, err := tr.Transpile(query)
Expand Down Expand Up @@ -1757,3 +1761,16 @@ func isAlterTableNotTableError(err error) bool {
return strings.Contains(msg, "cannot use alter table") &&
strings.Contains(msg, "not a table")
}

// fixJDBCEscapedOperators removes backslash escaping from regex operators.
// PostgreSQL JDBC driver sometimes sends escaped operators like \!~ which
// pg_query cannot parse. This converts them back to standard operators.
func fixJDBCEscapedOperators(query string) string {
// Fix escaped regex operators: \!~ -> !~, \!~* -> !~*
query = strings.ReplaceAll(query, `\!~*`, `!~*`)
query = strings.ReplaceAll(query, `\!~`, `!~`)
// Fix escaped LIKE operators: \!~~ -> !~~, \!~~* -> !~~*
query = strings.ReplaceAll(query, `\!~~*`, `!~~*`)
query = strings.ReplaceAll(query, `\!~~`, `!~~`)
return query
}
Loading