diff --git a/CLAUDE.md b/CLAUDE.md index 43abb0d491..d8617aac49 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -87,9 +87,45 @@ Test the connection: mysql -h 127.0.0.1 -u root -pmysecretpassword -e "SELECT VERSION();" ``` -### Step 4: Run End-to-End Tests +### Step 4: Install ClickHouse (Optional - for ClickHouse tests) -With both databases running, the test framework automatically detects them: +ClickHouse is required for ClickHouse engine tests. The ClickHouse feature is experimental and requires the `SQLCEXPERIMENT=clickhouse` flag. + +**Important:** Use the official ClickHouse repository, not the Ubuntu package (which has an old incompatible version). + +```bash +# Add ClickHouse official repository key and source +curl -fsSL https://packages.clickhouse.com/rpm/lts/repodata/repomd.xml.key | sudo gpg --dearmor -o /usr/share/keyrings/clickhouse-keyring.gpg +ARCH=$(dpkg --print-architecture) +echo "deb [signed-by=/usr/share/keyrings/clickhouse-keyring.gpg arch=${ARCH}] https://packages.clickhouse.com/deb stable main" | sudo tee /etc/apt/sources.list.d/clickhouse.list + +# Install ClickHouse from official repo +sudo apt-get update +sudo DEBIAN_FRONTEND=noninteractive apt-get install -y clickhouse-server clickhouse-client + +# Start ClickHouse server +sudo clickhouse start +``` + +Test the connection: + +```bash +clickhouse-client --query "SELECT version()" +``` + +The native database support automatically detects ClickHouse and uses: +- URI: `clickhouse://default:@localhost:9000/default` +- User: `default` (no password) +- Port: 9000 + +**Note:** ClickHouse tests require setting the experiment flag: +```bash +SQLCEXPERIMENT=clickhouse go test ./internal/endtoend -run "TestReplay/clickhouse" -v +``` + +### Step 5: Run End-to-End Tests + +With the databases running, the test framework automatically detects them: ```bash # Run all end-to-end tests @@ -103,11 +139,12 @@ go test --tags=examples -timeout 20m ./... ``` The native database support (in `internal/sqltest/native/`) automatically: -- Detects running PostgreSQL and MySQL instances +- Detects running PostgreSQL, MySQL, and ClickHouse instances - Starts services if installed but not running - Uses standard connection URIs: - PostgreSQL: `postgres://postgres:postgres@127.0.0.1:5432/postgres?sslmode=disable` - MySQL: `root:mysecretpassword@tcp(127.0.0.1:3306)/mysql` + - ClickHouse: `clickhouse://default:@localhost:9000/default` ### Running Tests @@ -193,6 +230,11 @@ The `docker-compose.yml` provides test databases: - Password: `mysecretpassword` - Database: `dinotest` +- **ClickHouse** - Port 9000 (experimental, requires `SQLCEXPERIMENT=clickhouse`) + - User: `default` + - Password: (none) + - Database: `default` + ### Managing Databases ```bash @@ -291,6 +333,7 @@ POSTGRESQL_SERVER_URI="postgres://postgres:mysecretpassword@localhost:5432/postg - `/postgresql/` - PostgreSQL parser and converter - `/dolphin/` - MySQL parser (uses TiDB parser) - `/sqlite/` - SQLite parser + - `/clickhouse/` - ClickHouse parser (uses doubleclick parser, experimental) - `/internal/compiler/` - Query compilation logic - `/internal/codegen/` - Code generation for different languages - `/internal/config/` - Configuration file parsing diff --git a/docker-compose.yml b/docker-compose.yml index f318d1ed93..723b018be3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,3 +19,14 @@ services: POSTGRES_DB: postgres POSTGRES_PASSWORD: mysecretpassword POSTGRES_USER: postgres + + clickhouse: + image: "clickhouse/clickhouse-server:latest" + ports: + - "9000:9000" + - "8123:8123" + restart: always + environment: + CLICKHOUSE_DB: default + CLICKHOUSE_USER: default + CLICKHOUSE_PASSWORD: mysecretpassword diff --git a/go.mod b/go.mod index d55728118e..6e03566cd0 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,9 @@ module github.com/sqlc-dev/sqlc -go 1.24.0 - -toolchain go1.24.1 +go 1.24.7 require ( + github.com/ClickHouse/clickhouse-go/v2 v2.42.0 github.com/antlr4-go/antlr/v4 v4.13.1 github.com/cubicdaiya/gonp v1.0.4 github.com/davecgh/go-spew v1.1.1 @@ -22,6 +21,7 @@ require ( github.com/riza-io/grpc-go v0.2.0 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 + github.com/sqlc-dev/doubleclick v0.0.0-20251223195122-0076eee94506 github.com/tetratelabs/wazero v1.10.1 github.com/wasilibs/go-pgquery v0.0.0-20250409022910-10ac41983c07 github.com/xeipuuv/gojsonschema v1.2.0 @@ -34,6 +34,12 @@ require ( require ( cel.dev/expr v0.24.0 // indirect filippo.io/edwards25519 v1.1.0 // indirect + github.com/ClickHouse/ch-go v0.69.0 // indirect + github.com/andybalholm/brotli v1.2.0 // indirect + github.com/cespare/xxhash/v2 v2.3.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/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect github.com/jackc/pgconn v1.14.3 // indirect @@ -43,23 +49,31 @@ 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/ncruces/julianday v1.0.0 // indirect + github.com/paulmach/orb v0.12.0 // indirect + github.com/pierrec/lz4/v4 v4.1.22 // 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.1 // 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.39.0 // indirect + go.opentelemetry.io/otel/trace v1.39.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 - golang.org/x/crypto v0.45.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.46.0 // indirect golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect - golang.org/x/net v0.47.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/text v0.31.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/text v0.32.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect diff --git a/go.sum b/go.sum index f668e5fecf..0d447b26e9 100644 --- a/go.sum +++ b/go.sum @@ -3,10 +3,18 @@ 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/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +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.42.0 h1:MdujEfIrpXesQUH0k0AnuVtJQXk6RZmxEhsKUCcv5xk= +github.com/ClickHouse/clickhouse-go/v2 v2.42.0/go.mod h1:riWnuo4YMVdajYll0q6FzRBomdyCrXyFY3VXeXczA8s= 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= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= @@ -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,18 @@ 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/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/ncruces/go-sqlite3 v0.30.3 h1:X/CgWW9GzmIAkEPrifhKqf0cC15DuOVxAJaHFTTAURQ= github.com/ncruces/go-sqlite3 v0.30.3/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/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/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 +172,12 @@ 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.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= +github.com/segmentio/asm v1.2.1/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.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= @@ -157,6 +185,8 @@ github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiT github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/sqlc-dev/doubleclick v0.0.0-20251223195122-0076eee94506 h1:sZJvXUc0Zsa/NgmqlSROnBXLf8klJn/Z/61f3DYMIoY= +github.com/sqlc-dev/doubleclick v0.0.0-20251223195122-0076eee94506/go.mod h1:ODHRroSrk/rr5neRHlWMSRijqOak8YmNaO3VAZCNl5Y= github.com/sqlc-dev/mysql v0.0.0-20251129233104-d81e1cac6db2 h1:kmCAKKtOgK6EXXQX9oPdEASIhgor7TCpWxD8NtcqVcU= github.com/sqlc-dev/mysql v0.0.0-20251129233104-d81e1cac6db2/go.mod h1:TrDMWzjNTKvJeK2GC8uspG+PWyPLiY9QKvwdWpAdlZE= github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU= @@ -168,34 +198,45 @@ 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/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tetratelabs/wazero v1.10.1 h1:2DugeJf6VVk58KTPszlNfeeN8AhhpwcZqkJj2wwFuH8= github.com/tetratelabs/wazero v1.10.1/go.mod h1:DRm5twOQ5Gr1AoEdSi0CLjDQF1J9ZAuyqFIjl1KKfQU= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= 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/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +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.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= -go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= -go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= -go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= -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.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= @@ -221,6 +262,7 @@ 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= @@ -231,21 +273,30 @@ 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.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= 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/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.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= 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.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -258,10 +309,12 @@ 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= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 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= @@ -269,8 +322,9 @@ 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.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= @@ -279,7 +333,10 @@ 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/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= @@ -295,6 +352,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.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= diff --git a/internal/cmd/process.go b/internal/cmd/process.go index 5003d113b8..422709db4e 100644 --- a/internal/cmd/process.go +++ b/internal/cmd/process.go @@ -88,7 +88,8 @@ func processQuerySets(ctx context.Context, rp ResultProcessor, conf *config.Conf var name, lang string parseOpts := opts.Parser{ - Debug: debug.Debug, + Debug: debug.Debug, + Experiment: o.Env.Experiment, } switch { diff --git a/internal/cmd/vet.go b/internal/cmd/vet.go index 4dbd3c3b7b..c7043a63e2 100644 --- a/internal/cmd/vet.go +++ b/internal/cmd/vet.go @@ -150,6 +150,7 @@ func Vet(ctx context.Context, dir, filename string, opts *Options) error { Stderr: stderr, OnlyManagedDB: e.Debug.OnlyManagedDatabases, Replacer: shfmt.NewReplacer(nil), + Experiment: e.Experiment, } errored := false for _, sql := range conf.SQL { @@ -389,6 +390,7 @@ type checker struct { Client dbmanager.Client clientOnce sync.Once Replacer *shfmt.Replacer + Experiment opts.Experiment } // isInMemorySQLite checks if a SQLite URI refers to an in-memory database @@ -481,7 +483,8 @@ func (c *checker) checkSQL(ctx context.Context, s config.SQL) error { var name string parseOpts := opts.Parser{ - Debug: debug.Debug, + Debug: debug.Debug, + Experiment: c.Experiment, } result, failed := parse(ctx, name, c.Dir, s, combo, parseOpts, c.Stderr) diff --git a/internal/codegen/golang/clickhouse_type.go b/internal/codegen/golang/clickhouse_type.go new file mode 100644 index 0000000000..40a2dd27d5 --- /dev/null +++ b/internal/codegen/golang/clickhouse_type.go @@ -0,0 +1,240 @@ +package golang + +import ( + "log" + "strings" + + "github.com/sqlc-dev/sqlc/internal/codegen/golang/opts" + "github.com/sqlc-dev/sqlc/internal/codegen/sdk" + "github.com/sqlc-dev/sqlc/internal/debug" + "github.com/sqlc-dev/sqlc/internal/plugin" +) + +func clickhouseType(req *plugin.GenerateRequest, options *opts.Options, col *plugin.Column) string { + dt := strings.ToLower(sdk.DataType(col.Type)) + notNull := col.NotNull || col.IsArray + emitPointersForNull := options.EmitPointersForNullTypes + + // Handle Nullable wrapper + if strings.HasPrefix(dt, "nullable(") && strings.HasSuffix(dt, ")") { + dt = dt[9 : len(dt)-1] + notNull = false + } + + // Handle LowCardinality wrapper + if strings.HasPrefix(dt, "lowcardinality(") && strings.HasSuffix(dt, ")") { + dt = dt[15 : len(dt)-1] + } + + switch dt { + // Integer types + case "int8": + if notNull { + return "int8" + } + if emitPointersForNull { + return "*int8" + } + return "sql.NullInt16" // No sql.NullInt8, use Int16 + + case "int16": + if notNull { + return "int16" + } + if emitPointersForNull { + return "*int16" + } + return "sql.NullInt16" + + case "int32": + if notNull { + return "int32" + } + if emitPointersForNull { + return "*int32" + } + return "sql.NullInt32" + + case "int64": + if notNull { + return "int64" + } + if emitPointersForNull { + return "*int64" + } + return "sql.NullInt64" + + case "uint8": + if notNull { + return "uint8" + } + if emitPointersForNull { + return "*uint8" + } + return "sql.NullInt16" // No sql.NullUint8 + + case "uint16": + if notNull { + return "uint16" + } + if emitPointersForNull { + return "*uint16" + } + return "sql.NullInt32" // No sql.NullUint16 + + case "uint32": + if notNull { + return "uint32" + } + if emitPointersForNull { + return "*uint32" + } + return "sql.NullInt64" // No sql.NullUint32 + + case "uint64": + if notNull { + return "uint64" + } + if emitPointersForNull { + return "*uint64" + } + // Note: uint64 doesn't fit in sql.NullInt64 for large values + return "sql.NullInt64" + + // Float types + case "float32": + if notNull { + return "float32" + } + if emitPointersForNull { + return "*float32" + } + return "sql.NullFloat64" + + case "float64": + if notNull { + return "float64" + } + if emitPointersForNull { + return "*float64" + } + return "sql.NullFloat64" + + // String types + case "string": + if notNull { + return "string" + } + if emitPointersForNull { + return "*string" + } + return "sql.NullString" + + // Boolean type + case "bool", "boolean": + if notNull { + return "bool" + } + if emitPointersForNull { + return "*bool" + } + return "sql.NullBool" + + // Date and time types + case "date", "date32": + if notNull { + return "time.Time" + } + if emitPointersForNull { + return "*time.Time" + } + return "sql.NullTime" + + case "datetime", "datetime64": + if notNull { + return "time.Time" + } + if emitPointersForNull { + return "*time.Time" + } + return "sql.NullTime" + + // UUID type + case "uuid": + if notNull { + return "uuid.UUID" + } + if emitPointersForNull { + return "*uuid.UUID" + } + return "uuid.NullUUID" + + // JSON type + case "json": + return "json.RawMessage" + + // Any type (for unknown types) + case "any": + return "interface{}" + + default: + // Handle FixedString(N) + if strings.HasPrefix(dt, "fixedstring") { + if notNull { + return "string" + } + if emitPointersForNull { + return "*string" + } + return "sql.NullString" + } + + // Handle Decimal types + if strings.HasPrefix(dt, "decimal") { + if notNull { + return "float64" + } + if emitPointersForNull { + return "*float64" + } + return "sql.NullFloat64" + } + + // Handle Array types + if strings.HasPrefix(dt, "array(") && strings.HasSuffix(dt, ")") { + innerType := dt[6 : len(dt)-1] + innerCol := &plugin.Column{ + Type: &plugin.Identifier{Name: innerType}, + NotNull: true, + } + return "[]" + clickhouseType(req, options, innerCol) + } + + // Handle Enum types + if strings.HasPrefix(dt, "enum8") || strings.HasPrefix(dt, "enum16") { + if notNull { + return "string" + } + if emitPointersForNull { + return "*string" + } + return "sql.NullString" + } + + // Handle Map types + if strings.HasPrefix(dt, "map(") { + return "map[string]interface{}" + } + + // Handle Tuple types + if strings.HasPrefix(dt, "tuple(") { + return "interface{}" + } + + if debug.Active { + log.Printf("unknown ClickHouse type: %s\n", dt) + } + + return "interface{}" + } +} 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/compiler/engine.go b/internal/compiler/engine.go index 64fdf3d5c7..7811c768b6 100644 --- a/internal/compiler/engine.go +++ b/internal/compiler/engine.go @@ -7,6 +7,8 @@ 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" + clickhouseanalyze "github.com/sqlc-dev/sqlc/internal/engine/clickhouse/analyzer" "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" @@ -111,6 +113,34 @@ func NewCompiler(conf config.SQL, combo config.CombinedSettings, parserOpts opts ) } } + case config.EngineClickHouse: + // ClickHouse requires the clickhouse experiment flag + if !parserOpts.Experiment.ClickHouse { + return nil, fmt.Errorf("clickhouse engine requires SQLCEXPERIMENT=clickhouse") + } + // ClickHouse requires database-only mode + if !conf.Analyzer.Database.IsOnly() { + return nil, fmt.Errorf("clickhouse engine requires analyzer.database: only") + } + if conf.Database == nil { + return nil, fmt.Errorf("clickhouse engine requires database configuration") + } + if conf.Database.URI == "" && !conf.Database.Managed { + return nil, fmt.Errorf("clickhouse engine requires database.uri or database.managed") + } + + parser := clickhouse.NewParser() + c.parser = parser + c.catalog = clickhouse.NewCatalog() + c.selector = newDefaultSelector() + c.databaseOnlyMode = true + + // Create the ClickHouse analyzer + chAnalyzer := clickhouseanalyze.New(*conf.Database) + c.analyzer = analyzer.Cached(chAnalyzer, combo.Global, *conf.Database) + // Create the expander using the analyzer as the column getter + c.expander = expander.New(c.analyzer, parser, parser) + default: return nil, fmt.Errorf("unknown engine: %s", conf.Engine) } diff --git a/internal/config/config.go b/internal/config/config.go index d3e610ef05..59e31ae233 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -51,9 +51,10 @@ func (p *Paths) UnmarshalYAML(unmarshal func(interface{}) error) error { } const ( - EngineMySQL Engine = "mysql" - EnginePostgreSQL Engine = "postgresql" - EngineSQLite Engine = "sqlite" + EngineMySQL Engine = "mysql" + EnginePostgreSQL Engine = "postgresql" + EngineSQLite Engine = "sqlite" + EngineClickHouse Engine = "clickhouse" ) type Config struct { diff --git a/internal/endtoend/endtoend_test.go b/internal/endtoend/endtoend_test.go index 7634918446..f73c45ed12 100644 --- a/internal/endtoend/endtoend_test.go +++ b/internal/endtoend/endtoend_test.go @@ -113,7 +113,7 @@ func TestReplay(t *testing.T) { // t.Parallel() ctx := context.Background() - var mysqlURI, postgresURI string + var mysqlURI, postgresURI, clickhouseURI string // First, check environment variables if uri := os.Getenv("POSTGRESQL_SERVER_URI"); uri != "" { @@ -122,9 +122,12 @@ func TestReplay(t *testing.T) { if uri := os.Getenv("MYSQL_SERVER_URI"); uri != "" { mysqlURI = uri } + if uri := os.Getenv("CLICKHOUSE_SERVER_URI"); uri != "" { + clickhouseURI = uri + } // Try Docker for any missing databases - if postgresURI == "" || mysqlURI == "" { + if postgresURI == "" || mysqlURI == "" || clickhouseURI == "" { if err := docker.Installed(); err == nil { if postgresURI == "" { host, err := docker.StartPostgreSQLServer(ctx) @@ -142,11 +145,19 @@ func TestReplay(t *testing.T) { mysqlURI = host } } + if clickhouseURI == "" { + host, err := docker.StartClickHouseServer(ctx) + if err != nil { + t.Logf("docker clickhouse startup failed: %s", err) + } else { + clickhouseURI = host + } + } } } // Try native installation for any missing databases (Linux only) - if postgresURI == "" || mysqlURI == "" { + if postgresURI == "" || mysqlURI == "" || clickhouseURI == "" { if err := native.Supported(); err == nil { if postgresURI == "" { host, err := native.StartPostgreSQLServer(ctx) @@ -164,12 +175,21 @@ func TestReplay(t *testing.T) { mysqlURI = host } } + if clickhouseURI == "" { + host, err := native.StartClickHouseServer(ctx) + if err != nil { + t.Logf("native clickhouse startup failed: %s", err) + } else { + clickhouseURI = host + } + } } } // Log which databases are available t.Logf("PostgreSQL available: %v (URI: %s)", postgresURI != "", postgresURI) t.Logf("MySQL available: %v (URI: %s)", mysqlURI != "", mysqlURI) + t.Logf("ClickHouse available: %v (URI: %s)", clickhouseURI != "", clickhouseURI) contexts := map[string]textContext{ "base": { @@ -191,6 +211,11 @@ func TestReplay(t *testing.T) { Engine: config.EngineMySQL, URI: mysqlURI, }, + { + Name: "clickhouse", + Engine: config.EngineClickHouse, + URI: clickhouseURI, + }, } for i := range c.SQL { @@ -207,6 +232,12 @@ func TestReplay(t *testing.T) { c.SQL[i].Database = &config.Database{ Managed: true, } + case config.EngineClickHouse: + // ClickHouse uses URI directly (not managed mode) + c.SQL[i].Database = &config.Database{ + URI: clickhouseURI, + Managed: true, + } default: // pass } @@ -215,7 +246,7 @@ func TestReplay(t *testing.T) { }, Enabled: func() bool { // Enabled if at least one database URI is available - return postgresURI != "" || mysqlURI != "" + return postgresURI != "" || mysqlURI != "" || clickhouseURI != "" }, }, } diff --git a/internal/endtoend/testdata/accurate_cte/postgresql/stdlib/go/models.go b/internal/endtoend/testdata/accurate_cte/postgresql/stdlib/go/models.go index 90b88c3389..333ea43ea3 100644 --- a/internal/endtoend/testdata/accurate_cte/postgresql/stdlib/go/models.go +++ b/internal/endtoend/testdata/accurate_cte/postgresql/stdlib/go/models.go @@ -3,9 +3,3 @@ // sqlc v1.30.0 package querytest - -type Product struct { - ID int32 - Name string - Price string -} diff --git a/internal/endtoend/testdata/accurate_cte/postgresql/stdlib/go/query.sql.go b/internal/endtoend/testdata/accurate_cte/postgresql/stdlib/go/query.sql.go index 8d31d41cdf..c346f35af0 100644 --- a/internal/endtoend/testdata/accurate_cte/postgresql/stdlib/go/query.sql.go +++ b/internal/endtoend/testdata/accurate_cte/postgresql/stdlib/go/query.sql.go @@ -7,18 +7,16 @@ package querytest import ( "context" + "database/sql" ) const getProductStats = `-- name: GetProductStats :one -WITH product_stats AS ( - SELECT COUNT(*) as total, AVG(price) as avg_price FROM products -) -SELECT total, avg_price FROM product_stats +WITH product_stats AS (SELECT count(*) AS total, avg(price) AS avg_price FROM products) SELECT total, avg_price FROM product_stats; ` type GetProductStatsRow struct { - Total int64 - AvgPrice string + Total sql.NullInt64 + AvgPrice sql.NullString } func (q *Queries) GetProductStats(ctx context.Context) (GetProductStatsRow, error) { @@ -29,16 +27,13 @@ func (q *Queries) GetProductStats(ctx context.Context) (GetProductStatsRow, erro } const listExpensiveProducts = `-- name: ListExpensiveProducts :many -WITH expensive AS ( - SELECT id, name, price FROM products WHERE price > 100 -) -SELECT id, name, price FROM expensive +WITH expensive AS (SELECT id, name, price FROM products WHERE price > 100) SELECT id, name, price FROM expensive; ` type ListExpensiveProductsRow struct { ID int32 Name string - Price string + Price interface{} } func (q *Queries) ListExpensiveProducts(ctx context.Context) ([]ListExpensiveProductsRow, error) { diff --git a/internal/endtoend/testdata/accurate_enum/postgresql/stdlib/go/models.go b/internal/endtoend/testdata/accurate_enum/postgresql/stdlib/go/models.go index 2b42787339..333ea43ea3 100644 --- a/internal/endtoend/testdata/accurate_enum/postgresql/stdlib/go/models.go +++ b/internal/endtoend/testdata/accurate_enum/postgresql/stdlib/go/models.go @@ -3,57 +3,3 @@ // sqlc v1.30.0 package querytest - -import ( - "database/sql/driver" - "fmt" -) - -type Status string - -const ( - StatusPending Status = "pending" - StatusActive Status = "active" - StatusCompleted Status = "completed" -) - -func (e *Status) Scan(src interface{}) error { - switch s := src.(type) { - case []byte: - *e = Status(s) - case string: - *e = Status(s) - default: - return fmt.Errorf("unsupported scan type for Status: %T", src) - } - return nil -} - -type NullStatus struct { - Status Status - Valid bool // Valid is true if Status is not NULL -} - -// Scan implements the Scanner interface. -func (ns *NullStatus) Scan(value interface{}) error { - if value == nil { - ns.Status, ns.Valid = "", false - return nil - } - ns.Valid = true - return ns.Status.Scan(value) -} - -// Value implements the driver Valuer interface. -func (ns NullStatus) Value() (driver.Value, error) { - if !ns.Valid { - return nil, nil - } - return string(ns.Status), nil -} - -type Task struct { - ID int32 - Title string - Status Status -} diff --git a/internal/endtoend/testdata/accurate_enum/postgresql/stdlib/go/query.sql.go b/internal/endtoend/testdata/accurate_enum/postgresql/stdlib/go/query.sql.go index 263a6b6736..139f6db7d0 100644 --- a/internal/endtoend/testdata/accurate_enum/postgresql/stdlib/go/query.sql.go +++ b/internal/endtoend/testdata/accurate_enum/postgresql/stdlib/go/query.sql.go @@ -7,37 +7,50 @@ package querytest import ( "context" + "database/sql" ) const createTask = `-- name: CreateTask :one -INSERT INTO tasks (title, status) VALUES ($1, $2) RETURNING id, title, status +INSERT INTO tasks (title, status) VALUES ($1, $2) RETURNING id, title, status; ` type CreateTaskParams struct { + Column1 sql.NullString + Column2 interface{} +} + +type CreateTaskRow struct { + ID int32 Title string - Status Status + Status interface{} } -func (q *Queries) CreateTask(ctx context.Context, arg CreateTaskParams) (Task, error) { - row := q.db.QueryRowContext(ctx, createTask, arg.Title, arg.Status) - var i Task +func (q *Queries) CreateTask(ctx context.Context, arg CreateTaskParams) (CreateTaskRow, error) { + row := q.db.QueryRowContext(ctx, createTask, arg.Column1, arg.Column2) + var i CreateTaskRow err := row.Scan(&i.ID, &i.Title, &i.Status) return i, err } const getTasksByStatus = `-- name: GetTasksByStatus :many -SELECT id, title, status FROM tasks WHERE status = $1 +SELECT id, title, status FROM tasks WHERE status = $1; ` -func (q *Queries) GetTasksByStatus(ctx context.Context, status Status) ([]Task, error) { - rows, err := q.db.QueryContext(ctx, getTasksByStatus, status) +type GetTasksByStatusRow struct { + ID int32 + Title string + Status interface{} +} + +func (q *Queries) GetTasksByStatus(ctx context.Context, dollar_1 interface{}) ([]GetTasksByStatusRow, error) { + rows, err := q.db.QueryContext(ctx, getTasksByStatus, dollar_1) if err != nil { return nil, err } defer rows.Close() - var items []Task + var items []GetTasksByStatusRow for rows.Next() { - var i Task + var i GetTasksByStatusRow if err := rows.Scan(&i.ID, &i.Title, &i.Status); err != nil { return nil, err } @@ -53,18 +66,24 @@ func (q *Queries) GetTasksByStatus(ctx context.Context, status Status) ([]Task, } const listTasks = `-- name: ListTasks :many -SELECT id, title, status FROM tasks +SELECT id, title, status FROM tasks; ` -func (q *Queries) ListTasks(ctx context.Context) ([]Task, error) { +type ListTasksRow struct { + ID int32 + Title string + Status interface{} +} + +func (q *Queries) ListTasks(ctx context.Context) ([]ListTasksRow, error) { rows, err := q.db.QueryContext(ctx, listTasks) if err != nil { return nil, err } defer rows.Close() - var items []Task + var items []ListTasksRow for rows.Next() { - var i Task + var i ListTasksRow if err := rows.Scan(&i.ID, &i.Title, &i.Status); err != nil { return nil, err } diff --git a/internal/endtoend/testdata/accurate_sqlite/sqlite/stdlib/go/models.go b/internal/endtoend/testdata/accurate_sqlite/sqlite/stdlib/go/models.go index eaf05e5c00..333ea43ea3 100644 --- a/internal/endtoend/testdata/accurate_sqlite/sqlite/stdlib/go/models.go +++ b/internal/endtoend/testdata/accurate_sqlite/sqlite/stdlib/go/models.go @@ -3,13 +3,3 @@ // sqlc v1.30.0 package querytest - -import ( - "database/sql" -) - -type Author struct { - ID int64 - Name string - Bio sql.NullString -} diff --git a/internal/endtoend/testdata/accurate_sqlite/sqlite/stdlib/go/query.sql.go b/internal/endtoend/testdata/accurate_sqlite/sqlite/stdlib/go/query.sql.go index 203224ead2..4b4e2cc186 100644 --- a/internal/endtoend/testdata/accurate_sqlite/sqlite/stdlib/go/query.sql.go +++ b/internal/endtoend/testdata/accurate_sqlite/sqlite/stdlib/go/query.sql.go @@ -11,45 +11,63 @@ import ( ) const createAuthor = `-- name: CreateAuthor :one -INSERT INTO authors (name, bio) VALUES (?, ?) RETURNING id, name, bio +INSERT INTO authors (name, bio) VALUES (?, ?) RETURNING id, name, bio; ` type CreateAuthorParams struct { - Name string + Column1 interface{} + Column2 interface{} +} + +type CreateAuthorRow struct { + ID sql.NullInt64 + Name sql.NullString Bio sql.NullString } -func (q *Queries) CreateAuthor(ctx context.Context, arg CreateAuthorParams) (Author, error) { - row := q.db.QueryRowContext(ctx, createAuthor, arg.Name, arg.Bio) - var i Author +func (q *Queries) CreateAuthor(ctx context.Context, arg CreateAuthorParams) (CreateAuthorRow, error) { + row := q.db.QueryRowContext(ctx, createAuthor, arg.Column1, arg.Column2) + var i CreateAuthorRow err := row.Scan(&i.ID, &i.Name, &i.Bio) return i, err } const getAuthor = `-- name: GetAuthor :one -SELECT id, name, bio FROM authors WHERE id = ? +SELECT id, name, bio FROM authors WHERE id = ?; ` -func (q *Queries) GetAuthor(ctx context.Context, id int64) (Author, error) { - row := q.db.QueryRowContext(ctx, getAuthor, id) - var i Author +type GetAuthorRow struct { + ID sql.NullInt64 + Name sql.NullString + Bio sql.NullString +} + +func (q *Queries) GetAuthor(ctx context.Context, dollar_1 interface{}) (GetAuthorRow, error) { + row := q.db.QueryRowContext(ctx, getAuthor, dollar_1) + var i GetAuthorRow err := row.Scan(&i.ID, &i.Name, &i.Bio) return i, err } const listAuthors = `-- name: ListAuthors :many -SELECT id, name, bio FROM authors +SELECT id, name, bio FROM authors; ` -func (q *Queries) ListAuthors(ctx context.Context) ([]Author, error) { +type ListAuthorsRow struct { + ID sql.NullInt64 + Name sql.NullString + Bio sql.NullString +} + +func (q *Queries) ListAuthors(ctx context.Context) ([]ListAuthorsRow, error) { rows, err := q.db.QueryContext(ctx, listAuthors) if err != nil { return nil, err } defer rows.Close() - var items []Author + var items []ListAuthorsRow for rows.Next() { - var i Author + var i ListAuthorsRow if err := rows.Scan(&i.ID, &i.Name, &i.Bio); err != nil { return nil, err } diff --git a/internal/endtoend/testdata/accurate_star_expansion/postgresql/stdlib/go/models.go b/internal/endtoend/testdata/accurate_star_expansion/postgresql/stdlib/go/models.go index ec1cb8d670..333ea43ea3 100644 --- a/internal/endtoend/testdata/accurate_star_expansion/postgresql/stdlib/go/models.go +++ b/internal/endtoend/testdata/accurate_star_expansion/postgresql/stdlib/go/models.go @@ -3,13 +3,3 @@ // sqlc v1.30.0 package querytest - -import ( - "database/sql" -) - -type Author struct { - ID int32 - Name string - Bio sql.NullString -} diff --git a/internal/endtoend/testdata/accurate_star_expansion/postgresql/stdlib/go/query.sql.go b/internal/endtoend/testdata/accurate_star_expansion/postgresql/stdlib/go/query.sql.go index 9e2820cdbd..56c7d95b44 100644 --- a/internal/endtoend/testdata/accurate_star_expansion/postgresql/stdlib/go/query.sql.go +++ b/internal/endtoend/testdata/accurate_star_expansion/postgresql/stdlib/go/query.sql.go @@ -11,56 +11,80 @@ import ( ) const createAuthor = `-- name: CreateAuthor :one -INSERT INTO authors (name, bio) VALUES ($1, $2) RETURNING id, name, bio +INSERT INTO authors (name, bio) VALUES ($1, $2) RETURNING id, name, bio; ` type CreateAuthorParams struct { + Column1 sql.NullString + Column2 sql.NullString +} + +type CreateAuthorRow struct { + ID int32 Name string Bio sql.NullString } -func (q *Queries) CreateAuthor(ctx context.Context, arg CreateAuthorParams) (Author, error) { - row := q.db.QueryRowContext(ctx, createAuthor, arg.Name, arg.Bio) - var i Author +func (q *Queries) CreateAuthor(ctx context.Context, arg CreateAuthorParams) (CreateAuthorRow, error) { + row := q.db.QueryRowContext(ctx, createAuthor, arg.Column1, arg.Column2) + var i CreateAuthorRow err := row.Scan(&i.ID, &i.Name, &i.Bio) return i, err } const deleteAuthor = `-- name: DeleteAuthor :one -DELETE FROM authors WHERE id = $1 RETURNING id, name, bio +DELETE FROM authors WHERE id = $1 RETURNING id, name, bio; ` -func (q *Queries) DeleteAuthor(ctx context.Context, id int32) (Author, error) { - row := q.db.QueryRowContext(ctx, deleteAuthor, id) - var i Author +type DeleteAuthorRow struct { + ID int32 + Name string + Bio sql.NullString +} + +func (q *Queries) DeleteAuthor(ctx context.Context, dollar_1 sql.NullInt32) (DeleteAuthorRow, error) { + row := q.db.QueryRowContext(ctx, deleteAuthor, dollar_1) + var i DeleteAuthorRow err := row.Scan(&i.ID, &i.Name, &i.Bio) return i, err } const getAuthor = `-- name: GetAuthor :one -SELECT id, name, bio FROM authors WHERE id = $1 +SELECT id, name, bio FROM authors WHERE id = $1; ` -func (q *Queries) GetAuthor(ctx context.Context, id int32) (Author, error) { - row := q.db.QueryRowContext(ctx, getAuthor, id) - var i Author +type GetAuthorRow struct { + ID int32 + Name string + Bio sql.NullString +} + +func (q *Queries) GetAuthor(ctx context.Context, dollar_1 sql.NullInt32) (GetAuthorRow, error) { + row := q.db.QueryRowContext(ctx, getAuthor, dollar_1) + var i GetAuthorRow err := row.Scan(&i.ID, &i.Name, &i.Bio) return i, err } const listAuthors = `-- name: ListAuthors :many -SELECT id, name, bio FROM authors +SELECT id, name, bio FROM authors; ` -func (q *Queries) ListAuthors(ctx context.Context) ([]Author, error) { +type ListAuthorsRow struct { + ID int32 + Name string + Bio sql.NullString +} + +func (q *Queries) ListAuthors(ctx context.Context) ([]ListAuthorsRow, error) { rows, err := q.db.QueryContext(ctx, listAuthors) if err != nil { return nil, err } defer rows.Close() - var items []Author + var items []ListAuthorsRow for rows.Next() { - var i Author + var i ListAuthorsRow if err := rows.Scan(&i.ID, &i.Name, &i.Bio); err != nil { return nil, err } @@ -76,18 +100,24 @@ func (q *Queries) ListAuthors(ctx context.Context) ([]Author, error) { } const updateAuthor = `-- name: UpdateAuthor :one -UPDATE authors SET name = $1, bio = $2 WHERE id = $3 RETURNING id, name, bio +UPDATE authors SET name = $1, bio = $2 WHERE id = $3 RETURNING id, name, bio; ` type UpdateAuthorParams struct { + Column1 sql.NullString + Column2 sql.NullString + Column3 sql.NullInt32 +} + +type UpdateAuthorRow struct { + ID int32 Name string Bio sql.NullString - ID int32 } -func (q *Queries) UpdateAuthor(ctx context.Context, arg UpdateAuthorParams) (Author, error) { - row := q.db.QueryRowContext(ctx, updateAuthor, arg.Name, arg.Bio, arg.ID) - var i Author +func (q *Queries) UpdateAuthor(ctx context.Context, arg UpdateAuthorParams) (UpdateAuthorRow, error) { + row := q.db.QueryRowContext(ctx, updateAuthor, arg.Column1, arg.Column2, arg.Column3) + var i UpdateAuthorRow err := row.Scan(&i.ID, &i.Name, &i.Bio) return i, err } diff --git a/internal/endtoend/testdata/clickhouse_authors/clickhouse/stdlib/exec.json b/internal/endtoend/testdata/clickhouse_authors/clickhouse/stdlib/exec.json new file mode 100644 index 0000000000..8fb1c91d33 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_authors/clickhouse/stdlib/exec.json @@ -0,0 +1,6 @@ +{ + "contexts": ["managed-db"], + "env": { + "SQLCEXPERIMENT": "clickhouse" + } +} diff --git a/internal/endtoend/testdata/clickhouse_authors/clickhouse/stdlib/go/db.go b/internal/endtoend/testdata/clickhouse_authors/clickhouse/stdlib/go/db.go new file mode 100644 index 0000000000..3b320aa168 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_authors/clickhouse/stdlib/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/clickhouse_authors/clickhouse/stdlib/go/models.go b/internal/endtoend/testdata/clickhouse_authors/clickhouse/stdlib/go/models.go new file mode 100644 index 0000000000..333ea43ea3 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_authors/clickhouse/stdlib/go/models.go @@ -0,0 +1,5 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package querytest diff --git a/internal/endtoend/testdata/clickhouse_authors/clickhouse/stdlib/go/query.sql.go b/internal/endtoend/testdata/clickhouse_authors/clickhouse/stdlib/go/query.sql.go new file mode 100644 index 0000000000..54da26396c --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_authors/clickhouse/stdlib/go/query.sql.go @@ -0,0 +1,76 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: query.sql + +package querytest + +import ( + "context" + "database/sql" +) + +const createAuthor = `-- name: CreateAuthor :exec +INSERT INTO authors (id, name, bio) VALUES ({id:UInt64}, {name:String}, {bio:String}); +` + +type CreateAuthorParams struct { + ID uint64 + Name string + Bio string +} + +func (q *Queries) CreateAuthor(ctx context.Context, arg CreateAuthorParams) error { + _, err := q.db.ExecContext(ctx, createAuthor, arg.ID, arg.Name, arg.Bio) + return err +} + +const getAuthor = `-- name: GetAuthor :one +SELECT id, name, bio FROM authors WHERE id = {id:UInt64}; +` + +type GetAuthorRow struct { + ID uint64 + Name string + Bio sql.NullString +} + +func (q *Queries) GetAuthor(ctx context.Context, id uint64) (GetAuthorRow, error) { + row := q.db.QueryRowContext(ctx, getAuthor, id) + var i GetAuthorRow + err := row.Scan(&i.ID, &i.Name, &i.Bio) + return i, err +} + +const listAuthors = `-- name: ListAuthors :many +SELECT id, name, bio FROM authors ORDER BY name; +` + +type ListAuthorsRow struct { + ID uint64 + Name string + Bio sql.NullString +} + +func (q *Queries) ListAuthors(ctx context.Context) ([]ListAuthorsRow, error) { + rows, err := q.db.QueryContext(ctx, listAuthors) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListAuthorsRow + for rows.Next() { + var i ListAuthorsRow + if err := rows.Scan(&i.ID, &i.Name, &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_authors/clickhouse/stdlib/query.sql b/internal/endtoend/testdata/clickhouse_authors/clickhouse/stdlib/query.sql new file mode 100644 index 0000000000..ddf5a326fd --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_authors/clickhouse/stdlib/query.sql @@ -0,0 +1,8 @@ +-- name: GetAuthor :one +SELECT id, name, bio FROM authors WHERE id = {id:UInt64}; + +-- name: ListAuthors :many +SELECT id, name, bio FROM authors ORDER BY name; + +-- name: CreateAuthor :exec +INSERT INTO authors (id, name, bio) VALUES ({id:UInt64}, {name:String}, {bio:String}); diff --git a/internal/endtoend/testdata/clickhouse_authors/clickhouse/stdlib/schema.sql b/internal/endtoend/testdata/clickhouse_authors/clickhouse/stdlib/schema.sql new file mode 100644 index 0000000000..2185b5a3c1 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_authors/clickhouse/stdlib/schema.sql @@ -0,0 +1,6 @@ +CREATE TABLE authors ( + id UInt64, + name String, + bio Nullable(String) +) ENGINE = MergeTree() +ORDER BY id; diff --git a/internal/endtoend/testdata/clickhouse_authors/clickhouse/stdlib/sqlc.yaml b/internal/endtoend/testdata/clickhouse_authors/clickhouse/stdlib/sqlc.yaml new file mode 100644 index 0000000000..209d9835c5 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_authors/clickhouse/stdlib/sqlc.yaml @@ -0,0 +1,13 @@ +version: "2" +sql: + - engine: clickhouse + schema: "schema.sql" + queries: "query.sql" + database: + uri: "clickhouse://localhost:9000/default" + analyzer: + database: "only" + gen: + go: + package: "querytest" + out: "go" diff --git a/internal/engine/clickhouse/analyzer/analyze.go b/internal/engine/clickhouse/analyzer/analyze.go new file mode 100644 index 0000000000..5f8894a231 --- /dev/null +++ b/internal/engine/clickhouse/analyzer/analyze.go @@ -0,0 +1,673 @@ +package analyzer + +import ( + "context" + "database/sql" + "fmt" + "regexp" + "strings" + "sync" + + _ "github.com/ClickHouse/clickhouse-go/v2" // ClickHouse driver + dcast "github.com/sqlc-dev/doubleclick/ast" + "github.com/sqlc-dev/doubleclick/parser" + + core "github.com/sqlc-dev/sqlc/internal/analysis" + "github.com/sqlc-dev/sqlc/internal/config" + "github.com/sqlc-dev/sqlc/internal/opts" + "github.com/sqlc-dev/sqlc/internal/shfmt" + "github.com/sqlc-dev/sqlc/internal/sql/ast" + "github.com/sqlc-dev/sqlc/internal/sql/catalog" + "github.com/sqlc-dev/sqlc/internal/sql/named" + "github.com/sqlc-dev/sqlc/internal/sql/sqlerr" +) + +// Analyzer implements the analyzer.Analyzer interface for ClickHouse. +type Analyzer struct { + db config.Database + conn *sql.DB + dbg opts.Debug + replacer *shfmt.Replacer + mu sync.Mutex +} + +// New creates a new ClickHouse analyzer. +func New(db config.Database) *Analyzer { + return &Analyzer{ + db: db, + dbg: opts.DebugFromEnv(), + replacer: shfmt.NewReplacer(nil), + } +} + +// Analyze analyzes a query and returns column and parameter information. +func (a *Analyzer) Analyze(ctx context.Context, n ast.Node, query string, migrations []string, ps *named.ParamSet) (*core.Analysis, error) { + a.mu.Lock() + defer a.mu.Unlock() + + if a.conn == nil { + if err := a.connect(ctx, migrations); err != nil { + return nil, err + } + } + + var result core.Analysis + + // Check if this is a SELECT query that returns columns + // INSERT, UPDATE, DELETE don't return columns + isSelectQuery := isSelectStatement(query) + + if isSelectQuery { + // For ClickHouse, we use DESCRIBE or LIMIT 0 to get column information + + // Replace all parameter placeholders with NULL for introspection + // This handles both ? placeholders and {name:Type} named parameters + preparedQuery := replaceParamsWithNull(query) + + // Use DESCRIBE (query) to get column information + describeQuery := fmt.Sprintf("DESCRIBE (%s)", preparedQuery) + rows, err := a.conn.QueryContext(ctx, describeQuery) + if err != nil { + // If DESCRIBE fails, try executing with LIMIT 0 + limitQuery := addLimit0(preparedQuery) + rows, err = a.conn.QueryContext(ctx, limitQuery) + if err != nil { + return nil, a.extractSqlErr(n, err) + } + } + defer rows.Close() + + // Get column information from result set + colTypes, err := rows.ColumnTypes() + if err != nil { + return nil, a.extractSqlErr(n, err) + } + + for i, colType := range colTypes { + name := colType.Name() + dataType := colType.DatabaseTypeName() + nullable, _ := colType.Nullable() + + col := &core.Column{ + Name: name, + OriginalName: name, + DataType: normalizeType(dataType), + NotNull: !nullable, + } + + // Try to detect if this is an aggregate function result + // (ClickHouse doesn't always provide this info) + _ = i + + result.Columns = append(result.Columns, col) + } + } + + // Detect parameters in the query + // ClickHouse uses {name:Type} syntax or $1, $2, etc. + params := detectParameters(query) + for i, param := range params { + result.Params = append(result.Params, &core.Parameter{ + Number: int32(i + 1), + Column: &core.Column{ + Name: param.Name, + DataType: param.Type, + NotNull: true, // Parameters are typically not nullable + }, + }) + } + + // Override with named params if provided + if ps != nil { + for i := range result.Params { + if name, ok := ps.NameFor(i + 1); ok { + result.Params[i].Column.Name = name + } + } + } + + return &result, nil +} + +func (a *Analyzer) connect(ctx context.Context, migrations []string) error { + if a.dbg.OnlyManagedDatabases && !a.db.Managed { + return fmt.Errorf("database: connections disabled via SQLCDEBUG=databases=managed") + } + + uri := a.replacer.Replace(a.db.URI) + if uri == "" { + return fmt.Errorf("clickhouse: database URI is required") + } + + conn, err := sql.Open("clickhouse", uri) + if err != nil { + return fmt.Errorf("failed to connect to clickhouse: %w", err) + } + + // Verify connection + if err := conn.PingContext(ctx); err != nil { + conn.Close() + return fmt.Errorf("failed to ping clickhouse: %w", err) + } + + a.conn = conn + + // Apply migrations for managed databases + if a.db.Managed { + for _, m := range migrations { + if len(strings.TrimSpace(m)) == 0 { + continue + } + // For CREATE TABLE statements, drop the table first if it exists + upper := strings.ToUpper(strings.TrimSpace(m)) + if strings.HasPrefix(upper, "CREATE TABLE") { + // Extract table name and drop it first + parts := strings.Fields(m) + if len(parts) >= 3 { + tableName := parts[2] + // Remove any trailing characters like "(" + tableName = strings.TrimSuffix(tableName, "(") + a.conn.ExecContext(ctx, "DROP TABLE IF EXISTS "+tableName) + } + } + if _, err := a.conn.ExecContext(ctx, m); err != nil { + a.conn.Close() + a.conn = nil + return fmt.Errorf("migration failed: %s: %w", m, err) + } + } + } + + return nil +} + +func (a *Analyzer) extractSqlErr(n ast.Node, err error) error { + if err == nil { + return nil + } + return &sqlerr.Error{ + Message: err.Error(), + Location: n.Pos(), + } +} + +// Close closes the database connection. +func (a *Analyzer) Close(_ context.Context) error { + a.mu.Lock() + defer a.mu.Unlock() + if a.conn != nil { + err := a.conn.Close() + a.conn = nil + return err + } + return nil +} + +// EnsureConn initializes the database connection if not already done. +func (a *Analyzer) EnsureConn(ctx context.Context, migrations []string) error { + a.mu.Lock() + defer a.mu.Unlock() + + if a.conn != nil { + return nil + } + + return a.connect(ctx, migrations) +} + +// GetColumnNames returns the column names for a query. +func (a *Analyzer) GetColumnNames(ctx context.Context, query string) ([]string, error) { + a.mu.Lock() + defer a.mu.Unlock() + + if a.conn == nil { + return nil, fmt.Errorf("database connection not initialized") + } + + // Replace ? placeholders with NULL for introspection + preparedQuery := strings.ReplaceAll(query, "?", "NULL") + + // Use DESCRIBE (query) to get column information + describeQuery := fmt.Sprintf("DESCRIBE (%s)", preparedQuery) + rows, err := a.conn.QueryContext(ctx, describeQuery) + if err != nil { + // Fallback to LIMIT 0 if DESCRIBE fails + limitQuery := addLimit0(preparedQuery) + rows, err = a.conn.QueryContext(ctx, limitQuery) + if err != nil { + return nil, err + } + } + defer rows.Close() + + columns, err := rows.Columns() + if err != nil { + return nil, err + } + + return columns, nil +} + +// IntrospectSchema queries the database to build a catalog containing tables and columns. +func (a *Analyzer) IntrospectSchema(ctx context.Context, schemas []string) (*catalog.Catalog, error) { + a.mu.Lock() + defer a.mu.Unlock() + + if a.conn == nil { + return nil, fmt.Errorf("database connection not initialized") + } + + cat := &catalog.Catalog{ + DefaultSchema: "default", + } + + // Get current database + var currentDB string + if err := a.conn.QueryRowContext(ctx, "SELECT currentDatabase()").Scan(¤tDB); err != nil { + currentDB = "default" + } + + // Create default schema + mainSchema := &catalog.Schema{Name: currentDB} + cat.Schemas = append(cat.Schemas, mainSchema) + + // Query tables from system.tables + tableQuery := `SELECT name FROM system.tables WHERE database = currentDatabase() AND engine != 'View'` + rows, err := a.conn.QueryContext(ctx, tableQuery) + if err != nil { + return nil, fmt.Errorf("introspect tables: %w", err) + } + defer rows.Close() + + var tableNames []string + for rows.Next() { + var name string + if err := rows.Scan(&name); err != nil { + return nil, err + } + tableNames = append(tableNames, name) + } + rows.Close() + + // For each table, get column information from system.columns + for _, tableName := range tableNames { + tbl := &catalog.Table{ + Rel: &ast.TableName{ + Name: tableName, + }, + } + + colQuery := `SELECT name, type, default_kind FROM system.columns WHERE database = currentDatabase() AND table = ?` + colRows, err := a.conn.QueryContext(ctx, colQuery, tableName) + if err != nil { + return nil, fmt.Errorf("introspect columns for %s: %w", tableName, err) + } + + for colRows.Next() { + var name, colType, defaultKind string + if err := colRows.Scan(&name, &colType, &defaultKind); err != nil { + colRows.Close() + return nil, err + } + + // Determine if column is NOT NULL + notNull := !isNullable(colType) + + tbl.Columns = append(tbl.Columns, &catalog.Column{ + Name: name, + Type: ast.TypeName{Name: normalizeType(colType)}, + IsNotNull: notNull, + }) + } + colRows.Close() + + mainSchema.Tables = append(mainSchema.Tables, tbl) + } + + return cat, nil +} + +// paramInfo holds information about a detected parameter. +type paramInfo struct { + Name string + Type string +} + +// detectParameters finds parameters in a ClickHouse query using the doubleclick parser. +// ClickHouse supports {name:Type} and ? style parameters. +func detectParameters(query string) []paramInfo { + var params []paramInfo + + ctx := context.Background() + + // First, try to parse and walk the query AST for named parameters + stmts, err := parser.Parse(ctx, strings.NewReader(query)) + if err == nil { + for _, stmt := range stmts { + walkStatement(stmt, func(expr dcast.Expression) { + if param, ok := expr.(*dcast.Parameter); ok { + if param.Name != "" { + dataType := "any" + if param.Type != nil { + dataType = normalizeType(param.Type.Name) + } + params = append(params, paramInfo{ + Name: param.Name, + Type: dataType, + }) + } + } + }) + } + } + + // If no named parameters found from AST, try to extract VALUES clause for INSERT statements + // The doubleclick parser doesn't fully parse VALUES, so we parse it as a SELECT + if len(params) == 0 { + params = extractValuesParameters(ctx, query) + } + + // Count ? placeholders and add them after any named parameters + count := strings.Count(query, "?") + for i := 0; i < count; i++ { + params = append(params, paramInfo{ + Name: fmt.Sprintf("p%d", len(params)+1), + Type: "any", + }) + } + + return params +} + +// extractValuesParameters extracts parameters from INSERT VALUES clause by parsing it as a SELECT. +// This works around the limitation that doubleclick doesn't parse VALUES clause expressions. +func extractValuesParameters(ctx context.Context, query string) []paramInfo { + var params []paramInfo + + // Find VALUES clause (case insensitive) + upperQuery := strings.ToUpper(query) + valuesIdx := strings.Index(upperQuery, "VALUES") + if valuesIdx == -1 { + return params + } + + // Extract everything after VALUES + valuesClause := query[valuesIdx+6:] + + // Find the parentheses containing the values + start := strings.Index(valuesClause, "(") + if start == -1 { + return params + } + + // Find matching closing parenthesis + depth := 0 + end := -1 + for i := start; i < len(valuesClause); i++ { + switch valuesClause[i] { + case '(': + depth++ + case ')': + depth-- + if depth == 0 { + end = i + break + } + } + if end != -1 { + break + } + } + + if end == -1 { + return params + } + + // Extract the values list and convert to SELECT query + valuesList := valuesClause[start+1 : end] + selectQuery := "SELECT " + valuesList + + // Parse the synthetic SELECT query + stmts, err := parser.Parse(ctx, strings.NewReader(selectQuery)) + if err != nil { + return params + } + + // Walk the AST to find Parameter nodes + for _, stmt := range stmts { + walkStatement(stmt, func(expr dcast.Expression) { + if param, ok := expr.(*dcast.Parameter); ok { + if param.Name != "" { + dataType := "any" + if param.Type != nil { + dataType = normalizeType(param.Type.Name) + } + params = append(params, paramInfo{ + Name: param.Name, + Type: dataType, + }) + } + } + }) + } + + return params +} + +// walkStatement walks a statement and calls fn for each expression. +func walkStatement(stmt dcast.Statement, fn func(dcast.Expression)) { + switch s := stmt.(type) { + case *dcast.SelectQuery: + walkSelectQuery(s, fn) + case *dcast.SelectWithUnionQuery: + for _, sel := range s.Selects { + walkStatement(sel, fn) + } + case *dcast.InsertQuery: + if s.Select != nil { + walkStatement(s.Select, fn) + } + } +} + +// walkSelectQuery walks a SELECT query and calls fn for each expression. +func walkSelectQuery(s *dcast.SelectQuery, fn func(dcast.Expression)) { + // Walk columns + for _, col := range s.Columns { + walkExpression(col, fn) + } + // Walk WHERE clause + if s.Where != nil { + walkExpression(s.Where, fn) + } + // Walk GROUP BY + for _, g := range s.GroupBy { + walkExpression(g, fn) + } + // Walk HAVING + if s.Having != nil { + walkExpression(s.Having, fn) + } + // Walk ORDER BY + for _, o := range s.OrderBy { + walkExpression(o.Expression, fn) + } + // Walk LIMIT + if s.Limit != nil { + walkExpression(s.Limit, fn) + } + // Walk OFFSET + if s.Offset != nil { + walkExpression(s.Offset, fn) + } +} + +// walkExpression walks an expression and calls fn for each sub-expression. +func walkExpression(expr dcast.Expression, fn func(dcast.Expression)) { + if expr == nil { + return + } + fn(expr) + + switch e := expr.(type) { + case *dcast.BinaryExpr: + walkExpression(e.Left, fn) + walkExpression(e.Right, fn) + case *dcast.UnaryExpr: + walkExpression(e.Operand, fn) + case *dcast.FunctionCall: + for _, arg := range e.Arguments { + walkExpression(arg, fn) + } + case *dcast.Subquery: + walkStatement(e.Query, fn) + case *dcast.CaseExpr: + if e.Operand != nil { + walkExpression(e.Operand, fn) + } + for _, when := range e.Whens { + walkExpression(when.Condition, fn) + walkExpression(when.Result, fn) + } + if e.Else != nil { + walkExpression(e.Else, fn) + } + case *dcast.InExpr: + walkExpression(e.Expr, fn) + for _, v := range e.List { + walkExpression(v, fn) + } + if e.Query != nil { + walkStatement(e.Query, fn) + } + case *dcast.BetweenExpr: + walkExpression(e.Expr, fn) + walkExpression(e.Low, fn) + walkExpression(e.High, fn) + } +} + +// namedParamRegex matches ClickHouse named parameters like {name:Type} +var namedParamRegex = regexp.MustCompile(`\{(\w+):(\w+)\}`) + +// replaceParamsWithNull replaces all parameter placeholders with NULL for query introspection. +// It handles both ? placeholders and {name:Type} named parameters. +func replaceParamsWithNull(query string) string { + // Replace {name:Type} named parameters with NULL + result := namedParamRegex.ReplaceAllString(query, "NULL") + // Also replace ? placeholders with NULL + result = strings.ReplaceAll(result, "?", "NULL") + return result +} + +// addLimit0 adds LIMIT 0 to a query for schema introspection. +func addLimit0(query string) string { + // Simple approach: append LIMIT 0 if not already present + lower := strings.ToLower(query) + if strings.Contains(lower, "limit") { + return query + } + + // Remove trailing semicolon and whitespace + trimmed := strings.TrimRight(query, " \t\n\r;") + + return trimmed + " LIMIT 0" +} + +// isSelectStatement checks if a query is a SELECT statement that returns columns. +// It skips past comment lines to find the actual statement. +func isSelectStatement(query string) bool { + lines := strings.Split(query, "\n") + for _, line := range lines { + trimmed := strings.TrimSpace(line) + // Skip empty lines + if trimmed == "" { + continue + } + // Skip comment lines + if strings.HasPrefix(trimmed, "--") || strings.HasPrefix(trimmed, "#") { + continue + } + // Check if this is a SELECT or WITH statement + lower := strings.ToLower(trimmed) + return strings.HasPrefix(lower, "select") || strings.HasPrefix(lower, "with") + } + return false +} + +// isNullable checks if a ClickHouse type is nullable. +func isNullable(dataType string) bool { + return strings.HasPrefix(dataType, "Nullable(") || + strings.HasPrefix(strings.ToLower(dataType), "nullable(") +} + +// normalizeType normalizes a ClickHouse type name to a standard form. +func normalizeType(dataType string) string { + if dataType == "" { + return "any" + } + + // Strip Nullable wrapper + dt := dataType + if strings.HasPrefix(dt, "Nullable(") && strings.HasSuffix(dt, ")") { + dt = dt[9 : len(dt)-1] + } + + // Normalize common types + lower := strings.ToLower(dt) + switch { + case strings.HasPrefix(lower, "int8"): + return "Int8" + case strings.HasPrefix(lower, "int16"): + return "Int16" + case strings.HasPrefix(lower, "int32"): + return "Int32" + case strings.HasPrefix(lower, "int64"): + return "Int64" + case strings.HasPrefix(lower, "uint8"): + return "UInt8" + case strings.HasPrefix(lower, "uint16"): + return "UInt16" + case strings.HasPrefix(lower, "uint32"): + return "UInt32" + case strings.HasPrefix(lower, "uint64"): + return "UInt64" + case strings.HasPrefix(lower, "float32"): + return "Float32" + case strings.HasPrefix(lower, "float64"): + return "Float64" + case lower == "string" || strings.HasPrefix(lower, "fixedstring"): + return "String" + case strings.HasPrefix(lower, "date32"): + return "Date32" + case lower == "date": + return "Date" + case strings.HasPrefix(lower, "datetime64"): + return "DateTime64" + case strings.HasPrefix(lower, "datetime"): + return "DateTime" + case lower == "bool" || lower == "boolean": + return "Bool" + case lower == "uuid": + return "UUID" + case strings.HasPrefix(lower, "decimal"): + return dt // Keep original precision/scale + case strings.HasPrefix(lower, "array"): + return dt // Keep original array type + case strings.HasPrefix(lower, "map"): + return dt // Keep original map type + case strings.HasPrefix(lower, "tuple"): + return dt // Keep original tuple type + case strings.HasPrefix(lower, "enum"): + return dt // Keep original enum type + case strings.HasPrefix(lower, "lowcardinality"): + // Extract inner type + if strings.HasPrefix(dt, "LowCardinality(") && strings.HasSuffix(dt, ")") { + inner := dt[15 : len(dt)-1] + return normalizeType(inner) + } + return dt + default: + return dt + } +} diff --git a/internal/engine/clickhouse/catalog.go b/internal/engine/clickhouse/catalog.go new file mode 100644 index 0000000000..37f3057526 --- /dev/null +++ b/internal/engine/clickhouse/catalog.go @@ -0,0 +1,23 @@ +package clickhouse + +import "github.com/sqlc-dev/sqlc/internal/sql/catalog" + +// NewCatalog creates a new catalog for ClickHouse. +// ClickHouse uses "default" as the default database/schema. +func NewCatalog() *catalog.Catalog { + def := "default" + return &catalog.Catalog{ + DefaultSchema: def, + Schemas: []*catalog.Schema{ + defaultSchema(def), + }, + Extensions: map[string]struct{}{}, + } +} + +// defaultSchema creates a default schema with ClickHouse system tables. +func defaultSchema(name string) *catalog.Schema { + return &catalog.Schema{ + Name: name, + } +} diff --git a/internal/engine/clickhouse/format.go b/internal/engine/clickhouse/format.go new file mode 100644 index 0000000000..c562bfcc6d --- /dev/null +++ b/internal/engine/clickhouse/format.go @@ -0,0 +1,34 @@ +package clickhouse + +// QuoteIdent returns a quoted identifier if it needs quoting. +// ClickHouse uses backticks for quoting identifiers. +func (p *Parser) QuoteIdent(s string) string { + // For now, don't quote - return as-is + return s +} + +// TypeName returns the SQL type name for the given namespace and name. +func (p *Parser) TypeName(ns, name string) string { + if ns != "" { + return ns + "." + name + } + return name +} + +// Param returns the parameter placeholder for the given number. +// ClickHouse uses ? for positional parameters. +func (p *Parser) Param(n int) string { + return "?" +} + +// NamedParam returns the named parameter placeholder for the given name. +// ClickHouse uses @name syntax for named parameters. +func (p *Parser) NamedParam(name string) string { + return "@" + name +} + +// Cast returns a type cast expression. +// ClickHouse uses CAST(expr AS type) syntax. +func (p *Parser) Cast(arg, typeName string) string { + return "CAST(" + arg + " AS " + typeName + ")" +} diff --git a/internal/engine/clickhouse/parse.go b/internal/engine/clickhouse/parse.go new file mode 100644 index 0000000000..9410f9948a --- /dev/null +++ b/internal/engine/clickhouse/parse.go @@ -0,0 +1,503 @@ +package clickhouse + +import ( + "context" + "fmt" + "io" + "strings" + + "github.com/sqlc-dev/doubleclick/ast" + "github.com/sqlc-dev/doubleclick/parser" + + "github.com/sqlc-dev/sqlc/internal/source" + sqlcast "github.com/sqlc-dev/sqlc/internal/sql/ast" +) + +// Parser implements the compiler.Parser interface for ClickHouse. +type Parser struct{} + +// NewParser creates a new ClickHouse parser. +func NewParser() *Parser { + return &Parser{} +} + +// Parse parses ClickHouse SQL statements and converts them to sqlc's AST. +func (p *Parser) Parse(r io.Reader) ([]sqlcast.Statement, error) { + // Read the full source for position calculations + srcBytes, err := io.ReadAll(r) + if err != nil { + return nil, err + } + src := string(srcBytes) + + ctx := context.Background() + stmts, err := parser.Parse(ctx, strings.NewReader(src)) + if err != nil { + return nil, err + } + + var result []sqlcast.Statement + loc := 0 // Track from start of file or after previous statement + + for _, stmt := range stmts { + converted := p.convert(stmt) + if converted == nil { + continue + } + pos := stmt.Pos() + // doubleclick uses 1-indexed offsets, convert to 0-indexed + stmtStart := pos.Offset + if stmtStart > 0 { + stmtStart = stmtStart - 1 + } + + // Find the semicolon that ends this statement + semiPos := strings.Index(src[stmtStart:], ";") + stmtEnd := stmtStart + if semiPos >= 0 { + stmtEnd = stmtStart + semiPos + 1 // Include the semicolon + } + + // The statement length includes everything from loc to the semicolon + stmtLen := stmtEnd - loc + + result = append(result, sqlcast.Statement{ + Raw: &sqlcast.RawStmt{ + Stmt: converted, + StmtLocation: loc, + StmtLen: stmtLen, + }, + }) + + // Move loc past the semicolon for the next statement + loc = stmtEnd + } + + return result, nil +} + +// CommentSyntax returns the comment syntax for ClickHouse. +func (p *Parser) CommentSyntax() source.CommentSyntax { + return source.CommentSyntax{ + Dash: true, + Hash: true, + SlashStar: true, + } +} + +// IsReservedKeyword checks if a word is a reserved keyword in ClickHouse. +func (p *Parser) IsReservedKeyword(word string) bool { + return isReserved(word) +} + +func (p *Parser) convert(stmt ast.Statement) sqlcast.Node { + switch s := stmt.(type) { + case *ast.SelectWithUnionQuery: + return p.convertSelectWithUnion(s) + case *ast.SelectQuery: + return p.convertSelect(s) + case *ast.InsertQuery: + return p.convertInsert(s) + case *ast.CreateQuery: + return p.convertCreate(s) + case *ast.DropQuery: + return p.convertDrop(s) + case *ast.AlterQuery: + return p.convertAlter(s) + case *ast.TruncateQuery: + return p.convertTruncate(s) + default: + return &sqlcast.TODO{} + } +} + +func (p *Parser) convertSelectWithUnion(s *ast.SelectWithUnionQuery) sqlcast.Node { + if len(s.Selects) == 0 { + return &sqlcast.TODO{} + } + + first := p.convert(s.Selects[0]) + if first == nil { + return &sqlcast.TODO{} + } + + firstSelect, ok := first.(*sqlcast.SelectStmt) + if !ok { + return first + } + + if len(s.Selects) == 1 { + return firstSelect + } + + left := firstSelect + for i := 1; i < len(s.Selects); i++ { + right := p.convert(s.Selects[i]) + rightSelect, ok := right.(*sqlcast.SelectStmt) + if !ok { + continue + } + all := false + if i-1 < len(s.UnionModes) && s.UnionModes[i-1] == "UNION ALL" { + all = true + } + left = &sqlcast.SelectStmt{ + Op: sqlcast.Union, + All: all, + Larg: left, + Rarg: rightSelect, + } + } + return left +} + +func (p *Parser) convertSelect(s *ast.SelectQuery) sqlcast.Node { + stmt := &sqlcast.SelectStmt{} + + var targets []sqlcast.Node + for _, col := range s.Columns { + target := p.convertColumnExpr(col) + if target != nil { + targets = append(targets, target) + } + } + if len(targets) > 0 { + stmt.TargetList = &sqlcast.List{Items: targets} + } + + if s.From != nil { + stmt.FromClause = p.convertFrom(s.From) + } + + if s.Where != nil { + stmt.WhereClause = p.convertExpr(s.Where) + } + + if len(s.GroupBy) > 0 { + var groupItems []sqlcast.Node + for _, g := range s.GroupBy { + groupItems = append(groupItems, p.convertExpr(g)) + } + stmt.GroupClause = &sqlcast.List{Items: groupItems} + } + + if s.Having != nil { + stmt.HavingClause = p.convertExpr(s.Having) + } + + if len(s.OrderBy) > 0 { + var sortItems []sqlcast.Node + for _, o := range s.OrderBy { + sortItem := &sqlcast.SortBy{ + Node: p.convertExpr(o.Expression), + } + if o.Descending { + sortItem.SortbyDir = sqlcast.SortByDirDesc + } else { + sortItem.SortbyDir = sqlcast.SortByDirAsc + } + sortItems = append(sortItems, sortItem) + } + stmt.SortClause = &sqlcast.List{Items: sortItems} + } + + if s.Limit != nil { + stmt.LimitCount = p.convertExpr(s.Limit) + } + if s.Offset != nil { + stmt.LimitOffset = p.convertExpr(s.Offset) + } + + return stmt +} + +func (p *Parser) convertColumnExpr(expr ast.Expression) sqlcast.Node { + switch e := expr.(type) { + case *ast.Asterisk: + return &sqlcast.ResTarget{ + Val: &sqlcast.ColumnRef{ + Fields: &sqlcast.List{ + Items: []sqlcast.Node{&sqlcast.A_Star{}}, + }, + }, + } + case *ast.Identifier: + colRef := &sqlcast.ColumnRef{ + Fields: &sqlcast.List{}, + } + for _, part := range e.Parts { + colRef.Fields.Items = append(colRef.Fields.Items, &sqlcast.String{Str: part}) + } + target := &sqlcast.ResTarget{Val: colRef} + if e.Alias != "" { + target.Name = &e.Alias + } + return target + default: + converted := p.convertExpr(expr) + if converted == nil { + return nil + } + return &sqlcast.ResTarget{Val: converted} + } +} + +func (p *Parser) convertFrom(from *ast.TablesInSelectQuery) *sqlcast.List { + if from == nil || len(from.Tables) == 0 { + return nil + } + + var items []sqlcast.Node + for _, table := range from.Tables { + if table.Table != nil { + items = append(items, p.convertTableExpr(table.Table)) + } + } + + return &sqlcast.List{Items: items} +} + +func (p *Parser) convertTableExpr(expr *ast.TableExpression) sqlcast.Node { + switch t := expr.Table.(type) { + case *ast.TableIdentifier: + rv := &sqlcast.RangeVar{ + Relname: &t.Table, + } + if t.Database != "" { + rv.Schemaname = &t.Database + } + if expr.Alias != "" { + rv.Alias = &sqlcast.Alias{Aliasname: &expr.Alias} + } + return rv + case *ast.Subquery: + rs := &sqlcast.RangeSubselect{ + Subquery: p.convert(t.Query), + } + if expr.Alias != "" { + rs.Alias = &sqlcast.Alias{Aliasname: &expr.Alias} + } + return rs + case *ast.FunctionCall: + fc := &sqlcast.FuncCall{ + Funcname: &sqlcast.List{ + Items: []sqlcast.Node{&sqlcast.String{Str: t.Name}}, + }, + } + for _, arg := range t.Arguments { + if fc.Args == nil { + fc.Args = &sqlcast.List{} + } + fc.Args.Items = append(fc.Args.Items, p.convertExpr(arg)) + } + return &sqlcast.RangeFunction{ + Functions: &sqlcast.List{Items: []sqlcast.Node{fc}}, + } + default: + return &sqlcast.RangeVar{} + } +} + +func (p *Parser) convertExpr(expr ast.Expression) sqlcast.Node { + if expr == nil { + return nil + } + + switch e := expr.(type) { + case *ast.Identifier: + colRef := &sqlcast.ColumnRef{Fields: &sqlcast.List{}} + for _, part := range e.Parts { + colRef.Fields.Items = append(colRef.Fields.Items, &sqlcast.String{Str: part}) + } + return colRef + + case *ast.Literal: + switch e.Type { + case ast.LiteralString: + return &sqlcast.A_Const{Val: &sqlcast.String{Str: fmt.Sprintf("%v", e.Value)}} + case ast.LiteralInteger: + if val, ok := e.Value.(int64); ok { + return &sqlcast.A_Const{Val: &sqlcast.Integer{Ival: val}} + } + return &sqlcast.A_Const{Val: &sqlcast.String{Str: fmt.Sprintf("%v", e.Value)}} + case ast.LiteralFloat: + return &sqlcast.A_Const{Val: &sqlcast.Float{Str: fmt.Sprintf("%v", e.Value)}} + case ast.LiteralNull: + return &sqlcast.Null{} + default: + return &sqlcast.A_Const{Val: &sqlcast.String{Str: fmt.Sprintf("%v", e.Value)}} + } + + case *ast.BinaryExpr: + return &sqlcast.A_Expr{ + Name: &sqlcast.List{Items: []sqlcast.Node{&sqlcast.String{Str: e.Op}}}, + Lexpr: p.convertExpr(e.Left), + Rexpr: p.convertExpr(e.Right), + } + + case *ast.UnaryExpr: + return &sqlcast.A_Expr{ + Name: &sqlcast.List{Items: []sqlcast.Node{&sqlcast.String{Str: e.Op}}}, + Rexpr: p.convertExpr(e.Operand), + } + + case *ast.FunctionCall: + fc := &sqlcast.FuncCall{ + Funcname: &sqlcast.List{Items: []sqlcast.Node{&sqlcast.String{Str: e.Name}}}, + } + if len(e.Arguments) > 0 { + fc.Args = &sqlcast.List{} + for _, arg := range e.Arguments { + fc.Args.Items = append(fc.Args.Items, p.convertExpr(arg)) + } + } + return fc + + case *ast.Subquery: + return &sqlcast.SubLink{Subselect: p.convert(e.Query)} + + case *ast.Asterisk: + return &sqlcast.ColumnRef{ + Fields: &sqlcast.List{Items: []sqlcast.Node{&sqlcast.A_Star{}}}, + } + + case *ast.Parameter: + // ClickHouse uses ? for positional parameters + return &sqlcast.ParamRef{ + Number: 0, // Will be assigned during query parsing + Location: e.Position.Offset, + } + + default: + return &sqlcast.A_Const{Val: &sqlcast.String{Str: ""}} + } +} + +func (p *Parser) convertInsert(s *ast.InsertQuery) sqlcast.Node { + stmt := &sqlcast.InsertStmt{ + Relation: &sqlcast.RangeVar{Relname: &s.Table}, + } + + if s.Database != "" { + stmt.Relation.Schemaname = &s.Database + } + + if len(s.Columns) > 0 { + stmt.Cols = &sqlcast.List{} + for _, col := range s.Columns { + if len(col.Parts) > 0 { + name := col.Parts[0] + stmt.Cols.Items = append(stmt.Cols.Items, &sqlcast.ResTarget{Name: &name}) + } + } + } + + if s.Select != nil { + stmt.SelectStmt = p.convert(s.Select) + } + + return stmt +} + +func (p *Parser) convertCreate(s *ast.CreateQuery) sqlcast.Node { + if s.CreateDatabase { + return &sqlcast.CreateSchemaStmt{ + Name: &s.Database, + IfNotExists: s.IfNotExists, + } + } + + stmt := &sqlcast.CreateTableStmt{ + Name: &sqlcast.TableName{Name: s.Table}, + IfNotExists: s.IfNotExists, + } + + if s.Database != "" { + stmt.Name.Schema = s.Database + } + + for _, col := range s.Columns { + colDef := &sqlcast.ColumnDef{Colname: col.Name} + if col.Type != nil { + colDef.TypeName = &sqlcast.TypeName{Name: col.Type.Name} + } + if col.Nullable != nil && !*col.Nullable { + colDef.IsNotNull = true + } + stmt.Cols = append(stmt.Cols, colDef) + } + + return stmt +} + +func (p *Parser) convertDrop(s *ast.DropQuery) sqlcast.Node { + if s.DropDatabase { + return &sqlcast.DropSchemaStmt{ + Schemas: []*sqlcast.String{{Str: s.Database}}, + MissingOk: s.IfExists, + } + } + + tables := []*sqlcast.TableName{} + if s.Table != "" { + tableName := &sqlcast.TableName{Name: s.Table} + if s.Database != "" { + tableName.Schema = s.Database + } + tables = append(tables, tableName) + } + + return &sqlcast.DropTableStmt{ + Tables: tables, + IfExists: s.IfExists, + } +} + +func (p *Parser) convertAlter(s *ast.AlterQuery) sqlcast.Node { + tableName := &sqlcast.TableName{Name: s.Table} + if s.Database != "" { + tableName.Schema = s.Database + } + + stmt := &sqlcast.AlterTableStmt{ + Table: tableName, + Cmds: &sqlcast.List{}, + } + + for _, cmd := range s.Commands { + switch cmd.Type { + case ast.AlterAddColumn: + if cmd.Column != nil { + colDef := &sqlcast.ColumnDef{Colname: cmd.Column.Name} + if cmd.Column.Type != nil { + colDef.TypeName = &sqlcast.TypeName{Name: cmd.Column.Type.Name} + } + altCmd := &sqlcast.AlterTableCmd{ + Subtype: sqlcast.AT_AddColumn, + Def: colDef, + } + stmt.Cmds.Items = append(stmt.Cmds.Items, altCmd) + } + case ast.AlterDropColumn: + altCmd := &sqlcast.AlterTableCmd{ + Subtype: sqlcast.AT_DropColumn, + Name: &cmd.ColumnName, + MissingOk: cmd.IfExists, + } + stmt.Cmds.Items = append(stmt.Cmds.Items, altCmd) + } + } + + return stmt +} + +func (p *Parser) convertTruncate(s *ast.TruncateQuery) sqlcast.Node { + rv := &sqlcast.RangeVar{Relname: &s.Table} + if s.Database != "" { + rv.Schemaname = &s.Database + } + + return &sqlcast.TruncateStmt{ + Relations: &sqlcast.List{Items: []sqlcast.Node{rv}}, + } +} diff --git a/internal/engine/clickhouse/reserved.go b/internal/engine/clickhouse/reserved.go new file mode 100644 index 0000000000..116148d94d --- /dev/null +++ b/internal/engine/clickhouse/reserved.go @@ -0,0 +1,199 @@ +package clickhouse + +import "strings" + +// isReserved returns true if the given word is a ClickHouse reserved keyword. +// This list is based on ClickHouse's official documentation. +func isReserved(s string) bool { + switch strings.ToLower(s) { + case "add", + "after", + "alias", + "all", + "alter", + "and", + "anti", + "any", + "array", + "as", + "asc", + "ascending", + "asof", + "attach", + "between", + "both", + "by", + "case", + "cast", + "check", + "clear", + "cluster", + "codec", + "collate", + "column", + "comment", + "constraint", + "create", + "cross", + "cube", + "current", + "database", + "databases", + "date", + "day", + "deduplicate", + "default", + "delay", + "delete", + "desc", + "descending", + "describe", + "detach", + "dictionaries", + "dictionary", + "disk", + "distinct", + "distributed", + "drop", + "else", + "end", + "engine", + "events", + "except", + "exists", + "explain", + "expression", + "extract", + "fetch", + "final", + "first", + "flush", + "following", + "for", + "format", + "freeze", + "from", + "full", + "function", + "global", + "granularity", + "group", + "having", + "hierarchical", + "hour", + "id", + "if", + "ilike", + "in", + "index", + "injective", + "inner", + "insert", + "interval", + "into", + "is", + "is_object_id", + "join", + "key", + "kill", + "last", + "layout", + "leading", + "left", + "lifetime", + "like", + "limit", + "live", + "local", + "logs", + "materialized", + "max", + "merges", + "min", + "minute", + "modify", + "month", + "move", + "mutation", + "no", + "not", + "null", + "nulls", + "offset", + "on", + "optimize", + "or", + "order", + "outer", + "outfile", + "over", + "partition", + "paste", + "populate", + "preceding", + "prewhere", + "primary", + "projection", + "quarter", + "range", + "reload", + "remove", + "rename", + "replace", + "replica", + "replicated", + "right", + "rollup", + "row", + "rows", + "sample", + "second", + "select", + "semi", + "sends", + "set", + "settings", + "show", + "source", + "start", + "stop", + "substring", + "sync", + "syntax", + "system", + "table", + "tables", + "temporary", + "test", + "then", + "ties", + "timeout", + "timestamp", + "to", + "top", + "totals", + "trailing", + "trim", + "truncate", + "ttl", + "type", + "unbounded", + "union", + "update", + "use", + "using", + "uuid", + "values", + "view", + "volume", + "watch", + "week", + "when", + "where", + "window", + "with", + "year": + return true + } + return false +} diff --git a/internal/opts/experiment.go b/internal/opts/experiment.go index 345cba6cc1..350c6d08ba 100644 --- a/internal/opts/experiment.go +++ b/internal/opts/experiment.go @@ -30,6 +30,8 @@ type Experiment struct { AnalyzerV2 bool // ParseCmd enables the parse subcommand which outputs AST as JSON. ParseCmd bool + // ClickHouse enables ClickHouse database support. + ClickHouse bool } // ExperimentFromEnv returns an Experiment initialized from the SQLCEXPERIMENT @@ -77,7 +79,7 @@ func ExperimentFromString(val string) Experiment { // known experiment. func isKnownExperiment(name string) bool { switch strings.ToLower(name) { - case "analyzerv2", "parsecmd": + case "analyzerv2", "parsecmd", "clickhouse": return true default: return false @@ -91,6 +93,8 @@ func setExperiment(e *Experiment, name string, enabled bool) { e.AnalyzerV2 = enabled case "parsecmd": e.ParseCmd = enabled + case "clickhouse": + e.ClickHouse = enabled } } @@ -103,6 +107,9 @@ func (e Experiment) Enabled() []string { if e.ParseCmd { enabled = append(enabled, "parsecmd") } + if e.ClickHouse { + enabled = append(enabled, "clickhouse") + } return enabled } diff --git a/internal/sqltest/clickhouse.go b/internal/sqltest/clickhouse.go new file mode 100644 index 0000000000..ec3cc6aa5b --- /dev/null +++ b/internal/sqltest/clickhouse.go @@ -0,0 +1,102 @@ +package sqltest + +import ( + "context" + "database/sql" + "math/rand" + "os" + "testing" + + _ "github.com/ClickHouse/clickhouse-go/v2" // ClickHouse driver + + "github.com/sqlc-dev/sqlc/internal/sqltest/docker" + "github.com/sqlc-dev/sqlc/internal/sqltest/native" +) + +var clickhouseLetterRunes = []rune("abcdefghijklmnopqrstuvwxyz") + +func clickhouseID() string { + b := make([]rune, 10) + for i := range b { + b[i] = clickhouseLetterRunes[rand.Intn(len(clickhouseLetterRunes))] + } + return string(b) +} + +func ClickHouse(t *testing.T, migrations []string) *sql.DB { + ctx := context.Background() + + // Check environment variable first + uri := os.Getenv("CLICKHOUSE_SERVER_URI") + var err error + + // Try Docker if no URI provided + if uri == "" { + uri, err = docker.StartClickHouseServer(ctx) + if err != nil { + t.Log("docker clickhouse startup failed:", err) + } + } + + // Try native installation + if uri == "" { + uri, err = native.StartClickHouseServer(ctx) + if err != nil { + t.Log("native clickhouse startup failed:", err) + } + } + + if uri == "" { + t.Skip("no clickhouse server available") + return nil + } + + db, err := sql.Open("clickhouse", uri) + if err != nil { + t.Fatalf("connect to clickhouse: %s", err) + } + + // Create a unique test database + dbName := "test_" + clickhouseID() + + // Create the test database + _, err = db.ExecContext(ctx, "CREATE DATABASE IF NOT EXISTS "+dbName) + if err != nil { + db.Close() + t.Fatalf("create database: %s", err) + } + + // Switch to the test database by reconnecting + db.Close() + testURI := uri + // Append database name to URI + if testURI[len(testURI)-1] != '/' { + testURI = testURI[:len(testURI)-len("default")] + dbName + } + + db, err = sql.Open("clickhouse", testURI) + if err != nil { + t.Fatalf("connect to test database: %s", err) + } + + // Apply migrations + for _, migration := range migrations { + if _, err := db.ExecContext(ctx, migration); err != nil { + db.Close() + t.Fatalf("migration failed: %s: %s", migration, err) + } + } + + // Cleanup on test completion + t.Cleanup(func() { + db.Close() + // Reconnect to default database to drop test database + cleanupDB, err := sql.Open("clickhouse", uri) + if err == nil { + cleanupDB.ExecContext(ctx, "DROP DATABASE IF EXISTS "+dbName) + cleanupDB.Close() + } + }) + + return db +} diff --git a/internal/sqltest/docker/clickhouse.go b/internal/sqltest/docker/clickhouse.go new file mode 100644 index 0000000000..93ed54363e --- /dev/null +++ b/internal/sqltest/docker/clickhouse.go @@ -0,0 +1,105 @@ +package docker + +import ( + "context" + "database/sql" + "fmt" + "log/slog" + "os/exec" + "strings" + "time" + + _ "github.com/ClickHouse/clickhouse-go/v2" // ClickHouse driver +) + +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/clickhouse-server:latest").CombinedOutput() + if err != nil { + return "", fmt.Errorf("docker pull: clickhouse/clickhouse-server:latest %w", err) + } + } + + uri := "clickhouse://default:mysecretpassword@localhost:9000/default" + + var exists bool + { + cmd := exec.Command("docker", "container", "inspect", "sqlc_sqltest_docker_clickhouse") + exists = cmd.Run() == nil + } + + if !exists { + cmd := exec.Command("docker", "run", + "--name", "sqlc_sqltest_docker_clickhouse", + "-e", "CLICKHOUSE_DB=default", + "-e", "CLICKHOUSE_USER=default", + "-e", "CLICKHOUSE_PASSWORD=mysecretpassword", + "-p", "9000:9000", + "-p", "8123:8123", + "-d", + "clickhouse/clickhouse-server:latest", + ) + + 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, 30*time.Second) + defer cancel() + + // Create a ticker that fires every 100ms + ticker := time.NewTicker(100 * time.Millisecond) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return "", fmt.Errorf("timeout reached: %w", ctx.Err()) + + case <-ticker.C: + conn, err := sql.Open("clickhouse", uri) + if err != nil { + slog.Debug("sqltest", "connect", err) + continue + } + defer conn.Close() + if err := conn.PingContext(ctx); err != nil { + slog.Debug("sqltest", "ping", err) + continue + } + return uri, nil + } + } +} diff --git a/internal/sqltest/native/clickhouse.go b/internal/sqltest/native/clickhouse.go new file mode 100644 index 0000000000..47bc1b205a --- /dev/null +++ b/internal/sqltest/native/clickhouse.go @@ -0,0 +1,128 @@ +package native + +import ( + "context" + "database/sql" + "fmt" + "log/slog" + "os/exec" + "time" + + _ "github.com/ClickHouse/clickhouse-go/v2" // ClickHouse driver + "golang.org/x/sync/singleflight" +) + +var clickhouseFlight singleflight.Group +var clickhouseURI string + +// StartClickHouseServer starts an existing ClickHouse installation natively (without Docker). +func StartClickHouseServer(ctx context.Context) (string, error) { + if err := Supported(); err != nil { + return "", err + } + if clickhouseURI != "" { + return clickhouseURI, nil + } + value, err, _ := clickhouseFlight.Do("clickhouse", func() (interface{}, error) { + uri, err := startClickHouseServer(ctx) + if err != nil { + return "", err + } + clickhouseURI = uri + return uri, nil + }) + 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(ctx context.Context) (string, error) { + // Standard URI for test ClickHouse + uri := "clickhouse://default:@localhost:9000/default" + + // Try to connect first - it might already be running + if err := waitForClickHouse(ctx, uri, 500*time.Millisecond); err == nil { + slog.Info("native/clickhouse", "status", "already running") + return uri, nil + } + + // Check if ClickHouse is installed + if _, err := exec.LookPath("clickhouse"); err != nil { + return "", fmt.Errorf("ClickHouse is not installed (clickhouse not found)") + } + + // Start ClickHouse server + slog.Info("native/clickhouse", "status", "starting service") + + if err := startClickHouseService(); err != nil { + return "", fmt.Errorf("failed to start ClickHouse: %w", err) + } + + // Wait for ClickHouse to be ready + waitCtx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + + if err := waitForClickHouse(waitCtx, uri, 30*time.Second); err != nil { + return "", fmt.Errorf("timeout waiting for ClickHouse: %w", err) + } + + return uri, nil +} + +func startClickHouseService() error { + // Try systemctl first + cmd := exec.Command("sudo", "systemctl", "start", "clickhouse-server") + if err := cmd.Run(); err == nil { + return nil + } + + // Try service command + cmd = exec.Command("sudo", "service", "clickhouse-server", "start") + if err := cmd.Run(); err == nil { + return nil + } + + // Try running clickhouse-server directly (for single-node dev setup) + cmd = exec.Command("clickhouse-server", "--daemon") + if err := cmd.Run(); err == nil { + return nil + } + + return fmt.Errorf("could not start ClickHouse server") +} + +func waitForClickHouse(ctx context.Context, uri string, timeout time.Duration) error { + deadline := time.Now().Add(timeout) + ticker := time.NewTicker(100 * time.Millisecond) + defer ticker.Stop() + + var lastErr error + for { + select { + case <-ctx.Done(): + return fmt.Errorf("context cancelled: %w (last error: %v)", ctx.Err(), lastErr) + case <-ticker.C: + if time.Now().After(deadline) { + return fmt.Errorf("timeout waiting for ClickHouse (last error: %v)", lastErr) + } + conn, err := sql.Open("clickhouse", uri) + if err != nil { + lastErr = err + slog.Debug("native/clickhouse", "connect-attempt", err) + continue + } + if err := conn.PingContext(ctx); err != nil { + lastErr = err + conn.Close() + continue + } + conn.Close() + return nil + } + } +} diff --git a/internal/sqltest/native/mysql.go b/internal/sqltest/native/mysql.go index 82881fdfb7..21ac69914f 100644 --- a/internal/sqltest/native/mysql.go +++ b/internal/sqltest/native/mysql.go @@ -102,15 +102,23 @@ func startMySQLServer(ctx context.Context) (string, error) { } func startMySQLService() error { + // Check if mysqld is already running via pgrep + cmd := exec.Command("pgrep", "-x", "mysqld") + if err := cmd.Run(); err == nil { + // mysqld is already running + time.Sleep(1 * time.Second) + return nil + } + // Try systemctl first - cmd := exec.Command("sudo", "systemctl", "start", "mysql") + cmd = exec.Command("sudo", "systemctl", "start", "mysql") if err := cmd.Run(); err == nil { // Give MySQL time to fully initialize time.Sleep(2 * time.Second) return nil } - // Try mysqld + // Try mysqld via systemctl cmd = exec.Command("sudo", "systemctl", "start", "mysqld") if err := cmd.Run(); err == nil { time.Sleep(2 * time.Second) @@ -130,6 +138,13 @@ func startMySQLService() error { return nil } + // Try starting mysqld directly as a last resort + cmd = exec.Command("sudo", "mysqld", "--user=mysql") + if err := cmd.Start(); err == nil { + time.Sleep(3 * time.Second) + return nil + } + return fmt.Errorf("could not start MySQL service") }