From 13c9bf9679ec03e1ee0c16ab22251dee07ea9c1b Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 4 Oct 2026 18:09:07 +0800 Subject: [PATCH 01/32] test: add sql_exec fuzz target and CI smoke run Add a standalone cargo-fuzz crate under fuzz/ with an end-to-end SQL target that runs tests/slt scripts (slt syntax stripped) against an in-memory database. Any panic or sanitizer report fails the run. `make fuzz` runs it on a pinned nightly (nightly-2026-10-04) and CI runs it for 300s. Timeouts and OOMs are ignored for now via libFuzzer fork mode, since mutated queries can be legitimately unbounded (TODO noted). --- .github/workflows/ci.yml | 29 ++ Makefile | 19 +- fuzz/.gitignore | 4 + fuzz/Cargo.lock | 805 ++++++++++++++++++++++++++++++++++ fuzz/Cargo.toml | 27 ++ fuzz/fuzz_targets/sql_exec.rs | 150 +++++++ fuzz/sql.dict | 95 ++++ 7 files changed, 1128 insertions(+), 1 deletion(-) create mode 100644 fuzz/.gitignore create mode 100644 fuzz/Cargo.lock create mode 100644 fuzz/Cargo.toml create mode 100644 fuzz/fuzz_targets/sql_exec.rs create mode 100644 fuzz/sql.dict diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 20ce6b74..5de2137e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -133,3 +133,32 @@ jobs: - name: Run python binding tests run: PYO3_PYTHON="$(which python)" make test-python + # 9 + fuzz: + name: Fuzz smoke test (sql_exec) + runs-on: ubuntu-latest + env: + # The fuzz crate is a standalone workspace; keep its build out of the shared target dir. + CARGO_TARGET_DIR: ${{ github.workspace }}/fuzz/target + steps: + - uses: actions/checkout@v4 + # Pinned; keep in sync with FUZZ_TOOLCHAIN in the Makefile. + - uses: dtolnay/rust-toolchain@master + with: + toolchain: nightly-2026-10-04 + - uses: Swatinem/rust-cache@v2 + with: + workspaces: fuzz + - uses: taiki-e/install-action@v2 + with: + tool: cargo-fuzz + + - name: Run sql_exec fuzzer + run: make fuzz FUZZ_TIME=300 + + - name: Upload crash artifacts + if: failure() + uses: actions/upload-artifact@v4 + with: + name: fuzz-artifacts + path: fuzz/artifacts diff --git a/Makefile b/Makefile index f8600bb3..1666e836 100644 --- a/Makefile +++ b/Makefile @@ -15,9 +15,15 @@ COVERAGE_PROFILE_DIR ?= target/grcov/profraw COVERAGE_HTML_DIR ?= target/grcov/html COVERAGE_RUSTFLAGS ?= -Cinstrument-coverage COVERAGE_TEST_FEATURES ?= copy,decimal,orm +# Pinned nightly for fuzzing; keep in sync with the `fuzz` job in .github/workflows/ci.yml. +FUZZ_TOOLCHAIN ?= nightly-2026-10-04 +FUZZ_TARGET ?= sql_exec +FUZZ_TIME ?= 60 +# Pass the host triple explicitly: prebuilt (musl) cargo-fuzz binaries otherwise default to musl. +FUZZ_TRIPLE ?= $(shell rustc -vV | sed -n 's/^host: //p') COVERAGE_REPORT_ARGS ?= --llvm --ignore-not-existing --keep-only 'src/**' --ignore 'src/**/tests/**' --ignore 'tests/**' --ignore 'tpcc/**' --excl-start 'GRCOV_EXCL_START' --excl-stop 'GRCOV_EXCL_STOP' -.PHONY: test test-python test-wasm test-slt test-all codecov codecov-html wasm-build check tpcc tpcc-kitesql-rocksdb tpcc-kitesql-lmdb tpcc-lmdb-flamegraph tpcc-lmdb-heaptrack tpcc-sqlite tpcc-sqlite-practical tpcc-sqlite-balanced tpcc-dual cargo-check build wasm-examples native-examples fmt clippy +.PHONY: test test-python test-wasm test-slt test-all codecov codecov-html wasm-build check tpcc tpcc-kitesql-rocksdb tpcc-kitesql-lmdb tpcc-lmdb-flamegraph tpcc-lmdb-heaptrack tpcc-sqlite tpcc-sqlite-practical tpcc-sqlite-balanced tpcc-dual cargo-check build wasm-examples native-examples fmt clippy fuzz ## Run default Rust tests in the current environment (non-WASM). test: @@ -71,6 +77,17 @@ codecov-html: $(GRCOV) . --binary-path \"$${CARGO_TARGET_DIR:-target}/debug\" -s . -t html $(COVERAGE_REPORT_ARGS) -o '$(COVERAGE_HTML_DIR)'" @echo "Coverage report: $(COVERAGE_HTML_DIR)/index.html" +## Run a fuzz target for FUZZ_TIME seconds (needs a nightly toolchain and cargo-fuzz). +## tests/slt is a read-only seed dir: libFuzzer only writes new inputs to the FIRST dir. +fuzz: + @mkdir -p fuzz/corpus/$(FUZZ_TARGET) + $(CARGO) +$(FUZZ_TOOLCHAIN) fuzz run --target $(FUZZ_TRIPLE) $(FUZZ_TARGET) \ + fuzz/corpus/$(FUZZ_TARGET) tests/slt -- \ + -dict=fuzz/sql.dict -max_len=32768 -timeout=10 -rss_limit_mb=4096 -max_total_time=$(FUZZ_TIME) \ + -fork=1 -ignore_timeouts=1 -ignore_ooms=1 +# TODO: stop ignoring timeouts/OOMs once unbounded queries can be told apart from hangs +# (see fuzz/fuzz_targets/sql_exec.rs). Crashes still fail the run. + ## Run formatting (check mode) across the workspace. fmt: $(CARGO) fmt --all -- --check diff --git a/fuzz/.gitignore b/fuzz/.gitignore new file mode 100644 index 00000000..1a45eee7 --- /dev/null +++ b/fuzz/.gitignore @@ -0,0 +1,4 @@ +target +corpus +artifacts +coverage diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock new file mode 100644 index 00000000..9b88ed39 --- /dev/null +++ b/fuzz/Cargo.lock @@ -0,0 +1,805 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "borsh" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "553c5d846a6ba5150c65e3b1b8ec073bcf1abc20f9b7220de384a4443ea4e20a" +dependencies = [ + "borsh-derive", + "bytes", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12cdfe656708a01f89b451a7d36466e6fe6c414de0aa18fc54f864f6f9ca9f56" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cc" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f74872d07caf508b30a21f6836e7d7016a2eaf7d9ff4f48deaa58cd8a0407630" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.119", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "find-msvc-tools" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "kite_sql" +version = "0.4.1" +dependencies = [ + "bumpalo", + "chrono", + "kite_sql_serde_macros", + "ordered-float", + "paste", + "rust_decimal", + "sqlparser", +] + +[[package]] +name = "kite_sql-fuzz" +version = "0.0.0" +dependencies = [ + "kite_sql", + "libfuzzer-sys", +] + +[[package]] +name = "kite_sql_serde_macros" +version = "0.2.4" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "libc" +version = "0.2.190" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" +dependencies = [ + "arbitrary", + "cc", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rust_decimal" +version = "1.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7653272e75dcac41dc199fbea6f5797633994fafd339943c06c9af16bf29cd3a" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand 0.8.8", + "rand 0.9.5", + "serde", + "serde_json", + "wasm-bindgen", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "sqlparser" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf5ea8d4d7c808e1af1cbabebca9a2abe603bcefc22294c5b95018d53200cb7" +dependencies = [ + "log", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.15+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1340ea94a5856333492c9064b02c778b191dd2c853778d9609debdcdfea3a614" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow", +] + +[[package]] +name = "unicode-ident" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.6", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "zerocopy" +version = "0.8.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6df92bf3d9227be3d53173901ddbffac2babc27ae50f397776ffd6dc33f800cb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac4f328cf2f05d084e496c3e9c3f33ed0a183656a16e1fcec4d464d8373aec82" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml new file mode 100644 index 00000000..e51afc17 --- /dev/null +++ b/fuzz/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "kite_sql-fuzz" +version = "0.0.0" +publish = false +edition = "2021" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = { version = "=0.4.10" } +# In-memory storage only: no RocksDB/LMDB build, no file IO (`copy`/`spill` off). +kite_sql = { path = "..", default-features = false, features = ["parser", "macros", "time", "decimal"] } + +# Standalone workspace: fuzzing needs nightly, the main workspace stays on stable. +[workspace] +members = ["."] + +[profile.release] +debug = 1 + +[[bin]] +name = "sql_exec" +path = "fuzz_targets/sql_exec.rs" +test = false +doc = false +bench = false diff --git a/fuzz/fuzz_targets/sql_exec.rs b/fuzz/fuzz_targets/sql_exec.rs new file mode 100644 index 00000000..501cb925 --- /dev/null +++ b/fuzz/fuzz_targets/sql_exec.rs @@ -0,0 +1,150 @@ +// Copyright 2024 KipData/KiteSQL +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! End-to-end SQL fuzzing against an in-memory database. +//! +//! The input is treated as a `;`-separated SQL script. Every chunk is run on +//! its own, so a mutation that breaks one statement does not discard the rest +//! of the script (e.g. the `CREATE TABLE` / `INSERT` that later queries need). +//! +//! sqllogictest syntax is stripped first, so `tests/slt` can be used directly +//! as the seed corpus; plain SQL inputs pass through unchanged. +//! +//! `Err` results are expected and ignored. Any panic or sanitizer report is a +//! bug. +//! +//! TODO: timeouts and OOMs are skipped (`make fuzz` runs libFuzzer in fork +//! mode). Mutations easily produce legitimately unbounded queries, e.g. a +//! recursive CTE that never converges, and KiteSQL cannot interrupt a running +//! statement, so they are indistinguishable from real hangs. Revisit once the +//! executor has a cheap interrupt point or the structured generator avoids +//! unbounded queries. + +#![no_main] + +use kite_sql::binder::{command_type, CommandType}; +use kite_sql::db::{prepare_all, DataBaseBuilder, Database, Statement}; +use kite_sql::storage::memory::MemoryStorage; +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &[u8]| { + let Ok(script) = std::str::from_utf8(data) else { + return; + }; + let Ok(mut db) = DataBaseBuilder::path(".").build_in_memory() else { + return; + }; + + for sql in strip_slt(script).split(';') { + run_one(&mut db, sql); + } +}); + +/// sqllogictest record headers and directives; such lines never start SQL. +const SLT_DIRECTIVES: &[&str] = &[ + "statement", + "query", + "onlyif", + "skipif", + "control", + "halt", + "hash-threshold", + "subtest", + "sleep", + "include", +]; + +/// Keeps only the SQL of an sqllogictest script. +/// +/// Directives, comments and blank lines become `;` (a record ends at a blank +/// line and `query` SQL usually has no trailing `;`). Expected results, from +/// `----` up to the next blank line, are dropped. +fn strip_slt(script: &str) -> String { + let mut sql = String::with_capacity(script.len()); + let mut in_results = false; + + for line in script.lines() { + let trimmed = line.trim(); + if in_results { + if trimmed.is_empty() { + in_results = false; + sql.push(';'); + } + continue; + } + if trimmed.starts_with("----") { + in_results = true; + sql.push(';'); + } else if trimmed.is_empty() + || trimmed.starts_with('#') + || trimmed + .split_whitespace() + .next() + .is_some_and(|word| SLT_DIRECTIVES.contains(&word)) + { + sql.push(';'); + } else { + sql.push_str(line); + sql.push('\n'); + } + } + sql +} + +fn run_one(db: &mut Database, sql: &str) { + let Ok(statements) = prepare_all(sql) else { + return; + }; + // `split(';')` leaves one statement per chunk except for `;` inside + // string literals; such chunks usually fail to parse and are skipped. + let [statement] = statements.as_slice() else { + return; + }; + let Ok(kind) = command_type(statement) else { + return; + }; + + match kind { + CommandType::DDL => { + let _ = db.ddl(sql); + } + CommandType::Analyze => { + if let Statement::Analyze(analyze) = statement { + if let Some(table_name) = analyze.table_name.as_ref() { + let _ = db.analyze(table_name.to_string()); + } + } + } + _ => { + let Ok(mut iter) = db.run(sql) else { + return; + }; + // Drain and render every value, like the sqllogictest harness does. + loop { + match iter.next_tuple(|_, tuple| { + for value in tuple.values.iter() { + let _ = value.to_string(); + } + }) { + Ok(Some(())) => continue, + Ok(None) => { + let _ = iter.done(); + break; + } + Err(_) => break, + } + } + } + } +} diff --git a/fuzz/sql.dict b/fuzz/sql.dict new file mode 100644 index 00000000..78395974 --- /dev/null +++ b/fuzz/sql.dict @@ -0,0 +1,95 @@ +# libFuzzer dictionary for sql_exec: tokens the mutator may splice in. +"select" +"from" +"where" +"group by" +"having" +"order by" +"limit" +"offset" +"distinct" +"union" +"union all" +"except" +"intersect" +"join" +"left join" +"right join" +"full join" +"cross join" +"inner join" +"on" +"using" +"exists" +"in" +"not" +"and" +"or" +"is null" +"is not null" +"between" +"like" +"case when" +"then" +"else" +"end" +"cast(" +"as" +"null" +"true" +"false" +"count(" +"sum(" +"avg(" +"min(" +"max(" +"over (" +"partition by" +"with" +"insert into" +"values" +"update" +"set" +"delete from" +"create table" +"create index" +"create unique index" +"create view" +"alter table" +"add column" +"drop column" +"drop table" +"primary key" +"unique" +"default" +"analyze table" +"explain" +"int" +"bigint" +"smallint" +"tinyint" +"float" +"double" +"decimal(10,2)" +"varchar" +"char(4)" +"boolean" +"date" +"datetime" +"timestamp" +"time" +"9223372036854775807" +"-9223372036854775808" +"2147483647" +"-2147483648" +"0" +"-1" +"1e308" +"''" +"=" +"<>" +"<=" +">=" +"||" +"%" +"/" From 45f1f170bdfeeade35779f004d04cd77c0d41bbd Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 4 Oct 2026 18:20:27 +0800 Subject: [PATCH 02/32] test: fail fuzz runs on OOM and tolerate trailing ignored timeouts Run libFuzzer with -ignore_ooms=0 so OOMs fail `make fuzz` like crashes; only timeouts stay ignored (TODO kept). Fork mode exits with the last child's exit code, so a run whose final job hit an ignored timeout exited 70 and failed. Build with `cargo fuzz build` and run the binary directly, treating 0 and 70 as success. --- Makefile | 21 +++++++++++++++------ fuzz/fuzz_targets/sql_exec.rs | 16 ++++++++-------- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/Makefile b/Makefile index 1666e836..40853e66 100644 --- a/Makefile +++ b/Makefile @@ -21,6 +21,8 @@ FUZZ_TARGET ?= sql_exec FUZZ_TIME ?= 60 # Pass the host triple explicitly: prebuilt (musl) cargo-fuzz binaries otherwise default to musl. FUZZ_TRIPLE ?= $(shell rustc -vV | sed -n 's/^host: //p') +FUZZ_TARGET_DIR ?= $(CURDIR)/fuzz/target +FUZZ_BIN = $(FUZZ_TARGET_DIR)/$(FUZZ_TRIPLE)/release/$(FUZZ_TARGET) COVERAGE_REPORT_ARGS ?= --llvm --ignore-not-existing --keep-only 'src/**' --ignore 'src/**/tests/**' --ignore 'tests/**' --ignore 'tpcc/**' --excl-start 'GRCOV_EXCL_START' --excl-stop 'GRCOV_EXCL_STOP' .PHONY: test test-python test-wasm test-slt test-all codecov codecov-html wasm-build check tpcc tpcc-kitesql-rocksdb tpcc-kitesql-lmdb tpcc-lmdb-flamegraph tpcc-lmdb-heaptrack tpcc-sqlite tpcc-sqlite-practical tpcc-sqlite-balanced tpcc-dual cargo-check build wasm-examples native-examples fmt clippy fuzz @@ -79,14 +81,21 @@ codecov-html: ## Run a fuzz target for FUZZ_TIME seconds (needs a nightly toolchain and cargo-fuzz). ## tests/slt is a read-only seed dir: libFuzzer only writes new inputs to the FIRST dir. +## Fork mode ignores OOMs by default, hence the explicit -ignore_ooms=0: crashes and OOMs fail the run. +## It also exits with the LAST child's code, so a trailing ignored timeout (70) is not a failure. +## The binary is run directly because `cargo fuzz run` turns every non-zero exit into a failure. +## TODO: stop ignoring timeouts once unbounded queries can be told apart from hangs +## (see fuzz/fuzz_targets/sql_exec.rs). fuzz: - @mkdir -p fuzz/corpus/$(FUZZ_TARGET) - $(CARGO) +$(FUZZ_TOOLCHAIN) fuzz run --target $(FUZZ_TRIPLE) $(FUZZ_TARGET) \ - fuzz/corpus/$(FUZZ_TARGET) tests/slt -- \ + @mkdir -p fuzz/corpus/$(FUZZ_TARGET) fuzz/artifacts/$(FUZZ_TARGET) + $(CARGO) +$(FUZZ_TOOLCHAIN) fuzz build --target $(FUZZ_TRIPLE) --target-dir $(FUZZ_TARGET_DIR) $(FUZZ_TARGET) + @status=0; $(FUZZ_BIN) fuzz/corpus/$(FUZZ_TARGET) tests/slt \ + -artifact_prefix=fuzz/artifacts/$(FUZZ_TARGET)/ \ -dict=fuzz/sql.dict -max_len=32768 -timeout=10 -rss_limit_mb=4096 -max_total_time=$(FUZZ_TIME) \ - -fork=1 -ignore_timeouts=1 -ignore_ooms=1 -# TODO: stop ignoring timeouts/OOMs once unbounded queries can be told apart from hangs -# (see fuzz/fuzz_targets/sql_exec.rs). Crashes still fail the run. + -fork=1 -ignore_timeouts=1 -ignore_ooms=0 -timeout_exitcode=70 || status=$$?; \ + if [ $$status -ne 0 ] && [ $$status -ne 70 ]; then \ + echo "fuzz: $(FUZZ_TARGET) failed (exit $$status), see fuzz/artifacts/$(FUZZ_TARGET)/"; exit $$status; \ + fi ## Run formatting (check mode) across the workspace. fmt: diff --git a/fuzz/fuzz_targets/sql_exec.rs b/fuzz/fuzz_targets/sql_exec.rs index 501cb925..8bd8696a 100644 --- a/fuzz/fuzz_targets/sql_exec.rs +++ b/fuzz/fuzz_targets/sql_exec.rs @@ -21,15 +21,15 @@ //! sqllogictest syntax is stripped first, so `tests/slt` can be used directly //! as the seed corpus; plain SQL inputs pass through unchanged. //! -//! `Err` results are expected and ignored. Any panic or sanitizer report is a -//! bug. +//! `Err` results are expected and ignored. Any panic, sanitizer report or OOM +//! is a bug. //! -//! TODO: timeouts and OOMs are skipped (`make fuzz` runs libFuzzer in fork -//! mode). Mutations easily produce legitimately unbounded queries, e.g. a -//! recursive CTE that never converges, and KiteSQL cannot interrupt a running -//! statement, so they are indistinguishable from real hangs. Revisit once the -//! executor has a cheap interrupt point or the structured generator avoids -//! unbounded queries. +//! TODO: timeouts are skipped (`make fuzz` runs libFuzzer in fork mode with +//! `-ignore_timeouts=1`). Mutations easily produce legitimately unbounded +//! queries, e.g. a recursive CTE that never converges, and KiteSQL cannot +//! interrupt a running statement, so they are indistinguishable from real +//! hangs. Revisit once the executor has a cheap interrupt point or the +//! structured generator avoids unbounded queries. #![no_main] From 7346b55f0fce075244216464184e637994881676 Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 4 Oct 2026 20:29:51 +0800 Subject: [PATCH 03/32] fix: keep projected columns aligned when a skipped column is NULL Tuple::deserialize_from_into pushed a NULL for every NULL column, including columns skipped by the projection (SkipFixed / SkipVariable), which yield no value otherwise. Later projected columns were shifted by one, so a plain `select c, b from t` could return wrong values, and type-specialized evaluators were handed values of the wrong type (UB in release builds). --- src/types/tuple.rs | 23 ++++++++++++++++++++++- tests/slt/projection.slt | 20 ++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/types/tuple.rs b/src/types/tuple.rs index bd9f07da..abbc9611 100644 --- a/src/types/tuple.rs +++ b/src/types/tuple.rs @@ -216,7 +216,15 @@ impl Tuple { for (i, deserializer) in deserializers.into_iter().enumerate() { if is_null(bytes[i / BITS_MAX_INDEX], i % BITS_MAX_INDEX) { - self.values.push(DataValue::Null); + // A skipped (unprojected) column yields no value, NULL or not; + // pushing one here would shift every later projected column. + if !matches!( + deserializer.borrow(), + TupleValueSerializableImpl::SkipFixed(_) + | TupleValueSerializableImpl::SkipVariable + ) { + self.values.push(DataValue::Null); + } continue; } deserializer @@ -536,6 +544,19 @@ mod tests { values: vec![DataValue::Int32(0), DataValue::Int16(1)], } ); + + // A NULL in a skipped column must not shift later projected columns. + let mut null_skipped = tuples[0].clone(); + null_skipped.values[1] = DataValue::Null; + null_skipped.serialize_to(&serializers, &mut bytes).unwrap(); + tuple_2 + .deserialize_from_into(&projection_serializers, &bytes, columns.len()) + .unwrap(); + + assert_eq!( + tuple_2.values, + vec![DataValue::Int32(0), DataValue::Int16(1)] + ); } // multiple pk { diff --git a/tests/slt/projection.slt b/tests/slt/projection.slt index 94a356a7..82589a41 100644 --- a/tests/slt/projection.slt +++ b/tests/slt/projection.slt @@ -11,3 +11,23 @@ select id, v1 from wide_table; statement ok drop table wide_table + +# A NULL in an unprojected column must not shift the projected ones. +statement ok +create table null_skip(id int primary key, a int, b bigint, c varchar, d double); + +statement ok +insert into null_skip values (4, null, 2, 'a', null); + +query TI +select c, b from null_skip; +---- +a 2 + +query T +select c from null_skip where c like 'a_' or b is not null; +---- +a + +statement ok +drop table null_skip From 7985496813cc05bb69055990e3767d7020101a37 Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 4 Oct 2026 20:29:51 +0800 Subject: [PATCH 04/32] fix: only rearrange comparisons exactly and check integer negation Simplify moved arithmetic across comparisons with wrong formulas (e.g. `2 + b = 4` became `b = 6`), inverted `*` / `/` without regard to sign or integer truncation, mixed up terms of different columns, and typed rewritten nodes inconsistently (unreachable_unchecked). Replace it with Simplify::isolate_column, which only peels constant `+`/`-` terms in integer domains and unary signs, aborting on overflow. Negating the minimum integer panicked (debug) or wrapped (release); integer unary minus now returns DatabaseError::OverFlow like binary `+`/`-`, so UnaryEvaluatorRef::unary_eval returns a Result. --- src/expression/evaluator.rs | 2 +- src/expression/simplify.rs | 371 +++++++++++++---------------------- src/types/evaluator/mod.rs | 2 +- src/types/evaluator/unary.rs | 53 +++-- tests/slt/bigint.slt | 27 ++- tests/slt/where.slt | 101 +++++++++- 6 files changed, 307 insertions(+), 249 deletions(-) diff --git a/src/expression/evaluator.rs b/src/expression/evaluator.rs index aff9f0e7..ee4689c0 100644 --- a/src/expression/evaluator.rs +++ b/src/expression/evaluator.rs @@ -145,7 +145,7 @@ impl ScalarExpression { evaluator .as_ref() .ok_or(DatabaseError::EvaluatorNotFound)? - .unary_eval(&value), + .unary_eval(&value)?, )) } ScalarExpression::AggCall { .. } => { diff --git a/src/expression/simplify.rs b/src/expression/simplify.rs index 8823b898..54982d2d 100644 --- a/src/expression/simplify.rs +++ b/src/expression/simplify.rs @@ -23,28 +23,6 @@ use crate::types::value::DataValue; use crate::types::LogicalType; use std::borrow::Cow; -#[derive(Debug)] -enum Replace { - Binary(ReplaceBinary), - Unary(ReplaceUnary), -} - -#[derive(Debug)] -struct ReplaceBinary { - column_expr: ExprRef, - val_expr: ExprRef, - op: BinaryOperator, - ty: LogicalType, - is_column_left: bool, -} - -#[derive(Debug)] -struct ReplaceUnary { - child_expr: ExprRef, - op: UnaryOperator, - ty: LogicalType, -} - pub struct ConstantCalculator; impl ConstantCalculator { @@ -73,9 +51,9 @@ impl ExprVisitorMut for ConstantCalculator { return Ok(false); } let value = if let Some(evaluator) = evaluator { - evaluator.unary_eval(unary_val) + evaluator.unary_eval(unary_val)? } else { - unary_create(Cow::Borrowed(ty), *op)?.unary_eval(unary_val) + unary_create(Cow::Borrowed(ty), *op)?.unary_eval(unary_val)? }; *expr = ScalarExpression::Constant(value); } @@ -130,9 +108,7 @@ impl ExprVisitorMut for ConstantCalculator { } #[derive(Debug, Default)] -pub struct Simplify { - replaces: Vec, -} +pub struct Simplify; impl ExprVisitorMut for Simplify { fn visit_expression( @@ -149,13 +125,15 @@ impl ExprVisitorMut for Simplify { } => { let op = *op; let ty = ty.clone(); - let child_expr = *arg_expr; + // An overflowing fold (`-MIN`) is left to fail at runtime. let value = if let Some(value) = arg_expr.unpack_val(arena) { - Some(if let Some(evaluator) = evaluator { - evaluator.unary_eval(&value) + if let Some(evaluator) = evaluator { + evaluator.unary_eval(&value).ok() } else { - unary_create(Cow::Borrowed(&ty), op)?.unary_eval(&value) - }) + unary_create(Cow::Borrowed(&ty), op)? + .unary_eval(&value) + .ok() + } } else { None }; @@ -166,26 +144,17 @@ impl ExprVisitorMut for Simplify { if let Some(new_expr) = Self::take_negated_range_comparison(*arg_expr, arena) { *expr = new_expr; return self.visit_expression(expr, arena); - } else { - self.replaces - .push(Replace::Unary(ReplaceUnary { child_expr, op, ty })); } - } else { - self.replaces - .push(Replace::Unary(ReplaceUnary { child_expr, op, ty })); } } ScalarExpression::Binary { op, left_expr, right_expr, - ty, .. } => { - self.fix_expr(left_expr, right_expr, op, arena)?; - - // `(c1 - 1) and (c1 + 2)` cannot fix! - self.fix_expr(right_expr, left_expr, op, arena)?; + self.visit(left_expr, arena)?; + self.visit(right_expr, arena)?; if let Some(new_expr) = Self::take_bool_normalized_range_comparison(*op, *left_expr, *right_expr, arena) @@ -194,66 +163,18 @@ impl ExprVisitorMut for Simplify { return self.visit_expression(expr, arena); } - if Self::is_arithmetic(op) { - match ( - left_expr.unpack_bound_col(arena, false), - right_expr.unpack_bound_col(arena, false), - ) { - (Some((col, position)), None) => { - self.replaces.push(Replace::Binary(ReplaceBinary { - column_expr: arena - .alloc_expression(ScalarExpression::column_expr(col, position)), - val_expr: *right_expr, - op: *op, - ty: ty.clone(), - is_column_left: true, - })); - } - (None, Some((col, position))) => { - self.replaces.push(Replace::Binary(ReplaceBinary { - column_expr: arena - .alloc_expression(ScalarExpression::column_expr(col, position)), - val_expr: *left_expr, - op: *op, - ty: ty.clone(), - is_column_left: false, - })); - } - (None, None) => { - if self.replaces.is_empty() { - return Ok(false); - } - - match ( - left_expr.unpack_bound_col(arena, true), - right_expr.unpack_bound_col(arena, true), - ) { - (Some((col, position)), None) => { - self.replaces.push(Replace::Binary(ReplaceBinary { - column_expr: arena.alloc_expression( - ScalarExpression::column_expr(col, position), - ), - val_expr: *right_expr, - op: *op, - ty: ty.clone(), - is_column_left: true, - })); - } - (None, Some((col, position))) => { - self.replaces.push(Replace::Binary(ReplaceBinary { - column_expr: arena.alloc_expression( - ScalarExpression::column_expr(col, position), - ), - val_expr: *left_expr, - op: *op, - ty: ty.clone(), - is_column_left: false, - })); - } - _ => (), - } - } - _ => (), + // Move constant terms and signs off the column side, e.g. + // `1 < -(c1 + 1)` => `c1 < -2`, so a range can be detached. + if Self::is_rearrangeable_comparison(op) { + let isolated = Self::isolate_column(*left_expr, *op, *right_expr, arena) + .or_else(|| { + let flipped = Self::flip_comparison(*op); + Self::isolate_column(*right_expr, flipped, *left_expr, arena) + }); + if let Some((column, fixed_op, value)) = isolated { + *op = fixed_op; + *left_expr = column; + *right_expr = arena.alloc_expression(ScalarExpression::Constant(value)); } } } @@ -351,16 +272,6 @@ impl ExprVisitorMut for Simplify { } impl Simplify { - fn is_arithmetic(op: &mut BinaryOperator) -> bool { - matches!( - op, - BinaryOperator::Plus - | BinaryOperator::Divide - | BinaryOperator::Minus - | BinaryOperator::Multiply - ) - } - fn is_rearrangeable_comparison(op: &BinaryOperator) -> bool { matches!( op, @@ -459,127 +370,126 @@ impl Simplify { None } - fn fix_expr( - &mut self, - left_expr: &mut ExprRef, - right_expr: &mut ExprRef, - op: &mut BinaryOperator, - arena: &mut (dyn MetaArena + '_), - ) -> Result<(), DatabaseError> { - self.visit(left_expr, arena)?; - - if Self::is_arithmetic(op) { - return Ok(()); - } - // Terms can only be moved across a comparison. Operators such as `%` - // are not invertible, so pending replaces must not be applied to them. - if !Self::is_rearrangeable_comparison(op) { - self.replaces.clear(); - return Ok(()); - } - while let Some(replace) = self.replaces.pop() { - match replace { - Replace::Binary(binary) => { - Self::fix_binary(binary, left_expr, right_expr, op, arena) - } - Replace::Unary(unary) => { - Self::fix_unary(unary, left_expr, right_expr, op, arena); - self.fix_expr(left_expr, right_expr, op, arena)?; - } - } + /// `a op b` <=> `b flip(op) a`. + fn flip_comparison(op: BinaryOperator) -> BinaryOperator { + match op { + BinaryOperator::Gt => BinaryOperator::Lt, + BinaryOperator::Lt => BinaryOperator::Gt, + BinaryOperator::GtEq => BinaryOperator::LtEq, + BinaryOperator::LtEq => BinaryOperator::GtEq, + op => op, } - - Ok(()) } - fn fix_unary( - replace_unary: ReplaceUnary, - col_expr: &mut ExprRef, - val_expr: &mut ExprRef, - op: &mut BinaryOperator, - arena: &mut (dyn MetaArena + '_), - ) { - let ReplaceUnary { - child_expr, - op: fix_op, - ty: fix_ty, - } = replace_unary; - *col_expr = child_expr; - - *val_expr = arena.alloc_expression(ScalarExpression::Unary { - op: fix_op, - expr: *val_expr, - evaluator: None, - ty: fix_ty, - }); - *op = match fix_op { - UnaryOperator::Plus => *op, - UnaryOperator::Minus => match *op { - BinaryOperator::Plus => BinaryOperator::Minus, - BinaryOperator::Minus => BinaryOperator::Plus, - BinaryOperator::Multiply => BinaryOperator::Divide, - BinaryOperator::Divide => BinaryOperator::Multiply, - BinaryOperator::Gt => BinaryOperator::Lt, - BinaryOperator::Lt => BinaryOperator::Gt, - BinaryOperator::GtEq => BinaryOperator::LtEq, - BinaryOperator::LtEq => BinaryOperator::GtEq, - source_op => source_op, - }, - UnaryOperator::Not => match *op { - BinaryOperator::Gt => BinaryOperator::Lt, - BinaryOperator::Lt => BinaryOperator::Gt, - BinaryOperator::GtEq => BinaryOperator::LtEq, - BinaryOperator::LtEq => BinaryOperator::GtEq, - source_op => source_op, - }, - }; + /// Evaluates `left op right` in `ty`; `None` on a failed cast or overflow. + fn eval_in( + ty: &LogicalType, + op: BinaryOperator, + left: DataValue, + right: DataValue, + ) -> Option { + let left = left.cast(ty).ok()?; + let right = right.cast(ty).ok()?; + binary_create(Cow::Borrowed(ty), op) + .ok()? + .binary_eval(&left, &right) + .ok() } - fn fix_binary( - replace_binary: ReplaceBinary, - left_expr: &mut ExprRef, - right_expr: &mut ExprRef, - op: &mut BinaryOperator, - arena: &mut (dyn MetaArena + '_), - ) { - let ReplaceBinary { - column_expr, - val_expr, - op: fix_op, - ty: fix_ty, - is_column_left, - } = replace_binary; - let op_flip = |op: BinaryOperator| match op { - BinaryOperator::Plus => BinaryOperator::Minus, - BinaryOperator::Minus => BinaryOperator::Plus, - BinaryOperator::Multiply => BinaryOperator::Divide, - BinaryOperator::Divide => BinaryOperator::Multiply, - _ => unreachable!(), - }; - let comparison_flip = |op: BinaryOperator| match op { - BinaryOperator::Gt => BinaryOperator::Lt, - BinaryOperator::GtEq => BinaryOperator::LtEq, - BinaryOperator::Lt => BinaryOperator::Gt, - BinaryOperator::LtEq => BinaryOperator::GtEq, - source_op => source_op, - }; - let (fixed_op, fixed_left_expr, fixed_right_expr) = if is_column_left { - (op_flip(fix_op), *right_expr, val_expr) - } else { - if matches!(fix_op, BinaryOperator::Minus | BinaryOperator::Multiply) { - *op = comparison_flip(*op); - } - (fix_op, val_expr, *right_expr) - }; + /// Rewrites `expr op value` (`value` a constant) into `column op' value'` + /// by moving constant `+`/`-` terms and unary signs to the constant side, + /// e.g. `-(c1 + 1) > 1` => `c1 < -2`. + /// + /// Only exact rewrites are done: `+`/`-` terms only in integer domains + /// (float and decimal arithmetic round), never `*`/`/` (sign-dependent and + /// not invertible for integers), and any overflow while folding aborts. + /// Returns `None` when nothing could be peeled. + fn isolate_column( + mut expr: ExprRef, + mut op: BinaryOperator, + value_expr: ExprRef, + arena: &(dyn MetaArena + '_), + ) -> Option<(ExprRef, BinaryOperator, DataValue)> { + let mut value = value_expr.unpack_val(arena)?; + if value.has_parameter() { + return None; + } + let mut peeled = false; - *left_expr = column_expr; - *right_expr = arena.alloc_expression(ScalarExpression::Binary { - op: fixed_op, - left_expr: fixed_left_expr, - right_expr: fixed_right_expr, - evaluator: None, - ty: fix_ty, - }); + loop { + match arena.expression(expr) { + ScalarExpression::ColumnRef { .. } => { + return peeled.then_some((expr, op, value)); + } + ScalarExpression::Alias { expr: inner, .. } => expr = *inner, + ScalarExpression::Unary { + op: UnaryOperator::Plus, + expr: inner, + .. + } => { + expr = *inner; + peeled = true; + } + // `-x op v` <=> `x flip(op) -v`. Negation is exact, but the + // binder casts unsigned operands to signed first, so skip them. + ScalarExpression::Unary { + op: UnaryOperator::Minus, + expr: inner, + .. + } => { + if inner.return_type(arena).is_unsigned_numeric() { + return None; + } + let ty = value.logical_type(); + value = Self::eval_in(&ty, BinaryOperator::Minus, DataValue::Int32(0), value)?; + op = Self::flip_comparison(op); + expr = *inner; + peeled = true; + } + ScalarExpression::Binary { + op: arith @ (BinaryOperator::Plus | BinaryOperator::Minus), + left_expr, + right_expr, + ty, + .. + } => { + let (inner, constant, constant_left) = + match (left_expr.unpack_val(arena), right_expr.unpack_val(arena)) { + (None, Some(constant)) => (*left_expr, constant, false), + (Some(constant), None) => (*right_expr, constant, true), + _ => return None, + }; + if constant.has_parameter() { + return None; + } + // Compare in the domain the original comparison used. + let domain = LogicalType::max_logical_type(ty, &value.logical_type()) + .ok()? + .into_owned(); + if !(domain.is_signed_numeric() || domain.is_unsigned_numeric()) { + return None; + } + value = match (*arith, constant_left) { + // `x + c op v`, `c + x op v` => `x op v - c` + (BinaryOperator::Plus, _) => { + Self::eval_in(&domain, BinaryOperator::Minus, value, constant)? + } + // `x - c op v` => `x op v + c` + (_, false) => { + Self::eval_in(&domain, BinaryOperator::Plus, value, constant)? + } + // `c - x op v` => `x flip(op) c - v` + (_, true) => { + op = Self::flip_comparison(op); + Self::eval_in(&domain, BinaryOperator::Minus, constant, value)? + } + }; + expr = inner; + peeled = true; + } + _ => return None, + } + } } } @@ -605,13 +515,14 @@ impl ExprRef { if value.has_parameter() { return None; } - Some(if let Some(evaluator) = evaluator { + if let Some(evaluator) = evaluator { evaluator.unary_eval(&value) } else { unary_create(Cow::Borrowed(ty), *op) .ok()? .unary_eval(&value) - }) + } + .ok() } ScalarExpression::Binary { left_expr, diff --git a/src/types/evaluator/mod.rs b/src/types/evaluator/mod.rs index e37be249..9b9e41a6 100644 --- a/src/types/evaluator/mod.rs +++ b/src/types/evaluator/mod.rs @@ -84,7 +84,7 @@ impl UnaryEvaluatorRef { Self { pos } } - pub fn unary_eval(&self, value: &DataValue) -> DataValue { + pub fn unary_eval(&self, value: &DataValue) -> Result { unary::eval_unary(self.pos, value) } } diff --git a/src/types/evaluator/unary.rs b/src/types/evaluator/unary.rs index f56fcac5..ac441c3a 100644 --- a/src/types/evaluator/unary.rs +++ b/src/types/evaluator/unary.rs @@ -79,23 +79,23 @@ pub fn unary_create( pub(crate) fn eval_unary( pos: u16, value: &crate::types::value::DataValue, -) -> crate::types::value::DataValue { - match pos { +) -> Result { + Ok(match pos { UNARY_INT8_PLUS => int8_plus_unary_eval(value), - UNARY_INT8_MINUS => int8_minus_unary_eval(value), + UNARY_INT8_MINUS => int8_minus_unary_eval(value)?, UNARY_INT16_PLUS => int16_plus_unary_eval(value), - UNARY_INT16_MINUS => int16_minus_unary_eval(value), + UNARY_INT16_MINUS => int16_minus_unary_eval(value)?, UNARY_INT32_PLUS => int32_plus_unary_eval(value), - UNARY_INT32_MINUS => int32_minus_unary_eval(value), + UNARY_INT32_MINUS => int32_minus_unary_eval(value)?, UNARY_INT64_PLUS => int64_plus_unary_eval(value), - UNARY_INT64_MINUS => int64_minus_unary_eval(value), + UNARY_INT64_MINUS => int64_minus_unary_eval(value)?, UNARY_BOOLEAN_NOT => boolean_not_unary_eval(value), UNARY_FLOAT32_PLUS => float32_plus_unary_eval(value), UNARY_FLOAT32_MINUS => float32_minus_unary_eval(value), UNARY_FLOAT64_PLUS => float64_plus_unary_eval(value), UNARY_FLOAT64_MINUS => float64_minus_unary_eval(value), _ => unreachable!("unknown unary evaluator position {pos}"), - } + }) } #[macro_export] @@ -107,14 +107,20 @@ macro_rules! numeric_unary_evaluator_definition { ) -> $crate::types::value::DataValue { value.clone() } + // `-MIN` does not fit the type: report it like binary `+`/`-` do + // instead of panicking (debug) or wrapping (release). pub fn [<$value_type:snake _minus_unary_eval>]( value: &$crate::types::value::DataValue, - ) -> $crate::types::value::DataValue { - match value { - $compute_type(value) => $compute_type(-value), + ) -> Result<$crate::types::value::DataValue, $crate::errors::DatabaseError> { + Ok(match value { + $compute_type(value) => $compute_type( + value + .checked_neg() + .ok_or($crate::errors::DatabaseError::OverFlow)?, + ), $crate::types::value::DataValue::Null => $crate::types::value::DataValue::Null, _ => unsafe { std::hint::unreachable_unchecked() }, - } + }) } } }; @@ -242,21 +248,38 @@ mod test { for (ty, op, value, expected) in cases { let evaluator = create(ty, op)?; - assert_eq!(evaluator.unary_eval(&value), expected); - assert_eq!(evaluator.unary_eval(&DataValue::Null), DataValue::Null); + assert_eq!(evaluator.unary_eval(&value)?, expected); + assert_eq!(evaluator.unary_eval(&DataValue::Null)?, DataValue::Null); } Ok(()) } + #[test] + fn test_integer_minus_overflow_is_an_error() -> Result<(), DatabaseError> { + for (ty, min) in [ + (LogicalType::Tinyint, DataValue::Int8(i8::MIN)), + (LogicalType::Smallint, DataValue::Int16(i16::MIN)), + (LogicalType::Integer, DataValue::Int32(i32::MIN)), + (LogicalType::Bigint, DataValue::Int64(i64::MIN)), + ] { + let evaluator = create(ty, UnaryOperator::Minus)?; + assert!(matches!( + evaluator.unary_eval(&min), + Err(DatabaseError::OverFlow) + )); + } + Ok(()) + } + #[test] fn test_boolean_unary_eval() -> Result<(), DatabaseError> { let evaluator = create(LogicalType::Boolean, UnaryOperator::Not)?; assert_eq!( - evaluator.unary_eval(&DataValue::Boolean(true)), + evaluator.unary_eval(&DataValue::Boolean(true))?, DataValue::Boolean(false) ); - assert_eq!(evaluator.unary_eval(&DataValue::Null), DataValue::Null); + assert_eq!(evaluator.unary_eval(&DataValue::Null)?, DataValue::Null); Ok(()) } diff --git a/tests/slt/bigint.slt b/tests/slt/bigint.slt index 9bd021c2..585d7065 100644 --- a/tests/slt/bigint.slt +++ b/tests/slt/bigint.slt @@ -25,4 +25,29 @@ select sum(v2) from t where v2 < 10; 9 statement ok -drop table t \ No newline at end of file +drop table t + +# Negating the minimum integer overflows: an error, like `+`/`-` overflow, +# not a panic (debug) or a wrapped value (release). +statement ok +create table neg(id int primary key, a int, b bigint); + +statement ok +insert into neg values (1, -2147483648, -9223372036854775807 - 1), (2, 3, 4); + +statement error +select - a from neg; + +statement error +select - b from neg; + +statement error +select min(- a) from neg; + +query II rowsort +select - a, - b from neg where id = 2; +---- +-3 -4 + +statement ok +drop table neg diff --git a/tests/slt/where.slt b/tests/slt/where.slt index ef88561a..bf42148f 100644 --- a/tests/slt/where.slt +++ b/tests/slt/where.slt @@ -82,4 +82,103 @@ select v2 from t where v1 is null; # 4 statement ok -drop table t \ No newline at end of file +drop table t + +# Predicates whose arithmetic the optimizer may move across the comparison +# (Simplify::isolate_column). Each must return the same rows as evaluating +# the predicate as written. + +statement ok +create table t(id int primary key, a int, b bigint, d double); + +statement ok +insert into t values (1, 1, 2, 1.5), (2, 5, 3, -2.5), (3, -4, -2, null); + +# constant on the left of `+` / `-` +query I rowsort +select id from t where 2 + b = 4; +---- +1 + +query I rowsort +select id from t where 2 + b > 3; +---- +1 +2 + +query I rowsort +select id from t where 1 + (2 - b) = 1; +---- +1 + +query I rowsort +select id from t where 2 + (b + 1) = 6; +---- +2 + +query I rowsort +select id from t where 2 + -b = 4; +---- +3 + +# unary minus over a sum +query I rowsort +select id from t where -(b + 1) > 1; +---- + +query I rowsort +select id from t where -(1 - b) > 1; +---- +2 + +# `*` / `/` are not rearranged (sign-dependent, not invertible for integers) +query I rowsort +select id from t where 3 * b = 6; +---- +1 + +query I rowsort +select id from t where b * -1 > 0; +---- +3 + +query I rowsort +select id from t where b / 2 = 1; +---- +1 + +# more than one column +query I rowsort +select id from t where (1 - b) * id = -4; +---- +2 + +query I rowsort +select id from t where (b + 1) + (a + 1) = 10; +---- +2 + +# mixed types and overflow boundaries +query I rowsort +select id from t where -d > 2; +---- +2 + +query I rowsort +select id from t where -b > 1.5; +---- +3 + +query I rowsort +select id from t where -a < -2147483648; +---- + +query I rowsort +select id from t where a + 1 > -2147483648; +---- +1 +2 +3 + +statement ok +drop table t From bc6235a1b231d8a635585bdcd4f2199ff18eb461 Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 4 Oct 2026 20:29:51 +0800 Subject: [PATCH 05/32] fix: char_length/octet_length return BIGINT and NULL for NULL They declared a VARCHAR return type while producing an unsigned integer, so e.g. avg(char_length(c)) built a non-numeric accumulator, and they returned 0 instead of NULL for a NULL argument. --- src/function/char_length.rs | 12 ++++++++---- src/function/octet_length.rs | 13 +++++++++---- tests/slt/string_length.slt | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 8 deletions(-) create mode 100644 tests/slt/string_length.slt diff --git a/src/function/char_length.rs b/src/function/char_length.rs index 8a413802..e2a49bbc 100644 --- a/src/function/char_length.rs +++ b/src/function/char_length.rs @@ -49,6 +49,10 @@ impl ScalarFunctionImpl for CharLength { tuples: Option<&dyn TupleLike>, ) -> Result { let mut value = arena.expression(exprs[0]).eval(arena, tuples)?; + // SQL: the length of NULL is NULL, not 0. + if value.is_null() { + return Ok(DataValue::Null); + } if !matches!(value.logical_type(), LogicalType::Varchar(_, _)) { value = std::borrow::Cow::Owned( value @@ -56,11 +60,11 @@ impl ScalarFunctionImpl for CharLength { .cast(&LogicalType::Varchar(None, CharLengthUnits::Characters))?, ); } - let mut length: u64 = 0; + let mut length: i64 = 0; if let DataValue::Utf8 { value, ty, unit } = value.as_ref() { - length = value.chars().count() as u64; + length = value.chars().count() as i64; } - Ok(DataValue::UInt64(length)) + Ok(DataValue::Int64(length)) } fn monotonicity(&self) -> Option { @@ -68,7 +72,7 @@ impl ScalarFunctionImpl for CharLength { } fn return_type(&self) -> &LogicalType { - &LogicalType::Varchar(None, CharLengthUnits::Characters) + &LogicalType::Bigint } fn summary(&self) -> &FunctionSummary { diff --git a/src/function/octet_length.rs b/src/function/octet_length.rs index 273ff930..0a0ad13b 100644 --- a/src/function/octet_length.rs +++ b/src/function/octet_length.rs @@ -50,6 +50,10 @@ impl ScalarFunctionImpl for OctetLength { tuples: Option<&dyn TupleLike>, ) -> Result { let mut value = arena.expression(exprs[0]).eval(arena, tuples)?; + // SQL: the length of NULL is NULL, not 0. + if value.is_null() { + return Ok(DataValue::Null); + } if !matches!(value.logical_type(), LogicalType::Varchar(_, _)) { value = std::borrow::Cow::Owned( value @@ -57,19 +61,20 @@ impl ScalarFunctionImpl for OctetLength { .cast(&LogicalType::Varchar(None, CharLengthUnits::Characters))?, ); } - let mut length: u64 = 0; + let mut length: i64 = 0; if let DataValue::Utf8 { value, ty, unit } = value.as_ref() { - length = value.len() as u64; + length = value.len() as i64; } - Ok(DataValue::UInt64(length)) + Ok(DataValue::Int64(length)) } fn monotonicity(&self) -> Option { todo!() } + // Must match what `eval` produces (see char_length). fn return_type(&self) -> &LogicalType { - &LogicalType::Varchar(None, CharLengthUnits::Characters) + &LogicalType::Bigint } fn summary(&self) -> &FunctionSummary { diff --git a/tests/slt/string_length.slt b/tests/slt/string_length.slt new file mode 100644 index 00000000..8d12547c --- /dev/null +++ b/tests/slt/string_length.slt @@ -0,0 +1,35 @@ +# char_length / octet_length: integer result, NULL in -> NULL out. + +statement ok +create table s(id int primary key, c varchar); + +statement ok +insert into s values (1, 'ab'), (2, 'abcd'), (3, null); + +query R +select avg(char_length(c)) from s; +---- +3.0 + +query I +select sum(octet_length(c)) from s; +---- +6 + +query I rowsort +select id from s where char_length(c) = 2; +---- +1 + +query I rowsort +select id from s where char_length(c) + 1 > 3; +---- +2 + +query I +select char_length(c) from s where id = 3; +---- +null + +statement ok +drop table s From 0b333286be5f5a3ebebb5fba5bd17922756dce1a Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 4 Oct 2026 20:29:51 +0800 Subject: [PATCH 06/32] fix: do not rewrite DISTINCT group keys through shared select nodes The select list shares its expression nodes with the DISTINCT aggregate's group keys. Rewriting a named alias in place turned e.g. the group key of `select distinct lower(c) as x, c` into a reference to the aggregate's own output. Rewrite a copy instead. --- src/binder/distinct.rs | 24 +++++++++++++----------- tests/slt/distinct.slt | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 11 deletions(-) diff --git a/src/binder/distinct.rs b/src/binder/distinct.rs index fb95a97b..19234fbb 100644 --- a/src/binder/distinct.rs +++ b/src/binder/distinct.rs @@ -106,18 +106,20 @@ impl ExprVisitorMut for DistinctOutputBinder<'_> { expr: &mut ExprRef, arena: &mut (dyn MetaArena + '_), ) -> Result<(), DatabaseError> { - if let ScalarExpression::Alias { - alias: crate::expression::AliasType::Name(_), - .. - } = arena.expression(*expr) - { - return walk_mut_expr(self, expr, arena); - } - - if let Some(output_ref) = self.output_ref(*expr, arena) { - *expr = arena.alloc_expression(output_ref); - return Ok(()); + let is_named_alias = matches!( + arena.expression(*expr), + ScalarExpression::Alias { + alias: crate::expression::AliasType::Name(_), + .. + } + ); + if !is_named_alias { + if let Some(output_ref) = self.output_ref(*expr, arena) { + *expr = arena.alloc_expression(output_ref); + return Ok(()); + } } + *expr = arena.alloc_expression(arena.expression(*expr).clone()); walk_mut_expr(self, expr, arena) } } diff --git a/tests/slt/distinct.slt b/tests/slt/distinct.slt index 1fc29f17..6f6be572 100644 --- a/tests/slt/distinct.slt +++ b/tests/slt/distinct.slt @@ -41,3 +41,39 @@ SELECT DISTINCT b, a FROM distinct_pos ORDER BY a DESC LIMIT 2; statement ok DROP TABLE distinct_pos; + +# DISTINCT over aliased expressions: the select list shares nodes with the +# DISTINCT group keys, which must not be rewritten into self-references. +statement ok +create table distinct_alias(id int primary key, a int, b bigint, c varchar, d double); + +statement ok +insert into distinct_alias values (0, 1, 2, 'a', 1.5), (1, 2, null, null, null), (2, 1, 3, 'A', 0.5); + +query TT rowsort +select distinct lower(c) as x, c from distinct_alias; +---- +a A +a a +null null + +query T rowsort +select distinct (a = 1) as x from distinct_alias; +---- +false +true + +query I rowsort +select distinct a + 1 as x from distinct_alias; +---- +2 +3 + +query T rowsort +select distinct upper(c) as x from distinct_alias order by x; +---- +A +null + +statement ok +drop table distinct_alias From 74b84048865b5b90468693e70ab61c3d588e7ec5 Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 4 Oct 2026 20:29:51 +0800 Subject: [PATCH 07/32] fix: allow sum(NULL) SumAccumulator asserted a numeric type, but sum(NULL) binds with the NULL literal's type. It never sees a non-NULL value, so the result is NULL. --- src/execution/dql/aggregate/sum.rs | 4 +++- tests/slt/aggregation.slt | 27 ++++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/execution/dql/aggregate/sum.rs b/src/execution/dql/aggregate/sum.rs index f85c50c3..9c5a9f9c 100644 --- a/src/execution/dql/aggregate/sum.rs +++ b/src/execution/dql/aggregate/sum.rs @@ -28,7 +28,9 @@ pub struct SumAccumulator { impl SumAccumulator { pub fn new(ty: Cow<'_, LogicalType>) -> Result { - debug_assert!(ty.is_numeric()); + // `sum(null)` binds with the type of the NULL literal; it never sees a + // non-NULL value, so the result is NULL like in release builds. + debug_assert!(ty.is_numeric() || matches!(ty.as_ref(), LogicalType::SqlNull)); Ok(Self { result: DataValue::Null, diff --git a/tests/slt/aggregation.slt b/tests/slt/aggregation.slt index 80f55d7e..6afd2efb 100644 --- a/tests/slt/aggregation.slt +++ b/tests/slt/aggregation.slt @@ -74,4 +74,29 @@ select sum(v1) as a, v2 from t group by v2 order by a 6 3 statement ok -drop table t \ No newline at end of file +drop table t + +# sum(NULL) is NULL, not an assertion failure. +statement ok +create table sn(id int primary key, a int); + +statement ok +insert into sn values (1, 1), (2, 2); + +query I +select sum(null) from sn; +---- +null + +query I +select sum(distinct null) from sn; +---- +null + +query I +select (select sum(null) from sn) from sn where id = 1; +---- +null + +statement ok +drop table sn From f7024663f3de5fb76ab3250a92ab78cefe156c3a Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 4 Oct 2026 20:30:06 +0800 Subject: [PATCH 08/32] fix: do not share the operand of expanded IN / BETWEEN Simplify expanded `e IN (a, b)` / `e BETWEEN a AND b` into comparisons that all pointed at the same `e` node. A later in-place position rewrite (e.g. predicate pushdown below a multi-way join) then shifted that node once per comparison, so it read the wrong column. Give each comparison its own copy. --- src/expression/simplify.rs | 10 ++++++++-- tests/slt/where.slt | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/expression/simplify.rs b/src/expression/simplify.rs index 54982d2d..84b3254a 100644 --- a/src/expression/simplify.rs +++ b/src/expression/simplify.rs @@ -212,11 +212,15 @@ impl ExprVisitorMut for Simplify { }; for arg in args.drain(..) { + // Each comparison gets its own copy of the operand: a shared + // node is shifted twice by later in-place position rewrites + // (e.g. predicate pushdown), reading the wrong slot. + let arg_copy = arg_expr.clone_expression(arena)?; new_expr = ScalarExpression::Binary { op: op_2, left_expr: arena.alloc_expression(ScalarExpression::Binary { op: op_1, - left_expr: *arg_expr, + left_expr: arg_copy, right_expr: arg, evaluator: None, ty: LogicalType::Boolean, @@ -244,6 +248,8 @@ impl ExprVisitorMut for Simplify { BinaryOperator::LtEq, ) }; + // Same as IN: the operand must not be shared by both comparisons. + let arg_copy = arg_expr.clone_expression(arena)?; *expr = ScalarExpression::Binary { op, left_expr: arena.alloc_expression(ScalarExpression::Binary { @@ -255,7 +261,7 @@ impl ExprVisitorMut for Simplify { }), right_expr: arena.alloc_expression(ScalarExpression::Binary { op: right_op, - left_expr: *arg_expr, + left_expr: arg_copy, right_expr: *right_expr, evaluator: None, ty: LogicalType::Boolean, diff --git a/tests/slt/where.slt b/tests/slt/where.slt index bf42148f..e0d391dc 100644 --- a/tests/slt/where.slt +++ b/tests/slt/where.slt @@ -182,3 +182,39 @@ select id from t where a + 1 > -2147483648; statement ok drop table t + +# IN / BETWEEN over an expression below a multi-way join: each expanded +# comparison needs its own operand node, or a later position rewrite shifts a +# shared node twice and reads the wrong column. +statement ok +create table in_one(id int primary key); + +statement ok +create table in_share(id int primary key, a int, d double); + +statement ok +insert into in_one values (0); + +statement ok +insert into in_share values (0, 0, 1.0), (1, 1, 0.0); + +query I rowsort +select y.id from in_one as w cross join in_share as x inner join in_share as y on x.a = y.id where (y.d + 2) not in (1.5, 3); +---- +1 + +query I rowsort +select y.id from in_one as w cross join in_share as x inner join in_share as y on x.a = y.id where (y.d + 2) in (1.5, 3); +---- +0 + +query I rowsort +select y.id from in_one as w cross join in_share as x inner join in_share as y on x.a = y.id where (y.d + 2) between 2.5 and 3; +---- +0 + +statement ok +drop table in_share + +statement ok +drop table in_one From 49c2a707efd453004d81525501c0b4bef57c20e0 Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 4 Oct 2026 20:59:56 +0800 Subject: [PATCH 09/32] fix: reject DISTINCT/GROUP BY/ORDER BY over SELECT-list scalar subqueries SELECT-list scalar subqueries are joined in at the Project step, above DISTINCT / GROUP BY / ORDER BY, so those clauses read another column instead of the subquery result: a crash when the types differ, a silently wrong result when they match (e.g. `select (select count(*) from t) - id as x from t order by x`). Reject such references until #386 is done. --- src/binder/select.rs | 70 +++++++++++++++++++++++++++++++++++++++++- tests/slt/subquery.slt | 43 ++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 1 deletion(-) diff --git a/src/binder/select.rs b/src/binder/select.rs index 6258c0fb..ca3f3590 100644 --- a/src/binder/select.rs +++ b/src/binder/select.rs @@ -24,7 +24,10 @@ use crate::{ operator::{join::JoinType, table_scan::TableScanOperator}, }, }; -use std::{borrow::Cow, collections::HashSet}; +use std::{ + borrow::Cow, + collections::{HashMap, HashSet}, +}; use super::{Binder, BinderContext, QueryBindStep, SetOperatorKind, Source, SubQueryType}; @@ -68,6 +71,41 @@ impl ExprVisitorMut for RightSidePositionGlobalizer<'_> { } } +/// Whether any of `exprs` reads the output of a SELECT-list scalar subquery. +/// Those subqueries are only joined in at the Project step, above DISTINCT / +/// GROUP BY / ORDER BY, so such a reference would read another column. +/// +/// Takes the subquery map rather than the binder so callers can pass +/// expressions borrowed from the same binder context. +// TODO(#386): evaluate uncorrelated scalar subqueries once, as constants +// (like PostgreSQL's InitPlan), so these clauses can use them. +fn references_select_list_sub_query( + sub_queries: &mut HashMap>, + exprs: impl IntoIterator, + arena: &mut PlanArena, +) -> Result { + let Some(sub_queries) = sub_queries.get_mut(&QueryBindStep::Project) else { + return Ok(false); + }; + // `exprs` can only be traversed once, so it is the outer loop. + for expr in exprs { + for sub_query in sub_queries.iter_mut() { + let SubQueryType::SubQuery { plan, .. } = sub_query else { + continue; + }; + let schema = plan.output_schema(arena); + if expr.any_referenced_column(arena, |arena, candidate| { + schema + .iter() + .any(|column| arena.same_column(*column, *candidate)) + })? { + return Ok(true); + } + } + } + Ok(false) +} + struct AppendedRightOutput { column: ColumnRef, child_position: usize, @@ -531,6 +569,16 @@ where self.arena, )?; } + let context = &mut self.binder.context; + if references_select_list_sub_query( + &mut context.sub_queries, + context.group_by_exprs.iter().copied(), + self.arena, + )? { + return Err(DatabaseError::UnsupportedStmt( + "GROUP BY over a scalar subquery in the SELECT list is not supported".to_string(), + )); + } if !self.binder.context.agg_calls.is_empty() || !self.binder.context.group_by_exprs.is_empty() { @@ -625,6 +673,16 @@ where distinct: bool, ) -> Result, DatabaseError> { if distinct { + if references_select_list_sub_query( + &mut self.binder.context.sub_queries, + self.select_list.iter().copied(), + self.arena, + )? { + return Err(DatabaseError::UnsupportedStmt( + "DISTINCT over a scalar subquery in the SELECT list is not supported" + .to_string(), + )); + } let distinct_outputs = self.select_list.clone(); self.binder.bind_distinct_output_exprs( &distinct_outputs, @@ -657,6 +715,16 @@ where mut self, ) -> Result, DatabaseError> { if let Some(orderby) = self.orderby { + if references_select_list_sub_query( + &mut self.binder.context.sub_queries, + orderby.iter().map(|field| field.expr), + self.arena, + )? { + return Err(DatabaseError::UnsupportedStmt( + "ORDER BY over a scalar subquery in the SELECT list is not supported" + .to_string(), + )); + } self.plan = self.binder.bind_sort(self.plan, orderby, self.arena)?; } diff --git a/tests/slt/subquery.slt b/tests/slt/subquery.slt index 1c2b958c..191ac0a4 100644 --- a/tests/slt/subquery.slt +++ b/tests/slt/subquery.slt @@ -550,3 +550,46 @@ from scalar_eof outer_row order by outer_row.id limit 1; statement ok drop table scalar_eof; + +# SELECT-list scalar subqueries are joined in above DISTINCT / GROUP BY / +# ORDER BY, so those clauses cannot read them yet: reject instead of reading +# another column (which crashed, or silently returned wrong results). +statement ok +create table sq_ref(id int primary key, c varchar); + +statement ok +insert into sq_ref values (0, 'a'), (1, 'b'), (2, 'a'); + +statement error +select id, (select count(*) from sq_ref) - id as x from sq_ref order by x; + +statement error +select (select max(id) from sq_ref) + 1 as x from sq_ref order by x; + +statement error +select distinct (select max(id) from sq_ref) from sq_ref; + +statement error +select (select max(id) from sq_ref) as x from sq_ref group by x; + +# Not referenced by those clauses: still supported. +query II +select id, (select max(id) from sq_ref) as x from sq_ref order by id; +---- +0 2 +1 2 +2 2 + +query II +select (select max(id) from sq_ref), count(*) from sq_ref; +---- +2 3 + +query T rowsort +select distinct c from sq_ref where id < (select max(id) from sq_ref); +---- +a +b + +statement ok +drop table sq_ref; From c5c79e673710b6a3a1f7a432b09ba5f21d4455c4 Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 4 Oct 2026 21:30:36 +0800 Subject: [PATCH 10/32] fix: cast equi-join keys of different types to a common type ON / USING / NATURAL key pairs are split out of `l = r` at bind time, so they never got the cast `visit_binary` adds to a filter comparison. The join compares (and hashes) key values directly, so e.g. `on x.bigint_col = y.int_col` silently matched nothing. Cast both sides of each key pair to the common type when it is built. --- src/binder/select.rs | 56 +++++++++++++++++++++++++++++++++-------- tests/slt/join.slt | 59 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 10 deletions(-) diff --git a/src/binder/select.rs b/src/binder/select.rs index ca3f3590..cf5a72f4 100644 --- a/src/binder/select.rs +++ b/src/binder/select.rs @@ -2131,7 +2131,15 @@ impl<'a: 'b, 'b, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b *right_column, left_schema.len() + right_position, )); - on_keys.push((left_expr, right_expr)); + let ty = LogicalType::max_logical_type( + &left_expr.return_type(arena), + &right_expr.return_type(arena), + )? + .into_owned(); + on_keys.push(( + left_expr.type_cast(Cow::Borrowed(&ty), arena)?, + right_expr.type_cast(Cow::Borrowed(&ty), arena)?, + )); } Ok(JoinCondition::On { on: on_keys, @@ -2179,7 +2187,15 @@ impl<'a: 'b, 'b, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b right_column, left_schema.len() + right_position, )?; - on_keys.push((left_expr, right_expr)); + let ty = LogicalType::max_logical_type( + &left_expr.return_type(arena), + &right_expr.return_type(arena), + )? + .into_owned(); + on_keys.push(( + left_expr.type_cast(Cow::Borrowed(&ty), arena)?, + right_expr.type_cast(Cow::Borrowed(&ty), arena)?, + )); } } Ok(JoinCondition::On { @@ -2207,7 +2223,7 @@ impl<'a: 'b, 'b, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b accum_filter: &mut Vec, left_schema: &Schema, right_schema: &Schema, - arena: &crate::planner::PlanArena, + arena: &mut crate::planner::PlanArena, ) -> Result<(), DatabaseError> { let fn_contains = |schema: &Schema, column: ColumnRef| { let summary = arena.column(column).summary(); @@ -2239,14 +2255,33 @@ impl<'a: 'b, 'b, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b ScalarExpression::ColumnRef { column: r, .. }, ) => { // reorder left and right joins keys to pattern: (left, right) - if fn_contains(left_schema, *l) && fn_contains(right_schema, *r) { - accum.push((*left_expr, *right_expr)); + let key = if fn_contains(left_schema, *l) + && fn_contains(right_schema, *r) + { + Some((*left_expr, *right_expr)) } else if fn_contains(left_schema, *r) && fn_contains(right_schema, *l) { - accum.push((*right_expr, *left_expr)); - } else if fn_or_contains(*l) || fn_or_contains(*r) { - accum_filter.push(expr); + Some((*right_expr, *left_expr)) + } else { + if fn_or_contains(*l) || fn_or_contains(*r) { + accum_filter.push(expr); + } + None + }; + // Join keys are compared (and hashed) directly, so cast + // both to one type like `l = r` in a filter; otherwise + // e.g. `bigint = int` never matches. + if let Some((left, right)) = key { + let ty = LogicalType::max_logical_type( + &left.return_type(arena), + &right.return_type(arena), + )? + .into_owned(); + accum.push(( + left.type_cast(Cow::Borrowed(&ty), arena)?, + right.type_cast(Cow::Borrowed(&ty), arena)?, + )); } } (ScalarExpression::ColumnRef { column, .. }, _) @@ -2271,8 +2306,9 @@ impl<'a: 'b, 'b, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b } BinaryOperator::And => { // example: foo = bar AND baz > 1 + let (left_expr, right_expr) = (*left_expr, *right_expr); Self::extract_join_keys( - *left_expr, + left_expr, accum, accum_filter, left_schema, @@ -2280,7 +2316,7 @@ impl<'a: 'b, 'b, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b arena, )?; Self::extract_join_keys( - *right_expr, + right_expr, accum, accum_filter, left_schema, diff --git a/tests/slt/join.slt b/tests/slt/join.slt index a1ef0c12..afd61b74 100644 --- a/tests/slt/join.slt +++ b/tests/slt/join.slt @@ -457,3 +457,62 @@ drop table hj_right; statement ok drop table hj_left; + +# Equi-join keys of different types (bigint = int) must still match. +statement ok +create table join_ty(id int primary key, a int, b bigint); + +statement ok +insert into join_ty values (0, 0, 0), (1, 1, 1); + +query I rowsort +select y.id from join_ty as x inner join join_ty as y on x.b = y.id; +---- +0 +1 + +query I rowsort +select y.id from join_ty as x inner join join_ty as y on y.id = x.b; +---- +0 +1 + +query I rowsort +select y.id from join_ty as x left join join_ty as y on x.b = y.a; +---- +0 +1 + +statement ok +drop table join_ty + +# USING / NATURAL join keys of different types (bigint = int) must still match. +statement ok +create table using_a(id int primary key, k bigint); + +statement ok +create table using_b(pk int primary key, k int); + +statement ok +insert into using_a values (0, 0), (1, 1); + +statement ok +insert into using_b values (10, 0), (11, 1); + +query I rowsort +select using_a.id from using_a inner join using_b using (k); +---- +0 +1 + +query I rowsort +select using_a.id from using_a natural join using_b; +---- +0 +1 + +statement ok +drop table using_a + +statement ok +drop table using_b From 8249f8d8c003d7cdc54de26184359131a745d56e Mon Sep 17 00:00:00 2001 From: kould Date: Sun, 4 Oct 2026 21:30:36 +0800 Subject: [PATCH 11/32] fix: unify CASE branch types and allow unary operators on NULL CASE rejected branches of different types (e.g. `then bigint else int`) with Incomparable; unify them like a comparison does and cast each branch, as PostgreSQL does. Unary operators on the NULL literal (`- null`) were unsupported; they now yield NULL via a new evaluator position appended at the end. --- src/binder/parser.rs | 16 +++++++++++--- src/types/evaluator/unary.rs | 4 ++++ tests/slt/dummy.slt | 43 +++++++++++++++++++++++++++++++++++- 3 files changed, 59 insertions(+), 4 deletions(-) diff --git a/src/binder/parser.rs b/src/binder/parser.rs index 610c33ee..99e0c87d 100644 --- a/src/binder/parser.rs +++ b/src/binder/parser.rs @@ -2289,16 +2289,18 @@ impl<'a, 'parent, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, ' else_result, .. } => { - let fn_check_ty = |ty: &mut LogicalType, result_ty| { + // Branches of different types are unified like `l = r` (e.g. + // `then bigint else int` => bigint), as in PostgreSQL. + let fn_check_ty = |ty: &mut LogicalType, result_ty: LogicalType| { if result_ty != LogicalType::SqlNull { if ty == &LogicalType::SqlNull { *ty = result_ty; } else if ty != &result_ty { - return Err(DatabaseError::Incomparable(ty.clone(), result_ty)); + *ty = LogicalType::max_logical_type(ty, &result_ty)?.into_owned(); } } - Ok(()) + Ok::<(), DatabaseError>(()) }; let mut operand_expr = None; let mut ty = LogicalType::SqlNull; @@ -2330,6 +2332,14 @@ impl<'a, 'parent, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, ' fn_check_ty(&mut ty, else_ty)?; else_expr = Some(temp_expr); } + if ty != LogicalType::SqlNull { + for (_, result) in expr_pairs.iter_mut() { + *result = result.type_cast(Cow::Borrowed(&ty), arena)?; + } + if let Some(expr) = else_expr.as_mut() { + *expr = expr.type_cast(Cow::Borrowed(&ty), arena)?; + } + } Ok(ScalarExpression::CaseWhen { operand_expr, diff --git a/src/types/evaluator/unary.rs b/src/types/evaluator/unary.rs index ac441c3a..79e553ad 100644 --- a/src/types/evaluator/unary.rs +++ b/src/types/evaluator/unary.rs @@ -38,6 +38,8 @@ const UNARY_FLOAT32_PLUS: u16 = 9; const UNARY_FLOAT32_MINUS: u16 = 10; const UNARY_FLOAT64_PLUS: u16 = 11; const UNARY_FLOAT64_MINUS: u16 = 12; +// Any unary operator on the NULL literal yields NULL, as in PostgreSQL. +const UNARY_SQL_NULL: u16 = 13; // Evaluator positions are serialized ABI. Do not reorder or reuse existing // positions; only append new positions at the end of the current layout. @@ -72,6 +74,7 @@ pub fn unary_create( }, LogicalType::Float => numeric_unary_ref(UNARY_FLOAT32_PLUS, UNARY_FLOAT32_MINUS, ty, op), LogicalType::Double => numeric_unary_ref(UNARY_FLOAT64_PLUS, UNARY_FLOAT64_MINUS, ty, op), + LogicalType::SqlNull => Ok(UnaryEvaluatorRef::new(UNARY_SQL_NULL)), _ => Err(DatabaseError::UnsupportedUnaryOperator(ty.clone(), op)), } } @@ -94,6 +97,7 @@ pub(crate) fn eval_unary( UNARY_FLOAT32_MINUS => float32_minus_unary_eval(value), UNARY_FLOAT64_PLUS => float64_plus_unary_eval(value), UNARY_FLOAT64_MINUS => float64_minus_unary_eval(value), + UNARY_SQL_NULL => crate::types::value::DataValue::Null, _ => unreachable!("unknown unary evaluator position {pos}"), }) } diff --git a/tests/slt/dummy.slt b/tests/slt/dummy.slt index 0a28fc47..d77179cb 100644 --- a/tests/slt/dummy.slt +++ b/tests/slt/dummy.slt @@ -198,4 +198,45 @@ SELECT CASE WHEN FALSE THEN 0 WHEN 1=1 THEN 1 WHEN FALSE THEN 9 ELSE 2 END # That's good. statement error -SELECT * \ No newline at end of file +SELECT * + +# CASE branches of different types are unified (e.g. bigint / int => bigint), +# and unary operators on NULL yield NULL, as in PostgreSQL. +statement ok +create table case_ty(id int primary key, a int, b bigint, d double); + +statement ok +insert into case_ty values (1, 1, 2, 1.5); + +query I +select case when id > 0 then b else 1 end from case_ty; +---- +2 + +query R +select case when id > 0 then b else 1.5 end from case_ty; +---- +2.0 + +query I +select (case when id > 0 then b else 1 end) + 1 from case_ty; +---- +3 + +query T +select (case when id > 0 then b else 1 end) = 2 from case_ty; +---- +true + +query I +select - (null) from case_ty; +---- +null + +query T +select 3 = - null from case_ty; +---- +null + +statement ok +drop table case_ty From 5ac445c28dd8efd6409b3b0d29f6231564c8c264 Mon Sep 17 00:00:00 2001 From: kould Date: Mon, 5 Oct 2026 06:14:26 +0800 Subject: [PATCH 12/32] chore: fix clippy warnings and drop dead code Apply the remaining clippy suggestions (needless borrows, while-let loops, byte strings, is_multiple_of, unit struct construction, ...) and remove helpers only kept alive by tests: TableArena::expression, PlanArena::{fill_parameters, alloc_dummy} and runtime_probe_depth. --- src/bin/shell.rs | 7 +----- src/binder/select.rs | 10 ++++---- src/execution/dml/delete.rs | 4 +-- src/execution/dql/external_sort.rs | 2 +- src/execution/dql/join/hash_join.rs | 1 - src/execution/dql/mark_apply.rs | 2 -- src/execution/mod.rs | 4 --- src/execution/spill/codec.rs | 1 - src/expression/evaluator.rs | 2 +- src/expression/mod.rs | 2 +- src/expression/range_detacher.rs | 2 ++ src/optimizer/core/cm_sketch.rs | 4 +-- .../rule/normalization/combine_operators.rs | 5 +--- .../rule/normalization/elimination.rs | 2 +- .../rule/normalization/simplification.rs | 4 +-- src/planner/arena.rs | 25 ------------------- src/serdes/column.rs | 2 +- src/serdes/num.rs | 1 - src/storage/table_codec.rs | 8 +++--- src/types/value.rs | 11 +++----- 20 files changed, 27 insertions(+), 72 deletions(-) diff --git a/src/bin/shell.rs b/src/bin/shell.rs index 1769421a..9742bb48 100644 --- a/src/bin/shell.rs +++ b/src/bin/shell.rs @@ -346,12 +346,7 @@ Transaction commands: println!("type .help for help, .quit to exit"); } - loop { - let Some(line) = input.read_line(prompt(tx.is_some(), !buffer.trim().is_empty()))? - else { - break; - }; - + while let Some(line) = input.read_line(prompt(tx.is_some(), !buffer.trim().is_empty()))? { let trimmed = line.trim(); if buffer.is_empty() && trimmed.starts_with('.') { match handle_command(trimmed, database, &mut tx) { diff --git a/src/binder/select.rs b/src/binder/select.rs index cf5a72f4..5ba50197 100644 --- a/src/binder/select.rs +++ b/src/binder/select.rs @@ -957,14 +957,14 @@ impl<'a: 'b, 'b, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b for predicate in predicates { if rebind_positions { - Self::rebind_split_scope_positions(predicate, &left_schema, &right_schema, arena)?; + Self::rebind_split_scope_positions(predicate, left_schema, right_schema, arena)?; } Self::extract_join_keys( predicate, &mut on_keys, &mut filter, - &left_schema, - &right_schema, + left_schema, + right_schema, arena, )?; } @@ -1471,7 +1471,7 @@ impl<'a: 'b, 'b, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b let left_schema = left.output_schema(arena); let right_schema = right.output_schema(arena); let mut on = - self.bind_join_constraint(join_type, constraint, &left_schema, &right_schema, arena)?; + self.bind_join_constraint(join_type, constraint, left_schema, right_schema, arena)?; Self::localize_join_condition_from_join_scope(&mut on, left_len, arena)?; Ok(LJoinOperator::build( @@ -1701,7 +1701,7 @@ impl<'a: 'b, 'b, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b let right_schema = plan.output_schema(arena); for expr in &mut apply_predicates { RightSidePositionGlobalizer { - right_schema: &right_schema, + right_schema, left_len, } .visit(expr, arena)?; diff --git a/src/execution/dml/delete.rs b/src/execution/dml/delete.rs index ecdff0c9..be80def3 100644 --- a/src/execution/dml/delete.rs +++ b/src/execution/dml/delete.rs @@ -98,12 +98,12 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Delete<'a> { let mut state = arena.local_state(plan_arena); let (values, transaction, table_codec) = state.index_values_transaction_codec_mut(); let index = Index::new(*index_id, values, *index_ty); - transaction.del_index(table_codec, self.table_name, &index, &tuple_id)?; + transaction.del_index(table_codec, self.table_name, &index, tuple_id)?; } let mut state = arena.local_state(plan_arena); let (transaction, table_codec) = state.transaction_codec_mut(); - transaction.remove_tuple(table_codec, self.table_name, &tuple_id)?; + transaction.remove_tuple(table_codec, self.table_name, tuple_id)?; deleted_count += 1; } diff --git a/src/execution/dql/external_sort.rs b/src/execution/dql/external_sort.rs index 2a913426..8a276fba 100644 --- a/src/execution/dql/external_sort.rs +++ b/src/execution/dql/external_sort.rs @@ -437,7 +437,7 @@ mod test { let mut runs = Vec::new(); for position in 0..ROW_COUNT { let sequence = (position * 7919) % ROW_COUNT; - let key = if sequence % 113 == 0 { + let key = if sequence.is_multiple_of(113) { DataValue::Null } else { DataValue::Int32((sequence % 257) as i32 - 128) diff --git a/src/execution/dql/join/hash_join.rs b/src/execution/dql/join/hash_join.rs index 11d6bfcc..9b12f4f1 100644 --- a/src/execution/dql/join/hash_join.rs +++ b/src/execution/dql/join/hash_join.rs @@ -171,7 +171,6 @@ impl HashJoin { Self::own_bump_vec(key), BuildState { tuples: vec![(build_count, tuple)], - ..Default::default() }, ); } diff --git a/src/execution/dql/mark_apply.rs b/src/execution/dql/mark_apply.rs index f4b7c9cc..cd17b9d4 100644 --- a/src/execution/dql/mark_apply.rs +++ b/src/execution/dql/mark_apply.rs @@ -533,7 +533,6 @@ mod tests { ); assert_eq!(arena.nodes.items.as_ptr(), address); assert_eq!(arena.nodes.items.len(), count); - assert_eq!(arena.runtime_probe_depth(), 0); } assert!(!arena.next_tuple(root, &mut plan_arena)?); Ok(()) @@ -616,7 +615,6 @@ mod tests { tuples.push(arena.materialize_tuple()); assert_eq!(arena.nodes.items.len(), count); assert_eq!(arena.nodes.items.as_ptr(), address); - assert_eq!(arena.runtime_probe_depth(), 0); } assert_eq!( diff --git a/src/execution/mod.rs b/src/execution/mod.rs index e7d87195..f08780cd 100644 --- a/src/execution/mod.rs +++ b/src/execution/mod.rs @@ -604,10 +604,6 @@ impl<'a, T: Transaction + 'a> ExecArena<'a, T> { .expect("runtime probe scope initialized") } - pub(crate) fn runtime_probe_depth(&self) -> usize { - self.runtime_probe_stack.len() - } - pub(crate) fn set_recursive_input(&mut self, input: RecursiveInput) { debug_assert!(self.recursive_input.is_none()); self.recursive_input = Some(input); diff --git a/src/execution/spill/codec.rs b/src/execution/spill/codec.rs index 20e618a3..15989af6 100644 --- a/src/execution/spill/codec.rs +++ b/src/execution/spill/codec.rs @@ -16,7 +16,6 @@ use super::SpillCodec; use crate::errors::DatabaseError; use crate::planner::operator::sort::SortField; use crate::planner::MetaArena; -use crate::planner::PlanArena; use crate::types::tuple::Tuple; use crate::types::value::DataValue; use std::io::{Read, Write}; diff --git a/src/expression/evaluator.rs b/src/expression/evaluator.rs index ee4689c0..671fd974 100644 --- a/src/expression/evaluator.rs +++ b/src/expression/evaluator.rs @@ -17,7 +17,6 @@ use crate::planner::PlanArena; use crate::errors::DatabaseError; use crate::expression::function::scala::ScalarFunction; use crate::expression::{AliasType, BinaryOperator, ScalarExpression, TrimWhereField}; -use crate::planner::ExprRef; use crate::planner::MetaArena; use crate::types::evaluator::binary_create; use crate::types::tuple::TupleLike; @@ -402,6 +401,7 @@ fn trim_string(value: &str, trim_what: &str, trim_where: Option) mod tests { use super::*; use crate::planner::test::PlanArenaTestExt; + use crate::planner::ExprRef; fn const_in( arena: &mut PlanArena<'_>, diff --git a/src/expression/mod.rs b/src/expression/mod.rs index 58663675..54363036 100644 --- a/src/expression/mod.rs +++ b/src/expression/mod.rs @@ -758,7 +758,7 @@ impl ExprRef { } } - pub fn unpack_alias_ref<'a, A: MetaArena + ?Sized>(self, arena: &'a A) -> &'a ScalarExpression { + pub fn unpack_alias_ref(self, arena: &A) -> &ScalarExpression { arena.expression(self.unpack_alias(arena)) } diff --git a/src/expression/range_detacher.rs b/src/expression/range_detacher.rs index 6aa3b966..7f0ebbde 100644 --- a/src/expression/range_detacher.rs +++ b/src/expression/range_detacher.rs @@ -568,6 +568,7 @@ impl<'a, M: RangeColumnMatcher, A: MetaArena + ?Sized> RangeDetacher<'a, M, A> { }) } + #[allow(clippy::result_large_err)] fn merge_binary( op: BinaryOperator, left_binary: Range, @@ -1003,6 +1004,7 @@ impl<'a, M: RangeColumnMatcher, A: MetaArena + ?Sized> RangeDetacher<'a, M, A> { } } + #[allow(clippy::result_large_err)] fn and_scope_merge( left_min: Bound, left_max: Bound, diff --git a/src/optimizer/core/cm_sketch.rs b/src/optimizer/core/cm_sketch.rs index 944ac77d..04dd0b71 100644 --- a/src/optimizer/core/cm_sketch.rs +++ b/src/optimizer/core/cm_sketch.rs @@ -293,9 +293,7 @@ impl CountMinSketch { #[allow(dead_code)] pub fn clear(&mut self) { for k_i in 0..self.k_num { - for counter in &mut self.counters[k_i] { - *counter = 0 - } + self.counters[k_i].fill(0); } self.hashers = Self::new_hashers(); } diff --git a/src/optimizer/rule/normalization/combine_operators.rs b/src/optimizer/rule/normalization/combine_operators.rs index f432d891..91d1bf13 100644 --- a/src/optimizer/rule/normalization/combine_operators.rs +++ b/src/optimizer/rule/normalization/combine_operators.rs @@ -126,10 +126,7 @@ impl NormalizationRule for CollapseProject { let mut removed = false; let mut remapped_positions = Vec::with_capacity(parent_op.exprs.len()); - loop { - let Childrens::Only(child) = plan.childrens.as_mut() else { - break; - }; + while let Childrens::Only(child) = plan.childrens.as_mut() { match &child.operator { Operator::Project(child_op) if is_passthrough_project(child_op, arena) diff --git a/src/optimizer/rule/normalization/elimination.rs b/src/optimizer/rule/normalization/elimination.rs index 18d0a6b2..e37e5a2d 100644 --- a/src/optimizer/rule/normalization/elimination.rs +++ b/src/optimizer/rule/normalization/elimination.rs @@ -932,7 +932,7 @@ mod tests { { info.lookup = lookup; } - super::mark_sort_preserving_indexes(&mut plan, &[c2.clone()], &arena)?; + super::mark_sort_preserving_indexes(&mut plan, std::slice::from_ref(&c2), &arena)?; assert!(EliminateRedundantSort.apply(&mut plan, &mut arena)?); } Ok(()) diff --git a/src/optimizer/rule/normalization/simplification.rs b/src/optimizer/rule/normalization/simplification.rs index 6b9276b2..052dc486 100644 --- a/src/optimizer/rule/normalization/simplification.rs +++ b/src/optimizer/rule/normalization/simplification.rs @@ -94,7 +94,7 @@ impl NormalizationRule for SimplifyFilter { } } ConstantCalculator::new(arena).visit(&mut filter_op.predicate, arena)?; - Simplify::default().visit(&mut filter_op.predicate, arena)?; + Simplify.visit(&mut filter_op.predicate, arena)?; filter_op.is_optimized = true; return Ok(true); } @@ -255,7 +255,7 @@ mod test { expression.unpack_val(&arena), Some(DataValue::Boolean(expected)) ); - Simplify::default().visit(&mut expression, &mut arena)?; + Simplify.visit(&mut expression, &mut arena)?; assert_eq!( arena.expression(expression), &ScalarExpression::Constant(DataValue::Boolean(expected)) diff --git a/src/planner/arena.rs b/src/planner/arena.rs index 21b66dbb..a167d5a0 100644 --- a/src/planner/arena.rs +++ b/src/planner/arena.rs @@ -338,10 +338,6 @@ impl TableArena { ::index(self, index) } - pub(crate) fn expression(&self, expression: ExprRef) -> &ScalarExpression { - ::expression(self, expression) - } - fn dummy_column(&self, column: ColumnRef) -> Option<&ColumnCatalog> { column .pos() @@ -570,22 +566,6 @@ impl<'a> PlanArena<'a> { parameters } - pub(crate) fn fill_parameters( - &mut self, - params: &[(usize, crate::types::value::DataValue)], - ) -> Result<(), crate::errors::DatabaseError> { - for expression in &mut self.expressions { - if let ScalarExpression::Constant(value) = &mut *(ArenaExprMut { expr: expression }) { - value.bind_parameters(&|id| { - params - .iter() - .find_map(|(candidate, value)| (*candidate == id).then_some(value)) - })?; - } - } - Ok(()) - } - pub(crate) fn table_arena_cell(&self) -> &'a TableArenaCell { self.table_arena } @@ -687,11 +667,6 @@ impl<'a> PlanArena<'a> { ::alloc_index(self, index) } - pub(crate) fn alloc_dummy(&mut self, name: &str) -> ColumnRef { - self.assert_table_arena_unchanged(); - self.table_arena.borrow().alloc_dummy(name) - } - pub(crate) fn temp_table(&mut self) -> TableName { let table_name = format!("_temp_table_{}_", self.temp_table_id); self.temp_table_id += 1; diff --git a/src/serdes/column.rs b/src/serdes/column.rs index 7ecb837b..94d786c9 100644 --- a/src/serdes/column.rs +++ b/src/serdes/column.rs @@ -136,7 +136,7 @@ pub(crate) mod test { use crate::db::test::build_table; use crate::errors::DatabaseError; use crate::expression::ScalarExpression; - use crate::planner::{PlanArena, TableArenaCell}; + use crate::planner::{MetaArena, PlanArena, TableArenaCell}; use crate::serdes::{ReferenceDecodeContext, ReferenceSerialization, ReferenceTables}; use crate::storage::rocksdb::RocksStorage; use crate::storage::rocksdb::RocksTransaction; diff --git a/src/serdes/num.rs b/src/serdes/num.rs index a22db8da..70923cb3 100644 --- a/src/serdes/num.rs +++ b/src/serdes/num.rs @@ -87,7 +87,6 @@ impl ReferenceSerialization for usize { #[cfg(all(test, not(target_arch = "wasm32")))] pub(crate) mod test { use crate::errors::DatabaseError; - use crate::planner::MetaArena; use crate::serdes::{ReferenceSerialization, ReferenceTables}; use crate::storage::rocksdb::RocksTransaction; use std::io::{Cursor, Seek, SeekFrom}; diff --git a/src/storage/table_codec.rs b/src/storage/table_codec.rs index e956a0bf..07d29733 100644 --- a/src/storage/table_codec.rs +++ b/src/storage/table_codec.rs @@ -1803,8 +1803,8 @@ mod tests { .unwrap() }; - let value_0 = Bytes::from([b'A'].as_slice()); - let value_1 = Bytes::from([b'Z'].as_slice()); + let value_0 = Bytes::from(b"A".as_slice()); + let value_1 = Bytes::from(b"Z".as_slice()); set.insert(value_0); set.insert(value_1); @@ -1840,8 +1840,8 @@ mod tests { .unwrap() }; - let value_0 = Bytes::from([b'A'].as_slice()); - let value_1 = Bytes::from([b'Z'].as_slice()); + let value_0 = Bytes::from(b"A".as_slice()); + let value_1 = Bytes::from(b"Z".as_slice()); set.insert(value_0); set.insert(value_1); diff --git a/src/types/value.rs b/src/types/value.rs index e4ed887c..a2bdc311 100644 --- a/src/types/value.rs +++ b/src/types/value.rs @@ -281,13 +281,10 @@ impl<'a> TupleMappingRef<'a> { #[inline] pub fn scan_index(&self, index_pos: usize) -> Option { - self.index_to_scan.get(index_pos).copied().and_then(|slot| { - if slot == usize::MAX { - None - } else { - Some(slot) - } - }) + self.index_to_scan + .get(index_pos) + .copied() + .filter(|&slot| slot != usize::MAX) } } From 9b9386d79f7db9cf2d24c425e37cd3860d4aa98e Mon Sep 17 00:00:00 2001 From: kould Date: Mon, 5 Oct 2026 06:14:26 +0800 Subject: [PATCH 13/32] refactor: drop Pattern children and the test-only plan matcher The optimizer only checks a rule pattern's root predicate. The children predicates were only read by PlanMatcher, which nothing but its own tests used. --- src/optimizer/core/pattern.rs | 18 +-- src/optimizer/heuristic/matcher.rs | 110 ------------------ src/optimizer/heuristic/mod.rs | 1 - .../rule/implementation/ddl/add_column.rs | 3 +- .../rule/implementation/ddl/change_column.rs | 3 +- .../rule/implementation/ddl/create_table.rs | 3 +- .../rule/implementation/ddl/drop_column.rs | 3 +- .../rule/implementation/ddl/drop_table.rs | 3 +- .../rule/implementation/ddl/truncate.rs | 3 +- .../rule/implementation/dml/analyze.rs | 3 +- .../rule/implementation/dml/copy_from_file.rs | 3 +- .../rule/implementation/dml/copy_to_file.rs | 3 +- .../rule/implementation/dml/delete.rs | 3 +- .../rule/implementation/dml/insert.rs | 3 +- .../rule/implementation/dml/update.rs | 3 +- .../rule/implementation/dql/aggregate.rs | 4 +- .../rule/implementation/dql/dummy.rs | 3 +- .../rule/implementation/dql/filter.rs | 3 +- .../rule/implementation/dql/function_scan.rs | 3 +- src/optimizer/rule/implementation/dql/join.rs | 3 +- .../rule/implementation/dql/limit.rs | 3 +- .../rule/implementation/dql/mark_apply.rs | 3 +- .../rule/implementation/dql/projection.rs | 3 +- .../rule/implementation/dql/scalar_apply.rs | 3 +- .../implementation/dql/scalar_subquery.rs | 3 +- src/optimizer/rule/implementation/dql/sort.rs | 3 +- .../rule/implementation/dql/table_scan.rs | 3 +- .../rule/implementation/dql/top_k.rs | 3 +- .../rule/implementation/dql/values.rs | 3 +- .../rule/implementation/dql/window.rs | 3 +- 30 files changed, 28 insertions(+), 183 deletions(-) delete mode 100644 src/optimizer/heuristic/matcher.rs diff --git a/src/optimizer/core/pattern.rs b/src/optimizer/core/pattern.rs index f7127dbc..61cdad9d 100644 --- a/src/optimizer/core/pattern.rs +++ b/src/optimizer/core/pattern.rs @@ -14,24 +14,8 @@ use crate::planner::operator::Operator; -#[allow(dead_code)] -pub enum PatternChildrenPredicate { - /// all childrens nodes match all - Recursive, - /// match many pattern at the time - Predicate(Vec), - /// childrens jump out match - None, -} - -/// The pattern tree to match a plan tree. It defined in `Rule` and used in `PatternMatcher`. +/// The pattern to match a plan node. It defined in `Rule`. pub struct Pattern { /// The root node predicate, not contains the children. pub predicate: fn(&Operator) -> bool, - /// The children's predicate of current node. - pub children: PatternChildrenPredicate, -} - -pub trait PatternMatcher { - fn match_opt_expr(&self) -> bool; } diff --git a/src/optimizer/heuristic/matcher.rs b/src/optimizer/heuristic/matcher.rs deleted file mode 100644 index 196155f9..00000000 --- a/src/optimizer/heuristic/matcher.rs +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright 2024 KipData/KiteSQL -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use crate::optimizer::core::pattern::{Pattern, PatternChildrenPredicate, PatternMatcher}; -use crate::planner::LogicalPlan; - -/// Use pattern to determine which rule can be applied on a [`LogicalPlan`] subtree. -pub struct PlanMatcher<'a> { - pattern: &'a Pattern, - plan: &'a LogicalPlan, -} - -impl<'a> PlanMatcher<'a> { - pub(crate) fn new(pattern: &'a Pattern, plan: &'a LogicalPlan) -> Self { - Self { pattern, plan } - } -} - -impl PatternMatcher for PlanMatcher<'_> { - fn match_opt_expr(&self) -> bool { - if !(self.pattern.predicate)(&self.plan.operator) { - return false; - } - - match &self.pattern.children { - PatternChildrenPredicate::Recursive => { - for child in self.plan.childrens.iter() { - if !(self.pattern.predicate)(&child.operator) { - return false; - } - if !PlanMatcher::new(self.pattern, child).match_opt_expr() { - return false; - } - } - } - PatternChildrenPredicate::Predicate(patterns) => { - for child in self.plan.childrens.iter() { - for pattern in patterns { - if !PlanMatcher::new(pattern, child).match_opt_expr() { - return false; - } - } - } - } - PatternChildrenPredicate::None => {} - } - - true - } -} - -#[cfg(all(test, not(target_arch = "wasm32")))] -mod tests { - use super::*; - use crate::binder::test::build_t1_table; - use crate::errors::DatabaseError; - use crate::optimizer::core::pattern::{Pattern, PatternChildrenPredicate, PatternMatcher}; - use crate::planner::operator::Operator; - use crate::planner::{Childrens, LogicalPlan}; - - #[test] - fn test_predicate() -> Result<(), DatabaseError> { - let table_state = build_t1_table()?; - let plan = table_state.plan("select * from t1")?; - - let project_into_table_scan_pattern = Pattern { - predicate: |p| matches!(p, Operator::Project(_)), - children: PatternChildrenPredicate::Predicate(vec![Pattern { - predicate: |p| matches!(p, Operator::TableScan(_)), - children: PatternChildrenPredicate::None, - }]), - }; - - assert!(PlanMatcher::new(&project_into_table_scan_pattern, &plan).match_opt_expr()); - - Ok(()) - } - - #[test] - fn test_recursive() { - let all_dummy_plan = LogicalPlan::new( - Operator::Dummy, - Childrens::Twins { - left: Box::new(LogicalPlan::new( - Operator::Dummy, - Childrens::Only(Box::new(LogicalPlan::new(Operator::Dummy, Childrens::None))), - )), - right: Box::new(LogicalPlan::new(Operator::Dummy, Childrens::None)), - }, - ); - - let only_dummy_pattern = Pattern { - predicate: |p| matches!(p, Operator::Dummy), - children: PatternChildrenPredicate::Recursive, - }; - - assert!(PlanMatcher::new(&only_dummy_pattern, &all_dummy_plan).match_opt_expr()); - } -} diff --git a/src/optimizer/heuristic/mod.rs b/src/optimizer/heuristic/mod.rs index 491d1353..b25a43b9 100644 --- a/src/optimizer/heuristic/mod.rs +++ b/src/optimizer/heuristic/mod.rs @@ -13,5 +13,4 @@ // limitations under the License. pub(crate) mod batch; -pub(crate) mod matcher; pub mod optimizer; diff --git a/src/optimizer/rule/implementation/ddl/add_column.rs b/src/optimizer/rule/implementation/ddl/add_column.rs index 824e28a4..d2f3bf17 100644 --- a/src/optimizer/rule/implementation/ddl/add_column.rs +++ b/src/optimizer/rule/implementation/ddl/add_column.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::errors::DatabaseError; -use crate::optimizer::core::pattern::{Pattern, PatternChildrenPredicate}; +use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::BestPhysicalOption; use crate::optimizer::core::rule::{ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; @@ -23,7 +23,6 @@ use std::sync::LazyLock; static ADD_COLUMN_PATTERN: LazyLock = LazyLock::new(|| Pattern { predicate: |op| matches!(op, Operator::AddColumn(_)), - children: PatternChildrenPredicate::None, }); #[derive(Clone)] diff --git a/src/optimizer/rule/implementation/ddl/change_column.rs b/src/optimizer/rule/implementation/ddl/change_column.rs index 658e2be6..e9613793 100644 --- a/src/optimizer/rule/implementation/ddl/change_column.rs +++ b/src/optimizer/rule/implementation/ddl/change_column.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::errors::DatabaseError; -use crate::optimizer::core::pattern::{Pattern, PatternChildrenPredicate}; +use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::BestPhysicalOption; use crate::optimizer::core::rule::{ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; @@ -23,7 +23,6 @@ use std::sync::LazyLock; static CHANGE_COLUMN_PATTERN: LazyLock = LazyLock::new(|| Pattern { predicate: |op| matches!(op, Operator::ChangeColumn(_)), - children: PatternChildrenPredicate::None, }); #[derive(Clone)] diff --git a/src/optimizer/rule/implementation/ddl/create_table.rs b/src/optimizer/rule/implementation/ddl/create_table.rs index 5fc8c987..c50fc810 100644 --- a/src/optimizer/rule/implementation/ddl/create_table.rs +++ b/src/optimizer/rule/implementation/ddl/create_table.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::errors::DatabaseError; -use crate::optimizer::core::pattern::{Pattern, PatternChildrenPredicate}; +use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::BestPhysicalOption; use crate::optimizer::core::rule::{ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; @@ -23,7 +23,6 @@ use std::sync::LazyLock; static CREATE_TABLE_PATTERN: LazyLock = LazyLock::new(|| Pattern { predicate: |op| matches!(op, Operator::CreateTable(_)), - children: PatternChildrenPredicate::None, }); #[derive(Clone)] diff --git a/src/optimizer/rule/implementation/ddl/drop_column.rs b/src/optimizer/rule/implementation/ddl/drop_column.rs index 25646f9a..a8b86ab7 100644 --- a/src/optimizer/rule/implementation/ddl/drop_column.rs +++ b/src/optimizer/rule/implementation/ddl/drop_column.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::errors::DatabaseError; -use crate::optimizer::core::pattern::{Pattern, PatternChildrenPredicate}; +use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::BestPhysicalOption; use crate::optimizer::core::rule::{ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; @@ -23,7 +23,6 @@ use std::sync::LazyLock; static DROP_COLUMN_PATTERN: LazyLock = LazyLock::new(|| Pattern { predicate: |op| matches!(op, Operator::DropColumn(_)), - children: PatternChildrenPredicate::None, }); #[derive(Clone)] diff --git a/src/optimizer/rule/implementation/ddl/drop_table.rs b/src/optimizer/rule/implementation/ddl/drop_table.rs index 3da2881e..351663d4 100644 --- a/src/optimizer/rule/implementation/ddl/drop_table.rs +++ b/src/optimizer/rule/implementation/ddl/drop_table.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::errors::DatabaseError; -use crate::optimizer::core::pattern::{Pattern, PatternChildrenPredicate}; +use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::BestPhysicalOption; use crate::optimizer::core::rule::{ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; @@ -23,7 +23,6 @@ use std::sync::LazyLock; static DROP_TABLE_PATTERN: LazyLock = LazyLock::new(|| Pattern { predicate: |op| matches!(op, Operator::DropTable(_)), - children: PatternChildrenPredicate::None, }); #[derive(Clone)] diff --git a/src/optimizer/rule/implementation/ddl/truncate.rs b/src/optimizer/rule/implementation/ddl/truncate.rs index 7ba42d2d..c9cb9248 100644 --- a/src/optimizer/rule/implementation/ddl/truncate.rs +++ b/src/optimizer/rule/implementation/ddl/truncate.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::errors::DatabaseError; -use crate::optimizer::core::pattern::{Pattern, PatternChildrenPredicate}; +use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::BestPhysicalOption; use crate::optimizer::core::rule::{ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; @@ -23,7 +23,6 @@ use std::sync::LazyLock; static TRUNCATE_PATTERN: LazyLock = LazyLock::new(|| Pattern { predicate: |op| matches!(op, Operator::Truncate(_)), - children: PatternChildrenPredicate::None, }); #[derive(Clone)] diff --git a/src/optimizer/rule/implementation/dml/analyze.rs b/src/optimizer/rule/implementation/dml/analyze.rs index 8a361947..0f571861 100644 --- a/src/optimizer/rule/implementation/dml/analyze.rs +++ b/src/optimizer/rule/implementation/dml/analyze.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::errors::DatabaseError; -use crate::optimizer::core::pattern::{Pattern, PatternChildrenPredicate}; +use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::BestPhysicalOption; use crate::optimizer::core::rule::{ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; @@ -23,7 +23,6 @@ use std::sync::LazyLock; static ANALYZE_PATTERN: LazyLock = LazyLock::new(|| Pattern { predicate: |op| matches!(op, Operator::Analyze(_)), - children: PatternChildrenPredicate::None, }); #[derive(Clone)] diff --git a/src/optimizer/rule/implementation/dml/copy_from_file.rs b/src/optimizer/rule/implementation/dml/copy_from_file.rs index 7d9b821d..593fc843 100644 --- a/src/optimizer/rule/implementation/dml/copy_from_file.rs +++ b/src/optimizer/rule/implementation/dml/copy_from_file.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::errors::DatabaseError; -use crate::optimizer::core::pattern::{Pattern, PatternChildrenPredicate}; +use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::BestPhysicalOption; use crate::optimizer::core::rule::{ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; @@ -23,7 +23,6 @@ use std::sync::LazyLock; static COPY_FROM_FILE_PATTERN: LazyLock = LazyLock::new(|| Pattern { predicate: |op| matches!(op, Operator::CopyFromFile(_)), - children: PatternChildrenPredicate::None, }); #[derive(Clone)] diff --git a/src/optimizer/rule/implementation/dml/copy_to_file.rs b/src/optimizer/rule/implementation/dml/copy_to_file.rs index 0ef60ae5..d1f9f234 100644 --- a/src/optimizer/rule/implementation/dml/copy_to_file.rs +++ b/src/optimizer/rule/implementation/dml/copy_to_file.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::errors::DatabaseError; -use crate::optimizer::core::pattern::{Pattern, PatternChildrenPredicate}; +use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::BestPhysicalOption; use crate::optimizer::core::rule::{ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; @@ -23,7 +23,6 @@ use std::sync::LazyLock; static COPY_TO_FILE_PATTERN: LazyLock = LazyLock::new(|| Pattern { predicate: |op| matches!(op, Operator::CopyToFile(_)), - children: PatternChildrenPredicate::None, }); #[derive(Clone)] diff --git a/src/optimizer/rule/implementation/dml/delete.rs b/src/optimizer/rule/implementation/dml/delete.rs index 324b820a..80f3cb18 100644 --- a/src/optimizer/rule/implementation/dml/delete.rs +++ b/src/optimizer/rule/implementation/dml/delete.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::errors::DatabaseError; -use crate::optimizer::core::pattern::{Pattern, PatternChildrenPredicate}; +use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::BestPhysicalOption; use crate::optimizer::core::rule::{ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; @@ -23,7 +23,6 @@ use std::sync::LazyLock; static DELETE_PATTERN: LazyLock = LazyLock::new(|| Pattern { predicate: |op| matches!(op, Operator::Delete(_)), - children: PatternChildrenPredicate::None, }); #[derive(Clone)] diff --git a/src/optimizer/rule/implementation/dml/insert.rs b/src/optimizer/rule/implementation/dml/insert.rs index 4b4fcf21..5f71fa9a 100644 --- a/src/optimizer/rule/implementation/dml/insert.rs +++ b/src/optimizer/rule/implementation/dml/insert.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::errors::DatabaseError; -use crate::optimizer::core::pattern::{Pattern, PatternChildrenPredicate}; +use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::BestPhysicalOption; use crate::optimizer::core::rule::{ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; @@ -23,7 +23,6 @@ use std::sync::LazyLock; static INSERT_PATTERN: LazyLock = LazyLock::new(|| Pattern { predicate: |op| matches!(op, Operator::Insert(_)), - children: PatternChildrenPredicate::None, }); #[derive(Clone)] diff --git a/src/optimizer/rule/implementation/dml/update.rs b/src/optimizer/rule/implementation/dml/update.rs index 30e34607..df120c2b 100644 --- a/src/optimizer/rule/implementation/dml/update.rs +++ b/src/optimizer/rule/implementation/dml/update.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::errors::DatabaseError; -use crate::optimizer::core::pattern::{Pattern, PatternChildrenPredicate}; +use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::BestPhysicalOption; use crate::optimizer::core::rule::{ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; @@ -23,7 +23,6 @@ use std::sync::LazyLock; static UPDATE_PATTERN: LazyLock = LazyLock::new(|| Pattern { predicate: |op| matches!(op, Operator::Update(_)), - children: PatternChildrenPredicate::None, }); #[derive(Clone)] diff --git a/src/optimizer/rule/implementation/dql/aggregate.rs b/src/optimizer/rule/implementation/dql/aggregate.rs index 08dd7663..2466076f 100644 --- a/src/optimizer/rule/implementation/dql/aggregate.rs +++ b/src/optimizer/rule/implementation/dql/aggregate.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::errors::DatabaseError; -use crate::optimizer::core::pattern::{Pattern, PatternChildrenPredicate}; +use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::BestPhysicalOption; use crate::optimizer::core::rule::{ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; @@ -28,7 +28,6 @@ static GROUP_BY_AGGREGATE_PATTERN: LazyLock = LazyLock::new(|| Pattern } false }, - children: PatternChildrenPredicate::None, }); static SIMPLE_AGGREGATE_PATTERN: LazyLock = LazyLock::new(|| Pattern { @@ -38,7 +37,6 @@ static SIMPLE_AGGREGATE_PATTERN: LazyLock = LazyLock::new(|| Pattern { } false }, - children: PatternChildrenPredicate::None, }); #[derive(Clone)] diff --git a/src/optimizer/rule/implementation/dql/dummy.rs b/src/optimizer/rule/implementation/dql/dummy.rs index efb0b169..9fd3cdd7 100644 --- a/src/optimizer/rule/implementation/dql/dummy.rs +++ b/src/optimizer/rule/implementation/dql/dummy.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::errors::DatabaseError; -use crate::optimizer::core::pattern::{Pattern, PatternChildrenPredicate}; +use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::BestPhysicalOption; use crate::optimizer::core::rule::{ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; @@ -23,7 +23,6 @@ use std::sync::LazyLock; static DUMMY_PATTERN: LazyLock = LazyLock::new(|| Pattern { predicate: |op| matches!(op, Operator::Dummy), - children: PatternChildrenPredicate::None, }); #[derive(Clone)] diff --git a/src/optimizer/rule/implementation/dql/filter.rs b/src/optimizer/rule/implementation/dql/filter.rs index 8f5f284c..cd254bc2 100644 --- a/src/optimizer/rule/implementation/dql/filter.rs +++ b/src/optimizer/rule/implementation/dql/filter.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::errors::DatabaseError; -use crate::optimizer::core::pattern::{Pattern, PatternChildrenPredicate}; +use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::BestPhysicalOption; use crate::optimizer::core::rule::{ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; @@ -23,7 +23,6 @@ use std::sync::LazyLock; static FILTER_PATTERN: LazyLock = LazyLock::new(|| Pattern { predicate: |op| matches!(op, Operator::Filter(_)), - children: PatternChildrenPredicate::None, }); #[derive(Clone)] diff --git a/src/optimizer/rule/implementation/dql/function_scan.rs b/src/optimizer/rule/implementation/dql/function_scan.rs index 8e23711d..1b7c7f92 100644 --- a/src/optimizer/rule/implementation/dql/function_scan.rs +++ b/src/optimizer/rule/implementation/dql/function_scan.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::errors::DatabaseError; -use crate::optimizer::core::pattern::{Pattern, PatternChildrenPredicate}; +use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::BestPhysicalOption; use crate::optimizer::core::rule::{ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; @@ -23,7 +23,6 @@ use std::sync::LazyLock; static FUNCTION_SCAN_PATTERN: LazyLock = LazyLock::new(|| Pattern { predicate: |op| matches!(op, Operator::FunctionScan(_)), - children: PatternChildrenPredicate::None, }); #[derive(Clone)] diff --git a/src/optimizer/rule/implementation/dql/join.rs b/src/optimizer/rule/implementation/dql/join.rs index 61310f5d..6e703ae7 100644 --- a/src/optimizer/rule/implementation/dql/join.rs +++ b/src/optimizer/rule/implementation/dql/join.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::errors::DatabaseError; -use crate::optimizer::core::pattern::{Pattern, PatternChildrenPredicate}; +use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::{BestPhysicalOption, ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; use crate::planner::operator::{Operator, PhysicalOption, SortOption}; @@ -21,7 +21,6 @@ use std::sync::LazyLock; static JOIN_PATTERN: LazyLock = LazyLock::new(|| Pattern { predicate: |op| matches!(op, Operator::Join(_)), - children: PatternChildrenPredicate::None, }); #[derive(Clone)] diff --git a/src/optimizer/rule/implementation/dql/limit.rs b/src/optimizer/rule/implementation/dql/limit.rs index ada011c8..10e17b6b 100644 --- a/src/optimizer/rule/implementation/dql/limit.rs +++ b/src/optimizer/rule/implementation/dql/limit.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::errors::DatabaseError; -use crate::optimizer::core::pattern::{Pattern, PatternChildrenPredicate}; +use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::BestPhysicalOption; use crate::optimizer::core::rule::{ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; @@ -23,7 +23,6 @@ use std::sync::LazyLock; static LIMIT_PATTERN: LazyLock = LazyLock::new(|| Pattern { predicate: |op| matches!(op, Operator::Limit(_)), - children: PatternChildrenPredicate::None, }); #[derive(Clone)] diff --git a/src/optimizer/rule/implementation/dql/mark_apply.rs b/src/optimizer/rule/implementation/dql/mark_apply.rs index 014ca9d4..c79d3dc4 100644 --- a/src/optimizer/rule/implementation/dql/mark_apply.rs +++ b/src/optimizer/rule/implementation/dql/mark_apply.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::errors::DatabaseError; -use crate::optimizer::core::pattern::{Pattern, PatternChildrenPredicate}; +use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::BestPhysicalOption; use crate::optimizer::core::rule::{ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; @@ -23,7 +23,6 @@ use std::sync::LazyLock; static MARK_APPLY_PATTERN: LazyLock = LazyLock::new(|| Pattern { predicate: |op| matches!(op, Operator::MarkApply(_)), - children: PatternChildrenPredicate::None, }); #[derive(Clone)] diff --git a/src/optimizer/rule/implementation/dql/projection.rs b/src/optimizer/rule/implementation/dql/projection.rs index 951ad627..02806e9a 100644 --- a/src/optimizer/rule/implementation/dql/projection.rs +++ b/src/optimizer/rule/implementation/dql/projection.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::errors::DatabaseError; -use crate::optimizer::core::pattern::{Pattern, PatternChildrenPredicate}; +use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::BestPhysicalOption; use crate::optimizer::core::rule::{ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; @@ -23,7 +23,6 @@ use std::sync::LazyLock; static PROJECTION_PATTERN: LazyLock = LazyLock::new(|| Pattern { predicate: |op| matches!(op, Operator::Project(_)), - children: PatternChildrenPredicate::None, }); #[derive(Clone)] diff --git a/src/optimizer/rule/implementation/dql/scalar_apply.rs b/src/optimizer/rule/implementation/dql/scalar_apply.rs index a77a2714..55151508 100644 --- a/src/optimizer/rule/implementation/dql/scalar_apply.rs +++ b/src/optimizer/rule/implementation/dql/scalar_apply.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::errors::DatabaseError; -use crate::optimizer::core::pattern::{Pattern, PatternChildrenPredicate}; +use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::BestPhysicalOption; use crate::optimizer::core::rule::{ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; @@ -23,7 +23,6 @@ use std::sync::LazyLock; static SCALAR_APPLY_PATTERN: LazyLock = LazyLock::new(|| Pattern { predicate: |op| matches!(op, Operator::ScalarApply(_)), - children: PatternChildrenPredicate::None, }); #[derive(Clone)] diff --git a/src/optimizer/rule/implementation/dql/scalar_subquery.rs b/src/optimizer/rule/implementation/dql/scalar_subquery.rs index 64e85738..dcaaf88a 100644 --- a/src/optimizer/rule/implementation/dql/scalar_subquery.rs +++ b/src/optimizer/rule/implementation/dql/scalar_subquery.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::errors::DatabaseError; -use crate::optimizer::core::pattern::{Pattern, PatternChildrenPredicate}; +use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::BestPhysicalOption; use crate::optimizer::core::rule::{ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; @@ -23,7 +23,6 @@ use std::sync::LazyLock; static SCALAR_SUBQUERY_PATTERN: LazyLock = LazyLock::new(|| Pattern { predicate: |op| matches!(op, Operator::ScalarSubquery(_)), - children: PatternChildrenPredicate::None, }); #[derive(Clone)] diff --git a/src/optimizer/rule/implementation/dql/sort.rs b/src/optimizer/rule/implementation/dql/sort.rs index 468e1f54..6ea317e2 100644 --- a/src/optimizer/rule/implementation/dql/sort.rs +++ b/src/optimizer/rule/implementation/dql/sort.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::errors::DatabaseError; -use crate::optimizer::core::pattern::{Pattern, PatternChildrenPredicate}; +use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::{BestPhysicalOption, ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, SortOption}; @@ -21,7 +21,6 @@ use std::sync::LazyLock; static SORT_PATTERN: LazyLock = LazyLock::new(|| Pattern { predicate: |op| matches!(op, Operator::Sort(_)), - children: PatternChildrenPredicate::None, }); #[derive(Clone)] diff --git a/src/optimizer/rule/implementation/dql/table_scan.rs b/src/optimizer/rule/implementation/dql/table_scan.rs index bf1cdc3a..e40663af 100644 --- a/src/optimizer/rule/implementation/dql/table_scan.rs +++ b/src/optimizer/rule/implementation/dql/table_scan.rs @@ -14,7 +14,7 @@ use crate::errors::DatabaseError; use crate::expression::range_detacher::Range; -use crate::optimizer::core::pattern::{Pattern, PatternChildrenPredicate}; +use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::{BestPhysicalOption, ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, SortOption}; @@ -23,7 +23,6 @@ use std::sync::LazyLock; static TABLE_SCAN_PATTERN: LazyLock = LazyLock::new(|| Pattern { predicate: |op| matches!(op, Operator::TableScan(_)), - children: PatternChildrenPredicate::None, }); #[derive(Clone)] diff --git a/src/optimizer/rule/implementation/dql/top_k.rs b/src/optimizer/rule/implementation/dql/top_k.rs index 85c5322e..5c024221 100644 --- a/src/optimizer/rule/implementation/dql/top_k.rs +++ b/src/optimizer/rule/implementation/dql/top_k.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::errors::DatabaseError; -use crate::optimizer::core::pattern::{Pattern, PatternChildrenPredicate}; +use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::{BestPhysicalOption, ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; use crate::planner::operator::{Operator, PhysicalOption, PlanImpl, SortOption}; @@ -21,7 +21,6 @@ use std::sync::LazyLock; static TOPK_PATTERN: LazyLock = LazyLock::new(|| Pattern { predicate: |op| matches!(op, Operator::TopK(_)), - children: PatternChildrenPredicate::None, }); #[derive(Clone)] diff --git a/src/optimizer/rule/implementation/dql/values.rs b/src/optimizer/rule/implementation/dql/values.rs index ee4d530a..99ec57f0 100644 --- a/src/optimizer/rule/implementation/dql/values.rs +++ b/src/optimizer/rule/implementation/dql/values.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::errors::DatabaseError; -use crate::optimizer::core::pattern::{Pattern, PatternChildrenPredicate}; +use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::BestPhysicalOption; use crate::optimizer::core::rule::{ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; @@ -23,7 +23,6 @@ use std::sync::LazyLock; static VALUES_PATTERN: LazyLock = LazyLock::new(|| Pattern { predicate: |op| matches!(op, Operator::Values(_)), - children: PatternChildrenPredicate::None, }); #[derive(Clone)] diff --git a/src/optimizer/rule/implementation/dql/window.rs b/src/optimizer/rule/implementation/dql/window.rs index f740b1d7..ab234f8d 100644 --- a/src/optimizer/rule/implementation/dql/window.rs +++ b/src/optimizer/rule/implementation/dql/window.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::errors::DatabaseError; -use crate::optimizer::core::pattern::{Pattern, PatternChildrenPredicate}; +use crate::optimizer::core::pattern::Pattern; use crate::optimizer::core::rule::{BestPhysicalOption, ImplementationRule, MatchPattern}; use crate::optimizer::core::statistics_meta::StatisticMetaLoader; use crate::planner::operator::{Operator, PhysicalOption, PlanImpl}; @@ -21,7 +21,6 @@ use std::sync::LazyLock; static WINDOW_PATTERN: LazyLock = LazyLock::new(|| Pattern { predicate: |op| matches!(op, Operator::Window(_)), - children: PatternChildrenPredicate::None, }); #[derive(Clone)] From ec89ee0bbbea26c7147d441bcc599981fdeb0fdf Mon Sep 17 00:00:00 2001 From: kould Date: Mon, 5 Oct 2026 06:15:18 +0800 Subject: [PATCH 14/32] fix: emit a row for every matched pair in nested loop join emit_tuple reported whether the joined tuple had any values, and a pair was only emitted when it did. With column pruning both sides can project no column at all (e.g. `count(*)` over a join), so every pair was dropped and the join returned no rows. --- src/execution/dql/join/nested_loop_join.rs | 37 +++++++++++----------- tests/slt/join.slt | 32 +++++++++++++++++++ 2 files changed, 51 insertions(+), 18 deletions(-) diff --git a/src/execution/dql/join/nested_loop_join.rs b/src/execution/dql/join/nested_loop_join.rs index fb24a828..987f0814 100644 --- a/src/execution/dql/join/nested_loop_join.rs +++ b/src/execution/dql/join/nested_loop_join.rs @@ -221,26 +221,27 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for NestedLoopJoin<'a> { } } active_left.has_matched = true; - if Self::emit_tuple( + // A matched pair is always one output row, even when + // neither side projects a column (e.g. `count(*)`). + Self::emit_tuple( &active_left.left_tuple, arena.result_tuple_mut(), self.ty, - ) { - if matches!(self.ty, JoinType::Full) { - if let Some(bits) = right_bitmap.as_mut() { - bits.insert(idx); - } else { - active_left.first_matches.push(idx); - } + ); + if matches!(self.ty, JoinType::Full) { + if let Some(bits) = right_bitmap.as_mut() { + bits.insert(idx); + } else { + active_left.first_matches.push(idx); } - - self.state = NestedLoopJoinState::ScanRight { - active_left, - right_bitmap, - }; - arena.resume(); - return Ok(()); } + + self.state = NestedLoopJoinState::ScanRight { + active_left, + right_bitmap, + }; + arena.resume(); + return Ok(()); } if matches!(self.ty, JoinType::Full) { @@ -265,7 +266,8 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for NestedLoopJoin<'a> { right_tuple.pk = None; right_tuple.values.clear(); right_tuple.values.resize(right_schema_len, DataValue::Null); - Self::emit_tuple(&active_left.left_tuple, right_tuple, self.ty) + Self::emit_tuple(&active_left.left_tuple, right_tuple, self.ty); + true } _ => false, }; @@ -337,7 +339,7 @@ impl<'a> NestedLoopJoin<'a> { /// `left_tuple`: retained outer tuple (logical right for RightOuter). /// `right_tuple`: current inner tuple (logical left for RightOuter), rewritten in place. /// `ty`: the type of join - fn emit_tuple(left_tuple: &Tuple, right_tuple: &mut Tuple, ty: JoinType) -> bool { + fn emit_tuple(left_tuple: &Tuple, right_tuple: &mut Tuple, ty: JoinType) { let right_len = right_tuple.values.len(); right_tuple.values.extend(left_tuple.values.iter().cloned()); if matches!(ty, JoinType::RightOuter) { @@ -350,7 +352,6 @@ impl<'a> NestedLoopJoin<'a> { right_tuple.pk = left_tuple.pk.clone(); } } - !right_tuple.values.is_empty() } } diff --git a/tests/slt/join.slt b/tests/slt/join.slt index afd61b74..1643bad2 100644 --- a/tests/slt/join.slt +++ b/tests/slt/join.slt @@ -516,3 +516,35 @@ drop table using_a statement ok drop table using_b + +# A join where neither side projects a column still yields one row per pair. +statement ok +create table nlj_empty(id int primary key, a int); + +statement ok +insert into nlj_empty values (0, 0), (1, 2), (2, null); + +query I +select count(*) from nlj_empty as x cross join nlj_empty as y; +---- +9 + +query I +select count(*) from nlj_empty as x inner join nlj_empty as y on true; +---- +9 + +query I +select count(*) from nlj_empty as x left join nlj_empty as y on false; +---- +3 + +query T rowsort +select 'b' from nlj_empty as x cross join nlj_empty as y where x.id = 0; +---- +b +b +b + +statement ok +drop table nlj_empty From 952ddc8c04ee1fe4ea963a7b3072582a92971442 Mon Sep 17 00:00:00 2001 From: kould Date: Mon, 5 Oct 2026 06:15:19 +0800 Subject: [PATCH 15/32] fix: bind IN / BETWEEN evaluators and compare row values with SQL NULL semantics IN compared values with DataValue's type-strict PartialEq and BETWEEN with partial_cmp, so e.g. `bigint_expr in (0, 2)` never matched and `double_col between 0 and 3` was always NULL; BETWEEN with a NULL bound also ignored three-valued logic. Like Binary, In and Between now carry `=` / `>=`, `<=` evaluators bound by BindEvaluator after unifying the operand types, so they share `=`'s semantics. Row value `=` / `<` treated NULL elements as equal (DataValue's PartialEq, which grouping relies on); they now follow SQL: `(4, NULL) = (4, NULL)` is NULL. Tuple types unify element-wise. crdb/where.slt is restored to the upstream expected result. --- src/binder/parser.rs | 3 + src/expression/eq_col.rs | 11 +++ src/expression/evaluator.rs | 58 ++++++++-------- src/expression/mod.rs | 58 ++++++++++++++++ src/expression/simplify.rs | 2 + src/expression/visitor.rs | 2 + src/expression/visitor_mut.rs | 19 ++++- src/orm/mod.rs | 12 ++++ src/types/evaluator/tuple.rs | 47 +++++++++---- src/types/mod.rs | 17 +++-- tests/slt/crdb/where.slt | 2 - tests/slt/where.slt | 127 ++++++++++++++++++++++++++++++++++ 12 files changed, 310 insertions(+), 48 deletions(-) diff --git a/src/binder/parser.rs b/src/binder/parser.rs index 99e0c87d..3c847fcb 100644 --- a/src/binder/parser.rs +++ b/src/binder/parser.rs @@ -2150,6 +2150,7 @@ impl<'a, 'parent, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, ' .map(|expr| arena.alloc_expression(expr))?; Ok(ScalarExpression::In { negated: *negated, + evaluator: None, expr, args, }) @@ -2193,6 +2194,8 @@ impl<'a, 'parent, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, ' .map(|expr| arena.alloc_expression(expr))?; Ok(ScalarExpression::Between { negated: *negated, + left_evaluator: None, + right_evaluator: None, expr, left_expr, right_expr, diff --git a/src/expression/eq_col.rs b/src/expression/eq_col.rs index af814950..73817db9 100644 --- a/src/expression/eq_col.rs +++ b/src/expression/eq_col.rs @@ -250,6 +250,7 @@ impl ExprVisitor for EqIgnoreColRefPosVisitor<'_, '_> { negated: rhs_negated, expr: rhs_expr, args: rhs_args, + .. } => { lhs_negated == *rhs_negated && Self::equals(lhs_expr, *rhs_expr, self.arena) @@ -274,6 +275,7 @@ impl ExprVisitor for EqIgnoreColRefPosVisitor<'_, '_> { expr: rhs_expr, left_expr: rhs_left, right_expr: rhs_right, + .. } => { lhs_negated == *rhs_negated && Self::equals(lhs_expr, *rhs_expr, self.arena) @@ -727,16 +729,19 @@ mod tests { negated: false, expr: lhs_child, args: vec![lhs_one], + evaluator: None, }, ScalarExpression::In { negated: false, expr: rhs_child, args: vec![rhs_one], + evaluator: None, }, ScalarExpression::In { negated: true, expr: rhs_child, args: vec![rhs_one], + evaluator: None, }, ); assert_case( @@ -746,18 +751,24 @@ mod tests { expr: lhs_child, left_expr: lhs_one, right_expr: lhs_three, + left_evaluator: None, + right_evaluator: None, }, ScalarExpression::Between { negated: false, expr: rhs_child, left_expr: rhs_one, right_expr: rhs_three, + left_evaluator: None, + right_evaluator: None, }, ScalarExpression::Between { negated: true, expr: rhs_child, left_expr: rhs_one, right_expr: rhs_three, + left_evaluator: None, + right_evaluator: None, }, ); assert_case( diff --git a/src/expression/evaluator.rs b/src/expression/evaluator.rs index 671fd974..b0be5fbd 100644 --- a/src/expression/evaluator.rs +++ b/src/expression/evaluator.rs @@ -24,7 +24,6 @@ use crate::types::value::{DataValue, Utf8Type}; use crate::types::{CharLengthUnits, LogicalType}; use std::borrow::Cow; use std::cmp; -use std::cmp::Ordering; macro_rules! eval_to_num { ($num_expr:expr, $arena:expr, $tuple:expr) => { @@ -106,34 +105,31 @@ impl ScalarExpression { expr, args, negated, + evaluator, } => { let value = arena.expression(*expr).eval(arena, tuple)?; if value.is_null() { return Ok(Cow::Owned(DataValue::Null)); } - let mut matched = false; + let eq = evaluator.as_ref().ok_or(DatabaseError::EvaluatorNotFound)?; let mut saw_null = false; for arg in args { let arg_value = arena.expression(*arg).eval(arena, tuple)?; - - if arg_value.is_null() { - saw_null = true; - continue; - } - if arg_value == value { - matched = true; - break; + match eq.binary_eval(&value, &arg_value)? { + DataValue::Boolean(true) => { + return Ok(Cow::Owned(DataValue::Boolean(!negated))) + } + DataValue::Null => saw_null = true, + _ => {} } } - if matched { - Ok(Cow::Owned(DataValue::Boolean(!negated))) - } else if saw_null { - Ok(Cow::Owned(DataValue::Null)) + Ok(Cow::Owned(if saw_null { + DataValue::Null } else { - Ok(Cow::Owned(DataValue::Boolean(*negated))) - } + DataValue::Boolean(*negated) + })) } ScalarExpression::Unary { expr, evaluator, .. @@ -155,23 +151,27 @@ impl ScalarExpression { left_expr, right_expr, negated, + left_evaluator, + right_evaluator, } => { let value = arena.expression(*expr).eval(arena, tuple)?; let left = arena.expression(*left_expr).eval(arena, tuple)?; let right = arena.expression(*right_expr).eval(arena, tuple)?; - let mut is_between = match ( - value.partial_cmp(&left).map(Ordering::is_ge), - value.partial_cmp(&right).map(Ordering::is_le), - ) { - (Some(true), Some(true)) => true, - (None, _) | (_, None) => return Ok(Cow::Owned(DataValue::Null)), - _ => false, + let ge = left_evaluator + .as_ref() + .ok_or(DatabaseError::EvaluatorNotFound)? + .binary_eval(&value, &left)?; + let le = right_evaluator + .as_ref() + .ok_or(DatabaseError::EvaluatorNotFound)? + .binary_eval(&value, &right)?; + let is_between = match (ge, le) { + (DataValue::Boolean(false), _) | (_, DataValue::Boolean(false)) => false, + (DataValue::Boolean(true), DataValue::Boolean(true)) => true, + _ => return Ok(Cow::Owned(DataValue::Null)), }; - if *negated { - is_between = !is_between; - } - Ok(Cow::Owned(DataValue::Boolean(is_between))) + Ok(Cow::Owned(DataValue::Boolean(is_between != *negated))) } ScalarExpression::SubString { expr, @@ -415,6 +415,10 @@ mod tests { negated, expr, args, + // The tests only use Int32 operands (plus NULL). + evaluator: Some( + binary_create(Cow::Owned(LogicalType::Integer), BinaryOperator::Eq).unwrap(), + ), }) } diff --git a/src/expression/mod.rs b/src/expression/mod.rs index 54363036..f65f8ad5 100644 --- a/src/expression/mod.rs +++ b/src/expression/mod.rs @@ -106,12 +106,15 @@ pub enum ScalarExpression { negated: bool, expr: ExprRef, args: Vec, + evaluator: Option, }, Between { negated: bool, expr: ExprRef, left_expr: ExprRef, right_expr: ExprRef, + left_evaluator: Option, + right_evaluator: Option, }, SubString { expr: ExprRef, @@ -344,6 +347,56 @@ impl ExprVisitorMut for BindEvaluator { Ok(()) } + + fn visit_in( + &mut self, + _negated: bool, + expr: &mut ExprRef, + args: &mut [ExprRef], + evaluator: &mut Option, + arena: &mut (dyn MetaArena + '_), + ) -> Result<(), DatabaseError> { + self.visit(expr, arena)?; + let mut ty = expr.return_type(arena).into_owned(); + for arg in args.iter_mut() { + self.visit(arg, arena)?; + ty = LogicalType::max_logical_type(&ty, &arg.return_type(arena))?.into_owned(); + } + *expr = expr.type_cast(Cow::Borrowed(&ty), arena)?; + for arg in args.iter_mut() { + *arg = arg.type_cast(Cow::Borrowed(&ty), arena)?; + } + *evaluator = Some(binary_create(Cow::Owned(ty), BinaryOperator::Eq)?); + + Ok(()) + } + + fn visit_between( + &mut self, + _negated: bool, + expr: &mut ExprRef, + left_expr: &mut ExprRef, + right_expr: &mut ExprRef, + left_evaluator: &mut Option, + right_evaluator: &mut Option, + arena: &mut (dyn MetaArena + '_), + ) -> Result<(), DatabaseError> { + self.visit(expr, arena)?; + self.visit(left_expr, arena)?; + self.visit(right_expr, arena)?; + + let ty = + LogicalType::max_logical_type(&expr.return_type(arena), &left_expr.return_type(arena))? + .into_owned(); + let ty = LogicalType::max_logical_type(&ty, &right_expr.return_type(arena))?.into_owned(); + for operand in [&mut *expr, &mut *left_expr, &mut *right_expr] { + *operand = operand.type_cast(Cow::Borrowed(&ty), arena)?; + } + *left_evaluator = Some(binary_create(Cow::Borrowed(&ty), BinaryOperator::GtEq)?); + *right_evaluator = Some(binary_create(Cow::Owned(ty), BinaryOperator::LtEq)?); + + Ok(()) + } } #[derive(Default)] @@ -572,6 +625,7 @@ impl Explain for ExprRef { args, negated, expr, + .. } => { write!( f, @@ -587,6 +641,7 @@ impl Explain for ExprRef { left_expr, right_expr, negated, + .. } => write!( f, "{} {} [{}, {}]", @@ -1263,6 +1318,7 @@ mod test { negated: true, expr: empty, args: vec![empty], + evaluator: None, }, Some(&context), &mut reference_tables, @@ -1275,6 +1331,8 @@ mod test { expr: empty, left_expr: empty, right_expr: empty, + left_evaluator: None, + right_evaluator: None, }, Some(&context), &mut reference_tables, diff --git a/src/expression/simplify.rs b/src/expression/simplify.rs index 84b3254a..ee395f7b 100644 --- a/src/expression/simplify.rs +++ b/src/expression/simplify.rs @@ -193,6 +193,7 @@ impl ExprVisitorMut for Simplify { negated, expr: arg_expr, args, + .. } => { if args.is_empty() { return Ok(false); @@ -238,6 +239,7 @@ impl ExprVisitorMut for Simplify { expr: arg_expr, left_expr, right_expr, + .. } => { let (op, left_op, right_op) = if *negated { (BinaryOperator::Or, BinaryOperator::Lt, BinaryOperator::Gt) diff --git a/src/expression/visitor.rs b/src/expression/visitor.rs index a4b653f2..3f4f586f 100644 --- a/src/expression/visitor.rs +++ b/src/expression/visitor.rs @@ -325,12 +325,14 @@ pub fn walk_expr>( negated, expr, args, + .. } => visitor.visit_in(*negated, *expr, args, arena), ScalarExpression::Between { negated, expr, left_expr, right_expr, + .. } => visitor.visit_between(*negated, *expr, *left_expr, *right_expr, arena), ScalarExpression::SubString { expr, diff --git a/src/expression/visitor_mut.rs b/src/expression/visitor_mut.rs index f9c1ad7c..439d9e4a 100644 --- a/src/expression/visitor_mut.rs +++ b/src/expression/visitor_mut.rs @@ -204,6 +204,7 @@ pub trait ExprVisitorMut: Sized { _negated: bool, expr: &mut ExprRef, args: &mut [ExprRef], + _evaluator: &mut Option, arena: &mut (dyn MetaArena + '_), ) -> Result<(), DatabaseError> { self.visit(expr, arena)?; @@ -213,12 +214,15 @@ pub trait ExprVisitorMut: Sized { Ok(()) } + #[allow(clippy::too_many_arguments)] fn visit_between( &mut self, _negated: bool, expr: &mut ExprRef, left_expr: &mut ExprRef, right_expr: &mut ExprRef, + _left_evaluator: &mut Option, + _right_evaluator: &mut Option, arena: &mut (dyn MetaArena + '_), ) -> Result<(), DatabaseError> { self.visit(expr, arena)?; @@ -424,13 +428,24 @@ pub fn walk_mut_expr( negated, expr, args, - } => visitor.visit_in(*negated, expr, args, arena), + evaluator, + } => visitor.visit_in(*negated, expr, args, evaluator, arena), ScalarExpression::Between { negated, expr, left_expr, right_expr, - } => visitor.visit_between(*negated, expr, left_expr, right_expr, arena), + left_evaluator, + right_evaluator, + } => visitor.visit_between( + *negated, + expr, + left_expr, + right_expr, + left_evaluator, + right_evaluator, + arena, + ), ScalarExpression::SubString { expr, for_expr, diff --git a/src/orm/mod.rs b/src/orm/mod.rs index f65f6b4f..da34d1e0 100644 --- a/src/orm/mod.rs +++ b/src/orm/mod.rs @@ -835,6 +835,7 @@ where let scope = self.scope; let expr = ScalarExpression::In { negated: false, + evaluator: None, expr: self.expr, args: values .into_iter() @@ -852,6 +853,7 @@ where let scope = self.scope; let expr = ScalarExpression::In { negated: true, + evaluator: None, expr: self.expr, args: values .into_iter() @@ -869,6 +871,8 @@ where let scope = self.scope; let expr = ScalarExpression::Between { negated: false, + left_evaluator: None, + right_evaluator: None, expr: self.expr, left_expr: scope.bind(low.into_orm_expression()), right_expr: scope.bind(high.into_orm_expression()), @@ -884,6 +888,8 @@ where let scope = self.scope; let expr = ScalarExpression::Between { negated: true, + left_evaluator: None, + right_evaluator: None, expr: self.expr, left_expr: scope.bind(low.into_orm_expression()), right_expr: scope.bind(high.into_orm_expression()), @@ -1535,6 +1541,7 @@ where let scope = self.handle(); let expr = ScalarExpression::In { negated: false, + evaluator: None, expr: scope.bind(expr.into_orm_expression()), args: args .into_iter() @@ -1556,6 +1563,7 @@ where let scope = self.handle(); let expr = ScalarExpression::In { negated: true, + evaluator: None, expr: scope.bind(expr.into_orm_expression()), args: args .into_iter() @@ -1574,6 +1582,8 @@ where let scope = self.handle(); let expr = ScalarExpression::Between { negated: false, + left_evaluator: None, + right_evaluator: None, expr: scope.bind(expr.into_orm_expression()), left_expr: scope.bind(low.into_orm_expression()), right_expr: scope.bind(high.into_orm_expression()), @@ -1590,6 +1600,8 @@ where let scope = self.handle(); let expr = ScalarExpression::Between { negated: true, + left_evaluator: None, + right_evaluator: None, expr: scope.bind(expr.into_orm_expression()), left_expr: scope.bind(low.into_orm_expression()), right_expr: scope.bind(high.into_orm_expression()), diff --git a/src/types/evaluator/tuple.rs b/src/types/evaluator/tuple.rs index 16964ef9..33dc5671 100644 --- a/src/types/evaluator/tuple.rs +++ b/src/types/evaluator/tuple.rs @@ -18,27 +18,46 @@ use crate::types::evaluator::DataValue; use std::cmp::Ordering; use std::hint; -fn tuple_cmp(v1: &[DataValue], v2: &[DataValue]) -> Option { - let mut order = Ordering::Equal; - let mut v1_iter = v1.iter(); - let mut v2_iter = v2.iter(); +fn element_cmp(v1: &DataValue, v2: &DataValue) -> Option { + match (v1, v2) { + (DataValue::Null, _) | (_, DataValue::Null) => None, + (DataValue::Tuple(v1), DataValue::Tuple(v2)) => tuple_cmp(v1, v2), + (v1, v2) => v1.partial_cmp(v2), + } +} - while order == Ordering::Equal { - order = match (v1_iter.next(), v2_iter.next()) { - (Some(v1), Some(v2)) => v1.partial_cmp(v2)?, - (Some(_), None) => Ordering::Greater, - (None, Some(_)) => Ordering::Less, - (None, None) => break, +fn tuple_eq(v1: &[DataValue], v2: &[DataValue]) -> Option { + if v1.len() != v2.len() { + return Some(false); + } + let mut unknown = false; + for (v1, v2) in v1.iter().zip(v2) { + match element_cmp(v1, v2) { + Some(Ordering::Equal) => {} + Some(_) => return Some(false), + None => unknown = true, } } - Some(order) + (!unknown).then_some(true) +} + +fn tuple_cmp(v1: &[DataValue], v2: &[DataValue]) -> Option { + for (v1, v2) in v1.iter().zip(v2) { + match element_cmp(v1, v2)? { + Ordering::Equal => {} + order => return Some(order), + } + } + Some(v1.len().cmp(&v2.len())) } pub fn tuple_eq_binary_eval( left: &DataValue, right: &DataValue, ) -> Result { Ok(match (left, right) { - (DataValue::Tuple(v1), DataValue::Tuple(v2)) => DataValue::Boolean(*v1 == *v2), + (DataValue::Tuple(v1), DataValue::Tuple(v2)) => { + tuple_eq(v1, v2).map_or(DataValue::Null, DataValue::Boolean) + } (DataValue::Null, DataValue::Boolean(_)) | (DataValue::Boolean(_), DataValue::Null) | (DataValue::Null, DataValue::Null) => DataValue::Null, @@ -50,7 +69,9 @@ pub fn tuple_not_eq_binary_eval( right: &DataValue, ) -> Result { Ok(match (left, right) { - (DataValue::Tuple(v1), DataValue::Tuple(v2)) => DataValue::Boolean(*v1 != *v2), + (DataValue::Tuple(v1), DataValue::Tuple(v2)) => { + tuple_eq(v1, v2).map_or(DataValue::Null, |eq| DataValue::Boolean(!eq)) + } (DataValue::Null, DataValue::Boolean(_)) | (DataValue::Boolean(_), DataValue::Null) | (DataValue::Null, DataValue::Null) => DataValue::Null, diff --git a/src/types/mod.rs b/src/types/mod.rs index 0dc3f7ae..7c8e4baa 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -219,11 +219,20 @@ impl LogicalType { (LogicalType::SqlNull, _) => return Ok(Cow::Borrowed(right)), (_, LogicalType::SqlNull) => return Ok(Cow::Borrowed(left)), (LogicalType::Tuple(types_0), LogicalType::Tuple(types_1)) => { - if types_0.len() > types_1.len() { - return Ok(Cow::Borrowed(left)); - } else { - return Ok(Cow::Borrowed(right)); + if types_0.len() != types_1.len() { + let longer = if types_0.len() > types_1.len() { + left + } else { + right + }; + return Ok(Cow::Borrowed(longer)); } + let types = types_0 + .iter() + .zip(types_1) + .map(|(ty_0, ty_1)| Ok(Self::max_logical_type(ty_0, ty_1)?.into_owned())) + .collect::>()?; + return Ok(Cow::Owned(LogicalType::Tuple(types))); } _ => {} } diff --git a/tests/slt/crdb/where.slt b/tests/slt/crdb/where.slt index 92079ffd..06f1ce95 100644 --- a/tests/slt/crdb/where.slt +++ b/tests/slt/crdb/where.slt @@ -107,5 +107,3 @@ query SELECT a, b FROM ab WHERE (a, b) IN ((1, 10), (4, NULL), (NULL, 50)) ---- 1 10 -4 null -null 50 diff --git a/tests/slt/where.slt b/tests/slt/where.slt index e0d391dc..d79fefb2 100644 --- a/tests/slt/where.slt +++ b/tests/slt/where.slt @@ -218,3 +218,130 @@ drop table in_share statement ok drop table in_one + +# IN compares the operand and the list in one type, like `=`. +statement ok +create table in_ty(id int primary key, b bigint); + +statement ok +insert into in_ty values (0, 0), (2, 2), (3, 1); + +query I rowsort +select id from in_ty where b in (2, 0); +---- +0 +2 + +query I rowsort +select id from in_ty where (b % 2) in (0, null); +---- +0 +2 + +query T rowsort +select (b % 2) in (0, null) from in_ty; +---- +null +true +true + +query I rowsort +select id from in_ty where ((b % 2) in (0, null)) is null; +---- +3 + +query I rowsort +select id from in_ty where b not in (1); +---- +0 +2 + +statement ok +drop table in_ty + +# BETWEEN compares all three operands in one type, like `<=`. +statement ok +create table between_ty(id int primary key, d double); + +statement ok +insert into between_ty values (0, 1e300), (1, 1.5), (2, null); + +query T rowsort +select d not between 0 and 3 from between_ty; +---- +false +null +true + +query I rowsort +select id from between_ty where (d between 0 and 3) is null; +---- +2 + +query I rowsort +select id from between_ty where d between 1 and 2; +---- +1 + +statement ok +drop table between_ty + +# BETWEEN with a NULL bound follows three-valued AND, as in PostgreSQL. +query TTTTT +select 1 between 3 and null, 1 not between 3 and null, 5 between 3 and null, -1 not between 2 and null, 5 between null and 3; +---- +false true null true false + +statement ok +create table between_null(id int primary key, a int); + +statement ok +insert into between_null values (0, 1), (1, 5), (2, null); + +query I rowsort +select id from between_null where a not between 3 and null; +---- +0 + +query I rowsort +select id from between_null where (a between 3 and null) is null; +---- +1 +2 + +statement ok +drop table between_null + +# Row values compare element-wise with SQL NULL semantics, as in PostgreSQL. +statement ok +create table tuple_in(id int primary key, a int, b bigint); + +statement ok +insert into tuple_in values (0, 1, 10), (1, 4, null), (2, null, 50), (3, null, null); + +query I rowsort +select id from tuple_in where (a, b) in ((1, 10), (4, null), (null, 50)); +---- +0 + +query T rowsort +select (a, b) in ((1, 10), (4, null)) from tuple_in; +---- +null +null +null +true + +query I rowsort +select id from tuple_in where (a, b) = (1, 10); +---- +0 + +query I rowsort +select id from tuple_in where (a, b) <> (1, 10); +---- +1 +2 + +statement ok +drop table tuple_in From 0f9e8550e683ab79c571882326715b51a429dd7c Mon Sep 17 00:00:00 2001 From: kould Date: Mon, 5 Oct 2026 06:15:19 +0800 Subject: [PATCH 16/32] fix: push LIMIT through an outer join once and without its OFFSET The pushed-down Limit copied OFFSET, so the outer side skipped those rows and the Limit above the join skipped them again. The rule also re-applied on every fix-point pass, stacking Limits; JoinOperator now records that a Limit was pushed. --- src/execution/dql/join/hash_join.rs | 5 +++ src/execution/dql/join/nested_loop_join.rs | 9 +++++ .../normalization/compilation_in_advance.rs | 1 + .../rule/normalization/pushdown_limit.rs | 30 +++++++++------ src/planner/operator/join.rs | 3 ++ src/planner/operator/mod.rs | 1 + src/planner/operator/visitor.rs | 1 + tests/slt/limit.slt | 37 ++++++++++++++++++- 8 files changed, 74 insertions(+), 13 deletions(-) diff --git a/src/execution/dql/join/hash_join.rs b/src/execution/dql/join/hash_join.rs index 9b12f4f1..52f05b9e 100644 --- a/src/execution/dql/join/hash_join.rs +++ b/src/execution/dql/join/hash_join.rs @@ -500,6 +500,7 @@ mod test { }, join_type: JoinType::Inner, force_nested_loop: false, + limit_pushed: false, }), Childrens::Twins { left: Box::new(left), @@ -560,6 +561,7 @@ mod test { }, join_type: JoinType::LeftOuter, force_nested_loop: false, + limit_pushed: false, }), Childrens::Twins { left: Box::new(left), @@ -625,6 +627,7 @@ mod test { }, join_type: JoinType::RightOuter, force_nested_loop: false, + limit_pushed: false, }), Childrens::Twins { left: Box::new(left), @@ -733,6 +736,7 @@ mod test { }, join_type: JoinType::RightOuter, force_nested_loop: false, + limit_pushed: false, }), Childrens::Twins { left: Box::new(left), @@ -789,6 +793,7 @@ mod test { }, join_type: JoinType::Full, force_nested_loop: false, + limit_pushed: false, }), Childrens::Twins { left: Box::new(left), diff --git a/src/execution/dql/join/nested_loop_join.rs b/src/execution/dql/join/nested_loop_join.rs index 987f0814..ef5b94e5 100644 --- a/src/execution/dql/join/nested_loop_join.rs +++ b/src/execution/dql/join/nested_loop_join.rs @@ -561,6 +561,7 @@ mod test { on: JoinCondition::None, join_type: JoinType::Cross, force_nested_loop: true, + limit_pushed: false, }), Childrens::Twins { left: Box::new(left), @@ -609,6 +610,7 @@ mod test { }, join_type: JoinType::Inner, force_nested_loop: false, + limit_pushed: false, }), Childrens::Twins { left: Box::new(left), @@ -664,6 +666,7 @@ mod test { }, join_type: JoinType::LeftOuter, force_nested_loop: false, + limit_pushed: false, }), Childrens::Twins { left: Box::new(left), @@ -748,6 +751,7 @@ mod test { }, join_type: JoinType::Cross, force_nested_loop: false, + limit_pushed: false, }), Childrens::Twins { left: Box::new(left), @@ -803,6 +807,7 @@ mod test { }, join_type: JoinType::Cross, force_nested_loop: false, + limit_pushed: false, }), Childrens::Twins { left: Box::new(left), @@ -873,6 +878,7 @@ mod test { }, join_type: JoinType::Cross, force_nested_loop: false, + limit_pushed: false, }), Childrens::Twins { left: Box::new(left), @@ -918,6 +924,7 @@ mod test { }, join_type: JoinType::RightOuter, force_nested_loop: false, + limit_pushed: false, }), Childrens::Twins { left: Box::new(left), @@ -997,6 +1004,7 @@ mod test { }, join_type: JoinType::Full, force_nested_loop: false, + limit_pushed: false, }), Childrens::Twins { left: Box::new(left), @@ -1155,6 +1163,7 @@ mod test { }, join_type: JoinType::RightOuter, force_nested_loop: false, + limit_pushed: false, }), Childrens::Twins { left: Box::new(left), diff --git a/src/optimizer/rule/normalization/compilation_in_advance.rs b/src/optimizer/rule/normalization/compilation_in_advance.rs index e35a8c19..7423f969 100644 --- a/src/optimizer/rule/normalization/compilation_in_advance.rs +++ b/src/optimizer/rule/normalization/compilation_in_advance.rs @@ -210,6 +210,7 @@ mod tests { Operator::Join(JoinOperator { join_type: JoinType::Inner, force_nested_loop: false, + limit_pushed: false, on: JoinCondition::On { on: vec![(join_left, join_right)], filter: Some(join_filter), diff --git a/src/optimizer/rule/normalization/pushdown_limit.rs b/src/optimizer/rule/normalization/pushdown_limit.rs index e61779d4..1bff3ee0 100644 --- a/src/optimizer/rule/normalization/pushdown_limit.rs +++ b/src/optimizer/rule/normalization/pushdown_limit.rs @@ -16,6 +16,7 @@ use crate::errors::DatabaseError; use crate::optimizer::core::rule::NormalizationRule; use crate::optimizer::plan_utils::{only_child_mut, replace_with_only_child, wrap_child_with}; use crate::planner::operator::join::JoinType; +use crate::planner::operator::limit::LimitOperator; use crate::planner::operator::Operator; use crate::planner::LogicalPlan; @@ -73,23 +74,28 @@ impl NormalizationRule for PushLimitThroughJoin { _: &mut crate::planner::PlanArena, ) -> Result { let limit_op = match &plan.operator { - Operator::Limit(op) => op.clone(), + Operator::Limit(LimitOperator { + offset, + limit: Some(limit), + }) => LimitOperator { + offset: None, + limit: Some(limit.saturating_add(offset.unwrap_or(0))), + }, _ => return Ok(false), }; if let Some(child) = only_child_mut(plan) { - if let Operator::Join(join_op) = &child.operator { - let mut applied = false; - match join_op.join_type { - JoinType::LeftOuter => { - applied |= wrap_child_with(child, 0, Operator::Limit(limit_op.clone())); - } - JoinType::RightOuter => { - applied |= wrap_child_with(child, 1, Operator::Limit(limit_op)); - } - _ => {} + if let Operator::Join(join_op) = &mut child.operator { + if join_op.limit_pushed { + return Ok(false); } - return Ok(applied); + let side = match join_op.join_type { + JoinType::LeftOuter => 0, + JoinType::RightOuter => 1, + _ => return Ok(false), + }; + join_op.limit_pushed = true; + return Ok(wrap_child_with(child, side, Operator::Limit(limit_op))); } } diff --git a/src/planner/operator/join.rs b/src/planner/operator/join.rs index b734e900..ea46b9d2 100644 --- a/src/planner/operator/join.rs +++ b/src/planner/operator/join.rs @@ -50,6 +50,7 @@ pub struct JoinOperator { pub on: JoinCondition, pub join_type: JoinType, pub force_nested_loop: bool, + pub limit_pushed: bool, } impl JoinOperator { @@ -65,6 +66,7 @@ impl JoinOperator { on, join_type, force_nested_loop, + limit_pushed: false, }), Childrens::Twins { left: Box::new(left), @@ -143,6 +145,7 @@ mod tests { }, join_type: JoinType::Inner, force_nested_loop: false, + limit_pushed: false, }; assert_eq!(operator.plan_impl(), PlanImpl::HashJoin); diff --git a/src/planner/operator/mod.rs b/src/planner/operator/mod.rs index c038066e..beefe902 100644 --- a/src/planner/operator/mod.rs +++ b/src/planner/operator/mod.rs @@ -889,6 +889,7 @@ mod tests { let join = Operator::Join(JoinOperator { join_type: join::JoinType::Inner, force_nested_loop: false, + limit_pushed: false, on: JoinCondition::On { on: vec![(column_expr(a, 0, &mut arena), column_expr(b, 1, &mut arena))], filter: Some(column_expr(c, 2, &mut arena)), diff --git a/src/planner/operator/visitor.rs b/src/planner/operator/visitor.rs index c8aa0775..b1512982 100644 --- a/src/planner/operator/visitor.rs +++ b/src/planner/operator/visitor.rs @@ -462,6 +462,7 @@ pub(crate) mod tests { }, join_type: JoinType::Inner, force_nested_loop: false, + limit_pushed: false, }), Operator::Project(ProjectOperator { exprs: vec![expr(9)], diff --git a/tests/slt/limit.slt b/tests/slt/limit.slt index 7ab441c0..fb1694bd 100644 --- a/tests/slt/limit.slt +++ b/tests/slt/limit.slt @@ -47,4 +47,39 @@ insert into t values (5, 1, 1) query I select v1 from t limit 0 ----- \ No newline at end of file +---- + +# LIMIT ... OFFSET over an outer join: the pushed-down limit must not apply +# OFFSET a second time. +statement ok +create table limit_join_l(id int primary key); + +statement ok +create table limit_join_r(id int primary key, l int); + +statement ok +insert into limit_join_l values (0), (1), (2), (3); + +statement ok +insert into limit_join_r values (10, 2); + +query I +select count(*) from (select l.id from limit_join_l as l left join limit_join_r as r on l.id = r.l limit 4 offset 1) as s; +---- +3 + +query I +select count(*) from (select l.id from limit_join_l as l left join limit_join_r as r on l.id = r.l limit 2 offset 1) as s; +---- +2 + +query I +select count(*) from (select r.id from limit_join_r as r right join limit_join_l as l on l.id = r.l limit 4 offset 3) as s; +---- +1 + +statement ok +drop table limit_join_l + +statement ok +drop table limit_join_r From 458ce10ecb63bbd5d30b13b321c1a4eedd68ca29 Mon Sep 17 00:00:00 2001 From: kould Date: Mon, 5 Oct 2026 06:15:31 +0800 Subject: [PATCH 17/32] fix: fold a negated number literal into one constant `-9223372036854775808` was bound as unary minus over 9223372036854775808, which does not fit bigint and became a double, so inserting the bigint minimum overflowed. Fold `-` into one literal as PostgreSQL does. crdb/overflow.slt is restored to the upstream bigint sum overflow test. --- src/binder/parser.rs | 11 ++++++++++- tests/slt/crdb/overflow.slt | 18 +++++++----------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/binder/parser.rs b/src/binder/parser.rs index 3c847fcb..cfcf1a51 100644 --- a/src/binder/parser.rs +++ b/src/binder/parser.rs @@ -2083,6 +2083,15 @@ impl<'a, 'parent, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, ' Expr::Function(func) => self.bind_function_sql(func, arena), Expr::Nested(expr) => self.bind_expr(expr, arena), Expr::UnaryOp { expr, op } => { + // Fold `-` into one literal, as PostgreSQL does: parsing + // the number first cannot express a type's minimum, e.g. + // `-9223372036854775808` (9223372036854775808 overflows bigint). + if let (UnaryOperator::Minus, Expr::Value(v)) = (op, expr.as_ref()) { + if let Value::Number(n, long) = &v.value { + let value = DataValue::try_from(&Value::Number(format!("-{n}"), *long))?; + return Ok(ScalarExpression::Constant(value)); + } + } let expr = self .bind_expr(expr, arena) .map(|expr| arena.alloc_expression(expr))?; @@ -3380,7 +3389,7 @@ mod tests { assert!(matches!( tables.plan("select * from t1 limit -1").unwrap_err(), - DatabaseError::InvalidColumn { .. } + DatabaseError::InvalidType )); assert!(matches!( tables.plan("select * from t1 limit 1.5").unwrap_err(), diff --git a/tests/slt/crdb/overflow.slt b/tests/slt/crdb/overflow.slt index 70eed9dd..0b663e0b 100644 --- a/tests/slt/crdb/overflow.slt +++ b/tests/slt/crdb/overflow.slt @@ -2,23 +2,19 @@ statement ok drop table if exists large_numbers statement ok -CREATE TABLE large_numbers (a TINYINT PRIMARY KEY) +CREATE TABLE large_numbers (id INT PRIMARY KEY, a BIGINT) -statement error -INSERT INTO large_numbers VALUES (9223372036854775807),(1) +statement ok +INSERT INTO large_numbers VALUES (0, 9223372036854775807), (1, 1) -query I +query error SELECT sum(a) FROM large_numbers ----- -null statement ok DELETE FROM large_numbers -statement error -INSERT INTO large_numbers VALUES (-9223372036854775808),(-1) +statement ok +INSERT INTO large_numbers VALUES (0, -9223372036854775808), (1, -1) -query I +query error SELECT sum(a) FROM large_numbers ----- -null \ No newline at end of file From 5e55bc8dad830b35a26184f42f3c063e439c089e Mon Sep 17 00:00:00 2001 From: kould Date: Mon, 5 Oct 2026 06:15:31 +0800 Subject: [PATCH 18/32] fix: reject comparing row values of different degrees max_logical_type picked the longer row type, so e.g. `(0, 1, 2) = (0, 1)` was evaluated. PostgreSQL rejects it (unequal number of entries in row expressions); return Incomparable. --- src/types/mod.rs | 32 +++++++++++--------------------- tests/slt/dummy.slt | 8 ++------ 2 files changed, 13 insertions(+), 27 deletions(-) diff --git a/src/types/mod.rs b/src/types/mod.rs index 7c8e4baa..2092af3f 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -219,13 +219,9 @@ impl LogicalType { (LogicalType::SqlNull, _) => return Ok(Cow::Borrowed(right)), (_, LogicalType::SqlNull) => return Ok(Cow::Borrowed(left)), (LogicalType::Tuple(types_0), LogicalType::Tuple(types_1)) => { + // Rows of different degrees cannot be compared. if types_0.len() != types_1.len() { - let longer = if types_0.len() > types_1.len() { - left - } else { - right - }; - return Ok(Cow::Borrowed(longer)); + return Err(DatabaseError::Incomparable(left.clone(), right.clone())); } let types = types_0 .iter() @@ -630,22 +626,16 @@ pub(crate) mod test { LogicalType::max_logical_type(&LogicalType::Boolean, &LogicalType::SqlNull)?.as_ref(), &LogicalType::Boolean ); - assert_eq!( - LogicalType::max_logical_type( - &LogicalType::Tuple(vec![LogicalType::Integer, LogicalType::Bigint]), - &LogicalType::Tuple(vec![LogicalType::Integer]) - )? - .as_ref(), - &LogicalType::Tuple(vec![LogicalType::Integer, LogicalType::Bigint]) - ); - assert_eq!( - LogicalType::max_logical_type( - &LogicalType::Tuple(vec![LogicalType::Integer]), - &LogicalType::Tuple(vec![LogicalType::Integer, LogicalType::Bigint]) - )? - .as_ref(), + assert!(LogicalType::max_logical_type( + &LogicalType::Tuple(vec![LogicalType::Integer, LogicalType::Bigint]), + &LogicalType::Tuple(vec![LogicalType::Integer]) + ) + .is_err()); + assert!(LogicalType::max_logical_type( + &LogicalType::Tuple(vec![LogicalType::Integer]), &LogicalType::Tuple(vec![LogicalType::Integer, LogicalType::Bigint]) - ); + ) + .is_err()); let numeric_cases = vec![ ( diff --git a/tests/slt/dummy.slt b/tests/slt/dummy.slt index d77179cb..4dcb3eba 100644 --- a/tests/slt/dummy.slt +++ b/tests/slt/dummy.slt @@ -26,15 +26,11 @@ SELECT (0, 1) != (0, 1) ---- false -query B +query error SELECT (0, 1, 2) = (0, 1) ----- -false -query B +query error SELECT (0, 1, 2) >= (0, 1) ----- -true query B SELECT 1::CHAR = 1::VARCHAR From d9b50051af7af253ca4e03b1c44310d941072c51 Mon Sep 17 00:00:00 2001 From: kould Date: Mon, 5 Oct 2026 06:15:31 +0800 Subject: [PATCH 19/32] fix: coalesce FULL JOIN USING columns in wildcard expansion `*` hid the right USING column and output the left one, which is NULL for rows only in the right table. Named references already used coalesce(left, right); `*` now does the same. crdb/join.slt expects the query to succeed again instead of `statement error`. --- src/binder/select.rs | 24 ++++++++++++++++++++---- tests/slt/crdb/join.slt | 8 +++++++- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/binder/select.rs b/src/binder/select.rs index 5ba50197..a06435ae 100644 --- a/src/binder/select.rs +++ b/src/binder/select.rs @@ -1450,10 +1450,26 @@ impl<'a: 'b, 'b, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b if !fn_not_on_using(column, arena) { continue; } - exprs.push(arena.alloc_expression(ScalarExpression::column_expr( - *column, - position_offset + position, - ))); + let expr = context + .using + .values() + .find(|using| { + !is_qualified_wildcard + && matches!(using.join_type, JoinType::Full) + && arena.same_column(using.left_column, *column) + }) + .cloned() + .map(|using| { + let alias = AliasType::Name(arena.column(*column).name().to_string()); + let expr = using.visible_expr(arena)?; + let expr = arena.alloc_expression(expr); + Ok::<_, DatabaseError>(ScalarExpression::Alias { expr, alias }) + }) + .transpose()? + .unwrap_or_else(|| { + ScalarExpression::column_expr(*column, position_offset + position) + }); + exprs.push(arena.alloc_expression(expr)); } Ok(()) } diff --git a/tests/slt/crdb/join.slt b/tests/slt/crdb/join.slt index 6f42b2c1..eb48fd96 100644 --- a/tests/slt/crdb/join.slt +++ b/tests/slt/crdb/join.slt @@ -776,8 +776,14 @@ SELECT * FROM xyu LEFT OUTER JOIN xyv USING(x, y) WHERE x > 2 order by xyu.uid # ---- # 2 31 7 3 1 31 -statement error +query IIIIII rowsort SELECT * FROM xyu FULL OUTER JOIN xyv USING(x, y) WHERE x > 2 +---- +2 3 1 31 7 31 +3 3 2 32 null null +4 4 4 44 null null +null 3 3 null 8 33 +null 5 5 null 9 55 query IIIIII SELECT * FROM xyu INNER JOIN xyv ON xyu.x = xyv.x AND xyu.y = xyv.y WHERE xyu.x = 1 AND xyu.y < 10 From 62503a35d95fd13f8b95b2646e600f09058e90a6 Mon Sep 17 00:00:00 2001 From: kould Date: Mon, 5 Oct 2026 06:15:31 +0800 Subject: [PATCH 20/32] test: restore crdb expectations changed on import and_or.slt expected errors for AND/OR short-circuit projections and select.slt made the `wide` columns NOT NULL so an insert failed; upstream expects both to succeed, and KiteSQL does. --- tests/slt/crdb/and_or.slt | 16 ++++++++++++++-- tests/slt/crdb/select.slt | 10 +++++----- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/tests/slt/crdb/and_or.slt b/tests/slt/crdb/and_or.slt index 88d33984..e418691f 100644 --- a/tests/slt/crdb/and_or.slt +++ b/tests/slt/crdb/and_or.slt @@ -7,16 +7,28 @@ CREATE TABLE t (k INT PRIMARY KEY null, a INT null, b INT null) statement ok INSERT INTO t VALUES (1, NULL, NULL), (2, NULL, 1), (3, 1, NULL), (4, 2, 0), (5, 3, 3) -statement error (?s)1006.*divided by zero while evaluating function `divide\(3, 0\)` +query T SELECT a <> 2 AND 3 / b = 1 FROM t ORDER BY k +---- +null +false +null +false +true query I SELECT a FROM t WHERE a <> 2 AND 3 / b = 1 ORDER BY k ---- 3 -statement error (?s)1006.*divided by zero while evaluating function `divide\(3, 0\)` +query T SELECT a = 2 OR 3 / b = 1 FROM t ORDER BY k +---- +null +null +null +true +true query I SELECT a FROM t WHERE a = 2 OR 3 / b = 1 ORDER BY k diff --git a/tests/slt/crdb/select.slt b/tests/slt/crdb/select.slt index 35528a60..417aea65 100644 --- a/tests/slt/crdb/select.slt +++ b/tests/slt/crdb/select.slt @@ -522,12 +522,12 @@ statement ok drop table if exists wide statement ok -CREATE TABLE wide (id TINYINT PRIMARY KEY, a TINYINT NOT NULL, b STRING NOT NULL, c TINYINT NOT NULL, d STRING NOT NULL, e STRING NOT NULL, f TINYINT NOT NULL, g STRING NOT NULL, h STRING NOT NULL, i STRING NOT NULL, j STRING NOT NULL, k TINYINT NOT NULL, l FLOAT NOT NULL, m FLOAT NOT NULL, n TINYINT NOT NULL) +CREATE TABLE wide (id TINYINT PRIMARY KEY, a TINYINT, b STRING, c TINYINT, d STRING, e STRING, f TINYINT, g STRING, h STRING, i STRING, j STRING, k TINYINT, l FLOAT, m FLOAT, n TINYINT) -statement error +statement ok INSERT INTO wide(id, n) VALUES(0, 10) -query T -SELECT count(*) FROM wide +query IITITTITTTTIRRI +SELECT * FROM wide ---- -0 \ No newline at end of file +0 null null null null null null null null null null null null null 10 \ No newline at end of file From 3b6992578c8ca7e018682aff02ef787ea3373de3 Mon Sep 17 00:00:00 2001 From: kould Date: Mon, 5 Oct 2026 06:31:32 +0800 Subject: [PATCH 21/32] fix: coalesce FULL JOIN USING columns for tables with column aliases Wildcard expansion of a table with a column alias list (e.g. `t AS a (x, y)`) took a separate path that still output the left USING column, which is NULL for rows only in the right table. --- src/binder/select.rs | 63 ++++++++++++++++++++++++++++---------------- tests/slt/join.slt | 32 ++++++++++++++++++++++ 2 files changed, 72 insertions(+), 23 deletions(-) diff --git a/src/binder/select.rs b/src/binder/select.rs index a06435ae..5f37eaba 100644 --- a/src/binder/select.rs +++ b/src/binder/select.rs @@ -1435,10 +1435,13 @@ impl<'a: 'b, 'b, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b if !fn_not_on_using(column, arena) { continue; } - exprs.push(arena.alloc_expression(ScalarExpression::column_expr( - *column, + exprs.push(Self::wildcard_column_expr( + context, + arena, + column, position_offset + position, - ))); + is_qualified_wildcard, + )?); pushed_alias_columns = true; } @@ -1450,30 +1453,44 @@ impl<'a: 'b, 'b, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b if !fn_not_on_using(column, arena) { continue; } - let expr = context - .using - .values() - .find(|using| { - !is_qualified_wildcard - && matches!(using.join_type, JoinType::Full) - && arena.same_column(using.left_column, *column) - }) - .cloned() - .map(|using| { - let alias = AliasType::Name(arena.column(*column).name().to_string()); - let expr = using.visible_expr(arena)?; - let expr = arena.alloc_expression(expr); - Ok::<_, DatabaseError>(ScalarExpression::Alias { expr, alias }) - }) - .transpose()? - .unwrap_or_else(|| { - ScalarExpression::column_expr(*column, position_offset + position) - }); - exprs.push(arena.alloc_expression(expr)); + exprs.push(Self::wildcard_column_expr( + context, + arena, + column, + position_offset + position, + is_qualified_wildcard, + )?); } Ok(()) } + fn wildcard_column_expr( + context: &BinderContext<'a, T>, + arena: &mut crate::planner::PlanArena, + column: &ColumnRef, + position: usize, + is_qualified_wildcard: bool, + ) -> Result { + let expr = context + .using + .values() + .find(|using| { + !is_qualified_wildcard + && matches!(using.join_type, JoinType::Full) + && arena.same_column(using.left_column, *column) + }) + .cloned() + .map(|using| { + let alias = AliasType::Name(arena.column(*column).name().to_string()); + let expr = using.visible_expr(arena)?; + let expr = arena.alloc_expression(expr); + Ok::<_, DatabaseError>(ScalarExpression::Alias { expr, alias }) + }) + .transpose()? + .unwrap_or_else(|| ScalarExpression::column_expr(*column, position)); + Ok(arena.alloc_expression(expr)) + } + pub(crate) fn bind_join_plans( &mut self, mut left: LogicalPlan, diff --git a/tests/slt/join.slt b/tests/slt/join.slt index 1643bad2..5de62ada 100644 --- a/tests/slt/join.slt +++ b/tests/slt/join.slt @@ -548,3 +548,35 @@ b statement ok drop table nlj_empty + +statement ok +create table full_using_l(id int primary key, x int, v int) + +statement ok +create table full_using_r(id int primary key, x int, v int) + +statement ok +insert into full_using_l values (0, 1, 10), (1, 2, 20) + +statement ok +insert into full_using_r values (5, 2, 200), (6, 3, 300) + +query IIIII rowsort +select * from full_using_l as l (lid, x, lv) full join full_using_r using(x) +---- +0 10 1 null null +1 20 2 5 200 +null null 3 6 300 + +query IIIII rowsort +select * from full_using_l full join full_using_r using(x) +---- +0 1 10 null null +1 2 20 5 200 +null 3 null 6 300 + +statement ok +drop table full_using_l + +statement ok +drop table full_using_r From 883188ad7d14e26eae36144b21f5e0ba08a8b03e Mon Sep 17 00:00:00 2001 From: kould Date: Mon, 5 Oct 2026 06:43:21 +0800 Subject: [PATCH 22/32] fix: expand aliased table columns in declaration order Wildcard expansion of a table with a column alias list iterated the alias map (a BTreeMap), so columns came out in alias-name order: `select * from t as l (lid, x, lv)` returned (lid, lv, x), and INSERT ... SELECT wrote values into the wrong columns. The loop was otherwise redundant with the schema loop below it, which already yields the alias columns in order; drop it. --- src/binder/select.rs | 35 ----------------------------------- tests/slt/crdb/join.slt | 36 ++++++++++++++++++------------------ tests/slt/join.slt | 6 +++--- tests/slt/projection.slt | 28 ++++++++++++++++++++++++++++ 4 files changed, 49 insertions(+), 56 deletions(-) diff --git a/src/binder/select.rs b/src/binder/select.rs index 5f37eaba..f6ec56f4 100644 --- a/src/binder/select.rs +++ b/src/binder/select.rs @@ -1414,41 +1414,6 @@ impl<'a: 'b, 'b, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b .any(|using_column| using_column.hides_column(column, arena)) }; - let mut pushed_alias_columns = false; - - for alias_column in context - .expr_aliases - .keys() - .filter_map(|(alias_table, alias_column)| { - matches!(alias_table.as_deref(), Some(alias) if alias == table_name.as_ref()) - .then_some(alias_column.as_str()) - }) - { - let Some((position, column)) = source - .schema() - .iter() - .enumerate() - .find(|(_, column)| arena.column(**column).name() == alias_column) - else { - continue; - }; - if !fn_not_on_using(column, arena) { - continue; - } - exprs.push(Self::wildcard_column_expr( - context, - arena, - column, - position_offset + position, - is_qualified_wildcard, - )?); - pushed_alias_columns = true; - } - - if pushed_alias_columns { - return Ok(()); - } - for (position, column) in source.schema().iter().enumerate() { if !fn_not_on_using(column, arena) { continue; diff --git a/tests/slt/crdb/join.slt b/tests/slt/crdb/join.slt index eb48fd96..88c563d8 100644 --- a/tests/slt/crdb/join.slt +++ b/tests/slt/crdb/join.slt @@ -98,12 +98,12 @@ INSERT INTO onecolumn_w(w_id, w) VALUES (0, 42),(1, 43) query II SELECT * FROM onecolumn AS a NATURAL JOIN onecolumn_w as b ---- -0 44 42 0 -0 44 43 1 -1 null 42 0 -1 null 43 1 -2 42 42 0 -2 42 43 1 +0 44 0 42 +0 44 1 43 +1 null 0 42 +1 null 1 43 +2 42 0 42 +2 42 1 43 statement ok drop table if exists othercolumn @@ -816,9 +816,9 @@ null null null null 9 5 5 55 query IIII rowsort SELECT * FROM (SELECT * FROM xyu ORDER BY x, y) AS xyu LEFT OUTER JOIN (SELECT * FROM xyv ORDER BY x, y) AS xyv USING(x, y) WHERE x > 2 ---- -31 2 3 1 31 7 -32 3 3 2 null null -44 4 4 4 null null +2 3 1 31 7 31 +3 3 2 32 null null +4 4 4 44 null null # TODO: x > 2 means x on both sides # query IIII @@ -836,19 +836,19 @@ query IIITTT rowsort SELECT * FROM (SELECT * FROM xyu ORDER BY x, y) AS xyu LEFT OUTER JOIN (SELECT * FROM xyv ORDER BY x, y) AS xyv ON xyu.x = xyv.x AND xyu.y = xyv.y AND xyu.x = 1 AND xyu.y < 10 ---- 0 0 0 0 null null null null -1 1 1 1 1 5 1 1 -31 2 3 1 null null null null -32 3 3 2 null null null null -44 4 4 4 null null null null +1 1 1 1 5 1 1 1 +2 3 1 31 null null null null +3 3 2 32 null null null null +4 4 4 44 null null null null query IIIIII SELECT * FROM xyu RIGHT OUTER JOIN (SELECT * FROM xyv ORDER BY x, y) AS xyv ON xyu.x = xyv.x AND xyu.y = xyv.y AND xyu.x = 1 AND xyu.y < 10 ORDER BY v ---- -1 1 1 1 1 5 1 1 -null null null null 2 6 2 2 -null null null null 31 7 3 1 -null null null null 33 8 3 3 -null null null null 55 9 5 5 +1 1 1 1 5 1 1 1 +null null null null 6 2 2 2 +null null null null 7 3 1 31 +null null null null 8 3 3 33 +null null null null 9 5 5 55 statement ok drop table if exists l diff --git a/tests/slt/join.slt b/tests/slt/join.slt index 5de62ada..11daa01d 100644 --- a/tests/slt/join.slt +++ b/tests/slt/join.slt @@ -564,9 +564,9 @@ insert into full_using_r values (5, 2, 200), (6, 3, 300) query IIIII rowsort select * from full_using_l as l (lid, x, lv) full join full_using_r using(x) ---- -0 10 1 null null -1 20 2 5 200 -null null 3 6 300 +0 1 10 null null +1 2 20 5 200 +null 3 null 6 300 query IIIII rowsort select * from full_using_l full join full_using_r using(x) diff --git a/tests/slt/projection.slt b/tests/slt/projection.slt index 82589a41..1aacd381 100644 --- a/tests/slt/projection.slt +++ b/tests/slt/projection.slt @@ -31,3 +31,31 @@ a statement ok drop table null_skip + +statement ok +create table alias_order(id int primary key, x int, v int) + +statement ok +create table alias_order_dst(a int primary key, b int, c int) + +statement ok +insert into alias_order values (0, 1, 10) + +query III +select * from alias_order as l (lid, x, lv) +---- +0 1 10 + +statement ok +insert into alias_order_dst select * from alias_order as l (lid, x, lv) + +query III +select a, b, c from alias_order_dst +---- +0 1 10 + +statement ok +drop table alias_order + +statement ok +drop table alias_order_dst From 5817cf4caa69036876ec1c80d084238168bd3e4d Mon Sep 17 00:00:00 2001 From: kould Date: Mon, 5 Oct 2026 06:43:21 +0800 Subject: [PATCH 23/32] chore: note NULL argument function lookup TODO and drop a comment --- src/binder/expr.rs | 3 +++ src/types/mod.rs | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/binder/expr.rs b/src/binder/expr.rs index 85e40a86..6a0b1929 100644 --- a/src/binder/expr.rs +++ b/src/binder/expr.rs @@ -595,6 +595,9 @@ impl<'a, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, '_, T, A> name: function_name.into(), arg_types, }; + // TODO: a NULL literal argument has type `SqlNull`, so the exact lookup + // misses e.g. `upper(null)` (FunctionNotFound). PostgreSQL treats it as + // an unknown type that matches any parameter and returns NULL. if let Some(function) = self.context.scala_functions.get(&summary) { return Ok(ScalarExpression::ScalaFunction(ScalarFunction { args, diff --git a/src/types/mod.rs b/src/types/mod.rs index 2092af3f..bcb0e7ea 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -219,7 +219,6 @@ impl LogicalType { (LogicalType::SqlNull, _) => return Ok(Cow::Borrowed(right)), (_, LogicalType::SqlNull) => return Ok(Cow::Borrowed(left)), (LogicalType::Tuple(types_0), LogicalType::Tuple(types_1)) => { - // Rows of different degrees cannot be compared. if types_0.len() != types_1.len() { return Err(DatabaseError::Incomparable(left.clone(), right.clone())); } From fb1bf81297bcea2749358ab1e39ac944af4dff66 Mon Sep 17 00:00:00 2001 From: kould Date: Mon, 5 Oct 2026 07:38:05 +0800 Subject: [PATCH 24/32] feat: support ORDER BY column position `ORDER BY 2` was bound as the constant 2, so it did not sort at all. An integer literal now refers to that SELECT-list item, as in PostgreSQL; position 0 / out of range and non-integer constants are errors. --- src/binder/parser.rs | 53 ++++++++++++++++++++++++++++--------- src/binder/select.rs | 8 +++--- tests/slt/crdb/order_by.slt | 8 +----- tests/slt/crdb/sqlite.slt | 11 ++++---- 4 files changed, 52 insertions(+), 28 deletions(-) diff --git a/src/binder/parser.rs b/src/binder/parser.rs index cfcf1a51..472eb8d0 100644 --- a/src/binder/parser.rs +++ b/src/binder/parser.rs @@ -49,7 +49,7 @@ pub(super) use sqlparser::ast::{ ObjectType, OrderByExpr, OrderByKind, Query, Select, SelectInto, SelectItem, SelectItemQualifiedWildcardKind, SetExpr, SetOperator, SetQuantifier, Spanned, TableAlias, TableConstraint, TableFactor, TableObject, TableWithJoins, TypedString, UnaryOperator, Value, - WindowType, + ValueWithSpan, WindowType, }; #[cfg(feature = "copy")] pub(super) use sqlparser::ast::{CopyOption, CopySource, CopyTarget}; @@ -1416,17 +1416,46 @@ where }) }) .transpose()?; - self.aggregate(group_by, having, orderby, |binder, arena, orderby| { - let OrderByExpr { expr, options, .. } = orderby; - with_query_bind_step!(binder, QueryBindStep::Sort, { - let expr = binder.bind_expr(expr, arena)?; - SortField::new( - arena.alloc_expression(expr), - options.asc.is_none_or(|asc| asc), - options.nulls_first.unwrap_or(false), - ) - }) - }) + self.aggregate( + group_by, + having, + orderby, + |binder, arena, select_list, orderby| { + let OrderByExpr { expr, options, .. } = orderby; + with_query_bind_step!(binder, QueryBindStep::Sort, { + let expr = match expr { + Expr::Value(ValueWithSpan { + value: Value::Number(n, _), + .. + }) => { + let position = n.parse::().map_err(|_| { + DatabaseError::InvalidValue(format!( + "non-integer constant in ORDER BY: {n}" + )) + })?; + let item = position + .checked_sub(1) + .and_then(|i| select_list.get(i)) + .ok_or_else(|| { + DatabaseError::InvalidValue(format!( + "ORDER BY position {n} is not in select list" + )) + })?; + item.clone_expression(arena)? + } + expr => { + let expr = binder.bind_expr(expr, arena)?; + arena.alloc_expression(expr) + } + }; + SortField::new( + expr, + options.asc.is_none_or(|asc| asc), + options.nulls_first.unwrap_or(false), + ) + }) + }, + ) } } diff --git a/src/binder/select.rs b/src/binder/select.rs index f6ec56f4..6baa33fd 100644 --- a/src/binder/select.rs +++ b/src/binder/select.rs @@ -398,7 +398,7 @@ where vec![expr], None, None::>, - |_binder, _arena, order| Ok(order), + |_binder, _arena, _select_list, order| Ok(order), )? .having()? .window()? @@ -421,7 +421,7 @@ where Vec::new(), None, None::>, - |_binder, _arena, order| Ok(order), + |_binder, _arena, _select_list, order| Ok(order), )? .having()? .window()? @@ -544,6 +544,7 @@ where mut bind_sort_field: impl FnMut( &mut Binder<'a, 'b, T, A>, &mut crate::planner::PlanArena<'arena>, + &[ExprRef], O, ) -> Result, ) -> Result, DatabaseError> { @@ -562,10 +563,11 @@ where let mut having_orderby = (None, None); if having.is_some() || orderby.is_some() { + let select_list = &self.select_list; having_orderby = self.binder.extract_having_orderby_aggregate_exprs( having, orderby, - |binder, orderby, arena| bind_sort_field(binder, arena, orderby), + |binder, orderby, arena| bind_sort_field(binder, arena, select_list, orderby), self.arena, )?; } diff --git a/tests/slt/crdb/order_by.slt b/tests/slt/crdb/order_by.slt index a960d3fc..d5d4ff3c 100644 --- a/tests/slt/crdb/order_by.slt +++ b/tests/slt/crdb/order_by.slt @@ -179,14 +179,8 @@ SELECT * FROM t ORDER BY "a" 4 7 null 5 7 null -query III +query error SELECT * FROM t ORDER BY 2.5,a ----- -1 9 true -2 8 false -3 7 null -4 7 null -5 7 null statement error SELECT * FROM t ORDER BY foo diff --git a/tests/slt/crdb/sqlite.slt b/tests/slt/crdb/sqlite.slt index b0b603a4..9c23f12c 100644 --- a/tests/slt/crdb/sqlite.slt +++ b/tests/slt/crdb/sqlite.slt @@ -30,16 +30,15 @@ CREATE TABLE tab64784(pk INTEGER primary key, col0 INTEGER, col1 FLOAT, col2 VAR statement ok INSERT INTO tab64784 VALUES(0,212,202.62,'nshdy',212,208.79,'wsxfc'),(1,213,203.64,'xwfuo',213,209.26,'lyswz'),(2,214,204.82,'jnued',216,210.48,'qczzf'),(3,215,205.40,'jtijf',217,211.96,'dpugl'),(4,216,206.3,'dpdzk',219,212.43,'xfirg'),(5,218,207.43,'qpwyw',220,213.50,'fmgky'),(6,219,208.3,'uooxb',221,215.30,'xpmdy'),(7,220,209.54,'ndtbb',225,218.8,'ivqyw'),(8,221,210.65,'zjpts',226,219.82,'sezsm'),(9,222,211.57,'slaxq',227,220.91,'bdqyb') -# TODO: Order by column index query II SELECT pk, col0 FROM tab64784 WHERE (col0 IN (SELECT col3 FROM tab64784 WHERE col3 IS NULL OR (col1 < 22.54) OR col4 > 85.74) OR ((col4 IS NULL)) AND col3 < 8 OR (col4 > 82.93 AND (col0 <= 61) AND col0 > 94 AND col0 > 15)) ORDER BY 2 DESC ---- -0 212 -1 213 -4 216 -6 219 -7 220 8 221 +7 220 +6 219 +4 216 +1 213 +0 212 statement ok drop table tab64784 From e327315d01e5be31139b4bbf53aed932a79d6e13 Mon Sep 17 00:00:00 2001 From: kould Date: Mon, 5 Oct 2026 07:38:06 +0800 Subject: [PATCH 25/32] fix: reject ambiguous column references A bare column name present in several FROM sources resolved to the first one, and ORDER BY on an alias shared by different SELECT items used the last one. Both now fail with AmbiguousColumn, as in PostgreSQL. --- src/binder/expr.rs | 20 +++++++++++--------- src/binder/parser.rs | 21 +++++++++++++++++++++ src/db.rs | 4 ++-- src/errors.rs | 2 ++ tests/slt/crdb/order_by.slt | 2 +- 5 files changed, 37 insertions(+), 12 deletions(-) diff --git a/src/binder/expr.rs b/src/binder/expr.rs index 6a0b1929..97b75fd4 100644 --- a/src/binder/expr.rs +++ b/src/binder/expr.rs @@ -55,8 +55,9 @@ impl<'a, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, '_, T, A> context: &BinderContext<'a, T>, arena: &PlanArena, column_name: &str, - ) -> Option { + ) -> Result, DatabaseError> { let mut position_offset = 0; + let mut found = None; for bound_source in &context.bind_table { let source = &bound_source.source; @@ -64,7 +65,10 @@ impl<'a, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, '_, T, A> if let Some((position, column)) = Self::find_column_in_schema(source.schema().iter(), arena, column_name) { - return Some(ScalarExpression::column_expr( + if found.is_some() { + return Err(DatabaseError::AmbiguousColumn(column_name.to_string())); + } + found = Some(ScalarExpression::column_expr( column, position_offset + position, )); @@ -73,7 +77,7 @@ impl<'a, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, '_, T, A> position_offset += source.schema_len(); } - None + Ok(found) } pub(crate) fn bind_temp_table( &mut self, @@ -369,12 +373,10 @@ impl<'a, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, '_, T, A> // handle col syntax let mut find_visible_column = |context: &BinderContext<'a, T>| -> Result, DatabaseError> { - Ok(context - .using - .get(column_name) - .map(|using_column| using_column.visible_expr(arena)) - .transpose()? - .or_else(|| Self::find_column_in_scope(context, arena, column_name))) + match context.using.get(column_name) { + Some(using_column) => using_column.visible_expr(arena).map(Some), + None => Self::find_column_in_scope(context, arena, column_name), + } }; let mut got_column = find_visible_column(&self.context)?; if got_column.is_none() { diff --git a/src/binder/parser.rs b/src/binder/parser.rs index 472eb8d0..218341c6 100644 --- a/src/binder/parser.rs +++ b/src/binder/parser.rs @@ -1444,6 +1444,27 @@ where item.clone_expression(arena)? } expr => { + if let Expr::Identifier(ident) = expr { + let name = lower_ident(ident); + let mut aliased = select_list.iter().filter_map(|item| match arena + .expression(*item) + { + ScalarExpression::Alias { + expr, + alias: AliasType::Name(alias), + } if alias.as_str() == name => Some(*expr), + _ => None, + }); + if let Some(first) = aliased.next() { + if aliased + .any(|other| !first.eq_ignore_colref_pos(other, arena)) + { + return Err(DatabaseError::AmbiguousColumn( + name.into_owned(), + )); + } + } + } let expr = binder.bind_expr(expr, arena)?; arena.alloc_expression(expr) } diff --git a/src/db.rs b/src/db.rs index 4e58cf54..cce05bb8 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1794,7 +1794,7 @@ pub(crate) mod test { ); } { - let statement = kite_sql.prepare_sql("explain select *, $1 from (select * from t1 where b > $2) left join (select * from t1 where a > $3) on a > $4", &[(1, LogicalType::Integer), (2, LogicalType::Integer), (3, LogicalType::Integer), (4, LogicalType::Integer)])?; + let statement = kite_sql.prepare_sql("explain select *, $1 from (select * from t1 where b > $2) as l left join (select * from t1 where a > $3) on l.a > $4", &[(1, LogicalType::Integer), (2, LogicalType::Integer), (3, LogicalType::Integer), (4, LogicalType::Integer)])?; let mut iter = kite_sql.execute( &statement, @@ -1809,7 +1809,7 @@ pub(crate) mod test { let plan = row.values[0].utf8().unwrap(); assert_eq!( plan, - "Projection [t1.a, t1.b, t1.a, t1.b, 9] [Project => (Sort Option: Follow)] LeftOuter Join Where (t1.a > 0) [NestLoopJoin => (Sort Option: None)] Projection [t1.a, t1.b] [Project => (Sort Option: Follow)] Filter (t1.b > 0), Is Having: false [Filter => (Sort Option: Follow)] TableScan t1 -> [t1.a, t1.b] [SeqScan => (Sort Option: None)] Projection [t1.a, t1.b] [Project => (Sort Option: Follow)] TableScan t1 -> [t1.a, t1.b] [IndexScan By pk_index => ($3, +inf) => (Sort Option: OrderBy: (t1.a Asc Nulls Last) ignore_prefix_len: 0)]" + "Projection [l.a, l.b, t1.a, t1.b, 9] [Project => (Sort Option: Follow)] LeftOuter Join Where (l.a > 0) [NestLoopJoin => (Sort Option: None)] Projection [(t1.a) as (l.a), (t1.b) as (l.b)] [Project => (Sort Option: Follow)] Filter (t1.b > 0), Is Having: false [Filter => (Sort Option: Follow)] TableScan t1 -> [t1.a, t1.b] [SeqScan => (Sort Option: None)] Projection [t1.a, t1.b] [Project => (Sort Option: Follow)] TableScan t1 -> [t1.a, t1.b] [IndexScan By pk_index => ($3, +inf) => (Sort Option: OrderBy: (t1.a Asc Nulls Last) ignore_prefix_len: 0)]" ); } diff --git a/src/errors.rs b/src/errors.rs index e6ff28ca..e1f4d04b 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -59,6 +59,7 @@ fn format_not_null_message(column: &Option, span: &Option) #[derive(Debug)] pub enum DatabaseError { AggMiss(String), + AmbiguousColumn(String), CacheSizeOverFlow, CastFail { from: LogicalType, @@ -151,6 +152,7 @@ impl fmt::Display for DatabaseError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::AggMiss(value) => write!(f, "agg miss: {value}"), + Self::AmbiguousColumn(value) => write!(f, "column: `{value}` is ambiguous"), Self::CacheSizeOverFlow => f.write_str("cache size overflow"), Self::CastFail { from, to, span } => { write!(f, "cast fail: {from} -> {to}{}", format_sql_error_loc(span)) diff --git a/tests/slt/crdb/order_by.slt b/tests/slt/crdb/order_by.slt index d5d4ff3c..8e6bb245 100644 --- a/tests/slt/crdb/order_by.slt +++ b/tests/slt/crdb/order_by.slt @@ -69,7 +69,7 @@ SELECT a AS foo, b FROM t ORDER BY foo DESC 2 8 1 9 -statement error 1065 +statement error SELECT a AS foo, b AS foo FROM t ORDER BY foo query II From ccdf89898046a691502a851f731263a8912c4dfa Mon Sep 17 00:00:00 2001 From: kould Date: Mon, 5 Oct 2026 07:38:06 +0800 Subject: [PATCH 26/32] fix: reject multiple ORDER BY clauses and SELECT * without FROM `((SELECT ... ORDER BY a)) ORDER BY a` and a bare `SELECT *` were accepted; PostgreSQL rejects both. --- src/binder/parser.rs | 16 ++++++++++++++++ tests/slt/crdb/order_by.slt | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/binder/parser.rs b/src/binder/parser.rs index 218341c6..5145ee9f 100644 --- a/src/binder/parser.rs +++ b/src/binder/parser.rs @@ -2900,6 +2900,11 @@ impl<'a, 'parent, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, ' })); } SelectItem::Wildcard(_) => { + if self.context.bind_table.is_empty() { + return Err(DatabaseError::UnsupportedStmt( + "SELECT * with no tables specified is not valid".to_string(), + )); + } let visible_names = self .context .bind_table @@ -3128,6 +3133,17 @@ impl<'a, 'parent, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, ' } else { None }; + if order_by_exprs.is_some() { + let mut body = query.body.as_ref(); + while let SetExpr::Query(inner) = body { + if inner.order_by.is_some() { + return Err(DatabaseError::UnsupportedStmt( + "multiple ORDER BY clauses not allowed".to_string(), + )); + } + body = inner.body.as_ref(); + } + } let is_plain_select = matches!(query.body.as_ref(), SetExpr::Select(_)); let mut plan = match query.body.as_ref() { SetExpr::Select(select) => self.bind_select(select, order_by_exprs, arena), diff --git a/tests/slt/crdb/order_by.slt b/tests/slt/crdb/order_by.slt index 8e6bb245..08bc3c75 100644 --- a/tests/slt/crdb/order_by.slt +++ b/tests/slt/crdb/order_by.slt @@ -152,7 +152,7 @@ SELECT a FROM t ORDER BY (((a))) # 3 # 2 -statement error 1005 +statement error ((SELECT a FROM t ORDER BY a)) ORDER BY a statement error From cc6a7bf521c955b3e8a1007caeb163db05f4ae8e Mon Sep 17 00:00:00 2001 From: kould Date: Mon, 5 Oct 2026 07:38:07 +0800 Subject: [PATCH 27/32] refactor: drop the COLUMN_REF column from DESCRIBE It printed the column's arena slot, an internal value that depends on what else the process has bound. --- src/execution/dql/describe.rs | 15 +------- src/planner/arena.rs | 3 +- src/planner/mod.rs | 25 ++---------- tests/slt/alter_table.slt | 12 +++--- tests/slt/change_column.slt | 72 +++++++++++++++++------------------ tests/slt/describe.slt | 16 ++++---- 6 files changed, 57 insertions(+), 86 deletions(-) diff --git a/src/execution/dql/describe.rs b/src/execution/dql/describe.rs index 23128b44..98316a26 100644 --- a/src/execution/dql/describe.rs +++ b/src/execution/dql/describe.rs @@ -98,12 +98,11 @@ impl<'a, T: Transaction + 'a> ExecutorNode<'a, T> for Describe { self.cursor += 1; let column = plan_arena.column(column_ref); let default = describe_default(column, plan_arena); - let mapping = column_ref.to_string(); let output = arena.result_tuple_mut(); output.pk = None; output.values.clear(); - fill_describe_row(&mut output.values, column, default, mapping); + fill_describe_row(&mut output.values, column, default); arena.resume(); Ok(()) @@ -119,12 +118,7 @@ fn describe_default(column: &ColumnCatalog, arena: &(dyn MetaArena + '_)) -> Str .unwrap_or_else(|| "null".to_string()) } -fn fill_describe_row( - values: &mut Vec, - column: &ColumnCatalog, - default: String, - mapping: String, -) { +fn fill_describe_row(values: &mut Vec, column: &ColumnCatalog, default: String) { let datatype = column.datatype(); values.push(DataValue::Utf8 { @@ -156,11 +150,6 @@ fn fill_describe_row( ty: Utf8Type::Variable(None), unit: CharLengthUnits::Characters, }); - values.push(DataValue::Utf8 { - value: mapping, - ty: Utf8Type::Variable(None), - unit: CharLengthUnits::Characters, - }); } fn key_value(column: &ColumnCatalog) -> DataValue { diff --git a/src/planner/arena.rs b/src/planner/arena.rs index a167d5a0..4c801b85 100644 --- a/src/planner/arena.rs +++ b/src/planner/arena.rs @@ -245,7 +245,6 @@ const DUMMY_COLUMN_NAMES: [&str; DUMMY_COLUMN_COUNT] = [ "NULL", "Key", "DEFAULT", - "COLUMN_REF", "INSERTED", "UPDATED", "DELETED", @@ -263,7 +262,7 @@ const DUMMY_COLUMN_NAMES: [&str; DUMMY_COLUMN_COUNT] = [ "COPY FROM SOURCE", "COPY TO TARGET", ]; -const DUMMY_COLUMN_COUNT: usize = 26; +const DUMMY_COLUMN_COUNT: usize = 25; const DUMMY_COLUMN_BASE: usize = usize::MAX - DUMMY_COLUMN_COUNT + 1; impl TableArenaCell { diff --git a/src/planner/mod.rs b/src/planner/mod.rs index 155f7bac..d1ed25d2 100644 --- a/src/planner/mod.rs +++ b/src/planner/mod.rs @@ -312,18 +312,9 @@ impl LogicalPlan { Operator::ShowTable => Self::dummy_schema(arena, ["TABLE"]), Operator::ShowView => Self::dummy_schema(arena, ["VIEW"]), Operator::Explain => Self::dummy_schema(arena, ["PLAN"]), - Operator::Describe(_) => Self::dummy_schema( - arena, - [ - "FIELD", - "TYPE", - "LEN", - "NULL", - "Key", - "DEFAULT", - "COLUMN_REF", - ], - ), + Operator::Describe(_) => { + Self::dummy_schema(arena, ["FIELD", "TYPE", "LEN", "NULL", "Key", "DEFAULT"]) + } Operator::Insert(_) => Self::dummy_schema(arena, ["INSERTED"]), Operator::Update(_) => Self::dummy_schema(arena, ["UPDATED"]), Operator::Delete(_) => Self::dummy_schema(arena, ["DELETED"]), @@ -667,15 +658,7 @@ mod tests { Operator::Describe(DescribeOperator { table_name: "users".into(), }), - vec![ - "FIELD", - "TYPE", - "LEN", - "NULL", - "Key", - "DEFAULT", - "COLUMN_REF", - ], + vec!["FIELD", "TYPE", "LEN", "NULL", "Key", "DEFAULT"], ), ]; diff --git a/tests/slt/alter_table.slt b/tests/slt/alter_table.slt index 4bdb0269..0b87fadf 100644 --- a/tests/slt/alter_table.slt +++ b/tests/slt/alter_table.slt @@ -69,11 +69,11 @@ create table t3(id int primary key, v int) statement ok insert into t3 values (1, 10), (2, 20) -query TTTTTTT +query TTTTTT describe t3 ---- -id Integer 4 false PRIMARY null #1 -v Integer 4 true EMPTY null #2 +id Integer 4 false PRIMARY null +v Integer 4 true EMPTY null statement ok insert into t3 values (3, 10) @@ -104,11 +104,11 @@ select * from t3 3 10 4 10 -query TTTTTTT +query TTTTTT describe t3 ---- -id Integer 4 false PRIMARY null #1 -v Integer 4 true EMPTY null #2 +id Integer 4 false PRIMARY null +v Integer 4 true EMPTY null statement error alter table t3 modify column v int first diff --git a/tests/slt/change_column.slt b/tests/slt/change_column.slt index 753f196d..c25c441a 100644 --- a/tests/slt/change_column.slt +++ b/tests/slt/change_column.slt @@ -22,12 +22,12 @@ alter table alter_users alter column id type bigint statement ok alter table alter_users alter column v2 type bigint -query TTTTTTT +query TTTTTT describe alter_users ---- -id Integer 4 false PRIMARY null #1 -value1 Integer 4 true UNIQUE null #4 -v2 Bigint 8 true EMPTY null #2 +id Integer 4 false PRIMARY null +value1 Integer 4 true UNIQUE null +v2 Bigint 8 true EMPTY null query I select v2 from alter_users order by id @@ -38,12 +38,12 @@ select v2 from alter_users order by id statement ok alter table alter_users change column v2 value2 bigint -query TTTTTTT +query TTTTTT describe alter_users ---- -id Integer 4 false PRIMARY null #1 -value1 Integer 4 true UNIQUE null #4 -value2 Bigint 8 true EMPTY null #3 +id Integer 4 false PRIMARY null +value1 Integer 4 true UNIQUE null +value2 Bigint 8 true EMPTY null statement ok alter table alter_users alter column value2 set default 999 @@ -76,12 +76,12 @@ update alter_users set value2 = 404 where id = 4 statement ok alter table alter_users alter column value2 set not null -query TTTTTTT +query TTTTTT describe alter_users ---- -id Integer 4 false PRIMARY null #1 -value1 Integer 4 true UNIQUE null #4 -value2 Bigint 8 false EMPTY null #2 +id Integer 4 false PRIMARY null +value1 Integer 4 true UNIQUE null +value2 Bigint 8 false EMPTY null statement error insert into alter_users (id, value1) values (5, 55) @@ -100,12 +100,12 @@ null statement ok alter table alter_users modify column value2 int -query TTTTTTT +query TTTTTT describe alter_users ---- -id Integer 4 false PRIMARY null #1 -value1 Integer 4 true UNIQUE null #4 -value2 Integer 4 true EMPTY null #2 +id Integer 4 false PRIMARY null +value1 Integer 4 true UNIQUE null +value2 Integer 4 true EMPTY null query I select value2 from alter_users where value2 is not null order by id @@ -121,12 +121,12 @@ update alter_users set value2 = 606 where id = 6 statement ok alter table alter_users change column value2 value2 int not null -query TTTTTTT +query TTTTTT describe alter_users ---- -id Integer 4 false PRIMARY null #1 -value1 Integer 4 true UNIQUE null #4 -value2 Integer 4 false EMPTY null #3 +id Integer 4 false PRIMARY null +value1 Integer 4 true UNIQUE null +value2 Integer 4 false EMPTY null statement error insert into alter_users (id, value1) values (7, 77) @@ -142,12 +142,12 @@ select value2 from alter_users where id = 7 ---- 707 -query TTTTTTT +query TTTTTT describe alter_users ---- -id Integer 4 false PRIMARY null #1 -value1 Integer 4 true UNIQUE null #4 -value2 Integer 4 false EMPTY 707 #2 +id Integer 4 false PRIMARY null +value1 Integer 4 true UNIQUE null +value2 Integer 4 false EMPTY 707 statement ok alter table alter_users modify column value2 int null @@ -160,22 +160,22 @@ select value2 from alter_users where id = 8 ---- 707 -query TTTTTTT +query TTTTTT describe alter_users ---- -id Integer 4 false PRIMARY null #1 -value1 Integer 4 true UNIQUE null #4 -value2 Integer 4 true EMPTY 707 #3 +id Integer 4 false PRIMARY null +value1 Integer 4 true UNIQUE null +value2 Integer 4 true EMPTY 707 statement ok alter table alter_users modify column value2 int not null -query TTTTTTT +query TTTTTT describe alter_users ---- -id Integer 4 false PRIMARY null #1 -value1 Integer 4 true UNIQUE null #4 -value2 Integer 4 false EMPTY 707 #2 +id Integer 4 false PRIMARY null +value1 Integer 4 true UNIQUE null +value2 Integer 4 false EMPTY 707 statement ok alter table alter_users modify column value2 int default 808 @@ -188,12 +188,12 @@ select value2 from alter_users where id = 9 ---- 808 -query TTTTTTT +query TTTTTT describe alter_users ---- -id Integer 4 false PRIMARY null #1 -value1 Integer 4 true UNIQUE null #4 -value2 Integer 4 false EMPTY 808 #3 +id Integer 4 false PRIMARY null +value1 Integer 4 true UNIQUE null +value2 Integer 4 false EMPTY 808 statement ok drop table alter_users diff --git a/tests/slt/describe.slt b/tests/slt/describe.slt index 8fb3ccb9..e327103e 100644 --- a/tests/slt/describe.slt +++ b/tests/slt/describe.slt @@ -1,12 +1,12 @@ statement ok create table t9 (c1 int primary key, c2 int default 0, c3 varchar unique); -query TTTTTTT +query TTTTTT describe t9; ---- -c1 Integer 4 false PRIMARY null #1 -c2 Integer 4 true EMPTY 0 #2 -c3 Varchar(None, CHARACTERS) variable true UNIQUE null #3 +c1 Integer 4 false PRIMARY null +c2 Integer 4 true EMPTY 0 +c3 Varchar(None, CHARACTERS) variable true UNIQUE null statement ok drop table t9; @@ -14,12 +14,12 @@ drop table t9; statement ok create table t9_m (c1 int primary key, c2 int primary key, c3 varchar unique); -query TTTTTTT +query TTTTTT describe t9_m; ---- -c1 Integer 4 false PRIMARY null #1 -c2 Integer 4 false PRIMARY null #2 -c3 Varchar(None, CHARACTERS) variable true UNIQUE null #3 +c1 Integer 4 false PRIMARY null +c2 Integer 4 false PRIMARY null +c3 Varchar(None, CHARACTERS) variable true UNIQUE null statement ok drop table t9_m; From 4198c96e18dadfbcd2c6b6ad7cddc43f9c4ebe3b Mon Sep 17 00:00:00 2001 From: kould Date: Mon, 5 Oct 2026 07:38:07 +0800 Subject: [PATCH 28/32] test: upgrade sqllogictest and share one database across slt files sqllogictest 0.14 passed a `statement error` record whenever the statement returned rows, so those checks never ran; 0.29 checks them. All slt files now run on one runner and database, so each file drops what it creates and resets its sort mode. Two expectations that were wrong are fixed. --- Cargo.lock | 187 ++++++++++++++------------- tests/slt/alter_table_large.slt | 2 +- tests/slt/basic_test.slt | 3 + tests/slt/copy.slt | 6 + tests/slt/count.slt | 5 +- tests/slt/crdb/delete.slt | 12 ++ tests/slt/crdb/join.slt | 69 ++++++++++ tests/slt/crdb/limit.slt | 11 +- tests/slt/crdb/natural_join.slt | 16 ++- tests/slt/crdb/order_by.slt | 18 +++ tests/slt/crdb/overflow.slt | 3 + tests/slt/crdb/select.slt | 29 ++++- tests/slt/crdb/where.slt | 18 ++- tests/slt/create.slt | 8 +- tests/slt/cte.slt | 3 + tests/slt/distinct.slt | 3 + tests/slt/exists.slt | 8 +- tests/slt/having.slt | 3 + tests/slt/join.slt | 9 ++ tests/slt/limit.slt | 3 + tests/slt/parameterized_subquery.slt | 24 ++++ tests/slt/select_into.slt | 5 + tests/slt/show_view.slt | 3 + tests/slt/sql_2016/E011_01.slt | 12 ++ tests/slt/sql_2016/E011_02.slt | 12 ++ tests/slt/sql_2016/E011_03.slt | 18 +++ tests/slt/sql_2016/E021_01.slt | 24 ++++ tests/slt/sql_2016/E021_02.slt | 36 ++++++ tests/slt/sql_2016/E021_10.slt | 3 + tests/slt/sql_2016/E031_01.slt | 3 + tests/slt/sql_2016/E031_02.slt | 3 + tests/slt/sql_2016/E031_03.slt | 3 + tests/slt/sql_2016/E051.slt | 12 ++ tests/slt/sql_2016/E051_01.slt | 6 + tests/slt/sql_2016/E051_02.slt | 36 ++++++ tests/slt/sql_2016/E051_04.slt | 18 +++ tests/slt/sql_2016/E051_05.slt | 3 + tests/slt/sql_2016/E051_06.slt | 3 + tests/slt/sql_2016/E051_07.slt | 18 +++ tests/slt/sql_2016/E051_08.slt | 18 +++ tests/slt/sql_2016/E051_09.slt | 9 ++ tests/slt/sql_2016/E061_01.slt | 54 ++++++++ tests/slt/sql_2016/E061_02.slt | 6 + tests/slt/sql_2016/E061_03.slt | 12 ++ tests/slt/sql_2016/E061_04.slt | 6 + tests/slt/sql_2016/E061_05.slt | 6 + tests/slt/sql_2016/E061_06.slt | 6 + tests/slt/sql_2016/E061_07.slt | 54 ++++++++ tests/slt/sql_2016/E061_08.slt | 3 + tests/slt/sql_2016/E061_09.slt | 18 +++ tests/slt/sql_2016/E061_11.slt | 3 + tests/slt/sql_2016/E061_12.slt | 54 ++++++++ tests/slt/sql_2016/E061_13.slt | 12 ++ tests/slt/sql_2016/E061_14.slt | 3 + tests/slt/sql_2016/E071_01.slt | 6 + tests/slt/sql_2016/E071_02.slt | 6 + tests/slt/sql_2016/E071_03.slt | 6 + tests/slt/sql_2016/E071_05.slt | 8 +- tests/slt/sql_2016/E071_06.slt | 30 +++++ tests/slt/sql_2016/E091_01.slt | 3 + tests/slt/sql_2016/E091_02.slt | 6 + tests/slt/sql_2016/E091_03.slt | 5 +- tests/slt/sql_2016/E091_04.slt | 5 +- tests/slt/sql_2016/E091_05.slt | 5 +- tests/slt/sql_2016/E091_06.slt | 15 +++ tests/slt/sql_2016/E091_07.slt | 15 +++ tests/slt/sql_2016/E101_01.slt | 3 + tests/slt/sql_2016/E101_03.slt | 3 + tests/slt/sql_2016/E101_04.slt | 6 + tests/slt/sql_2016/E141_01.slt | 3 + tests/slt/sql_2016/E141_02.slt | 18 +++ tests/slt/sql_2016/E141_03.slt | 18 +++ tests/slt/sql_2016/E141_07.slt | 6 + tests/slt/sql_2016/E141_08.slt | 36 ++++++ tests/slt/sql_2016/E141_10.slt | 30 +++++ tests/slt/sql_2016/E153.slt | 6 + tests/slt/sql_2016/F031_01.slt | 6 + tests/slt/sql_2016/F031_02.slt | 6 + tests/slt/sql_2016/F031_04.slt | 6 + tests/slt/sql_2016/F031_16.slt | 3 + tests/slt/sql_2016/F041_01.slt | 18 +++ tests/slt/sql_2016/F041_02.slt | 18 +++ tests/slt/sql_2016/F041_03.slt | 36 ++++++ tests/slt/sql_2016/F041_04.slt | 36 ++++++ tests/slt/sql_2016/F041_05.slt | 9 ++ tests/slt/sql_2016/F041_07.slt | 18 +++ tests/slt/sql_2016/F041_08.slt | 36 ++++++ tests/slt/sql_2016/F051_01.slt | 3 + tests/slt/sql_2016/F051_02.slt | 3 + tests/slt/sql_2016/F051_03.slt | 3 + tests/slt/sql_2016/F081.slt | 24 ++++ tests/slt/sql_2016/F131_01.slt | 36 ++++++ tests/slt/sql_2016/F131_02.slt | 9 ++ tests/slt/sql_2016/F131_03.slt | 12 ++ tests/slt/sql_2016/F131_04.slt | 180 ++++++++++++++++++++++++++ tests/slt/sql_2016/F221.slt | 6 + tests/slt/sql_2016/F471.slt | 3 + tests/slt/sql_2016/F481.slt | 6 + tests/slt/sql_2016/T631.slt | 3 + tests/slt/view.slt | 12 ++ tests/slt/window.slt | 3 + tests/sqllogictest/Cargo.toml | 2 +- tests/sqllogictest/src/main.rs | 12 +- 103 files changed, 1559 insertions(+), 111 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 759df72a..2ae9f788 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -87,7 +87,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" dependencies = [ "anstyle", - "anstyle-parse", + "anstyle-parse 0.2.7", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse 1.0.0", "anstyle-query", "anstyle-wincon", "colorchoice", @@ -110,6 +125,15 @@ dependencies = [ "utf8parse", ] +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + [[package]] name = "anstyle-query" version = "1.1.3" @@ -412,7 +436,7 @@ version = "4.5.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d" dependencies = [ - "anstream", + "anstream 0.6.19", "anstyle", "clap_lex", "strsim", @@ -643,14 +667,14 @@ dependencies = [ [[package]] name = "educe" -version = "0.4.23" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f0042ff8246a363dbe77d2ceedb073339e85a804b9a47636c6e016a9a32c05f" +checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" dependencies = [ "enum-ordinalize", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.104", ] [[package]] @@ -667,15 +691,22 @@ checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" [[package]] name = "enum-ordinalize" -version = "3.1.15" +version = "4.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf1fa3f06bbff1ea5b1a9c7b14aa992a39657db60a2759457328d7e058f49ee" +checksum = "89dd01549b09589510cf0647475075d12071456586d70f5c75c98ae2a5537677" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65863d15a4ce2888bd2f0f543cc963d3879c3a022c8ee43f6141d479a3ac815" dependencies = [ - "num-bigint", - "num-traits", "proc-macro2", "quote", - "syn 2.0.104", + "syn 3.0.6", ] [[package]] @@ -720,6 +751,12 @@ version = "3.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" +[[package]] +name = "escape8259" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5692dd7b5a1978a5aeb0ce83b7655c58ca8efdcb79d21036ea249da95afec2c6" + [[package]] name = "fastrand" version = "2.3.0" @@ -746,9 +783,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "fs-err" -version = "2.11.0" +version = "3.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" +checksum = "b5c95b673b8f6f7235229ae11c5642d81b04c2e64c1e2fb417bc0cf73ca45f29" dependencies = [ "autocfg", ] @@ -1061,6 +1098,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -1175,13 +1221,14 @@ dependencies = [ [[package]] name = "libtest-mimic" -version = "0.6.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d8de370f98a6cb8a4606618e53e802f93b094ddec0f96988eaec2c27e6e9ce7" +checksum = "14e6ba06f0ade6e504aff834d7c34298e5155c6baca353cc6a4aaff2f9fd7f33" dependencies = [ + "anstream 1.0.0", + "anstyle", "clap", - "termcolor", - "threadpool", + "escape8259", ] [[package]] @@ -1348,17 +1395,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", -] - -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", + "windows-sys 0.60.2", ] [[package]] @@ -1371,15 +1408,6 @@ dependencies = [ "itoa", ] -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - [[package]] name = "num-traits" version = "0.2.19" @@ -1390,16 +1418,6 @@ dependencies = [ "libm", ] -[[package]] -name = "num_cpus" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" -dependencies = [ - "hermit-abi", - "libc", -] - [[package]] name = "number_prefix" version = "0.4.0" @@ -1444,9 +1462,9 @@ dependencies = [ [[package]] name = "owo-colors" -version = "3.5.0" +version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" +checksum = "13c45bb4a6ae1280ec0803b1ef9d3455eb50f01efbbe1447ab020f1d54fba9d8" [[package]] name = "paste" @@ -1526,7 +1544,7 @@ dependencies = [ "spin", "symbolic-demangle", "tempfile", - "thiserror 2.0.12", + "thiserror", ] [[package]] @@ -2001,9 +2019,9 @@ dependencies = [ [[package]] name = "sqllogictest" -version = "0.14.0" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556b08485afb80bb29ed93a0a364641dbfdb65d832c0d08f5b16cb3b750696e5" +checksum = "d03b2262a244037b0b510edbd25a8e6c9fb8d73ee0237fc6cc95a54c16f94a82" dependencies = [ "async-trait", "educe", @@ -2011,14 +2029,16 @@ dependencies = [ "futures", "glob", "humantime", - "itertools 0.10.5", + "itertools 0.13.0", "libtest-mimic", "md-5", "owo-colors", + "rand", "regex", "similar", + "subst", "tempfile", - "thiserror 1.0.69", + "thiserror", "tracing", ] @@ -2060,6 +2080,16 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subst" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a9a86e5144f63c2d18334698269a8bfae6eece345c70b64821ea5b35054ec99" +dependencies = [ + "memchr", + "unicode-width 0.1.14", +] + [[package]] name = "symbolic-common" version = "12.15.5" @@ -2105,6 +2135,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "tap" version = "1.0.1" @@ -2130,42 +2171,13 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - [[package]] name = "thiserror" version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ - "thiserror-impl 2.0.12", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", + "thiserror-impl", ] [[package]] @@ -2179,15 +2191,6 @@ dependencies = [ "syn 2.0.104", ] -[[package]] -name = "threadpool" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" -dependencies = [ - "num_cpus", -] - [[package]] name = "tinytemplate" version = "1.2.1" diff --git a/tests/slt/alter_table_large.slt b/tests/slt/alter_table_large.slt index 3b523dd2..c5fb28dc 100644 --- a/tests/slt/alter_table_large.slt +++ b/tests/slt/alter_table_large.slt @@ -51,7 +51,7 @@ select v2 from alter_large where id = 30000 statement ok alter table alter_large alter column v3 set not null -statement error +statement ok insert into alter_large (id, v2) values (60000, 60002) statement ok diff --git a/tests/slt/basic_test.slt b/tests/slt/basic_test.slt index b96cb70f..f05fd01d 100644 --- a/tests/slt/basic_test.slt +++ b/tests/slt/basic_test.slt @@ -129,3 +129,6 @@ select c1 from t2; 0 1 2 + +statement ok +drop table t2 diff --git a/tests/slt/copy.slt b/tests/slt/copy.slt index d6366524..1b6145e9 100644 --- a/tests/slt/copy.slt +++ b/tests/slt/copy.slt @@ -31,3 +31,9 @@ query I SELECT * FROM test_copy_query ---- 1 two + +statement ok +drop table test_copy + +statement ok +drop table test_copy_query diff --git a/tests/slt/count.slt b/tests/slt/count.slt index f8930bb9..9f8b0874 100644 --- a/tests/slt/count.slt +++ b/tests/slt/count.slt @@ -46,4 +46,7 @@ select count(*) from t where v > 5 # query I # select count(*) from t where 0 = 1 # ---- -# 0 \ No newline at end of file +# 0 + +statement ok +drop table t diff --git a/tests/slt/crdb/delete.slt b/tests/slt/crdb/delete.slt index 560ed4b0..053241e0 100644 --- a/tests/slt/crdb/delete.slt +++ b/tests/slt/crdb/delete.slt @@ -332,3 +332,15 @@ drop table t1; statement ok drop table t2; + +statement ok +drop view kview + +statement ok +drop table family + +statement ok +drop table unindexed + +statement ok +drop table kv diff --git a/tests/slt/crdb/join.slt b/tests/slt/crdb/join.slt index 88c563d8..62debc74 100644 --- a/tests/slt/crdb/join.slt +++ b/tests/slt/crdb/join.slt @@ -1118,3 +1118,72 @@ select * from onecolumn as a left join twocolumn as b on a.x = b.x where b.x > 4 # query # SELECT abcd.*, dxby.* FROM abcd INNER JOIN dxby USING (d, b) + +statement ok +drop table xyv + +statement ok +drop table pkba + +statement ok +drop table othercolumn + +statement ok +drop table r + +statement ok +drop table s + +statement ok +drop table pkbc + +statement ok +drop table str2 + +statement ok +drop table othertype + +statement ok +drop table a + +statement ok +drop table empty + +statement ok +drop table l + +statement ok +drop table pkbac + +statement ok +drop table t1 + +statement ok +drop table xyu + +statement ok +drop table twocolumn + +statement ok +drop table b + +statement ok +drop table pairs + +statement ok +drop table square + +statement ok +drop table t2 + +statement ok +drop table pkbad + +statement ok +drop table onecolumn + +statement ok +drop table onecolumn_w + +statement ok +drop table str1 diff --git a/tests/slt/crdb/limit.slt b/tests/slt/crdb/limit.slt index bf2f2df9..97b28944 100644 --- a/tests/slt/crdb/limit.slt +++ b/tests/slt/crdb/limit.slt @@ -194,4 +194,13 @@ SELECT * FROM t ORDER BY v, w LIMIT 3 ---- 2 -4 8 4 -16 94 -6 -36 216 \ No newline at end of file +6 -36 216 + +statement ok +drop table t + +statement ok +drop table t65171 + +statement ok +drop table t_47283 diff --git a/tests/slt/crdb/natural_join.slt b/tests/slt/crdb/natural_join.slt index a7a699c4..505e9be1 100644 --- a/tests/slt/crdb/natural_join.slt +++ b/tests/slt/crdb/natural_join.slt @@ -136,4 +136,18 @@ INSERT INTO r VALUES (1) query II SELECT *, true FROM (SELECT l FROM l WHERE l NOT IN (SELECT r FROM r)) ---- -2 true \ No newline at end of file +2 true + +statement ok +drop table l + +statement ok +drop table r + +statement ok +drop table t1 + +statement ok +drop table t2 + +control sortmode nosort diff --git a/tests/slt/crdb/order_by.slt b/tests/slt/crdb/order_by.slt index 08bc3c75..f93ed8d8 100644 --- a/tests/slt/crdb/order_by.slt +++ b/tests/slt/crdb/order_by.slt @@ -398,3 +398,21 @@ null 6 2 null 2 5 4 8 + +statement ok +drop table xy + +statement ok +drop table abcd + +statement ok +drop table abc + +statement ok +drop table t + +statement ok +drop table bar + +statement ok +drop table store diff --git a/tests/slt/crdb/overflow.slt b/tests/slt/crdb/overflow.slt index 0b663e0b..561ba76c 100644 --- a/tests/slt/crdb/overflow.slt +++ b/tests/slt/crdb/overflow.slt @@ -18,3 +18,6 @@ INSERT INTO large_numbers VALUES (0, -9223372036854775808), (1, -1) query error SELECT sum(a) FROM large_numbers + +statement ok +drop table large_numbers diff --git a/tests/slt/crdb/select.slt b/tests/slt/crdb/select.slt index 417aea65..b4eba0be 100644 --- a/tests/slt/crdb/select.slt +++ b/tests/slt/crdb/select.slt @@ -530,4 +530,31 @@ INSERT INTO wide(id, n) VALUES(0, 10) query IITITTITTTTIRRI SELECT * FROM wide ---- -0 null null null null null null null null null null null null null 10 \ No newline at end of file +0 null null null null null null null null null null null null null 10 + +statement ok +drop table xyzw + +statement ok +drop table wide + +statement ok +drop table c + +statement ok +drop table a + +statement ok +drop table abc + +statement ok +drop table b + +statement ok +drop table boolean_table + +statement ok +drop table maxinttest + +statement ok +drop table kv diff --git a/tests/slt/crdb/where.slt b/tests/slt/crdb/where.slt index 06f1ce95..48cd417f 100644 --- a/tests/slt/crdb/where.slt +++ b/tests/slt/crdb/where.slt @@ -48,8 +48,13 @@ SELECT * FROM kv WHERE k IN (SELECT k FROM kv) 5 6 7 8 -statement error 1065 +query II SELECT * FROM kv WHERE (k,v) IN (SELECT * FROM kv) +---- +1 2 +3 4 +5 6 +7 8 query II SELECT * FROM kv WHERE k IN (SELECT k FROM kv) @@ -107,3 +112,14 @@ query SELECT a, b FROM ab WHERE (a, b) IN ((1, 10), (4, NULL), (NULL, 50)) ---- 1 10 + +statement ok +drop table ab + +statement ok +drop table kv + +statement ok +drop table kvstring + +control sortmode nosort diff --git a/tests/slt/create.slt b/tests/slt/create.slt index 1c532450..6062194a 100644 --- a/tests/slt/create.slt +++ b/tests/slt/create.slt @@ -26,4 +26,10 @@ statement ok create table if not exists t(id int primary key, v1 int, v2 int, v3 int) statement error -create table test_default_expr(id int primary key, v1 int, v2 int, v3 int default (v1 + 1)) \ No newline at end of file +create table test_default_expr(id int primary key, v1 int, v2 int, v3 int default (v1 + 1)) + +statement ok +drop table t_m + +statement ok +drop table t diff --git a/tests/slt/cte.slt b/tests/slt/cte.slt index f35c9ac4..7977ffb8 100644 --- a/tests/slt/cte.slt +++ b/tests/slt/cte.slt @@ -238,3 +238,6 @@ SELECT * FROM recursive_cte statement error WITH selected AS (SELECT 1) FROM cte_source SELECT * FROM selected + +statement ok +drop table cte_source diff --git a/tests/slt/distinct.slt b/tests/slt/distinct.slt index 6f6be572..0ce6bab9 100644 --- a/tests/slt/distinct.slt +++ b/tests/slt/distinct.slt @@ -77,3 +77,6 @@ null statement ok drop table distinct_alias + +statement ok +drop table test diff --git a/tests/slt/exists.slt b/tests/slt/exists.slt index dc4d3392..80cadb32 100644 --- a/tests/slt/exists.slt +++ b/tests/slt/exists.slt @@ -31,4 +31,10 @@ SELECT id, val FROM t1 WHERE NOT EXISTS (SELECT 1 FROM t2 WHERE 1 = 0) ---- 1 10 2 20 -3 30 \ No newline at end of file +3 30 + +statement ok +drop table t1 + +statement ok +drop table t2 diff --git a/tests/slt/having.slt b/tests/slt/having.slt index b7290a58..713d0b79 100644 --- a/tests/slt/having.slt +++ b/tests/slt/having.slt @@ -60,3 +60,6 @@ select x from test group by x having x between 1 and 10 ---- 1 2 + +statement ok +drop table test diff --git a/tests/slt/join.slt b/tests/slt/join.slt index 11daa01d..40c5bcc2 100644 --- a/tests/slt/join.slt +++ b/tests/slt/join.slt @@ -580,3 +580,12 @@ drop table full_using_l statement ok drop table full_using_r + +statement ok +drop table z + +statement ok +drop table a + +statement ok +drop table b diff --git a/tests/slt/limit.slt b/tests/slt/limit.slt index fb1694bd..7022ef02 100644 --- a/tests/slt/limit.slt +++ b/tests/slt/limit.slt @@ -83,3 +83,6 @@ drop table limit_join_l statement ok drop table limit_join_r + +statement ok +drop table t diff --git a/tests/slt/parameterized_subquery.slt b/tests/slt/parameterized_subquery.slt index 58d4f47e..83c1c6e1 100644 --- a/tests/slt/parameterized_subquery.slt +++ b/tests/slt/parameterized_subquery.slt @@ -171,3 +171,27 @@ select id from exists_outer where not exists (select 1 from exists_inner where e 1 2 3 + +statement ok +drop table in_outer_flag + +statement ok +drop table exists_inner + +statement ok +drop table in_inner + +statement ok +drop table in_inner_nn + +statement ok +drop table exists_outer + +statement ok +drop table in_inner_flag + +statement ok +drop table in_outer + +statement ok +drop table in_inner_flag_nn diff --git a/tests/slt/select_into.slt b/tests/slt/select_into.slt index d2b9035c..4f3d0fd4 100644 --- a/tests/slt/select_into.slt +++ b/tests/slt/select_into.slt @@ -42,3 +42,8 @@ select * from t3; statement ok drop table t1; +statement ok +drop table t3 + +statement ok +drop table t2 diff --git a/tests/slt/show_view.slt b/tests/slt/show_view.slt index 4e749c5d..7b4b804b 100644 --- a/tests/slt/show_view.slt +++ b/tests/slt/show_view.slt @@ -41,3 +41,6 @@ drop view v2 query I show views ---- + +statement ok +drop table t1 diff --git a/tests/slt/sql_2016/E011_01.slt b/tests/slt/sql_2016/E011_01.slt index 6e3e4a1c..3396b990 100644 --- a/tests/slt/sql_2016/E011_01.slt +++ b/tests/slt/sql_2016/E011_01.slt @@ -26,3 +26,15 @@ query I SELECT 5 ---- 5 + +statement ok +drop table table_e011_01_01_01 + +statement ok +drop table table_e011_01_01_03 + +statement ok +drop table table_e011_01_01_04 + +statement ok +drop table table_e011_01_01_02 diff --git a/tests/slt/sql_2016/E011_02.slt b/tests/slt/sql_2016/E011_02.slt index 06f4ccfa..764ce6c1 100644 --- a/tests/slt/sql_2016/E011_02.slt +++ b/tests/slt/sql_2016/E011_02.slt @@ -261,3 +261,15 @@ query R SELECT 2E2 ---- 200.0 + +statement ok +drop table table_e011_02_01_04 + +statement ok +drop table table_e011_02_01_02 + +statement ok +drop table table_e011_02_01_01 + +statement ok +drop table table_e011_02_01_03 diff --git a/tests/slt/sql_2016/E011_03.slt b/tests/slt/sql_2016/E011_03.slt index 339a316e..fbcff0de 100644 --- a/tests/slt/sql_2016/E011_03.slt +++ b/tests/slt/sql_2016/E011_03.slt @@ -27,3 +27,21 @@ CREATE TABLE TABLE_E011_03_01_06 ( ID INT PRIMARY KEY, A DECIMAL ) # statement ok # CREATE TABLE TABLE_E011_03_01_09 ( ID INT PRIMARY KEY, A NUMERIC ) + +statement ok +drop table table_e011_03_01_03 + +statement ok +drop table table_e011_03_01_06 + +statement ok +drop table table_e011_03_01_04 + +statement ok +drop table table_e011_03_01_01 + +statement ok +drop table table_e011_03_01_05 + +statement ok +drop table table_e011_03_01_02 diff --git a/tests/slt/sql_2016/E021_01.slt b/tests/slt/sql_2016/E021_01.slt index 1c0cea04..baa2eeac 100644 --- a/tests/slt/sql_2016/E021_01.slt +++ b/tests/slt/sql_2016/E021_01.slt @@ -23,3 +23,27 @@ CREATE TABLE TABLE_E021_01_01_07 ( ID INT PRIMARY KEY, A CHARACTER ( 8 OCTETS ) statement ok CREATE TABLE TABLE_E021_01_01_08 ( ID INT PRIMARY KEY, A CHARACTER ) + +statement ok +drop table table_e021_01_01_07 + +statement ok +drop table table_e021_01_01_06 + +statement ok +drop table table_e021_01_01_08 + +statement ok +drop table table_e021_01_01_05 + +statement ok +drop table table_e021_01_01_04 + +statement ok +drop table table_e021_01_01_02 + +statement ok +drop table table_e021_01_01_03 + +statement ok +drop table table_e021_01_01_01 diff --git a/tests/slt/sql_2016/E021_02.slt b/tests/slt/sql_2016/E021_02.slt index b4cc607c..dc2a1304 100644 --- a/tests/slt/sql_2016/E021_02.slt +++ b/tests/slt/sql_2016/E021_02.slt @@ -35,3 +35,39 @@ CREATE TABLE TABLE_E021_02_01_11 ( ID INT PRIMARY KEY, A VARCHAR ( 8 OCTETS ) ) statement ok CREATE TABLE TABLE_E021_02_01_12 ( ID INT PRIMARY KEY, A VARCHAR ) + +statement ok +drop table table_e021_02_01_10 + +statement ok +drop table table_e021_02_01_02 + +statement ok +drop table table_e021_02_01_08 + +statement ok +drop table table_e021_02_01_03 + +statement ok +drop table table_e021_02_01_12 + +statement ok +drop table table_e021_02_01_06 + +statement ok +drop table table_e021_02_01_05 + +statement ok +drop table table_e021_02_01_04 + +statement ok +drop table table_e021_02_01_01 + +statement ok +drop table table_e021_02_01_07 + +statement ok +drop table table_e021_02_01_09 + +statement ok +drop table table_e021_02_01_11 diff --git a/tests/slt/sql_2016/E021_10.slt b/tests/slt/sql_2016/E021_10.slt index a22291ca..ba3fdc8d 100644 --- a/tests/slt/sql_2016/E021_10.slt +++ b/tests/slt/sql_2016/E021_10.slt @@ -10,3 +10,6 @@ query B SELECT A = B FROM TABLE_E021_10_01_01 ---- false + +statement ok +drop table table_e021_10_01_01 diff --git a/tests/slt/sql_2016/E031_01.slt b/tests/slt/sql_2016/E031_01.slt index 85ff4df3..fa2d5323 100644 --- a/tests/slt/sql_2016/E031_01.slt +++ b/tests/slt/sql_2016/E031_01.slt @@ -6,3 +6,6 @@ CREATE TABLE TABLE_E031_01_01_01 ( ID INT PRIMARY KEY, "A" INT ); query T SELECT "A" FROM TABLE_E031_01_01_01 + +statement ok +drop table table_e031_01_01_01 diff --git a/tests/slt/sql_2016/E031_02.slt b/tests/slt/sql_2016/E031_02.slt index 0d2db9b6..8419e887 100644 --- a/tests/slt/sql_2016/E031_02.slt +++ b/tests/slt/sql_2016/E031_02.slt @@ -5,3 +5,6 @@ CREATE TABLE TABLE_E031_02_01_01 ( ID INT PRIMARY KEY, a INT ); query I SELECT a FROM TABLE_E031_02_01_01 + +statement ok +drop table table_e031_02_01_01 diff --git a/tests/slt/sql_2016/E031_03.slt b/tests/slt/sql_2016/E031_03.slt index 7a370797..f3781f20 100644 --- a/tests/slt/sql_2016/E031_03.slt +++ b/tests/slt/sql_2016/E031_03.slt @@ -5,3 +5,6 @@ CREATE TABLE TABLE_E031_03_01_01 ( ID INT PRIMARY KEY, A_ INT ); query I SELECT A_ FROM TABLE_E031_03_01_01 + +statement ok +drop table table_e031_03_01_01 diff --git a/tests/slt/sql_2016/E051.slt b/tests/slt/sql_2016/E051.slt index 43b1b0eb..9160af76 100644 --- a/tests/slt/sql_2016/E051.slt +++ b/tests/slt/sql_2016/E051.slt @@ -23,3 +23,15 @@ CREATE TABLE TABLE_E051_01_04 ( ID INT PRIMARY KEY, A INT ); query I SELECT ALL A FROM TABLE_E051_01_04 WHERE A = 1 + +statement ok +drop table table_e051_01_04 + +statement ok +drop table table_e051_01_03 + +statement ok +drop table table_e051_01_02 + +statement ok +drop table table_e051_01_01 diff --git a/tests/slt/sql_2016/E051_01.slt b/tests/slt/sql_2016/E051_01.slt index 7a0f7193..be38bad6 100644 --- a/tests/slt/sql_2016/E051_01.slt +++ b/tests/slt/sql_2016/E051_01.slt @@ -11,3 +11,9 @@ CREATE TABLE TABLE_E051_01_01_02 ( ID INT PRIMARY KEY, A INT ); query I SELECT DISTINCT A FROM TABLE_E051_01_01_02 WHERE A = 1 + +statement ok +drop table table_e051_01_01_02 + +statement ok +drop table table_e051_01_01_01 diff --git a/tests/slt/sql_2016/E051_02.slt b/tests/slt/sql_2016/E051_02.slt index c6ae3b42..e6a7a202 100644 --- a/tests/slt/sql_2016/E051_02.slt +++ b/tests/slt/sql_2016/E051_02.slt @@ -73,3 +73,39 @@ CREATE TABLE TABLE_E051_02_02_06 ( ID INT PRIMARY KEY, A INT, B INT ); query I SELECT DISTINCT A FROM TABLE_E051_02_02_06 WHERE A = 1 GROUP BY A + +statement ok +drop table table_e051_02_01_02 + +statement ok +drop table table_e051_02_02_06 + +statement ok +drop table table_e051_02_01_03 + +statement ok +drop table table_e051_02_01_05 + +statement ok +drop table table_e051_02_02_04 + +statement ok +drop table table_e051_02_02_03 + +statement ok +drop table table_e051_02_02_02 + +statement ok +drop table table_e051_02_01_01 + +statement ok +drop table table_e051_02_02_05 + +statement ok +drop table table_e051_02_02_01 + +statement ok +drop table table_e051_02_01_06 + +statement ok +drop table table_e051_02_01_04 diff --git a/tests/slt/sql_2016/E051_04.slt b/tests/slt/sql_2016/E051_04.slt index db46b107..9cbbc6d7 100644 --- a/tests/slt/sql_2016/E051_04.slt +++ b/tests/slt/sql_2016/E051_04.slt @@ -35,3 +35,21 @@ CREATE TABLE TABLE_E051_04_01_06 ( ID INT PRIMARY KEY, A INT, B INT ); query I SELECT DISTINCT B FROM TABLE_E051_04_01_06 WHERE A = 1 GROUP BY B + +statement ok +drop table table_e051_04_01_04 + +statement ok +drop table table_e051_04_01_05 + +statement ok +drop table table_e051_04_01_06 + +statement ok +drop table table_e051_04_01_03 + +statement ok +drop table table_e051_04_01_01 + +statement ok +drop table table_e051_04_01_02 diff --git a/tests/slt/sql_2016/E051_05.slt b/tests/slt/sql_2016/E051_05.slt index 2f5d411f..fd0aaf24 100644 --- a/tests/slt/sql_2016/E051_05.slt +++ b/tests/slt/sql_2016/E051_05.slt @@ -5,3 +5,6 @@ CREATE TABLE TABLE_E051_05_01_01 ( ID INT PRIMARY KEY, A INT ); query I SELECT A AS RENAMED FROM TABLE_E051_05_01_01 + +statement ok +drop table table_e051_05_01_01 diff --git a/tests/slt/sql_2016/E051_06.slt b/tests/slt/sql_2016/E051_06.slt index 3f3e1b49..841f377e 100644 --- a/tests/slt/sql_2016/E051_06.slt +++ b/tests/slt/sql_2016/E051_06.slt @@ -5,3 +5,6 @@ CREATE TABLE TABLE_E051_06_01_01 ( ID INT PRIMARY KEY, A INT, B INT ); query I SELECT A FROM TABLE_E051_06_01_01 GROUP BY A HAVING A = 1 + +statement ok +drop table table_e051_06_01_01 diff --git a/tests/slt/sql_2016/E051_07.slt b/tests/slt/sql_2016/E051_07.slt index 3df18090..c2a3dc88 100644 --- a/tests/slt/sql_2016/E051_07.slt +++ b/tests/slt/sql_2016/E051_07.slt @@ -79,3 +79,21 @@ CREATE TABLE TABLE_E051_07_01_12 ( ID INT PRIMARY KEY, A INT, B INT ); query III SELECT TABLE_E051_07_01_12 . * FROM TABLE_E051_07_01_12 + +statement ok +drop table table_e051_07_01_10 + +statement ok +drop table table_e051_07_01_06 + +statement ok +drop table table_e051_07_01_12 + +statement ok +drop table table_e051_07_01_08 + +statement ok +drop table table_e051_07_01_02 + +statement ok +drop table table_e051_07_01_04 diff --git a/tests/slt/sql_2016/E051_08.slt b/tests/slt/sql_2016/E051_08.slt index 5d6872b3..5f03586e 100644 --- a/tests/slt/sql_2016/E051_08.slt +++ b/tests/slt/sql_2016/E051_08.slt @@ -77,3 +77,21 @@ CREATE TABLE TABLE_E051_08_01_12 ( ID INT PRIMARY KEY, A INT, B INT ); query III SELECT MYTEMP . * FROM TABLE_E051_08_01_12 AS MYTEMP + +statement ok +drop table table_e051_08_01_12 + +statement ok +drop table table_e051_08_01_10 + +statement ok +drop table table_e051_08_01_04 + +statement ok +drop table table_e051_08_01_06 + +statement ok +drop table table_e051_08_01_02 + +statement ok +drop table table_e051_08_01_08 diff --git a/tests/slt/sql_2016/E051_09.slt b/tests/slt/sql_2016/E051_09.slt index 0b813d40..dc88f633 100644 --- a/tests/slt/sql_2016/E051_09.slt +++ b/tests/slt/sql_2016/E051_09.slt @@ -17,3 +17,12 @@ CREATE TABLE TABLE_E051_09_01_03 ( ID INT PRIMARY KEY, A INT, B INT ); query III SELECT MYTEMP . X , Y, Z FROM TABLE_E051_09_01_03 AS MYTEMP ( X, Y, Z ) + +statement ok +drop table table_e051_09_01_03 + +statement ok +drop table table_e051_09_01_02 + +statement ok +drop table table_e051_09_01_01 diff --git a/tests/slt/sql_2016/E061_01.slt b/tests/slt/sql_2016/E061_01.slt index 94f960a1..7726edc9 100644 --- a/tests/slt/sql_2016/E061_01.slt +++ b/tests/slt/sql_2016/E061_01.slt @@ -107,3 +107,57 @@ CREATE TABLE TABLE_E061_01_01_18 ( ID INT PRIMARY KEY, A INT ); query I SELECT DISTINCT A FROM TABLE_E061_01_01_18 WHERE A >= 1 + +statement ok +drop table table_e061_01_01_06 + +statement ok +drop table table_e061_01_01_10 + +statement ok +drop table table_e061_01_01_12 + +statement ok +drop table table_e061_01_01_16 + +statement ok +drop table table_e061_01_01_17 + +statement ok +drop table table_e061_01_01_11 + +statement ok +drop table table_e061_01_01_07 + +statement ok +drop table table_e061_01_01_03 + +statement ok +drop table table_e061_01_01_09 + +statement ok +drop table table_e061_01_01_04 + +statement ok +drop table table_e061_01_01_02 + +statement ok +drop table table_e061_01_01_15 + +statement ok +drop table table_e061_01_01_01 + +statement ok +drop table table_e061_01_01_18 + +statement ok +drop table table_e061_01_01_14 + +statement ok +drop table table_e061_01_01_05 + +statement ok +drop table table_e061_01_01_08 + +statement ok +drop table table_e061_01_01_13 diff --git a/tests/slt/sql_2016/E061_02.slt b/tests/slt/sql_2016/E061_02.slt index 5f3acaae..3a19f43c 100644 --- a/tests/slt/sql_2016/E061_02.slt +++ b/tests/slt/sql_2016/E061_02.slt @@ -39,3 +39,9 @@ SELECT A FROM TABLE_E061_02_01_04 WHERE A NOT BETWEEN 1 AND 1 # sqlparser-rs unsupported # query I # SELECT A FROM TABLE_E061_02_01_06 WHERE A NOT BETWEEN SYMMETRIC 1 AND 1 + +statement ok +drop table table_e061_02_01_04 + +statement ok +drop table table_e061_02_01_01 diff --git a/tests/slt/sql_2016/E061_03.slt b/tests/slt/sql_2016/E061_03.slt index 1d2e91d3..4c4d2449 100644 --- a/tests/slt/sql_2016/E061_03.slt +++ b/tests/slt/sql_2016/E061_03.slt @@ -23,3 +23,15 @@ CREATE TABLE TABLE_E061_03_01_04 ( ID INT PRIMARY KEY, A INT ); query I SELECT A FROM TABLE_E061_03_01_04 WHERE A NOT IN ( 1, 2 ) + +statement ok +drop table table_e061_03_01_04 + +statement ok +drop table table_e061_03_01_01 + +statement ok +drop table table_e061_03_01_03 + +statement ok +drop table table_e061_03_01_02 diff --git a/tests/slt/sql_2016/E061_04.slt b/tests/slt/sql_2016/E061_04.slt index dd1b1878..e56ce925 100644 --- a/tests/slt/sql_2016/E061_04.slt +++ b/tests/slt/sql_2016/E061_04.slt @@ -11,3 +11,9 @@ CREATE TABLE TABLE_E061_04_01_02 ( ID INT PRIMARY KEY, A VARCHAR ( 255 ) ); query T SELECT A FROM TABLE_E061_04_01_02 WHERE A NOT LIKE 'foo' + +statement ok +drop table table_e061_04_01_01 + +statement ok +drop table table_e061_04_01_02 diff --git a/tests/slt/sql_2016/E061_05.slt b/tests/slt/sql_2016/E061_05.slt index 3d7989f5..1c127164 100644 --- a/tests/slt/sql_2016/E061_05.slt +++ b/tests/slt/sql_2016/E061_05.slt @@ -11,3 +11,9 @@ CREATE TABLE TABLE_E061_05_01_02 ( ID INT PRIMARY KEY, A VARCHAR ( 255 ) ); query T SELECT A FROM TABLE_E061_05_01_02 WHERE A NOT LIKE 'foo' ESCAPE 'f' + +statement ok +drop table table_e061_05_01_01 + +statement ok +drop table table_e061_05_01_02 diff --git a/tests/slt/sql_2016/E061_06.slt b/tests/slt/sql_2016/E061_06.slt index 45f27cf3..3cd2c5ad 100644 --- a/tests/slt/sql_2016/E061_06.slt +++ b/tests/slt/sql_2016/E061_06.slt @@ -11,3 +11,9 @@ CREATE TABLE TABLE_E061_06_01_02 ( ID INT PRIMARY KEY, A INT ); query I SELECT A FROM TABLE_E061_06_01_02 WHERE A IS NULL + +statement ok +drop table table_e061_06_01_02 + +statement ok +drop table table_e061_06_01_01 diff --git a/tests/slt/sql_2016/E061_07.slt b/tests/slt/sql_2016/E061_07.slt index f558cfc4..66349eb7 100644 --- a/tests/slt/sql_2016/E061_07.slt +++ b/tests/slt/sql_2016/E061_07.slt @@ -107,3 +107,57 @@ CREATE TABLE TABLE_E061_07_01_18 ( ID INT PRIMARY KEY, A INT ); query I SELECT A FROM TABLE_E061_07_01_18 WHERE A >= SOME ( SELECT 1 ) + +statement ok +drop table table_e061_07_01_16 + +statement ok +drop table table_e061_07_01_07 + +statement ok +drop table table_e061_07_01_11 + +statement ok +drop table table_e061_07_01_03 + +statement ok +drop table table_e061_07_01_10 + +statement ok +drop table table_e061_07_01_12 + +statement ok +drop table table_e061_07_01_14 + +statement ok +drop table table_e061_07_01_06 + +statement ok +drop table table_e061_07_01_04 + +statement ok +drop table table_e061_07_01_05 + +statement ok +drop table table_e061_07_01_18 + +statement ok +drop table table_e061_07_01_15 + +statement ok +drop table table_e061_07_01_17 + +statement ok +drop table table_e061_07_01_08 + +statement ok +drop table table_e061_07_01_13 + +statement ok +drop table table_e061_07_01_01 + +statement ok +drop table table_e061_07_01_02 + +statement ok +drop table table_e061_07_01_09 diff --git a/tests/slt/sql_2016/E061_08.slt b/tests/slt/sql_2016/E061_08.slt index 89b5561e..bcb4eb6d 100644 --- a/tests/slt/sql_2016/E061_08.slt +++ b/tests/slt/sql_2016/E061_08.slt @@ -5,3 +5,6 @@ CREATE TABLE TABLE_E061_08_01_01 ( ID INT PRIMARY KEY, A INT ); query I SELECT A FROM TABLE_E061_08_01_01 WHERE EXISTS ( SELECT 1 ) + +statement ok +drop table table_e061_08_01_01 diff --git a/tests/slt/sql_2016/E061_09.slt b/tests/slt/sql_2016/E061_09.slt index 47b8d535..4fb8f597 100644 --- a/tests/slt/sql_2016/E061_09.slt +++ b/tests/slt/sql_2016/E061_09.slt @@ -36,3 +36,21 @@ CREATE TABLE TABLE_E061_09_01_06 ( ID INT PRIMARY KEY, A INT ); query I SELECT A FROM TABLE_E061_09_01_06 WHERE A >= ( SELECT 1 ) + +statement ok +drop table table_e061_09_01_04 + +statement ok +drop table table_e061_09_01_02 + +statement ok +drop table table_e061_09_01_06 + +statement ok +drop table table_e061_09_01_03 + +statement ok +drop table table_e061_09_01_05 + +statement ok +drop table table_e061_09_01_01 diff --git a/tests/slt/sql_2016/E061_11.slt b/tests/slt/sql_2016/E061_11.slt index 19e9b691..af34ddaf 100644 --- a/tests/slt/sql_2016/E061_11.slt +++ b/tests/slt/sql_2016/E061_11.slt @@ -5,3 +5,6 @@ CREATE TABLE TABLE_E061_11_01_01 ( ID INT PRIMARY KEY, A INT ); query I SELECT A FROM TABLE_E061_11_01_01 WHERE A IN ( SELECT 1 ); + +statement ok +drop table table_e061_11_01_01 diff --git a/tests/slt/sql_2016/E061_12.slt b/tests/slt/sql_2016/E061_12.slt index 142e4bcf..51ed83f8 100644 --- a/tests/slt/sql_2016/E061_12.slt +++ b/tests/slt/sql_2016/E061_12.slt @@ -107,3 +107,57 @@ CREATE TABLE TABLE_E061_12_01_18 ( ID INT PRIMARY KEY, A INT ); query I SELECT A FROM TABLE_E061_12_01_18 WHERE A >= SOME ( SELECT A FROM TABLE_E061_12_01_18 ) + +statement ok +drop table table_e061_12_01_11 + +statement ok +drop table table_e061_12_01_07 + +statement ok +drop table table_e061_12_01_05 + +statement ok +drop table table_e061_12_01_10 + +statement ok +drop table table_e061_12_01_06 + +statement ok +drop table table_e061_12_01_02 + +statement ok +drop table table_e061_12_01_13 + +statement ok +drop table table_e061_12_01_04 + +statement ok +drop table table_e061_12_01_01 + +statement ok +drop table table_e061_12_01_08 + +statement ok +drop table table_e061_12_01_15 + +statement ok +drop table table_e061_12_01_16 + +statement ok +drop table table_e061_12_01_09 + +statement ok +drop table table_e061_12_01_03 + +statement ok +drop table table_e061_12_01_14 + +statement ok +drop table table_e061_12_01_12 + +statement ok +drop table table_e061_12_01_17 + +statement ok +drop table table_e061_12_01_18 diff --git a/tests/slt/sql_2016/E061_13.slt b/tests/slt/sql_2016/E061_13.slt index 5ffc02df..88472bc0 100644 --- a/tests/slt/sql_2016/E061_13.slt +++ b/tests/slt/sql_2016/E061_13.slt @@ -17,3 +17,15 @@ CREATE TABLE TABLE_E061_13_01_022 ( ID INT PRIMARY KEY, B INT ); query I SELECT A FROM TABLE_E061_13_01_021 WHERE A NOT IN ( SELECT B FROM TABLE_E061_13_01_022 WHERE B = A ); + +statement ok +drop table table_e061_13_01_012 + +statement ok +drop table table_e061_13_01_022 + +statement ok +drop table table_e061_13_01_011 + +statement ok +drop table table_e061_13_01_021 diff --git a/tests/slt/sql_2016/E061_14.slt b/tests/slt/sql_2016/E061_14.slt index e3497716..5adc698f 100644 --- a/tests/slt/sql_2016/E061_14.slt +++ b/tests/slt/sql_2016/E061_14.slt @@ -5,3 +5,6 @@ CREATE TABLE TABLE_E061_14_01_01 ( ID INT PRIMARY KEY, A INT ); query I SELECT A FROM TABLE_E061_14_01_01 WHERE A = 1 + +statement ok +drop table table_e061_14_01_01 diff --git a/tests/slt/sql_2016/E071_01.slt b/tests/slt/sql_2016/E071_01.slt index cbd33e03..cf27c8fb 100644 --- a/tests/slt/sql_2016/E071_01.slt +++ b/tests/slt/sql_2016/E071_01.slt @@ -25,3 +25,9 @@ SELECT A FROM TABLE_E071_01_01_011 UNION ALL SELECT B FROM TABLE_E071_01_01_012 1 2 2 + +statement ok +drop table table_e071_01_01_012 + +statement ok +drop table table_e071_01_01_011 diff --git a/tests/slt/sql_2016/E071_02.slt b/tests/slt/sql_2016/E071_02.slt index be33cf81..60874910 100644 --- a/tests/slt/sql_2016/E071_02.slt +++ b/tests/slt/sql_2016/E071_02.slt @@ -8,3 +8,9 @@ CREATE TABLE TABLE_E071_02_01_012 ( ID INT PRIMARY KEY, B INT ); query I SELECT A FROM TABLE_E071_02_01_011 UNION ALL SELECT B FROM TABLE_E071_02_01_012 + +statement ok +drop table table_e071_02_01_011 + +statement ok +drop table table_e071_02_01_012 diff --git a/tests/slt/sql_2016/E071_03.slt b/tests/slt/sql_2016/E071_03.slt index ab11910b..f1680622 100644 --- a/tests/slt/sql_2016/E071_03.slt +++ b/tests/slt/sql_2016/E071_03.slt @@ -22,3 +22,9 @@ SELECT A FROM TABLE_E071_03_01_011 EXCEPT ALL SELECT B FROM TABLE_E071_03_01_012 ---- 1 1 + +statement ok +drop table table_e071_03_01_012 + +statement ok +drop table table_e071_03_01_011 diff --git a/tests/slt/sql_2016/E071_05.slt b/tests/slt/sql_2016/E071_05.slt index 93ae2f95..56cb4a65 100644 --- a/tests/slt/sql_2016/E071_05.slt +++ b/tests/slt/sql_2016/E071_05.slt @@ -26,4 +26,10 @@ SELECT B,D FROM TABLE_E071_05_01_012 UNION ALL SELECT A,C FROM TABLE_E071_05_01_ 1.0 1.2 1.0 5.4 2.0 2.3 -2.12 23.0 \ No newline at end of file +2.12 23.0 + +statement ok +drop table table_e071_05_01_011 + +statement ok +drop table table_e071_05_01_012 diff --git a/tests/slt/sql_2016/E071_06.slt b/tests/slt/sql_2016/E071_06.slt index a5ac2079..1380ea52 100644 --- a/tests/slt/sql_2016/E071_06.slt +++ b/tests/slt/sql_2016/E071_06.slt @@ -45,3 +45,33 @@ CREATE TABLE TABLE_E071_06_02_022 ( ID INT PRIMARY KEY, B FLOAT ); query I SELECT A FROM TABLE_E071_06_02_021 WHERE EXISTS ( SELECT A FROM TABLE_E071_06_02_021 EXCEPT SELECT B FROM TABLE_E071_06_02_022 ) + +statement ok +drop table table_e071_06_02_012 + +statement ok +drop table table_e071_06_02_011 + +statement ok +drop table table_e071_06_01_021 + +statement ok +drop table table_e071_06_01_031 + +statement ok +drop table table_e071_06_01_022 + +statement ok +drop table table_e071_06_01_012 + +statement ok +drop table table_e071_06_01_011 + +statement ok +drop table table_e071_06_02_021 + +statement ok +drop table table_e071_06_02_022 + +statement ok +drop table table_e071_06_01_032 diff --git a/tests/slt/sql_2016/E091_01.slt b/tests/slt/sql_2016/E091_01.slt index 341d7935..81415d59 100644 --- a/tests/slt/sql_2016/E091_01.slt +++ b/tests/slt/sql_2016/E091_01.slt @@ -7,3 +7,6 @@ query F SELECT AVG ( A ) FROM TABLE_E091_01_01_01 ---- null + +statement ok +drop table table_e091_01_01_01 diff --git a/tests/slt/sql_2016/E091_02.slt b/tests/slt/sql_2016/E091_02.slt index 9cb40e34..8d92c1c6 100644 --- a/tests/slt/sql_2016/E091_02.slt +++ b/tests/slt/sql_2016/E091_02.slt @@ -15,3 +15,9 @@ query I SELECT COUNT ( A ) FROM TABLE_E091_02_01_02 ---- 0 + +statement ok +drop table table_e091_02_01_02 + +statement ok +drop table table_e091_02_01_01 diff --git a/tests/slt/sql_2016/E091_03.slt b/tests/slt/sql_2016/E091_03.slt index f7d428fc..77cc285b 100644 --- a/tests/slt/sql_2016/E091_03.slt +++ b/tests/slt/sql_2016/E091_03.slt @@ -6,4 +6,7 @@ CREATE TABLE TABLE_E091_03_01_01 ( ID INT PRIMARY KEY, A FLOAT ); query F SELECT MAX ( A ) FROM TABLE_E091_03_01_01 ---- -null \ No newline at end of file +null + +statement ok +drop table table_e091_03_01_01 diff --git a/tests/slt/sql_2016/E091_04.slt b/tests/slt/sql_2016/E091_04.slt index 199fa097..8bce7520 100644 --- a/tests/slt/sql_2016/E091_04.slt +++ b/tests/slt/sql_2016/E091_04.slt @@ -6,4 +6,7 @@ CREATE TABLE TABLE_E091_04_01_01 ( ID INT PRIMARY KEY, A FLOAT ); query F SELECT MIN ( A ) FROM TABLE_E091_04_01_01 ---- -null \ No newline at end of file +null + +statement ok +drop table table_e091_04_01_01 diff --git a/tests/slt/sql_2016/E091_05.slt b/tests/slt/sql_2016/E091_05.slt index 046380a7..7408593a 100644 --- a/tests/slt/sql_2016/E091_05.slt +++ b/tests/slt/sql_2016/E091_05.slt @@ -6,4 +6,7 @@ CREATE TABLE TABLE_E091_05_01_01 ( ID INT PRIMARY KEY, A FLOAT ); query F SELECT SUM ( A ) FROM TABLE_E091_05_01_01 ---- -null \ No newline at end of file +null + +statement ok +drop table table_e091_05_01_01 diff --git a/tests/slt/sql_2016/E091_06.slt b/tests/slt/sql_2016/E091_06.slt index a01f31b4..4aadf607 100644 --- a/tests/slt/sql_2016/E091_06.slt +++ b/tests/slt/sql_2016/E091_06.slt @@ -39,3 +39,18 @@ query F SELECT SUM ( ALL A ) FROM TABLE_E091_06_05_01 ---- null + +statement ok +drop table table_e091_06_05_01 + +statement ok +drop table table_e091_06_04_01 + +statement ok +drop table table_e091_06_02_01 + +statement ok +drop table table_e091_06_03_01 + +statement ok +drop table table_e091_06_01_01 diff --git a/tests/slt/sql_2016/E091_07.slt b/tests/slt/sql_2016/E091_07.slt index 98efe265..3bb91f12 100644 --- a/tests/slt/sql_2016/E091_07.slt +++ b/tests/slt/sql_2016/E091_07.slt @@ -39,3 +39,18 @@ query F SELECT SUM ( DISTINCT A ) FROM TABLE_E091_07_01_05 ---- null + +statement ok +drop table table_e091_07_01_01 + +statement ok +drop table table_e091_07_01_05 + +statement ok +drop table table_e091_07_01_02 + +statement ok +drop table table_e091_07_01_04 + +statement ok +drop table table_e091_07_01_03 diff --git a/tests/slt/sql_2016/E101_01.slt b/tests/slt/sql_2016/E101_01.slt index e00a88e2..e708c474 100644 --- a/tests/slt/sql_2016/E101_01.slt +++ b/tests/slt/sql_2016/E101_01.slt @@ -5,3 +5,6 @@ CREATE TABLE TABLE_E101_01_01_01 ( ID INT PRIMARY KEY, A INT, B INT ); statement ok INSERT INTO TABLE_E101_01_01_01 VALUES ( 0, 1, 2 ) + +statement ok +drop table table_e101_01_01_01 diff --git a/tests/slt/sql_2016/E101_03.slt b/tests/slt/sql_2016/E101_03.slt index 864e4656..db16fb55 100644 --- a/tests/slt/sql_2016/E101_03.slt +++ b/tests/slt/sql_2016/E101_03.slt @@ -15,3 +15,6 @@ statement ok CREATE TABLE TABLE_E101_03_01_02 ( ID INT PRIMARY KEY, A INT, B INT ); INSERT INTO TABLE_E101_03_01_02 VALUES ( 0, 1, 2 ); UPDATE TABLE_E101_03_01_02 SET A = 3, B = 4 WHERE A = 1 + +statement ok +drop table table_e101_03_01_01 diff --git a/tests/slt/sql_2016/E101_04.slt b/tests/slt/sql_2016/E101_04.slt index 9e2fa783..cfa3ec27 100644 --- a/tests/slt/sql_2016/E101_04.slt +++ b/tests/slt/sql_2016/E101_04.slt @@ -17,3 +17,9 @@ INSERT INTO TABLE_E101_04_01_02 VALUES ( 0, 1, 2 ); statement ok DELETE FROM TABLE_E101_04_01_02 WHERE A = 1 + +statement ok +drop table table_e101_04_01_01 + +statement ok +drop table table_e101_04_01_02 diff --git a/tests/slt/sql_2016/E141_01.slt b/tests/slt/sql_2016/E141_01.slt index 199326b9..0b852b74 100644 --- a/tests/slt/sql_2016/E141_01.slt +++ b/tests/slt/sql_2016/E141_01.slt @@ -7,3 +7,6 @@ statement ok CREATE TABLE TABLE_E141_01_01_02 ( ID INT PRIMARY KEY, A INT NOT NULL ) + +statement ok +drop table table_e141_01_01_02 diff --git a/tests/slt/sql_2016/E141_02.slt b/tests/slt/sql_2016/E141_02.slt index 0e172408..b5dd5960 100644 --- a/tests/slt/sql_2016/E141_02.slt +++ b/tests/slt/sql_2016/E141_02.slt @@ -17,3 +17,21 @@ CREATE TABLE TABLE_E141_02_02_01 ( ID INT PRIMARY KEY, A INT NOT NULL CONSTRAINT statement ok CREATE TABLE TABLE_E141_02_02_02 ( ID INT PRIMARY KEY, A INT NOT NULL UNIQUE ) + +statement ok +drop table table_e141_02_02_02 + +statement ok +drop table table_e141_02_01_04 + +statement ok +drop table table_e141_02_01_03 + +statement ok +drop table table_e141_02_02_01 + +statement ok +drop table table_e141_02_01_01 + +statement ok +drop table table_e141_02_01_02 diff --git a/tests/slt/sql_2016/E141_03.slt b/tests/slt/sql_2016/E141_03.slt index 325d90a7..9c3c0b1d 100644 --- a/tests/slt/sql_2016/E141_03.slt +++ b/tests/slt/sql_2016/E141_03.slt @@ -17,3 +17,21 @@ CREATE TABLE TABLE_E141_03_02_01 ( A INT NOT NULL CONSTRAINT CONST_E141_03_02_01 statement ok CREATE TABLE TABLE_E141_03_02_02 ( A INT NOT NULL PRIMARY KEY ) + +statement ok +drop table table_e141_03_01_02 + +statement ok +drop table table_e141_03_01_03 + +statement ok +drop table table_e141_03_01_04 + +statement ok +drop table table_e141_03_02_02 + +statement ok +drop table table_e141_03_02_01 + +statement ok +drop table table_e141_03_01_01 diff --git a/tests/slt/sql_2016/E141_07.slt b/tests/slt/sql_2016/E141_07.slt index 2392f590..31db025d 100644 --- a/tests/slt/sql_2016/E141_07.slt +++ b/tests/slt/sql_2016/E141_07.slt @@ -36,3 +36,9 @@ CREATE TABLE TABLE_E141_07_11_01 ( ID INT PRIMARY KEY, A TIMESTAMP WITH TIME ZON # TODO: LOCALTIMESTAMP # statement ok # CREATE TABLE TABLE_E141_07_13_01 ( A TIMESTAMP WITH TIME ZONE DEFAULT LOCALTIMESTAMP ) + +statement ok +drop table table_e141_07_11_01 + +statement ok +drop table table_e141_07_02_01 diff --git a/tests/slt/sql_2016/E141_08.slt b/tests/slt/sql_2016/E141_08.slt index c7404f7d..338902e9 100644 --- a/tests/slt/sql_2016/E141_08.slt +++ b/tests/slt/sql_2016/E141_08.slt @@ -35,3 +35,39 @@ CREATE TABLE TABLE_E141_08_04_01 ( A INT CONSTRAINT CONST_E141_08_04_01 PRIMARY statement ok CREATE TABLE TABLE_E141_08_04_02 ( A INT PRIMARY KEY ) + +statement ok +drop table table_e141_08_03_04 + +statement ok +drop table table_e141_08_02_01 + +statement ok +drop table table_e141_08_03_03 + +statement ok +drop table table_e141_08_03_02 + +statement ok +drop table table_e141_08_04_01 + +statement ok +drop table table_e141_08_02_02 + +statement ok +drop table table_e141_08_01_04 + +statement ok +drop table table_e141_08_01_01 + +statement ok +drop table table_e141_08_03_01 + +statement ok +drop table table_e141_08_01_03 + +statement ok +drop table table_e141_08_04_02 + +statement ok +drop table table_e141_08_01_02 diff --git a/tests/slt/sql_2016/E141_10.slt b/tests/slt/sql_2016/E141_10.slt index fb36bfcb..ceb5cc12 100644 --- a/tests/slt/sql_2016/E141_10.slt +++ b/tests/slt/sql_2016/E141_10.slt @@ -49,3 +49,33 @@ statement ok CREATE TABLE TABLE_E141_10_02_021 ( ID INT PRIMARY KEY, A INT NOT NULL, B INT NOT NULL, UNIQUE ( A, B ) ); # CREATE TABLE TABLE_E141_10_02_022 ( C INT NOT NULL, D INT NOT NULL, FOREIGN KEY ( C, D ) REFERENCES TABLE_E141_10_02_021 ( B, A ) ON DELETE NO ACTION ON UPDATE NO ACTION ) + +statement ok +drop table table_e141_10_01_061 + +statement ok +drop table table_e141_10_01_041 + +statement ok +drop table table_e141_10_01_011 + +statement ok +drop table table_e141_10_01_081 + +statement ok +drop table table_e141_10_02_021 + +statement ok +drop table table_e141_10_01_031 + +statement ok +drop table table_e141_10_01_051 + +statement ok +drop table table_e141_10_01_071 + +statement ok +drop table table_e141_10_02_011 + +statement ok +drop table table_e141_10_01_021 diff --git a/tests/slt/sql_2016/E153.slt b/tests/slt/sql_2016/E153.slt index 349ddd3f..c80a799f 100644 --- a/tests/slt/sql_2016/E153.slt +++ b/tests/slt/sql_2016/E153.slt @@ -17,3 +17,9 @@ INSERT INTO TABLE_E153_02_01 VALUES ( 1, 2 ); statement ok DELETE FROM TABLE_E153_02_01 WHERE A = ( SELECT 1 ) + +statement ok +drop table table_e153_02_01 + +statement ok +drop table table_e153_01_01 diff --git a/tests/slt/sql_2016/F031_01.slt b/tests/slt/sql_2016/F031_01.slt index 44a05278..e20cda20 100644 --- a/tests/slt/sql_2016/F031_01.slt +++ b/tests/slt/sql_2016/F031_01.slt @@ -5,3 +5,9 @@ CREATE TABLE TABLE_F031_01_01_01 ( ID INT PRIMARY KEY, A INTEGER ) statement ok CREATE TABLE TABLE_F031_01_01_02 ( ID INT PRIMARY KEY, A INTEGER , B INTEGER ) + +statement ok +drop table table_f031_01_01_02 + +statement ok +drop table table_f031_01_01_01 diff --git a/tests/slt/sql_2016/F031_02.slt b/tests/slt/sql_2016/F031_02.slt index 2c3fcfd0..73861bc2 100644 --- a/tests/slt/sql_2016/F031_02.slt +++ b/tests/slt/sql_2016/F031_02.slt @@ -5,3 +5,9 @@ CREATE TABLE TABLE_F031_02_01_01 ( ID INT PRIMARY KEY, A INTEGER ); statement ok CREATE VIEW VIEW_F031_02_01_01 AS SELECT A FROM TABLE_F031_02_01_01 + +statement ok +drop view view_f031_02_01_01 + +statement ok +drop table table_f031_02_01_01 diff --git a/tests/slt/sql_2016/F031_04.slt b/tests/slt/sql_2016/F031_04.slt index fa7142c3..786b27fc 100644 --- a/tests/slt/sql_2016/F031_04.slt +++ b/tests/slt/sql_2016/F031_04.slt @@ -11,3 +11,9 @@ CREATE TABLE TABLE_F031_04_01_02 ( ID INT PRIMARY KEY, A INTEGER ); statement ok ALTER TABLE TABLE_F031_04_01_02 ADD COLUMN B INT + +statement ok +drop table table_f031_04_01_01 + +statement ok +drop table table_f031_04_01_02 diff --git a/tests/slt/sql_2016/F031_16.slt b/tests/slt/sql_2016/F031_16.slt index 41dc3de6..51af9886 100644 --- a/tests/slt/sql_2016/F031_16.slt +++ b/tests/slt/sql_2016/F031_16.slt @@ -8,3 +8,6 @@ CREATE VIEW VIEW_F031_16_01_01 AS SELECT A FROM TABLE_F031_16_01_01; statement ok DROP VIEW VIEW_F031_16_01_01 + +statement ok +drop table table_f031_16_01_01 diff --git a/tests/slt/sql_2016/F041_01.slt b/tests/slt/sql_2016/F041_01.slt index 8bcae3d0..c46080a3 100644 --- a/tests/slt/sql_2016/F041_01.slt +++ b/tests/slt/sql_2016/F041_01.slt @@ -27,3 +27,21 @@ CREATE TABLE TABLE_F041_01_01_032 ( ID INT PRIMARY KEY, A INTEGER ); # sqlparser-rs unsupported # query II # SELECT TABLE_F041_01_01_031.A, TABLE_F041_01_01_032.A FROM TABLE_F041_01_01_031 JOIN TABLE_F041_01_01_032 USING ( A ) AS FOO + +statement ok +drop table table_f041_01_01_012 + +statement ok +drop table table_f041_01_01_032 + +statement ok +drop table table_f041_01_01_021 + +statement ok +drop table table_f041_01_01_031 + +statement ok +drop table table_f041_01_01_022 + +statement ok +drop table table_f041_01_01_011 diff --git a/tests/slt/sql_2016/F041_02.slt b/tests/slt/sql_2016/F041_02.slt index 0865c4fc..495ed486 100644 --- a/tests/slt/sql_2016/F041_02.slt +++ b/tests/slt/sql_2016/F041_02.slt @@ -27,3 +27,21 @@ CREATE TABLE TABLE_F041_02_01_032 ( ID INT PRIMARY KEY, A INTEGER ); # sqlparser-rs unsupported # query II # SELECT TABLE_F041_02_01_031.A, TABLE_F041_02_01_032.A FROM TABLE_F041_02_01_031 INNER JOIN TABLE_F041_02_01_032 USING ( A ) AS FOO + +statement ok +drop table table_f041_02_01_011 + +statement ok +drop table table_f041_02_01_012 + +statement ok +drop table table_f041_02_01_032 + +statement ok +drop table table_f041_02_01_021 + +statement ok +drop table table_f041_02_01_031 + +statement ok +drop table table_f041_02_01_022 diff --git a/tests/slt/sql_2016/F041_03.slt b/tests/slt/sql_2016/F041_03.slt index cdc1ea62..07fb78ed 100644 --- a/tests/slt/sql_2016/F041_03.slt +++ b/tests/slt/sql_2016/F041_03.slt @@ -55,3 +55,39 @@ CREATE TABLE TABLE_F041_03_01_062 ( ID INT PRIMARY KEY, A INTEGER ); # sqlparser-rs unsupported # query II # SELECT TABLE_F041_03_01_061.A, TABLE_F041_03_01_062.A FROM TABLE_F041_03_01_061 LEFT OUTER JOIN TABLE_F041_03_01_062 USING ( A ) AS FOO + +statement ok +drop table table_f041_03_01_052 + +statement ok +drop table table_f041_03_01_041 + +statement ok +drop table table_f041_03_01_061 + +statement ok +drop table table_f041_03_01_032 + +statement ok +drop table table_f041_03_01_012 + +statement ok +drop table table_f041_03_01_021 + +statement ok +drop table table_f041_03_01_022 + +statement ok +drop table table_f041_03_01_011 + +statement ok +drop table table_f041_03_01_042 + +statement ok +drop table table_f041_03_01_062 + +statement ok +drop table table_f041_03_01_051 + +statement ok +drop table table_f041_03_01_031 diff --git a/tests/slt/sql_2016/F041_04.slt b/tests/slt/sql_2016/F041_04.slt index 873bbb9e..c0436977 100644 --- a/tests/slt/sql_2016/F041_04.slt +++ b/tests/slt/sql_2016/F041_04.slt @@ -55,3 +55,39 @@ CREATE TABLE TABLE_F041_04_01_062 ( ID INT PRIMARY KEY, A INTEGER ); # sqlparser-rs unsupported # query II # SELECT TABLE_F041_04_01_061.A, TABLE_F041_04_01_062.A FROM TABLE_F041_04_01_061 RIGHT OUTER JOIN TABLE_F041_04_01_062 USING ( A ) AS FOO + +statement ok +drop table table_f041_04_01_041 + +statement ok +drop table table_f041_04_01_022 + +statement ok +drop table table_f041_04_01_011 + +statement ok +drop table table_f041_04_01_012 + +statement ok +drop table table_f041_04_01_042 + +statement ok +drop table table_f041_04_01_031 + +statement ok +drop table table_f041_04_01_062 + +statement ok +drop table table_f041_04_01_021 + +statement ok +drop table table_f041_04_01_032 + +statement ok +drop table table_f041_04_01_061 + +statement ok +drop table table_f041_04_01_051 + +statement ok +drop table table_f041_04_01_052 diff --git a/tests/slt/sql_2016/F041_05.slt b/tests/slt/sql_2016/F041_05.slt index 6b1cb428..4bf5cbff 100644 --- a/tests/slt/sql_2016/F041_05.slt +++ b/tests/slt/sql_2016/F041_05.slt @@ -11,3 +11,12 @@ CREATE TABLE TABLE_F041_05_01_013 ( ID INT PRIMARY KEY, A INTEGER ); query I SELECT TABLE_F041_05_01_011.A FROM TABLE_F041_05_01_011 LEFT JOIN TABLE_F041_05_01_012 ON TABLE_F041_05_01_011.A = TABLE_F041_05_01_012.A LEFT JOIN TABLE_F041_05_01_013 ON TABLE_F041_05_01_012.A = TABLE_F041_05_01_013.A + +statement ok +drop table table_f041_05_01_011 + +statement ok +drop table table_f041_05_01_012 + +statement ok +drop table table_f041_05_01_013 diff --git a/tests/slt/sql_2016/F041_07.slt b/tests/slt/sql_2016/F041_07.slt index bae01f16..3bf6edb6 100644 --- a/tests/slt/sql_2016/F041_07.slt +++ b/tests/slt/sql_2016/F041_07.slt @@ -37,3 +37,21 @@ CREATE TABLE TABLE_F041_07_01_061 ( ID INT PRIMARY KEY, A INTEGER ); # sqlparser-rs unsupported # query I # SELECT TABLE_F041_07_01_061.A FROM TABLE_F041_07_01_061 JOIN TABLE_F041_07_01_061 AS TABLE_F041_07_01_062 USING ( A ) AS FOO + +statement ok +drop table table_f041_07_01_011 + +statement ok +drop table table_f041_07_01_061 + +statement ok +drop table table_f041_07_01_021 + +statement ok +drop table table_f041_07_01_051 + +statement ok +drop table table_f041_07_01_041 + +statement ok +drop table table_f041_07_01_031 diff --git a/tests/slt/sql_2016/F041_08.slt b/tests/slt/sql_2016/F041_08.slt index 50e31c01..6c67c5fa 100644 --- a/tests/slt/sql_2016/F041_08.slt +++ b/tests/slt/sql_2016/F041_08.slt @@ -53,3 +53,39 @@ CREATE TABLE TABLE_F041_08_01_062 ( ID INT PRIMARY KEY, B INTEGER ); query II SELECT TABLE_F041_08_01_061.A, TABLE_F041_08_01_062.B FROM TABLE_F041_08_01_061 JOIN TABLE_F041_08_01_062 ON TABLE_F041_08_01_061.A >= TABLE_F041_08_01_062.B + +statement ok +drop table table_f041_08_01_052 + +statement ok +drop table table_f041_08_01_031 + +statement ok +drop table table_f041_08_01_042 + +statement ok +drop table table_f041_08_01_012 + +statement ok +drop table table_f041_08_01_021 + +statement ok +drop table table_f041_08_01_041 + +statement ok +drop table table_f041_08_01_011 + +statement ok +drop table table_f041_08_01_061 + +statement ok +drop table table_f041_08_01_062 + +statement ok +drop table table_f041_08_01_022 + +statement ok +drop table table_f041_08_01_051 + +statement ok +drop table table_f041_08_01_032 diff --git a/tests/slt/sql_2016/F051_01.slt b/tests/slt/sql_2016/F051_01.slt index 5f264edd..f7372772 100644 --- a/tests/slt/sql_2016/F051_01.slt +++ b/tests/slt/sql_2016/F051_01.slt @@ -7,3 +7,6 @@ query T SELECT DATE '2016-03-26' ---- 2016-03-26 + +statement ok +drop table table_f051_01_01_011 diff --git a/tests/slt/sql_2016/F051_02.slt b/tests/slt/sql_2016/F051_02.slt index 1293bb46..236b1e45 100644 --- a/tests/slt/sql_2016/F051_02.slt +++ b/tests/slt/sql_2016/F051_02.slt @@ -7,3 +7,6 @@ query T SELECT TIME '01:02:03' ---- 01:02:03 + +statement ok +drop table table_f051_02_01_011 diff --git a/tests/slt/sql_2016/F051_03.slt b/tests/slt/sql_2016/F051_03.slt index 0771f510..1f90ac8d 100644 --- a/tests/slt/sql_2016/F051_03.slt +++ b/tests/slt/sql_2016/F051_03.slt @@ -7,3 +7,6 @@ query T SELECT TIMESTAMP '2016-03-26 01:02:03' ---- 2016-03-26 01:02:03 + +statement ok +drop table table_f051_03_01_011 diff --git a/tests/slt/sql_2016/F081.slt b/tests/slt/sql_2016/F081.slt index 48d86f90..04978265 100644 --- a/tests/slt/sql_2016/F081.slt +++ b/tests/slt/sql_2016/F081.slt @@ -27,3 +27,27 @@ CREATE TABLE TABLE_F081_01_032 ( ID INT PRIMARY KEY, A INTEGER ); statement ok CREATE VIEW VIEW_F081_01_03 AS SELECT A FROM TABLE_F081_01_031 UNION SELECT A FROM TABLE_F081_01_032 + +statement ok +drop view view_f081_01_03 + +statement ok +drop view view_f081_01_02 + +statement ok +drop table table_f081_01_011 + +statement ok +drop table table_f081_01_031 + +statement ok +drop table table_f081_01_012 + +statement ok +drop table table_f081_01_032 + +statement ok +drop table table_f081_01_022 + +statement ok +drop table table_f081_01_021 diff --git a/tests/slt/sql_2016/F131_01.slt b/tests/slt/sql_2016/F131_01.slt index bc6bbe13..0af3162d 100644 --- a/tests/slt/sql_2016/F131_01.slt +++ b/tests/slt/sql_2016/F131_01.slt @@ -53,3 +53,39 @@ CREATE VIEW VIEW_F131_01_01_06 AS SELECT A FROM TABLE_F131_01_01_06 GROUP BY A; query I SELECT A FROM VIEW_F131_01_01_06 WHERE A = 1 GROUP BY A HAVING A = 2 + +statement ok +drop view view_f131_01_01_06 + +statement ok +drop view view_f131_01_01_04 + +statement ok +drop view view_f131_01_01_01 + +statement ok +drop view view_f131_01_01_03 + +statement ok +drop view view_f131_01_01_05 + +statement ok +drop view view_f131_01_01_02 + +statement ok +drop table table_f131_01_01_03 + +statement ok +drop table table_f131_01_01_01 + +statement ok +drop table table_f131_01_01_02 + +statement ok +drop table table_f131_01_01_04 + +statement ok +drop table table_f131_01_01_05 + +statement ok +drop table table_f131_01_01_06 diff --git a/tests/slt/sql_2016/F131_02.slt b/tests/slt/sql_2016/F131_02.slt index a5a75e86..3b5ddb44 100644 --- a/tests/slt/sql_2016/F131_02.slt +++ b/tests/slt/sql_2016/F131_02.slt @@ -11,3 +11,12 @@ CREATE VIEW VIEW_F131_02_01_01 AS SELECT A FROM TABLE_F131_02_01_011 GROUP BY A; query I SELECT A FROM VIEW_F131_02_01_01 JOIN TABLE_F131_02_01_012 USING ( A ) + +statement ok +drop view view_f131_02_01_01 + +statement ok +drop table table_f131_02_01_011 + +statement ok +drop table table_f131_02_01_012 diff --git a/tests/slt/sql_2016/F131_03.slt b/tests/slt/sql_2016/F131_03.slt index ec0aab1e..0c302c08 100644 --- a/tests/slt/sql_2016/F131_03.slt +++ b/tests/slt/sql_2016/F131_03.slt @@ -22,3 +22,15 @@ CREATE VIEW VIEW_F131_03_01_02 AS SELECT A, MIN ( B ) AS C FROM TABLE_F131_03_01 query I SELECT A, SUM ( C ) FROM VIEW_F131_03_01_02 GROUP BY A ---- + +statement ok +drop view view_f131_03_01_01 + +statement ok +drop view view_f131_03_01_02 + +statement ok +drop table table_f131_03_01_021 + +statement ok +drop table table_f131_03_01_011 diff --git a/tests/slt/sql_2016/F131_04.slt b/tests/slt/sql_2016/F131_04.slt index 8b7916a5..69174628 100644 --- a/tests/slt/sql_2016/F131_04.slt +++ b/tests/slt/sql_2016/F131_04.slt @@ -251,3 +251,183 @@ CREATE VIEW VIEW_F131_04_02_12 AS SELECT A FROM TABLE_F131_04_02_121 GROUP BY A query B SELECT B >= ( SELECT A FROM VIEW_F131_04_02_12 ) FROM TABLE_F131_04_02_122 + +statement ok +drop view view_f131_04_02_07 + +statement ok +drop view view_f131_04_02_03 + +statement ok +drop view view_f131_04_02_02 + +statement ok +drop view view_f131_04_02_01 + +statement ok +drop view view_f131_04_02_04 + +statement ok +drop view view_f131_04_02_09 + +statement ok +drop view view_f131_04_02_12 + +statement ok +drop view view_f131_04_02_11 + +statement ok +drop view view_f131_04_02_06 + +statement ok +drop view view_f131_04_02_10 + +statement ok +drop view view_f131_04_02_08 + +statement ok +drop view view_f131_04_02_05 + +statement ok +drop table table_f131_04_02_012 + +statement ok +drop table table_f131_04_01_031 + +statement ok +drop table table_f131_04_02_041 + +statement ok +drop table table_f131_04_02_021 + +statement ok +drop table table_f131_04_02_081 + +statement ok +drop table table_f131_04_02_111 + +statement ok +drop table table_f131_04_02_091 + +statement ok +drop table table_f131_04_02_102 + +statement ok +drop table table_f131_04_01_072 + +statement ok +drop table table_f131_04_02_032 + +statement ok +drop table table_f131_04_02_101 + +statement ok +drop table table_f131_04_01_122 + +statement ok +drop table table_f131_04_02_112 + +statement ok +drop table table_f131_04_02_072 + +statement ok +drop table table_f131_04_01_051 + +statement ok +drop table table_f131_04_01_112 + +statement ok +drop table table_f131_04_01_042 + +statement ok +drop table table_f131_04_01_071 + +statement ok +drop table table_f131_04_02_092 + +statement ok +drop table table_f131_04_01_081 + +statement ok +drop table table_f131_04_01_011 + +statement ok +drop table table_f131_04_02_042 + +statement ok +drop table table_f131_04_02_022 + +statement ok +drop table table_f131_04_01_022 + +statement ok +drop table table_f131_04_02_082 + +statement ok +drop table table_f131_04_01_111 + +statement ok +drop table table_f131_04_02_062 + +statement ok +drop table table_f131_04_01_091 + +statement ok +drop table table_f131_04_01_062 + +statement ok +drop table table_f131_04_02_071 + +statement ok +drop table table_f131_04_01_021 + +statement ok +drop table table_f131_04_01_082 + +statement ok +drop table table_f131_04_01_092 + +statement ok +drop table table_f131_04_02_031 + +statement ok +drop table table_f131_04_02_122 + +statement ok +drop table table_f131_04_01_101 + +statement ok +drop table table_f131_04_02_121 + +statement ok +drop table table_f131_04_01_041 + +statement ok +drop table table_f131_04_02_061 + +statement ok +drop table table_f131_04_01_061 + +statement ok +drop table table_f131_04_02_052 + +statement ok +drop table table_f131_04_01_102 + +statement ok +drop table table_f131_04_01_121 + +statement ok +drop table table_f131_04_01_052 + +statement ok +drop table table_f131_04_02_011 + +statement ok +drop table table_f131_04_02_051 + +statement ok +drop table table_f131_04_01_032 + +statement ok +drop table table_f131_04_01_012 diff --git a/tests/slt/sql_2016/F221.slt b/tests/slt/sql_2016/F221.slt index bb845b0f..61863909 100644 --- a/tests/slt/sql_2016/F221.slt +++ b/tests/slt/sql_2016/F221.slt @@ -11,3 +11,9 @@ CREATE TABLE TABLE_F221_02_011 ( ID INT PRIMARY KEY, A INTEGER DEFAULT 123 ); statement ok UPDATE TABLE_F221_02_011 SET A = DEFAULT + +statement ok +drop table table_f221_02_011 + +statement ok +drop table table_f221_01_011 diff --git a/tests/slt/sql_2016/F471.slt b/tests/slt/sql_2016/F471.slt index 5857d3a0..3e56ef7a 100644 --- a/tests/slt/sql_2016/F471.slt +++ b/tests/slt/sql_2016/F471.slt @@ -12,3 +12,6 @@ query I SELECT ( SELECT A FROM TABLE_F471_01_01 ) FROM TABLE_F471_01_01 ---- 1 + +statement ok +drop table table_f471_01_01 diff --git a/tests/slt/sql_2016/F481.slt b/tests/slt/sql_2016/F481.slt index e1446ee5..720ead27 100644 --- a/tests/slt/sql_2016/F481.slt +++ b/tests/slt/sql_2016/F481.slt @@ -11,3 +11,9 @@ CREATE TABLE TABLE_F481_01_02 ( ID INT PRIMARY KEY, A INTEGER ); query B SELECT A + A IS NULL FROM TABLE_F481_01_02 + +statement ok +drop table table_f481_01_02 + +statement ok +drop table table_f481_01_01 diff --git a/tests/slt/sql_2016/T631.slt b/tests/slt/sql_2016/T631.slt index 72165fb9..d9f3f5c3 100644 --- a/tests/slt/sql_2016/T631.slt +++ b/tests/slt/sql_2016/T631.slt @@ -10,3 +10,6 @@ query I SELECT A FROM TABLE_T631_01_01 WHERE A IN ( 1 ) ---- 1 + +statement ok +drop table table_t631_01_01 diff --git a/tests/slt/view.slt b/tests/slt/view.slt index a8ea62f9..36597119 100644 --- a/tests/slt/view.slt +++ b/tests/slt/view.slt @@ -75,3 +75,15 @@ select c1, sum(c0) from v1 group by c1 0 0 1 1 2 2 + +statement ok +drop view v_alias + +statement ok +drop view v2 + +statement ok +drop view v1 + +statement ok +drop table t1 diff --git a/tests/slt/window.slt b/tests/slt/window.slt index 54309212..29305e80 100644 --- a/tests/slt/window.slt +++ b/tests/slt/window.slt @@ -139,3 +139,6 @@ select id from window_test where row_number() over () = 1 statement error select row_number() over (named_window order by id) from window_test window named_window as (partition by k) + +statement ok +drop table window_test diff --git a/tests/sqllogictest/Cargo.toml b/tests/sqllogictest/Cargo.toml index cd51e384..9ee0f0c6 100644 --- a/tests/sqllogictest/Cargo.toml +++ b/tests/sqllogictest/Cargo.toml @@ -7,5 +7,5 @@ edition = "2021" clap = { version = "4" } "kite_sql" = { path = "../..", default-features = false, features = ["copy", "decimal", "lmdb", "macros", "orm", "parser", "spill", "time"] } glob = { version = "0.3" } -sqllogictest = { version = "0.14" } +sqllogictest = { version = "0.29.1" } tempfile = { version = "3.10" } diff --git a/tests/sqllogictest/src/main.rs b/tests/sqllogictest/src/main.rs index cdb0d472..e7939d65 100644 --- a/tests/sqllogictest/src/main.rs +++ b/tests/sqllogictest/src/main.rs @@ -42,8 +42,13 @@ fn main() { let mut file_num = 0; let start = Instant::now(); + let temp_dir = TempDir::new().expect("unable to create temporary working directory"); + let mut tester = Runner::new(|| { + let db = DataBaseBuilder::path(temp_dir.path()).build_lmdb(); + std::future::ready(db.map(|db| SQLBase { db })) + }); + for slt_file in glob::glob(&args.path).expect("failed to find slt files") { - let temp_dir = TempDir::new().expect("unable to create temporary working directory"); let filepath = slt_file.expect("failed to read slt file"); println!( "-> Now the test file is: {}, num: {}", @@ -51,11 +56,6 @@ fn main() { file_num ); - let db = DataBaseBuilder::path(temp_dir.path()) - .build_lmdb() - .expect("init db error"); - let mut tester = Runner::new(SQLBase { db }); - if let Err(err) = tester.run_file(filepath) { panic!("test error: {}", err); } From 3c9109fce95c414e3180dd395a6076fd5c1daa88 Mon Sep 17 00:00:00 2001 From: kould Date: Mon, 5 Oct 2026 08:04:59 +0800 Subject: [PATCH 29/32] fix: reject outer references in the SELECT list of IN/ANY/ALL subqueries IN/ANY/ALL subqueries keep their projection, whose outer column refs were not rewritten to outer positions: they read the subquery's own row, so `id IN (SELECT b * 0 + t1.k FROM t2)` returned wrong rows, and column pruning panicked on them (found by sql_exec fuzzing). Reject such projections instead of returning wrong results. --- src/binder/select.rs | 8 ++++++++ tests/slt/subquery.slt | 43 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/src/binder/select.rs b/src/binder/select.rs index 6baa33fd..f7d4ebd4 100644 --- a/src/binder/select.rs +++ b/src/binder/select.rs @@ -1874,6 +1874,14 @@ impl<'a: 'b, 'b, T: Transaction, A: AsRef<[(usize, LogicalType)]>> Binder<'a, 'b if !preserve_projection || Self::is_temp_alias_projection(&op.exprs, arena) { Ok((child, correlated_filters)) } else { + for expr in &op.exprs { + if Self::expr_has_correlated_refs(*expr, left_schema, arena)? { + return Err(DatabaseError::UnsupportedStmt( + "correlated references in the SELECT list of an IN/ANY/ALL subquery are not supported" + .to_string(), + )); + } + } let mut binder = ProjectionOutputBinder::new(&op.exprs); for expr in &mut correlated_filters { binder.visit(expr, arena)?; diff --git a/tests/slt/subquery.slt b/tests/slt/subquery.slt index 191ac0a4..75b65510 100644 --- a/tests/slt/subquery.slt +++ b/tests/slt/subquery.slt @@ -593,3 +593,46 @@ b statement ok drop table sq_ref; + +statement ok +create table sq_outer(k int primary key, name varchar, id int); + +statement ok +create table sq_inner(b int primary key, c int, e int); + +statement ok +insert into sq_outer values (10, 'a', 1), (20, 'b', 2); + +statement ok +insert into sq_inner values (1, 2, 3), (2, 1, 4); + +statement error +select k from sq_outer where id in (select sq_outer.id from sq_inner); + +statement error +select k from sq_outer where id in (select b * 0 + sq_outer.k from sq_inner); + +statement error +select k from sq_outer where id not in (select b * 0 + sq_outer.id from sq_inner); + +query I rowsort +select k from sq_outer where id in (select b from sq_inner where c = sq_outer.id); +---- + +query I rowsort +select k from sq_outer where id in (select c from sq_inner where e > sq_outer.id); +---- +10 +20 + +query I rowsort +select k from sq_outer where exists (select sq_outer.id from sq_inner); +---- +10 +20 + +statement ok +drop table sq_outer; + +statement ok +drop table sq_inner; From fb2542b93fd53454810c5f47ea883eaf1ab9eeba Mon Sep 17 00:00:00 2001 From: kould Date: Mon, 5 Oct 2026 08:59:30 +0800 Subject: [PATCH 30/32] fix: avoid overflow in integer MIN % -1 `i32::MIN % -1` panicked (debug) on Rust's remainder overflow; PostgreSQL returns 0, which is what wrapping_rem gives. Found by sql_gen fuzzing. --- src/types/evaluator/binary.rs | 2 +- tests/slt/bigint.slt | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/types/evaluator/binary.rs b/src/types/evaluator/binary.rs index a83c7942..6a7a20d3 100644 --- a/src/types/evaluator/binary.rs +++ b/src/types/evaluator/binary.rs @@ -647,7 +647,7 @@ macro_rules! numeric_binary_evaluator_definition { Ok(match (left, right) { // Integer remainder by zero would panic; report NULL like division. ($compute_type(_), $compute_type(v2)) if *v2 == 0 => $crate::types::value::DataValue::Null, - ($compute_type(v1), $compute_type(v2)) => $compute_type(*v1 % *v2), + ($compute_type(v1), $compute_type(v2)) => $compute_type(v1.wrapping_rem(*v2)), ($compute_type(_), $crate::types::value::DataValue::Null) | ($crate::types::value::DataValue::Null, $compute_type(_)) | ($crate::types::value::DataValue::Null, $crate::types::value::DataValue::Null) => $crate::types::value::DataValue::Null, diff --git a/tests/slt/bigint.slt b/tests/slt/bigint.slt index 585d7065..c0a52759 100644 --- a/tests/slt/bigint.slt +++ b/tests/slt/bigint.slt @@ -51,3 +51,17 @@ select - a, - b from neg where id = 2; statement ok drop table neg + +statement ok +create table rem_min(id int primary key, a int, b bigint); + +statement ok +insert into rem_min values (1, -2147483648, -9223372036854775807 - 1); + +query II +select a % -1, b % -1 from rem_min; +---- +0 0 + +statement ok +drop table rem_min From affdacf0ce76a6b4f8c20c568110f15233981f65 Mon Sep 17 00:00:00 2001 From: kould Date: Mon, 5 Oct 2026 08:59:30 +0800 Subject: [PATCH 31/32] test: add structured sql_gen fuzz target with a TLP oracle sql_gen turns the fuzz input into well-typed queries over a fixed schema and checks them with ternary logic partitioning: the query without WHERE and its p / NOT p / p IS NULL split must return the same rows. Both fuzz targets now run on one LMDB database per process; `make fuzz` runs either target and CI smoke-runs both. --- .github/workflows/ci.yml | 5 +- Makefile | 29 +- fuzz/Cargo.lock | 36 ++ fuzz/Cargo.toml | 11 +- fuzz/fuzz_targets/sql_exec.rs | 50 +- fuzz/fuzz_targets/sql_gen.rs | 828 ++++++++++++++++++++++++++++++++++ 6 files changed, 936 insertions(+), 23 deletions(-) create mode 100644 fuzz/fuzz_targets/sql_gen.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5de2137e..c4930a5a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -154,7 +154,10 @@ jobs: tool: cargo-fuzz - name: Run sql_exec fuzzer - run: make fuzz FUZZ_TIME=300 + run: make fuzz FUZZ_TARGET=sql_exec FUZZ_TIME=150 + + - name: Run sql_gen fuzzer + run: make fuzz FUZZ_TARGET=sql_gen FUZZ_TIME=150 - name: Upload crash artifacts if: failure() diff --git a/Makefile b/Makefile index 40853e66..ed1d71fa 100644 --- a/Makefile +++ b/Makefile @@ -23,6 +23,17 @@ FUZZ_TIME ?= 60 FUZZ_TRIPLE ?= $(shell rustc -vV | sed -n 's/^host: //p') FUZZ_TARGET_DIR ?= $(CURDIR)/fuzz/target FUZZ_BIN = $(FUZZ_TARGET_DIR)/$(FUZZ_TRIPLE)/release/$(FUZZ_TARGET) +# Per-target libFuzzer args and the exit code that still counts as a pass. +# sql_exec: tests/slt as read-only seeds (libFuzzer only writes to the FIRST dir); fork mode +# so timeouts are skipped (TODO in fuzz/fuzz_targets/sql_exec.rs). Fork mode ignores OOMs by +# default, hence -ignore_ooms=0, and exits with the LAST child's code, so a trailing ignored +# timeout (70) is not a failure. +FUZZ_ARGS_sql_exec = tests/slt -dict=fuzz/sql.dict -max_len=32768 \ + -fork=1 -ignore_timeouts=1 -ignore_ooms=0 -timeout_exitcode=70 +FUZZ_PASS_EXIT_sql_exec = 70 +# sql_gen: bounded generated queries, so crashes, timeouts and OOMs all fail. +FUZZ_ARGS_sql_gen = -max_len=4096 +FUZZ_PASS_EXIT_sql_gen = 0 COVERAGE_REPORT_ARGS ?= --llvm --ignore-not-existing --keep-only 'src/**' --ignore 'src/**/tests/**' --ignore 'tests/**' --ignore 'tpcc/**' --excl-start 'GRCOV_EXCL_START' --excl-stop 'GRCOV_EXCL_STOP' .PHONY: test test-python test-wasm test-slt test-all codecov codecov-html wasm-build check tpcc tpcc-kitesql-rocksdb tpcc-kitesql-lmdb tpcc-lmdb-flamegraph tpcc-lmdb-heaptrack tpcc-sqlite tpcc-sqlite-practical tpcc-sqlite-balanced tpcc-dual cargo-check build wasm-examples native-examples fmt clippy fuzz @@ -79,21 +90,17 @@ codecov-html: $(GRCOV) . --binary-path \"$${CARGO_TARGET_DIR:-target}/debug\" -s . -t html $(COVERAGE_REPORT_ARGS) -o '$(COVERAGE_HTML_DIR)'" @echo "Coverage report: $(COVERAGE_HTML_DIR)/index.html" -## Run a fuzz target for FUZZ_TIME seconds (needs a nightly toolchain and cargo-fuzz). -## tests/slt is a read-only seed dir: libFuzzer only writes new inputs to the FIRST dir. -## Fork mode ignores OOMs by default, hence the explicit -ignore_ooms=0: crashes and OOMs fail the run. -## It also exits with the LAST child's code, so a trailing ignored timeout (70) is not a failure. -## The binary is run directly because `cargo fuzz run` turns every non-zero exit into a failure. -## TODO: stop ignoring timeouts once unbounded queries can be told apart from hangs -## (see fuzz/fuzz_targets/sql_exec.rs). +## Run fuzz target FUZZ_TARGET (sql_exec | sql_gen) for FUZZ_TIME seconds +## (needs a nightly toolchain and cargo-fuzz). The binary is run directly because +## `cargo fuzz run` turns every non-zero exit into a failure. fuzz: @mkdir -p fuzz/corpus/$(FUZZ_TARGET) fuzz/artifacts/$(FUZZ_TARGET) $(CARGO) +$(FUZZ_TOOLCHAIN) fuzz build --target $(FUZZ_TRIPLE) --target-dir $(FUZZ_TARGET_DIR) $(FUZZ_TARGET) - @status=0; $(FUZZ_BIN) fuzz/corpus/$(FUZZ_TARGET) tests/slt \ + @status=0; $(FUZZ_BIN) fuzz/corpus/$(FUZZ_TARGET) $(FUZZ_ARGS_$(FUZZ_TARGET)) \ -artifact_prefix=fuzz/artifacts/$(FUZZ_TARGET)/ \ - -dict=fuzz/sql.dict -max_len=32768 -timeout=10 -rss_limit_mb=4096 -max_total_time=$(FUZZ_TIME) \ - -fork=1 -ignore_timeouts=1 -ignore_ooms=0 -timeout_exitcode=70 || status=$$?; \ - if [ $$status -ne 0 ] && [ $$status -ne 70 ]; then \ + -timeout=10 -rss_limit_mb=4096 -max_total_time=$(FUZZ_TIME) || status=$$?; \ + rm -rf "$${TMPDIR:-/tmp}"/kitesql-fuzz-$(FUZZ_TARGET)-*; \ + if [ $$status -ne 0 ] && [ $$status -ne $(FUZZ_PASS_EXIT_$(FUZZ_TARGET)) ]; then \ echo "fuzz: $(FUZZ_TARGET) failed (exit $$status), see fuzz/artifacts/$(FUZZ_TARGET)/"; exit $$status; \ fi diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 9b88ed39..cf4c95ff 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -29,6 +29,12 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "borsh" version = "1.8.1" @@ -299,6 +305,8 @@ dependencies = [ "bumpalo", "chrono", "kite_sql_serde_macros", + "lmdb", + "lmdb-sys", "ordered-float", "paste", "rust_decimal", @@ -339,6 +347,28 @@ dependencies = [ "cc", ] +[[package]] +name = "lmdb" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0908efb5d6496aa977d96f91413da2635a902e5e31dbef0bfb88986c248539" +dependencies = [ + "bitflags", + "libc", + "lmdb-sys", +] + +[[package]] +name = "lmdb-sys" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5b392838cfe8858e86fac37cf97a0e8c55cc60ba0a18365cadc33092f128ce9" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "log" version = "0.4.34" @@ -387,6 +417,12 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + [[package]] name = "ppv-lite86" version = "0.2.21" diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index e51afc17..6d3469bd 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -9,8 +9,8 @@ cargo-fuzz = true [dependencies] libfuzzer-sys = { version = "=0.4.10" } -# In-memory storage only: no RocksDB/LMDB build, no file IO (`copy`/`spill` off). -kite_sql = { path = "..", default-features = false, features = ["parser", "macros", "time", "decimal"] } +# LMDB storage; no RocksDB build, no `copy`/`spill` file IO. +kite_sql = { path = "..", default-features = false, features = ["parser", "macros", "time", "decimal", "lmdb"] } # Standalone workspace: fuzzing needs nightly, the main workspace stays on stable. [workspace] @@ -25,3 +25,10 @@ path = "fuzz_targets/sql_exec.rs" test = false doc = false bench = false + +[[bin]] +name = "sql_gen" +path = "fuzz_targets/sql_gen.rs" +test = false +doc = false +bench = false diff --git a/fuzz/fuzz_targets/sql_exec.rs b/fuzz/fuzz_targets/sql_exec.rs index 8bd8696a..71be8fe3 100644 --- a/fuzz/fuzz_targets/sql_exec.rs +++ b/fuzz/fuzz_targets/sql_exec.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! End-to-end SQL fuzzing against an in-memory database. +//! End-to-end SQL fuzzing against an LMDB database. //! //! The input is treated as a `;`-separated SQL script. Every chunk is run on //! its own, so a mutation that breaks one statement does not discard the rest @@ -35,21 +35,53 @@ use kite_sql::binder::{command_type, CommandType}; use kite_sql::db::{prepare_all, DataBaseBuilder, Database, Statement}; -use kite_sql::storage::memory::MemoryStorage; +use kite_sql::storage::lmdb::LmdbStorage; +use kite_sql::types::value::DataValue; use libfuzzer_sys::fuzz_target; +use std::cell::RefCell; fuzz_target!(|data: &[u8]| { let Ok(script) = std::str::from_utf8(data) else { return; }; - let Ok(mut db) = DataBaseBuilder::path(".").build_in_memory() else { - return; - }; + DB.with_borrow_mut(|db| { + reset(db); + for sql in strip_slt(script).split(';') { + run_one(db, sql); + } + }); +}); - for sql in strip_slt(script).split(';') { - run_one(&mut db, sql); +thread_local! { + // One database per process, reused by every input (emptied before each). + static DB: RefCell> = RefCell::new(open_db()); +} + +fn open_db() -> Database { + let path = std::env::temp_dir().join(format!("kitesql-fuzz-sql_exec-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&path); + DataBaseBuilder::path(path) + .lmdb_no_sync(true) + .build_lmdb() + .expect("lmdb database") +} + +/// Drops every view and table the previous input left behind. +fn reset(db: &mut Database) { + for (show, drop) in [("show views", "drop view"), ("show tables", "drop table")] { + let mut names = Vec::new(); + if let Ok(mut iter) = db.run(show) { + while let Ok(Some(())) = iter.next_tuple(|_, tuple| { + if let Some(DataValue::Utf8 { value, .. }) = tuple.values.first() { + names.push(value.clone()); + } + }) {} + } + for name in names { + let _ = db.ddl(format!("{drop} {name}")); + } } -}); +} /// sqllogictest record headers and directives; such lines never start SQL. const SLT_DIRECTIVES: &[&str] = &[ @@ -102,7 +134,7 @@ fn strip_slt(script: &str) -> String { sql } -fn run_one(db: &mut Database, sql: &str) { +fn run_one(db: &mut Database, sql: &str) { let Ok(statements) = prepare_all(sql) else { return; }; diff --git a/fuzz/fuzz_targets/sql_gen.rs b/fuzz/fuzz_targets/sql_gen.rs new file mode 100644 index 00000000..c6d06d4a --- /dev/null +++ b/fuzz/fuzz_targets/sql_gen.rs @@ -0,0 +1,828 @@ +// Copyright 2024 KipData/KiteSQL +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Structured SQL fuzzing over a fixed schema. +//! +//! The input is not SQL text: it drives the choices (rows, tables, columns, +//! expression shapes, literals) of a generator that only emits well-typed +//! queries, so nearly every query binds and reaches the optimizer and executor. +//! +//! Sizes are bounded (three tables of at most `MAX_ROWS` rows, at most 3-way +//! joins, at most one uncorrelated subquery per query, no recursive CTEs or +//! `numbers()`), so unlike `sql_exec` any panic, sanitizer report, timeout or +//! OOM is a bug. `Err` results are expected and ignored. +//! +//! The WHERE predicate is kept apart from the rest of the query so a later +//! oracle (e.g. TLP) can re-render the same query with derived predicates. +//! +//! Set `KITESQL_FUZZ_DEBUG=1` to print every statement before it runs, e.g. +//! when reproducing an artifact. + +#![no_main] + +use kite_sql::db::{DataBaseBuilder, Database}; +use kite_sql::errors::DatabaseError; +use kite_sql::storage::lmdb::LmdbStorage; +use kite_sql::types::value::DataValue; +use libfuzzer_sys::arbitrary::{self, Unstructured}; +use libfuzzer_sys::fuzz_target; +use std::collections::HashMap; +use std::fmt::{Display, Write}; +use std::sync::OnceLock; + +const SCHEMA: &[&str] = &[ + "create table t0(id int primary key, a int, b bigint, c varchar, d double)", + "create table t1(id int primary key, a int, b bigint, c varchar, d double)", + "create table t2(id int primary key, a int, b bigint, c varchar, d double)", + "create index t0_a on t0(a)", + "create unique index t1_b on t1(b)", + "create index t1_ac on t1(a, c)", +]; +const TABLES: &[&str] = &["t0", "t1", "t2"]; +const MAX_ROWS: usize = 8; +const MAX_QUERIES: usize = 8; +const MAX_DEPTH: u32 = 3; + +#[derive(Clone, Copy, PartialEq)] +enum Ty { + Num, + Str, +} + +// Every table has the same columns; `id` is the primary key. +const COLUMNS: &[(&str, Ty)] = &[ + ("id", Ty::Num), + ("a", Ty::Num), + ("b", Ty::Num), + ("c", Ty::Str), + ("d", Ty::Num), +]; + +// Small domains so predicates, joins and GROUP BY actually match rows, +// plus NULL and type boundaries. +const INT_VALUES: &[&str] = &[ + "null", + "0", + "1", + "2", + "3", + "-1", + "2147483647", + "-2147483648", +]; +const BIGINT_VALUES: &[&str] = &[ + "null", + "0", + "1", + "2", + "-1", + "9223372036854775807", + "-9223372036854775808", +]; +const DOUBLE_VALUES: &[&str] = &["null", "0.0", "1.5", "-2.5", "1e300", "-0.0"]; +const STR_VALUES: &[&str] = &["null", "''", "'a'", "'b'", "'ab'", "'A'", "'%'", "'a b'"]; +const NUM_LITERALS: &[&str] = &["null", "0", "1", "2", "-1", "3", "2147483647", "1.5"]; +const INT_LITERALS: &[&str] = &["null", "0", "1", "2", "-1", "3", "2147483647"]; +const LIKE_PATTERNS: &[&str] = &["'a%'", "'%b'", "'_'", "'%'", "''", "'a_'"]; + +const CMP_OPS: &[&str] = &["=", "<>", "<", "<=", ">", ">="]; +const ARITH_OPS: &[&str] = &["+", "-", "*", "/", "%"]; +// `/` always yields a double. +const INT_ARITH_OPS: &[&str] = &["+", "-", "*", "%"]; +const JOINS: &[&str] = &[ + "inner join", + "left join", + "right join", + "full join", + "cross join", +]; +const AGGS: &[&str] = &["count", "sum", "min", "max", "avg"]; + +/// An aggregate call, kept structured so the TLP oracle can point its argument +/// at a column of the partitioned derived table. +struct Agg { + func: &'static str, + distinct: bool, + // `None` is `count(*)`. + arg: Option, +} + +impl Agg { + fn render(&self, arg: Option<&str>) -> String { + match arg { + None => format!("{}(*)", self.func), + Some(arg) if self.distinct => format!("{}(distinct {arg})", self.func), + Some(arg) => format!("{}({arg})", self.func), + } + } +} + +enum Body { + Plain { + distinct: bool, + items: Vec, + }, + Grouped { + keys: Vec, + aggs: Vec, + // `having ` + having: Option<(Agg, &'static str, &'static str)>, + }, +} + +/// A generated query. The WHERE predicate is kept apart so the TLP oracle can +/// re-render the query with derived predicates. +struct Select { + from: String, + predicate: Option, + body: Body, + // (output column, descending) + order_by: Option<(usize, bool)>, + // (limit, offset) + limit: Option<(usize, Option)>, +} + +impl Select { + /// The query as generated, optionally without its WHERE clause and/or its + /// LIMIT / OFFSET. + fn render(&self, with_predicate: bool, with_limit: bool) -> String { + let mut sql = String::from("select "); + let mut tail = String::new(); + match &self.body { + Body::Plain { distinct, items } => { + if *distinct { + sql.push_str("distinct "); + } + aliased(&mut sql, items.iter()); + } + Body::Grouped { keys, aggs, having } => { + let outputs = keys + .iter() + .cloned() + .chain(aggs.iter().map(|agg| agg.render(agg.arg.as_deref()))); + aliased(&mut sql, outputs); + if !keys.is_empty() { + let _ = write!(tail, " group by {}", keys.join(", ")); + } + if let Some((agg, op, literal)) = having { + let _ = write!( + tail, + " having {} {op} {literal}", + agg.render(agg.arg.as_deref()) + ); + } + } + } + let _ = write!(sql, " from {}", self.from); + if let (true, Some(predicate)) = (with_predicate, &self.predicate) { + let _ = write!(sql, " where {predicate}"); + } + sql.push_str(&tail); + sql.push_str(&self.order_limit(with_limit)); + sql + } + + /// ORDER BY / LIMIT over the output columns `c0, c1, ...`. + fn order_limit(&self, with_limit: bool) -> String { + let mut sql = String::new(); + if let Some((column, desc)) = self.order_by { + let _ = write!( + sql, + " order by c{column}{}", + if desc { " desc" } else { "" } + ); + } + if let (true, Some((limit, offset))) = (with_limit, self.limit) { + let _ = write!(sql, " limit {limit}"); + if let Some(offset) = offset { + let _ = write!(sql, " offset {offset}"); + } + } + sql + } + /// TLP form: rows are split by `p`, `not p` and `p is null` (exactly one + /// holds per row), recombined with UNION ALL in a derived table, and the + /// rest of the query (DISTINCT, GROUP BY, aggregates, HAVING, ORDER BY, + /// LIMIT) is applied on top. It must return what the query without the + /// WHERE clause returns. + fn render_tlp(&self, predicate: &str, with_limit: bool) -> String { + // Columns each partition exposes, and the outer select over them. + let mut inner = Vec::new(); + let mut outer = String::from("select "); + let mut tail = String::new(); + match &self.body { + Body::Plain { distinct, items } => { + if *distinct { + outer.push_str("distinct "); + } + for (i, item) in items.iter().enumerate() { + inner.push(format!("{item} as x{i}")); + } + aliased(&mut outer, (0..items.len()).map(|i| format!("u.x{i}"))); + } + Body::Grouped { keys, aggs, having } => { + let mut outputs = Vec::new(); + for (i, key) in keys.iter().enumerate() { + inner.push(format!("{key} as x{i}")); + outputs.push(format!("u.x{i}")); + } + let key_refs = outputs.clone(); + // Moves an aggregate's argument into the partitions and + // aggregates the matching derived-table column instead. + let mut retarget = |agg: &Agg| match &agg.arg { + None => agg.render(None), + Some(arg) => { + let column = format!("x{}", inner.len()); + inner.push(format!("{arg} as {column}")); + agg.render(Some(&format!("u.{column}"))) + } + }; + for agg in aggs { + outputs.push(retarget(agg)); + } + if !key_refs.is_empty() { + let _ = write!(tail, " group by {}", key_refs.join(", ")); + } + if let Some((agg, op, literal)) = having { + let _ = write!(tail, " having {} {op} {literal}", retarget(agg)); + } + aliased(&mut outer, outputs.iter()); + } + } + // e.g. `select count(*) ...`: the partitions still need a column. + if inner.is_empty() { + inner.push("1 as x0".to_string()); + } + let columns = inner.join(", "); + let parts = [ + predicate.to_string(), + format!("not ({predicate})"), + format!("({predicate}) is null"), + ] + .map(|p| format!("select {columns} from {} where {p}", self.from)) + .join(" union all "); + format!( + "{outer} from ({parts}) as u{tail}{}", + self.order_limit(with_limit) + ) + } +} + +/// `e0 as c0, e1 as c1, ...` +fn aliased(sql: &mut String, exprs: impl Iterator) { + for (i, expr) in exprs.enumerate() { + let sep = if i == 0 { "" } else { ", " }; + let _ = write!(sql, "{sep}{expr} as c{i}"); + } +} + +struct Gen<'a, 'b> { + u: &'b mut Unstructured<'a>, + // Remaining subqueries for the current query (KiteSQL rejects some mixes). + subqueries: u32, + // KiteSQL only accepts EXISTS / IN subqueries in WHERE. + in_where: bool, + // Numeric expressions stay integral (no `d`, float literals or `/`). + ints_only: bool, + next_alias: u32, +} + +impl<'a, 'b> Gen<'a, 'b> { + fn pick<'x, T: Copy + 'x>( + &mut self, + items: impl IntoIterator, + ) -> arbitrary::Result { + let mut items = items.into_iter(); + let i = self.below(items.clone().count())?; + Ok(*items.nth(i).expect("index below the item count")) + } + + fn below(&mut self, n: usize) -> arbitrary::Result { + // Exhausted input yields 0, which is always the simplest choice. + self.u.choose_index(n) + } + + fn alias(&mut self) -> String { + self.next_alias += 1; + format!("q{}", self.next_alias) + } + + fn flag(&mut self, one_in: usize) -> arbitrary::Result { + // Exhausted input yields false (`Unstructured::ratio` would yield true). + Ok(self.below(one_in)? == 1) + } + + fn column(&mut self, scope: &[String], ty: Ty) -> arbitrary::Result { + let alias = &scope[self.below(scope.len())?]; + let ints_only = self.ints_only; + let name = self.pick( + COLUMNS + .iter() + .filter(|(name, t)| *t == ty && !(ints_only && *name == "d")) + .map(|(name, _)| name), + )?; + Ok(format!("{alias}.{name}")) + } + + /// Runs `f` with subqueries disabled (e.g. inside aggregates or ON). + fn without_subqueries( + &mut self, + f: impl FnOnce(&mut Self) -> arbitrary::Result, + ) -> arbitrary::Result { + let saved = std::mem::replace(&mut self.subqueries, 0); + let result = f(self); + self.subqueries = saved; + result + } + + fn num_expr(&mut self, scope: &[String], depth: u32) -> arbitrary::Result { + let choices = if depth == 0 { 2 } else { 8 }; + let next = depth.saturating_sub(1); + Ok(match self.below(choices)? { + 0 => self.column(scope, Ty::Num)?, + 1 => { + let literals = if self.ints_only { + INT_LITERALS + } else { + NUM_LITERALS + }; + self.pick(literals)?.to_string() + } + 2 => { + let l = self.num_expr(scope, next)?; + let ops = if self.ints_only { + INT_ARITH_OPS + } else { + ARITH_OPS + }; + let op = self.pick(ops)?; + let r = self.num_expr(scope, next)?; + format!("({l} {op} {r})") + } + // The space matters: `--1` would start a comment. + 3 => format!("(- {})", self.num_expr(scope, next)?), + 4 => { + let c = self.bool_expr(scope, next)?; + let t = self.num_expr(scope, next)?; + let e = self.num_expr(scope, next)?; + format!("(case when {c} then {t} else {e} end)") + } + 5 => { + let f = self.pick(&["coalesce", "nullif"])?; + let l = self.num_expr(scope, next)?; + let r = self.num_expr(scope, next)?; + format!("{f}({l}, {r})") + } + 6 => format!("char_length({})", self.str_expr(scope, next)?), + _ => match self.scalar_subquery()? { + Some(subquery) => subquery, + None => self.column(scope, Ty::Num)?, + }, + }) + } + + fn str_expr(&mut self, scope: &[String], depth: u32) -> arbitrary::Result { + let choices = if depth == 0 { 2 } else { 5 }; + let next = depth.saturating_sub(1); + Ok(match self.below(choices)? { + 0 => self.column(scope, Ty::Str)?, + 1 => self.pick(STR_VALUES)?.to_string(), + 2 => { + let f = self.pick(&["lower", "upper"])?; + format!("{f}({})", self.str_expr(scope, next)?) + } + 3 => { + let c = self.bool_expr(scope, next)?; + let t = self.str_expr(scope, next)?; + let e = self.str_expr(scope, next)?; + format!("(case when {c} then {t} else {e} end)") + } + _ => { + let l = self.str_expr(scope, next)?; + let r = self.str_expr(scope, next)?; + format!("coalesce({l}, {r})") + } + }) + } + + fn bool_expr(&mut self, scope: &[String], depth: u32) -> arbitrary::Result { + let choices = if depth == 0 { 3 } else { 10 }; + let next = depth.saturating_sub(1); + Ok(match self.below(choices)? { + 0 => { + let l = self.num_expr(scope, next)?; + let op = self.pick(CMP_OPS)?; + let r = self.num_expr(scope, next)?; + format!("({l} {op} {r})") + } + 1 => { + let l = self.str_expr(scope, next)?; + let op = self.pick(CMP_OPS)?; + let r = self.str_expr(scope, next)?; + format!("({l} {op} {r})") + } + 2 => { + let ty = if self.flag(2)? { Ty::Str } else { Ty::Num }; + let not = if self.flag(2)? { " not" } else { "" }; + format!("({} is{not} null)", self.column(scope, ty)?) + } + 3 => format!("(not {})", self.bool_expr(scope, next)?), + 4 | 5 => { + let l = self.bool_expr(scope, next)?; + let op = self.pick(&["and", "or"])?; + let r = self.bool_expr(scope, next)?; + format!("({l} {op} {r})") + } + 6 => { + let e = self.num_expr(scope, next)?; + let not = if self.flag(2)? { " not" } else { "" }; + let lo = self.pick(NUM_LITERALS)?; + let hi = self.pick(NUM_LITERALS)?; + format!("({e}{not} between {lo} and {hi})") + } + 7 => { + let e = self.num_expr(scope, next)?; + let not = if self.flag(2)? { " not" } else { "" }; + let len = self.below(3)? + 1; + let mut list = Vec::with_capacity(len); + for _ in 0..len { + list.push(self.pick(NUM_LITERALS)?); + } + format!("({e}{not} in ({}))", list.join(", ")) + } + 8 => { + let e = self.str_expr(scope, next)?; + let not = if self.flag(2)? { " not" } else { "" }; + format!("({e}{not} like {})", self.pick(LIKE_PATTERNS)?) + } + _ => match self.subquery_predicate(scope)? { + Some(predicate) => predicate, + None => format!("({} is not null)", self.column(scope, Ty::Num)?), + }, + }) + } + + /// Consumes one unit of the subquery budget; `false` if none is left. + fn take_subquery(&mut self) -> bool { + if self.subqueries == 0 { + return false; + } + self.subqueries -= 1; + true + } + + fn single_table(&mut self) -> arbitrary::Result<(String, String)> { + let table = self.pick(TABLES)?; + let alias = self.alias(); + Ok((format!("{table} as {alias}"), alias)) + } + + /// Uncorrelated `(select agg(expr) from t where p)`. + fn scalar_subquery(&mut self) -> arbitrary::Result> { + if !self.take_subquery() { + return Ok(None); + } + let (from, alias) = self.single_table()?; + let scope = [alias]; + let agg = self.pick(&["count", "sum", "min", "max"])?; + let arg = self.num_expr(&scope, 1)?; + let mut sql = format!("(select {agg}({arg}) from {from}"); + if self.flag(2)? { + let _ = write!(sql, " where {}", self.bool_expr(&scope, 1)?); + } + sql.push(')'); + Ok(Some(sql)) + } + + /// Uncorrelated `[not] exists (...)` or `expr [not] in (select ...)`. + fn subquery_predicate(&mut self, outer: &[String]) -> arbitrary::Result> { + if !self.in_where || !self.take_subquery() { + return Ok(None); + } + let (from, alias) = self.single_table()?; + let scope = [alias]; + let predicate = self.bool_expr(&scope, 1)?; + let not = if self.flag(2)? { "not " } else { "" }; + Ok(Some(if self.flag(2)? { + format!("({not}exists (select 1 from {from} where {predicate}))") + } else { + let e = self.num_expr(outer, 1)?; + let column = self.column(&scope, Ty::Num)?; + format!("({e} {not}in (select {column} from {from} where {predicate}))") + })) + } + + /// `t as q1 [join t as q2 on ...] [join ...]`, at most 3 tables. + fn from(&mut self) -> arbitrary::Result<(String, Vec)> { + let (mut from, alias) = self.single_table()?; + let mut scope = vec![alias]; + for _ in 0..self.below(3)? { + let join = self.pick(JOINS)?; + let (table, alias) = self.single_table()?; + let left = scope[self.below(scope.len())?].clone(); + scope.push(alias.clone()); + let _ = write!(from, " {join} {table}"); + if join != "cross join" { + // Bias towards equi-joins so hash joins are exercised. + let on = if self.flag(3)? { + self.without_subqueries(|g| g.bool_expr(&scope, 1))? + } else { + let l = self.pick(&["id", "a", "b"])?; + let r = self.pick(&["id", "a", "b"])?; + format!("{left}.{l} = {alias}.{r}") + }; + let _ = write!(from, " on {on}"); + } + } + Ok((from, scope)) + } + + fn aggregate(&mut self, scope: &[String]) -> arbitrary::Result { + Ok(match self.below(3)? { + 0 => Agg { + func: "count", + distinct: false, + arg: None, + }, + 1 => Agg { + func: "count", + distinct: true, + arg: Some(self.column(scope, Ty::Num)?), + }, + _ => { + let func = self.pick(AGGS)?; + // Integral arguments keep `sum` / `avg` exact whatever the row + // order, which differs between a query and its TLP form. + let saved = std::mem::replace(&mut self.ints_only, matches!(func, "sum" | "avg")); + let arg = self.without_subqueries(|g| g.num_expr(scope, 1)); + self.ints_only = saved; + Agg { + func, + distinct: false, + arg: Some(arg?), + } + } + }) + } + + fn select(&mut self) -> arbitrary::Result