Merge branch 'main' into ext-static-feature

This commit is contained in:
Jussi Saurio 2025-05-03 19:18:28 +03:00 committed by GitHub
commit 4e05023bd3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
166 changed files with 18841 additions and 6029 deletions

View file

@ -6,10 +6,10 @@ runs:
steps:
- name: Install SQLite
env:
SQLITE_VERSION: "3470200"
YEAR: 2024
SQLITE_VERSION: "3490100"
YEAR: 2025
run: |
curl -o /tmp/sqlite.zip https://www.sqlite.org/$YEAR/sqlite-tools-linux-x64-$SQLITE_VERSION.zip > /dev/null
curl -o /tmp/sqlite.zip https://sqlite.org/$YEAR/sqlite-tools-linux-x64-$SQLITE_VERSION.zip > /dev/null
unzip -j /tmp/sqlite.zip sqlite3 -d /usr/local/bin/
sqlite3 --version
shell: bash

View file

@ -1,10 +1,12 @@
# Copyright 2022-2024, axodotdev
# This file was autogenerated by dist: https://github.com/astral-sh/cargo-dist
#
# Copyright 2025 Astral Software Inc.
# SPDX-License-Identifier: MIT or Apache-2.0
#
# CI that:
#
# * checks for a Git Tag that looks like a release
# * builds artifacts with cargo-dist (archives, installers, hashes)
# * builds artifacts with dist (archives, installers, hashes)
# * uploads those artifacts to temporary workflow zip
# * on success, uploads the artifacts to a GitHub Release
#
@ -24,10 +26,10 @@ permissions:
# must be a Cargo-style SemVer Version (must have at least major.minor.patch).
#
# If PACKAGE_NAME is specified, then the announcement will be for that
# package (erroring out if it doesn't have the given version or isn't cargo-dist-able).
# package (erroring out if it doesn't have the given version or isn't dist-able).
#
# If PACKAGE_NAME isn't specified, then the announcement will be for all
# (cargo-dist-able) packages in the workspace with that version (this mode is
# (dist-able) packages in the workspace with that version (this mode is
# intended for workspaces with only one dist-able package, or with all dist-able
# packages versioned/released in lockstep).
#
@ -45,9 +47,9 @@ on:
- '**[0-9]+.[0-9]+.[0-9]+*'
jobs:
# Run 'cargo dist plan' (or host) to determine what tasks we need to do
# Run 'dist plan' (or host) to determine what tasks we need to do
plan:
runs-on: "ubuntu-20.04"
runs-on: "ubuntu-22.04"
outputs:
val: ${{ steps.plan.outputs.manifest }}
tag: ${{ !github.event.pull_request && github.ref_name || '' }}
@ -59,16 +61,16 @@ jobs:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install cargo-dist
- name: Install dist
# we specify bash to get pipefail; it guards against the `curl` command
# failing. otherwise `sh` won't catch that `curl` returned non-0
shell: bash
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.21.0/cargo-dist-installer.sh | sh"
- name: Cache cargo-dist
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/cargo-dist/releases/download/v0.28.3/cargo-dist-installer.sh | sh"
- name: Cache dist
uses: actions/upload-artifact@v4
with:
name: cargo-dist-cache
path: ~/.cargo/bin/cargo-dist
path: ~/.cargo/bin/dist
# sure would be cool if github gave us proper conditionals...
# so here's a doubly-nested ternary-via-truthiness to try to provide the best possible
# functionality based on whether this is a pull_request, and whether it's from a fork.
@ -76,8 +78,8 @@ jobs:
# but also really annoying to build CI around when it needs secrets to work right.)
- id: plan
run: |
cargo dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json
echo "cargo dist ran successfully"
dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json
echo "dist ran successfully"
cat plan-dist-manifest.json
echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT"
- name: "Upload dist-manifest.json"
@ -95,18 +97,19 @@ jobs:
if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }}
strategy:
fail-fast: false
# Target platforms/runners are computed by cargo-dist in create-release.
# Target platforms/runners are computed by dist in create-release.
# Each member of the matrix has the following arguments:
#
# - runner: the github runner
# - dist-args: cli flags to pass to cargo dist
# - install-dist: expression to run to install cargo-dist on the runner
# - dist-args: cli flags to pass to dist
# - install-dist: expression to run to install dist on the runner
#
# Typically there will be:
# - 1 "global" task that builds universal installers
# - N "local" tasks that build each platform's binaries and platform-specific installers
matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }}
runs-on: ${{ matrix.runner }}
container: ${{ matrix.container && matrix.container.image || null }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json
@ -117,8 +120,15 @@ jobs:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install cargo-dist
run: ${{ matrix.install_dist }}
- name: Install Rust non-interactively if not already installed
if: ${{ matrix.container }}
run: |
if ! command -v cargo > /dev/null 2>&1; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
fi
- name: Install dist
run: ${{ matrix.install_dist.run }}
# Get the dist-manifest
- name: Fetch local artifacts
uses: actions/download-artifact@v4
@ -132,10 +142,10 @@ jobs:
- name: Build artifacts
run: |
# Actually do builds and make zips and whatnot
cargo dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
echo "cargo dist ran successfully"
dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
echo "dist ran successfully"
- name: Attest
uses: actions/attest-build-provenance@v1
uses: actions/attest-build-provenance@v2
with:
subject-path: "target/distrib/*${{ join(matrix.targets, ', ') }}*"
- id: cargo-dist
@ -147,7 +157,7 @@ jobs:
run: |
# Parse out what we just built and upload it to scratch storage
echo "paths<<EOF" >> "$GITHUB_OUTPUT"
jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT"
dist print-upload-files-from-manifest --manifest dist-manifest.json >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
@ -164,7 +174,7 @@ jobs:
needs:
- plan
- build-local-artifacts
runs-on: "ubuntu-20.04"
runs-on: "ubuntu-22.04"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json
@ -172,12 +182,12 @@ jobs:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install cached cargo-dist
- name: Install cached dist
uses: actions/download-artifact@v4
with:
name: cargo-dist-cache
path: ~/.cargo/bin/
- run: chmod +x ~/.cargo/bin/cargo-dist
- run: chmod +x ~/.cargo/bin/dist
# Get all the local artifacts for the global tasks to use (for e.g. checksums)
- name: Fetch local artifacts
uses: actions/download-artifact@v4
@ -188,8 +198,8 @@ jobs:
- id: cargo-dist
shell: bash
run: |
cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json
echo "cargo dist ran successfully"
dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json
echo "dist ran successfully"
# Parse out what we just built and upload it to scratch storage
echo "paths<<EOF" >> "$GITHUB_OUTPUT"
@ -214,19 +224,19 @@ jobs:
if: ${{ always() && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
runs-on: "ubuntu-20.04"
runs-on: "ubuntu-22.04"
outputs:
val: ${{ steps.host.outputs.manifest }}
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install cached cargo-dist
- name: Install cached dist
uses: actions/download-artifact@v4
with:
name: cargo-dist-cache
path: ~/.cargo/bin/
- run: chmod +x ~/.cargo/bin/cargo-dist
- run: chmod +x ~/.cargo/bin/dist
# Fetch artifacts from scratch-storage
- name: Fetch artifacts
uses: actions/download-artifact@v4
@ -237,7 +247,7 @@ jobs:
- id: host
shell: bash
run: |
cargo dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json
dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json
echo "artifacts uploaded and released successfully"
cat dist-manifest.json
echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT"
@ -278,7 +288,7 @@ jobs:
# still allowing individual publish jobs to skip themselves (for prereleases).
# "host" however must run to completion, no skipping allowed!
if: ${{ always() && needs.host.result == 'success' }}
runs-on: "ubuntu-20.04"
runs-on: "ubuntu-22.04"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:

View file

@ -75,6 +75,18 @@ jobs:
curl -L $LINK/$CARGO_C_FILE | tar xz -C ~/.cargo/bin
- uses: actions/checkout@v3
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
- name: Set up Python
run: uv python install
- name: Install the project
run: uv sync --all-extras --dev --all-packages
- uses: "./.github/shared/install_sqlite"
- name: Test
run: make test

2
.gitignore vendored
View file

@ -34,3 +34,5 @@ dist/
# testing
testing/limbo_output.txt
**/limbo_output.txt
testing/*.log
.bugbase

1
.python-version Normal file
View file

@ -0,0 +1 @@
3.13

View file

@ -1,5 +1,91 @@
# Changelog
## 0.0.19 - 2025-04-16
### Added
* Add `BeginSubrtn`, `NotFound` and `Affinity` bytecodes (Diego Reis)
* Add Ansi Colors to tcl test runner (Pedro Muniz)
* support modifiers for julianday() (meteorgan)
* Implement Once and OpenAutoindex opcodes (Jussi Saurio)
* Add support for OpenEphemeral bytecode (Diego Reis)
* simulator: Add Bug Database(BugBase) (Alperen Keleş)
* feat: Add timediff data and time function (Sachin Kumar Singh)
* core/btree: Add PageContent::new() helper (Pekka Enberg)
* Add support to load log file with stress test (Pere Diaz Bou)
* Support UPDATE for virtual tables (Preston Thorpe)
* Add `.timer` command to print SQL execution statistics (Pere Diaz Bou)
* Strict table support (Ihor Andrianov)
* Support backwards index scan and seeks + utilize indexes in removing ORDER BY (Jussi Saurio)
* Add deterministic Clock (Avinash Sajjanshetty)
* Support offset clause in Update queries (Preston Thorpe)
* Support Create Index (Preston Thorpe)
* Support insert default values syntax (Preston Thorpe)
* Add support for default values in INSERT statements (Diego Reis)
### Updated
* Test: write tests for file backed db (Pedro Muniz)
* btree: move some blocks of code to more reasonable places (Jussi Saurio)
* Parse hex integers 2 (Anton Harniakou)
* More index utils (Jussi Saurio)
* Index utils (Jussi Saurio)
* Feature: VDestroy for Dropping Virtual Tables (Pedro Muniz)
* Feat balance shallower (Lâm Hoàng Phúc)
* Parse hexidecimal integers (Anton Harniakou)
* Code clean-ups (Diego Reis)
* Return null when parameter is unbound (Levy A.)
* Enhance robusteness of optimization for Binary expressions (Diego Reis)
* Check that index seek key members are not null (Jussi Saurio)
* Better diagnostics (Pedro Muniz)
* simulator: provide high level commands on top of a single runner (Alperen Keleş)
* build(deps-dev): bump vite from 6.0.7 to 6.2.6 in /bindings/wasm/test-limbo-pkg (dependabot[bot])
* btree: remove IterationState (Jussi Saurio)
* build(deps): bump pyo3 from 0.24.0 to 0.24.1 (dependabot[bot])
* Multi column indexes + index seek refactor (Jussi Saurio)
* Emit ANSI codes only when tracing is outputting to terminal (Preston Thorpe)
* B-Tree code cleanups (Pekka Enberg)
* btree index selection on rightmost pointer in `balance_non_root` (Pere Diaz Bou)
* io/linux: make syscallio the default (io_uring is really slow) (Jussi Saurio)
* Stress improvements (Pekka Enberg)
* VDBE code cleanups (Pekka Enberg)
* Memory tests to track large blob insertions (Pedro Muniz)
* Setup tracing to allow output during test runs (Preston Thorpe)
* allow insertion of multiple overflow cells (Pere Diaz Bou)
* Properly handle insertion of indexed columns (Preston Thorpe)
* VTabs: Proper handling of re-opened db files without the relevant extensions loaded (Preston Thorpe)
* Account divider cell in size while distributing cells (Pere Diaz Bou)
* Format infinite float as "Inf"/"-Inf" (jachewz)
* update sqlite download version to 2025 + remove www. (Pere Diaz Bou)
* Improve validation of btree balancing (Pere Diaz Bou)
* Aggregation without group by produces incorrect results for scalars (Ihor Andrianov)
* Dot command completion (Pedro Muniz)
* Allow reading altered tables by defaulting to null in Column insn (Preston Thorpe)
* docs(readme): update discord link (Jamie Barton)
* More VDBE cleanups (Pekka Enberg)
* Request load page on `insert_into_page` (Pere Diaz Bou)
* core/vdbe: Rename execute_insn_* to op_* (Pekka Enberg)
* Remove RWLock from Shared wal state (Pere Diaz Bou)
* VDBE with indirect function dispatch (Pere Diaz Bou)
### Fixed
* Fix truncation of error output in tests (Pedro Muniz)
* Fix Unary Negate Operation on Blobs (Pedro Muniz)
* Fix incompatibility `AND` Operation (Pedro Muniz)
* Fix: comment out incorrect assert in fuzz (Pedro Muniz)
* Fix two issues with indexes (Jussi Saurio)
* Fuzz fix some operations (Pedro Muniz)
* simulator: updates to bug base, refactors (Alperen Keleş)
* Fix overwrite cell with size less than cell size (Pere Diaz Bou)
* Fix `EXPLAIN` to be case insensitive (Pedro Muniz)
* core: Fix syscall VFS on Linux (Pekka Enberg)
* Index insert fixes (Pere Diaz Bou)
* Decrease page count on balancing fixes (Pere Diaz Bou)
* Remainder fixes (jachewz)
* Fix virtual table translation issues (Preston Thorpe)
* Fix overflow position in write_page() (Lâm Hoàng Phúc)
## 0.0.18 - 2025-04-02
### Added

View file

@ -4,6 +4,8 @@ This document describes the compatibility of Limbo with SQLite.
## Table of contents
- [Limbo compatibility with SQLite](#limbo-compatibility-with-sqlite)
- [Table of contents](#table-of-contents)
- [Overview](#overview)
- [Features](#features)
- [Limitations](#limitations)
@ -41,7 +43,6 @@ Limbo aims to be fully compatible with SQLite, with opt-in features not supporte
* ⛔️ Concurrent access from multiple processes is not supported.
* ⛔️ Savepoints are not supported.
* ⛔️ Triggers are not supported.
* ⛔️ Indexes are not supported.
* ⛔️ Views are not supported.
* ⛔️ Vacuum is not supported.
@ -56,15 +57,16 @@ Limbo aims to be fully compatible with SQLite, with opt-in features not supporte
| ATTACH DATABASE | No | |
| BEGIN TRANSACTION | Partial | Transaction names are not supported. |
| COMMIT TRANSACTION | Partial | Transaction names are not supported. |
| CREATE INDEX | No | |
| CREATE INDEX | Yes | |
| CREATE TABLE | Partial | |
| CREATE TABLE ... STRICT | Yes | |
| CREATE TRIGGER | No | |
| CREATE VIEW | No | |
| CREATE VIRTUAL TABLE | No | |
| CREATE VIRTUAL TABLE | Yes | |
| DELETE | Yes | |
| DETACH DATABASE | No | |
| DROP INDEX | No | |
| DROP TABLE | No | |
| DROP TABLE | Yes | |
| DROP TRIGGER | No | |
| DROP VIEW | No | |
| END TRANSACTION | Partial | Alias for `COMMIT TRANSACTION` |
@ -198,7 +200,7 @@ Feature support of [sqlite expr syntax](https://www.sqlite.org/lang_expr.html).
| (NOT) MATCH | No | |
| IS (NOT) | Yes | |
| IS (NOT) DISTINCT FROM | Yes | |
| (NOT) BETWEEN ... AND ... | No | |
| (NOT) BETWEEN ... AND ... | Yes | Expression is rewritten in the optimizer |
| (NOT) IN (subquery) | No | |
| (NOT) EXISTS (subquery) | No | |
| CASE WHEN THEN ELSE END | Yes | |
@ -226,8 +228,8 @@ Feature support of [sqlite expr syntax](https://www.sqlite.org/lang_expr.html).
| length(X) | Yes | |
| like(X,Y) | Yes | |
| like(X,Y,Z) | Yes | |
| likelihood(X,Y) | No | |
| likely(X) | No | |
| likelihood(X,Y) | Yes | |
| likely(X) | Yes | |
| load_extension(X) | Yes | sqlite3 extensions not yet supported |
| load_extension(X,Y) | No | |
| lower(X) | Yes | |
@ -325,10 +327,10 @@ Feature support of [sqlite expr syntax](https://www.sqlite.org/lang_expr.html).
| date() | Yes | partially supports modifiers |
| time() | Yes | partially supports modifiers |
| datetime() | Yes | partially supports modifiers |
| julianday() | Partial | does not support modifiers |
| julianday() | Yes | partially support modifiers |
| unixepoch() | Partial | does not support modifiers |
| strftime() | Yes | partially supports modifiers |
| timediff() | No | |
| timediff() | Yes | partially supports modifiers |
Modifiers:
@ -425,6 +427,7 @@ Modifiers:
| BitNot | Yes | |
| BitOr | Yes | |
| Blob | Yes | |
| BeginSubrtn | Yes | |
| Checkpoint | No | |
| Clear | No | |
| Close | No | |
@ -460,7 +463,7 @@ Modifiers:
| HaltIfNull | No | |
| IdxDelete | No | |
| IdxGE | Yes | |
| IdxInsert | No | |
| IdxInsert | Yes | |
| IdxLE | Yes | |
| IdxLT | Yes | |
| IdxRowid | No | |
@ -472,9 +475,7 @@ Modifiers:
| IncrVacuum | No | |
| Init | Yes | |
| InitCoroutine | Yes | |
| Insert | No | |
| InsertAsync | Yes | |
| InsertAwait | Yes | |
| Insert | Yes | |
| InsertInt | No | |
| Int64 | No | |
| Integer | Yes | |
@ -495,9 +496,7 @@ Modifiers:
| MustBeInt | Yes | |
| Ne | Yes | |
| NewRowid | Yes | |
| Next | No | |
| NextAsync | Yes | |
| NextAwait | Yes | |
| Next | Yes | |
| Noop | Yes | |
| Not | Yes | |
| NotExists | Yes | |
@ -505,23 +504,18 @@ Modifiers:
| NotNull | Yes | |
| Null | Yes | |
| NullRow | Yes | |
| Once | No | |
| OpenAutoindex | No | |
| OpenEphemeral | No | |
| Once | Yes | |
| OpenAutoindex | Yes | |
| OpenEphemeral | Yes | |
| OpenPseudo | Yes | |
| OpenRead | Yes | |
| OpenReadAsync | Yes | |
| OpenWrite | No | |
| OpenWriteAsync | Yes | |
| OpenWriteAwait | Yes | |
| OpenWrite | Yes | |
| Or | Yes | |
| Pagecount | Partial| no temp databases |
| Param | No | |
| ParseSchema | No | |
| Permutation | No | |
| Prev | No | |
| PrevAsync | Yes | |
| PrevAwait | Yes | |
| Prev | Yes | |
| Program | No | |
| ReadCookie | Partial| no temp databases, only user_version supported |
| Real | Yes | |
@ -531,8 +525,6 @@ Modifiers:
| ResultRow | Yes | |
| Return | Yes | |
| Rewind | Yes | |
| RewindAsync | Yes | |
| RewindAwait | Yes | |
| RowData | No | |
| RowId | Yes | |
| RowKey | No | |
@ -548,6 +540,7 @@ Modifiers:
| SeekLe | No | |
| SeekLt | No | |
| SeekRowid | Yes | |
| SeekEnd | Yes | |
| Sequence | No | |
| SetCookie | No | |
| ShiftLeft | Yes | |
@ -574,10 +567,10 @@ Modifiers:
| VBegin | No | |
| VColumn | Yes | |
| VCreate | Yes | |
| VDestroy | No | |
| VDestroy | Yes | |
| VFilter | Yes | |
| VNext | Yes | |
| VOpen | Yes |VOpenAsync|
| VOpen | Yes | |
| VRename | No | |
| VUpdate | Yes | |
| Vacuum | No | |

224
Cargo.lock generated
View file

@ -344,6 +344,7 @@ dependencies = [
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"wasm-bindgen",
"windows-link",
]
@ -397,6 +398,18 @@ dependencies = [
"strsim",
]
[[package]]
name = "clap_complete"
version = "4.5.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c06f5378ea264ad4f82bbc826628b5aad714a75abf6ece087e923010eb937fb6"
dependencies = [
"clap",
"clap_lex",
"is_executable",
"shlex",
]
[[package]]
name = "clap_derive"
version = "4.5.32"
@ -499,7 +512,7 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "core_tester"
version = "0.0.19-pre.4"
version = "0.0.19"
dependencies = [
"anyhow",
"assert_cmd",
@ -571,6 +584,15 @@ dependencies = [
"itertools",
]
[[package]]
name = "crossbeam-channel"
version = "0.5.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.6"
@ -711,7 +733,16 @@ version = "5.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
dependencies = [
"dirs-sys",
"dirs-sys 0.4.1",
]
[[package]]
name = "dirs"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
dependencies = [
"dirs-sys 0.5.0",
]
[[package]]
@ -722,10 +753,22 @@ checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
dependencies = [
"libc",
"option-ext",
"redox_users",
"redox_users 0.4.6",
"windows-sys 0.48.0",
]
[[package]]
name = "dirs-sys"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
dependencies = [
"libc",
"option-ext",
"redox_users 0.5.0",
"windows-sys 0.59.0",
]
[[package]]
name = "displaydoc"
version = "0.2.5"
@ -868,7 +911,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78"
dependencies = [
"cfg-if",
"rustix 1.0.3",
"rustix 1.0.7",
"windows-sys 0.59.0",
]
@ -1369,11 +1412,12 @@ dependencies = [
[[package]]
name = "io-uring"
version = "0.6.4"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "595a0399f411a508feb2ec1e970a4a30c249351e30208960d58298de8660b0e5"
checksum = "3c2f96dfbc20c12b9b4f12eef60472d8c29b9c3f29463570dcb47e4a48551168"
dependencies = [
"bitflags 1.3.2",
"bitflags 2.9.0",
"cfg-if",
"libc",
]
@ -1400,6 +1444,15 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45"
[[package]]
name = "is_executable"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4a1b5bad6f9072935961dfbf1cced2f3d129963d091b6f69f007fe04e758ae2"
dependencies = [
"winapi",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
@ -1488,9 +1541,9 @@ dependencies = [
[[package]]
name = "julian_day_converter"
version = "0.4.4"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aa5652b85ab018289638c6b924db618da9edd2ddfff7fa0ec38a8b51a9192d3"
checksum = "f2987f71b89b85c812c8484cbf0c5d7912589e77bfdc66fd3e52f760e7859f16"
dependencies = [
"chrono",
]
@ -1523,9 +1576,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.171"
version = "0.2.172"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
[[package]]
name = "libgit2-sys"
@ -1557,9 +1610,9 @@ checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
[[package]]
name = "libmimalloc-sys"
version = "0.1.40"
version = "0.1.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07d0e07885d6a754b9c7993f2625187ad694ee985d60f23355ff0e7077261502"
checksum = "ec9d6fac27761dabcd4ee73571cdb06b7022dc99089acbe5435691edffaac0f4"
dependencies = [
"cc",
"libc",
@ -1601,7 +1654,7 @@ dependencies = [
[[package]]
name = "limbo"
version = "0.0.19-pre.4"
version = "0.0.19"
dependencies = [
"limbo_core",
"thiserror 2.0.12",
@ -1610,14 +1663,14 @@ dependencies = [
[[package]]
name = "limbo-go"
version = "0.0.19-pre.4"
version = "0.0.19"
dependencies = [
"limbo_core",
]
[[package]]
name = "limbo-java"
version = "0.0.19-pre.4"
version = "0.0.19"
dependencies = [
"jni",
"limbo_core",
@ -1626,7 +1679,7 @@ dependencies = [
[[package]]
name = "limbo-wasm"
version = "0.0.19-pre.4"
version = "0.0.19"
dependencies = [
"console_error_panic_hook",
"getrandom 0.2.15",
@ -1639,28 +1692,32 @@ dependencies = [
[[package]]
name = "limbo_cli"
version = "0.0.19-pre.4"
version = "0.0.19"
dependencies = [
"anyhow",
"cfg-if",
"clap",
"clap_complete",
"comfy-table",
"csv",
"ctrlc",
"dirs",
"dirs 5.0.1",
"env_logger 0.10.2",
"libc",
"limbo_core",
"miette",
"nu-ansi-term 0.50.1",
"rustyline",
"shlex",
"syntect",
"tracing",
"tracing-appender",
"tracing-subscriber",
]
[[package]]
name = "limbo_completion"
version = "0.0.19-pre.4"
version = "0.0.19"
dependencies = [
"limbo_ext",
"mimalloc",
@ -1668,8 +1725,9 @@ dependencies = [
[[package]]
name = "limbo_core"
version = "0.0.19-pre.4"
version = "0.0.19"
dependencies = [
"bitflags 2.9.0",
"built",
"cfg_block",
"chrono",
@ -1710,7 +1768,7 @@ dependencies = [
"regex-syntax 0.8.5",
"rstest",
"rusqlite",
"rustix 0.38.44",
"rustix 1.0.7",
"ryu",
"strum",
"tempfile",
@ -1721,7 +1779,7 @@ dependencies = [
[[package]]
name = "limbo_crypto"
version = "0.0.19-pre.4"
version = "0.0.19"
dependencies = [
"blake3",
"data-encoding",
@ -1734,7 +1792,7 @@ dependencies = [
[[package]]
name = "limbo_ext"
version = "0.0.19-pre.4"
version = "0.0.19"
dependencies = [
"chrono",
"getrandom 0.3.2",
@ -1743,7 +1801,7 @@ dependencies = [
[[package]]
name = "limbo_ext_tests"
version = "0.0.19-pre.4"
version = "0.0.19"
dependencies = [
"env_logger 0.11.7",
"lazy_static",
@ -1754,7 +1812,7 @@ dependencies = [
[[package]]
name = "limbo_ipaddr"
version = "0.0.19-pre.4"
version = "0.0.19"
dependencies = [
"ipnetwork",
"limbo_ext",
@ -1763,7 +1821,7 @@ dependencies = [
[[package]]
name = "limbo_macros"
version = "0.0.19-pre.4"
version = "0.0.19"
dependencies = [
"proc-macro2",
"quote",
@ -1772,7 +1830,7 @@ dependencies = [
[[package]]
name = "limbo_node"
version = "0.0.19-pre.4"
version = "0.0.19"
dependencies = [
"limbo_core",
"napi",
@ -1782,7 +1840,7 @@ dependencies = [
[[package]]
name = "limbo_percentile"
version = "0.0.19-pre.4"
version = "0.0.19"
dependencies = [
"limbo_ext",
"mimalloc",
@ -1790,7 +1848,7 @@ dependencies = [
[[package]]
name = "limbo_regexp"
version = "0.0.19-pre.4"
version = "0.0.19"
dependencies = [
"limbo_ext",
"mimalloc",
@ -1799,7 +1857,7 @@ dependencies = [
[[package]]
name = "limbo_series"
version = "0.0.19-pre.4"
version = "0.0.19"
dependencies = [
"limbo_ext",
"mimalloc",
@ -1809,10 +1867,12 @@ dependencies = [
[[package]]
name = "limbo_sim"
version = "0.0.19-pre.4"
version = "0.0.19"
dependencies = [
"anarchist-readable-name-generator-lib",
"chrono",
"clap",
"dirs 6.0.0",
"env_logger 0.10.2",
"limbo_core",
"log",
@ -1824,12 +1884,11 @@ dependencies = [
"rusqlite",
"serde",
"serde_json",
"tempfile",
]
[[package]]
name = "limbo_sqlite3"
version = "0.0.19-pre.4"
version = "0.0.19"
dependencies = [
"env_logger 0.11.7",
"libc",
@ -1839,7 +1898,7 @@ dependencies = [
[[package]]
name = "limbo_sqlite3_parser"
version = "0.0.19-pre.4"
version = "0.0.19"
dependencies = [
"bitflags 2.9.0",
"cc",
@ -1859,18 +1918,22 @@ dependencies = [
[[package]]
name = "limbo_stress"
version = "0.0.19-pre.4"
version = "0.0.19"
dependencies = [
"anarchist-readable-name-generator-lib",
"antithesis_sdk",
"clap",
"hex",
"limbo",
"serde_json",
"tokio",
"tracing",
"tracing-appender",
"tracing-subscriber",
]
[[package]]
name = "limbo_time"
version = "0.0.19-pre.4"
version = "0.0.19"
dependencies = [
"chrono",
"limbo_ext",
@ -1882,7 +1945,7 @@ dependencies = [
[[package]]
name = "limbo_uuid"
version = "0.0.19-pre.4"
version = "0.0.19"
dependencies = [
"limbo_ext",
"mimalloc",
@ -1951,9 +2014,9 @@ checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "lru"
version = "0.13.0"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "227748d55f2f0ab4735d87fd623798cb6b664512fe979705f829c9f81c934465"
checksum = "9f8cc7106155f10bdf99a6f379688f543ad6596a415375b36a59a054ceda1198"
dependencies = [
"hashbrown",
]
@ -1999,9 +2062,9 @@ dependencies = [
[[package]]
name = "miette"
version = "7.5.0"
version = "7.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a955165f87b37fd1862df2a59547ac542c77ef6d17c666f619d1ad22dd89484"
checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7"
dependencies = [
"backtrace",
"backtrace-ext",
@ -2013,15 +2076,14 @@ dependencies = [
"supports-unicode",
"terminal_size",
"textwrap",
"thiserror 1.0.69",
"unicode-width 0.1.14",
]
[[package]]
name = "miette-derive"
version = "7.5.0"
version = "7.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf45bf44ab49be92fd1227a3be6fc6f617f1a337c06af54981048574d8783147"
checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b"
dependencies = [
"proc-macro2",
"quote",
@ -2030,9 +2092,9 @@ dependencies = [
[[package]]
name = "mimalloc"
version = "0.1.44"
version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99585191385958383e13f6b822e6b6d8d9cf928e7d286ceb092da92b43c87bc1"
checksum = "995942f432bbb4822a7e9c3faa87a695185b0d09273ba85f097b54f4e458f2af"
dependencies = [
"libmimalloc-sys",
]
@ -2238,9 +2300,9 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.21.1"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "onig"
@ -2521,7 +2583,7 @@ dependencies = [
[[package]]
name = "py-limbo"
version = "0.0.19-pre.4"
version = "0.0.19"
dependencies = [
"anyhow",
"limbo_core",
@ -2532,9 +2594,9 @@ dependencies = [
[[package]]
name = "pyo3"
version = "0.24.0"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f1c6c3591120564d64db2261bec5f910ae454f01def849b9c22835a84695e86"
checksum = "17da310086b068fbdcefbba30aeb3721d5bb9af8db4987d6735b2183ca567229"
dependencies = [
"anyhow",
"cfg-if",
@ -2551,9 +2613,9 @@ dependencies = [
[[package]]
name = "pyo3-build-config"
version = "0.24.0"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9b6c2b34cf71427ea37c7001aefbaeb85886a074795e35f161f5aecc7620a7a"
checksum = "e27165889bd793000a098bb966adc4300c312497ea25cf7a690a9f0ac5aa5fc1"
dependencies = [
"once_cell",
"target-lexicon",
@ -2561,9 +2623,9 @@ dependencies = [
[[package]]
name = "pyo3-ffi"
version = "0.24.0"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5507651906a46432cdda02cd02dd0319f6064f1374c9147c45b978621d2c3a9c"
checksum = "05280526e1dbf6b420062f3ef228b78c0c54ba94e157f5cb724a609d0f2faabc"
dependencies = [
"libc",
"pyo3-build-config",
@ -2571,9 +2633,9 @@ dependencies = [
[[package]]
name = "pyo3-macros"
version = "0.24.0"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0d394b5b4fd8d97d48336bb0dd2aebabad39f1d294edd6bcd2cccf2eefe6f42"
checksum = "5c3ce5686aa4d3f63359a5100c62a127c9f15e8398e5fdeb5deef1fed5cd5f44"
dependencies = [
"proc-macro2",
"pyo3-macros-backend",
@ -2583,9 +2645,9 @@ dependencies = [
[[package]]
name = "pyo3-macros-backend"
version = "0.24.0"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd72da09cfa943b1080f621f024d2ef7e2773df7badd51aa30a2be1f8caa7c8e"
checksum = "f4cf6faa0cbfb0ed08e89beb8103ae9724eb4750e3a78084ba4017cbe94f3855"
dependencies = [
"heck",
"proc-macro2",
@ -2759,6 +2821,17 @@ dependencies = [
"thiserror 1.0.69",
]
[[package]]
name = "redox_users"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
dependencies = [
"getrandom 0.2.15",
"libredox",
"thiserror 2.0.12",
]
[[package]]
name = "regex"
version = "1.11.1"
@ -2928,9 +3001,9 @@ dependencies = [
[[package]]
name = "rustix"
version = "1.0.3"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e56a18552996ac8d29ecc3b190b4fdbb2d91ca4ec396de7bbffaf43f3d637e96"
checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
dependencies = [
"bitflags 2.9.0",
"errno",
@ -3093,6 +3166,15 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "sqlparser_bench"
version = "0.1.0"
dependencies = [
"criterion",
"fallible-iterator",
"limbo_sqlite3_parser",
]
[[package]]
name = "stable_deref_trait"
version = "1.2.0"
@ -3247,7 +3329,7 @@ dependencies = [
"fastrand",
"getrandom 0.3.2",
"once_cell",
"rustix 1.0.3",
"rustix 1.0.7",
"windows-sys 0.59.0",
]
@ -3266,7 +3348,7 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed"
dependencies = [
"rustix 1.0.3",
"rustix 1.0.7",
"windows-sys 0.59.0",
]
@ -3449,6 +3531,18 @@ dependencies = [
"tracing-core",
]
[[package]]
name = "tracing-appender"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf"
dependencies = [
"crossbeam-channel",
"thiserror 1.0.69",
"time",
"tracing-subscriber",
]
[[package]]
name = "tracing-attributes"
version = "0.1.28"

View file

@ -25,56 +25,31 @@ members = [
"sqlite3",
"stress",
"tests",
"vendored/sqlite3-parser/sqlparser_bench",
]
exclude = ["perf/latency/limbo"]
[workspace.package]
version = "0.0.19-pre.4"
version = "0.0.19"
authors = ["the Limbo authors"]
edition = "2021"
license = "MIT"
repository = "https://github.com/tursodatabase/limbo"
[workspace.dependencies]
limbo_completion = { path = "extensions/completion", version = "0.0.19-pre.4" }
limbo_core = { path = "core", version = "0.0.19-pre.4" }
limbo_crypto = { path = "extensions/crypto", version = "0.0.19-pre.4" }
limbo_ext = { path = "extensions/core", version = "0.0.19-pre.4" }
limbo_ext_tests = { path = "extensions/tests", version = "0.0.19-pre.4" }
limbo_ipaddr = { path = "extensions/ipaddr", version = "0.0.19-pre.4" }
limbo_macros = { path = "macros", version = "0.0.19-pre.4" }
limbo_percentile = { path = "extensions/percentile", version = "0.0.19-pre.4" }
limbo_regexp = { path = "extensions/regexp", version = "0.0.19-pre.4" }
limbo_series = { path = "extensions/series", version = "0.0.19-pre.4" }
limbo_sqlite3_parser = { path = "vendored/sqlite3-parser", version = "0.0.19-pre.4" }
limbo_time = { path = "extensions/time", version = "0.0.19-pre.4" }
limbo_uuid = { path = "extensions/uuid", version = "0.0.19-pre.4" }
# Config for 'cargo dist'
[workspace.metadata.dist]
# The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax)
cargo-dist-version = "0.21.0"
# CI backends to support
ci = "github"
# The installers to generate for each app
installers = ["shell", "powershell"]
# Target platforms to build apps for (Rust target-triple syntax)
targets = [
"aarch64-apple-darwin",
"x86_64-apple-darwin",
"x86_64-unknown-linux-gnu",
"x86_64-pc-windows-msvc",
]
# Which actions to run on pull requests
pr-run-mode = "plan"
# Path that installers should place binaries in
install-path = "~/.limbo"
# Whether to install an updater program
install-updater = true
# Whether to consider the binaries in a package for distribution (defaults true)
dist = false
# Whether to enable GitHub Attestations
github-attestations = true
limbo_completion = { path = "extensions/completion", version = "0.0.19" }
limbo_core = { path = "core", version = "0.0.19" }
limbo_crypto = { path = "extensions/crypto", version = "0.0.19" }
limbo_ext = { path = "extensions/core", version = "0.0.19" }
limbo_ext_tests = { path = "extensions/tests", version = "0.0.19" }
limbo_ipaddr = { path = "extensions/ipaddr", version = "0.0.19" }
limbo_macros = { path = "macros", version = "0.0.19" }
limbo_percentile = { path = "extensions/percentile", version = "0.0.19" }
limbo_regexp = { path = "extensions/regexp", version = "0.0.19" }
limbo_series = { path = "extensions/series", version = "0.0.19" }
limbo_sqlite3_parser = { path = "vendored/sqlite3-parser", version = "0.0.19" }
limbo_time = { path = "extensions/time", version = "0.0.19" }
limbo_uuid = { path = "extensions/uuid", version = "0.0.19" }
[profile.release]
debug = "line-tables-only"
@ -82,6 +57,13 @@ codegen-units = 1
panic = "abort"
lto = true
[profile.antithesis]
inherits = "release"
debug = true
codegen-units = 1
panic = "abort"
lto = true
[profile.bench-profile]
inherits = "release"
debug = true

View file

@ -14,6 +14,7 @@ COPY ./Cargo.lock ./Cargo.lock
COPY ./Cargo.toml ./Cargo.toml
COPY ./bindings/go ./bindings/go/
COPY ./bindings/java ./bindings/java/
COPY ./bindings/javascript ./bindings/javascript/
COPY ./bindings/python ./bindings/python/
COPY ./bindings/rust ./bindings/rust/
COPY ./bindings/wasm ./bindings/wasm/
@ -51,7 +52,7 @@ COPY --from=planner /app/vendored ./vendored/
RUN if [ "$antithesis" = "true" ]; then \
cp /opt/antithesis/libvoidstar.so /usr/lib/libvoidstar.so && \
export RUSTFLAGS="-Ccodegen-units=1 -Cpasses=sancov-module -Cllvm-args=-sanitizer-coverage-level=3 -Cllvm-args=-sanitizer-coverage-trace-pc-guard -Clink-args=-Wl,--build-id -L/usr/lib/ -lvoidstar" && \
cargo build --bin limbo_stress --release; \
cargo build --bin limbo_stress; \
else \
cargo build --bin limbo_stress --release; \
fi
@ -61,7 +62,8 @@ RUN if [ "$antithesis" = "true" ]; then \
#
FROM debian:bullseye-slim AS runtime
RUN apt-get update && apt-get install -y bash && rm -rf /var/lib/apt/lists/*
RUN apt-get update && apt-get install -y bash curl xz-utils python3 sqlite3 bc binutils pip && rm -rf /var/lib/apt/lists/*
RUN pip install antithesis pylimbo
WORKDIR /app
EXPOSE 8080
@ -69,5 +71,15 @@ COPY --from=builder /usr/lib/libvoidstar.so* /usr/lib/
COPY --from=builder /app/target/release/limbo_stress /bin/limbo_stress
COPY stress/docker-entrypoint.sh /bin
RUN chmod +x /bin/docker-entrypoint.sh
COPY ./antithesis-tests/bank-test/*.py /opt/antithesis/test/v1/bank-test/
COPY ./antithesis-tests/stress-composer/*.py /opt/antithesis/test/v1/stress-composer/
COPY ./antithesis-tests/stress /opt/antithesis/test/v1/stress
RUN chmod 777 -R /opt/antithesis/test/v1
RUN mkdir /opt/antithesis/catalog
RUN ln -s /opt/antithesis/test/v1/bank-test/*.py /opt/antithesis/catalog
ENV RUST_BACKTRACE=1
ENTRYPOINT ["/bin/docker-entrypoint.sh"]
CMD ["/bin/limbo_stress"]

View file

@ -62,16 +62,19 @@ limbo-wasm:
cargo build --package limbo-wasm --target wasm32-wasi
.PHONY: limbo-wasm
test: limbo test-compat test-vector test-sqlite3 test-shell test-extensions
uv-sync:
uv sync --all-packages
.PHONE: uv-sync
test: limbo uv-sync test-compat test-vector test-sqlite3 test-shell test-extensions test-memory test-write test-update test-constraint
.PHONY: test
test-extensions: limbo
cargo build --package limbo_regexp
./testing/cli_tests/extensions.py
test-extensions: limbo uv-sync
uv run --project limbo_test test-extensions
.PHONY: test-extensions
test-shell: limbo
SQLITE_EXEC=$(SQLITE_EXEC) ./testing/cli_tests/cli_test_cases.py
test-shell: limbo uv-sync
SQLITE_EXEC=$(SQLITE_EXEC) uv run --project limbo_test test-shell
.PHONY: test-shell
test-compat:
@ -94,6 +97,26 @@ test-json:
SQLITE_EXEC=$(SQLITE_EXEC) ./testing/json.test
.PHONY: test-json
test-memory: limbo uv-sync
SQLITE_EXEC=$(SQLITE_EXEC) uv run --project limbo_test test-memory
.PHONY: test-memory
test-write: limbo uv-sync
SQLITE_EXEC=$(SQLITE_EXEC) uv run --project limbo_test test-write
.PHONY: test-write
test-update: limbo uv-sync
SQLITE_EXEC=$(SQLITE_EXEC) uv run --project limbo_test test-update
.PHONY: test-update
test-constraint: limbo uv-sync
SQLITE_EXEC=$(SQLITE_EXEC) uv run --project limbo_test test-constraint
.PHONY: test-constraint
bench-vfs: uv-sync
cargo build --release
uv run --project limbo_test bench-vfs "$(SQL)" "$(N)"
clickbench:
./perf/clickbench/benchmark.sh
.PHONY: clickbench

38
PERF.md
View file

@ -32,3 +32,41 @@ make clickbench
This will build Limbo in release mode, create a database, and run the benchmarks with a small subset of the Clickbench dataset.
It will run the queries for both Limbo and SQLite, and print the results.
## Comparing VFS's/IO Back-ends (io_uring | syscall)
```shell
make bench-vfs SQL="select * from users;" N=500
```
The naive script will build and run limbo in release mode and execute the given SQL (against a copy of the `testing/testing.db` file)
`N` times with each `vfs`. This is not meant to be a definitive or thorough performance benchmark but serves to compare the two.
## TPC-H
1. Clone the Taratool TPC-H benchmarking tool:
```shell
git clone git@github.com:tarantool/tpch.git
```
2. Patch the benchmark runner script:
```patch
diff --git a/bench_queries.sh b/bench_queries.sh
index 6b894f9..c808e9a 100755
--- a/bench_queries.sh
+++ b/bench_queries.sh
@@ -4,7 +4,7 @@ function check_q {
local query=queries/$*.sql
(
echo $query
- time ( sqlite3 TPC-H.db < $query > /dev/null )
+ time ( ../../limbo/target/release/limbo -m list TPC-H.db < $query > /dev/null )
)
}
```

View file

@ -10,7 +10,9 @@
<p align="center">
<a title="Build Status" target="_blank" href="https://github.com/tursodatabase/limbo/actions/workflows/rust.yml"><img src="https://img.shields.io/github/actions/workflow/status/tursodatabase/limbo/rust.yml?style=flat-square"></a>
<a title="Releases" target="_blank" href="https://github.com/tursodatabase/limbo/releases"><img src="https://img.shields.io/github/release/tursodatabase/limbo?style=flat-square&color=9CF"></a>
<a title="PyPI" target="_blank" href="https://pypi.org/project/pylimbo/"><img alt="PyPI" src="https://img.shields.io/pypi/v/pylimbo"></a>
<a title="Rust" target="_blank" href="https://crates.io/crates/limbo"><img alt="PyPI" src="https://img.shields.io/crates/v/limbo"></a>
<a title="JavaScript" target="_blank" href="https://www.npmjs.com/package/@tursodatabase/limbo"><img alt="PyPI" src="https://img.shields.io/npm/v/@tursodatabase/limbo"></a>
<a title="Python" target="_blank" href="https://pypi.org/project/pylimbo/"><img alt="PyPI" src="https://img.shields.io/pypi/v/pylimbo"></a>
<a title="MIT" target="_blank" href="https://github.com/tursodatabase/limbo/blob/main/LICENSE.md"><img src="http://img.shields.io/badge/license-MIT-orange.svg?style=flat-square"></a>
<br>
<a title="GitHub Pull Requests" target="_blank" href="https://github.com/tursodatabase/limbo/pulls"><img src="https://img.shields.io/github/issues-pr-closed/tursodatabase/limbo.svg?style=flat-square&color=FF9966"></a>
@ -45,7 +47,7 @@ In the future, we will be also working on:
<br>
You can install the latest `limbo` release with:
```shell
```shell
curl --proto '=https' --tlsv1.2 -LsSf \
https://github.com/tursodatabase/limbo/releases/latest/download/limbo_cli-installer.sh | sh
```
@ -72,6 +74,24 @@ cargo run
```
</details>
<details>
<summary>🦀 Rust</summary>
<br>
```console
cargo add limbo
```
Example usage:
```rust
let db = Builder::new_local("sqlite.db").build().await?;
let conn = db.connect()?;
let res = conn.query("SELECT * FROM users", ()).await?;
```
</details>
<details>
<summary>✨ JavaScript</summary>
<br>
@ -144,7 +164,7 @@ defer stmt.Close()
rows, _ = stmt.Query()
for rows.Next() {
var id int
var id int
var username string
_ := rows.Scan(&id, &username)
fmt.Printf("User: ID: %d, Username: %s\n", id, username)
@ -153,7 +173,7 @@ for rows.Next() {
</details>
<details>
<summary>☕️ Java</summary>
<br>
@ -190,3 +210,11 @@ terms or conditions.
[contribution guide]: https://github.com/tursodatabase/limbo/blob/main/CONTRIBUTING.md
[MIT license]: https://github.com/tursodatabase/limbo/blob/main/LICENSE.md
## Contributors
Thanks to all the contributors to Limbo!
<a href="https://github.com/tursodatabase/limbo/graphs/contributors">
<img src="https://contrib.rocks/image?repo=tursodatabase/limbo" />
</a>

View file

@ -0,0 +1,26 @@
#!/usr/bin/env -S python3 -u
import limbo
from antithesis.random import get_random
from antithesis.assertions import always
con = limbo.connect("bank_test.db")
cur = con.cursor()
initial_state = cur.execute(f'''
SELECT * FROM initial_state
''').fetchone()
curr_total = cur.execute(f'''
SELECT SUM(balance) AS total FROM accounts;
''').fetchone()
always(
initial_state[1] == curr_total[0],
'[Anytime] Initial balance always equals current balance',
{
'init_bal': initial_state[1],
'curr_bal': curr_total[0]
}
)

View file

@ -0,0 +1,26 @@
#!/usr/bin/env -S python3 -u
import limbo
from antithesis.random import get_random
from antithesis.assertions import always
con = limbo.connect("bank_test.db")
cur = con.cursor()
initial_state = cur.execute(f'''
SELECT * FROM initial_state
''').fetchone()
curr_total = cur.execute(f'''
SELECT SUM(balance) AS total FROM accounts;
''').fetchone()
always(
initial_state[1] == curr_total[0],
'[Eventually] Initial balance always equals current balance',
{
'init_bal': initial_state[1],
'curr_bal': curr_total[0]
}
)

View file

@ -0,0 +1,26 @@
#!/usr/bin/env -S python3 -u
import limbo
from antithesis.random import get_random
from antithesis.assertions import always
con = limbo.connect("bank_test.db")
cur = con.cursor()
initial_state = cur.execute(f'''
SELECT * FROM initial_state
''').fetchone()
curr_total = cur.execute(f'''
SELECT SUM(balance) AS total FROM accounts;
''').fetchone()
always(
initial_state[1] == curr_total[0],
'[Finally] Initial balance always equals current balance',
{
'init_bal': initial_state[1],
'curr_bal': curr_total[0]
}
)

View file

@ -0,0 +1,47 @@
#!/usr/bin/env -S python3 -u
import limbo
from antithesis.random import get_random
con = limbo.connect("bank_test.db")
cur = con.cursor()
# drop accounts table if it exists and create a new table
cur.execute(f'''
DROP TABLE IF EXISTS accounts;
''')
cur.execute(f'''
CREATE TABLE accounts (
account_id INTEGER PRIMARY KEY AUTOINCREMENT,
balance REAL NOT NULL DEFAULT 0.0
);
''')
# randomly create up to 100 accounts with a balance up to 1e9
total = 0
num_accts = get_random() % 100 + 1
for i in range(num_accts):
bal = get_random() % 1e9
total += bal
cur.execute(f'''
INSERT INTO accounts (balance)
VALUES ({bal})
''')
# drop initial_state table if it exists and create a new table
cur.execute(f'''
DROP TABLE IF EXISTS initial_state;
''')
cur.execute(f'''
CREATE TABLE initial_state (
num_accts INTEGER,
total REAL
);
''')
# store initial state in the table
cur.execute(f'''
INSERT INTO initial_state (num_accts, total)
VALUES ({num_accts}, {total})
''')

View file

@ -0,0 +1,54 @@
#!/usr/bin/env -S python3 -u
import limbo
import logging
from logging.handlers import RotatingFileHandler
from antithesis.random import get_random
handler = RotatingFileHandler(filename='bank_test.log', mode='a', maxBytes=1*1024*1024, backupCount=5, encoding=None, delay=0)
handler.setLevel(logging.INFO)
logger = logging.getLogger('root')
logger.setLevel(logging.INFO)
logger.addHandler(handler)
con = limbo.connect("bank_test.db")
cur = con.cursor()
length = cur.execute("SELECT num_accts FROM initial_state").fetchone()[0]
def transaction():
# check that sender and recipient are different
sender = get_random() % length + 1
recipient = get_random() % length + 1
if sender != recipient:
# get a random value to transfer between accounts
value = get_random() % 1e9
logger.info(f"Sender ID: {sender} | Recipient ID: {recipient} | Txn Val: {value}")
cur.execute("BEGIN TRANSACTION;")
# subtract value from balance of the sender account
cur.execute(f'''
UPDATE accounts
SET balance = balance - {value}
WHERE account_id = {sender};
''')
# add value to balance of the recipient account
cur.execute(f'''
UPDATE accounts
SET balance = balance + {value}
WHERE account_id = {recipient};
''')
cur.execute("COMMIT;")
# run up to 100 transactions
iterations = get_random() % 100
# logger.info(f"Starting {iterations} iterations")
for i in range(iterations):
transaction()
# logger.info(f"Finished {iterations} iterations")

View file

@ -0,0 +1,75 @@
#!/usr/bin/env -S python3 -u
import json
import glob
import os
import limbo
from antithesis.random import get_random, random_choice
constraints = ['NOT NULL', 'UNIQUE', '']
data_type = ['INTEGER', 'REAL', 'TEXT', 'BLOB', 'NUMERIC']
# remove any existing db files
for f in glob.glob('*.db'):
try:
os.remove(f)
except OSError:
pass
for f in glob.glob('*.db-wal'):
try:
os.remove(f)
except OSError:
pass
# store initial states in a separate db
con_init = limbo.connect('init_state.db')
cur_init = con_init.cursor()
cur_init.execute('CREATE TABLE schemas (schema TEXT, tbl INT PRIMARY KEY)')
cur_init.execute('CREATE TABLE tables (count INT)')
con = limbo.connect('stress_composer.db')
cur = con.cursor()
tbl_count = max(1, get_random() % 10)
cur_init.execute(f'INSERT INTO tables (count) VALUES ({tbl_count})')
schemas = []
for i in range(tbl_count):
col_count = max(1, get_random() % 10)
pk = get_random() % col_count
schema = {
'table': i,
'colCount': col_count,
'pk': pk
}
cols = []
cols_str = ''
for j in range(col_count):
col_data_type = random_choice(data_type)
col_constraint_1 = random_choice(constraints)
col_constraint_2 = random_choice(constraints)
col = f'col_{j} {col_data_type} {col_constraint_1} {col_constraint_2 if col_constraint_2 != col_constraint_1 else ""}' if j != pk else f'col_{j} {col_data_type} PRIMARY KEY NOT NULL'
cols.append(col)
schema[f'col_{j}'] = {
'data_type': col_data_type,
'constraint1': col_constraint_1 if j != pk else 'PRIMARY KEY',
'constraint2': col_constraint_2 if col_constraint_1 != col_constraint_2 else "" if j != pk else 'NOT NULL',
}
cols_str = ', '.join(cols)
schemas.append(schema)
cur_init.execute(f"INSERT INTO schemas (schema, tbl) VALUES ('{json.dumps(schema)}', {i})")
cur.execute(f'''
CREATE TABLE tbl_{i} ({cols_str})
''')
print(f'DB Schemas\n------------\n{json.dumps(schemas, indent=2)}')

View file

@ -0,0 +1,33 @@
#!/usr/bin/env -S python3 -u
import json
import limbo
from utils import generate_random_value
from antithesis.random import get_random
# Get initial state
con_init = limbo.connect('init_state.db')
cur_init = con_init.cursor()
tbl_len = cur_init.execute('SELECT count FROM tables').fetchone()[0]
selected_tbl = get_random() % tbl_len
tbl_schema = json.loads(cur_init.execute(f'SELECT schema FROM schemas WHERE tbl = {selected_tbl}').fetchone()[0])
# get primary key column
pk = tbl_schema['pk']
# get non-pk columns
cols = [f'col_{col}' for col in range(tbl_schema['colCount']) if col != pk]
con = limbo.connect('stress_composer.db')
cur = con.cursor()
deletions = get_random() % 100
print(f'Attempt to delete {deletions} rows in tbl_{selected_tbl}...')
for i in range(deletions):
where_clause = f"col_{pk} = {generate_random_value(tbl_schema[f'col_{pk}']['data_type'])}"
cur.execute(f'''
DELETE FROM tbl_{selected_tbl} WHERE {where_clause}
''')

View file

@ -0,0 +1,31 @@
#!/usr/bin/env -S python3 -u
import json
import limbo
from utils import generate_random_value
from antithesis.random import get_random
# Get initial state
con_init = limbo.connect('init_state.db')
cur_init = con_init.cursor()
tbl_len = cur_init.execute('SELECT count FROM tables').fetchone()[0]
selected_tbl = get_random() % tbl_len
tbl_schema = json.loads(cur_init.execute(f'SELECT schema FROM schemas WHERE tbl = {selected_tbl}').fetchone()[0])
cols = ', '.join([f'col_{col}' for col in range(tbl_schema['colCount'])])
con = limbo.connect('stress_composer.db')
cur = con.cursor()
# insert up to 100 rows in the selected table
insertions = get_random() % 100
print(f'Inserting {insertions} rows...')
for i in range(insertions):
values = [generate_random_value(tbl_schema[f'col_{col}']['data_type']) for col in range(tbl_schema['colCount'])]
cur.execute(f'''
INSERT INTO tbl_{selected_tbl} ({cols})
VALUES ({', '.join(values)})
''')

View file

@ -0,0 +1,45 @@
#!/usr/bin/env -S python3 -u
import json
import limbo
from utils import generate_random_value
from antithesis.random import get_random
# Get initial state
con_init = limbo.connect('init_state.db')
cur_init = con_init.cursor()
tbl_len = cur_init.execute('SELECT count FROM tables').fetchone()[0]
selected_tbl = get_random() % tbl_len
tbl_schema = json.loads(cur_init.execute(f'SELECT schema FROM schemas WHERE tbl = {selected_tbl}').fetchone()[0])
# get primary key column
pk = tbl_schema['pk']
# get non-pk columns
cols = [f'col_{col}' for col in range(tbl_schema['colCount']) if col != pk]
# print(cols)
con = limbo.connect('stress_composer.db')
cur = con.cursor()
# insert up to 100 rows in the selected table
updates = get_random() % 100
print(f'Attempt to update {updates} rows in tbl_{selected_tbl}...')
for i in range(updates):
set_clause = ''
if tbl_schema['colCount'] == 1:
set_clause = f"col_{pk} = {generate_random_value(tbl_schema[f'col_{pk}']['data_type'])}"
else:
values = []
for col in cols:
# print(col)
values.append(f"{col} = {generate_random_value(tbl_schema[col]['data_type'])}")
set_clause = ', '.join(values)
where_clause = f"col_{pk} = {generate_random_value(tbl_schema[f'col_{pk}']['data_type'])}"
# print(where_clause)
cur.execute(f'''
UPDATE tbl_{selected_tbl} SET {set_clause} WHERE {where_clause}
''')

View file

@ -0,0 +1,19 @@
import string
from antithesis.random import get_random, random_choice
def generate_random_identifier(type: str, num: int):
return ''.join(type, '_', get_random() % num)
def generate_random_value(type_str):
if type_str == 'INTEGER':
return str(get_random() % 100)
elif type_str == 'REAL':
return '{:.2f}'.format(get_random() % 100 / 100.0)
elif type_str == 'TEXT':
return f"'{''.join(random_choice(string.ascii_lowercase) for _ in range(5))}'"
elif type_str == 'BLOB':
return f"x'{''.join(random_choice(string.ascii_lowercase) for _ in range(5)).encode().hex()}'"
elif type_str == 'NUMERIC':
return str(get_random() % 100)
else:
return NULL

View file

@ -0,0 +1,3 @@
#!/usr/bin/env bash
/bin/limbo_stress

View file

@ -138,7 +138,7 @@ pub extern "system" fn Java_tech_turso_core_LimboStatement_columns<'local>(
for i in 0..num_columns {
let column_name = stmt.stmt.get_column_name(i);
let str = env.new_string(column_name.as_str()).unwrap();
let str = env.new_string(column_name.into_owned()).unwrap();
env.set_object_array_element(&obj_arr, i as i32, str)
.unwrap();
}

View file

@ -1,6 +1,6 @@
{
"name": "@tursodatabase/limbo-darwin-universal",
"version": "0.0.19-pre.4",
"version": "0.0.19",
"repository": {
"type": "git",
"url": "https://github.com/tursodatabase/limbo"

View file

@ -1,6 +1,6 @@
{
"name": "@tursodatabase/limbo-linux-x64-gnu",
"version": "0.0.19-pre.4",
"version": "0.0.19",
"repository": {
"type": "git",
"url": "https://github.com/tursodatabase/limbo"

View file

@ -1,6 +1,6 @@
{
"name": "@tursodatabase/limbo-win32-x64-msvc",
"version": "0.0.19-pre.4",
"version": "0.0.19",
"repository": {
"type": "git",
"url": "https://github.com/tursodatabase/limbo"

View file

@ -1,6 +1,6 @@
{
"name": "@tursodatabase/limbo",
"version": "0.0.19-pre.4",
"version": "0.0.19",
"repository": {
"type": "git",
"url": "https://github.com/tursodatabase/limbo"

View file

@ -4,6 +4,7 @@ use std::cell::RefCell;
use std::rc::Rc;
use std::sync::Arc;
use limbo_core::{maybe_init_database_file, Clock, Instant};
use napi::{Env, JsUnknown, Result as NapiResult};
use napi_derive::napi;
@ -28,20 +29,9 @@ impl Database {
let file = io
.open_file(&path, limbo_core::OpenFlags::Create, false)
.unwrap();
limbo_core::maybe_init_database_file(&file, &io).unwrap();
maybe_init_database_file(&file, &io).unwrap();
let db_file = Arc::new(DatabaseFile::new(file));
let db_header = limbo_core::Pager::begin_open(db_file.clone()).unwrap();
// ensure db header is there
io.run_once().unwrap();
let page_size = db_header.lock().page_size;
let wal_path = format!("{}-wal", path);
let wal_shared =
limbo_core::WalFileShared::open_shared(&io, wal_path.as_str(), page_size).unwrap();
let db = limbo_core::Database::open(io, db_file, wal_shared, false).unwrap();
let db = limbo_core::Database::open(io, &path, db_file, false).unwrap();
let conn = db.connect().unwrap();
Self {
memory,
@ -152,6 +142,12 @@ impl limbo_core::DatabaseStorage for DatabaseFile {
struct IO {}
impl Clock for IO {
fn now(&self) -> Instant {
todo!()
}
}
impl limbo_core::IO for IO {
fn open_file(
&self,
@ -170,7 +166,7 @@ impl limbo_core::IO for IO {
todo!();
}
fn get_current_time(&self) -> String {
todo!();
fn get_memory_io(&self) -> Arc<limbo_core::MemoryIO> {
Arc::new(limbo_core::MemoryIO::new())
}
}

View file

@ -18,7 +18,7 @@ extension-module = ["pyo3/extension-module"]
[dependencies]
anyhow = "1.0"
limbo_core = { path = "../../core", features = ["io_uring"] }
pyo3 = { version = "0.24.0", features = ["anyhow"] }
pyo3 = { version = "0.24.1", features = ["anyhow"] }
[build-dependencies]
version_check = "0.9.5"

View file

@ -6,6 +6,7 @@ pub use value::Value;
pub use params::params_from_iter;
use crate::params::*;
use std::fmt::Debug;
use std::num::NonZero;
use std::rc::Rc;
use std::sync::{Arc, Mutex};
@ -16,11 +17,13 @@ pub enum Error {
ToSqlConversionFailure(BoxError),
#[error("Mutex lock error: {0}")]
MutexError(String),
#[error("SQL execution failure: `{0}`")]
SqlExecutionFailure(String),
}
impl From<limbo_core::LimboError> for Error {
fn from(_err: limbo_core::LimboError) -> Self {
todo!();
fn from(err: limbo_core::LimboError) -> Self {
Error::SqlExecutionFailure(err.to_string())
}
}
@ -55,6 +58,7 @@ impl Builder {
}
}
#[derive(Clone)]
pub struct Database {
inner: Arc<limbo_core::Database>,
}
@ -62,6 +66,12 @@ pub struct Database {
unsafe impl Send for Database {}
unsafe impl Sync for Database {}
impl Debug for Database {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Database").finish()
}
}
impl Database {
pub fn connect(&self) -> Result<Connection> {
let conn = self.inner.connect()?;
@ -119,6 +129,14 @@ pub struct Statement {
inner: Arc<Mutex<limbo_core::Statement>>,
}
impl Clone for Statement {
fn clone(&self) -> Self {
Self {
inner: Arc::clone(&self.inner),
}
}
}
unsafe impl Send for Statement {}
unsafe impl Sync for Statement {}
@ -143,6 +161,10 @@ impl Statement {
}
pub async fn execute(&mut self, params: impl IntoParams) -> Result<u64> {
{
// Reset the statement before executing
self.inner.lock().unwrap().reset();
}
let params = params.into_params()?;
match params {
params::Params::None => (),
@ -180,6 +202,39 @@ impl Statement {
}
}
}
pub fn columns(&self) -> Vec<Column> {
let stmt = self.inner.lock().unwrap();
let n = stmt.num_columns();
let mut cols = Vec::with_capacity(n);
for i in 0..n {
let name = stmt.get_column_name(i).into_owned();
cols.push(Column {
name,
decl_type: None, // TODO
});
}
cols
}
}
pub struct Column {
name: String,
decl_type: Option<String>,
}
impl Column {
pub fn name(&self) -> &str {
&self.name
}
pub fn decl_type(&self) -> Option<&str> {
self.decl_type.as_deref()
}
}
pub trait IntoValue {
@ -198,6 +253,14 @@ pub struct Rows {
inner: Arc<Mutex<limbo_core::Statement>>,
}
impl Clone for Rows {
fn clone(&self) -> Self {
Self {
inner: Arc::clone(&self.inner),
}
}
}
unsafe impl Send for Rows {}
unsafe impl Sync for Rows {}
@ -220,6 +283,7 @@ impl Rows {
}
}
#[derive(Debug)]
pub struct Row {
values: Vec<limbo_core::OwnedValue>,
}
@ -238,4 +302,8 @@ impl Row {
limbo_core::OwnedValue::Blob(items) => Ok(Value::Blob(items.to_vec())),
}
}
pub fn column_count(&self) -> usize {
self.values.len()
}
}

View file

@ -1,5 +1,5 @@
use js_sys::{Array, Object};
use limbo_core::{maybe_init_database_file, OpenFlags, Pager, Result, WalFileShared};
use limbo_core::{maybe_init_database_file, Clock, Instant, OpenFlags, Result};
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::Arc;
@ -17,22 +17,10 @@ impl Database {
#[wasm_bindgen(constructor)]
pub fn new(path: &str) -> Database {
let io: Arc<dyn limbo_core::IO> = Arc::new(PlatformIO { vfs: VFS::new() });
let file = io
.open_file(path, limbo_core::OpenFlags::Create, false)
.unwrap();
let file = io.open_file(path, OpenFlags::Create, false).unwrap();
maybe_init_database_file(&file, &io).unwrap();
let db_file = Arc::new(DatabaseFile::new(file));
let db_header = Pager::begin_open(db_file.clone()).unwrap();
// ensure db header is there
io.run_once().unwrap();
let page_size = db_header.lock().page_size;
let wal_path = format!("{}-wal", path);
let wal_shared = WalFileShared::open_shared(&io, wal_path.as_str(), page_size).unwrap();
let db = limbo_core::Database::open(io, db_file, wal_shared, false).unwrap();
let db = limbo_core::Database::open(io, path, db_file, false).unwrap();
let conn = db.connect().unwrap();
Database { db, conn }
}
@ -269,6 +257,18 @@ pub struct PlatformIO {
unsafe impl Send for PlatformIO {}
unsafe impl Sync for PlatformIO {}
impl Clock for PlatformIO {
fn now(&self) -> Instant {
let date = Date::new();
let ms_since_epoch = date.getTime();
Instant {
secs: (ms_since_epoch / 1000.0) as i64,
micros: ((ms_since_epoch % 1000.0) * 1000.0) as u32,
}
}
}
impl limbo_core::IO for PlatformIO {
fn open_file(
&self,
@ -292,9 +292,8 @@ impl limbo_core::IO for PlatformIO {
(random_f64 * i64::MAX as f64) as i64
}
fn get_current_time(&self) -> String {
let date = Date::new();
date.toISOString()
fn get_memory_io(&self) -> Arc<limbo_core::MemoryIO> {
Arc::new(limbo_core::MemoryIO::new())
}
}
@ -312,6 +311,9 @@ extern "C" {
#[wasm_bindgen(method, getter)]
fn toISOString(this: &Date) -> String;
#[wasm_bindgen(method)]
fn getTime(this: &Date) -> f64;
}
pub struct DatabaseFile {

View file

@ -1,12 +1,12 @@
{
"name": "limbo-wasm",
"version": "0.0.19-pre.4",
"version": "0.0.19",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "limbo-wasm",
"version": "0.0.19-pre.4",
"version": "0.0.19",
"license": "MIT",
"devDependencies": {
"@playwright/test": "^1.49.1",

View file

@ -3,7 +3,7 @@
"collaborators": [
"the Limbo authors"
],
"version": "0.0.19-pre.4",
"version": "0.0.19",
"license": "MIT",
"repository": {
"type": "git",

View file

@ -6,17 +6,18 @@
"": {
"name": "test-limbo",
"dependencies": {
"limbo-wasm": "file:../limbo-wasm-0.0.11.tgz"
"limbo-wasm": ".."
},
"devDependencies": {
"vite": "^6.0.7",
"vite": "^6.2.6",
"vite-plugin-wasm": "^3.4.1"
}
},
"..": {},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz",
"integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz",
"integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==",
"cpu": [
"ppc64"
],
@ -31,9 +32,9 @@
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz",
"integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.2.tgz",
"integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==",
"cpu": [
"arm"
],
@ -48,9 +49,9 @@
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz",
"integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz",
"integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==",
"cpu": [
"arm64"
],
@ -65,9 +66,9 @@
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz",
"integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.2.tgz",
"integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==",
"cpu": [
"x64"
],
@ -82,9 +83,9 @@
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz",
"integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz",
"integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==",
"cpu": [
"arm64"
],
@ -99,9 +100,9 @@
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz",
"integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz",
"integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==",
"cpu": [
"x64"
],
@ -116,9 +117,9 @@
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz",
"integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz",
"integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==",
"cpu": [
"arm64"
],
@ -133,9 +134,9 @@
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz",
"integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz",
"integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==",
"cpu": [
"x64"
],
@ -150,9 +151,9 @@
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz",
"integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz",
"integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==",
"cpu": [
"arm"
],
@ -167,9 +168,9 @@
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz",
"integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz",
"integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==",
"cpu": [
"arm64"
],
@ -184,9 +185,9 @@
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz",
"integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz",
"integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==",
"cpu": [
"ia32"
],
@ -201,9 +202,9 @@
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz",
"integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz",
"integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==",
"cpu": [
"loong64"
],
@ -218,9 +219,9 @@
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz",
"integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz",
"integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==",
"cpu": [
"mips64el"
],
@ -235,9 +236,9 @@
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz",
"integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz",
"integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==",
"cpu": [
"ppc64"
],
@ -252,9 +253,9 @@
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz",
"integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz",
"integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==",
"cpu": [
"riscv64"
],
@ -269,9 +270,9 @@
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz",
"integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz",
"integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==",
"cpu": [
"s390x"
],
@ -286,9 +287,9 @@
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz",
"integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz",
"integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==",
"cpu": [
"x64"
],
@ -303,9 +304,9 @@
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz",
"integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz",
"integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==",
"cpu": [
"arm64"
],
@ -320,9 +321,9 @@
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz",
"integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz",
"integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==",
"cpu": [
"x64"
],
@ -337,9 +338,9 @@
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz",
"integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz",
"integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==",
"cpu": [
"arm64"
],
@ -354,9 +355,9 @@
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz",
"integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz",
"integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==",
"cpu": [
"x64"
],
@ -371,9 +372,9 @@
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz",
"integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz",
"integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==",
"cpu": [
"x64"
],
@ -388,9 +389,9 @@
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz",
"integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz",
"integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==",
"cpu": [
"arm64"
],
@ -405,9 +406,9 @@
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz",
"integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz",
"integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==",
"cpu": [
"ia32"
],
@ -422,9 +423,9 @@
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz",
"integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz",
"integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==",
"cpu": [
"x64"
],
@ -712,9 +713,9 @@
"license": "MIT"
},
"node_modules/esbuild": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz",
"integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==",
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz",
"integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@ -725,31 +726,31 @@
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.24.2",
"@esbuild/android-arm": "0.24.2",
"@esbuild/android-arm64": "0.24.2",
"@esbuild/android-x64": "0.24.2",
"@esbuild/darwin-arm64": "0.24.2",
"@esbuild/darwin-x64": "0.24.2",
"@esbuild/freebsd-arm64": "0.24.2",
"@esbuild/freebsd-x64": "0.24.2",
"@esbuild/linux-arm": "0.24.2",
"@esbuild/linux-arm64": "0.24.2",
"@esbuild/linux-ia32": "0.24.2",
"@esbuild/linux-loong64": "0.24.2",
"@esbuild/linux-mips64el": "0.24.2",
"@esbuild/linux-ppc64": "0.24.2",
"@esbuild/linux-riscv64": "0.24.2",
"@esbuild/linux-s390x": "0.24.2",
"@esbuild/linux-x64": "0.24.2",
"@esbuild/netbsd-arm64": "0.24.2",
"@esbuild/netbsd-x64": "0.24.2",
"@esbuild/openbsd-arm64": "0.24.2",
"@esbuild/openbsd-x64": "0.24.2",
"@esbuild/sunos-x64": "0.24.2",
"@esbuild/win32-arm64": "0.24.2",
"@esbuild/win32-ia32": "0.24.2",
"@esbuild/win32-x64": "0.24.2"
"@esbuild/aix-ppc64": "0.25.2",
"@esbuild/android-arm": "0.25.2",
"@esbuild/android-arm64": "0.25.2",
"@esbuild/android-x64": "0.25.2",
"@esbuild/darwin-arm64": "0.25.2",
"@esbuild/darwin-x64": "0.25.2",
"@esbuild/freebsd-arm64": "0.25.2",
"@esbuild/freebsd-x64": "0.25.2",
"@esbuild/linux-arm": "0.25.2",
"@esbuild/linux-arm64": "0.25.2",
"@esbuild/linux-ia32": "0.25.2",
"@esbuild/linux-loong64": "0.25.2",
"@esbuild/linux-mips64el": "0.25.2",
"@esbuild/linux-ppc64": "0.25.2",
"@esbuild/linux-riscv64": "0.25.2",
"@esbuild/linux-s390x": "0.25.2",
"@esbuild/linux-x64": "0.25.2",
"@esbuild/netbsd-arm64": "0.25.2",
"@esbuild/netbsd-x64": "0.25.2",
"@esbuild/openbsd-arm64": "0.25.2",
"@esbuild/openbsd-x64": "0.25.2",
"@esbuild/sunos-x64": "0.25.2",
"@esbuild/win32-arm64": "0.25.2",
"@esbuild/win32-ia32": "0.25.2",
"@esbuild/win32-x64": "0.25.2"
}
},
"node_modules/fsevents": {
@ -768,14 +769,13 @@
}
},
"node_modules/limbo-wasm": {
"version": "0.0.11",
"resolved": "file:../limbo-wasm-0.0.11.tgz",
"integrity": "sha512-Gxs1kqnCKbfwWjTSWaNQzh954DltmDK28j4EmzDEm/7NZtmwnbfeBj92pS3yJVeQpXuu6zQtaDAS0pYAhi3Q0w=="
"resolved": "..",
"link": true
},
"node_modules/nanoid": {
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"dev": true,
"funding": [
{
@ -799,9 +799,9 @@
"license": "ISC"
},
"node_modules/postcss": {
"version": "8.5.1",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz",
"integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==",
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
"integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
"dev": true,
"funding": [
{
@ -877,15 +877,15 @@
}
},
"node_modules/vite": {
"version": "6.0.7",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.0.7.tgz",
"integrity": "sha512-RDt8r/7qx9940f8FcOIAH9PTViRrghKaK2K1jY3RaAURrEUbm9Du1mJ72G+jlhtG3WwodnfzY8ORQZbBavZEAQ==",
"version": "6.2.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.6.tgz",
"integrity": "sha512-9xpjNl3kR4rVDZgPNdTL0/c6ao4km69a/2ihNQbcANz8RuCOK3hQBmLSJf3bRKVQjVMda+YvizNE8AwvogcPbw==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.24.2",
"postcss": "^8.4.49",
"rollup": "^4.23.0"
"esbuild": "^0.25.0",
"postcss": "^8.5.3",
"rollup": "^4.30.1"
},
"bin": {
"vite": "bin/vite.js"

View file

@ -9,7 +9,7 @@
"dev": "vite"
},
"devDependencies": {
"vite": "^6.0.7",
"vite": "^6.2.6",
"vite-plugin-wasm": "^3.4.1"
}
}

View file

@ -20,25 +20,28 @@ path = "main.rs"
[dependencies]
anyhow = "1.0.75"
cfg-if = "1.0.0"
clap = { version = "4.5.31", features = ["derive"] }
clap_complete = { version = "=4.5.47", features = ["unstable-dynamic"] }
comfy-table = "7.1.4"
csv = "1.3.1"
ctrlc = "3.4.4"
dirs = "5.0.1"
env_logger = "0.10.1"
libc = "0.2.172"
limbo_core = { path = "../core", default-features = true, features = [
"completion",
] }
miette = { version = "7.4.0", features = ["fancy"] }
nu-ansi-term = "0.50.1"
rustyline = { version = "15.0.0", default-features = true, features = [
"derive",
] }
ctrlc = "3.4.4"
csv = "1.3.1"
miette = { version = "7.4.0", features = ["fancy"] }
cfg-if = "1.0.0"
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
tracing = "0.1.41"
shlex = "1.3.0"
syntect = "5.2.0"
nu-ansi-term = "0.50.1"
tracing = "0.1.41"
tracing-appender = "0.2.3"
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
[features]
default = ["io_uring"]
@ -46,4 +49,3 @@ io_uring = ["limbo_core/io_uring"]
[build-dependencies]
syntect = "5.2.0"

View file

@ -1,23 +1,31 @@
use crate::{
commands::{args::EchoMode, import::ImportFile, Command, CommandParser},
commands::{
args::{EchoMode, TimerMode},
import::ImportFile,
Command, CommandParser,
},
helper::LimboHelper,
input::{get_io, get_writer, DbLocation, OutputMode, Settings},
opcodes_dictionary::OPCODE_DESCRIPTIONS,
HISTORY_FILE,
};
use comfy_table::{Attribute, Cell, CellAlignment, Color, ContentArrangement, Row, Table};
use limbo_core::{Database, LimboError, OwnedValue, Statement, StepResult};
use tracing_appender::non_blocking::WorkerGuard;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
use clap::Parser;
use rustyline::{history::DefaultHistory, Editor};
use rustyline::{error::ReadlineError, history::DefaultHistory, Editor};
use std::{
fmt,
io::{self, Write},
io::{self, BufRead as _, Write},
path::PathBuf,
rc::Rc,
sync::{
atomic::{AtomicUsize, Ordering},
Arc,
},
time::{Duration, Instant},
};
#[derive(Parser)]
@ -49,11 +57,13 @@ pub struct Opts {
pub vfs: Option<String>,
#[clap(long, help = "Enable experimental MVCC feature")]
pub experimental_mvcc: bool,
#[clap(short = 't', long, help = "specify output file for log traces")]
pub tracing_output: Option<String>,
}
const PROMPT: &str = "limbo> ";
pub struct Limbo<'a> {
pub struct Limbo {
pub prompt: String,
io: Arc<dyn limbo_core::IO>,
writer: Box<dyn Write>,
@ -61,7 +71,12 @@ pub struct Limbo<'a> {
pub interrupt_count: Arc<AtomicUsize>,
input_buff: String,
opts: Settings,
pub rl: &'a mut Editor<LimboHelper, DefaultHistory>,
pub rl: Option<Editor<LimboHelper, DefaultHistory>>,
}
struct QueryStatistics {
io_time_elapsed_samples: Vec<Duration>,
execute_time_elapsed_samples: Vec<Duration>,
}
macro_rules! query_internal {
@ -91,8 +106,8 @@ macro_rules! query_internal {
static COLORS: &[Color] = &[Color::Green, Color::Black, Color::Grey];
impl<'a> Limbo<'a> {
pub fn new(rl: &'a mut rustyline::Editor<LimboHelper, DefaultHistory>) -> anyhow::Result<Self> {
impl Limbo {
pub fn new() -> anyhow::Result<Self> {
let opts = Opts::parse();
let db_file = opts
.database
@ -119,8 +134,6 @@ impl<'a> Limbo<'a> {
)
};
let conn = db.connect()?;
let h = LimboHelper::new(conn.clone(), io.clone());
rl.set_helper(Some(h));
let interrupt_count = Arc::new(AtomicUsize::new(0));
{
let interrupt_count: Arc<AtomicUsize> = Arc::clone(&interrupt_count);
@ -130,6 +143,8 @@ impl<'a> Limbo<'a> {
})
.expect("Error setting Ctrl-C handler");
}
let sql = opts.sql.clone();
let quiet = opts.quiet;
let mut app = Self {
prompt: PROMPT.to_string(),
io,
@ -137,21 +152,32 @@ impl<'a> Limbo<'a> {
conn,
interrupt_count,
input_buff: String::new(),
opts: Settings::from(&opts),
rl,
opts: Settings::from(opts),
rl: None,
};
if opts.sql.is_some() {
app.handle_first_input(opts.sql.as_ref().unwrap());
}
if !opts.quiet {
app.write_fmt(format_args!("Limbo v{}", env!("CARGO_PKG_VERSION")))?;
app.writeln("Enter \".help\" for usage hints.")?;
app.display_in_memory()?;
}
app.first_run(sql, quiet)?;
Ok(app)
}
pub fn with_readline(mut self, mut rl: Editor<LimboHelper, DefaultHistory>) -> Self {
let h = LimboHelper::new(self.conn.clone(), self.io.clone());
rl.set_helper(Some(h));
self.rl = Some(rl);
self
}
fn first_run(&mut self, sql: Option<String>, quiet: bool) -> io::Result<()> {
if let Some(sql) = sql {
self.handle_first_input(&sql);
}
if !quiet {
self.write_fmt(format_args!("Limbo v{}", env!("CARGO_PKG_VERSION")))?;
self.writeln("Enter \".help\" for usage hints.")?;
self.display_in_memory()?;
}
Ok(())
}
fn handle_first_input(&mut self, cmd: &str) {
if cmd.trim().starts_with('.') {
self.handle_dot_command(&cmd[1..]);
@ -381,24 +407,84 @@ impl<'a> Limbo<'a> {
let _ = self.writeln(input);
}
if input.trim_start().starts_with("explain") {
if let Ok(Some(stmt)) = self.conn.query(input) {
let _ = self.writeln(stmt.explain().as_bytes());
let start = Instant::now();
let mut stats = QueryStatistics {
io_time_elapsed_samples: vec![],
execute_time_elapsed_samples: vec![],
};
// TODO this is a quickfix. Some ideas to do case insensitive comparisons is to use
// Uncased or Unicase.
let temp = input.to_lowercase();
if temp.trim_start().starts_with("explain") {
match self.conn.query(input) {
Ok(Some(stmt)) => {
let _ = self.writeln(stmt.explain().as_bytes());
}
Err(e) => {
let _ = self.writeln(e.to_string());
}
_ => {}
}
} else {
let conn = self.conn.clone();
let runner = conn.query_runner(input.as_bytes());
for output in runner {
if self.print_query_result(input, output).is_err() {
if self
.print_query_result(input, output, Some(&mut stats))
.is_err()
{
break;
}
}
}
self.print_query_performance_stats(start, stats);
self.reset_input();
}
fn reset_line(&mut self, line: &str) -> rustyline::Result<()> {
self.rl.add_history_entry(line.to_owned())?;
fn print_query_performance_stats(&mut self, start: Instant, stats: QueryStatistics) {
let elapsed_as_str = |duration: Duration| {
if duration.as_secs() >= 1 {
format!("{} s", duration.as_secs_f64())
} else if duration.as_millis() >= 1 {
format!("{} ms", duration.as_millis() as f64)
} else if duration.as_micros() >= 1 {
format!("{} us", duration.as_micros() as f64)
} else {
format!("{} ns", duration.as_nanos())
}
};
let sample_stats_as_str = |name: &str, samples: Vec<Duration>| {
if samples.is_empty() {
return format!("{}: No samples available", name);
}
let avg_time_spent = samples.iter().sum::<Duration>() / samples.len() as u32;
let total_time = samples.iter().fold(Duration::ZERO, |acc, x| acc + *x);
format!(
"{}: avg={}, total={}",
name,
elapsed_as_str(avg_time_spent),
elapsed_as_str(total_time),
)
};
if self.opts.timer {
let _ = self.writeln("Command stats:\n----------------------------");
let _ = self.writeln(format!(
"total: {} (this includes parsing/coloring of cli app)\n",
elapsed_as_str(start.elapsed())
));
let _ = self.writeln("query execution stats:\n----------------------------");
let _ = self.writeln(sample_stats_as_str(
"Execution",
stats.execute_time_elapsed_samples,
));
let _ = self.writeln(sample_stats_as_str("I/O", stats.io_time_elapsed_samples));
}
}
fn reset_line(&mut self, _line: &str) -> rustyline::Result<()> {
// Entry is auto added to history
// self.rl.add_history_entry(line.to_owned())?;
self.interrupt_count.store(0, Ordering::SeqCst);
Ok(())
}
@ -426,7 +512,7 @@ impl<'a> Limbo<'a> {
let conn = self.conn.clone();
let runner = conn.query_runner(after_comment.as_bytes());
for output in runner {
if let Err(e) = self.print_query_result(after_comment, output) {
if let Err(e) = self.print_query_result(after_comment, output, None) {
let _ = self.writeln(e.to_string());
}
}
@ -467,15 +553,18 @@ impl<'a> Limbo<'a> {
}
match CommandParser::try_parse_from(args) {
Err(err) => {
let _ = self.write_fmt(format_args!("{err}"));
// Let clap print with Styled Colors instead
let _ = err.print();
}
Ok(cmd) => match cmd.command {
Command::Exit(args) => {
self.save_history();
std::process::exit(args.code);
}
Command::Quit => {
let _ = self.writeln("Exiting Limbo SQL Shell.");
let _ = self.close_conn();
self.save_history();
std::process::exit(0)
}
Command::Open(args) => {
@ -554,6 +643,17 @@ impl<'a> Limbo<'a> {
let _ = self.writeln(v);
});
}
Command::ListIndexes(args) => {
if let Err(e) = self.display_indexes(args.tbl_name) {
let _ = self.writeln(e.to_string());
}
}
Command::Timer(timer_mode) => {
self.opts.timer = match timer_mode.mode {
TimerMode::On => true,
TimerMode::Off => false,
};
}
},
}
}
@ -562,6 +662,7 @@ impl<'a> Limbo<'a> {
&mut self,
sql: &str,
mut output: Result<Option<Statement>, LimboError>,
mut statistics: Option<&mut QueryStatistics>,
) -> anyhow::Result<()> {
match output {
Ok(Some(ref mut rows)) => match self.opts.output_mode {
@ -571,8 +672,13 @@ impl<'a> Limbo<'a> {
return Ok(());
}
let start = Instant::now();
match rows.step() {
Ok(StepResult::Row) => {
if let Some(ref mut stats) = statistics {
stats.execute_time_elapsed_samples.push(start.elapsed());
}
let row = rows.row().unwrap();
for (i, value) in row.get_values().enumerate() {
if i > 0 {
@ -587,17 +693,30 @@ impl<'a> Limbo<'a> {
let _ = self.writeln("");
}
Ok(StepResult::IO) => {
let start = Instant::now();
self.io.run_once()?;
if let Some(ref mut stats) = statistics {
stats.io_time_elapsed_samples.push(start.elapsed());
}
}
Ok(StepResult::Interrupt) => break,
Ok(StepResult::Done) => {
if let Some(ref mut stats) = statistics {
stats.execute_time_elapsed_samples.push(start.elapsed());
}
break;
}
Ok(StepResult::Busy) => {
if let Some(ref mut stats) = statistics {
stats.execute_time_elapsed_samples.push(start.elapsed());
}
let _ = self.writeln("database is busy");
break;
}
Err(err) => {
if let Some(ref mut stats) = statistics {
stats.execute_time_elapsed_samples.push(start.elapsed());
}
let _ = self.writeln(err.to_string());
break;
}
@ -625,8 +744,12 @@ impl<'a> Limbo<'a> {
table.set_header(header);
}
loop {
let start = Instant::now();
match rows.step() {
Ok(StepResult::Row) => {
if let Some(ref mut stats) = statistics {
stats.execute_time_elapsed_samples.push(start.elapsed());
}
let record = rows.row().unwrap();
let mut row = Row::new();
row.max_height(1);
@ -657,35 +780,52 @@ impl<'a> Limbo<'a> {
table.add_row(row);
}
Ok(StepResult::IO) => {
let start = Instant::now();
self.io.run_once()?;
if let Some(ref mut stats) = statistics {
stats.io_time_elapsed_samples.push(start.elapsed());
}
}
Ok(StepResult::Interrupt) => {
if let Some(ref mut stats) = statistics {
stats.execute_time_elapsed_samples.push(start.elapsed());
}
break;
}
Ok(StepResult::Done) => {
if let Some(ref mut stats) = statistics {
stats.execute_time_elapsed_samples.push(start.elapsed());
}
break;
}
Ok(StepResult::Interrupt) => break,
Ok(StepResult::Done) => break,
Ok(StepResult::Busy) => {
if let Some(ref mut stats) = statistics {
stats.execute_time_elapsed_samples.push(start.elapsed());
}
let _ = self.writeln("database is busy");
break;
}
Err(err) => {
let _ = self.write_fmt(format_args!(
"{:?}",
miette::Error::from(err).with_source_code(sql.to_owned())
));
if let Some(ref mut stats) = statistics {
stats.execute_time_elapsed_samples.push(start.elapsed());
}
let report =
miette::Error::from(err).with_source_code(sql.to_owned());
let _ = self.write_fmt(format_args!("{:?}", report));
break;
}
}
}
if table.header().is_some() {
if !table.is_empty() {
let _ = self.write_fmt(format_args!("{}", table));
}
}
},
Ok(None) => {}
Err(err) => {
let _ = self.write_fmt(format_args!(
"{:?}",
miette::Error::from(err).with_source_code(sql.to_owned())
));
let report = miette::Error::from(err).with_source_code(sql.to_owned());
let _ = self.write_fmt(format_args!("{:?}", report));
anyhow::bail!("We have to throw here, even if we printed error");
}
}
@ -694,6 +834,37 @@ impl<'a> Limbo<'a> {
Ok(())
}
pub fn init_tracing(&mut self) -> Result<WorkerGuard, std::io::Error> {
let ((non_blocking, guard), should_emit_ansi) =
if let Some(file) = &self.opts.tracing_output {
(
tracing_appender::non_blocking(
std::fs::File::options()
.append(true)
.create(true)
.open(file)?,
),
false,
)
} else {
(tracing_appender::non_blocking(std::io::stderr()), true)
};
if let Err(e) = tracing_subscriber::registry()
.with(
tracing_subscriber::fmt::layer()
.with_writer(non_blocking)
.with_line_number(true)
.with_thread_ids(true)
.with_ansi(should_emit_ansi),
)
.with(EnvFilter::from_default_env())
.try_init()
{
println!("Unable to setup tracing appender: {:?}", e);
}
Ok(guard)
}
fn display_schema(&mut self, table: Option<&str>) -> anyhow::Result<()> {
let sql = match table {
Some(table_name) => format!(
@ -752,6 +923,55 @@ impl<'a> Limbo<'a> {
Ok(())
}
fn display_indexes(&mut self, maybe_table: Option<String>) -> anyhow::Result<()> {
let sql = match maybe_table {
Some(ref tbl_name) => format!(
"SELECT name FROM sqlite_schema WHERE type='index' AND tbl_name = '{}' ORDER BY 1",
tbl_name
),
None => String::from("SELECT name FROM sqlite_schema WHERE type='index' ORDER BY 1"),
};
match self.conn.query(&sql) {
Ok(Some(ref mut rows)) => {
let mut indexes = String::new();
loop {
match rows.step()? {
StepResult::Row => {
let row = rows.row().unwrap();
if let Ok(OwnedValue::Text(idx)) = row.get::<&OwnedValue>(0) {
indexes.push_str(idx.as_str());
indexes.push(' ');
}
}
StepResult::IO => {
self.io.run_once()?;
}
StepResult::Interrupt => break,
StepResult::Done => break,
StepResult::Busy => {
let _ = self.writeln("database is busy");
break;
}
}
}
if !indexes.is_empty() {
let _ = self.writeln(indexes.trim_end());
}
}
Err(err) => {
if err.to_string().contains("no such table: sqlite_schema") {
return Err(anyhow::anyhow!("Unable to access database schema. The database may be using an older SQLite version or may not be properly initialized."));
} else {
return Err(anyhow::anyhow!("Error querying schema: {}", err));
}
}
Ok(None) => {}
}
Ok(())
}
fn display_tables(&mut self, pattern: Option<&str>) -> anyhow::Result<()> {
let sql = match pattern {
Some(pattern) => format!(
@ -822,4 +1042,38 @@ impl<'a> Limbo<'a> {
self.run_query(buff.as_str());
self.reset_input();
}
pub fn readline(&mut self) -> Result<String, ReadlineError> {
if let Some(rl) = &mut self.rl {
Ok(rl.readline(&self.prompt)?)
} else {
let mut input = String::new();
println!("");
let mut reader = std::io::stdin().lock();
if reader.read_line(&mut input)? == 0 {
return Err(ReadlineError::Eof.into());
}
// Remove trailing newline
if input.ends_with('\n') {
input.pop();
if input.ends_with('\r') {
input.pop();
}
}
Ok(input)
}
}
fn save_history(&mut self) {
if let Some(rl) = &mut self.rl {
let _ = rl.save_history(HISTORY_FILE.as_path());
}
}
}
impl Drop for Limbo {
fn drop(&mut self) {
self.save_history()
}
}

View file

@ -1,6 +1,13 @@
use clap::{Args, ValueEnum};
use clap_complete::{ArgValueCompleter, CompletionCandidate, PathCompleter};
use crate::input::OutputMode;
use crate::{input::OutputMode, opcodes_dictionary::OPCODE_DESCRIPTIONS};
#[derive(Debug, Clone, Args)]
pub struct IndexesArgs {
/// Name of table
pub tbl_name: Option<String>,
}
#[derive(Debug, Clone, Args)]
pub struct ExitArgs {
@ -12,13 +19,17 @@ pub struct ExitArgs {
#[derive(Debug, Clone, Args)]
pub struct OpenArgs {
/// Path to open database
#[arg(add = ArgValueCompleter::new(PathCompleter::file()))]
pub path: String,
// TODO see how to have this completed with the output of List Vfs function
// Currently not possible to pass arbitrary
/// Name of VFS
pub vfs_name: Option<String>,
}
#[derive(Debug, Clone, Args)]
pub struct SchemaArgs {
// TODO depends on PRAGMA table_list for completions
/// Table name to visualize schema
pub table_name: Option<String>,
}
@ -26,6 +37,7 @@ pub struct SchemaArgs {
#[derive(Debug, Clone, Args)]
pub struct SetOutputArgs {
/// File path to send output to
#[arg(add = ArgValueCompleter::new(PathCompleter::file()))]
pub path: Option<String>,
}
@ -35,15 +47,40 @@ pub struct OutputModeArgs {
pub mode: OutputMode,
}
fn opcodes_completer(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
let mut completions = vec![];
let Some(current) = current.to_str() else {
return completions;
};
let current = current.to_lowercase();
let opcodes = &OPCODE_DESCRIPTIONS;
for op in opcodes {
// TODO if someone know how to do prefix_match with case insensitve in Rust
// without converting the String to lowercase first, please fix this.
let op_name = op.name.to_ascii_lowercase();
if op_name.starts_with(&current) {
completions.push(CompletionCandidate::new(op.name).help(Some(op.description.into())));
}
}
completions
}
#[derive(Debug, Clone, Args)]
pub struct OpcodesArgs {
/// Opcode to display description
#[arg(add = ArgValueCompleter::new(opcodes_completer))]
pub opcode: Option<String>,
}
#[derive(Debug, Clone, Args)]
pub struct CwdArgs {
/// Target directory
#[arg(add = ArgValueCompleter::new(PathCompleter::dir()))]
pub directory: String,
}
@ -72,11 +109,18 @@ pub struct TablesArgs {
#[derive(Debug, Clone, Args)]
pub struct LoadExtensionArgs {
/// Path to extension file
#[arg(add = ArgValueCompleter::new(PathCompleter::file()))]
pub path: String,
}
#[derive(Debug, Clone, Args)]
pub struct ListVfsArgs {
/// Path to extension file
pub path: String,
#[derive(Debug, ValueEnum, Clone)]
pub enum TimerMode {
On,
Off,
}
#[derive(Debug, Clone, Args)]
pub struct TimerArgs {
#[arg(value_enum)]
pub mode: TimerMode,
}

View file

@ -1,4 +1,5 @@
use clap::Args;
use clap_complete::{ArgValueCompleter, PathCompleter};
use limbo_core::Connection;
use std::{fs::File, io::Write, path::PathBuf, rc::Rc, sync::Arc};
@ -13,6 +14,7 @@ pub struct ImportArgs {
/// Skip the first N rows of input
#[arg(long, default_value = "0")]
skip: u64,
#[arg(add = ArgValueCompleter::new(PathCompleter::file()))]
file: PathBuf,
table: String,
}

View file

@ -2,8 +2,8 @@ pub mod args;
pub mod import;
use args::{
CwdArgs, EchoArgs, ExitArgs, LoadExtensionArgs, NullValueArgs, OpcodesArgs, OpenArgs,
OutputModeArgs, SchemaArgs, SetOutputArgs, TablesArgs,
CwdArgs, EchoArgs, ExitArgs, IndexesArgs, LoadExtensionArgs, NullValueArgs, OpcodesArgs,
OpenArgs, OutputModeArgs, SchemaArgs, SetOutputArgs, TablesArgs, TimerArgs,
};
use clap::Parser;
use import::ImportArgs;
@ -35,9 +35,6 @@ pub enum Command {
/// Open a database file
#[command(display_name = ".open")]
Open(OpenArgs),
/// Print this message or the help of the given subcommand(s)
// #[command(display_name = ".help")]
// Help,
/// Display schema for a table
#[command(display_name = ".schema")]
Schema(SchemaArgs),
@ -75,6 +72,11 @@ pub enum Command {
/// List vfs modules available
#[command(name = "vfslist", display_name = ".vfslist")]
ListVfs,
/// Show names of indexes
#[command(name = "indexes", display_name = ".indexes")]
ListIndexes(IndexesArgs),
#[command(name = "timer", display_name = ".timer")]
Timer(TimerArgs),
}
const _HELP_TEMPLATE: &str = "{before-help}{name}

View file

@ -1,12 +1,18 @@
use std::rc::Rc;
use std::sync::Arc;
use clap::Parser;
use limbo_core::{Connection, StepResult};
use nu_ansi_term::{Color, Style};
use rustyline::completion::{extract_word, Completer, Pair};
use rustyline::highlight::Highlighter;
use rustyline::hint::HistoryHinter;
use rustyline::{Completer, Helper, Hinter, Validator};
use shlex::Shlex;
use std::cell::RefCell;
use std::marker::PhantomData;
use std::rc::Rc;
use std::sync::Arc;
use std::{ffi::OsString, path::PathBuf, str::FromStr as _};
use crate::commands::CommandParser;
macro_rules! try_result {
($expr:expr, $err:expr) => {
@ -20,7 +26,7 @@ macro_rules! try_result {
#[derive(Helper, Completer, Hinter, Validator)]
pub struct LimboHelper {
#[rustyline(Completer)]
completer: SqlCompleter,
completer: SqlCompleter<CommandParser>,
#[rustyline(Hinter)]
hinter: HistoryHinter,
}
@ -77,57 +83,70 @@ impl Highlighter for LimboHelper {
}
}
pub struct SqlCompleter {
pub struct SqlCompleter<C: Parser + Send + Sync + 'static> {
conn: Rc<Connection>,
io: Arc<dyn limbo_core::IO>,
// Has to be a ref cell as Rustyline takes immutable reference to self
// This problem would be solved with Reedline as it uses &mut self for completions
cmd: RefCell<clap::Command>,
_cmd_phantom: PhantomData<C>,
}
impl SqlCompleter {
impl<C: Parser + Send + Sync + 'static> SqlCompleter<C> {
pub fn new(conn: Rc<Connection>, io: Arc<dyn limbo_core::IO>) -> Self {
Self { conn, io }
}
}
// Got this from the FilenameCompleter.
// TODO have to see what chars break words in Sqlite
cfg_if::cfg_if! {
if #[cfg(unix)] {
// rl_basic_word_break_characters, rl_completer_word_break_characters
const fn default_break_chars(c : char) -> bool {
matches!(c, ' ' | '\t' | '\n' | '"' | '\\' | '\'' | '`' | '@' | '$' | '>' | '<' | '=' | ';' | '|' | '&' |
'{' | '(' | '\0')
Self {
conn,
io,
cmd: C::command().into(),
_cmd_phantom: PhantomData::default(),
}
const ESCAPE_CHAR: Option<char> = Some('\\');
// In double quotes, not all break_chars need to be escaped
// https://www.gnu.org/software/bash/manual/html_node/Double-Quotes.html
#[allow(dead_code)]
const fn double_quotes_special_chars(c: char) -> bool { matches!(c, '"' | '$' | '\\' | '`') }
} else if #[cfg(windows)] {
// Remove \ to make file completion works on windows
const fn default_break_chars(c: char) -> bool {
matches!(c, ' ' | '\t' | '\n' | '"' | '\'' | '`' | '@' | '$' | '>' | '<' | '=' | ';' | '|' | '&' | '{' |
'(' | '\0')
}
const ESCAPE_CHAR: Option<char> = None;
#[allow(dead_code)]
const fn double_quotes_special_chars(c: char) -> bool { c == '"' } // TODO Validate: only '"' ?
} else if #[cfg(target_arch = "wasm32")] {
const fn default_break_chars(c: char) -> bool { false }
const ESCAPE_CHAR: Option<char> = None;
#[allow(dead_code)]
const fn double_quotes_special_chars(c: char) -> bool { false }
}
}
impl Completer for SqlCompleter {
type Candidate = Pair;
fn complete(
fn dot_completion(
&self,
line: &str,
pos: usize,
_ctx: &rustyline::Context<'_>,
) -> rustyline::Result<(usize, Vec<Self::Candidate>)> {
mut line: &str,
mut pos: usize,
) -> rustyline::Result<(usize, Vec<Pair>)> {
// TODO maybe check to see if the line is empty and then just output the command names
line = &line[1..];
pos = pos - 1;
let (prefix_pos, _) = extract_word(line, pos, ESCAPE_CHAR, default_break_chars);
let args = Shlex::new(line);
let mut args = std::iter::once("".to_owned())
.chain(args)
.map(OsString::from)
.collect::<Vec<_>>();
if line.ends_with(' ') {
args.push(OsString::new());
}
let arg_index = args.len() - 1;
// dbg!(&pos, line, &args, arg_index);
let mut cmd = self.cmd.borrow_mut();
match clap_complete::engine::complete(
&mut cmd,
args,
arg_index,
PathBuf::from_str(".").ok().as_deref(),
) {
Ok(candidates) => {
let candidates = candidates
.iter()
.map(|candidate| Pair {
display: candidate.get_value().to_string_lossy().into_owned(),
replacement: candidate.get_value().to_string_lossy().into_owned(),
})
.collect::<Vec<Pair>>();
Ok((prefix_pos + 1, candidates))
}
Err(_) => Ok((prefix_pos + 1, Vec::new())),
}
}
fn sql_completion(&self, line: &str, pos: usize) -> rustyline::Result<(usize, Vec<Pair>)> {
// TODO: have to differentiate words if they are enclosed in single of double quotes
let (prefix_pos, prefix) = extract_word(line, pos, ESCAPE_CHAR, default_break_chars);
let mut candidates = Vec::new();
@ -167,3 +186,51 @@ impl Completer for SqlCompleter {
Ok((prefix_pos, candidates))
}
}
// Got this from the FilenameCompleter.
// TODO have to see what chars break words in Sqlite
cfg_if::cfg_if! {
if #[cfg(unix)] {
// rl_basic_word_break_characters, rl_completer_word_break_characters
const fn default_break_chars(c : char) -> bool {
matches!(c, ' ' | '\t' | '\n' | '"' | '\\' | '\'' | '`' | '@' | '$' | '>' | '<' | '=' | ';' | '|' | '&' |
'{' | '(' | '\0')
}
const ESCAPE_CHAR: Option<char> = Some('\\');
// In double quotes, not all break_chars need to be escaped
// https://www.gnu.org/software/bash/manual/html_node/Double-Quotes.html
#[allow(dead_code)]
const fn double_quotes_special_chars(c: char) -> bool { matches!(c, '"' | '$' | '\\' | '`') }
} else if #[cfg(windows)] {
// Remove \ to make file completion works on windows
const fn default_break_chars(c: char) -> bool {
matches!(c, ' ' | '\t' | '\n' | '"' | '\'' | '`' | '@' | '$' | '>' | '<' | '=' | ';' | '|' | '&' | '{' |
'(' | '\0')
}
const ESCAPE_CHAR: Option<char> = None;
#[allow(dead_code)]
const fn double_quotes_special_chars(c: char) -> bool { c == '"' } // TODO Validate: only '"' ?
} else if #[cfg(target_arch = "wasm32")] {
const fn default_break_chars(c: char) -> bool { false }
const ESCAPE_CHAR: Option<char> = None;
#[allow(dead_code)]
const fn double_quotes_special_chars(c: char) -> bool { false }
}
}
impl<C: Parser + Send + Sync + 'static> Completer for SqlCompleter<C> {
type Candidate = Pair;
fn complete(
&self,
line: &str,
pos: usize,
_ctx: &rustyline::Context<'_>,
) -> rustyline::Result<(usize, Vec<Self::Candidate>)> {
if line.starts_with(".") {
self.dot_completion(line, pos)
} else {
self.sql_completion(line, pos)
}
}
}

View file

@ -43,7 +43,7 @@ impl Default for Io {
true => {
#[cfg(all(target_os = "linux", feature = "io_uring"))]
{
Io::IoUring
Io::Syscall // FIXME: make io_uring faster so it can be the default
}
#[cfg(any(
not(target_os = "linux"),
@ -81,28 +81,32 @@ pub struct Settings {
pub echo: bool,
pub is_stdout: bool,
pub io: Io,
pub tracing_output: Option<String>,
pub timer: bool,
}
impl From<&Opts> for Settings {
fn from(opts: &Opts) -> Self {
impl From<Opts> for Settings {
fn from(opts: Opts) -> Self {
Self {
null_value: String::new(),
output_mode: opts.output_mode,
echo: false,
is_stdout: opts.output.is_empty(),
output_filename: opts.output.clone(),
output_filename: opts.output,
db_file: opts
.database
.as_ref()
.map_or(":memory:".to_string(), |p| p.to_string_lossy().to_string()),
io: match opts.vfs.as_ref().unwrap_or(&String::new()).as_str() {
"memory" => Io::Memory,
"memory" | ":memory:" => Io::Memory,
"syscall" => Io::Syscall,
#[cfg(all(target_os = "linux", feature = "io_uring"))]
"io_uring" => Io::IoUring,
"" => Io::default(),
vfs => Io::External(vfs.to_string()),
},
tracing_output: opts.tracing_output,
timer: false,
}
}
}
@ -214,6 +218,8 @@ pub const AFTER_HELP_MSG: &str = r#"Usage Examples:
13. To list all available VFS:
.listvfs
14. To show names of indexes:
.indexes ?TABLE?
Note:
- All SQL commands must end with a semicolon (;).

View file

@ -6,33 +6,39 @@ mod input;
mod opcodes_dictionary;
use rustyline::{error::ReadlineError, Config, Editor};
use std::sync::atomic::Ordering;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
use std::{
path::PathBuf,
sync::{atomic::Ordering, LazyLock},
};
fn rustyline_config() -> Config {
Config::builder()
.completion_type(rustyline::CompletionType::List)
.auto_add_history(true)
.build()
}
pub static HOME_DIR: LazyLock<PathBuf> =
LazyLock::new(|| dirs::home_dir().expect("Could not determine home directory"));
pub static HISTORY_FILE: LazyLock<PathBuf> = LazyLock::new(|| HOME_DIR.join(".limbo_history"));
fn main() -> anyhow::Result<()> {
let mut rl = Editor::with_config(rustyline_config())?;
tracing_subscriber::registry()
.with(
tracing_subscriber::fmt::layer()
.with_line_number(true)
.with_thread_ids(true),
)
.with(EnvFilter::from_default_env())
.init();
let mut app = app::Limbo::new(&mut rl)?;
let home = dirs::home_dir().expect("Could not determine home directory");
let history_file = home.join(".limbo_history");
if history_file.exists() {
app.rl.load_history(history_file.as_path())?;
let mut app = app::Limbo::new()?;
let _guard = app.init_tracing()?;
if std::io::IsTerminal::is_terminal(&std::io::stdin()) {
let mut rl = Editor::with_config(rustyline_config())?;
if HISTORY_FILE.exists() {
rl.load_history(HISTORY_FILE.as_path())?;
}
app = app.with_readline(rl);
} else {
tracing::debug!("not in tty");
}
loop {
let readline = app.rl.readline(&app.prompt);
let readline = app.readline();
match readline {
Ok(line) => match app.handle_input_line(line.trim()) {
Ok(_) => {}
@ -62,6 +68,5 @@ fn main() -> anyhow::Result<()> {
}
}
}
rl.save_history(history_file.as_path())?;
Ok(())
}

View file

@ -28,16 +28,17 @@ ipaddr = ["limbo_ipaddr/static"]
completion = ["limbo_completion/static"]
testvfs = ["limbo_ext_tests/static"]
static = ["limbo_ext/static"]
fuzz = []
[target.'cfg(target_os = "linux")'.dependencies]
io-uring = { version = "0.6.1", optional = true }
io-uring = { version = "0.7.5", optional = true }
[target.'cfg(target_family = "unix")'.dependencies]
polling = "3.7.2"
rustix = "0.38.34"
polling = "3.7.4"
rustix = { version = "1.0.5", features = ["fs"]}
[target.'cfg(not(target_family = "wasm"))'.dependencies]
mimalloc = { version = "0.1", default-features = false }
mimalloc = { version = "0.1.46", default-features = false }
libloading = "0.8.6"
[dependencies]
@ -45,7 +46,7 @@ limbo_ext = { workspace = true, features = ["core_only"] }
cfg_block = "0.1.1"
fallible-iterator = "0.3.0"
hex = "0.4.3"
libc = { version = "0.2.155", optional = true }
libc = { version = "0.2.172", optional = true }
limbo_sqlite3_parser = { workspace = true }
thiserror = "1.0.61"
getrandom = { version = "0.2.15" }
@ -54,7 +55,7 @@ regex-syntax = { version = "0.8.5", default-features = false, features = [
"unicode",
] }
chrono = { version = "0.4.38", default-features = false, features = ["clock"] }
julian_day_converter = "0.4.4"
julian_day_converter = "0.4.5"
rand = "0.8.5"
libm = "0.2"
limbo_macros = { workspace = true }
@ -67,12 +68,13 @@ limbo_series = { workspace = true, optional = true, features = ["static"] }
limbo_ipaddr = { workspace = true, optional = true, features = ["static"] }
limbo_completion = { workspace = true, optional = true, features = ["static"] }
limbo_ext_tests = { workspace = true, optional = true, features = ["static"] }
miette = "7.4.0"
miette = "7.6.0"
strum = "0.26"
parking_lot = "0.12.3"
crossbeam-skiplist = "0.1.3"
tracing = "0.1.41"
ryu = "1.0.19"
bitflags = "2.9.0"
[build-dependencies]
chrono = { version = "0.4.38", default-features = false }
@ -96,7 +98,7 @@ rand = "0.8.5" # Required for quickcheck
rand_chacha = "0.9.0"
env_logger = "0.11.6"
test-log = { version = "0.2.17", features = ["trace"] }
lru = "0.13.0"
lru = "0.14.0"
[[bench]]
name = "benchmark"

View file

@ -1,5 +1,3 @@
use std::num::NonZero;
use thiserror::Error;
#[derive(Debug, Error, miette::Diagnostic)]
@ -49,12 +47,12 @@ pub enum LimboError {
Constraint(String),
#[error("Extension error: {0}")]
ExtensionError(String),
#[error("Unbound parameter at index {0}")]
Unbound(NonZero<usize>),
#[error("Runtime error: integer overflow")]
IntegerOverflow,
#[error("Schema is locked for write")]
SchemaLocked,
#[error("Database Connection is read-only")]
ReadOnly,
}
#[macro_export]

View file

@ -6,6 +6,7 @@ use libloading::{Library, Symbol};
use limbo_ext::{ExtensionApi, ExtensionApiRef, ExtensionEntryPoint, ResultCode, VfsImpl};
use std::{
ffi::{c_char, CString},
rc::Rc,
sync::{Arc, Mutex, OnceLock},
};
@ -29,7 +30,10 @@ unsafe impl Send for VfsMod {}
unsafe impl Sync for VfsMod {}
impl Connection {
pub fn load_extension<P: AsRef<std::ffi::OsStr>>(&self, path: P) -> crate::Result<()> {
pub fn load_extension<P: AsRef<std::ffi::OsStr>>(
self: &Rc<Connection>,
path: P,
) -> crate::Result<()> {
use limbo_ext::ExtensionApiRef;
let api = Box::new(self.build_limbo_ext());
@ -44,7 +48,15 @@ impl Connection {
let result_code = unsafe { entry(api_ptr) };
if result_code.is_ok() {
let extensions = get_extension_libraries();
extensions.lock().unwrap().push((Arc::new(lib), api_ref));
extensions
.lock()
.map_err(|_| {
LimboError::ExtensionError("Error locking extension libraries".to_string())
})?
.push((Arc::new(lib), api_ref));
{
self.parse_schema_rows()?;
}
Ok(())
} else {
if !api_ptr.is_null() {

View file

@ -89,12 +89,12 @@ impl Database {
path: &str,
vfs: &str,
) -> crate::Result<(Arc<dyn IO>, Arc<Database>)> {
use crate::{MemoryIO, PlatformIO};
use crate::{MemoryIO, SyscallIO};
use dynamic::get_vfs_modules;
let io: Arc<dyn IO> = match vfs {
"memory" => Arc::new(MemoryIO::new()),
"syscall" => Arc::new(PlatformIO::new()?),
"syscall" => Arc::new(SyscallIO::new()?),
#[cfg(all(target_os = "linux", feature = "io_uring"))]
"io_uring" => Arc::new(UringIO::new()?),
other => match get_vfs_modules().iter().find(|v| v.0 == vfs) {

View file

@ -10,6 +10,12 @@ pub struct ExternalFunc {
pub func: ExtFunc,
}
impl ExternalFunc {
pub fn is_deterministic(&self) -> bool {
false // external functions can be whatever so let's just default to false
}
}
#[derive(Debug, Clone)]
pub enum ExtFunc {
Scalar(ScalarFunction),
@ -98,6 +104,13 @@ pub enum JsonFunc {
JsonQuote,
}
#[cfg(feature = "json")]
impl JsonFunc {
pub fn is_deterministic(&self) -> bool {
true
}
}
#[cfg(feature = "json")]
impl Display for JsonFunc {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
@ -145,6 +158,12 @@ pub enum VectorFunc {
VectorDistanceCos,
}
impl VectorFunc {
pub fn is_deterministic(&self) -> bool {
true
}
}
impl Display for VectorFunc {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let str = match self {
@ -198,6 +217,10 @@ impl PartialEq for AggFunc {
}
impl AggFunc {
pub fn is_deterministic(&self) -> bool {
false // consider aggregate functions nondeterministic since they depend on the number of rows, not only the input arguments
}
pub fn num_args(&self) -> usize {
match self {
Self::Avg => 1,
@ -292,6 +315,68 @@ pub enum ScalarFunc {
LoadExtension,
StrfTime,
Printf,
Likely,
TimeDiff,
Likelihood,
}
impl ScalarFunc {
pub fn is_deterministic(&self) -> bool {
match self {
ScalarFunc::Cast => true,
ScalarFunc::Changes => false, // depends on DB state
ScalarFunc::Char => true,
ScalarFunc::Coalesce => true,
ScalarFunc::Concat => true,
ScalarFunc::ConcatWs => true,
ScalarFunc::Glob => true,
ScalarFunc::IfNull => true,
ScalarFunc::Iif => true,
ScalarFunc::Instr => true,
ScalarFunc::Like => true,
ScalarFunc::Abs => true,
ScalarFunc::Upper => true,
ScalarFunc::Lower => true,
ScalarFunc::Random => false, // duh
ScalarFunc::RandomBlob => false, // duh
ScalarFunc::Trim => true,
ScalarFunc::LTrim => true,
ScalarFunc::RTrim => true,
ScalarFunc::Round => true,
ScalarFunc::Length => true,
ScalarFunc::OctetLength => true,
ScalarFunc::Min => true,
ScalarFunc::Max => true,
ScalarFunc::Nullif => true,
ScalarFunc::Sign => true,
ScalarFunc::Substr => true,
ScalarFunc::Substring => true,
ScalarFunc::Soundex => true,
ScalarFunc::Date => false,
ScalarFunc::Time => false,
ScalarFunc::TotalChanges => false,
ScalarFunc::DateTime => false,
ScalarFunc::Typeof => true,
ScalarFunc::Unicode => true,
ScalarFunc::Quote => true,
ScalarFunc::SqliteVersion => true,
ScalarFunc::SqliteSourceId => true,
ScalarFunc::UnixEpoch => false,
ScalarFunc::JulianDay => false,
ScalarFunc::Hex => true,
ScalarFunc::Unhex => true,
ScalarFunc::ZeroBlob => true,
ScalarFunc::LastInsertRowid => false,
ScalarFunc::Replace => true,
#[cfg(feature = "fs")]
ScalarFunc::LoadExtension => true,
ScalarFunc::StrfTime => false,
ScalarFunc::Printf => false,
ScalarFunc::Likely => true,
ScalarFunc::TimeDiff => false,
ScalarFunc::Likelihood => true,
}
}
}
impl Display for ScalarFunc {
@ -346,6 +431,9 @@ impl Display for ScalarFunc {
Self::LoadExtension => "load_extension".to_string(),
Self::StrfTime => "strftime".to_string(),
Self::Printf => "printf".to_string(),
Self::Likely => "likely".to_string(),
Self::TimeDiff => "timediff".to_string(),
Self::Likelihood => "likelihood".to_string(),
};
write!(f, "{}", str)
}
@ -392,6 +480,9 @@ pub enum MathFuncArity {
}
impl MathFunc {
pub fn is_deterministic(&self) -> bool {
true
}
pub fn arity(&self) -> MathFuncArity {
match self {
Self::Pi => MathFuncArity::Nullary,
@ -495,6 +586,17 @@ pub struct FuncCtx {
}
impl Func {
pub fn is_deterministic(&self) -> bool {
match self {
Self::Agg(agg_func) => agg_func.is_deterministic(),
Self::Scalar(scalar_func) => scalar_func.is_deterministic(),
Self::Math(math_func) => math_func.is_deterministic(),
Self::Vector(vector_func) => vector_func.is_deterministic(),
#[cfg(feature = "json")]
Self::Json(json_func) => json_func.is_deterministic(),
Self::External(external_func) => external_func.is_deterministic(),
}
}
pub fn resolve_function(name: &str, arg_count: usize) -> Result<Self, LimboError> {
match name {
"avg" => {
@ -553,6 +655,12 @@ impl Func {
}
Ok(Self::Agg(AggFunc::Total))
}
"timediff" => {
if arg_count != 2 {
crate::bail_parse_error!("wrong number of arguments to function {}()", name)
}
Ok(Self::Scalar(ScalarFunc::TimeDiff))
}
#[cfg(feature = "json")]
"jsonb_group_array" => Ok(Self::Agg(AggFunc::JsonbGroupArray)),
#[cfg(feature = "json")]
@ -596,6 +704,8 @@ impl Func {
"sqlite_version" => Ok(Self::Scalar(ScalarFunc::SqliteVersion)),
"sqlite_source_id" => Ok(Self::Scalar(ScalarFunc::SqliteSourceId)),
"replace" => Ok(Self::Scalar(ScalarFunc::Replace)),
"likely" => Ok(Self::Scalar(ScalarFunc::Likely)),
"likelihood" => Ok(Self::Scalar(ScalarFunc::Likelihood)),
#[cfg(feature = "json")]
"json" => Ok(Self::Json(JsonFunc::Json)),
#[cfg(feature = "json")]

View file

@ -46,21 +46,13 @@ enum DateTimeOutput {
DateTime,
// Holds the format string
StrfTime(String),
JuliaDay,
}
fn exec_datetime(values: &[Register], output_type: DateTimeOutput) -> OwnedValue {
if values.is_empty() {
let now = parse_naive_date_time(&OwnedValue::build_text("now")).unwrap();
let formatted_str = match output_type {
DateTimeOutput::DateTime => now.format("%Y-%m-%d %H:%M:%S").to_string(),
DateTimeOutput::Time => now.format("%H:%M:%S").to_string(),
DateTimeOutput::Date => now.format("%Y-%m-%d").to_string(),
DateTimeOutput::StrfTime(ref format_str) => strftime_format(&now, format_str),
};
// Parse here
return OwnedValue::build_text(&formatted_str);
return format_dt(now, output_type, false);
}
if let Some(mut dt) = parse_naive_date_time(values[0].get_owned_value()) {
// if successful, treat subsequent entries as modifiers
@ -91,28 +83,32 @@ fn modify_dt(dt: &mut NaiveDateTime, mods: &[Register], output_type: DateTimeOut
if is_leap_second(dt) || *dt > get_max_datetime_exclusive() {
return OwnedValue::build_text("");
}
let formatted = format_dt(*dt, output_type, subsec_requested);
OwnedValue::build_text(&formatted)
format_dt(*dt, output_type, subsec_requested)
}
fn format_dt(dt: NaiveDateTime, output_type: DateTimeOutput, subsec: bool) -> String {
fn format_dt(dt: NaiveDateTime, output_type: DateTimeOutput, subsec: bool) -> OwnedValue {
match output_type {
DateTimeOutput::Date => dt.format("%Y-%m-%d").to_string(),
DateTimeOutput::Date => OwnedValue::from_text(dt.format("%Y-%m-%d").to_string().as_str()),
DateTimeOutput::Time => {
if subsec {
let t = if subsec {
dt.format("%H:%M:%S%.3f").to_string()
} else {
dt.format("%H:%M:%S").to_string()
}
};
OwnedValue::from_text(t.as_str())
}
DateTimeOutput::DateTime => {
if subsec {
let t = if subsec {
dt.format("%Y-%m-%d %H:%M:%S%.3f").to_string()
} else {
dt.format("%Y-%m-%d %H:%M:%S").to_string()
}
};
OwnedValue::from_text(t.as_str())
}
DateTimeOutput::StrfTime(format_str) => strftime_format(&dt, &format_str),
DateTimeOutput::StrfTime(format_str) => {
OwnedValue::from_text(strftime_format(&dt, &format_str).as_str())
}
DateTimeOutput::JuliaDay => OwnedValue::Float(to_julian_day_exact(&dt)),
}
}
@ -325,14 +321,8 @@ fn last_day_in_month(year: i32, month: u32) -> u32 {
28
}
pub fn exec_julianday(time_value: &OwnedValue) -> Result<String> {
let dt = parse_naive_date_time(time_value);
match dt {
// if we did something heinous like: parse::<f64>().unwrap().to_string()
// that would solve the precision issue, but dear lord...
Some(dt) => Ok(format!("{:.1$}", to_julian_day_exact(&dt), 8)),
None => Ok(String::new()),
}
pub fn exec_julianday(values: &[Register]) -> OwnedValue {
exec_datetime(values, DateTimeOutput::JuliaDay)
}
fn to_julian_day_exact(dt: &NaiveDateTime) -> f64 {
@ -656,6 +646,61 @@ fn parse_modifier(modifier: &str) -> Result<Modifier> {
}
}
pub fn exec_timediff(values: &[Register]) -> OwnedValue {
if values.len() < 2 {
return OwnedValue::Null;
}
let start = parse_naive_date_time(values[0].get_owned_value());
let end = parse_naive_date_time(values[1].get_owned_value());
match (start, end) {
(Some(start), Some(end)) => {
let duration = start.signed_duration_since(end);
format_time_duration(&duration)
}
_ => OwnedValue::Null,
}
}
/// Format the time duration as +/-YYYY-MM-DD HH:MM:SS.SSS as per SQLite's timediff() function
fn format_time_duration(duration: &chrono::Duration) -> OwnedValue {
let is_negative = duration.num_seconds() < 0;
let abs_duration = if is_negative {
-duration.clone()
} else {
duration.clone()
};
let total_seconds = abs_duration.num_seconds();
let hours = (total_seconds % 86400) / 3600;
let minutes = (total_seconds % 3600) / 60;
let seconds = total_seconds % 60;
let days = total_seconds / 86400;
let years = days / 365;
let remaining_days = days % 365;
let months = 0;
let total_millis = abs_duration.num_milliseconds();
let millis = total_millis % 1000;
let result = format!(
"{}{:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:03}",
if is_negative { "-" } else { "+" },
years,
months,
remaining_days,
hours,
minutes,
seconds,
millis
);
OwnedValue::build_text(&result)
}
#[cfg(test)]
mod tests {
use super::*;
@ -1642,4 +1687,67 @@ mod tests {
#[test]
fn test_strftime() {}
#[test]
fn test_exec_timediff() {
let start = OwnedValue::build_text("12:00:00");
let end = OwnedValue::build_text("14:30:45");
let expected = OwnedValue::build_text("-0000-00-00 02:30:45.000");
assert_eq!(
exec_timediff(&[Register::OwnedValue(start), Register::OwnedValue(end)]),
expected
);
let start = OwnedValue::build_text("14:30:45");
let end = OwnedValue::build_text("12:00:00");
let expected = OwnedValue::build_text("+0000-00-00 02:30:45.000");
assert_eq!(
exec_timediff(&[Register::OwnedValue(start), Register::OwnedValue(end)]),
expected
);
let start = OwnedValue::build_text("12:00:01.300");
let end = OwnedValue::build_text("12:00:00.500");
let expected = OwnedValue::build_text("+0000-00-00 00:00:00.800");
assert_eq!(
exec_timediff(&[Register::OwnedValue(start), Register::OwnedValue(end)]),
expected
);
let start = OwnedValue::build_text("13:30:00");
let end = OwnedValue::build_text("16:45:30");
let expected = OwnedValue::build_text("-0000-00-00 03:15:30.000");
assert_eq!(
exec_timediff(&[Register::OwnedValue(start), Register::OwnedValue(end)]),
expected
);
let start = OwnedValue::build_text("2023-05-10 23:30:00");
let end = OwnedValue::build_text("2023-05-11 01:15:00");
let expected = OwnedValue::build_text("-0000-00-00 01:45:00.000");
assert_eq!(
exec_timediff(&[Register::OwnedValue(start), Register::OwnedValue(end)]),
expected
);
let start = OwnedValue::Null;
let end = OwnedValue::build_text("12:00:00");
let expected = OwnedValue::Null;
assert_eq!(
exec_timediff(&[Register::OwnedValue(start), Register::OwnedValue(end)]),
expected
);
let start = OwnedValue::build_text("not a time");
let end = OwnedValue::build_text("12:00:00");
let expected = OwnedValue::Null;
assert_eq!(
exec_timediff(&[Register::OwnedValue(start), Register::OwnedValue(end)]),
expected
);
let start = OwnedValue::build_text("12:00:00");
let expected = OwnedValue::Null;
assert_eq!(exec_timediff(&[Register::OwnedValue(start)]), expected);
}
}

9
core/io/clock.rs Normal file
View file

@ -0,0 +1,9 @@
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Instant {
pub secs: i64,
pub micros: u32,
}
pub trait Clock {
fn now(&self) -> Instant;
}

View file

@ -1,4 +1,5 @@
use crate::{Completion, File, LimboError, OpenFlags, Result, IO};
use super::MemoryIO;
use crate::{Clock, Completion, File, Instant, LimboError, OpenFlags, Result, IO};
use std::cell::RefCell;
use std::io::{Read, Seek, Write};
use std::sync::Arc;
@ -19,13 +20,18 @@ unsafe impl Sync for GenericIO {}
impl IO for GenericIO {
fn open_file(&self, path: &str, flags: OpenFlags, _direct: bool) -> Result<Arc<dyn File>> {
trace!("open_file(path = {})", path);
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.create(matches!(flags, OpenFlags::Create))
.open(path)?;
let mut file = std::fs::File::options();
file.read(true);
if !flags.contains(OpenFlags::ReadOnly) {
file.write(true);
file.create(flags.contains(OpenFlags::Create));
}
let file = file.open(path)?;
Ok(Arc::new(GenericFile {
file: RefCell::new(file),
memory_io: Arc::new(MemoryIO::new()),
}))
}
@ -39,13 +45,24 @@ impl IO for GenericIO {
i64::from_ne_bytes(buf)
}
fn get_current_time(&self) -> String {
chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string()
fn get_memory_io(&self) -> Arc<MemoryIO> {
Arc::new(MemoryIO::new())
}
}
impl Clock for GenericIO {
fn now(&self) -> Instant {
let now = chrono::Local::now();
Instant {
secs: now.timestamp(),
micros: now.timestamp_subsec_micros(),
}
}
}
pub struct GenericFile {
file: RefCell<std::fs::File>,
memory_io: Arc<MemoryIO>,
}
unsafe impl Send for GenericFile {}

View file

@ -1,5 +1,6 @@
use super::{common, Completion, File, OpenFlags, WriteCompletion, IO};
use crate::{LimboError, Result};
use crate::io::clock::{Clock, Instant};
use crate::{LimboError, MemoryIO, Result};
use rustix::fs::{self, FlockOperation, OFlags};
use rustix::io_uring::iovec;
use std::cell::RefCell;
@ -138,11 +139,15 @@ impl WrappedIOUring {
impl IO for UringIO {
fn open_file(&self, path: &str, flags: OpenFlags, direct: bool) -> Result<Arc<dyn File>> {
trace!("open_file(path = {})", path);
let file = std::fs::File::options()
.read(true)
.write(true)
.create(matches!(flags, OpenFlags::Create))
.open(path)?;
let mut file = std::fs::File::options();
file.read(true);
if !flags.contains(OpenFlags::ReadOnly) {
file.write(true);
file.create(flags.contains(OpenFlags::Create));
}
let file = file.open(path)?;
// Let's attempt to enable direct I/O. Not all filesystems support it
// so ignore any errors.
let fd = file.as_fd();
@ -157,7 +162,7 @@ impl IO for UringIO {
file,
});
if std::env::var(common::ENV_DISABLE_FILE_LOCK).is_err() {
uring_file.lock_file(true)?;
uring_file.lock_file(!flags.contains(OpenFlags::ReadOnly))?;
}
Ok(uring_file)
}
@ -197,8 +202,18 @@ impl IO for UringIO {
i64::from_ne_bytes(buf)
}
fn get_current_time(&self) -> String {
chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string()
fn get_memory_io(&self) -> Arc<MemoryIO> {
Arc::new(MemoryIO::new())
}
}
impl Clock for UringIO {
fn now(&self) -> Instant {
let now = chrono::Local::now();
Instant {
secs: now.timestamp(),
micros: now.timestamp_subsec_micros(),
}
}
}

View file

@ -1,6 +1,7 @@
use super::{Buffer, Completion, File, OpenFlags, IO};
use super::{Buffer, Clock, Completion, File, OpenFlags, IO};
use crate::Result;
use crate::io::clock::Instant;
use std::{
cell::{Cell, RefCell, UnsafeCell},
collections::BTreeMap,
@ -29,6 +30,16 @@ impl Default for MemoryIO {
}
}
impl Clock for MemoryIO {
fn now(&self) -> Instant {
let now = chrono::Local::now();
Instant {
secs: now.timestamp(),
micros: now.timestamp_subsec_micros(),
}
}
}
impl IO for MemoryIO {
fn open_file(&self, _path: &str, _flags: OpenFlags, _direct: bool) -> Result<Arc<dyn File>> {
Ok(Arc::new(MemoryFile {
@ -48,8 +59,8 @@ impl IO for MemoryIO {
i64::from_ne_bytes(buf)
}
fn get_current_time(&self) -> String {
chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string()
fn get_memory_io(&self) -> Arc<MemoryIO> {
Arc::new(MemoryIO::new())
}
}

View file

@ -1,4 +1,5 @@
use crate::Result;
use bitflags::bitflags;
use cfg_block::cfg_block;
use std::fmt;
use std::sync::Arc;
@ -19,29 +20,31 @@ pub trait File: Send + Sync {
fn size(&self) -> Result<u64>;
}
#[derive(Copy, Clone)]
pub enum OpenFlags {
None,
Create,
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct OpenFlags(i32);
impl OpenFlags {
pub fn to_flags(&self) -> i32 {
match self {
Self::None => 0,
Self::Create => 1,
}
bitflags! {
impl OpenFlags: i32 {
const None = 0b00000000;
const Create = 0b0000001;
const ReadOnly = 0b0000010;
}
}
pub trait IO: Send + Sync {
impl Default for OpenFlags {
fn default() -> Self {
Self::Create
}
}
pub trait IO: Clock + Send + Sync {
fn open_file(&self, path: &str, flags: OpenFlags, direct: bool) -> Result<Arc<dyn File>>;
fn run_once(&self) -> Result<()>;
fn generate_random_number(&self) -> i64;
fn get_current_time(&self) -> String;
fn get_memory_io(&self) -> Arc<MemoryIO>;
}
pub type Complete = dyn Fn(Arc<RefCell<Buffer>>);
@ -191,7 +194,8 @@ cfg_block! {
mod unix;
#[cfg(feature = "fs")]
pub use unix::UnixIO;
pub use io_uring::UringIO as PlatformIO;
pub use unix::UnixIO as SyscallIO;
pub use unix::UnixIO as PlatformIO;
}
#[cfg(any(all(target_os = "linux",not(feature = "io_uring")), target_os = "macos"))] {
@ -199,16 +203,19 @@ cfg_block! {
#[cfg(feature = "fs")]
pub use unix::UnixIO;
pub use unix::UnixIO as PlatformIO;
pub use PlatformIO as SyscallIO;
}
#[cfg(target_os = "windows")] {
mod windows;
pub use windows::WindowsIO as PlatformIO;
pub use PlatformIO as SyscallIO;
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))] {
mod generic;
pub use generic::GenericIO as PlatformIO;
pub use PlatformIO as SyscallIO;
}
}
@ -216,4 +223,6 @@ mod memory;
#[cfg(feature = "fs")]
mod vfs;
pub use memory::MemoryIO;
pub mod clock;
mod common;
pub use clock::Clock;

View file

@ -2,7 +2,8 @@ use crate::error::LimboError;
use crate::io::common;
use crate::Result;
use super::{Completion, File, OpenFlags, IO};
use super::{Completion, File, MemoryIO, OpenFlags, IO};
use crate::io::clock::{Clock, Instant};
use polling::{Event, Events, Poller};
use rustix::{
fd::{AsFd, AsRawFd},
@ -183,15 +184,28 @@ impl UnixIO {
}
}
impl Clock for UnixIO {
fn now(&self) -> Instant {
let now = chrono::Local::now();
Instant {
secs: now.timestamp(),
micros: now.timestamp_subsec_micros(),
}
}
}
impl IO for UnixIO {
fn open_file(&self, path: &str, flags: OpenFlags, _direct: bool) -> Result<Arc<dyn File>> {
trace!("open_file(path = {})", path);
let file = std::fs::File::options()
.read(true)
.custom_flags(OFlags::NONBLOCK.bits() as i32)
.write(true)
.create(matches!(flags, OpenFlags::Create))
.open(path)?;
let mut file = std::fs::File::options();
file.read(true).custom_flags(OFlags::NONBLOCK.bits() as i32);
if !flags.contains(OpenFlags::ReadOnly) {
file.write(true);
file.create(flags.contains(OpenFlags::Create));
}
let file = file.open(path)?;
#[allow(clippy::arc_with_non_send_sync)]
let unix_file = Arc::new(UnixFile {
@ -200,7 +214,7 @@ impl IO for UnixIO {
callbacks: BorrowedCallbacks(self.callbacks.as_mut().into()),
});
if std::env::var(common::ENV_DISABLE_FILE_LOCK).is_err() {
unix_file.lock_file(true)?;
unix_file.lock_file(!flags.contains(OpenFlags::ReadOnly))?;
}
Ok(unix_file)
}
@ -248,8 +262,8 @@ impl IO for UnixIO {
i64::from_ne_bytes(buf)
}
fn get_current_time(&self) -> String {
chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string()
fn get_memory_io(&self) -> Arc<MemoryIO> {
Arc::new(MemoryIO::new())
}
}

View file

@ -1,11 +1,21 @@
use super::{Buffer, Completion, File, MemoryIO, OpenFlags, IO};
use crate::ext::VfsMod;
use crate::io::clock::{Clock, Instant};
use crate::{LimboError, Result};
use limbo_ext::{VfsFileImpl, VfsImpl};
use std::cell::RefCell;
use std::ffi::{c_void, CString};
use std::sync::Arc;
use super::{Buffer, Completion, File, OpenFlags, IO};
impl Clock for VfsMod {
fn now(&self) -> Instant {
let now = chrono::Local::now();
Instant {
secs: now.timestamp(),
micros: now.timestamp_subsec_micros(),
}
}
}
impl IO for VfsMod {
fn open_file(&self, path: &str, flags: OpenFlags, direct: bool) -> Result<Arc<dyn File>> {
@ -14,7 +24,7 @@ impl IO for VfsMod {
})?;
let ctx = self.ctx as *mut c_void;
let vfs = unsafe { &*self.ctx };
let file = unsafe { (vfs.open)(ctx, c_path.as_ptr(), flags.to_flags(), direct) };
let file = unsafe { (vfs.open)(ctx, c_path.as_ptr(), flags.0, direct) };
if file.is_null() {
return Err(LimboError::ExtensionError("File not found".to_string()));
}
@ -41,6 +51,13 @@ impl IO for VfsMod {
unsafe { (vfs.gen_random_number)() }
}
fn get_memory_io(&self) -> Arc<MemoryIO> {
Arc::new(MemoryIO::new())
}
}
impl VfsMod {
#[allow(dead_code)] // used in FFI call
fn get_current_time(&self) -> String {
if self.ctx.is_null() {
return "".to_string();

View file

@ -1,9 +1,9 @@
use crate::{Completion, File, LimboError, OpenFlags, Result, IO};
use super::MemoryIO;
use crate::{Clock, Completion, File, Instant, LimboError, OpenFlags, Result, IO};
use std::cell::RefCell;
use std::io::{Read, Seek, Write};
use std::sync::Arc;
use tracing::{debug, trace};
pub struct WindowsIO {}
impl WindowsIO {
@ -19,11 +19,15 @@ unsafe impl Sync for WindowsIO {}
impl IO for WindowsIO {
fn open_file(&self, path: &str, flags: OpenFlags, direct: bool) -> Result<Arc<dyn File>> {
trace!("open_file(path = {})", path);
let file = std::fs::File::options()
.read(true)
.write(true)
.create(matches!(flags, OpenFlags::Create))
.open(path)?;
let mut file = std::fs::File::options();
file.read(true);
if !flags.contains(OpenFlags::ReadOnly) {
file.write(true);
file.create(flags.contains(OpenFlags::Create));
}
let file = file.open(path)?;
Ok(Arc::new(WindowsFile {
file: RefCell::new(file),
}))
@ -39,8 +43,18 @@ impl IO for WindowsIO {
i64::from_ne_bytes(buf)
}
fn get_current_time(&self) -> String {
chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string()
fn get_memory_io(&self) -> Arc<MemoryIO> {
Arc::new(MemoryIO::new())
}
}
impl Clock for WindowsIO {
fn now(&self) -> Instant {
let now = chrono::Local::now();
Instant {
secs: now.timestamp(),
micros: now.timestamp_subsec_micros(),
}
}
}

View file

@ -20,6 +20,12 @@ mod util;
mod vdbe;
mod vector;
#[cfg(feature = "fuzz")]
pub mod numeric;
#[cfg(not(feature = "fuzz"))]
mod numeric;
#[cfg(not(target_family = "wasm"))]
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
@ -27,12 +33,15 @@ static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
use crate::{fast_lock::SpinLock, translate::optimizer::optimize_plan};
pub use error::LimboError;
use fallible_iterator::FallibleIterator;
pub use io::clock::{Clock, Instant};
#[cfg(all(feature = "fs", target_family = "unix"))]
pub use io::UnixIO;
#[cfg(all(feature = "fs", target_os = "linux", feature = "io_uring"))]
pub use io::UringIO;
pub use io::{Buffer, Completion, File, MemoryIO, OpenFlags, PlatformIO, WriteCompletion, IO};
use limbo_ext::{ResultCode, VTabKind, VTabModuleImpl};
pub use io::{
Buffer, Completion, File, MemoryIO, OpenFlags, PlatformIO, SyscallIO, WriteCompletion, IO,
};
use limbo_ext::{ConstraintInfo, IndexInfo, OrderByInfo, ResultCode, VTabKind, VTabModuleImpl};
use limbo_sqlite3_parser::{ast, ast::Cmd, lexer::sql::Parser};
use parking_lot::RwLock;
use schema::{Column, Schema};
@ -66,20 +75,19 @@ pub use types::OwnedValue;
pub use types::RefValue;
use util::{columns_from_create_table_body, parse_schema_rows};
use vdbe::{builder::QueryMode, VTabOpaqueCursor};
pub type Result<T, E = LimboError> = std::result::Result<T, E>;
pub static DATABASE_VERSION: OnceLock<String> = OnceLock::new();
#[derive(Clone, PartialEq, Eq)]
#[derive(Clone, Copy, PartialEq, Eq)]
enum TransactionState {
Write,
Read,
None,
}
pub(crate) type MvStore = crate::mvcc::MvStore<crate::mvcc::LocalClock>;
pub(crate) type MvStore = mvcc::MvStore<mvcc::LocalClock>;
pub(crate) type MvCursor = crate::mvcc::cursor::ScanCursor<crate::mvcc::LocalClock>;
pub(crate) type MvCursor = mvcc::cursor::ScanCursor<mvcc::LocalClock>;
pub struct Database {
mv_store: Option<Rc<MvStore>>,
@ -88,11 +96,12 @@ pub struct Database {
header: Arc<SpinLock<DatabaseHeader>>,
db_file: Arc<dyn DatabaseStorage>,
io: Arc<dyn IO>,
page_size: u16,
page_size: u32,
// Shared structures of a Database are the parts that are common to multiple threads that might
// create DB connections.
shared_page_cache: Arc<RwLock<DumbLruPageCache>>,
shared_wal: Arc<UnsafeCell<WalFileShared>>,
open_flags: OpenFlags,
}
unsafe impl Send for Database {}
@ -101,53 +110,74 @@ unsafe impl Sync for Database {}
impl Database {
#[cfg(feature = "fs")]
pub fn open_file(io: Arc<dyn IO>, path: &str, enable_mvcc: bool) -> Result<Arc<Database>> {
use storage::wal::WalFileShared;
Self::open_file_with_flags(io, path, OpenFlags::default(), enable_mvcc)
}
let file = io.open_file(path, OpenFlags::Create, true)?;
#[cfg(feature = "fs")]
pub fn open_file_with_flags(
io: Arc<dyn IO>,
path: &str,
flags: OpenFlags,
enable_mvcc: bool,
) -> Result<Arc<Database>> {
let file = io.open_file(path, flags, true)?;
maybe_init_database_file(&file, &io)?;
let db_file = Arc::new(DatabaseFile::new(file));
let wal_path = format!("{}-wal", path);
let db_header = Pager::begin_open(db_file.clone())?;
io.run_once()?;
let page_size = db_header.lock().page_size;
let wal_shared = WalFileShared::open_shared(&io, wal_path.as_str(), page_size)?;
Self::open(io, db_file, wal_shared, enable_mvcc)
Self::open_with_flags(io, path, db_file, flags, enable_mvcc)
}
#[allow(clippy::arc_with_non_send_sync)]
pub fn open(
io: Arc<dyn IO>,
path: &str,
db_file: Arc<dyn DatabaseStorage>,
shared_wal: Arc<UnsafeCell<WalFileShared>>,
enable_mvcc: bool,
) -> Result<Arc<Database>> {
Self::open_with_flags(io, path, db_file, OpenFlags::default(), enable_mvcc)
}
#[allow(clippy::arc_with_non_send_sync)]
pub fn open_with_flags(
io: Arc<dyn IO>,
path: &str,
db_file: Arc<dyn DatabaseStorage>,
flags: OpenFlags,
enable_mvcc: bool,
) -> Result<Arc<Database>> {
let db_header = Pager::begin_open(db_file.clone())?;
// ensure db header is there
io.run_once()?;
let page_size = db_header.lock().get_page_size();
let wal_path = format!("{}-wal", path);
let shared_wal = WalFileShared::open_shared(&io, wal_path.as_str(), page_size)?;
DATABASE_VERSION.get_or_init(|| {
let version = db_header.lock().version_number;
version.to_string()
});
let mv_store = if enable_mvcc {
Some(Rc::new(MvStore::new(
crate::mvcc::LocalClock::new(),
crate::mvcc::persistent_storage::Storage::new_noop(),
mvcc::LocalClock::new(),
mvcc::persistent_storage::Storage::new_noop(),
)))
} else {
None
};
let shared_page_cache = Arc::new(RwLock::new(DumbLruPageCache::new(10)));
let page_size = db_header.lock().page_size;
let header = db_header;
let schema = Arc::new(RwLock::new(Schema::new()));
let db = Database {
mv_store,
schema: schema.clone(),
header: header.clone(),
header: db_header.clone(),
shared_page_cache: shared_page_cache.clone(),
shared_wal: shared_wal.clone(),
db_file,
io: io.clone(),
page_size,
open_flags: flags,
};
let db = Arc::new(db);
{
@ -158,7 +188,13 @@ impl Database {
.try_write()
.expect("lock on schema should succeed first try");
let syms = conn.syms.borrow();
parse_schema_rows(rows, &mut schema, io, syms.deref(), None)?;
if let Err(LimboError::ExtensionError(e)) =
parse_schema_rows(rows, &mut schema, io, &syms, None)
{
// this means that a vtab exists and we no longer have the module loaded. we print
// a warning to the user to load the module
eprintln!("Warning: {}", e);
}
}
Ok(db)
}
@ -168,14 +204,14 @@ impl Database {
let wal = Rc::new(RefCell::new(WalFile::new(
self.io.clone(),
self.page_size as usize,
self.page_size,
self.shared_wal.clone(),
buffer_pool.clone(),
)));
let pager = Rc::new(Pager::finish_open(
self.header.clone(),
self.db_file.clone(),
wal,
Some(wal),
self.io.clone(),
self.shared_page_cache.clone(),
buffer_pool,
@ -186,9 +222,9 @@ impl Database {
schema: self.schema.clone(),
header: self.header.clone(),
last_insert_rowid: Cell::new(0),
auto_commit: RefCell::new(true),
auto_commit: Cell::new(true),
mv_transactions: RefCell::new(Vec::new()),
transaction_state: RefCell::new(TransactionState::None),
transaction_state: Cell::new(TransactionState::None),
last_change: Cell::new(0),
syms: RefCell::new(SymbolTable::new()),
total_changes: Cell::new(0),
@ -204,12 +240,12 @@ impl Database {
#[cfg(feature = "fs")]
#[allow(clippy::arc_with_non_send_sync)]
pub fn open_new(path: &str, vfs: &str) -> Result<(Arc<dyn IO>, Arc<Database>)> {
let vfsmods = crate::ext::add_builtin_vfs_extensions(None)?;
let vfsmods = ext::add_builtin_vfs_extensions(None)?;
let io: Arc<dyn IO> = match vfsmods.iter().find(|v| v.0 == vfs).map(|v| v.1.clone()) {
Some(vfs) => vfs,
None => match vfs.trim() {
"memory" => Arc::new(MemoryIO::new()),
"syscall" => Arc::new(PlatformIO::new()?),
"syscall" => Arc::new(SyscallIO::new()?),
#[cfg(all(target_os = "linux", feature = "io_uring"))]
"io_uring" => Arc::new(UringIO::new()?),
other => {
@ -231,7 +267,7 @@ pub fn maybe_init_database_file(file: &Arc<dyn File>, io: &Arc<dyn IO>) -> Resul
let db_header = DatabaseHeader::default();
let page1 = allocate_page(
1,
&Rc::new(BufferPool::new(db_header.page_size as usize)),
&Rc::new(BufferPool::new(db_header.get_page_size() as usize)),
DATABASE_HEADER_SIZE,
);
{
@ -243,7 +279,7 @@ pub fn maybe_init_database_file(file: &Arc<dyn File>, io: &Arc<dyn IO>) -> Resul
&page1,
storage::sqlite3_ondisk::PageType::TableLeaf,
DATABASE_HEADER_SIZE,
db_header.page_size - db_header.reserved_space as u16,
(db_header.get_page_size() - db_header.reserved_space as u32) as u16,
);
let contents = page1.get().contents.as_mut().unwrap();
@ -278,9 +314,9 @@ pub struct Connection {
pager: Rc<Pager>,
schema: Arc<RwLock<Schema>>,
header: Arc<SpinLock<DatabaseHeader>>,
auto_commit: RefCell<bool>,
auto_commit: Cell<bool>,
mv_transactions: RefCell<Vec<crate::mvcc::database::TxID>>,
transaction_state: RefCell<TransactionState>,
transaction_state: Cell<TransactionState>,
last_insert_rowid: Cell<u64>,
last_change: Cell<i64>,
total_changes: Cell<i64>,
@ -517,7 +553,26 @@ impl Connection {
}
pub fn get_auto_commit(&self) -> bool {
*self.auto_commit.borrow()
self.auto_commit.get()
}
pub fn parse_schema_rows(self: &Rc<Connection>) -> Result<()> {
let rows = self.query("SELECT * FROM sqlite_schema")?;
let mut schema = self
.schema
.try_write()
.expect("lock on schema should succeed first try");
{
let syms = self.syms.borrow();
if let Err(LimboError::ExtensionError(e)) =
parse_schema_rows(rows, &mut schema, self.pager.io.clone(), &syms, None)
{
// this means that a vtab exists and we no longer have the module loaded. we print
// a warning to the user to load the module
eprintln!("Warning: {}", e);
}
}
Ok(())
}
}
@ -564,7 +619,7 @@ impl Statement {
self.program.result_columns.len()
}
pub fn get_column_name(&self, idx: usize) -> Cow<String> {
pub fn get_column_name(&self, idx: usize) -> Cow<str> {
let column = &self.program.result_columns[idx];
match column.name(&self.program.table_references) {
Some(name) => Cow::Borrowed(name),
@ -607,12 +662,28 @@ pub struct VirtualTable {
args: Option<Vec<ast::Expr>>,
pub implementation: Rc<VTabModuleImpl>,
columns: Vec<Column>,
kind: VTabKind,
}
impl VirtualTable {
pub(crate) fn rowid(&self, cursor: &VTabOpaqueCursor) -> i64 {
unsafe { (self.implementation.rowid)(cursor.as_ptr()) }
}
pub(crate) fn best_index(
&self,
constraints: &[ConstraintInfo],
order_by: &[OrderByInfo],
) -> IndexInfo {
unsafe {
IndexInfo::from_ffi((self.implementation.best_idx)(
constraints.as_ptr(),
constraints.len() as i32,
order_by.as_ptr(),
order_by.len() as i32,
))
}
}
/// takes ownership of the provided Args
pub(crate) fn from_args(
tbl_name: Option<&str>,
@ -630,7 +701,7 @@ impl VirtualTable {
module_name
)))?;
if let VTabKind::VirtualTable = kind {
if module.module_kind != VTabKind::VirtualTable {
if module.module_kind == VTabKind::TableValuedFunction {
return Err(LimboError::ExtensionError(format!(
"{} is not a virtual table module",
module_name
@ -648,6 +719,7 @@ impl VirtualTable {
implementation: module.implementation.clone(),
columns,
args: exprs,
kind,
});
return Ok(vtab);
}
@ -661,21 +733,30 @@ impl VirtualTable {
VTabOpaqueCursor::new(cursor)
}
#[tracing::instrument(skip(cursor))]
pub fn filter(
&self,
cursor: &VTabOpaqueCursor,
idx_num: i32,
idx_str: Option<String>,
arg_count: usize,
args: Vec<OwnedValue>,
args: Vec<limbo_ext::Value>,
) -> Result<bool> {
let mut filter_args = Vec::with_capacity(arg_count);
for i in 0..arg_count {
let ownedvalue_arg = args.get(i).unwrap();
filter_args.push(ownedvalue_arg.to_ffi());
}
tracing::trace!("xFilter");
let c_idx_str = idx_str
.map(|s| std::ffi::CString::new(s).unwrap())
.map(|cstr| cstr.into_raw())
.unwrap_or(std::ptr::null_mut());
let rc = unsafe {
(self.implementation.filter)(cursor.as_ptr(), arg_count as i32, filter_args.as_ptr())
(self.implementation.filter)(
cursor.as_ptr(),
arg_count as i32,
args.as_ptr(),
c_idx_str,
idx_num,
)
};
for arg in filter_args {
for arg in args {
unsafe {
arg.__free_internal_type();
}
@ -725,6 +806,19 @@ impl VirtualTable {
_ => Err(LimboError::ExtensionError(rc.to_string())),
}
}
pub fn destroy(&self) -> Result<()> {
let implementation = self.implementation.as_ref();
let rc = unsafe {
(self.implementation.destroy)(
implementation as *const VTabModuleImpl as *const std::ffi::c_void,
)
};
match rc {
ResultCode::OK => Ok(()),
_ => Err(LimboError::ExtensionError(rc.to_string())),
}
}
}
pub(crate) struct SymbolTable {

575
core/numeric.rs Normal file
View file

@ -0,0 +1,575 @@
use crate::OwnedValue;
mod nonnan;
use nonnan::NonNan;
// TODO: Remove when https://github.com/rust-lang/libs-team/issues/230 is available
trait SaturatingShl {
fn saturating_shl(self, rhs: u32) -> Self;
}
impl SaturatingShl for i64 {
fn saturating_shl(self, rhs: u32) -> Self {
if rhs >= Self::BITS {
0
} else {
self << rhs
}
}
}
// TODO: Remove when https://github.com/rust-lang/libs-team/issues/230 is available
trait SaturatingShr {
fn saturating_shr(self, rhs: u32) -> Self;
}
impl SaturatingShr for i64 {
fn saturating_shr(self, rhs: u32) -> Self {
if rhs >= Self::BITS {
if self >= 0 {
0
} else {
-1
}
} else {
self >> rhs
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum Numeric {
Null,
Integer(i64),
Float(NonNan),
}
impl Numeric {
pub fn try_into_bool(&self) -> Option<bool> {
match self {
Numeric::Null => None,
Numeric::Integer(0) => Some(false),
Numeric::Float(non_nan) if *non_nan == 0.0 => Some(false),
_ => Some(true),
}
}
}
impl From<Numeric> for NullableInteger {
fn from(value: Numeric) -> Self {
match value {
Numeric::Null => NullableInteger::Null,
Numeric::Integer(v) => NullableInteger::Integer(v),
Numeric::Float(v) => NullableInteger::Integer(f64::from(v) as i64),
}
}
}
impl From<Numeric> for OwnedValue {
fn from(value: Numeric) -> Self {
match value {
Numeric::Null => OwnedValue::Null,
Numeric::Integer(v) => OwnedValue::Integer(v),
Numeric::Float(v) => OwnedValue::Float(v.into()),
}
}
}
impl<T: AsRef<str>> From<T> for Numeric {
fn from(value: T) -> Self {
let text = value.as_ref();
match str_to_f64(text) {
None => Self::Integer(0),
Some(StrToF64::Fractional(value)) => Self::Float(value),
Some(StrToF64::Decimal(real)) => {
let integer = str_to_i64(text).unwrap_or(0);
if real == integer as f64 {
Self::Integer(integer)
} else {
Self::Float(real)
}
}
}
}
}
impl From<OwnedValue> for Numeric {
fn from(value: OwnedValue) -> Self {
Self::from(&value)
}
}
impl From<&OwnedValue> for Numeric {
fn from(value: &OwnedValue) -> Self {
match value {
OwnedValue::Null => Self::Null,
OwnedValue::Integer(v) => Self::Integer(*v),
OwnedValue::Float(v) => match NonNan::new(*v) {
Some(v) => Self::Float(v),
None => Self::Null,
},
OwnedValue::Text(text) => Numeric::from(text.as_str()),
OwnedValue::Blob(blob) => {
let text = String::from_utf8_lossy(blob.as_slice());
Numeric::from(&text)
}
}
}
}
impl std::ops::Add for Numeric {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
match (self, rhs) {
(Numeric::Null, _) | (_, Numeric::Null) => Numeric::Null,
(Numeric::Integer(lhs), Numeric::Integer(rhs)) => match lhs.checked_add(rhs) {
None => Numeric::Float(lhs.into()) + Numeric::Float(rhs.into()),
Some(i) => Numeric::Integer(i),
},
(Numeric::Float(lhs), Numeric::Float(rhs)) => match lhs + rhs {
Some(v) => Numeric::Float(v),
None => Numeric::Null,
},
(f @ Numeric::Float(_), Numeric::Integer(i))
| (Numeric::Integer(i), f @ Numeric::Float(_)) => f + Numeric::Float(i.into()),
}
}
}
impl std::ops::Sub for Numeric {
type Output = Self;
fn sub(self, rhs: Self) -> Self::Output {
match (self, rhs) {
(Numeric::Null, _) | (_, Numeric::Null) => Numeric::Null,
(Numeric::Float(lhs), Numeric::Float(rhs)) => match lhs - rhs {
Some(v) => Numeric::Float(v),
None => Numeric::Null,
},
(Numeric::Integer(lhs), Numeric::Integer(rhs)) => match lhs.checked_sub(rhs) {
None => Numeric::Float(lhs.into()) - Numeric::Float(rhs.into()),
Some(i) => Numeric::Integer(i),
},
(f @ Numeric::Float(_), Numeric::Integer(i)) => f - Numeric::Float(i.into()),
(Numeric::Integer(i), f @ Numeric::Float(_)) => Numeric::Float(i.into()) - f,
}
}
}
impl std::ops::Mul for Numeric {
type Output = Self;
fn mul(self, rhs: Self) -> Self::Output {
match (self, rhs) {
(Numeric::Null, _) | (_, Numeric::Null) => Numeric::Null,
(Numeric::Float(lhs), Numeric::Float(rhs)) => match lhs * rhs {
Some(v) => Numeric::Float(v),
None => Numeric::Null,
},
(Numeric::Integer(lhs), Numeric::Integer(rhs)) => match lhs.checked_mul(rhs) {
None => Numeric::Float(lhs.into()) * Numeric::Float(rhs.into()),
Some(i) => Numeric::Integer(i),
},
(f @ Numeric::Float(_), Numeric::Integer(i))
| (Numeric::Integer(i), f @ Numeric::Float(_)) => f * Numeric::Float(i.into()),
}
}
}
impl std::ops::Div for Numeric {
type Output = Self;
fn div(self, rhs: Self) -> Self::Output {
match (self, rhs) {
(Numeric::Null, _) | (_, Numeric::Null) => Numeric::Null,
(Numeric::Float(lhs), Numeric::Float(rhs)) => match lhs / rhs {
Some(v) if rhs != 0.0 => Numeric::Float(v),
_ => Numeric::Null,
},
(Numeric::Integer(lhs), Numeric::Integer(rhs)) => match lhs.checked_div(rhs) {
None => Numeric::Float(lhs.into()) / Numeric::Float(rhs.into()),
Some(v) => Numeric::Integer(v),
},
(f @ Numeric::Float(_), Numeric::Integer(i)) => f / Numeric::Float(i.into()),
(Numeric::Integer(i), f @ Numeric::Float(_)) => Numeric::Float(i.into()) / f,
}
}
}
impl std::ops::Neg for Numeric {
type Output = Self;
fn neg(self) -> Self::Output {
match self {
Numeric::Null => Numeric::Null,
Numeric::Integer(v) => match v.checked_neg() {
None => -Numeric::Float(v.into()),
Some(i) => Numeric::Integer(i),
},
Numeric::Float(v) => Numeric::Float(-v),
}
}
}
#[derive(Debug)]
pub enum NullableInteger {
Null,
Integer(i64),
}
impl From<NullableInteger> for OwnedValue {
fn from(value: NullableInteger) -> Self {
match value {
NullableInteger::Null => OwnedValue::Null,
NullableInteger::Integer(v) => OwnedValue::Integer(v),
}
}
}
impl<T: AsRef<str>> From<T> for NullableInteger {
fn from(value: T) -> Self {
Self::Integer(str_to_i64(value.as_ref()).unwrap_or(0))
}
}
impl From<OwnedValue> for NullableInteger {
fn from(value: OwnedValue) -> Self {
Self::from(&value)
}
}
impl From<&OwnedValue> for NullableInteger {
fn from(value: &OwnedValue) -> Self {
match value {
OwnedValue::Null => Self::Null,
OwnedValue::Integer(v) => Self::Integer(*v),
OwnedValue::Float(v) => Self::Integer(*v as i64),
OwnedValue::Text(text) => Self::from(text.as_str()),
OwnedValue::Blob(blob) => {
let text = String::from_utf8_lossy(blob.as_slice());
Self::from(text)
}
}
}
}
impl std::ops::Not for NullableInteger {
type Output = Self;
fn not(self) -> Self::Output {
match self {
NullableInteger::Null => NullableInteger::Null,
NullableInteger::Integer(lhs) => NullableInteger::Integer(!lhs),
}
}
}
impl std::ops::BitAnd for NullableInteger {
type Output = Self;
fn bitand(self, rhs: Self) -> Self::Output {
match (self, rhs) {
(NullableInteger::Null, _) | (_, NullableInteger::Null) => NullableInteger::Null,
(NullableInteger::Integer(lhs), NullableInteger::Integer(rhs)) => {
NullableInteger::Integer(lhs & rhs)
}
}
}
}
impl std::ops::BitOr for NullableInteger {
type Output = Self;
fn bitor(self, rhs: Self) -> Self::Output {
match (self, rhs) {
(NullableInteger::Null, _) | (_, NullableInteger::Null) => NullableInteger::Null,
(NullableInteger::Integer(lhs), NullableInteger::Integer(rhs)) => {
NullableInteger::Integer(lhs | rhs)
}
}
}
}
impl std::ops::Shl for NullableInteger {
type Output = Self;
fn shl(self, rhs: Self) -> Self::Output {
match (self, rhs) {
(NullableInteger::Null, _) | (_, NullableInteger::Null) => NullableInteger::Null,
(NullableInteger::Integer(lhs), NullableInteger::Integer(rhs)) => {
NullableInteger::Integer(if rhs.is_positive() {
lhs.saturating_shl(rhs.try_into().unwrap_or(u32::MAX))
} else {
lhs.saturating_shr(rhs.saturating_abs().try_into().unwrap_or(u32::MAX))
})
}
}
}
}
impl std::ops::Shr for NullableInteger {
type Output = Self;
fn shr(self, rhs: Self) -> Self::Output {
match (self, rhs) {
(NullableInteger::Null, _) | (_, NullableInteger::Null) => NullableInteger::Null,
(NullableInteger::Integer(lhs), NullableInteger::Integer(rhs)) => {
NullableInteger::Integer(if rhs.is_positive() {
lhs.saturating_shr(rhs.try_into().unwrap_or(u32::MAX))
} else {
lhs.saturating_shl(rhs.saturating_abs().try_into().unwrap_or(u32::MAX))
})
}
}
}
}
impl std::ops::Rem for NullableInteger {
type Output = Self;
fn rem(self, rhs: Self) -> Self::Output {
match (self, rhs) {
(NullableInteger::Null, _) | (_, NullableInteger::Null) => NullableInteger::Null,
(_, NullableInteger::Integer(0)) => NullableInteger::Null,
(lhs, NullableInteger::Integer(-1)) => lhs % NullableInteger::Integer(1),
(NullableInteger::Integer(lhs), NullableInteger::Integer(rhs)) => {
NullableInteger::Integer(lhs % rhs)
}
}
}
}
// Maximum u64 that can survive a f64 round trip
const MAX_EXACT: u64 = u64::MAX << 11;
const VERTICAL_TAB: char = '\u{b}';
/// Encapsulates Dekker's arithmetic for higher precision. This is spiritually the same as using a
/// f128 for arithmetic, but cross platform and compatible with sqlite.
#[derive(Debug, Clone, Copy)]
struct DoubleDouble(f64, f64);
impl From<u64> for DoubleDouble {
fn from(value: u64) -> Self {
let r = value as f64;
// If the value is smaller than MAX_EXACT, the error isn't significant
let rr = if r <= MAX_EXACT as f64 {
let round_tripped = value as f64 as u64;
let sign = if value >= round_tripped { 1.0 } else { -1.0 };
// Error term is the signed distance of the round tripped value and itself
sign * value.abs_diff(round_tripped) as f64
} else {
0.0
};
DoubleDouble(r, rr)
}
}
impl From<DoubleDouble> for f64 {
fn from(DoubleDouble(a, aa): DoubleDouble) -> Self {
a + aa
}
}
impl std::ops::Mul for DoubleDouble {
type Output = Self;
/// Double-Double multiplication. (self.0, self.1) *= (rhs.0, rhs.1)
///
/// Reference:
/// T. J. Dekker, "A Floating-Point Technique for Extending the Available Precision".
/// 1971-07-26.
///
fn mul(self, rhs: Self) -> Self::Output {
// TODO: Better variable naming
let mask = u64::MAX << 26;
let hx = f64::from_bits(self.0.to_bits() & mask);
let tx = self.0 - hx;
let hy = f64::from_bits(rhs.0.to_bits() & mask);
let ty = rhs.0 - hy;
let p = hx * hy;
let q = hx * ty + tx * hy;
let c = p + q;
let cc = p - c + q + tx * ty;
let cc = self.0 * rhs.1 + self.1 * rhs.0 + cc;
let r = c + cc;
let rr = (c - r) + cc;
DoubleDouble(r, rr)
}
}
impl std::ops::MulAssign for DoubleDouble {
fn mul_assign(&mut self, rhs: Self) {
*self = self.clone() * rhs;
}
}
pub fn str_to_i64(input: impl AsRef<str>) -> Option<i64> {
let input = input
.as_ref()
.trim_matches(|ch: char| ch.is_ascii_whitespace() || ch == VERTICAL_TAB);
let mut iter = input.chars().enumerate().peekable();
iter.next_if(|(_, ch)| matches!(ch, '+' | '-'));
let Some((end, _)) = iter.take_while(|(_, ch)| ch.is_ascii_digit()).last() else {
return Some(0);
};
input[0..=end].parse::<i64>().map_or_else(
|err| match err.kind() {
std::num::IntErrorKind::PosOverflow => Some(i64::MAX),
std::num::IntErrorKind::NegOverflow => Some(i64::MIN),
std::num::IntErrorKind::Empty => unreachable!(),
_ => Some(0),
},
Some,
)
}
pub enum StrToF64 {
Fractional(NonNan),
Decimal(NonNan),
}
pub fn str_to_f64(input: impl AsRef<str>) -> Option<StrToF64> {
let mut input = input
.as_ref()
.trim_matches(|ch: char| ch.is_ascii_whitespace() || ch == VERTICAL_TAB)
.chars()
.peekable();
let sign = match input.next_if(|ch| matches!(ch, '-' | '+')) {
Some('-') => -1.0,
_ => 1.0,
};
let mut had_digits = false;
let mut is_fractional = false;
if matches!(input.peek(), Some('e' | 'E')) {
return None;
}
let mut significant: u64 = 0;
// Copy as many significant digits as we can
while let Some(digit) = input.peek().and_then(|ch| ch.to_digit(10)) {
had_digits = true;
match significant
.checked_mul(10)
.and_then(|v| v.checked_add(digit as u64))
{
Some(new) => significant = new,
None => break,
}
input.next();
}
let mut exponent = 0;
// Increment the exponent for every non significant digit we skipped
while input.next_if(char::is_ascii_digit).is_some() {
exponent += 1
}
if input.next_if(|ch| matches!(ch, '.')).is_some() {
if had_digits || input.peek().is_some_and(char::is_ascii_digit) {
is_fractional = true
}
while let Some(digit) = input.peek().and_then(|ch| ch.to_digit(10)) {
if significant < (u64::MAX - 9) / 10 {
significant = significant * 10 + digit as u64;
exponent -= 1;
}
input.next();
}
};
if input.next_if(|ch| matches!(ch, 'e' | 'E')).is_some() {
let sign = match input.next_if(|ch| matches!(ch, '-' | '+')) {
Some('-') => -1,
_ => 1,
};
if input.peek().is_some_and(char::is_ascii_digit) {
is_fractional = true
}
let e = input.map_while(|ch| ch.to_digit(10)).fold(0, |acc, digit| {
if acc < 1000 {
acc * 10 + digit as i32
} else {
1000
}
});
exponent += sign * e;
};
while exponent.is_positive() && significant < MAX_EXACT / 10 {
significant *= 10;
exponent -= 1;
}
while exponent.is_negative() && significant % 10 == 0 {
significant /= 10;
exponent += 1;
}
let mut result = DoubleDouble::from(significant);
if exponent > 0 {
while exponent >= 100 {
exponent -= 100;
result *= DoubleDouble(1.0e+100, -1.5902891109759918046e+83);
}
while exponent >= 10 {
exponent -= 10;
result *= DoubleDouble(1.0e+10, 0.0);
}
while exponent >= 1 {
exponent -= 1;
result *= DoubleDouble(1.0e+01, 0.0);
}
} else {
while exponent <= -100 {
exponent += 100;
result *= DoubleDouble(1.0e-100, -1.99918998026028836196e-117);
}
while exponent <= -10 {
exponent += 10;
result *= DoubleDouble(1.0e-10, -3.6432197315497741579e-27);
}
while exponent <= -1 {
exponent += 1;
result *= DoubleDouble(1.0e-01, -5.5511151231257827021e-18);
}
}
let result = NonNan::new(f64::from(result) * sign)
.unwrap_or_else(|| NonNan::new(sign * f64::INFINITY).unwrap());
Some(if is_fractional {
StrToF64::Fractional(result)
} else {
StrToF64::Decimal(result)
})
}

105
core/numeric/nonnan.rs Normal file
View file

@ -0,0 +1,105 @@
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct NonNan(f64);
impl NonNan {
pub fn new(value: f64) -> Option<Self> {
if value.is_nan() {
return None;
}
Some(NonNan(value))
}
}
impl PartialEq<NonNan> for f64 {
fn eq(&self, other: &NonNan) -> bool {
*self == other.0
}
}
impl PartialEq<f64> for NonNan {
fn eq(&self, other: &f64) -> bool {
self.0 == *other
}
}
impl PartialOrd<f64> for NonNan {
fn partial_cmp(&self, other: &f64) -> Option<std::cmp::Ordering> {
self.0.partial_cmp(other)
}
}
impl PartialOrd<NonNan> for f64 {
fn partial_cmp(&self, other: &NonNan) -> Option<std::cmp::Ordering> {
self.partial_cmp(&other.0)
}
}
impl From<i64> for NonNan {
fn from(value: i64) -> Self {
NonNan(value as f64)
}
}
impl From<NonNan> for f64 {
fn from(value: NonNan) -> Self {
value.0
}
}
impl std::ops::Deref for NonNan {
type Target = f64;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl std::ops::Add for NonNan {
type Output = Option<NonNan>;
fn add(self, rhs: Self) -> Self::Output {
Self::new(self.0 + rhs.0)
}
}
impl std::ops::Sub for NonNan {
type Output = Option<NonNan>;
fn sub(self, rhs: Self) -> Self::Output {
Self::new(self.0 - rhs.0)
}
}
impl std::ops::Mul for NonNan {
type Output = Option<NonNan>;
fn mul(self, rhs: Self) -> Self::Output {
Self::new(self.0 * rhs.0)
}
}
impl std::ops::Div for NonNan {
type Output = Option<NonNan>;
fn div(self, rhs: Self) -> Self::Output {
Self::new(self.0 / rhs.0)
}
}
impl std::ops::Rem for NonNan {
type Output = Option<NonNan>;
fn rem(self, rhs: Self) -> Self::Output {
Self::new(self.0 % rhs.0)
}
}
impl std::ops::Neg for NonNan {
type Output = Self;
fn neg(self) -> Self::Output {
Self(-self.0)
}
}

View file

@ -1,8 +1,8 @@
use crate::VirtualTable;
use crate::{util::normalize_ident, Result};
use crate::{LimboError, VirtualTable};
use core::fmt;
use fallible_iterator::FallibleIterator;
use limbo_sqlite3_parser::ast::{Expr, Literal, TableOptions};
use limbo_sqlite3_parser::ast::{Expr, Literal, SortOrder, TableOptions};
use limbo_sqlite3_parser::{
ast::{Cmd, CreateTableBody, QualifiedName, ResultColumn, Stmt},
lexer::sql::Parser,
@ -30,6 +30,13 @@ impl Schema {
Self { tables, indexes }
}
pub fn is_unique_idx_name(&self, name: &str) -> bool {
!self
.indexes
.iter()
.any(|idx| idx.1.iter().any(|i| i.name == name))
}
pub fn add_btree_table(&mut self, table: Rc<BTreeTable>) {
let name = normalize_ident(&table.name);
self.tables.insert(name, Table::BTree(table).into());
@ -74,6 +81,14 @@ impl Schema {
.map_or_else(|| &[] as &[Arc<Index>], |v| v.as_slice())
}
pub fn get_index(&self, table_name: &str, index_name: &str) -> Option<&Arc<Index>> {
let name = normalize_ident(table_name);
self.indexes
.get(&name)?
.iter()
.find(|index| index.name == index_name)
}
pub fn remove_indices_for_table(&mut self, table_name: &str) {
let name = normalize_ident(table_name);
self.indexes.remove(&name);
@ -151,15 +166,16 @@ impl PartialEq for Table {
pub struct BTreeTable {
pub root_page: usize,
pub name: String,
pub primary_key_column_names: Vec<String>,
pub primary_key_columns: Vec<(String, SortOrder)>,
pub columns: Vec<Column>,
pub has_rowid: bool,
pub is_strict: bool,
}
impl BTreeTable {
pub fn get_rowid_alias_column(&self) -> Option<(usize, &Column)> {
if self.primary_key_column_names.len() == 1 {
let (idx, col) = self.get_column(&self.primary_key_column_names[0]).unwrap();
if self.primary_key_columns.len() == 1 {
let (idx, col) = self.get_column(&self.primary_key_columns[0].0).unwrap();
if self.column_is_rowid_alias(col) {
return Some((idx, col));
}
@ -171,6 +187,10 @@ impl BTreeTable {
col.is_rowid_alias
}
/// Returns the column position and column for a given column name.
/// Returns None if the column name is not found.
/// E.g. if table is CREATE TABLE t(a, b, c)
/// then get_column("b") returns (1, &Column { .. })
pub fn get_column(&self, name: &str) -> Option<(usize, &Column)> {
let name = normalize_ident(name);
for (i, column) in self.columns.iter().enumerate() {
@ -209,7 +229,7 @@ impl BTreeTable {
}
}
#[derive(Debug)]
#[derive(Debug, Default)]
pub struct PseudoTable {
pub columns: Vec<Column>,
}
@ -245,12 +265,6 @@ impl PseudoTable {
}
}
impl Default for PseudoTable {
fn default() -> Self {
Self::new()
}
}
fn create_table(
tbl_name: QualifiedName,
body: CreateTableBody,
@ -259,14 +273,16 @@ fn create_table(
let table_name = normalize_ident(&tbl_name.name.0);
trace!("Creating table {}", table_name);
let mut has_rowid = true;
let mut primary_key_column_names = vec![];
let mut primary_key_columns = vec![];
let mut cols = vec![];
let is_strict: bool;
match body {
CreateTableBody::ColumnsAndConstraints {
columns,
constraints,
options,
} => {
is_strict = options.contains(TableOptions::STRICT);
if let Some(constraints) = constraints {
for c in constraints {
if let limbo_sqlite3_parser::ast::TableConstraint::PrimaryKey {
@ -274,7 +290,7 @@ fn create_table(
} = c.constraint
{
for column in columns {
primary_key_column_names.push(match column.expr {
let col_name = match column.expr {
Expr::Id(id) => normalize_ident(&id.0),
Expr::Literal(Literal::String(value)) => {
value.trim_matches('\'').to_owned()
@ -282,7 +298,9 @@ fn create_table(
_ => {
todo!("Unsupported primary key expression");
}
});
};
primary_key_columns
.push((col_name, column.order.unwrap_or(SortOrder::Asc)));
}
}
}
@ -339,10 +357,17 @@ fn create_table(
let mut default = None;
let mut primary_key = false;
let mut notnull = false;
let mut order = SortOrder::Asc;
for c_def in &col_def.constraints {
match &c_def.constraint {
limbo_sqlite3_parser::ast::ColumnConstraint::PrimaryKey { .. } => {
limbo_sqlite3_parser::ast::ColumnConstraint::PrimaryKey {
order: o,
..
} => {
primary_key = true;
if let Some(o) = o {
order = o.clone();
}
}
limbo_sqlite3_parser::ast::ColumnConstraint::NotNull { .. } => {
notnull = true;
@ -355,8 +380,11 @@ fn create_table(
}
if primary_key {
primary_key_column_names.push(name.clone());
} else if primary_key_column_names.contains(&name) {
primary_key_columns.push((name.clone(), order));
} else if primary_key_columns
.iter()
.any(|(col_name, _)| col_name == &name)
{
primary_key = true;
}
@ -378,7 +406,7 @@ fn create_table(
};
// flip is_rowid_alias back to false if the table has multiple primary keys
// or if the table has no rowid
if !has_rowid || primary_key_column_names.len() > 1 {
if !has_rowid || primary_key_columns.len() > 1 {
for col in cols.iter_mut() {
col.is_rowid_alias = false;
}
@ -387,8 +415,9 @@ fn create_table(
root_page,
name: table_name,
has_rowid,
primary_key_column_names,
primary_key_columns,
columns: cols,
is_strict,
})
}
@ -455,7 +484,7 @@ pub fn affinity(datatype: &str) -> Affinity {
}
// Rule 3: BLOB or empty -> BLOB affinity (historically called NONE)
if datatype.contains("BLOB") || datatype.is_empty() {
if datatype.contains("BLOB") || datatype.is_empty() || datatype.contains("ANY") {
return Affinity::Blob;
}
@ -478,26 +507,72 @@ pub enum Type {
Blob,
}
/// # SQLite Column Type Affinities
///
/// Each column in an SQLite 3 database is assigned one of the following type affinities:
///
/// TEXT
/// NUMERIC
/// INTEGER
/// REAL
/// BLOB
/// (Historical note: The "BLOB" type affinity used to be called "NONE". But that term was easy to confuse with "no affinity" and so it was renamed.)
/// - **TEXT**
/// - **NUMERIC**
/// - **INTEGER**
/// - **REAL**
/// - **BLOB**
///
/// A column with TEXT affinity stores all data using storage classes NULL, TEXT or BLOB. If numerical data is inserted into a column with TEXT affinity it is converted into text form before being stored.
/// > **Note:** Historically, the "BLOB" type affinity was called "NONE". However, this term was renamed to avoid confusion with "no affinity".
///
/// A column with NUMERIC affinity may contain values using all five storage classes. When text data is inserted into a NUMERIC column, the storage class of the text is converted to INTEGER or REAL (in order of preference) if the text is a well-formed integer or real literal, respectively. If the TEXT value is a well-formed integer literal that is too large to fit in a 64-bit signed integer, it is converted to REAL. For conversions between TEXT and REAL storage classes, only the first 15 significant decimal digits of the number are preserved. If the TEXT value is not a well-formed integer or real literal, then the value is stored as TEXT. For the purposes of this paragraph, hexadecimal integer literals are not considered well-formed and are stored as TEXT. (This is done for historical compatibility with versions of SQLite prior to version 3.8.6 2014-08-15 where hexadecimal integer literals were first introduced into SQLite.) If a floating point value that can be represented exactly as an integer is inserted into a column with NUMERIC affinity, the value is converted into an integer. No attempt is made to convert NULL or BLOB values.
/// ## Affinity Descriptions
///
/// A string might look like a floating-point literal with a decimal point and/or exponent notation but as long as the value can be expressed as an integer, the NUMERIC affinity will convert it into an integer. Hence, the string '3.0e+5' is stored in a column with NUMERIC affinity as the integer 300000, not as the floating point value 300000.0.
/// ### **TEXT**
/// - Stores data using the NULL, TEXT, or BLOB storage classes.
/// - Numerical data inserted into a column with TEXT affinity is converted into text form before being stored.
/// - **Example:**
/// ```sql
/// CREATE TABLE example (col TEXT);
/// INSERT INTO example (col) VALUES (123); -- Stored as '123' (text)
/// SELECT typeof(col) FROM example; -- Returns 'text'
/// ```
///
/// A column that uses INTEGER affinity behaves the same as a column with NUMERIC affinity. The difference between INTEGER and NUMERIC affinity is only evident in a CAST expression: The expression "CAST(4.0 AS INT)" returns an integer 4, whereas "CAST(4.0 AS NUMERIC)" leaves the value as a floating-point 4.0.
/// ### **NUMERIC**
/// - Can store values using all five storage classes.
/// - Text data is converted to INTEGER or REAL (in that order of preference) if it is a well-formed integer or real literal.
/// - If the text represents an integer too large for a 64-bit signed integer, it is converted to REAL.
/// - If the text is not a well-formed literal, it is stored as TEXT.
/// - Hexadecimal integer literals are stored as TEXT for historical compatibility.
/// - Floating-point values that can be exactly represented as integers are converted to integers.
/// - **Example:**
/// ```sql
/// CREATE TABLE example (col NUMERIC);
/// INSERT INTO example (col) VALUES ('3.0e+5'); -- Stored as 300000 (integer)
/// SELECT typeof(col) FROM example; -- Returns 'integer'
/// ```
///
/// A column with REAL affinity behaves like a column with NUMERIC affinity except that it forces integer values into floating point representation. (As an internal optimization, small floating point values with no fractional component and stored in columns with REAL affinity are written to disk as integers in order to take up less space and are automatically converted back into floating point as the value is read out. This optimization is completely invisible at the SQL level and can only be detected by examining the raw bits of the database file.)
/// ### **INTEGER**
/// - Behaves like NUMERIC affinity but differs in `CAST` expressions.
/// - **Example:**
/// ```sql
/// CREATE TABLE example (col INTEGER);
/// INSERT INTO example (col) VALUES (4.0); -- Stored as 4 (integer)
/// SELECT typeof(col) FROM example; -- Returns 'integer'
/// ```
///
/// A column with affinity BLOB does not prefer one storage class over another and no attempt is made to coerce data from one storage class into another.
/// ### **REAL**
/// - Similar to NUMERIC affinity but forces integer values into floating-point representation.
/// - **Optimization:** Small floating-point values with no fractional component may be stored as integers on disk to save space. This is invisible at the SQL level.
/// - **Example:**
/// ```sql
/// CREATE TABLE example (col REAL);
/// INSERT INTO example (col) VALUES (4); -- Stored as 4.0 (real)
/// SELECT typeof(col) FROM example; -- Returns 'real'
/// ```
///
/// ### **BLOB**
/// - Does not prefer any storage class.
/// - No coercion is performed between storage classes.
/// - **Example:**
/// ```sql
/// CREATE TABLE example (col BLOB);
/// INSERT INTO example (col) VALUES (x'1234'); -- Stored as a binary blob
/// SELECT typeof(col) FROM example; -- Returns 'blob'
/// ```
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Affinity {
Integer,
@ -507,11 +582,11 @@ pub enum Affinity {
Numeric,
}
pub const SQLITE_AFF_TEXT: char = 'a';
pub const SQLITE_AFF_NONE: char = 'b'; // Historically called NONE, but it's the same as BLOB
pub const SQLITE_AFF_NUMERIC: char = 'c';
pub const SQLITE_AFF_INTEGER: char = 'd';
pub const SQLITE_AFF_REAL: char = 'e';
pub const SQLITE_AFF_NONE: char = 'A'; // Historically called NONE, but it's the same as BLOB
pub const SQLITE_AFF_TEXT: char = 'B';
pub const SQLITE_AFF_NUMERIC: char = 'C';
pub const SQLITE_AFF_INTEGER: char = 'D';
pub const SQLITE_AFF_REAL: char = 'E';
impl Affinity {
/// This is meant to be used in opcodes like Eq, which state:
@ -530,6 +605,20 @@ impl Affinity {
Affinity::Numeric => SQLITE_AFF_NUMERIC,
}
}
pub fn from_char(char: char) -> Result<Self> {
match char {
SQLITE_AFF_INTEGER => Ok(Affinity::Integer),
SQLITE_AFF_TEXT => Ok(Affinity::Text),
SQLITE_AFF_NONE => Ok(Affinity::Blob),
SQLITE_AFF_REAL => Ok(Affinity::Real),
SQLITE_AFF_NUMERIC => Ok(Affinity::Numeric),
_ => Err(LimboError::InternalError(format!(
"Invalid affinity character: {}",
char
))),
}
}
}
impl fmt::Display for Type {
@ -551,7 +640,8 @@ pub fn sqlite_schema_table() -> BTreeTable {
root_page: 1,
name: "sqlite_schema".to_string(),
has_rowid: true,
primary_key_column_names: vec![],
is_strict: false,
primary_key_columns: vec![],
columns: vec![
Column {
name: Some("type".to_string()),
@ -610,23 +700,24 @@ pub struct Index {
pub root_page: usize,
pub columns: Vec<IndexColumn>,
pub unique: bool,
pub ephemeral: bool,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct IndexColumn {
pub name: String,
pub order: Order,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Order {
Ascending,
Descending,
pub order: SortOrder,
/// the position of the column in the source table.
/// for example:
/// CREATE TABLE t(a,b,c)
/// CREATE INDEX idx ON t(b)
/// b.pos_in_table == 1
pub pos_in_table: usize,
}
impl Index {
pub fn from_sql(sql: &str, root_page: usize) -> Result<Index> {
pub fn from_sql(sql: &str, root_page: usize, table: &BTreeTable) -> Result<Index> {
let mut parser = Parser::new(sql.as_bytes());
let cmd = parser.next()?;
match cmd {
@ -638,23 +729,28 @@ impl Index {
..
})) => {
let index_name = normalize_ident(&idx_name.name.0);
let index_columns = columns
.into_iter()
.map(|col| IndexColumn {
name: normalize_ident(&col.expr.to_string()),
order: match col.order {
Some(limbo_sqlite3_parser::ast::SortOrder::Asc) => Order::Ascending,
Some(limbo_sqlite3_parser::ast::SortOrder::Desc) => Order::Descending,
None => Order::Ascending,
},
})
.collect();
let mut index_columns = Vec::with_capacity(columns.len());
for col in columns.into_iter() {
let name = normalize_ident(&col.expr.to_string());
let Some((pos_in_table, _)) = table.get_column(&name) else {
return Err(crate::LimboError::InternalError(format!(
"Column {} is in index {} but not found in table {}",
name, index_name, table.name
)));
};
index_columns.push(IndexColumn {
name,
order: col.order.unwrap_or(SortOrder::Asc),
pos_in_table,
});
}
Ok(Index {
name: index_name,
table_name: normalize_ident(&tbl_name.0),
root_page,
columns: index_columns,
unique,
ephemeral: false,
})
}
_ => todo!("Expected create index statement"),
@ -666,26 +762,27 @@ impl Index {
index_name: &str,
root_page: usize,
) -> Result<Index> {
if table.primary_key_column_names.is_empty() {
if table.primary_key_columns.is_empty() {
return Err(crate::LimboError::InternalError(
"Cannot create automatic index for table without primary key".to_string(),
));
}
let index_columns = table
.primary_key_column_names
.primary_key_columns
.iter()
.map(|col_name| {
.map(|(col_name, order)| {
// Verify that each primary key column exists in the table
if table.get_column(col_name).is_none() {
let Some((pos_in_table, _)) = table.get_column(col_name) else {
return Err(crate::LimboError::InternalError(format!(
"Primary key column {} not found in table {}",
col_name, table.name
"Column {} is in index {} but not found in table {}",
col_name, index_name, table.name
)));
}
};
Ok(IndexColumn {
name: normalize_ident(col_name),
order: Order::Ascending, // Primary key indexes are always ascending
order: order.clone(),
pos_in_table,
})
})
.collect::<Result<Vec<_>>>()?;
@ -696,8 +793,21 @@ impl Index {
root_page,
columns: index_columns,
unique: true, // Primary key indexes are always unique
ephemeral: false,
})
}
/// Given a column position in the table, return the position in the index.
/// Returns None if the column is not found in the index.
/// For example, given:
/// CREATE TABLE t(a, b, c)
/// CREATE INDEX idx ON t(b)
/// then column_table_pos_to_index_pos(1) returns Some(0)
pub fn column_table_pos_to_index_pos(&self, table_pos: usize) -> Option<usize> {
self.columns
.iter()
.position(|c| c.pos_in_table == table_pos)
}
}
#[cfg(test)]
@ -818,8 +928,8 @@ mod tests {
let column = table.get_column("c").unwrap().1;
assert!(!column.primary_key, "column 'c' shouldn't be a primary key");
assert_eq!(
vec!["a"],
table.primary_key_column_names,
vec![("a".to_string(), SortOrder::Asc)],
table.primary_key_columns,
"primary key column names should be ['a']"
);
Ok(())
@ -836,8 +946,11 @@ mod tests {
let column = table.get_column("c").unwrap().1;
assert!(!column.primary_key, "column 'c' shouldn't be a primary key");
assert_eq!(
vec!["a", "b"],
table.primary_key_column_names,
vec![
("a".to_string(), SortOrder::Asc),
("b".to_string(), SortOrder::Asc)
],
table.primary_key_columns,
"primary key column names should be ['a', 'b']"
);
Ok(())
@ -845,7 +958,7 @@ mod tests {
#[test]
pub fn test_primary_key_separate_single() -> Result<()> {
let sql = r#"CREATE TABLE t1 (a INTEGER, b TEXT, c REAL, PRIMARY KEY(a));"#;
let sql = r#"CREATE TABLE t1 (a INTEGER, b TEXT, c REAL, PRIMARY KEY(a desc));"#;
let table = BTreeTable::from_sql(sql, 0)?;
let column = table.get_column("a").unwrap().1;
assert!(column.primary_key, "column 'a' should be a primary key");
@ -854,8 +967,8 @@ mod tests {
let column = table.get_column("c").unwrap().1;
assert!(!column.primary_key, "column 'c' shouldn't be a primary key");
assert_eq!(
vec!["a"],
table.primary_key_column_names,
vec![("a".to_string(), SortOrder::Desc)],
table.primary_key_columns,
"primary key column names should be ['a']"
);
Ok(())
@ -863,7 +976,7 @@ mod tests {
#[test]
pub fn test_primary_key_separate_multiple() -> Result<()> {
let sql = r#"CREATE TABLE t1 (a INTEGER, b TEXT, c REAL, PRIMARY KEY(a, b));"#;
let sql = r#"CREATE TABLE t1 (a INTEGER, b TEXT, c REAL, PRIMARY KEY(a, b desc));"#;
let table = BTreeTable::from_sql(sql, 0)?;
let column = table.get_column("a").unwrap().1;
assert!(column.primary_key, "column 'a' should be a primary key");
@ -872,8 +985,11 @@ mod tests {
let column = table.get_column("c").unwrap().1;
assert!(!column.primary_key, "column 'c' shouldn't be a primary key");
assert_eq!(
vec!["a", "b"],
table.primary_key_column_names,
vec![
("a".to_string(), SortOrder::Asc),
("b".to_string(), SortOrder::Desc)
],
table.primary_key_columns,
"primary key column names should be ['a', 'b']"
);
Ok(())
@ -890,8 +1006,8 @@ mod tests {
let column = table.get_column("c").unwrap().1;
assert!(!column.primary_key, "column 'c' shouldn't be a primary key");
assert_eq!(
vec!["a"],
table.primary_key_column_names,
vec![("a".to_string(), SortOrder::Asc)],
table.primary_key_columns,
"primary key column names should be ['a']"
);
Ok(())
@ -907,8 +1023,8 @@ mod tests {
let column = table.get_column("c").unwrap().1;
assert!(!column.primary_key, "column 'c' shouldn't be a primary key");
assert_eq!(
vec!["a"],
table.primary_key_column_names,
vec![("a".to_string(), SortOrder::Asc)],
table.primary_key_columns,
"primary key column names should be ['a']"
);
Ok(())
@ -1012,7 +1128,7 @@ mod tests {
assert!(index.unique);
assert_eq!(index.columns.len(), 1);
assert_eq!(index.columns[0].name, "a");
assert!(matches!(index.columns[0].order, Order::Ascending));
assert!(matches!(index.columns[0].order, SortOrder::Asc));
Ok(())
}
@ -1029,8 +1145,8 @@ mod tests {
assert_eq!(index.columns.len(), 2);
assert_eq!(index.columns[0].name, "a");
assert_eq!(index.columns[1].name, "b");
assert!(matches!(index.columns[0].order, Order::Ascending));
assert!(matches!(index.columns[1].order, Order::Ascending));
assert!(matches!(index.columns[0].order, SortOrder::Asc));
assert!(matches!(index.columns[1].order, SortOrder::Asc));
Ok(())
}
@ -1055,7 +1171,8 @@ mod tests {
root_page: 0,
name: "t1".to_string(),
has_rowid: true,
primary_key_column_names: vec!["nonexistent".to_string()],
is_strict: false,
primary_key_columns: vec![("nonexistent".to_string(), SortOrder::Asc)],
columns: vec![Column {
name: Some("a".to_string()),
ty: Type::Integer,

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,3 @@
#[cfg(feature = "fs")]
use crate::error::LimboError;
use crate::{io::Completion, Buffer, Result};
use std::{cell::RefCell, sync::Arc};
@ -70,3 +69,52 @@ impl DatabaseFile {
Self { file }
}
}
pub struct FileMemoryStorage {
file: Arc<dyn crate::io::File>,
}
unsafe impl Send for FileMemoryStorage {}
unsafe impl Sync for FileMemoryStorage {}
impl DatabaseStorage for FileMemoryStorage {
fn read_page(&self, page_idx: usize, c: Completion) -> Result<()> {
let r = match c {
Completion::Read(ref r) => r,
_ => unreachable!(),
};
let size = r.buf().len();
assert!(page_idx > 0);
if !(512..=65536).contains(&size) || size & (size - 1) != 0 {
return Err(LimboError::NotADB);
}
let pos = (page_idx - 1) * size;
self.file.pread(pos, c)?;
Ok(())
}
fn write_page(
&self,
page_idx: usize,
buffer: Arc<RefCell<Buffer>>,
c: Completion,
) -> Result<()> {
let buffer_size = buffer.borrow().len();
assert!(buffer_size >= 512);
assert!(buffer_size <= 65536);
assert_eq!(buffer_size & (buffer_size - 1), 0);
let pos = (page_idx - 1) * buffer_size;
self.file.pwrite(pos, buffer, c)?;
Ok(())
}
fn sync(&self, c: Completion) -> Result<()> {
self.file.sync(c)
}
}
impl FileMemoryStorage {
pub fn new(file: Arc<dyn crate::io::File>) -> Self {
Self { file }
}
}

View file

@ -123,6 +123,13 @@ impl Page {
tracing::debug!("clear loaded {}", self.get().id);
self.get().flags.fetch_and(!PAGE_LOADED, Ordering::SeqCst);
}
pub fn is_index(&self) -> bool {
match self.get_contents().page_type() {
PageType::IndexLeaf | PageType::IndexInterior => true,
PageType::TableLeaf | PageType::TableInterior => false,
}
}
}
#[derive(Clone, Copy, Debug)]
@ -157,7 +164,7 @@ pub struct Pager {
/// Source of the database pages.
pub db_file: Arc<dyn DatabaseStorage>,
/// The write-ahead log (WAL) for the database.
wal: Rc<RefCell<dyn Wal>>,
wal: Option<Rc<RefCell<dyn Wal>>>,
/// A page cache for the database.
page_cache: Arc<RwLock<DumbLruPageCache>>,
/// Buffer pool for temporary data storage.
@ -183,7 +190,7 @@ impl Pager {
pub fn finish_open(
db_header_ref: Arc<SpinLock<DatabaseHeader>>,
db_file: Arc<dyn DatabaseStorage>,
wal: Rc<RefCell<dyn Wal>>,
wal: Option<Rc<RefCell<dyn Wal>>>,
io: Arc<dyn crate::io::IO>,
page_cache: Arc<RwLock<DumbLruPageCache>>,
buffer_pool: Rc<BufferPool>,
@ -206,20 +213,31 @@ impl Pager {
})
}
pub fn btree_create(&self, flags: usize) -> u32 {
pub fn btree_create(&self, flags: &CreateBTreeFlags) -> u32 {
let page_type = match flags {
1 => PageType::TableLeaf,
2 => PageType::IndexLeaf,
_ => unreachable!(
"wrong create table flags, should be 1 for table and 2 for index, got {}",
flags,
),
_ if flags.is_table() => PageType::TableLeaf,
_ if flags.is_index() => PageType::IndexLeaf,
_ => unreachable!("Invalid flags state"),
};
let page = self.do_allocate_page(page_type, 0);
let id = page.get().id;
id as u32
}
/// Allocate a new overflow page.
/// This is done when a cell overflows and new space is needed.
pub fn allocate_overflow_page(&self) -> PageRef {
let page = self.allocate_page().unwrap();
tracing::debug!("Pager::allocate_overflow_page(id={})", page.get().id);
// setup overflow page
let contents = page.get().contents.as_mut().unwrap();
let buf = contents.as_ptr();
buf.fill(0);
page
}
/// Allocate a new page to the btree via the pager.
/// This marks the page as dirty and writes the page header.
pub fn do_allocate_page(&self, page_type: PageType, offset: usize) -> PageRef {
@ -239,33 +257,47 @@ impl Pager {
/// In other words, if the page size is 512, then the reserved space size cannot exceed 32.
pub fn usable_space(&self) -> usize {
let db_header = self.db_header.lock();
(db_header.page_size - db_header.reserved_space as u16) as usize
(db_header.get_page_size() - db_header.reserved_space as u32) as usize
}
#[inline(always)]
pub fn begin_read_tx(&self) -> Result<LimboResult> {
self.wal.borrow_mut().begin_read_tx()
if let Some(wal) = &self.wal {
return wal.borrow_mut().begin_read_tx();
}
Ok(LimboResult::Ok)
}
#[inline(always)]
pub fn begin_write_tx(&self) -> Result<LimboResult> {
self.wal.borrow_mut().begin_write_tx()
if let Some(wal) = &self.wal {
return wal.borrow_mut().begin_write_tx();
}
Ok(LimboResult::Ok)
}
pub fn end_tx(&self) -> Result<CheckpointStatus> {
let checkpoint_status = self.cacheflush()?;
match checkpoint_status {
CheckpointStatus::IO => Ok(checkpoint_status),
CheckpointStatus::Done(_) => {
self.wal.borrow().end_write_tx()?;
self.wal.borrow().end_read_tx()?;
Ok(checkpoint_status)
}
if let Some(wal) = &self.wal {
let checkpoint_status = self.cacheflush()?;
return match checkpoint_status {
CheckpointStatus::IO => Ok(checkpoint_status),
CheckpointStatus::Done(_) => {
wal.borrow().end_write_tx()?;
wal.borrow().end_read_tx()?;
Ok(checkpoint_status)
}
};
}
Ok(CheckpointStatus::Done(CheckpointResult::default()))
}
pub fn end_read_tx(&self) -> Result<()> {
self.wal.borrow().end_read_tx()?;
if let Some(wal) = &self.wal {
wal.borrow().end_read_tx()?;
}
Ok(())
}
@ -273,7 +305,11 @@ impl Pager {
pub fn read_page(&self, page_idx: usize) -> Result<PageRef> {
tracing::trace!("read_page(page_idx = {})", page_idx);
let mut page_cache = self.page_cache.write();
let page_key = PageCacheKey::new(page_idx, Some(self.wal.borrow().get_max_frame()));
let max_frame = match &self.wal {
Some(wal) => wal.borrow().get_max_frame(),
None => 0,
};
let page_key = PageCacheKey::new(page_idx, Some(max_frame));
if let Some(page) = page_cache.get(&page_key) {
tracing::trace!("read_page(page_idx = {}) = cached", page_idx);
return Ok(page.clone());
@ -281,17 +317,18 @@ impl Pager {
let page = Arc::new(Page::new(page_idx));
page.set_locked();
if let Some(frame_id) = self.wal.borrow().find_frame(page_idx as u64)? {
self.wal
.borrow()
.read_frame(frame_id, page.clone(), self.buffer_pool.clone())?;
{
page.set_uptodate();
if let Some(wal) = &self.wal {
if let Some(frame_id) = wal.borrow().find_frame(page_idx as u64)? {
wal.borrow()
.read_frame(frame_id, page.clone(), self.buffer_pool.clone())?;
{
page.set_uptodate();
}
// TODO(pere) ensure page is inserted, we should probably first insert to page cache
// and if successful, read frame or page
page_cache.insert(page_key, page.clone());
return Ok(page);
}
// TODO(pere) ensure page is inserted, we should probably first insert to page cache
// and if successful, read frame or page
page_cache.insert(page_key, page.clone());
return Ok(page);
}
sqlite3_ondisk::begin_read_page(
self.db_file.clone(),
@ -310,19 +347,29 @@ impl Pager {
trace!("load_page(page_idx = {})", id);
let mut page_cache = self.page_cache.write();
page.set_locked();
let page_key = PageCacheKey::new(id, Some(self.wal.borrow().get_max_frame()));
if let Some(frame_id) = self.wal.borrow().find_frame(id as u64)? {
self.wal
.borrow()
.read_frame(frame_id, page.clone(), self.buffer_pool.clone())?;
{
page.set_uptodate();
let max_frame = match &self.wal {
Some(wal) => wal.borrow().get_max_frame(),
None => 0,
};
let page_key = PageCacheKey::new(id, Some(max_frame));
if let Some(wal) = &self.wal {
if let Some(frame_id) = wal.borrow().find_frame(id as u64)? {
wal.borrow()
.read_frame(frame_id, page.clone(), self.buffer_pool.clone())?;
{
page.set_uptodate();
}
// TODO(pere) ensure page is inserted
if !page_cache.contains_key(&page_key) {
page_cache.insert(page_key, page.clone());
}
return Ok(());
}
// TODO(pere) ensure page is inserted
if !page_cache.contains_key(&page_key) {
page_cache.insert(page_key, page.clone());
}
return Ok(());
}
// TODO(pere) ensure page is inserted
if !page_cache.contains_key(&page_key) {
page_cache.insert(page_key, page.clone());
}
sqlite3_ondisk::begin_read_page(
self.db_file.clone(),
@ -330,10 +377,7 @@ impl Pager {
page.clone(),
id,
)?;
// TODO(pere) ensure page is inserted
if !page_cache.contains_key(&page_key) {
page_cache.insert(page_key, page.clone());
}
Ok(())
}
@ -362,18 +406,23 @@ impl Pager {
match state {
FlushState::Start => {
let db_size = self.db_header.lock().database_size;
let max_frame = match &self.wal {
Some(wal) => wal.borrow().get_max_frame(),
None => 0,
};
for page_id in self.dirty_pages.borrow().iter() {
let mut cache = self.page_cache.write();
let page_key =
PageCacheKey::new(*page_id, Some(self.wal.borrow().get_max_frame()));
let page = cache.get(&page_key).expect("we somehow added a page to dirty list but we didn't mark it as dirty, causing cache to drop it.");
let page_type = page.get().contents.as_ref().unwrap().maybe_page_type();
trace!("cacheflush(page={}, page_type={:?}", page_id, page_type);
self.wal.borrow_mut().append_frame(
page.clone(),
db_size,
self.flush_info.borrow().in_flight_writes.clone(),
)?;
let page_key = PageCacheKey::new(*page_id, Some(max_frame));
if let Some(wal) = &self.wal {
let page = cache.get(&page_key).expect("we somehow added a page to dirty list but we didn't mark it as dirty, causing cache to drop it.");
let page_type = page.get().contents.as_ref().unwrap().maybe_page_type();
trace!("cacheflush(page={}, page_type={:?}", page_id, page_type);
wal.borrow_mut().append_frame(
page.clone(),
db_size,
self.flush_info.borrow().in_flight_writes.clone(),
)?;
}
// This page is no longer valid.
// For example:
// We took page with key (page_num, max_frame) -- this page is no longer valid for that max_frame so it must be invalidated.
@ -392,13 +441,16 @@ impl Pager {
}
}
FlushState::SyncWal => {
match self.wal.borrow_mut().sync() {
let wal = self.wal.clone().ok_or(LimboError::InternalError(
"SyncWal was called without a existing wal".to_string(),
))?;
match wal.borrow_mut().sync() {
Ok(CheckpointStatus::IO) => return Ok(CheckpointStatus::IO),
Ok(CheckpointStatus::Done(res)) => checkpoint_result = res,
Err(e) => return Err(e),
}
let should_checkpoint = self.wal.borrow().should_checkpoint();
let should_checkpoint = wal.borrow().should_checkpoint();
if should_checkpoint {
self.flush_info.borrow_mut().state = FlushState::Checkpoint;
} else {
@ -440,11 +492,13 @@ impl Pager {
match state {
CheckpointState::Checkpoint => {
let in_flight = self.checkpoint_inflight.clone();
match self.wal.borrow_mut().checkpoint(
self,
in_flight,
CheckpointMode::Passive,
)? {
let wal = self.wal.clone().ok_or(LimboError::InternalError(
"Checkpoint was called without a existing wal".to_string(),
))?;
match wal
.borrow_mut()
.checkpoint(self, in_flight, CheckpointMode::Passive)?
{
CheckpointStatus::IO => return Ok(CheckpointStatus::IO),
CheckpointStatus::Done(res) => {
checkpoint_result = res;
@ -481,7 +535,7 @@ impl Pager {
pub fn clear_page_cache(&self) -> CheckpointResult {
let checkpoint_result: CheckpointResult;
loop {
match self.wal.borrow_mut().checkpoint(
match self.wal.clone().unwrap().borrow_mut().checkpoint(
self,
Rc::new(RefCell::new(0)),
CheckpointMode::Passive,
@ -606,8 +660,12 @@ impl Pager {
page.set_dirty();
self.add_dirty(page.get().id);
let mut cache = self.page_cache.write();
let page_key =
PageCacheKey::new(page.get().id, Some(self.wal.borrow().get_max_frame()));
let max_frame = match &self.wal {
Some(wal) => wal.borrow().get_max_frame(),
None => 0,
};
let page_key = PageCacheKey::new(page.get().id, Some(max_frame));
cache.insert(page_key, page.clone());
}
Ok(page)
@ -616,14 +674,18 @@ impl Pager {
pub fn put_loaded_page(&self, id: usize, page: PageRef) {
let mut cache = self.page_cache.write();
// cache insert invalidates previous page
let page_key = PageCacheKey::new(id, Some(self.wal.borrow().get_max_frame()));
let max_frame = match &self.wal {
Some(wal) => wal.borrow().get_max_frame(),
None => 0,
};
let page_key = PageCacheKey::new(id, Some(max_frame));
cache.insert(page_key, page.clone());
page.set_loaded();
}
pub fn usable_size(&self) -> usize {
let db_header = self.db_header.lock();
(db_header.page_size - db_header.reserved_space as u16) as usize
(db_header.get_page_size() - db_header.reserved_space as u32) as usize
}
}
@ -637,15 +699,38 @@ pub fn allocate_page(page_id: usize, buffer_pool: &Rc<BufferPool>, offset: usize
});
let buffer = Arc::new(RefCell::new(Buffer::new(buffer, drop_fn)));
page.set_loaded();
page.get().contents = Some(PageContent {
offset,
buffer,
overflow_cells: Vec::new(),
});
page.get().contents = Some(PageContent::new(offset, buffer));
}
page
}
#[derive(Debug)]
pub struct CreateBTreeFlags(pub u8);
impl CreateBTreeFlags {
pub const TABLE: u8 = 0b0001;
pub const INDEX: u8 = 0b0010;
pub fn new_table() -> Self {
Self(CreateBTreeFlags::TABLE)
}
pub fn new_index() -> Self {
Self(CreateBTreeFlags::INDEX)
}
pub fn is_table(&self) -> bool {
(self.0 & CreateBTreeFlags::TABLE) != 0
}
pub fn is_index(&self) -> bool {
(self.0 & CreateBTreeFlags::INDEX) != 0
}
pub fn get_flags(&self) -> u8 {
self.0
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;

View file

@ -47,7 +47,9 @@ use crate::io::{Buffer, Completion, ReadCompletion, SyncCompletion, WriteComplet
use crate::storage::buffer_pool::BufferPool;
use crate::storage::database::DatabaseStorage;
use crate::storage::pager::Pager;
use crate::types::{ImmutableRecord, RawSlice, RefValue, TextRef, TextSubtype};
use crate::types::{
ImmutableRecord, RawSlice, RefValue, SerialType, SerialTypeKind, TextRef, TextSubtype,
};
use crate::{File, Result};
use std::cell::RefCell;
use std::mem::MaybeUninit;
@ -63,9 +65,19 @@ pub const DATABASE_HEADER_SIZE: usize = 100;
// DEFAULT_CACHE_SIZE negative values mean that we store the amount of pages a XKiB of memory can hold.
// We can calculate "real" cache size by diving by page size.
const DEFAULT_CACHE_SIZE: i32 = -2000;
// Minimum number of pages that cache can hold.
pub const MIN_PAGE_CACHE_SIZE: usize = 10;
/// The minimum page size in bytes.
const MIN_PAGE_SIZE: u32 = 512;
/// The maximum page size in bytes.
const MAX_PAGE_SIZE: u32 = 65536;
/// The default page size in bytes.
const DEFAULT_PAGE_SIZE: u16 = 4096;
/// The database header.
/// The first 100 bytes of the database file comprise the database file header.
/// The database file header is divided into fields as shown by the table below.
@ -77,7 +89,7 @@ pub struct DatabaseHeader {
/// The database page size in bytes. Must be a power of two between 512 and 32768 inclusive,
/// or the value 1 representing a page size of 65536.
pub page_size: u16,
page_size: u16,
/// File format write version. 1 for legacy; 2 for WAL.
write_version: u8,
@ -113,7 +125,7 @@ pub struct DatabaseHeader {
pub freelist_pages: u32,
/// The schema cookie. Incremented when the database schema changes.
schema_cookie: u32,
pub schema_cookie: u32,
/// The schema format number. Supported formats are 1, 2, 3, and 4.
schema_format: u32,
@ -168,7 +180,7 @@ pub struct WalHeader {
/// WAL format version. Currently 3007000
pub file_format: u32,
/// Database page size in bytes. Power of two between 512 and 32768 inclusive
/// Database page size in bytes. Power of two between 512 and 65536 inclusive
pub page_size: u32,
/// Checkpoint sequence number. Increases with each checkpoint
@ -217,7 +229,7 @@ impl Default for DatabaseHeader {
fn default() -> Self {
Self {
magic: *b"SQLite format 3\0",
page_size: 4096,
page_size: DEFAULT_PAGE_SIZE,
write_version: 2,
read_version: 2,
reserved_space: 0,
@ -243,6 +255,28 @@ impl Default for DatabaseHeader {
}
}
impl DatabaseHeader {
pub fn update_page_size(&mut self, size: u32) {
if !(MIN_PAGE_SIZE..=MAX_PAGE_SIZE).contains(&size) || (size & (size - 1) != 0) {
return;
}
self.page_size = if size == MAX_PAGE_SIZE {
1u16
} else {
size as u16
};
}
pub fn get_page_size(&self) -> u32 {
if self.page_size == 1 {
MAX_PAGE_SIZE
} else {
self.page_size as u32
}
}
}
pub fn begin_read_database_header(
db_file: Arc<dyn DatabaseStorage>,
) -> Result<Arc<SpinLock<DatabaseHeader>>> {
@ -413,6 +447,14 @@ impl Clone for PageContent {
}
impl PageContent {
pub fn new(offset: usize, buffer: Arc<RefCell<Buffer>>) -> Self {
Self {
offset,
buffer,
overflow_cells: Vec::new(),
}
}
pub fn page_type(&self) -> PageType {
self.read_u8(0).try_into().unwrap()
}
@ -590,6 +632,54 @@ impl PageContent {
usable_size,
)
}
/// Read the rowid of a table interior cell.
#[inline(always)]
pub fn cell_table_interior_read_rowid(&self, idx: usize) -> Result<u64> {
debug_assert!(self.page_type() == PageType::TableInterior);
let buf = self.as_ptr();
const INTERIOR_PAGE_HEADER_SIZE_BYTES: usize = 12;
let cell_pointer_array_start = INTERIOR_PAGE_HEADER_SIZE_BYTES;
let cell_pointer = cell_pointer_array_start + (idx * 2);
let cell_pointer = self.read_u16(cell_pointer) as usize;
const LEFT_CHILD_PAGE_SIZE_BYTES: usize = 4;
let (rowid, _) = read_varint(&buf[cell_pointer + LEFT_CHILD_PAGE_SIZE_BYTES..])?;
Ok(rowid)
}
/// Read the left child page of a table interior cell.
#[inline(always)]
pub fn cell_table_interior_read_left_child_page(&self, idx: usize) -> Result<u32> {
debug_assert!(self.page_type() == PageType::TableInterior);
let buf = self.as_ptr();
const INTERIOR_PAGE_HEADER_SIZE_BYTES: usize = 12;
let cell_pointer_array_start = INTERIOR_PAGE_HEADER_SIZE_BYTES;
let cell_pointer = cell_pointer_array_start + (idx * 2);
let cell_pointer = self.read_u16(cell_pointer) as usize;
Ok(u32::from_be_bytes([
buf[cell_pointer],
buf[cell_pointer + 1],
buf[cell_pointer + 2],
buf[cell_pointer + 3],
]))
}
/// Read the rowid of a table leaf cell.
#[inline(always)]
pub fn cell_table_leaf_read_rowid(&self, idx: usize) -> Result<u64> {
debug_assert!(self.page_type() == PageType::TableLeaf);
let buf = self.as_ptr();
const LEAF_PAGE_HEADER_SIZE_BYTES: usize = 8;
let cell_pointer_array_start = LEAF_PAGE_HEADER_SIZE_BYTES;
let cell_pointer = cell_pointer_array_start + (idx * 2);
let cell_pointer = self.read_u16(cell_pointer) as usize;
let mut pos = cell_pointer;
let (_, nr) = read_varint(&buf[pos..])?;
pos += nr;
let (rowid, _) = read_varint(&buf[pos..])?;
Ok(rowid)
}
/// The cell pointer array of a b-tree page immediately follows the b-tree page header.
/// Let K be the number of cells on the btree.
/// The cell pointer array consists of K 2-byte integer offsets to the cell contents.
@ -626,9 +716,9 @@ impl PageContent {
usable_size,
);
if overflows {
4 + to_read + n_payload + 4
4 + to_read + n_payload
} else {
4 + len_payload as usize + n_payload + 4
4 + len_payload as usize + n_payload
}
}
PageType::TableInterior => {
@ -644,9 +734,9 @@ impl PageContent {
usable_size,
);
if overflows {
to_read + n_payload + 4
to_read + n_payload
} else {
len_payload as usize + n_payload + 4
len_payload as usize + n_payload
}
}
PageType::TableLeaf => {
@ -741,11 +831,7 @@ fn finish_read_page(
} else {
0
};
let inner = PageContent {
offset: pos,
buffer: buffer_ref.clone(),
overflow_cells: Vec::new(),
};
let inner = PageContent::new(pos, buffer_ref.clone());
{
page.get().contents.replace(inner);
page.set_uptodate();
@ -950,116 +1036,24 @@ fn read_payload(unread: &'static [u8], payload_size: usize) -> (&'static [u8], O
}
}
pub type SerialType = u64;
pub const SERIAL_TYPE_NULL: SerialType = 0;
pub const SERIAL_TYPE_INT8: SerialType = 1;
pub const SERIAL_TYPE_BEINT16: SerialType = 2;
pub const SERIAL_TYPE_BEINT24: SerialType = 3;
pub const SERIAL_TYPE_BEINT32: SerialType = 4;
pub const SERIAL_TYPE_BEINT48: SerialType = 5;
pub const SERIAL_TYPE_BEINT64: SerialType = 6;
pub const SERIAL_TYPE_BEFLOAT64: SerialType = 7;
pub const SERIAL_TYPE_CONSTINT0: SerialType = 8;
pub const SERIAL_TYPE_CONSTINT1: SerialType = 9;
pub trait SerialTypeExt {
fn is_null(self) -> bool;
fn is_int8(self) -> bool;
fn is_beint16(self) -> bool;
fn is_beint24(self) -> bool;
fn is_beint32(self) -> bool;
fn is_beint48(self) -> bool;
fn is_beint64(self) -> bool;
fn is_befloat64(self) -> bool;
fn is_constint0(self) -> bool;
fn is_constint1(self) -> bool;
fn is_blob(self) -> bool;
fn is_string(self) -> bool;
fn blob_size(self) -> usize;
fn string_size(self) -> usize;
fn is_valid(self) -> bool;
#[inline(always)]
pub fn validate_serial_type(value: u64) -> Result<()> {
if !SerialType::u64_is_valid_serial_type(value) {
crate::bail_corrupt_error!("Invalid serial type: {}", value);
}
Ok(())
}
impl SerialTypeExt for u64 {
fn is_null(self) -> bool {
self == SERIAL_TYPE_NULL
}
fn is_int8(self) -> bool {
self == SERIAL_TYPE_INT8
}
fn is_beint16(self) -> bool {
self == SERIAL_TYPE_BEINT16
}
fn is_beint24(self) -> bool {
self == SERIAL_TYPE_BEINT24
}
fn is_beint32(self) -> bool {
self == SERIAL_TYPE_BEINT32
}
fn is_beint48(self) -> bool {
self == SERIAL_TYPE_BEINT48
}
fn is_beint64(self) -> bool {
self == SERIAL_TYPE_BEINT64
}
fn is_befloat64(self) -> bool {
self == SERIAL_TYPE_BEFLOAT64
}
fn is_constint0(self) -> bool {
self == SERIAL_TYPE_CONSTINT0
}
fn is_constint1(self) -> bool {
self == SERIAL_TYPE_CONSTINT1
}
fn is_blob(self) -> bool {
self >= 12 && self % 2 == 0
}
fn is_string(self) -> bool {
self >= 13 && self % 2 == 1
}
fn blob_size(self) -> usize {
debug_assert!(self.is_blob());
((self - 12) / 2) as usize
}
fn string_size(self) -> usize {
debug_assert!(self.is_string());
((self - 13) / 2) as usize
}
fn is_valid(self) -> bool {
self <= 9 || self.is_blob() || self.is_string()
}
}
pub fn validate_serial_type(value: u64) -> Result<SerialType> {
if value.is_valid() {
Ok(value)
} else {
crate::bail_corrupt_error!("Invalid serial type: {}", value)
}
}
struct SmallVec<T> {
pub data: [std::mem::MaybeUninit<T>; 64],
pub struct SmallVec<T, const N: usize = 64> {
/// Stack allocated data
pub data: [std::mem::MaybeUninit<T>; N],
/// Length of the vector, accounting for both stack and heap allocated data
pub len: usize,
/// Extra data on heap
pub extra_data: Option<Vec<T>>,
}
impl<T: Default + Copy> SmallVec<T> {
impl<T: Default + Copy, const N: usize> SmallVec<T, N> {
pub fn new() -> Self {
Self {
data: unsafe { std::mem::MaybeUninit::uninit().assume_init() },
@ -1080,6 +1074,50 @@ impl<T: Default + Copy> SmallVec<T> {
self.len += 1;
}
}
fn get_from_heap(&self, index: usize) -> T {
assert!(self.extra_data.is_some());
assert!(index >= self.data.len());
let extra_data_index = index - self.data.len();
let extra_data = self.extra_data.as_ref().unwrap();
assert!(extra_data_index < extra_data.len());
extra_data[extra_data_index]
}
pub fn get(&self, index: usize) -> Option<T> {
if index >= self.len {
return None;
}
let data_is_on_stack = index < self.data.len();
if data_is_on_stack {
// SAFETY: We know this index is initialized we checked for index < self.len earlier above.
unsafe { Some(self.data[index].assume_init()) }
} else {
Some(self.get_from_heap(index))
}
}
}
impl<T: Default + Copy, const N: usize> SmallVec<T, N> {
pub fn iter(&self) -> SmallVecIter<'_, T, N> {
SmallVecIter { vec: self, pos: 0 }
}
}
pub struct SmallVecIter<'a, T, const N: usize> {
vec: &'a SmallVec<T, N>,
pos: usize,
}
impl<'a, T: Default + Copy, const N: usize> Iterator for SmallVecIter<'a, T, N> {
type Item = T;
fn next(&mut self) -> Option<Self::Item> {
self.vec.get(self.pos).map(|item| {
self.pos += 1;
item
})
}
}
pub fn read_record(payload: &[u8], reuse_immutable: &mut ImmutableRecord) -> Result<()> {
@ -1095,10 +1133,10 @@ pub fn read_record(payload: &[u8], reuse_immutable: &mut ImmutableRecord) -> Res
let mut header_size = (header_size as usize) - nr;
pos += nr;
let mut serial_types = SmallVec::new();
let mut serial_types = SmallVec::<u64, 64>::new();
while header_size > 0 {
let (serial_type, nr) = read_varint(&reuse_immutable.get_payload()[pos..])?;
let serial_type = validate_serial_type(serial_type)?;
validate_serial_type(serial_type)?;
serial_types.push(serial_type);
pos += nr;
assert!(header_size >= nr);
@ -1107,14 +1145,17 @@ pub fn read_record(payload: &[u8], reuse_immutable: &mut ImmutableRecord) -> Res
for &serial_type in &serial_types.data[..serial_types.len.min(serial_types.data.len())] {
let (value, n) = read_value(&reuse_immutable.get_payload()[pos..], unsafe {
*serial_type.as_ptr()
serial_type.assume_init().try_into()?
})?;
pos += n;
reuse_immutable.add_value(value);
}
if let Some(extra) = serial_types.extra_data.as_ref() {
for serial_type in extra {
let (value, n) = read_value(&reuse_immutable.get_payload()[pos..], *serial_type)?;
let (value, n) = read_value(
&reuse_immutable.get_payload()[pos..],
(*serial_type).try_into()?,
)?;
pos += n;
reuse_immutable.add_value(value);
}
@ -1127,140 +1168,125 @@ pub fn read_record(payload: &[u8], reuse_immutable: &mut ImmutableRecord) -> Res
/// always.
#[inline(always)]
pub fn read_value(buf: &[u8], serial_type: SerialType) -> Result<(RefValue, usize)> {
if serial_type.is_null() {
return Ok((RefValue::Null, 0));
}
if serial_type.is_int8() {
if buf.is_empty() {
crate::bail_corrupt_error!("Invalid UInt8 value");
match serial_type.kind() {
SerialTypeKind::Null => Ok((RefValue::Null, 0)),
SerialTypeKind::I8 => {
if buf.is_empty() {
crate::bail_corrupt_error!("Invalid UInt8 value");
}
let val = buf[0] as i8;
Ok((RefValue::Integer(val as i64), 1))
}
let val = buf[0] as i8;
return Ok((RefValue::Integer(val as i64), 1));
}
if serial_type.is_beint16() {
if buf.len() < 2 {
crate::bail_corrupt_error!("Invalid BEInt16 value");
SerialTypeKind::I16 => {
if buf.len() < 2 {
crate::bail_corrupt_error!("Invalid BEInt16 value");
}
Ok((
RefValue::Integer(i16::from_be_bytes([buf[0], buf[1]]) as i64),
2,
))
}
return Ok((
RefValue::Integer(i16::from_be_bytes([buf[0], buf[1]]) as i64),
2,
));
}
if serial_type.is_beint24() {
if buf.len() < 3 {
crate::bail_corrupt_error!("Invalid BEInt24 value");
SerialTypeKind::I24 => {
if buf.len() < 3 {
crate::bail_corrupt_error!("Invalid BEInt24 value");
}
let sign_extension = if buf[0] <= 127 { 0 } else { 255 };
Ok((
RefValue::Integer(
i32::from_be_bytes([sign_extension, buf[0], buf[1], buf[2]]) as i64
),
3,
))
}
let sign_extension = if buf[0] <= 127 { 0 } else { 255 };
return Ok((
RefValue::Integer(i32::from_be_bytes([sign_extension, buf[0], buf[1], buf[2]]) as i64),
3,
));
}
if serial_type.is_beint32() {
if buf.len() < 4 {
crate::bail_corrupt_error!("Invalid BEInt32 value");
SerialTypeKind::I32 => {
if buf.len() < 4 {
crate::bail_corrupt_error!("Invalid BEInt32 value");
}
Ok((
RefValue::Integer(i32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]) as i64),
4,
))
}
return Ok((
RefValue::Integer(i32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]) as i64),
4,
));
}
if serial_type.is_beint48() {
if buf.len() < 6 {
crate::bail_corrupt_error!("Invalid BEInt48 value");
SerialTypeKind::I48 => {
if buf.len() < 6 {
crate::bail_corrupt_error!("Invalid BEInt48 value");
}
let sign_extension = if buf[0] <= 127 { 0 } else { 255 };
Ok((
RefValue::Integer(i64::from_be_bytes([
sign_extension,
sign_extension,
buf[0],
buf[1],
buf[2],
buf[3],
buf[4],
buf[5],
])),
6,
))
}
let sign_extension = if buf[0] <= 127 { 0 } else { 255 };
return Ok((
RefValue::Integer(i64::from_be_bytes([
sign_extension,
sign_extension,
buf[0],
buf[1],
buf[2],
buf[3],
buf[4],
buf[5],
])),
6,
));
}
if serial_type.is_beint64() {
if buf.len() < 8 {
crate::bail_corrupt_error!("Invalid BEInt64 value");
SerialTypeKind::I64 => {
if buf.len() < 8 {
crate::bail_corrupt_error!("Invalid BEInt64 value");
}
Ok((
RefValue::Integer(i64::from_be_bytes([
buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7],
])),
8,
))
}
return Ok((
RefValue::Integer(i64::from_be_bytes([
buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7],
])),
8,
));
}
if serial_type.is_befloat64() {
if buf.len() < 8 {
crate::bail_corrupt_error!("Invalid BEFloat64 value");
SerialTypeKind::F64 => {
if buf.len() < 8 {
crate::bail_corrupt_error!("Invalid BEFloat64 value");
}
Ok((
RefValue::Float(f64::from_be_bytes([
buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7],
])),
8,
))
}
return Ok((
RefValue::Float(f64::from_be_bytes([
buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7],
])),
8,
));
}
if serial_type.is_constint0() {
return Ok((RefValue::Integer(0), 0));
}
if serial_type.is_constint1() {
return Ok((RefValue::Integer(1), 0));
}
if serial_type.is_blob() {
let n = serial_type.blob_size();
if buf.len() < n {
crate::bail_corrupt_error!("Invalid Blob value");
SerialTypeKind::ConstInt0 => Ok((RefValue::Integer(0), 0)),
SerialTypeKind::ConstInt1 => Ok((RefValue::Integer(1), 0)),
SerialTypeKind::Blob => {
let content_size = serial_type.size();
if buf.len() < content_size {
crate::bail_corrupt_error!("Invalid Blob value");
}
if content_size == 0 {
Ok((RefValue::Blob(RawSlice::new(std::ptr::null(), 0)), 0))
} else {
let ptr = &buf[0] as *const u8;
let slice = RawSlice::new(ptr, content_size);
Ok((RefValue::Blob(slice), content_size))
}
}
if n == 0 {
return Ok((RefValue::Blob(RawSlice::new(std::ptr::null(), 0)), 0));
SerialTypeKind::Text => {
let content_size = serial_type.size();
if buf.len() < content_size {
crate::bail_corrupt_error!(
"Invalid String value, length {} < expected length {}",
buf.len(),
content_size
);
}
let slice = if content_size == 0 {
RawSlice::new(std::ptr::null(), 0)
} else {
let ptr = &buf[0] as *const u8;
RawSlice::new(ptr, content_size)
};
Ok((
RefValue::Text(TextRef {
value: slice,
subtype: TextSubtype::Text,
}),
content_size,
))
}
let ptr = &buf[0] as *const u8;
let slice = RawSlice::new(ptr, n);
return Ok((RefValue::Blob(slice), n));
}
if serial_type.is_string() {
let n = serial_type.string_size();
if buf.len() < n {
crate::bail_corrupt_error!(
"Invalid String value, length {} < expected length {}",
buf.len(),
n
);
}
let slice = if n == 0 {
RawSlice::new(std::ptr::null(), 0)
} else {
let ptr = &buf[0] as *const u8;
RawSlice::new(ptr, n)
};
return Ok((
RefValue::Text(TextRef {
value: slice,
subtype: TextSubtype::Text,
}),
n,
));
}
// This should never happen if validate_serial_type is used correctly
crate::bail_corrupt_error!("Invalid serial type: {}", serial_type)
}
#[inline(always)]
@ -1393,6 +1419,7 @@ pub fn begin_write_wal_frame(
io: &Arc<dyn File>,
offset: usize,
page: &PageRef,
page_size: u16,
db_size: u32,
write_counter: Rc<RefCell<usize>>,
wal_header: &WalHeader,
@ -1429,15 +1456,16 @@ pub fn begin_write_wal_frame(
let content_len = contents_buf.len();
buf[WAL_FRAME_HEADER_SIZE..WAL_FRAME_HEADER_SIZE + content_len]
.copy_from_slice(contents_buf);
if content_len < 4096 {
buf[WAL_FRAME_HEADER_SIZE + content_len..WAL_FRAME_HEADER_SIZE + 4096].fill(0);
if content_len < page_size as usize {
buf[WAL_FRAME_HEADER_SIZE + content_len..WAL_FRAME_HEADER_SIZE + page_size as usize]
.fill(0);
}
let expects_be = wal_header.magic & 1;
let use_native_endian = cfg!(target_endian = "big") as u32 == expects_be;
let header_checksum = checksum_wal(&buf[0..8], wal_header, checksums, use_native_endian); // Only 8 bytes
let final_checksum = checksum_wal(
&buf[WAL_FRAME_HEADER_SIZE..WAL_FRAME_HEADER_SIZE + 4096],
&buf[WAL_FRAME_HEADER_SIZE..WAL_FRAME_HEADER_SIZE + page_size as usize],
wal_header,
header_checksum,
use_native_endian,
@ -1594,32 +1622,32 @@ mod tests {
use rstest::rstest;
#[rstest]
#[case(&[], SERIAL_TYPE_NULL, OwnedValue::Null)]
#[case(&[255], SERIAL_TYPE_INT8, OwnedValue::Integer(-1))]
#[case(&[0x12, 0x34], SERIAL_TYPE_BEINT16, OwnedValue::Integer(0x1234))]
#[case(&[0xFE], SERIAL_TYPE_INT8, OwnedValue::Integer(-2))]
#[case(&[0x12, 0x34, 0x56], SERIAL_TYPE_BEINT24, OwnedValue::Integer(0x123456))]
#[case(&[0x12, 0x34, 0x56, 0x78], SERIAL_TYPE_BEINT32, OwnedValue::Integer(0x12345678))]
#[case(&[0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC], SERIAL_TYPE_BEINT48, OwnedValue::Integer(0x123456789ABC))]
#[case(&[0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xFF], SERIAL_TYPE_BEINT64, OwnedValue::Integer(0x123456789ABCDEFF))]
#[case(&[0x40, 0x09, 0x21, 0xFB, 0x54, 0x44, 0x2D, 0x18], SERIAL_TYPE_BEFLOAT64, OwnedValue::Float(std::f64::consts::PI))]
#[case(&[1, 2], SERIAL_TYPE_CONSTINT0, OwnedValue::Integer(0))]
#[case(&[65, 66], SERIAL_TYPE_CONSTINT1, OwnedValue::Integer(1))]
#[case(&[1, 2, 3], 18, OwnedValue::Blob(vec![1, 2, 3].into()))]
#[case(&[], 12, OwnedValue::Blob(vec![].into()))] // empty blob
#[case(&[65, 66, 67], 19, OwnedValue::build_text("ABC"))]
#[case(&[0x80], SERIAL_TYPE_INT8, OwnedValue::Integer(-128))]
#[case(&[0x80, 0], SERIAL_TYPE_BEINT16, OwnedValue::Integer(-32768))]
#[case(&[0x80, 0, 0], SERIAL_TYPE_BEINT24, OwnedValue::Integer(-8388608))]
#[case(&[0x80, 0, 0, 0], SERIAL_TYPE_BEINT32, OwnedValue::Integer(-2147483648))]
#[case(&[0x80, 0, 0, 0, 0, 0], SERIAL_TYPE_BEINT48, OwnedValue::Integer(-140737488355328))]
#[case(&[0x80, 0, 0, 0, 0, 0, 0, 0], SERIAL_TYPE_BEINT64, OwnedValue::Integer(-9223372036854775808))]
#[case(&[0x7f], SERIAL_TYPE_INT8, OwnedValue::Integer(127))]
#[case(&[0x7f, 0xff], SERIAL_TYPE_BEINT16, OwnedValue::Integer(32767))]
#[case(&[0x7f, 0xff, 0xff], SERIAL_TYPE_BEINT24, OwnedValue::Integer(8388607))]
#[case(&[0x7f, 0xff, 0xff, 0xff], SERIAL_TYPE_BEINT32, OwnedValue::Integer(2147483647))]
#[case(&[0x7f, 0xff, 0xff, 0xff, 0xff, 0xff], SERIAL_TYPE_BEINT48, OwnedValue::Integer(140737488355327))]
#[case(&[0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff], SERIAL_TYPE_BEINT64, OwnedValue::Integer(9223372036854775807))]
#[case(&[], SerialType::null(), OwnedValue::Null)]
#[case(&[255], SerialType::i8(), OwnedValue::Integer(-1))]
#[case(&[0x12, 0x34], SerialType::i16(), OwnedValue::Integer(0x1234))]
#[case(&[0xFE], SerialType::i8(), OwnedValue::Integer(-2))]
#[case(&[0x12, 0x34, 0x56], SerialType::i24(), OwnedValue::Integer(0x123456))]
#[case(&[0x12, 0x34, 0x56, 0x78], SerialType::i32(), OwnedValue::Integer(0x12345678))]
#[case(&[0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC], SerialType::i48(), OwnedValue::Integer(0x123456789ABC))]
#[case(&[0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xFF], SerialType::i64(), OwnedValue::Integer(0x123456789ABCDEFF))]
#[case(&[0x40, 0x09, 0x21, 0xFB, 0x54, 0x44, 0x2D, 0x18], SerialType::f64(), OwnedValue::Float(std::f64::consts::PI))]
#[case(&[1, 2], SerialType::const_int0(), OwnedValue::Integer(0))]
#[case(&[65, 66], SerialType::const_int1(), OwnedValue::Integer(1))]
#[case(&[1, 2, 3], SerialType::blob(3), OwnedValue::Blob(vec![1, 2, 3].into()))]
#[case(&[], SerialType::blob(0), OwnedValue::Blob(vec![].into()))] // empty blob
#[case(&[65, 66, 67], SerialType::text(3), OwnedValue::build_text("ABC"))]
#[case(&[0x80], SerialType::i8(), OwnedValue::Integer(-128))]
#[case(&[0x80, 0], SerialType::i16(), OwnedValue::Integer(-32768))]
#[case(&[0x80, 0, 0], SerialType::i24(), OwnedValue::Integer(-8388608))]
#[case(&[0x80, 0, 0, 0], SerialType::i32(), OwnedValue::Integer(-2147483648))]
#[case(&[0x80, 0, 0, 0, 0, 0], SerialType::i48(), OwnedValue::Integer(-140737488355328))]
#[case(&[0x80, 0, 0, 0, 0, 0, 0, 0], SerialType::i64(), OwnedValue::Integer(-9223372036854775808))]
#[case(&[0x7f], SerialType::i8(), OwnedValue::Integer(127))]
#[case(&[0x7f, 0xff], SerialType::i16(), OwnedValue::Integer(32767))]
#[case(&[0x7f, 0xff, 0xff], SerialType::i24(), OwnedValue::Integer(8388607))]
#[case(&[0x7f, 0xff, 0xff, 0xff], SerialType::i32(), OwnedValue::Integer(2147483647))]
#[case(&[0x7f, 0xff, 0xff, 0xff, 0xff, 0xff], SerialType::i48(), OwnedValue::Integer(140737488355327))]
#[case(&[0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff], SerialType::i64(), OwnedValue::Integer(9223372036854775807))]
fn test_read_value(
#[case] buf: &[u8],
#[case] serial_type: SerialType,
@ -1631,54 +1659,94 @@ mod tests {
#[test]
fn test_serial_type_helpers() {
assert!(SERIAL_TYPE_NULL.is_null());
assert!(SERIAL_TYPE_INT8.is_int8());
assert!(SERIAL_TYPE_BEINT16.is_beint16());
assert!(SERIAL_TYPE_BEINT24.is_beint24());
assert!(SERIAL_TYPE_BEINT32.is_beint32());
assert!(SERIAL_TYPE_BEINT48.is_beint48());
assert!(SERIAL_TYPE_BEINT64.is_beint64());
assert!(SERIAL_TYPE_BEFLOAT64.is_befloat64());
assert!(SERIAL_TYPE_CONSTINT0.is_constint0());
assert!(SERIAL_TYPE_CONSTINT1.is_constint1());
assert!(12u64.is_blob());
assert!(14u64.is_blob());
assert!(13u64.is_string());
assert!(15u64.is_string());
assert_eq!(12u64.blob_size(), 0);
assert_eq!(14u64.blob_size(), 1);
assert_eq!(16u64.blob_size(), 2);
assert_eq!(13u64.string_size(), 0);
assert_eq!(15u64.string_size(), 1);
assert_eq!(17u64.string_size(), 2);
assert_eq!(
TryInto::<SerialType>::try_into(12u64).unwrap(),
SerialType::blob(0)
);
assert_eq!(
TryInto::<SerialType>::try_into(14u64).unwrap(),
SerialType::blob(1)
);
assert_eq!(
TryInto::<SerialType>::try_into(13u64).unwrap(),
SerialType::text(0)
);
assert_eq!(
TryInto::<SerialType>::try_into(15u64).unwrap(),
SerialType::text(1)
);
assert_eq!(
TryInto::<SerialType>::try_into(16u64).unwrap(),
SerialType::blob(2)
);
assert_eq!(
TryInto::<SerialType>::try_into(17u64).unwrap(),
SerialType::text(2)
);
}
#[rstest]
#[case(0, SERIAL_TYPE_NULL)]
#[case(1, SERIAL_TYPE_INT8)]
#[case(2, SERIAL_TYPE_BEINT16)]
#[case(3, SERIAL_TYPE_BEINT24)]
#[case(4, SERIAL_TYPE_BEINT32)]
#[case(5, SERIAL_TYPE_BEINT48)]
#[case(6, SERIAL_TYPE_BEINT64)]
#[case(7, SERIAL_TYPE_BEFLOAT64)]
#[case(8, SERIAL_TYPE_CONSTINT0)]
#[case(9, SERIAL_TYPE_CONSTINT1)]
#[case(12, 12)] // Blob(0)
#[case(13, 13)] // String(0)
#[case(14, 14)] // Blob(1)
#[case(15, 15)] // String(1)
fn test_validate_serial_type(#[case] input: u64, #[case] expected: SerialType) {
let result = validate_serial_type(input).unwrap();
#[case(0, SerialType::null())]
#[case(1, SerialType::i8())]
#[case(2, SerialType::i16())]
#[case(3, SerialType::i24())]
#[case(4, SerialType::i32())]
#[case(5, SerialType::i48())]
#[case(6, SerialType::i64())]
#[case(7, SerialType::f64())]
#[case(8, SerialType::const_int0())]
#[case(9, SerialType::const_int1())]
#[case(12, SerialType::blob(0))]
#[case(13, SerialType::text(0))]
#[case(14, SerialType::blob(1))]
#[case(15, SerialType::text(1))]
fn test_parse_serial_type(#[case] input: u64, #[case] expected: SerialType) {
let result = SerialType::try_from(input).unwrap();
assert_eq!(result, expected);
}
#[test]
fn test_invalid_serial_type() {
let result = validate_serial_type(10);
assert!(result.is_err());
fn test_validate_serial_type() {
for i in 0..=9 {
let result = validate_serial_type(i);
assert!(result.is_ok());
}
for i in 10..=11 {
let result = validate_serial_type(i);
assert!(result.is_err());
}
for i in 12..=1000 {
let result = validate_serial_type(i);
assert!(result.is_ok());
}
}
#[test]
fn test_smallvec_iter() {
let mut small_vec = SmallVec::<i32, 4>::new();
(0..8).for_each(|i| small_vec.push(i));
let mut iter = small_vec.iter();
assert_eq!(iter.next(), Some(0));
assert_eq!(iter.next(), Some(1));
assert_eq!(iter.next(), Some(2));
assert_eq!(iter.next(), Some(3));
assert_eq!(iter.next(), Some(4));
assert_eq!(iter.next(), Some(5));
assert_eq!(iter.next(), Some(6));
assert_eq!(iter.next(), Some(7));
assert_eq!(iter.next(), None);
}
#[test]
fn test_smallvec_get() {
let mut small_vec = SmallVec::<i32, 4>::new();
(0..8).for_each(|i| small_vec.push(i));
(0..8).for_each(|i| {
assert_eq!(small_vec.get(i), Some(i as i32));
});
assert_eq!(small_vec.get(8), None);
}
}

View file

@ -246,7 +246,7 @@ pub struct WalFile {
sync_state: RefCell<SyncState>,
syncing: Rc<RefCell<bool>>,
page_size: usize,
page_size: u32,
shared: Arc<UnsafeCell<WalFileShared>>,
ongoing_checkpoint: OngoingCheckpoint,
@ -462,6 +462,7 @@ impl Wal for WalFile {
&shared.file,
offset,
&page,
self.page_size as u16,
db_size,
write_counter,
&header,
@ -687,7 +688,7 @@ impl Wal for WalFile {
impl WalFile {
pub fn new(
io: Arc<dyn IO>,
page_size: usize,
page_size: u32,
shared: Arc<UnsafeCell<WalFileShared>>,
buffer_pool: Rc<BufferPool>,
) -> Self {
@ -698,11 +699,10 @@ impl WalFile {
let drop_fn = Rc::new(move |buf| {
buffer_pool.put(buf);
});
checkpoint_page.get().contents = Some(PageContent {
offset: 0,
buffer: Arc::new(RefCell::new(Buffer::new(buffer, drop_fn))),
overflow_cells: Vec::new(),
});
checkpoint_page.get().contents = Some(PageContent::new(
0,
Arc::new(RefCell::new(Buffer::new(buffer, drop_fn))),
));
}
Self {
io,
@ -728,7 +728,7 @@ impl WalFile {
fn frame_offset(&self, frame_id: u64) -> usize {
assert!(frame_id > 0, "Frame ID must be 1-based");
let page_size = self.page_size;
let page_offset = (frame_id - 1) * (page_size + WAL_FRAME_HEADER_SIZE) as u64;
let page_offset = (frame_id - 1) * (page_size + WAL_FRAME_HEADER_SIZE as u32) as u64;
let offset = WAL_HEADER_SIZE as u64 + page_offset;
offset as usize
}
@ -743,7 +743,7 @@ impl WalFileShared {
pub fn open_shared(
io: &Arc<dyn IO>,
path: &str,
page_size: u16,
page_size: u32,
) -> Result<Arc<UnsafeCell<WalFileShared>>> {
let file = io.open_file(path, crate::io::OpenFlags::Create, false)?;
let header = if file.size()? > 0 {
@ -764,7 +764,7 @@ impl WalFileShared {
let mut wal_header = WalHeader {
magic,
file_format: 3007000,
page_size: page_size as u32,
page_size,
checkpoint_seq: 0, // TODO implement sequence number
salt_1: io.generate_random_number() as u32,
salt_2: io.generate_random_number() as u32,

View file

@ -7,7 +7,7 @@ use crate::vdbe::builder::{ProgramBuilder, ProgramBuilderOpts, QueryMode};
use crate::{schema::Schema, Result, SymbolTable};
use limbo_sqlite3_parser::ast::{Expr, Limit, QualifiedName};
use super::plan::TableReference;
use super::plan::{ColumnUsedMask, IterationDirection, TableReference};
pub fn translate_delete(
query_mode: QueryMode,
@ -50,11 +50,20 @@ pub fn prepare_delete_plan(
crate::bail_corrupt_error!("Table is neither a virtual table nor a btree table");
};
let name = tbl_name.name.0.as_str().to_string();
let table_references = vec![TableReference {
let indexes = schema
.get_indices(table.get_name())
.iter()
.cloned()
.collect();
let mut table_references = vec![TableReference {
table,
identifier: name,
op: Operation::Scan { iter_dir: None },
op: Operation::Scan {
iter_dir: IterationDirection::Forwards,
index: None,
},
join_info: None,
col_used_mask: ColumnUsedMask::new(),
}];
let mut where_predicates = vec![];
@ -62,13 +71,13 @@ pub fn prepare_delete_plan(
// Parse the WHERE clause
parse_where(
where_clause.map(|e| *e),
&table_references,
&mut table_references,
None,
&mut where_predicates,
)?;
// Parse the LIMIT/OFFSET clause
let (resolved_limit, resolved_offset) = limit.map_or(Ok((None, None)), |l| parse_limit(*l))?;
let (resolved_limit, resolved_offset) = limit.map_or(Ok((None, None)), |l| parse_limit(&l))?;
let plan = DeletePlan {
table_references,
@ -78,6 +87,7 @@ pub fn prepare_delete_plan(
limit: resolved_limit,
offset: resolved_offset,
contains_constant_false_condition: false,
indexes,
};
Ok(Plan::Delete(plan))
@ -86,7 +96,5 @@ pub fn prepare_delete_plan(
fn estimate_num_instructions(plan: &DeletePlan) -> usize {
let base = 20;
let num_instructions = base + plan.table_references.len() * 10;
num_instructions
base + plan.table_references.len() * 10
}

View file

@ -1,12 +1,17 @@
// This module contains code for emitting bytecode instructions for SQL query execution.
// It handles translating high-level SQL operations into low-level bytecode that can be executed by the virtual machine.
use std::rc::Rc;
use std::sync::Arc;
use limbo_sqlite3_parser::ast::{self};
use crate::function::Func;
use crate::schema::Index;
use crate::translate::plan::{DeletePlan, Plan, Search};
use crate::util::exprs_are_equivalent;
use crate::vdbe::builder::ProgramBuilder;
use crate::vdbe::builder::{CursorType, ProgramBuilder};
use crate::vdbe::insn::{IdxInsertFlags, RegisterOrLiteral};
use crate::vdbe::{insn::Insn, BranchOffset};
use crate::{Result, SymbolTable};
@ -62,6 +67,10 @@ pub struct TranslateCtx<'a> {
pub label_main_loop_end: Option<BranchOffset>,
// First register of the aggregation results
pub reg_agg_start: Option<usize>,
// In non-group-by statements with aggregations (e.g. SELECT foo, bar, sum(baz) FROM t),
// we want to emit the non-aggregate columns (foo and bar) only once.
// This register is a flag that tracks whether we have already done that.
pub reg_nonagg_emit_once_flag: Option<usize>,
// First register of the result columns of the query
pub reg_result_cols_start: Option<usize>,
// The register holding the limit value, if any.
@ -84,11 +93,12 @@ pub struct TranslateCtx<'a> {
// This vector holds the indexes of the result columns that we need to skip.
pub result_columns_to_skip_in_orderby_sorter: Option<Vec<usize>>,
pub resolver: Resolver<'a>,
pub omit_predicates: Vec<usize>,
}
/// Used to distinguish database operations
#[allow(clippy::upper_case_acronyms, dead_code)]
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OperationMode {
SELECT,
INSERT,
@ -115,6 +125,7 @@ fn prologue<'a>(
labels_main_loop: (0..table_count).map(|_| LoopLabels::new(program)).collect(),
label_main_loop_end: None,
reg_agg_start: None,
reg_nonagg_emit_once_flag: None,
reg_limit: None,
reg_offset: None,
reg_limit_offset_sum: None,
@ -125,11 +136,13 @@ fn prologue<'a>(
result_column_indexes_in_orderby_sorter: (0..result_column_count).collect(),
result_columns_to_skip_in_orderby_sorter: None,
resolver: Resolver::new(syms),
omit_predicates: Vec::new(),
};
Ok((t_ctx, init_label, start_offset))
}
#[derive(Clone, Copy, Debug)]
pub enum TransactionMode {
None,
Read,
@ -149,8 +162,7 @@ fn epilogue(
err_code: 0,
description: String::new(),
});
program.resolve_label(init_label, program.offset());
program.preassign_label_to_next_insn(init_label);
match txn_mode {
TransactionMode::Read => program.emit_insn(Insn::Transaction { write: false }),
@ -243,6 +255,18 @@ pub fn emit_query<'a>(
});
}
// For non-grouped aggregation queries that also have non-aggregate columns,
// we need to ensure non-aggregate columns are only emitted once.
// This flag helps track whether we've already emitted these columns.
if !plan.aggregates.is_empty()
&& plan.group_by.is_none()
&& plan.result_columns.iter().any(|c| !c.contains_aggregates)
{
let flag = program.alloc_register();
program.emit_int(0, flag); // Initialize flag to 0 (not yet emitted)
t_ctx.reg_nonagg_emit_once_flag = Some(flag);
}
// Allocate registers for result columns
t_ctx.reg_result_cols_start = Some(program.alloc_registers(plan.result_columns.len()));
@ -251,8 +275,8 @@ pub fn emit_query<'a>(
init_order_by(program, t_ctx, order_by)?;
}
if let Some(ref mut group_by) = plan.group_by {
init_group_by(program, t_ctx, group_by, &plan.aggregates)?;
if let Some(ref group_by) = plan.group_by {
init_group_by(program, t_ctx, group_by, &plan)?;
}
init_loop(
program,
@ -275,7 +299,7 @@ pub fn emit_query<'a>(
condition_metadata,
&t_ctx.resolver,
)?;
program.resolve_label(jump_target_when_true, program.offset());
program.preassign_label_to_next_insn(jump_target_when_true);
}
// Set up main query execution loop
@ -286,8 +310,7 @@ pub fn emit_query<'a>(
// Clean up and close the main execution loop
close_loop(program, t_ctx, &plan.table_references)?;
program.resolve_label(after_main_loop_label, program.offset());
program.preassign_label_to_next_insn(after_main_loop_label);
let mut order_by_necessary = plan.order_by.is_some() && !plan.contains_constant_false_condition;
let order_by = plan.order_by.as_ref();
@ -353,12 +376,17 @@ fn emit_program_for_delete(
&plan.table_references,
&plan.where_clause,
)?;
emit_delete_insns(program, &mut t_ctx, &plan.table_references, &plan.limit)?;
emit_delete_insns(
program,
&mut t_ctx,
&plan.table_references,
&plan.indexes,
&plan.limit,
)?;
// Clean up and close the main execution loop
close_loop(program, &mut t_ctx, &plan.table_references)?;
program.resolve_label(after_main_loop_label, program.offset());
program.preassign_label_to_next_insn(after_main_loop_label);
// Finalize program
epilogue(program, init_label, start_offset, TransactionMode::Write)?;
@ -371,24 +399,28 @@ fn emit_delete_insns(
program: &mut ProgramBuilder,
t_ctx: &mut TranslateCtx,
table_references: &[TableReference],
index_references: &[Arc<Index>],
limit: &Option<isize>,
) -> Result<()> {
let table_reference = table_references.first().unwrap();
let cursor_id = match &table_reference.op {
Operation::Scan { .. } => program.resolve_cursor_id(&table_reference.identifier),
Operation::Search(search) => match search {
Search::RowidEq { .. } | Search::RowidSearch { .. } => {
Search::RowidEq { .. } | Search::Seek { index: None, .. } => {
program.resolve_cursor_id(&table_reference.identifier)
}
Search::IndexSearch { index, .. } => program.resolve_cursor_id(&index.name),
Search::Seek {
index: Some(index), ..
} => program.resolve_cursor_id(&index.name),
},
_ => return Ok(()),
};
let main_table_cursor_id = program.resolve_cursor_id(table_reference.table.get_name());
// Emit the instructions to delete the row
let key_reg = program.alloc_register();
program.emit_insn(Insn::RowId {
cursor_id,
cursor_id: main_table_cursor_id,
dest: key_reg,
});
@ -409,8 +441,43 @@ fn emit_delete_insns(
conflict_action,
});
} else {
program.emit_insn(Insn::DeleteAsync { cursor_id });
program.emit_insn(Insn::DeleteAwait { cursor_id });
for index in index_references {
let index_cursor_id = program.alloc_cursor_id(
Some(index.name.clone()),
crate::vdbe::builder::CursorType::BTreeIndex(index.clone()),
);
program.emit_insn(Insn::OpenWrite {
cursor_id: index_cursor_id,
root_page: RegisterOrLiteral::Literal(index.root_page),
});
let num_regs = index.columns.len() + 1;
let start_reg = program.alloc_registers(num_regs);
// Emit columns that are part of the index
index
.columns
.iter()
.enumerate()
.for_each(|(reg_offset, column_index)| {
program.emit_insn(Insn::Column {
cursor_id: main_table_cursor_id,
column: column_index.pos_in_table,
dest: start_reg + reg_offset,
});
});
program.emit_insn(Insn::RowId {
cursor_id: main_table_cursor_id,
dest: start_reg + num_regs - 1,
});
program.emit_insn(Insn::IdxDelete {
start_reg,
num_regs,
cursor_id: index_cursor_id,
});
}
program.emit_insn(Insn::Delete {
cursor_id: main_table_cursor_id,
});
}
if let Some(limit) = limit {
let limit_reg = program.alloc_register();
@ -442,25 +509,11 @@ fn emit_program_for_update(
// Exit on LIMIT 0
if let Some(0) = plan.limit {
epilogue(program, init_label, start_offset, TransactionMode::Read)?;
epilogue(program, init_label, start_offset, TransactionMode::None)?;
program.result_columns = plan.returning.unwrap_or_default();
program.table_references = plan.table_references;
return Ok(());
}
let after_main_loop_label = program.allocate_label();
t_ctx.label_main_loop_end = Some(after_main_loop_label);
if plan.contains_constant_false_condition {
program.emit_insn(Insn::Goto {
target_pc: after_main_loop_label,
});
}
let skip_label = program.allocate_label();
init_loop(
program,
&mut t_ctx,
&plan.table_references,
OperationMode::UPDATE,
)?;
if t_ctx.reg_limit.is_none() && plan.limit.is_some() {
let reg = program.alloc_register();
t_ctx.reg_limit = Some(reg);
@ -469,6 +522,50 @@ fn emit_program_for_update(
dest: reg,
});
program.mark_last_insn_constant();
if t_ctx.reg_offset.is_none() && plan.offset.is_some_and(|n| n.ne(&0)) {
let reg = program.alloc_register();
t_ctx.reg_offset = Some(reg);
program.emit_insn(Insn::Integer {
value: plan.offset.unwrap() as i64,
dest: reg,
});
program.mark_last_insn_constant();
let combined_reg = program.alloc_register();
t_ctx.reg_limit_offset_sum = Some(combined_reg);
program.emit_insn(Insn::OffsetLimit {
limit_reg: t_ctx.reg_limit.unwrap(),
offset_reg: reg,
combined_reg,
});
}
}
let after_main_loop_label = program.allocate_label();
t_ctx.label_main_loop_end = Some(after_main_loop_label);
if plan.contains_constant_false_condition {
program.emit_insn(Insn::Goto {
target_pc: after_main_loop_label,
});
}
init_loop(
program,
&mut t_ctx,
&plan.table_references,
OperationMode::UPDATE,
)?;
// Open indexes for update.
let mut index_cursors = Vec::with_capacity(plan.indexes_to_update.len());
// TODO: do not reopen if there is table reference using it.
for index in &plan.indexes_to_update {
let index_cursor = program.alloc_cursor_id(
Some(index.table_name.clone()),
CursorType::BTreeIndex(index.clone()),
);
program.emit_insn(Insn::OpenWrite {
cursor_id: index_cursor,
root_page: RegisterOrLiteral::Literal(index.root_page),
});
index_cursors.push(index_cursor);
}
open_loop(
program,
@ -476,11 +573,9 @@ fn emit_program_for_update(
&plan.table_references,
&plan.where_clause,
)?;
emit_update_insns(&plan, &t_ctx, program)?;
program.resolve_label(skip_label, program.offset());
emit_update_insns(&plan, &t_ctx, program, index_cursors)?;
close_loop(program, &mut t_ctx, &plan.table_references)?;
program.resolve_label(after_main_loop_label, program.offset());
program.preassign_label_to_next_insn(after_main_loop_label);
// Finalize program
epilogue(program, init_label, start_offset, TransactionMode::Write)?;
@ -493,17 +588,28 @@ fn emit_update_insns(
plan: &UpdatePlan,
t_ctx: &TranslateCtx,
program: &mut ProgramBuilder,
index_cursors: Vec<usize>,
) -> crate::Result<()> {
let table_ref = &plan.table_references.first().unwrap();
let (cursor_id, index) = match &table_ref.op {
Operation::Scan { .. } => (program.resolve_cursor_id(&table_ref.identifier), None),
let loop_labels = t_ctx.labels_main_loop.first().unwrap();
let (cursor_id, index, is_virtual) = match &table_ref.op {
Operation::Scan { .. } => (
program.resolve_cursor_id(&table_ref.identifier),
None,
table_ref.virtual_table().is_some(),
),
Operation::Search(search) => match search {
&Search::RowidEq { .. } | Search::RowidSearch { .. } => {
(program.resolve_cursor_id(&table_ref.identifier), None)
}
Search::IndexSearch { index, .. } => (
&Search::RowidEq { .. } | Search::Seek { index: None, .. } => (
program.resolve_cursor_id(&table_ref.identifier),
None,
false,
),
Search::Seek {
index: Some(index), ..
} => (
program.resolve_cursor_id(&table_ref.identifier),
Some((index.clone(), program.resolve_cursor_id(&index.name))),
false,
),
},
_ => return Ok(()),
@ -523,25 +629,102 @@ fn emit_update_insns(
meta,
&t_ctx.resolver,
)?;
program.resolve_label(jump_target, program.offset());
program.preassign_label_to_next_insn(jump_target);
}
let first_col_reg = program.alloc_registers(table_ref.table.columns().len());
let rowid_reg = program.alloc_register();
let beg = program.alloc_registers(
table_ref.table.columns().len()
+ if is_virtual {
2 // two args before the relevant columns for VUpdate
} else {
1 // rowid reg
},
);
program.emit_insn(Insn::RowId {
cursor_id,
dest: rowid_reg,
dest: beg,
});
// if no rowid, we're done
program.emit_insn(Insn::IsNull {
reg: rowid_reg,
reg: beg,
target_pc: t_ctx.label_main_loop_end.unwrap(),
});
if is_virtual {
program.emit_insn(Insn::Copy {
src_reg: beg,
dst_reg: beg + 1,
amount: 0,
})
}
if let Some(offset) = t_ctx.reg_offset {
program.emit_insn(Insn::IfPos {
reg: offset,
target_pc: loop_labels.next,
decrement_by: 1,
});
}
for cond in plan.where_clause.iter().filter(|c| c.is_constant()) {
let meta = ConditionMetadata {
jump_if_condition_is_true: false,
jump_target_when_true: BranchOffset::Placeholder,
jump_target_when_false: loop_labels.next,
};
translate_condition_expr(
program,
&plan.table_references,
&cond.expr,
meta,
&t_ctx.resolver,
)?;
}
// Update indexes first. Columns that are updated will be translated from an expression and those who aren't modified will be
// read from table. Mutiple value index key could be updated partially.
for (index, index_cursor) in plan.indexes_to_update.iter().zip(index_cursors) {
let index_record_reg_count = index.columns.len() + 1;
let index_record_reg_start = program.alloc_registers(index_record_reg_count);
for (idx, column) in index.columns.iter().enumerate() {
if let Some((_, expr)) = plan.set_clauses.iter().find(|(i, _)| *i == idx) {
translate_expr(
program,
Some(&plan.table_references),
expr,
index_record_reg_start + idx,
&t_ctx.resolver,
)?;
} else {
program.emit_insn(Insn::Column {
cursor_id: cursor_id,
column: column.pos_in_table,
dest: index_record_reg_start + idx,
});
}
}
program.emit_insn(Insn::RowId {
cursor_id: cursor_id,
dest: index_record_reg_start + index.columns.len(),
});
let index_record_reg = program.alloc_register();
program.emit_insn(Insn::MakeRecord {
start_reg: index_record_reg_start,
count: index_record_reg_count,
dest_reg: index_record_reg,
});
program.emit_insn(Insn::IdxInsert {
cursor_id: index_cursor,
record_reg: index_record_reg,
unpacked_start: Some(index_record_reg_start),
unpacked_count: Some(index_record_reg_count as u16),
flags: IdxInsertFlags::new(),
});
}
// we scan a column at a time, loading either the column's values, or the new value
// from the Set expression, into registers so we can emit a MakeRecord and update the row.
let start = if is_virtual { beg + 2 } else { beg + 1 };
for idx in 0..table_ref.columns().len() {
if let Some((idx, expr)) = plan.set_clauses.iter().find(|(i, _)| *i == idx) {
let target_reg = first_col_reg + idx;
let target_reg = start + idx;
if let Some((_, expr)) = plan.set_clauses.iter().find(|(i, _)| *i == idx) {
translate_expr(
program,
Some(&plan.table_references),
@ -556,9 +739,17 @@ fn emit_update_insns(
.iter()
.position(|c| Some(&c.name) == table_column.name.as_ref())
});
let dest = first_col_reg + idx;
if table_column.primary_key {
program.emit_null(dest, None);
// don't emit null for pkey of virtual tables. they require first two args
// before the 'record' to be explicitly non-null
if table_column.is_rowid_alias && !is_virtual {
program.emit_null(target_reg, None);
} else if is_virtual {
program.emit_insn(Insn::VColumn {
cursor_id,
column: idx,
dest: target_reg,
});
} else {
program.emit_insn(Insn::Column {
cursor_id: *index
@ -572,24 +763,42 @@ fn emit_update_insns(
})
.unwrap_or(&cursor_id),
column: column_idx_in_index.unwrap_or(idx),
dest,
dest: target_reg,
});
}
}
}
let record_reg = program.alloc_register();
program.emit_insn(Insn::MakeRecord {
start_reg: first_col_reg,
count: table_ref.columns().len(),
dest_reg: record_reg,
});
program.emit_insn(Insn::InsertAsync {
cursor: cursor_id,
key_reg: rowid_reg,
record_reg,
flag: 0,
});
program.emit_insn(Insn::InsertAwait { cursor_id });
if let Some(btree_table) = table_ref.btree() {
if btree_table.is_strict {
program.emit_insn(Insn::TypeCheck {
start_reg: start,
count: table_ref.columns().len(),
check_generated: true,
table_reference: Rc::clone(&btree_table),
});
}
let record_reg = program.alloc_register();
program.emit_insn(Insn::MakeRecord {
start_reg: start,
count: table_ref.columns().len(),
dest_reg: record_reg,
});
program.emit_insn(Insn::Insert {
cursor: cursor_id,
key_reg: beg,
record_reg,
flag: 0,
});
} else if let Some(vtab) = table_ref.virtual_table() {
let arg_count = table_ref.columns().len() + 2;
program.emit_insn(Insn::VUpdate {
cursor_id,
arg_count,
start_reg: beg,
vtab_ptr: vtab.implementation.as_ref().ctx as usize,
conflict_action: 0u16,
});
}
if let Some(limit_reg) = t_ctx.reg_limit {
program.emit_insn(Insn::DecrJumpZero {

View file

@ -1,10 +1,14 @@
use limbo_sqlite3_parser::ast::{self, UnaryOperator};
use super::emitter::Resolver;
use super::optimizer::Optimizable;
use super::plan::{Operation, TableReference};
#[cfg(feature = "json")]
use crate::function::JsonFunc;
use crate::function::{Func, FuncCtx, MathFuncArity, ScalarFunc, VectorFunc};
use crate::functions::datetime;
use crate::schema::{Table, Type};
use crate::util::normalize_ident;
use crate::util::{exprs_are_equivalent, normalize_ident};
use crate::vdbe::{
builder::ProgramBuilder,
insn::{CmpInsFlags, Insn},
@ -12,9 +16,6 @@ use crate::vdbe::{
};
use crate::Result;
use super::emitter::Resolver;
use super::plan::{Operation, TableReference};
#[derive(Debug, Clone, Copy)]
pub struct ConditionMetadata {
pub jump_if_condition_is_true: bool,
@ -186,7 +187,9 @@ pub fn translate_condition_expr(
resolver: &Resolver,
) -> Result<()> {
match expr {
ast::Expr::Between { .. } => todo!(),
ast::Expr::Between { .. } => {
unreachable!("expression should have been rewritten in optmizer")
}
ast::Expr::Binary(lhs, ast::Operator::And, rhs) => {
// In a binary AND, never jump to the parent 'jump_target_when_true' label on the first condition, because
// the second condition MUST also be true. Instead we instruct the child expression to jump to a local
@ -203,7 +206,7 @@ pub fn translate_condition_expr(
},
resolver,
)?;
program.resolve_label(jump_target_when_true, program.offset());
program.preassign_label_to_next_insn(jump_target_when_true);
translate_condition_expr(
program,
referenced_tables,
@ -228,7 +231,7 @@ pub fn translate_condition_expr(
},
resolver,
)?;
program.resolve_label(jump_target_when_false, program.offset());
program.preassign_label_to_next_insn(jump_target_when_false);
translate_condition_expr(
program,
referenced_tables,
@ -252,8 +255,8 @@ pub fn translate_condition_expr(
{
let lhs_reg = program.alloc_register();
let rhs_reg = program.alloc_register();
translate_and_mark(program, Some(referenced_tables), lhs, lhs_reg, resolver)?;
translate_and_mark(program, Some(referenced_tables), rhs, rhs_reg, resolver)?;
translate_expr(program, Some(referenced_tables), lhs, lhs_reg, resolver)?;
translate_expr(program, Some(referenced_tables), rhs, rhs_reg, resolver)?;
match op {
ast::Operator::Greater => {
emit_cmp_insn!(program, condition_metadata, Gt, Le, lhs_reg, rhs_reg)
@ -408,7 +411,7 @@ pub fn translate_condition_expr(
}
if !condition_metadata.jump_if_condition_is_true {
program.resolve_label(jump_target_when_true, program.offset());
program.preassign_label_to_next_insn(jump_target_when_true);
}
}
ast::Expr::Like { not, .. } => {
@ -476,6 +479,38 @@ pub fn translate_condition_expr(
Ok(())
}
/// Reason why [translate_expr_no_constant_opt()] was called.
#[derive(Debug)]
pub enum NoConstantOptReason {
/// The expression translation involves reusing register(s),
/// so hoisting those register assignments is not safe.
/// e.g. SELECT COALESCE(1, t.x, NULL) would overwrite 1 with NULL, which is invalid.
RegisterReuse,
}
/// Translate an expression into bytecode via [translate_expr()], and forbid any constant values from being hoisted
/// into the beginning of the program. This is a good idea in most cases where
/// a register will end up being reused e.g. in a coroutine.
pub fn translate_expr_no_constant_opt(
program: &mut ProgramBuilder,
referenced_tables: Option<&[TableReference]>,
expr: &ast::Expr,
target_register: usize,
resolver: &Resolver,
deopt_reason: NoConstantOptReason,
) -> Result<usize> {
tracing::debug!(
"translate_expr_no_constant_opt: expr={:?}, deopt_reason={:?}",
expr,
deopt_reason
);
let next_span_idx = program.constant_spans_next_idx();
let translated = translate_expr(program, referenced_tables, expr, target_register, resolver)?;
program.constant_spans_invalidate_after(next_span_idx);
Ok(translated)
}
/// Translate an expression into bytecode.
pub fn translate_expr(
program: &mut ProgramBuilder,
referenced_tables: Option<&[TableReference]>,
@ -483,34 +518,51 @@ pub fn translate_expr(
target_register: usize,
resolver: &Resolver,
) -> Result<usize> {
let constant_span = if expr.is_constant(resolver) {
if !program.constant_span_is_open() {
Some(program.constant_span_start())
} else {
None
}
} else {
program.constant_span_end_all();
None
};
if let Some(reg) = resolver.resolve_cached_expr_reg(expr) {
program.emit_insn(Insn::Copy {
src_reg: reg,
dst_reg: target_register,
amount: 0,
});
if let Some(span) = constant_span {
program.constant_span_end(span);
}
return Ok(target_register);
}
match expr {
ast::Expr::Between { .. } => todo!(),
ast::Expr::Between { .. } => {
unreachable!("expression should have been rewritten in optmizer")
}
ast::Expr::Binary(e1, op, e2) => {
// Check if both sides of the expression are identical and reuse the same register if so
if e1 == e2 {
// Check if both sides of the expression are equivalent and reuse the same register if so
if exprs_are_equivalent(e1, e2) {
let shared_reg = program.alloc_register();
translate_expr(program, referenced_tables, e1, shared_reg, resolver)?;
emit_binary_insn(program, op, shared_reg, shared_reg, target_register)?;
return Ok(target_register);
Ok(target_register)
} else {
let e1_reg = program.alloc_registers(2);
let e2_reg = e1_reg + 1;
translate_expr(program, referenced_tables, e1, e1_reg, resolver)?;
translate_expr(program, referenced_tables, e2, e2_reg, resolver)?;
emit_binary_insn(program, op, e1_reg, e2_reg, target_register)?;
Ok(target_register)
}
let e1_reg = program.alloc_registers(2);
let e2_reg = e1_reg + 1;
translate_expr(program, referenced_tables, e1, e1_reg, resolver)?;
translate_expr(program, referenced_tables, e2, e2_reg, resolver)?;
emit_binary_insn(program, op, e1_reg, e2_reg, target_register)?;
Ok(target_register)
}
ast::Expr::Case {
base,
@ -541,7 +593,14 @@ pub fn translate_expr(
)?;
};
for (when_expr, then_expr) in when_then_pairs {
translate_expr(program, referenced_tables, when_expr, expr_reg, resolver)?;
translate_expr_no_constant_opt(
program,
referenced_tables,
when_expr,
expr_reg,
resolver,
NoConstantOptReason::RegisterReuse,
)?;
match base_reg {
// CASE 1 WHEN 0 THEN 0 ELSE 1 becomes 1==0, Ne branch to next clause
Some(base_reg) => program.emit_insn(Insn::Ne {
@ -559,12 +618,13 @@ pub fn translate_expr(
}),
};
// THEN...
translate_expr(
translate_expr_no_constant_opt(
program,
referenced_tables,
then_expr,
target_register,
resolver,
NoConstantOptReason::RegisterReuse,
)?;
program.emit_insn(Insn::Goto {
target_pc: return_label,
@ -576,7 +636,14 @@ pub fn translate_expr(
}
match else_expr {
Some(expr) => {
translate_expr(program, referenced_tables, expr, target_register, resolver)?;
translate_expr_no_constant_opt(
program,
referenced_tables,
expr,
target_register,
resolver,
NoConstantOptReason::RegisterReuse,
)?;
}
// If ELSE isn't specified, it means ELSE null.
None => {
@ -586,7 +653,7 @@ pub fn translate_expr(
});
}
};
program.resolve_label(return_label, program.offset());
program.preassign_label_to_next_insn(return_label);
Ok(target_register)
}
ast::Expr::Cast { expr, type_name } => {
@ -772,7 +839,7 @@ pub fn translate_expr(
if let Some(args) = args {
for (i, arg) in args.iter().enumerate() {
// register containing result of each argument expression
translate_and_mark(
translate_expr(
program,
referenced_tables,
arg,
@ -900,12 +967,13 @@ pub fn translate_expr(
// whenever a not null check succeeds, we jump to the end of the series
let label_coalesce_end = program.allocate_label();
for (index, arg) in args.iter().enumerate() {
let reg = translate_expr(
let reg = translate_expr_no_constant_opt(
program,
referenced_tables,
arg,
target_register,
resolver,
NoConstantOptReason::RegisterReuse,
)?;
if index < args.len() - 1 {
program.emit_insn(Insn::NotNull {
@ -987,12 +1055,13 @@ pub fn translate_expr(
};
let temp_reg = program.alloc_register();
translate_expr(
translate_expr_no_constant_opt(
program,
referenced_tables,
&args[0],
temp_reg,
resolver,
NoConstantOptReason::RegisterReuse,
)?;
let before_copy_label = program.allocate_label();
program.emit_insn(Insn::NotNull {
@ -1000,12 +1069,13 @@ pub fn translate_expr(
target_pc: before_copy_label,
});
translate_expr(
translate_expr_no_constant_opt(
program,
referenced_tables,
&args[1],
temp_reg,
resolver,
NoConstantOptReason::RegisterReuse,
)?;
program.resolve_label(before_copy_label, program.offset());
program.emit_insn(Insn::Copy {
@ -1025,12 +1095,13 @@ pub fn translate_expr(
),
};
let temp_reg = program.alloc_register();
translate_expr(
translate_expr_no_constant_opt(
program,
referenced_tables,
&args[0],
temp_reg,
resolver,
NoConstantOptReason::RegisterReuse,
)?;
let jump_target_when_false = program.allocate_label();
program.emit_insn(Insn::IfNot {
@ -1038,26 +1109,28 @@ pub fn translate_expr(
target_pc: jump_target_when_false,
jump_if_null: true,
});
translate_expr(
translate_expr_no_constant_opt(
program,
referenced_tables,
&args[1],
target_register,
resolver,
NoConstantOptReason::RegisterReuse,
)?;
let jump_target_result = program.allocate_label();
program.emit_insn(Insn::Goto {
target_pc: jump_target_result,
});
program.resolve_label(jump_target_when_false, program.offset());
translate_expr(
program.preassign_label_to_next_insn(jump_target_when_false);
translate_expr_no_constant_opt(
program,
referenced_tables,
&args[2],
target_register,
resolver,
NoConstantOptReason::RegisterReuse,
)?;
program.resolve_label(jump_target_result, program.offset());
program.preassign_label_to_next_insn(jump_target_result);
Ok(target_register)
}
ScalarFunc::Glob | ScalarFunc::Like => {
@ -1109,7 +1182,7 @@ pub fn translate_expr(
| ScalarFunc::ZeroBlob => {
let args = expect_arguments_exact!(args, 1, srf);
let start_reg = program.alloc_register();
translate_and_mark(
translate_expr(
program,
referenced_tables,
&args[0],
@ -1128,7 +1201,7 @@ pub fn translate_expr(
ScalarFunc::LoadExtension => {
let args = expect_arguments_exact!(args, 1, srf);
let start_reg = program.alloc_register();
translate_and_mark(
translate_expr(
program,
referenced_tables,
&args[0],
@ -1159,13 +1232,13 @@ pub fn translate_expr(
});
Ok(target_register)
}
ScalarFunc::Date | ScalarFunc::DateTime => {
ScalarFunc::Date | ScalarFunc::DateTime | ScalarFunc::JulianDay => {
let start_reg = program
.alloc_registers(args.as_ref().map(|x| x.len()).unwrap_or(1));
if let Some(args) = args {
for (i, arg) in args.iter().enumerate() {
// register containing result of each argument expression
translate_and_mark(
translate_expr(
program,
referenced_tables,
arg,
@ -1244,7 +1317,7 @@ pub fn translate_expr(
crate::bail_parse_error!("hex function with no arguments",);
};
let start_reg = program.alloc_register();
translate_and_mark(
translate_expr(
program,
referenced_tables,
&args[0],
@ -1259,11 +1332,11 @@ pub fn translate_expr(
});
Ok(target_register)
}
ScalarFunc::UnixEpoch | ScalarFunc::JulianDay => {
ScalarFunc::UnixEpoch => {
let mut start_reg = 0;
match args {
Some(args) if args.len() > 1 => {
crate::bail_parse_error!("epoch or julianday function with > 1 arguments. Modifiers are not yet supported.");
crate::bail_parse_error!("epoch function with > 1 arguments. Modifiers are not yet supported.");
}
Some(args) if args.len() == 1 => {
let arg_reg = program.alloc_register();
@ -1292,7 +1365,7 @@ pub fn translate_expr(
if let Some(args) = args {
for (i, arg) in args.iter().enumerate() {
// register containing result of each argument expression
translate_and_mark(
translate_expr(
program,
referenced_tables,
arg,
@ -1309,6 +1382,33 @@ pub fn translate_expr(
});
Ok(target_register)
}
ScalarFunc::TimeDiff => {
let args = expect_arguments_exact!(args, 2, srf);
let start_reg = program.alloc_registers(2);
translate_expr(
program,
referenced_tables,
&args[0],
start_reg,
resolver,
)?;
translate_expr(
program,
referenced_tables,
&args[1],
start_reg + 1,
resolver,
)?;
program.emit_insn(Insn::Function {
constant_mask: 0,
start_reg,
dest: target_register,
func: func_ctx,
});
Ok(target_register)
}
ScalarFunc::TotalChanges => {
if args.is_some() {
crate::bail_parse_error!(
@ -1334,7 +1434,7 @@ pub fn translate_expr(
let start_reg = program.alloc_registers(args.len());
for (i, arg) in args.iter().enumerate() {
translate_and_mark(
translate_expr(
program,
referenced_tables,
arg,
@ -1363,7 +1463,7 @@ pub fn translate_expr(
};
let start_reg = program.alloc_registers(args.len());
for (i, arg) in args.iter().enumerate() {
translate_and_mark(
translate_expr(
program,
referenced_tables,
arg,
@ -1393,7 +1493,7 @@ pub fn translate_expr(
};
let start_reg = program.alloc_registers(args.len());
for (i, arg) in args.iter().enumerate() {
translate_and_mark(
translate_expr(
program,
referenced_tables,
arg,
@ -1546,7 +1646,7 @@ pub fn translate_expr(
if let Some(args) = args {
for (i, arg) in args.iter().enumerate() {
// register containing result of each argument expression
translate_and_mark(
translate_expr(
program,
referenced_tables,
arg,
@ -1571,6 +1671,85 @@ pub fn translate_expr(
target_register,
func_ctx,
),
ScalarFunc::Likely => {
let args = if let Some(args) = args {
if args.len() != 1 {
crate::bail_parse_error!(
"likely function must have exactly 1 argument",
);
}
args
} else {
crate::bail_parse_error!("likely function with no arguments",);
};
let start_reg = program.alloc_register();
translate_expr(
program,
referenced_tables,
&args[0],
start_reg,
resolver,
)?;
program.emit_insn(Insn::Function {
constant_mask: 0,
start_reg,
dest: target_register,
func: func_ctx,
});
Ok(target_register)
}
ScalarFunc::Likelihood => {
let args = if let Some(args) = args {
if args.len() != 2 {
crate::bail_parse_error!(
"likelihood() function must have exactly 2 arguments",
);
}
args
} else {
crate::bail_parse_error!("likelihood() function with no arguments",);
};
if let ast::Expr::Literal(ast::Literal::Numeric(ref value)) = args[1] {
if let Ok(probability) = value.parse::<f64>() {
if !(0.0..=1.0).contains(&probability) {
crate::bail_parse_error!(
"second argument of likelihood() must be between 0.0 and 1.0",
);
}
if !value.contains('.') {
crate::bail_parse_error!(
"second argument of likelihood() must be a floating point number with decimal point",
);
}
} else {
crate::bail_parse_error!(
"second argument of likelihood() must be a floating point constant",
);
}
} else {
crate::bail_parse_error!(
"second argument of likelihood() must be a numeric literal",
);
}
let start_reg = program.alloc_register();
translate_expr(
program,
referenced_tables,
&args[0],
start_reg,
resolver,
)?;
program.emit_insn(Insn::Copy {
src_reg: start_reg,
dst_reg: target_register,
amount: 0,
});
Ok(target_register)
}
}
}
Func::Math(math_func) => match math_func.arity() {
@ -1591,13 +1770,7 @@ pub fn translate_expr(
MathFuncArity::Unary => {
let args = expect_arguments_exact!(args, 1, math_func);
let start_reg = program.alloc_register();
translate_and_mark(
program,
referenced_tables,
&args[0],
start_reg,
resolver,
)?;
translate_expr(program, referenced_tables, &args[0], start_reg, resolver)?;
program.emit_insn(Insn::Function {
constant_mask: 0,
start_reg,
@ -1664,41 +1837,79 @@ pub fn translate_expr(
is_rowid_alias,
} => {
let table_reference = referenced_tables.as_ref().unwrap().get(*table).unwrap();
let index = table_reference.op.index();
let use_covering_index = table_reference.utilizes_covering_index();
match table_reference.op {
// If we are reading a column from a table, we find the cursor that corresponds to
// the table and read the column from the cursor.
Operation::Scan { .. } | Operation::Search(_) => match &table_reference.table {
Table::BTree(_) => {
let cursor_id = program.resolve_cursor_id(&table_reference.identifier);
if *is_rowid_alias {
program.emit_insn(Insn::RowId {
cursor_id,
dest: target_register,
});
} else {
program.emit_insn(Insn::Column {
// If we have a covering index, we don't have an open table cursor so we read from the index cursor.
Operation::Scan { .. } | Operation::Search(_) => {
match &table_reference.table {
Table::BTree(_) => {
let table_cursor_id = if use_covering_index {
None
} else {
Some(program.resolve_cursor_id(&table_reference.identifier))
};
let index_cursor_id = if let Some(index) = index {
Some(program.resolve_cursor_id(&index.name))
} else {
None
};
if *is_rowid_alias {
if let Some(index_cursor_id) = index_cursor_id {
program.emit_insn(Insn::IdxRowId {
cursor_id: index_cursor_id,
dest: target_register,
});
} else if let Some(table_cursor_id) = table_cursor_id {
program.emit_insn(Insn::RowId {
cursor_id: table_cursor_id,
dest: target_register,
});
} else {
unreachable!("Either index or table cursor must be opened");
}
} else {
let read_cursor = if use_covering_index {
index_cursor_id
.expect("index cursor should be opened when use_covering_index=true")
} else {
table_cursor_id
.expect("table cursor should be opened when use_covering_index=false")
};
let column = if use_covering_index {
let index = index.expect("index cursor should be opened when use_covering_index=true");
index.column_table_pos_to_index_pos(*column).unwrap_or_else(|| {
panic!("covering index {} does not contain column number {} of table {}", index.name, column, table_reference.identifier)
})
} else {
*column
};
program.emit_insn(Insn::Column {
cursor_id: read_cursor,
column,
dest: target_register,
});
}
let Some(column) = table_reference.table.get_column_at(*column) else {
crate::bail_parse_error!("column index out of bounds");
};
maybe_apply_affinity(column.ty, target_register, program);
Ok(target_register)
}
Table::Virtual(_) => {
let cursor_id = program.resolve_cursor_id(&table_reference.identifier);
program.emit_insn(Insn::VColumn {
cursor_id,
column: *column,
dest: target_register,
});
Ok(target_register)
}
let Some(column) = table_reference.table.get_column_at(*column) else {
crate::bail_parse_error!("column index out of bounds");
};
maybe_apply_affinity(column.ty, target_register, program);
Ok(target_register)
_ => unreachable!(),
}
Table::Virtual(_) => {
let cursor_id = program.resolve_cursor_id(&table_reference.identifier);
program.emit_insn(Insn::VColumn {
cursor_id,
column: *column,
dest: target_register,
});
Ok(target_register)
}
_ => unreachable!(),
},
}
// If we are reading a column from a subquery, we instead copy the column from the
// subquery's result registers.
Operation::Subquery {
@ -1716,11 +1927,23 @@ pub fn translate_expr(
}
ast::Expr::RowId { database: _, table } => {
let table_reference = referenced_tables.as_ref().unwrap().get(*table).unwrap();
let cursor_id = program.resolve_cursor_id(&table_reference.identifier);
program.emit_insn(Insn::RowId {
cursor_id,
dest: target_register,
});
let index = table_reference.op.index();
let use_covering_index = table_reference.utilizes_covering_index();
if use_covering_index {
let index =
index.expect("index cursor should be opened when use_covering_index=true");
let cursor_id = program.resolve_cursor_id(&index.name);
program.emit_insn(Insn::IdxRowId {
cursor_id,
dest: target_register,
});
} else {
let cursor_id = program.resolve_cursor_id(&table_reference.identifier);
program.emit_insn(Insn::RowId {
cursor_id,
dest: target_register,
});
}
Ok(target_register)
}
ast::Expr::InList { .. } => todo!(),
@ -1744,8 +1967,14 @@ pub fn translate_expr(
}
ast::Expr::Literal(lit) => match lit {
ast::Literal::Numeric(val) => {
let maybe_int = val.parse::<i64>();
if let Ok(int_value) = maybe_int {
if val.starts_with("0x") || val.starts_with("0X") {
// must be a hex decimal
let int_value = i64::from_str_radix(&val[2..], 16)?;
program.emit_insn(Insn::Integer {
value: int_value,
dest: target_register,
});
} else if let Ok(int_value) = val.parse::<i64>() {
program.emit_insn(Insn::Integer {
value: int_value,
dest: target_register,
@ -1791,9 +2020,27 @@ pub fn translate_expr(
});
Ok(target_register)
}
ast::Literal::CurrentDate => todo!(),
ast::Literal::CurrentTime => todo!(),
ast::Literal::CurrentTimestamp => todo!(),
ast::Literal::CurrentDate => {
program.emit_insn(Insn::String8 {
value: datetime::exec_date(&[]).to_string(),
dest: target_register,
});
Ok(target_register)
}
ast::Literal::CurrentTime => {
program.emit_insn(Insn::String8 {
value: datetime::exec_time(&[]).to_string(),
dest: target_register,
});
Ok(target_register)
}
ast::Literal::CurrentTimestamp => {
program.emit_insn(Insn::String8 {
value: datetime::exec_datetime_full(&[]).to_string(),
dest: target_register,
});
Ok(target_register)
}
},
ast::Expr::Name(_) => todo!(),
ast::Expr::NotNull(_) => todo!(),
@ -1829,14 +2076,22 @@ pub fn translate_expr(
// Special case: if we're negating "9223372036854775808", this is exactly MIN_INT64
// If we don't do this -1 * 9223372036854775808 will overflow and parse will fail
// and trigger conversion to Real.
if numeric_value == "9223372036854775808" {
if numeric_value == "9223372036854775808"
|| numeric_value.eq_ignore_ascii_case("0x7fffffffffffffff")
{
program.emit_insn(Insn::Integer {
value: i64::MIN,
dest: target_register,
});
} else {
let maybe_int = numeric_value.parse::<i64>();
if let Ok(value) = maybe_int {
if numeric_value.starts_with("0x") || numeric_value.starts_with("0X") {
// must be a hex decimal
let int_value = i64::from_str_radix(&numeric_value[2..], 16)?;
program.emit_insn(Insn::Integer {
value: -int_value,
dest: target_register,
});
} else if let Ok(value) = numeric_value.parse::<i64>() {
program.emit_insn(Insn::Integer {
value: value * -1,
dest: target_register,
@ -1852,7 +2107,7 @@ pub fn translate_expr(
Ok(target_register)
}
(UnaryOperator::Negative, _) => {
let value = -1;
let value = 0;
let reg = program.alloc_register();
translate_expr(program, referenced_tables, expr, reg, resolver)?;
@ -1862,7 +2117,7 @@ pub fn translate_expr(
dest: zero_reg,
});
program.mark_last_insn_constant();
program.emit_insn(Insn::Multiply {
program.emit_insn(Insn::Subtract {
lhs: zero_reg,
rhs: reg,
dest: target_register,
@ -1870,8 +2125,13 @@ pub fn translate_expr(
Ok(target_register)
}
(UnaryOperator::BitwiseNot, ast::Expr::Literal(ast::Literal::Numeric(num_val))) => {
let maybe_int = num_val.parse::<i64>();
if let Ok(val) = maybe_int {
if num_val.starts_with("0x") || num_val.starts_with("0X") {
let int_value = i64::from_str_radix(&num_val[2..], 16)?;
program.emit_insn(Insn::Integer {
value: !int_value,
dest: target_register,
});
} else if let Ok(val) = num_val.parse::<i64>() {
program.emit_insn(Insn::Integer {
value: !val,
dest: target_register,
@ -1919,7 +2179,13 @@ pub fn translate_expr(
});
Ok(target_register)
}
}?;
if let Some(span) = constant_span {
program.constant_span_end(span);
}
Ok(target_register)
}
fn emit_binary_insn(
@ -2188,17 +2454,11 @@ fn translate_like_base(
let arg_count = if matches!(escape, Some(_)) { 3 } else { 2 };
let start_reg = program.alloc_registers(arg_count);
let mut constant_mask = 0;
translate_and_mark(program, referenced_tables, lhs, start_reg + 1, resolver)?;
translate_expr(program, referenced_tables, lhs, start_reg + 1, resolver)?;
let _ = translate_expr(program, referenced_tables, rhs, start_reg, resolver)?;
if arg_count == 3 {
if let Some(escape) = escape {
translate_and_mark(
program,
referenced_tables,
escape,
start_reg + 2,
resolver,
)?;
translate_expr(program, referenced_tables, escape, start_reg + 2, resolver)?;
}
}
if matches!(rhs.as_ref(), ast::Expr::Literal(_)) {
@ -2303,20 +2563,6 @@ pub fn maybe_apply_affinity(col_type: Type, target_register: usize, program: &mu
}
}
pub fn translate_and_mark(
program: &mut ProgramBuilder,
referenced_tables: Option<&[TableReference]>,
expr: &ast::Expr,
target_register: usize,
resolver: &Resolver,
) -> Result<()> {
translate_expr(program, referenced_tables, expr, target_register, resolver)?;
if matches!(expr, ast::Expr::Literal(_)) {
program.mark_last_insn_constant();
}
Ok(())
}
/// Sanitaizes a string literal by removing single quote at front and back
/// and escaping double single quotes
pub fn sanitize_string(input: &str) -> String {

View file

@ -1,11 +1,11 @@
use std::rc::Rc;
use limbo_sqlite3_parser::ast;
use limbo_sqlite3_parser::ast::{self, SortOrder};
use crate::{
function::AggFunc,
schema::{Column, PseudoTable},
types::{OwnedValue, Record},
util::exprs_are_equivalent,
vdbe::{
builder::{CursorType, ProgramBuilder},
insn::Insn,
@ -37,12 +37,15 @@ pub struct GroupByMetadata {
pub reg_sorter_key: usize,
// Register holding a flag to abort the grouping process if necessary
pub reg_abort_flag: usize,
// Register holding the start of the accumulator group registers (i.e. the groups, not the aggregates)
pub reg_group_exprs_acc: usize,
// Register holding the start of the non aggregate query members (all columns except aggregate arguments)
pub reg_non_aggregate_exprs_acc: usize,
// Starting index of the register(s) that hold the comparison result between the current row and the previous row
// The comparison result is used to determine if the current row belongs to the same group as the previous row
// Each group by expression has a corresponding register
pub reg_group_exprs_cmp: usize,
// Columns that not part of GROUP BY clause and not arguments of Aggregation function.
// Heavy calculation and needed in different functions, so it is reasonable to do it once and save.
pub non_group_by_non_agg_column_count: Option<usize>,
}
/// Initialize resources needed for GROUP BY processing
@ -50,29 +53,30 @@ pub fn init_group_by(
program: &mut ProgramBuilder,
t_ctx: &mut TranslateCtx,
group_by: &GroupBy,
aggregates: &[Aggregate],
plan: &SelectPlan,
) -> Result<()> {
let num_aggs = aggregates.len();
let num_aggs = plan.aggregates.len();
let non_aggregate_count = plan
.result_columns
.iter()
.filter(|rc| !rc.contains_aggregates)
.count();
let sort_cursor = program.alloc_cursor_id(None, CursorType::Sorter);
let reg_abort_flag = program.alloc_register();
let reg_group_exprs_cmp = program.alloc_registers(group_by.exprs.len());
let reg_group_exprs_acc = program.alloc_registers(group_by.exprs.len());
let reg_non_aggregate_exprs_acc = program.alloc_registers(non_aggregate_count);
let reg_agg_exprs_start = program.alloc_registers(num_aggs);
let reg_sorter_key = program.alloc_register();
let label_subrtn_acc_clear = program.allocate_label();
let mut order = Vec::new();
const ASCENDING: i64 = 0;
for _ in group_by.exprs.iter() {
order.push(OwnedValue::Integer(ASCENDING));
}
program.emit_insn(Insn::SorterOpen {
cursor_id: sort_cursor,
columns: aggregates.len() + group_by.exprs.len(),
order: Record::new(order),
columns: non_aggregate_count + plan.aggregates.len(),
order: (0..group_by.exprs.len()).map(|_| SortOrder::Asc).collect(),
});
program.add_comment(program.offset(), "clear group by abort flag");
@ -110,9 +114,10 @@ pub fn init_group_by(
label_acc_indicator_set_flag_true: program.allocate_label(),
reg_subrtn_acc_clear_return_offset,
reg_abort_flag,
reg_group_exprs_acc,
reg_non_aggregate_exprs_acc,
reg_group_exprs_cmp,
reg_sorter_key,
non_group_by_non_agg_column_count: None,
});
Ok(())
}
@ -146,25 +151,57 @@ pub fn emit_group_by<'a>(
sort_cursor,
reg_group_exprs_cmp,
reg_subrtn_acc_clear_return_offset,
reg_group_exprs_acc,
reg_non_aggregate_exprs_acc,
reg_abort_flag,
reg_sorter_key,
label_subrtn_acc_clear,
label_acc_indicator_set_flag_true,
non_group_by_non_agg_column_count,
..
} = *t_ctx.meta_group_by.as_mut().unwrap();
let group_by = plan.group_by.as_ref().unwrap();
// all group by columns and all arguments of agg functions are in the sorter.
// the sort keys are the group by columns (the aggregation within groups is done based on how long the sort keys remain the same)
let sorter_column_count = group_by.exprs.len()
+ plan
.aggregates
let agg_args_count = plan
.aggregates
.iter()
.map(|agg| agg.args.len())
.sum::<usize>();
let group_by_count = group_by.exprs.len();
let non_group_by_non_agg_column_count = non_group_by_non_agg_column_count.unwrap();
// We have to know which group by expr present in resulting set
let group_by_expr_in_res_cols = group_by.exprs.iter().map(|expr| {
plan.result_columns
.iter()
.map(|agg| agg.args.len())
.sum::<usize>();
// sorter column names do not matter
.any(|e| exprs_are_equivalent(&e.expr, expr))
});
// Create a map from sorter column index to result register
// This helps track where each column from the sorter should be stored
let mut column_register_mapping =
vec![None; group_by_count + non_group_by_non_agg_column_count];
let mut next_reg = reg_non_aggregate_exprs_acc;
// Map GROUP BY columns that are in the result set to registers
for (i, is_in_result) in group_by_expr_in_res_cols.clone().enumerate() {
if is_in_result {
column_register_mapping[i] = Some(next_reg);
next_reg += 1;
}
}
// Handle other non-aggregate columns that aren't part of GROUP BY and not part of Aggregation function
for i in group_by_count..group_by_count + non_group_by_non_agg_column_count {
column_register_mapping[i] = Some(next_reg);
next_reg += 1;
}
// Calculate total number of columns in the sorter
// The sorter contains all GROUP BY columns, aggregate arguments, and other columns
let sorter_column_count = agg_args_count + group_by_count + non_group_by_non_agg_column_count;
// Create pseudo-columns for the pseudo-table
// (these are placeholders as we only care about structure, not semantics)
let ty = crate::schema::Type::Null;
let pseudo_columns = (0..sorter_column_count)
.map(|_| Column {
@ -178,7 +215,8 @@ pub fn emit_group_by<'a>(
})
.collect::<Vec<_>>();
// A pseudo table is a "fake" table to which we read one row at a time from the sorter
// Create a pseudo-table to read one row at a time from the sorter
// This allows us to use standard table access operations on the sorted data
let pseudo_table = Rc::new(PseudoTable {
columns: pseudo_columns,
});
@ -231,10 +269,21 @@ pub fn emit_group_by<'a>(
"start new group if comparison is not equal",
);
// If we are at a new group, continue. If we are at the same group, jump to the aggregation step (i.e. accumulate more values into the aggregations)
let label_jump_after_comparison = program.allocate_label();
program.emit_insn(Insn::Jump {
target_pc_lt: program.offset().add(1u32),
target_pc_lt: label_jump_after_comparison,
target_pc_eq: agg_step_label,
target_pc_gt: program.offset().add(1u32),
target_pc_gt: label_jump_after_comparison,
});
program.add_comment(
program.offset(),
"check if ended group had data, and output if so",
);
program.resolve_label(label_jump_after_comparison, program.offset());
program.emit_insn(Insn::Gosub {
target_pc: label_subrtn_acc_output,
return_reg: reg_subrtn_acc_output_return_offset,
});
// New group, move current group by columns into the comparison register
@ -244,15 +293,6 @@ pub fn emit_group_by<'a>(
count: group_by.exprs.len(),
});
program.add_comment(
program.offset(),
"check if ended group had data, and output if so",
);
program.emit_insn(Insn::Gosub {
target_pc: label_subrtn_acc_output,
return_reg: reg_subrtn_acc_output_return_offset,
});
program.add_comment(program.offset(), "check abort flag");
program.emit_insn(Insn::IfPos {
reg: reg_abort_flag,
@ -266,10 +306,10 @@ pub fn emit_group_by<'a>(
return_reg: reg_subrtn_acc_clear_return_offset,
});
// Accumulate the values into the aggregations
// Process each aggregate function for the current row
program.resolve_label(agg_step_label, program.offset());
let start_reg = t_ctx.reg_agg_start.unwrap();
let mut cursor_index = group_by.exprs.len();
let mut cursor_index = group_by_count + non_group_by_non_agg_column_count; // Skipping all columns in sorter that not an aggregation arguments
for (i, agg) in plan.aggregates.iter().enumerate() {
let agg_result_reg = start_reg + i;
translate_aggregation_step_groupby(
@ -284,7 +324,8 @@ pub fn emit_group_by<'a>(
cursor_index += agg.args.len();
}
// We only emit the group by columns if we are going to start a new group (i.e. the prev group will not accumulate any more values into the aggregations)
// We only need to store non-aggregate columns once per group
// Skip if we've already stored them for this group
program.add_comment(
program.offset(),
"don't emit group columns if continuing existing group",
@ -295,17 +336,18 @@ pub fn emit_group_by<'a>(
jump_if_null: false,
});
// Read the group by columns for a finished group
for i in 0..group_by.exprs.len() {
let key_reg = reg_group_exprs_acc + i;
let sorter_column_index = i;
program.emit_insn(Insn::Column {
cursor_id: pseudo_cursor,
column: sorter_column_index,
dest: key_reg,
});
// Read non-aggregate columns from the current row
for (sorter_column_index, dest_reg) in column_register_mapping.iter().enumerate() {
if let Some(dest_reg) = dest_reg {
program.emit_insn(Insn::Column {
cursor_id: pseudo_cursor,
column: sorter_column_index,
dest: *dest_reg,
});
}
}
// Mark that we've stored data for this group
program.resolve_label(label_acc_indicator_set_flag_true, program.offset());
program.add_comment(program.offset(), "indicate data in accumulator");
program.emit_insn(Insn::Integer {
@ -313,12 +355,12 @@ pub fn emit_group_by<'a>(
dest: reg_data_in_acc_flag,
});
// Continue to the next row in the sorter
program.emit_insn(Insn::SorterNext {
cursor_id: sort_cursor,
pc_if_next: label_grouping_loop_start,
});
program.resolve_label(label_grouping_loop_end, program.offset());
program.preassign_label_to_next_insn(label_grouping_loop_end);
program.add_comment(program.offset(), "emit row for final group");
program.emit_insn(Insn::Gosub {
@ -340,18 +382,22 @@ pub fn emit_group_by<'a>(
program.resolve_label(label_subrtn_acc_output, program.offset());
// Only output a row if there's data in the accumulator
program.add_comment(program.offset(), "output group by row subroutine start");
program.emit_insn(Insn::IfPos {
reg: reg_data_in_acc_flag,
target_pc: label_agg_final,
decrement_by: 0,
});
// If no data, return without outputting a row
let group_by_end_without_emitting_row_label = program.allocate_label();
program.resolve_label(group_by_end_without_emitting_row_label, program.offset());
program.emit_insn(Insn::Return {
return_reg: reg_subrtn_acc_output_return_offset,
});
// Finalize aggregate values for output
let agg_start_reg = t_ctx.reg_agg_start.unwrap();
// Resolve the label for the start of the group by output row subroutine
program.resolve_label(label_agg_final, program.offset());
@ -363,16 +409,34 @@ pub fn emit_group_by<'a>(
});
}
// we now have the group by columns in registers (group_exprs_start_register..group_exprs_start_register + group_by.len() - 1)
// and the agg results in (agg_start_reg..agg_start_reg + aggregates.len() - 1)
// we need to call translate_expr on each result column, but replace the expr with a register copy in case any part of the
// result column expression matches a) a group by column or b) an aggregation result.
for (i, expr) in group_by.exprs.iter().enumerate() {
t_ctx
.resolver
.expr_to_reg_cache
.push((expr, reg_group_exprs_acc + i));
// Map GROUP BY expressions to their registers in the result set
for (i, (expr, is_in_result)) in group_by
.exprs
.iter()
.zip(group_by_expr_in_res_cols)
.enumerate()
{
if is_in_result {
if let Some(reg) = &column_register_mapping.get(i).and_then(|opt| *opt) {
t_ctx.resolver.expr_to_reg_cache.push((expr, *reg));
}
}
}
// Map non-aggregate, non-GROUP BY columns to their registers
let non_agg_cols = plan
.result_columns
.iter()
.filter(|rc| !rc.contains_aggregates && !is_column_in_group_by(&rc.expr, &group_by.exprs));
for (idx, rc) in non_agg_cols.enumerate() {
let sorter_idx = group_by_count + idx;
if let Some(reg) = column_register_mapping.get(sorter_idx).and_then(|opt| *opt) {
t_ctx.resolver.expr_to_reg_cache.push((&rc.expr, reg));
}
}
// Map aggregate expressions to their result registers
for (i, agg) in plan.aggregates.iter().enumerate() {
t_ctx
.resolver
@ -415,12 +479,18 @@ pub fn emit_group_by<'a>(
return_reg: reg_subrtn_acc_output_return_offset,
});
// Subroutine to clear accumulators for a new group
program.add_comment(program.offset(), "clear accumulator subroutine start");
program.resolve_label(label_subrtn_acc_clear, program.offset());
let start_reg = reg_group_exprs_acc;
let start_reg = reg_non_aggregate_exprs_acc;
// Reset all accumulator registers to NULL
program.emit_insn(Insn::Null {
dest: start_reg,
dest_end: Some(start_reg + group_by.exprs.len() + plan.aggregates.len() - 1),
dest_end: Some(
start_reg + non_group_by_non_agg_column_count + group_by_count + plan.aggregates.len()
- 1,
),
});
program.emit_insn(Insn::Integer {
@ -430,8 +500,7 @@ pub fn emit_group_by<'a>(
program.emit_insn(Insn::Return {
return_reg: reg_subrtn_acc_clear_return_offset,
});
program.resolve_label(label_group_by_end, program.offset());
program.preassign_label_to_next_insn(label_group_by_end);
Ok(())
}
@ -668,3 +737,9 @@ pub fn translate_aggregation_step_groupby(
};
Ok(dest)
}
pub fn is_column_in_group_by(expr: &ast::Expr, group_by_exprs: &[ast::Expr]) -> bool {
group_by_exprs
.iter()
.any(|expr2| exprs_are_equivalent(expr, expr2))
}

298
core/translate/index.rs Normal file
View file

@ -0,0 +1,298 @@
use std::sync::Arc;
use crate::{
schema::{BTreeTable, Column, Index, IndexColumn, PseudoTable, Schema},
storage::pager::CreateBTreeFlags,
util::normalize_ident,
vdbe::{
builder::{CursorType, ProgramBuilder, QueryMode},
insn::{IdxInsertFlags, Insn, RegisterOrLiteral},
},
};
use limbo_sqlite3_parser::ast::{self, Expr, Id, SortOrder, SortedColumn};
use super::schema::{emit_schema_entry, SchemaEntryType, SQLITE_TABLEID};
pub fn translate_create_index(
mode: QueryMode,
unique_if_not_exists: (bool, bool),
idx_name: &str,
tbl_name: &str,
columns: &[SortedColumn],
schema: &Schema,
) -> crate::Result<ProgramBuilder> {
let idx_name = normalize_ident(idx_name);
let tbl_name = normalize_ident(tbl_name);
let mut program = ProgramBuilder::new(crate::vdbe::builder::ProgramBuilderOpts {
query_mode: mode,
num_cursors: 5,
approx_num_insns: 40,
approx_num_labels: 5,
});
// Check if the index is being created on a valid btree table and
// the name is globally unique in the schema.
if !schema.is_unique_idx_name(&idx_name) {
crate::bail_parse_error!("Error: index with name '{idx_name}' already exists.");
}
let Some(tbl) = schema.tables.get(&tbl_name) else {
crate::bail_parse_error!("Error: table '{tbl_name}' does not exist.");
};
let Some(tbl) = tbl.btree() else {
crate::bail_parse_error!("Error: table '{tbl_name}' is not a b-tree table.");
};
let columns = resolve_sorted_columns(&tbl, columns)?;
// Prologue:
let init_label = program.emit_init();
let start_offset = program.offset();
let idx = Arc::new(Index {
name: idx_name.clone(),
table_name: tbl.name.clone(),
root_page: 0, // we dont have access till its created, after we parse the schema table
columns: columns
.iter()
.map(|((pos_in_table, col), order)| IndexColumn {
name: col.name.as_ref().unwrap().clone(),
order: *order,
pos_in_table: *pos_in_table,
})
.collect(),
unique: unique_if_not_exists.0,
ephemeral: false,
});
// Allocate the necessary cursors:
//
// 1. sqlite_schema_cursor_id - sqlite_schema table
// 2. btree_cursor_id - new index btree
// 3. table_cursor_id - table we are creating the index on
// 4. sorter_cursor_id - sorter
// 5. pseudo_cursor_id - pseudo table to store the sorted index values
let sqlite_table = schema.get_btree_table(SQLITE_TABLEID).unwrap();
let sqlite_schema_cursor_id = program.alloc_cursor_id(
Some(SQLITE_TABLEID.to_owned()),
CursorType::BTreeTable(sqlite_table.clone()),
);
let btree_cursor_id = program.alloc_cursor_id(
Some(idx_name.to_owned()),
CursorType::BTreeIndex(idx.clone()),
);
let table_cursor_id = program.alloc_cursor_id(
Some(tbl_name.to_owned()),
CursorType::BTreeTable(tbl.clone()),
);
let sorter_cursor_id = program.alloc_cursor_id(None, CursorType::Sorter);
let pseudo_table = PseudoTable::new_with_columns(tbl.columns.clone());
let pseudo_cursor_id = program.alloc_cursor_id(None, CursorType::Pseudo(pseudo_table.into()));
// Create a new B-Tree and store the root page index in a register
let root_page_reg = program.alloc_register();
program.emit_insn(Insn::CreateBtree {
db: 0,
root: root_page_reg,
flags: CreateBTreeFlags::new_index(),
});
// open the sqlite schema table for writing and create a new entry for the index
program.emit_insn(Insn::OpenWrite {
cursor_id: sqlite_schema_cursor_id,
root_page: RegisterOrLiteral::Literal(sqlite_table.root_page),
});
let sql = create_idx_stmt_to_sql(&tbl_name, &idx_name, unique_if_not_exists, &columns);
emit_schema_entry(
&mut program,
sqlite_schema_cursor_id,
SchemaEntryType::Index,
&idx_name,
&tbl_name,
root_page_reg,
Some(sql),
);
// determine the order of the columns in the index for the sorter
let order = idx.columns.iter().map(|c| c.order.clone()).collect();
// open the sorter and the pseudo table
program.emit_insn(Insn::SorterOpen {
cursor_id: sorter_cursor_id,
columns: columns.len(),
order,
});
let content_reg = program.alloc_register();
program.emit_insn(Insn::OpenPseudo {
cursor_id: pseudo_cursor_id,
content_reg,
num_fields: columns.len() + 1,
});
// open the table we are creating the index on for reading
program.emit_insn(Insn::OpenRead {
cursor_id: table_cursor_id,
root_page: tbl.root_page,
});
let loop_start_label = program.allocate_label();
let loop_end_label = program.allocate_label();
program.emit_insn(Insn::Rewind {
cursor_id: table_cursor_id,
pc_if_empty: loop_end_label,
});
program.preassign_label_to_next_insn(loop_start_label);
// Loop start:
// Collect index values into start_reg..rowid_reg
// emit MakeRecord (index key + rowid) into record_reg.
//
// Then insert the record into the sorter
let start_reg = program.alloc_registers(columns.len() + 1);
for (i, (col, _)) in columns.iter().enumerate() {
program.emit_insn(Insn::Column {
cursor_id: table_cursor_id,
column: col.0,
dest: start_reg + i,
});
}
let rowid_reg = start_reg + columns.len();
program.emit_insn(Insn::RowId {
cursor_id: table_cursor_id,
dest: rowid_reg,
});
let record_reg = program.alloc_register();
program.emit_insn(Insn::MakeRecord {
start_reg,
count: columns.len() + 1,
dest_reg: record_reg,
});
program.emit_insn(Insn::SorterInsert {
cursor_id: sorter_cursor_id,
record_reg,
});
program.emit_insn(Insn::Next {
cursor_id: table_cursor_id,
pc_if_next: loop_start_label,
});
program.preassign_label_to_next_insn(loop_end_label);
// Open the index btree we created for writing to insert the
// newly sorted index records.
program.emit_insn(Insn::OpenWrite {
cursor_id: btree_cursor_id,
root_page: RegisterOrLiteral::Register(root_page_reg),
});
let sorted_loop_start = program.allocate_label();
let sorted_loop_end = program.allocate_label();
// Sort the index records in the sorter
program.emit_insn(Insn::SorterSort {
cursor_id: sorter_cursor_id,
pc_if_empty: sorted_loop_end,
});
program.preassign_label_to_next_insn(sorted_loop_start);
let sorted_record_reg = program.alloc_register();
program.emit_insn(Insn::SorterData {
pseudo_cursor: pseudo_cursor_id,
cursor_id: sorter_cursor_id,
dest_reg: sorted_record_reg,
});
// seek to the end of the index btree to position the cursor for appending
program.emit_insn(Insn::SeekEnd {
cursor_id: btree_cursor_id,
});
// insert new index record
program.emit_insn(Insn::IdxInsert {
cursor_id: btree_cursor_id,
record_reg: sorted_record_reg,
unpacked_start: None, // TODO: optimize with these to avoid decoding record twice
unpacked_count: None,
flags: IdxInsertFlags::new().use_seek(false),
});
program.emit_insn(Insn::SorterNext {
cursor_id: sorter_cursor_id,
pc_if_next: sorted_loop_start,
});
program.preassign_label_to_next_insn(sorted_loop_end);
// End of the outer loop
//
// Keep schema table open to emit ParseSchema, close the other cursors.
program.close_cursors(&[sorter_cursor_id, table_cursor_id, btree_cursor_id]);
// TODO: SetCookie for schema change
//
// Parse the schema table to get the index root page and add new index to Schema
let parse_schema_where_clause = format!("name = '{}' AND type = 'index'", idx_name);
program.emit_insn(Insn::ParseSchema {
db: sqlite_schema_cursor_id,
where_clause: parse_schema_where_clause,
});
// Close the final sqlite_schema cursor
program.emit_insn(Insn::Close {
cursor_id: sqlite_schema_cursor_id,
});
// Epilogue:
program.emit_halt();
program.preassign_label_to_next_insn(init_label);
program.emit_transaction(true);
program.emit_constant_insns();
program.emit_goto(start_offset);
Ok(program)
}
fn resolve_sorted_columns<'a>(
table: &'a BTreeTable,
cols: &[SortedColumn],
) -> crate::Result<Vec<((usize, &'a Column), SortOrder)>> {
let mut resolved = Vec::with_capacity(cols.len());
for sc in cols {
let ident = normalize_ident(match &sc.expr {
Expr::Id(Id(col_name)) | Expr::Name(ast::Name(col_name)) => col_name,
_ => crate::bail_parse_error!("Error: cannot use expressions in CREATE INDEX"),
});
let Some(col) = table.get_column(&ident) else {
crate::bail_parse_error!(
"Error: column '{ident}' does not exist in table '{}'",
table.name
);
};
resolved.push((col, sc.order.unwrap_or(SortOrder::Asc)));
}
Ok(resolved)
}
fn create_idx_stmt_to_sql(
tbl_name: &str,
idx_name: &str,
unique_if_not_exists: (bool, bool),
cols: &[((usize, &Column), SortOrder)],
) -> String {
let mut sql = String::with_capacity(128);
sql.push_str("CREATE ");
if unique_if_not_exists.0 {
sql.push_str("UNIQUE ");
}
sql.push_str("INDEX ");
if unique_if_not_exists.1 {
sql.push_str("IF NOT EXISTS ");
}
sql.push_str(idx_name);
sql.push_str(" ON ");
sql.push_str(tbl_name);
sql.push_str(" (");
for (i, (col, order)) in cols.iter().enumerate() {
if i > 0 {
sql.push_str(", ");
}
sql.push_str(col.1.name.as_ref().unwrap());
if *order == SortOrder::Desc {
sql.push_str(" DESC");
}
}
sql.push(')');
sql
}

View file

@ -6,22 +6,22 @@ use limbo_sqlite3_parser::ast::{
};
use crate::error::SQLITE_CONSTRAINT_PRIMARYKEY;
use crate::schema::Table;
use crate::schema::{IndexColumn, Table};
use crate::util::normalize_ident;
use crate::vdbe::builder::{ProgramBuilderOpts, QueryMode};
use crate::vdbe::insn::{IdxInsertFlags, RegisterOrLiteral};
use crate::vdbe::BranchOffset;
use crate::{
schema::{Column, Schema},
translate::expr::translate_expr,
vdbe::{
builder::{CursorType, ProgramBuilder},
insn::Insn,
},
SymbolTable,
};
use crate::{Result, VirtualTable};
use crate::{Result, SymbolTable, VirtualTable};
use super::emitter::Resolver;
use super::expr::{translate_expr_no_constant_opt, NoConstantOptReason};
#[allow(clippy::too_many_arguments)]
pub fn translate_insert(
@ -82,16 +82,33 @@ pub fn translate_insert(
Some(table_name.0.clone()),
CursorType::BTreeTable(btree_table.clone()),
);
// allocate cursor id's for each btree index cursor we'll need to populate the indexes
// (idx name, root_page, idx cursor id)
let idx_cursors = schema
.get_indices(&table_name.0)
.iter()
.map(|idx| {
(
&idx.name,
idx.root_page,
program.alloc_cursor_id(
Some(table_name.0.clone()),
CursorType::BTreeIndex(idx.clone()),
),
)
})
.collect::<Vec<(&String, usize, usize)>>();
let root_page = btree_table.root_page;
let values = match body {
InsertBody::Select(select, None) => match &select.body.select.deref() {
InsertBody::Select(select, _) => match &select.body.select.deref() {
OneSelect::Values(values) => values,
_ => todo!(),
},
_ => todo!(),
InsertBody::DefaultValues => &vec![vec![]],
};
let column_mappings = resolve_columns_for_insert(&table, columns, values)?;
let index_col_mappings = resolve_indicies_for_insert(schema, table.as_ref(), &column_mappings)?;
// Check if rowid was provided (through INTEGER PRIMARY KEY as a rowid alias)
let rowid_alias_index = btree_table.columns.iter().position(|c| c.is_rowid_alias);
let has_user_provided_rowid = {
@ -126,12 +143,15 @@ pub fn translate_insert(
if inserting_multiple_rows {
let yield_reg = program.alloc_register();
let jump_on_definition_label = program.allocate_label();
let start_offset_label = program.allocate_label();
program.emit_insn(Insn::InitCoroutine {
yield_reg,
jump_on_definition: jump_on_definition_label,
start_offset: program.offset().add(1u32),
start_offset: start_offset_label,
});
program.resolve_label(start_offset_label, program.offset());
for value in values {
populate_column_registers(
&mut program,
@ -148,13 +168,12 @@ pub fn translate_insert(
});
}
program.emit_insn(Insn::EndCoroutine { yield_reg });
program.resolve_label(jump_on_definition_label, program.offset());
program.preassign_label_to_next_insn(jump_on_definition_label);
program.emit_insn(Insn::OpenWriteAsync {
program.emit_insn(Insn::OpenWrite {
cursor_id,
root_page,
root_page: RegisterOrLiteral::Literal(root_page),
});
program.emit_insn(Insn::OpenWriteAwait {});
// Main loop
// FIXME: rollback is not implemented. E.g. if you insert 2 rows and one fails to unique constraint violation,
@ -166,11 +185,10 @@ pub fn translate_insert(
});
} else {
// Single row - populate registers directly
program.emit_insn(Insn::OpenWriteAsync {
program.emit_insn(Insn::OpenWrite {
cursor_id,
root_page,
root_page: RegisterOrLiteral::Literal(root_page),
});
program.emit_insn(Insn::OpenWriteAwait {});
populate_column_registers(
&mut program,
@ -182,7 +200,13 @@ pub fn translate_insert(
&resolver,
)?;
}
// Open all the index btrees for writing
for idx_cursor in idx_cursors.iter() {
program.emit_insn(Insn::OpenWrite {
cursor_id: idx_cursor.2,
root_page: idx_cursor.1.into(),
});
}
// Common record insertion logic for both single and multiple rows
let check_rowid_is_integer_label = rowid_alias_reg.and(Some(program.allocate_label()));
if let Some(reg) = rowid_alias_reg {
@ -246,8 +270,109 @@ pub fn translate_insert(
err_code: SQLITE_CONSTRAINT_PRIMARYKEY,
description: format!("{}.{}", table_name.0, rowid_column_name),
});
program.preassign_label_to_next_insn(make_record_label);
}
program.resolve_label(make_record_label, program.offset());
match table.btree() {
Some(t) if t.is_strict => {
program.emit_insn(Insn::TypeCheck {
start_reg: column_registers_start,
count: num_cols,
check_generated: true,
table_reference: Rc::clone(&t),
});
}
_ => (),
}
for index_col_mapping in index_col_mappings.iter() {
// find which cursor we opened earlier for this index
let idx_cursor_id = idx_cursors
.iter()
.find(|(name, _, _)| *name == &index_col_mapping.idx_name)
.map(|(_, _, c_id)| *c_id)
.expect("no cursor found for index");
let num_cols = index_col_mapping.columns.len();
// allocate scratch registers for the index columns plus rowid
let idx_start_reg = program.alloc_registers(num_cols + 1);
// copy each index column from the table's column registers into these scratch regs
for (i, col) in index_col_mapping.columns.iter().enumerate() {
// copy from the table's column register over to the index's scratch register
program.emit_insn(Insn::Copy {
src_reg: column_registers_start + col.0,
dst_reg: idx_start_reg + i,
amount: 0,
});
}
// last register is the rowid
program.emit_insn(Insn::Copy {
src_reg: rowid_reg,
dst_reg: idx_start_reg + num_cols,
amount: 0,
});
let record_reg = program.alloc_register();
program.emit_insn(Insn::MakeRecord {
start_reg: idx_start_reg,
count: num_cols + 1,
dest_reg: record_reg,
});
let index = schema
.get_index(&table_name.0, &index_col_mapping.idx_name)
.expect("index should be present");
if index.unique {
let label_idx_insert = program.allocate_label();
program.emit_insn(Insn::NoConflict {
cursor_id: idx_cursor_id,
target_pc: label_idx_insert,
record_reg: idx_start_reg,
num_regs: num_cols,
});
let column_names = index_col_mapping.columns.iter().enumerate().fold(
String::with_capacity(50),
|mut accum, (idx, (index, _))| {
if idx > 0 {
accum.push_str(", ");
}
accum.push_str(&btree_table.name);
accum.push('.');
let name = btree_table
.columns
.get(*index)
.unwrap()
.name
.as_ref()
.expect("column name is None");
accum.push_str(name);
accum
},
);
program.emit_insn(Insn::Halt {
err_code: SQLITE_CONSTRAINT_PRIMARYKEY,
description: column_names,
});
program.resolve_label(label_idx_insert, program.offset());
}
// now do the actual index insertion using the unpacked registers
program.emit_insn(Insn::IdxInsert {
cursor_id: idx_cursor_id,
record_reg,
unpacked_start: Some(idx_start_reg), // TODO: enable optimization
unpacked_count: Some((num_cols + 1) as u16),
// TODO: figure out how to determine whether or not we need to seek prior to insert.
flags: IdxInsertFlags::new(),
});
}
// Create and insert the record
@ -257,13 +382,12 @@ pub fn translate_insert(
dest_reg: record_register,
});
program.emit_insn(Insn::InsertAsync {
program.emit_insn(Insn::Insert {
cursor: cursor_id,
key_reg: rowid_reg,
record_reg: record_register,
flag: 0,
});
program.emit_insn(Insn::InsertAwait { cursor_id });
if inserting_multiple_rows {
// For multiple rows, loop back
@ -277,8 +401,8 @@ pub fn translate_insert(
err_code: 0,
description: String::new(),
});
program.preassign_label_to_next_insn(init_label);
program.resolve_label(init_label, program.offset());
program.emit_insn(Insn::Transaction { write: true });
program.emit_constant_insns();
program.emit_insn(Insn::Goto {
@ -297,6 +421,8 @@ struct ColumnMapping<'a> {
/// If Some(i), use the i-th value from the VALUES tuple
/// If None, use NULL (column was not specified in INSERT statement)
value_index: Option<usize>,
/// The default value for the column, if defined
default_value: Option<&'a Expr>,
}
/// Resolves how each column in a table should be populated during an INSERT.
@ -352,6 +478,7 @@ fn resolve_columns_for_insert<'a>(
.map(|(i, col)| ColumnMapping {
column: col,
value_index: if i < num_values { Some(i) } else { None },
default_value: col.default.as_ref(),
})
.collect());
}
@ -362,6 +489,7 @@ fn resolve_columns_for_insert<'a>(
.map(|col| ColumnMapping {
column: col,
value_index: None,
default_value: col.default.as_ref(),
})
.collect();
@ -388,6 +516,69 @@ fn resolve_columns_for_insert<'a>(
Ok(mappings)
}
/// Represents how a column in an index should be populated during an INSERT.
/// Similar to ColumnMapping above but includes the index name, as well as multiple
/// possible value indices for each.
#[derive(Debug, Default)]
struct IndexColMapping {
idx_name: String,
columns: Vec<(usize, IndexColumn)>,
value_indicies: Vec<Option<usize>>,
}
impl IndexColMapping {
fn new(name: String) -> Self {
IndexColMapping {
idx_name: name,
..Default::default()
}
}
}
/// Example:
/// Table 'test': (a, b, c);
/// Index 'idx': test(a, b);
///________________________________
/// Insert (a, c): (2, 3)
/// Record: (2, NULL, 3)
/// IndexColMapping: (a, b) = (2, NULL)
fn resolve_indicies_for_insert(
schema: &Schema,
table: &Table,
columns: &[ColumnMapping<'_>],
) -> Result<Vec<IndexColMapping>> {
let mut index_col_mappings = Vec::new();
// Iterate over all indices for this table
for index in schema.get_indices(table.get_name()) {
let mut idx_map = IndexColMapping::new(index.name.clone());
// For each column in the index (in the order defined by the index),
// try to find the corresponding column in the inserts column mapping.
for idx_col in &index.columns {
let target_name = normalize_ident(idx_col.name.as_str());
if let Some((i, col_mapping)) = columns.iter().enumerate().find(|(_, mapping)| {
mapping
.column
.name
.as_ref()
.map_or(false, |name| name.eq_ignore_ascii_case(&target_name))
}) {
idx_map.columns.push((i, idx_col.clone()));
idx_map.value_indicies.push(col_mapping.value_index);
} else {
return Err(crate::LimboError::ParseError(format!(
"Column {} not found in index {}",
target_name, index.name
)));
}
}
// Add the mapping if at least one column was found.
if !idx_map.columns.is_empty() {
index_col_mappings.push(idx_map);
}
}
Ok(index_col_mappings)
}
/// Populates the column registers with values for a single row
fn populate_column_registers(
program: &mut ProgramBuilder,
@ -413,18 +604,28 @@ fn populate_column_registers(
} else {
target_reg
};
translate_expr(
translate_expr_no_constant_opt(
program,
None,
value.get(value_index).expect("value index out of bounds"),
reg,
resolver,
NoConstantOptReason::RegisterReuse,
)?;
if write_directly_to_rowid_reg {
program.emit_insn(Insn::SoftNull { reg: target_reg });
}
} else if let Some(default_expr) = mapping.default_value {
translate_expr_no_constant_opt(
program,
None,
default_expr,
target_reg,
resolver,
NoConstantOptReason::RegisterReuse,
)?;
} else {
// Column was not specified - use NULL if it is nullable, otherwise error
// Column was not specified as has no DEFAULT - use NULL if it is nullable, otherwise error
// Rowid alias columns can be NULL because we will autogenerate a rowid in that case.
let is_nullable = !mapping.column.primary_key || mapping.column.is_rowid_alias;
if is_nullable {
@ -472,7 +673,14 @@ fn translate_virtual_table_insert(
let value_registers_start = program.alloc_registers(values[0].len());
for (i, expr) in values[0].iter().enumerate() {
translate_expr(program, None, expr, value_registers_start + i, resolver)?;
translate_expr_no_constant_opt(
program,
None,
expr,
value_registers_start + i,
resolver,
NoConstantOptReason::RegisterReuse,
)?;
}
/* *
* Inserts for virtual tables are done in a single step.
@ -526,12 +734,12 @@ fn translate_virtual_table_insert(
});
let halt_label = program.allocate_label();
program.resolve_label(halt_label, program.offset());
program.emit_insn(Insn::Halt {
err_code: 0,
description: String::new(),
});
program.resolve_label(halt_label, program.offset());
program.resolve_label(init_label, program.offset());
program.emit_insn(Insn::Goto {

File diff suppressed because it is too large Load diff

View file

@ -12,6 +12,7 @@ pub(crate) mod delete;
pub(crate) mod emitter;
pub(crate) mod expr;
pub(crate) mod group_by;
pub(crate) mod index;
pub(crate) mod insert;
pub(crate) mod main_loop;
pub(crate) mod optimizer;
@ -34,6 +35,7 @@ use crate::translate::delete::translate_delete;
use crate::vdbe::builder::{ProgramBuilder, ProgramBuilderOpts, QueryMode};
use crate::vdbe::Program;
use crate::{bail_parse_error, Connection, Result, SymbolTable};
use index::translate_create_index;
use insert::translate_insert;
use limbo_sqlite3_parser::ast::{self, Delete, Insert};
use schema::{translate_create_table, translate_create_virtual_table, translate_drop_table};
@ -61,7 +63,24 @@ pub fn translate(
ast::Stmt::Attach { .. } => bail_parse_error!("ATTACH not supported yet"),
ast::Stmt::Begin(tx_type, tx_name) => translate_tx_begin(tx_type, tx_name)?,
ast::Stmt::Commit(tx_name) => translate_tx_commit(tx_name)?,
ast::Stmt::CreateIndex { .. } => bail_parse_error!("CREATE INDEX not supported yet"),
ast::Stmt::CreateIndex {
unique,
if_not_exists,
idx_name,
tbl_name,
columns,
..
} => {
change_cnt_on = true;
translate_create_index(
query_mode,
(unique, if_not_exists),
&idx_name.name.0,
&tbl_name.0,
&columns,
schema,
)?
}
ast::Stmt::CreateTable {
temporary,
if_not_exists,
@ -78,7 +97,7 @@ pub fn translate(
ast::Stmt::CreateTrigger { .. } => bail_parse_error!("CREATE TRIGGER not supported yet"),
ast::Stmt::CreateView { .. } => bail_parse_error!("CREATE VIEW not supported yet"),
ast::Stmt::CreateVirtualTable(vtab) => {
translate_create_virtual_table(*vtab, schema, query_mode)?
translate_create_virtual_table(*vtab, schema, query_mode, &syms)?
}
ast::Stmt::Delete(delete) => {
let Delete {

File diff suppressed because it is too large Load diff

View file

@ -1,10 +1,9 @@
use std::rc::Rc;
use limbo_sqlite3_parser::ast;
use limbo_sqlite3_parser::ast::{self, SortOrder};
use crate::{
schema::{Column, PseudoTable},
types::{OwnedValue, Record},
util::exprs_are_equivalent,
vdbe::{
builder::{CursorType, ProgramBuilder},
@ -16,7 +15,7 @@ use crate::{
use super::{
emitter::TranslateCtx,
expr::translate_expr,
plan::{Direction, ResultSetColumn, SelectPlan},
plan::{ResultSetColumn, SelectPlan},
result_row::{emit_offset, emit_result_row_and_limit},
};
@ -33,21 +32,17 @@ pub struct SortMetadata {
pub fn init_order_by(
program: &mut ProgramBuilder,
t_ctx: &mut TranslateCtx,
order_by: &[(ast::Expr, Direction)],
order_by: &[(ast::Expr, SortOrder)],
) -> Result<()> {
let sort_cursor = program.alloc_cursor_id(None, CursorType::Sorter);
t_ctx.meta_sort = Some(SortMetadata {
sort_cursor,
reg_sorter_data: program.alloc_register(),
});
let mut order = Vec::new();
for (_, direction) in order_by.iter() {
order.push(OwnedValue::Integer(*direction as i64));
}
program.emit_insn(Insn::SorterOpen {
cursor_id: sort_cursor,
columns: order_by.len(),
order: Record::new(order),
order: order_by.iter().map(|(_, direction)| *direction).collect(),
});
Ok(())
}
@ -124,8 +119,8 @@ pub fn emit_order_by(
cursor_id: sort_cursor,
pc_if_empty: sort_loop_end_label,
});
program.preassign_label_to_next_insn(sort_loop_start_label);
program.resolve_label(sort_loop_start_label, program.offset());
emit_offset(program, t_ctx, plan, sort_loop_next_label)?;
program.emit_insn(Insn::SorterData {
@ -154,8 +149,7 @@ pub fn emit_order_by(
cursor_id: sort_cursor,
pc_if_next: sort_loop_start_label,
});
program.resolve_label(sort_loop_end_label, program.offset());
program.preassign_label_to_next_insn(sort_loop_end_label);
Ok(())
}
@ -258,7 +252,7 @@ pub fn sorter_insert(
///
/// If any result columns can be skipped, this returns list of 2-tuples of (SkippedResultColumnIndex: usize, ResultColumnIndexInOrderBySorter: usize)
pub fn order_by_deduplicate_result_columns(
order_by: &[(ast::Expr, Direction)],
order_by: &[(ast::Expr, SortOrder)],
result_columns: &[ResultSetColumn],
) -> Option<Vec<(usize, usize)>> {
let mut result_column_remapping: Option<Vec<(usize, usize)>> = None;

View file

@ -1,5 +1,6 @@
use core::fmt;
use limbo_sqlite3_parser::ast;
use limbo_ext::{ConstraintInfo, ConstraintOp};
use limbo_sqlite3_parser::ast::{self, SortOrder};
use std::{
cmp::Ordering,
fmt::{Display, Formatter},
@ -7,13 +8,22 @@ use std::{
sync::Arc,
};
use crate::schema::{PseudoTable, Type};
use crate::{
function::AggFunc,
schema::{BTreeTable, Column, Index, Table},
vdbe::BranchOffset,
VirtualTable,
vdbe::{
builder::{CursorType, ProgramBuilder},
BranchOffset, CursorID,
},
Result, VirtualTable,
};
use crate::{
schema::{PseudoTable, Type},
types::SeekOp,
util::can_pushdown_predicate,
};
use super::emitter::OperationMode;
#[derive(Debug, Clone)]
pub struct ResultSetColumn {
@ -24,13 +34,26 @@ pub struct ResultSetColumn {
}
impl ResultSetColumn {
pub fn name<'a>(&'a self, tables: &'a [TableReference]) -> Option<&'a String> {
pub fn name<'a>(&'a self, tables: &'a [TableReference]) -> Option<&'a str> {
if let Some(alias) = &self.alias {
return Some(alias);
}
match &self.expr {
ast::Expr::Column { table, column, .. } => {
tables[*table].columns()[*column].name.as_ref()
tables[*table].columns()[*column].name.as_deref()
}
ast::Expr::RowId { table, .. } => {
// If there is a rowid alias column, use its name
if let Table::BTree(table) = &tables[*table].table {
if let Some(rowid_alias_column) = table.get_rowid_alias_column() {
if let Some(name) = &rowid_alias_column.1.name {
return Some(name);
}
}
}
// If there is no rowid alias, use "rowid".
Some("rowid")
}
_ => None,
}
@ -72,6 +95,114 @@ impl WhereTerm {
}
}
use crate::ast::{Expr, Operator};
// This function takes an operator and returns the operator you would obtain if the operands were swapped.
// e.g. "literal < column"
// which is not the canonical order for constraint pushdown.
// This function will return > so that the expression can be treated as if it were written "column > literal"
fn reverse_operator(op: &Operator) -> Option<Operator> {
match op {
Operator::Equals => Some(Operator::Equals),
Operator::Less => Some(Operator::Greater),
Operator::LessEquals => Some(Operator::GreaterEquals),
Operator::Greater => Some(Operator::Less),
Operator::GreaterEquals => Some(Operator::LessEquals),
Operator::NotEquals => Some(Operator::NotEquals),
Operator::Is => Some(Operator::Is),
Operator::IsNot => Some(Operator::IsNot),
_ => None,
}
}
fn to_ext_constraint_op(op: &Operator) -> Option<ConstraintOp> {
match op {
Operator::Equals => Some(ConstraintOp::Eq),
Operator::Less => Some(ConstraintOp::Lt),
Operator::LessEquals => Some(ConstraintOp::Le),
Operator::Greater => Some(ConstraintOp::Gt),
Operator::GreaterEquals => Some(ConstraintOp::Ge),
Operator::NotEquals => Some(ConstraintOp::Ne),
_ => None,
}
}
/// This function takes a WhereTerm for a select involving a VTab at index 'table_index'.
/// It determines whether or not it involves the given table and whether or not it can
/// be converted into a ConstraintInfo which can be passed to the vtab module's xBestIndex
/// method, which will possibly calculate some information to improve the query plan, that we can send
/// back to it as arguments for the VFilter operation.
/// is going to be filtered against: e.g:
/// 'SELECT key, value FROM vtab WHERE key = 'some_key';
/// we need to send the OwnedValue('some_key') as an argument to VFilter, and possibly omit it from
/// the filtration in the vdbe layer.
pub fn convert_where_to_vtab_constraint(
term: &WhereTerm,
table_index: usize,
pred_idx: usize,
) -> Option<ConstraintInfo> {
if term.from_outer_join {
return None;
}
let Expr::Binary(lhs, op, rhs) = &term.expr else {
return None;
};
let expr_is_ready = |e: &Expr| -> bool { can_pushdown_predicate(e, table_index) };
let (vcol_idx, op_for_vtab, usable, is_rhs) = match (&**lhs, &**rhs) {
(
Expr::Column {
table: tbl_l,
column: col_l,
..
},
Expr::Column {
table: tbl_r,
column: col_r,
..
},
) => {
// one side must be the virtual table
let vtab_on_l = *tbl_l == table_index;
let vtab_on_r = *tbl_r == table_index;
if vtab_on_l == vtab_on_r {
return None; // either both or none -> not convertible
}
if vtab_on_l {
// vtab on left side: operator unchanged
let usable = *tbl_r < table_index; // usable if the other table is already positioned
(col_l, op, usable, false)
} else {
// vtab on right side of the expr: reverse operator
let usable = *tbl_l < table_index;
(col_r, &reverse_operator(op).unwrap_or(*op), usable, true)
}
}
(Expr::Column { table, column, .. }, other) if *table == table_index => {
(
column,
op,
expr_is_ready(other), // literal / earliertable / deterministic func ?
false,
)
}
(other, Expr::Column { table, column, .. }) if *table == table_index => (
column,
&reverse_operator(op).unwrap_or(*op),
expr_is_ready(other),
true,
),
_ => return None, // does not involve the virtual table at all
};
Some(ConstraintInfo {
column_index: *vcol_idx as u32,
op: to_ext_constraint_op(op_for_vtab)?,
usable,
plan_info: ConstraintInfo::pack_plan_info(pred_idx as u32, is_rhs),
})
}
/// The loop index where to evaluate the condition.
/// For example, in `SELECT * FROM u JOIN p WHERE u.id = 5`, the condition can already be evaluated at the first loop (idx 0),
/// because that is the rightmost table that it references.
@ -136,7 +267,7 @@ pub struct SelectPlan {
/// group by clause
pub group_by: Option<GroupBy>,
/// order by clause
pub order_by: Option<Vec<(ast::Expr, Direction)>>,
pub order_by: Option<Vec<(ast::Expr, SortOrder)>>,
/// all the aggregates collected from the result columns, order by, and (TODO) having clauses
pub aggregates: Vec<Aggregate>,
/// limit clause
@ -159,13 +290,15 @@ pub struct DeletePlan {
/// where clause split into a vec at 'AND' boundaries.
pub where_clause: Vec<WhereTerm>,
/// order by clause
pub order_by: Option<Vec<(ast::Expr, Direction)>>,
pub order_by: Option<Vec<(ast::Expr, SortOrder)>>,
/// limit clause
pub limit: Option<isize>,
/// offset clause
pub offset: Option<isize>,
/// query contains a constant condition that is always false
pub contains_constant_false_condition: bool,
/// Indexes that must be updated by the delete operation.
pub indexes: Vec<Arc<Index>>,
}
#[derive(Debug, Clone)]
@ -175,13 +308,14 @@ pub struct UpdatePlan {
// (colum index, new value) pairs
pub set_clauses: Vec<(usize, ast::Expr)>,
pub where_clause: Vec<WhereTerm>,
pub order_by: Option<Vec<(ast::Expr, Direction)>>,
// TODO: support OFFSET
pub order_by: Option<Vec<(ast::Expr, SortOrder)>>,
pub limit: Option<isize>,
pub offset: Option<isize>,
// TODO: optional RETURNING clause
pub returning: Option<Vec<ResultSetColumn>>,
// whether the WHERE clause is always false
pub contains_constant_false_condition: bool,
pub indexes_to_update: Vec<Arc<Index>>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@ -253,18 +387,54 @@ pub struct TableReference {
pub identifier: String,
/// The join info for this table reference, if it is the right side of a join (which all except the first table reference have)
pub join_info: Option<JoinInfo>,
/// Bitmask of columns that are referenced in the query.
/// Used to decide whether a covering index can be used.
pub col_used_mask: ColumnUsedMask,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[repr(transparent)]
pub struct ColumnUsedMask(u128);
impl ColumnUsedMask {
pub fn new() -> Self {
Self(0)
}
pub fn set(&mut self, index: usize) {
assert!(
index < 128,
"ColumnUsedMask only supports up to 128 columns"
);
self.0 |= 1 << index;
}
pub fn get(&self, index: usize) -> bool {
assert!(
index < 128,
"ColumnUsedMask only supports up to 128 columns"
);
self.0 & (1 << index) != 0
}
pub fn contains_all_set_bits_of(&self, other: &Self) -> bool {
self.0 & other.0 == other.0
}
pub fn is_empty(&self) -> bool {
self.0 == 0
}
}
#[derive(Clone, Debug)]
pub enum Operation {
// Scan operation
// This operation is used to scan a table.
// The iter_dir are uset to indicate the direction of the iterator.
// The use of Option for iter_dir is aimed at implementing a conservative optimization strategy: it only pushes
// iter_dir down to Scan when iter_dir is None, to prevent potential result set errors caused by multiple
// assignments. for more detailed discussions, please refer to https://github.com/tursodatabase/limbo/pull/376
// The iter_dir is used to indicate the direction of the iterator.
Scan {
iter_dir: Option<IterationDirection>,
iter_dir: IterationDirection,
/// The index that we are using to scan the table, if any.
index: Option<Arc<Index>>,
},
// Search operation
// This operation is used to search for a row in a table using an index
@ -279,6 +449,17 @@ pub enum Operation {
},
}
impl Operation {
pub fn index(&self) -> Option<&Arc<Index>> {
match self {
Operation::Scan { index, .. } => index.as_ref(),
Operation::Search(Search::RowidEq { .. }) => None,
Operation::Search(Search::Seek { index, .. }) => index.as_ref(),
Operation::Subquery { .. } => None,
}
}
}
impl TableReference {
/// Returns the btree table for this table reference, if it is a BTreeTable.
pub fn btree(&self) -> Option<Rc<BTreeTable>> {
@ -300,7 +481,7 @@ impl TableReference {
plan.result_columns
.iter()
.map(|rc| Column {
name: rc.name(&plan.table_references).map(String::clone),
name: rc.name(&plan.table_references).map(String::from),
ty: Type::Text, // FIXME: infer proper type
ty_str: "TEXT".to_string(),
is_rowid_alias: false,
@ -318,12 +499,172 @@ impl TableReference {
table,
identifier: identifier.clone(),
join_info,
col_used_mask: ColumnUsedMask::new(),
}
}
pub fn columns(&self) -> &[Column] {
self.table.columns()
}
/// Mark a column as used in the query.
/// This is used to determine whether a covering index can be used.
pub fn mark_column_used(&mut self, index: usize) {
self.col_used_mask.set(index);
}
/// Open the necessary cursors for this table reference.
/// Generally a table cursor is always opened unless a SELECT query can use a covering index.
/// An index cursor is opened if an index is used in any way for reading data from the table.
pub fn open_cursors(
&self,
program: &mut ProgramBuilder,
mode: OperationMode,
) -> Result<(Option<CursorID>, Option<CursorID>)> {
let index = self.op.index();
match &self.table {
Table::BTree(btree) => {
let use_covering_index = self.utilizes_covering_index();
let index_is_ephemeral = index.map_or(false, |index| index.ephemeral);
let table_not_required =
OperationMode::SELECT == mode && use_covering_index && !index_is_ephemeral;
let table_cursor_id = if table_not_required {
None
} else {
Some(program.alloc_cursor_id(
Some(self.identifier.clone()),
CursorType::BTreeTable(btree.clone()),
))
};
let index_cursor_id = if let Some(index) = index {
Some(program.alloc_cursor_id(
Some(index.name.clone()),
CursorType::BTreeIndex(index.clone()),
))
} else {
None
};
Ok((table_cursor_id, index_cursor_id))
}
Table::Virtual(virtual_table) => {
let table_cursor_id = Some(program.alloc_cursor_id(
Some(self.identifier.clone()),
CursorType::VirtualTable(virtual_table.clone()),
));
let index_cursor_id = None;
Ok((table_cursor_id, index_cursor_id))
}
Table::Pseudo(_) => Ok((None, None)),
}
}
/// Resolve the already opened cursors for this table reference.
pub fn resolve_cursors(
&self,
program: &mut ProgramBuilder,
) -> Result<(Option<CursorID>, Option<CursorID>)> {
let index = self.op.index();
let table_cursor_id = program.resolve_cursor_id_safe(&self.identifier);
let index_cursor_id = index.map(|index| program.resolve_cursor_id(&index.name));
Ok((table_cursor_id, index_cursor_id))
}
/// Returns true if a given index is a covering index for this [TableReference].
pub fn index_is_covering(&self, index: &Index) -> bool {
let Table::BTree(btree) = &self.table else {
return false;
};
if self.col_used_mask.is_empty() {
return false;
}
let mut index_cols_mask = ColumnUsedMask::new();
for col in index.columns.iter() {
index_cols_mask.set(col.pos_in_table);
}
// If a table has a rowid (i.e. is not a WITHOUT ROWID table), the index is guaranteed to contain the rowid as well.
if btree.has_rowid {
if let Some(pos_of_rowid_alias_col) = btree.get_rowid_alias_column().map(|(pos, _)| pos)
{
let mut empty_mask = ColumnUsedMask::new();
empty_mask.set(pos_of_rowid_alias_col);
if self.col_used_mask == empty_mask {
// However if the index would be ONLY used for the rowid, then let's not bother using it to cover the query.
// Example: if the query is SELECT id FROM t, and id is a rowid alias, then let's rather just scan the table
// instead of an index.
return false;
}
index_cols_mask.set(pos_of_rowid_alias_col);
}
}
index_cols_mask.contains_all_set_bits_of(&self.col_used_mask)
}
/// Returns true if the index selected for use with this [TableReference] is a covering index,
/// meaning that it contains all the columns that are referenced in the query.
pub fn utilizes_covering_index(&self) -> bool {
let Some(index) = self.op.index() else {
return false;
};
self.index_is_covering(index.as_ref())
}
pub fn column_is_used(&self, index: usize) -> bool {
self.col_used_mask.get(index)
}
}
/// A definition of a rowid/index search.
///
/// [SeekKey] is the condition that is used to seek to a specific row in a table/index.
/// [TerminationKey] is the condition that is used to terminate the search after a seek.
#[derive(Debug, Clone)]
pub struct SeekDef {
/// The key to use when seeking and when terminating the scan that follows the seek.
/// For example, given:
/// - CREATE INDEX i ON t (x, y desc)
/// - SELECT * FROM t WHERE x = 1 AND y >= 30
/// The key is [(1, ASC), (30, DESC)]
pub key: Vec<(ast::Expr, SortOrder)>,
/// The condition to use when seeking. See [SeekKey] for more details.
pub seek: Option<SeekKey>,
/// The condition to use when terminating the scan that follows the seek. See [TerminationKey] for more details.
pub termination: Option<TerminationKey>,
/// The direction of the scan that follows the seek.
pub iter_dir: IterationDirection,
}
/// A condition to use when seeking.
#[derive(Debug, Clone)]
pub struct SeekKey {
/// How many columns from [SeekDef::key] are used in seeking.
pub len: usize,
/// Whether to NULL pad the last column of the seek key to match the length of [SeekDef::key].
/// The reason it is done is that sometimes our full index key is not used in seeking,
/// but we want to find the lowest value that matches the non-null prefix of the key.
/// For example, given:
/// - CREATE INDEX i ON t (x, y)
/// - SELECT * FROM t WHERE x = 1 AND y < 30
/// We want to seek to the first row where x = 1, and then iterate forwards.
/// In this case, the seek key is GT(1, NULL) since NULL is always LT in index key comparisons.
/// We can't use just GT(1) because in index key comparisons, only the given number of columns are compared,
/// so this means any index keys with (x=1) will compare equal, e.g. (x=1, y=usize::MAX) will compare equal to the seek key (x:1)
pub null_pad: bool,
/// The comparison operator to use when seeking.
pub op: SeekOp,
}
#[derive(Debug, Clone)]
/// A condition to use when terminating the scan that follows a seek.
pub struct TerminationKey {
/// How many columns from [SeekDef::key] are used in terminating the scan that follows the seek.
pub len: usize,
/// Whether to NULL pad the last column of the termination key to match the length of [SeekDef::key].
/// See [SeekKey::null_pad].
pub null_pad: bool,
/// The comparison operator to use when terminating the scan that follows the seek.
pub op: SeekOp,
}
/// An enum that represents a search operation that can be used to search for a row in a table using an index
@ -333,32 +674,11 @@ impl TableReference {
pub enum Search {
/// A rowid equality point lookup. This is a special case that uses the SeekRowid bytecode instruction and does not loop.
RowidEq { cmp_expr: WhereTerm },
/// A rowid search. Uses bytecode instructions like SeekGT, SeekGE etc.
RowidSearch {
cmp_op: ast::Operator,
cmp_expr: WhereTerm,
/// A search on a table btree (via `rowid`) or a secondary index search. Uses bytecode instructions like SeekGE, SeekGT etc.
Seek {
index: Option<Arc<Index>>,
seek_def: SeekDef,
},
/// A secondary index search. Uses bytecode instructions like SeekGE, SeekGT etc.
IndexSearch {
index: Arc<Index>,
cmp_op: ast::Operator,
cmp_expr: WhereTerm,
},
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Direction {
Ascending,
Descending,
}
impl Display for Direction {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Direction::Ascending => write!(f, "ASC"),
Direction::Descending => write!(f, "DESC"),
}
}
}
#[derive(Clone, Debug, PartialEq)]
@ -419,14 +739,16 @@ impl Display for SelectPlan {
writeln!(f, "{}SCAN {}", indent, table_name)?;
}
Operation::Search(search) => match search {
Search::RowidEq { .. } | Search::RowidSearch { .. } => {
Search::RowidEq { .. } | Search::Seek { index: None, .. } => {
writeln!(
f,
"{}SEARCH {} USING INTEGER PRIMARY KEY (rowid=?)",
indent, reference.identifier
)?;
}
Search::IndexSearch { index, .. } => {
Search::Seek {
index: Some(index), ..
} => {
writeln!(
f,
"{}SEARCH {} USING INDEX {}",
@ -508,14 +830,16 @@ impl fmt::Display for UpdatePlan {
}
}
Operation::Search(search) => match search {
Search::RowidEq { .. } | Search::RowidSearch { .. } => {
Search::RowidEq { .. } | Search::Seek { index: None, .. } => {
writeln!(
f,
"{}SEARCH {} USING INTEGER PRIMARY KEY (rowid=?)",
indent, reference.identifier
)?;
}
Search::IndexSearch { index, .. } => {
Search::Seek {
index: Some(index), ..
} => {
writeln!(
f,
"{}SEARCH {} USING INDEX {}",
@ -534,7 +858,16 @@ impl fmt::Display for UpdatePlan {
if let Some(order_by) = &self.order_by {
writeln!(f, "ORDER BY:")?;
for (expr, dir) in order_by {
writeln!(f, " - {} {}", expr, dir)?;
writeln!(
f,
" - {} {}",
expr,
if *dir == SortOrder::Asc {
"ASC"
} else {
"DESC"
}
)?;
}
}
if let Some(limit) = self.limit {

View file

@ -1,7 +1,7 @@
use super::{
plan::{
Aggregate, EvalAt, JoinInfo, Operation, Plan, ResultSetColumn, SelectPlan, SelectQueryType,
TableReference, WhereTerm,
Aggregate, ColumnUsedMask, EvalAt, IterationDirection, JoinInfo, Operation, Plan,
ResultSetColumn, SelectPlan, SelectQueryType, TableReference, WhereTerm,
},
select::prepare_select_plan,
SymbolTable,
@ -85,7 +85,7 @@ pub fn resolve_aggregates(expr: &Expr, aggs: &mut Vec<Aggregate>) -> bool {
pub fn bind_column_references(
expr: &mut Expr,
referenced_tables: &[TableReference],
referenced_tables: &mut [TableReference],
result_columns: Option<&[ResultSetColumn]>,
) -> Result<()> {
match expr {
@ -128,6 +128,7 @@ pub fn bind_column_references(
column: col_idx,
is_rowid_alias,
};
referenced_tables[tbl_idx].mark_column_used(col_idx);
return Ok(());
}
@ -178,6 +179,7 @@ pub fn bind_column_references(
column: col_idx.unwrap(),
is_rowid_alias: col.is_rowid_alias,
};
referenced_tables[tbl_idx].mark_column_used(col_idx.unwrap());
Ok(())
}
Expr::Between {
@ -320,10 +322,14 @@ fn parse_from_clause_table<'a>(
));
};
scope.tables.push(TableReference {
op: Operation::Scan { iter_dir: None },
op: Operation::Scan {
iter_dir: IterationDirection::Forwards,
index: None,
},
table: tbl_ref,
identifier: alias.unwrap_or(normalized_qualified_name),
join_info: None,
col_used_mask: ColumnUsedMask::new(),
});
return Ok(());
};
@ -399,10 +405,14 @@ fn parse_from_clause_table<'a>(
.unwrap_or(normalized_name.to_string());
scope.tables.push(TableReference {
op: Operation::Scan { iter_dir: None },
op: Operation::Scan {
iter_dir: IterationDirection::Forwards,
index: None,
},
join_info: None,
table: Table::Virtual(vtab),
identifier: alias,
col_used_mask: ColumnUsedMask::new(),
});
Ok(())
@ -533,7 +543,7 @@ pub fn parse_from<'a>(
pub fn parse_where(
where_clause: Option<Expr>,
table_references: &[TableReference],
table_references: &mut [TableReference],
result_columns: Option<&[ResultSetColumn]>,
out_where_clause: &mut Vec<WhereTerm>,
) -> Result<()> {
@ -564,7 +574,7 @@ pub fn parse_where(
For expressions not referencing any tables (e.g. constants), this is before the main loop is
opened, because they do not need any table data.
*/
fn determine_where_to_eval_expr<'a>(predicate: &'a ast::Expr) -> Result<EvalAt> {
pub fn determine_where_to_eval_expr<'a>(predicate: &'a ast::Expr) -> Result<EvalAt> {
let mut eval_at: EvalAt = EvalAt::BeforeLoop;
match predicate {
ast::Expr::Binary(e1, _, e2) => {
@ -752,7 +762,7 @@ fn parse_join<'a>(
let mut preds = vec![];
break_predicate_at_and_boundaries(expr, &mut preds);
for predicate in preds.iter_mut() {
bind_column_references(predicate, &scope.tables, None)?;
bind_column_references(predicate, &mut scope.tables, None)?;
}
for pred in preds {
let cur_table_idx = scope.tables.len() - 1;
@ -826,6 +836,11 @@ fn parse_join<'a>(
is_rowid_alias: right_col.is_rowid_alias,
}),
);
let left_table = scope.tables.get_mut(left_table_idx).unwrap();
left_table.mark_column_used(left_col_idx);
let right_table = scope.tables.get_mut(cur_table_idx).unwrap();
right_table.mark_column_used(right_col_idx);
let eval_at = if outer {
EvalAt::Loop(cur_table_idx)
} else {
@ -850,30 +865,33 @@ fn parse_join<'a>(
Ok(())
}
pub fn parse_limit(limit: Limit) -> Result<(Option<isize>, Option<isize>)> {
let offset_val = match limit.offset {
pub fn parse_limit(limit: &Limit) -> Result<(Option<isize>, Option<isize>)> {
let offset_val = match &limit.offset {
Some(offset_expr) => match offset_expr {
Expr::Literal(ast::Literal::Numeric(n)) => n.parse().ok(),
// If OFFSET is negative, the result is as if OFFSET is zero
Expr::Unary(UnaryOperator::Negative, expr) => match *expr {
Expr::Literal(ast::Literal::Numeric(n)) => n.parse::<isize>().ok().map(|num| -num),
_ => crate::bail_parse_error!("Invalid OFFSET clause"),
},
Expr::Unary(UnaryOperator::Negative, expr) => {
if let Expr::Literal(ast::Literal::Numeric(ref n)) = &**expr {
n.parse::<isize>().ok().map(|num| -num)
} else {
crate::bail_parse_error!("Invalid OFFSET clause");
}
}
_ => crate::bail_parse_error!("Invalid OFFSET clause"),
},
None => Some(0),
};
if let Expr::Literal(ast::Literal::Numeric(n)) = limit.expr {
if let Expr::Literal(ast::Literal::Numeric(n)) = &limit.expr {
Ok((n.parse().ok(), offset_val))
} else if let Expr::Unary(UnaryOperator::Negative, expr) = limit.expr {
if let Expr::Literal(ast::Literal::Numeric(n)) = *expr {
} else if let Expr::Unary(UnaryOperator::Negative, expr) = &limit.expr {
if let Expr::Literal(ast::Literal::Numeric(n)) = &**expr {
let limit_val = n.parse::<isize>().ok().map(|num| -num);
Ok((limit_val, offset_val))
} else {
crate::bail_parse_error!("Invalid LIMIT clause");
}
} else if let Expr::Id(id) = limit.expr {
} else if let Expr::Id(id) = &limit.expr {
if id.0.eq_ignore_ascii_case("true") {
Ok((Some(1), offset_val))
} else if id.0.eq_ignore_ascii_case("false") {

View file

@ -29,7 +29,7 @@ fn list_pragmas(
}
program.emit_halt();
program.resolve_label(init_label, program.offset());
program.preassign_label_to_next_insn(init_label);
program.emit_constant_insns();
program.emit_goto(start_offset);
}
@ -104,7 +104,7 @@ pub fn translate_pragma(
},
};
program.emit_halt();
program.resolve_label(init_label, program.offset());
program.preassign_label_to_next_insn(init_label);
program.emit_transaction(write);
program.emit_constant_insns();
program.emit_goto(start_offset);
@ -154,12 +154,19 @@ fn update_pragma(
// TODO: Implement updating user_version
todo!("updating user_version not yet implemented")
}
PragmaName::SchemaVersion => {
// TODO: Implement updating schema_version
todo!("updating schema_version not yet implemented")
}
PragmaName::TableInfo => {
// because we need control over the write parameter for the transaction,
// this should be unreachable. We have to force-call query_pragma before
// getting here
unreachable!();
}
PragmaName::PageSize => {
todo!("updating page_size is not yet implemented")
}
}
}
@ -249,7 +256,6 @@ fn query_pragma(
}
}
PragmaName::UserVersion => {
program.emit_transaction(false);
program.emit_insn(Insn::ReadCookie {
db: 0,
dest: register,
@ -257,6 +263,18 @@ fn query_pragma(
});
program.emit_result_row(register, 1);
}
PragmaName::SchemaVersion => {
program.emit_insn(Insn::ReadCookie {
db: 0,
dest: register,
cookie: Cookie::SchemaVersion,
});
program.emit_result_row(register, 1);
}
PragmaName::PageSize => {
program.emit_int(database_header.lock().get_page_size().into(), register);
program.emit_result_row(register, 1);
}
}
Ok(())

View file

@ -25,7 +25,16 @@ pub fn emit_select_result(
}
let start_reg = t_ctx.reg_result_cols_start.unwrap();
for (i, rc) in plan.result_columns.iter().enumerate() {
for (i, rc) in plan.result_columns.iter().enumerate().filter(|(_, rc)| {
// For aggregate queries, we handle columns differently; example: select id, first_name, sum(age) from users limit 1;
// 1. Columns with aggregates (e.g., sum(age)) are computed in each iteration of aggregation
// 2. Non-aggregate columns (e.g., id, first_name) are only computed once in the first iteration
// This filter ensures we only emit expressions for non aggregate columns once,
// preserving previously calculated values while updating aggregate results
// For all other queries where reg_nonagg_emit_once_flag is none we do nothing.
t_ctx.reg_nonagg_emit_once_flag.is_some() && rc.contains_aggregates
|| t_ctx.reg_nonagg_emit_once_flag.is_none()
}) {
let reg = start_reg + i;
translate_expr(
program,

View file

@ -1,7 +1,11 @@
use std::fmt::Display;
use std::rc::Rc;
use crate::ast;
use crate::ext::VTabImpl;
use crate::schema::Schema;
use crate::schema::Table;
use crate::storage::pager::CreateBTreeFlags;
use crate::translate::ProgramBuilder;
use crate::translate::ProgramBuilderOpts;
use crate::translate::QueryMode;
@ -9,8 +13,10 @@ use crate::util::PRIMARY_KEY_AUTOMATIC_INDEX_NAME_PREFIX;
use crate::vdbe::builder::CursorType;
use crate::vdbe::insn::{CmpInsFlags, Insn};
use crate::LimboError;
use crate::SymbolTable;
use crate::{bail_parse_error, Result};
use limbo_ext::VTabKind;
use limbo_sqlite3_parser::ast::{fmt::ToTokens, CreateVirtualTable};
pub fn translate_create_table(
@ -35,7 +41,7 @@ pub fn translate_create_table(
let init_label = program.emit_init();
let start_offset = program.offset();
program.emit_halt();
program.resolve_label(init_label, program.offset());
program.preassign_label_to_next_insn(init_label);
program.emit_transaction(true);
program.emit_constant_insns();
program.emit_goto(start_offset);
@ -60,7 +66,7 @@ pub fn translate_create_table(
program.emit_insn(Insn::CreateBtree {
db: 0,
root: table_root_reg,
flags: 1, // Table leaf page
flags: CreateBTreeFlags::new_table(),
});
// Create an automatic index B-tree if needed
@ -92,7 +98,7 @@ pub fn translate_create_table(
program.emit_insn(Insn::CreateBtree {
db: 0,
root: index_root_reg,
flags: 2, // Index leaf page
flags: CreateBTreeFlags::new_index(),
});
}
@ -101,11 +107,10 @@ pub fn translate_create_table(
Some(SQLITE_TABLEID.to_owned()),
CursorType::BTreeTable(table.clone()),
);
program.emit_insn(Insn::OpenWriteAsync {
program.emit_insn(Insn::OpenWrite {
cursor_id: sqlite_schema_cursor_id,
root_page: 1,
root_page: 1usize.into(),
});
program.emit_insn(Insn::OpenWriteAwait {});
// Add the table entry to sqlite_schema
emit_schema_entry(
@ -147,7 +152,7 @@ pub fn translate_create_table(
// TODO: SqlExec
program.emit_halt();
program.resolve_label(init_label, program.offset());
program.preassign_label_to_next_insn(init_label);
program.emit_transaction(true);
program.emit_constant_insns();
program.emit_goto(start_offset);
@ -155,8 +160,8 @@ pub fn translate_create_table(
Ok(program)
}
#[derive(Debug)]
enum SchemaEntryType {
#[derive(Debug, Clone, Copy)]
pub enum SchemaEntryType {
Table,
Index,
}
@ -169,9 +174,9 @@ impl SchemaEntryType {
}
}
}
const SQLITE_TABLEID: &str = "sqlite_schema";
pub const SQLITE_TABLEID: &str = "sqlite_schema";
fn emit_schema_entry(
pub fn emit_schema_entry(
program: &mut ProgramBuilder,
sqlite_schema_cursor_id: usize,
entry_type: SchemaEntryType,
@ -219,15 +224,12 @@ fn emit_schema_entry(
dest_reg: record_reg,
});
program.emit_insn(Insn::InsertAsync {
program.emit_insn(Insn::Insert {
cursor: sqlite_schema_cursor_id,
key_reg: rowid_reg,
record_reg,
flag: 0,
});
program.emit_insn(Insn::InsertAwait {
cursor_id: sqlite_schema_cursor_id,
});
}
struct PrimaryKeyColumnInfo<'a> {
@ -398,7 +400,7 @@ fn create_table_body_to_str(tbl_name: &ast::QualifiedName, body: &ast::CreateTab
sql
}
fn create_vtable_body_to_str(vtab: &CreateVirtualTable) -> String {
fn create_vtable_body_to_str(vtab: &CreateVirtualTable, module: Rc<VTabImpl>) -> String {
let args = if let Some(args) = &vtab.args {
args.iter()
.map(|arg| arg.to_string())
@ -412,8 +414,25 @@ fn create_vtable_body_to_str(vtab: &CreateVirtualTable) -> String {
} else {
""
};
let ext_args = vtab
.args
.as_ref()
.unwrap_or(&vec![])
.iter()
.map(|a| limbo_ext::Value::from_text(a.to_string()))
.collect::<Vec<_>>();
let schema = module
.implementation
.init_schema(ext_args)
.unwrap_or_default();
let vtab_args = if let Some(first_paren) = schema.find('(') {
let closing_paren = schema.rfind(')').unwrap_or_default();
&schema[first_paren..=closing_paren]
} else {
"()"
};
format!(
"CREATE VIRTUAL TABLE {} {} USING {}{}",
"CREATE VIRTUAL TABLE {} {} USING {}{}\n /*{}{}*/",
vtab.tbl_name.name.0,
if_not_exists,
vtab.module_name.0,
@ -421,7 +440,9 @@ fn create_vtable_body_to_str(vtab: &CreateVirtualTable) -> String {
String::new()
} else {
format!("({})", args)
}
},
vtab.tbl_name.name.0,
vtab_args
)
}
@ -429,6 +450,7 @@ pub fn translate_create_virtual_table(
vtab: CreateVirtualTable,
schema: &Schema,
query_mode: QueryMode,
syms: &SymbolTable,
) -> Result<ProgramBuilder> {
let ast::CreateVirtualTable {
if_not_exists,
@ -440,7 +462,12 @@ pub fn translate_create_virtual_table(
let table_name = tbl_name.name.0.clone();
let module_name_str = module_name.0.clone();
let args_vec = args.clone().unwrap_or_default();
let Some(vtab_module) = syms.vtab_modules.get(&module_name_str) else {
bail_parse_error!("no such module: {}", module_name_str);
};
if !vtab_module.module_kind.eq(&VTabKind::VirtualTable) {
bail_parse_error!("module {} is not a virtual table", module_name_str);
};
if schema.get_table(&table_name).is_some() && *if_not_exists {
let mut program = ProgramBuilder::new(ProgramBuilderOpts {
query_mode,
@ -450,7 +477,7 @@ pub fn translate_create_virtual_table(
});
let init_label = program.emit_init();
program.emit_halt();
program.resolve_label(init_label, program.offset());
program.preassign_label_to_next_insn(init_label);
program.emit_transaction(true);
program.emit_constant_insns();
return Ok(program);
@ -462,10 +489,10 @@ pub fn translate_create_virtual_table(
approx_num_insns: 40,
approx_num_labels: 2,
});
let init_label = program.emit_init();
let start_offset = program.offset();
let module_name_reg = program.emit_string8_new_reg(module_name_str.clone());
let table_name_reg = program.emit_string8_new_reg(table_name.clone());
let args_reg = if !args_vec.is_empty() {
let args_start = program.alloc_register();
@ -491,19 +518,17 @@ pub fn translate_create_virtual_table(
table_name: table_name_reg,
args_reg,
});
let table = schema.get_btree_table(SQLITE_TABLEID).unwrap();
let sqlite_schema_cursor_id = program.alloc_cursor_id(
Some(SQLITE_TABLEID.to_owned()),
CursorType::BTreeTable(table.clone()),
);
program.emit_insn(Insn::OpenWriteAsync {
program.emit_insn(Insn::OpenWrite {
cursor_id: sqlite_schema_cursor_id,
root_page: 1,
root_page: 1usize.into(),
});
program.emit_insn(Insn::OpenWriteAwait {});
let sql = create_vtable_body_to_str(&vtab);
let sql = create_vtable_body_to_str(&vtab, vtab_module.clone());
emit_schema_entry(
&mut program,
sqlite_schema_cursor_id,
@ -520,10 +545,8 @@ pub fn translate_create_virtual_table(
where_clause: parse_schema_where_clause,
});
let init_label = program.emit_init();
let start_offset = program.offset();
program.emit_halt();
program.resolve_label(init_label, program.offset());
program.preassign_label_to_next_insn(init_label);
program.emit_transaction(true);
program.emit_constant_insns();
program.emit_goto(start_offset);
@ -543,13 +566,13 @@ pub fn translate_drop_table(
approx_num_insns: 30,
approx_num_labels: 1,
});
let table = schema.get_btree_table(tbl_name.name.0.as_str());
let table = schema.get_table(tbl_name.name.0.as_str());
if table.is_none() {
if if_exists {
let init_label = program.emit_init();
let start_offset = program.offset();
program.emit_halt();
program.resolve_label(init_label, program.offset());
program.preassign_label_to_next_insn(init_label);
program.emit_transaction(true);
program.emit_constant_insns();
program.emit_goto(start_offset);
@ -558,6 +581,7 @@ pub fn translate_drop_table(
}
bail_parse_error!("No such table: {}", tbl_name.name.0.as_str());
}
let table = table.unwrap(); // safe since we just checked for None
let init_label = program.emit_init();
@ -573,31 +597,27 @@ pub fn translate_drop_table(
let row_id_reg = program.alloc_register(); // r5
let table_name = "sqlite_schema";
let schema_table = schema.get_btree_table(&table_name).unwrap();
let schema_table = schema.get_btree_table(table_name).unwrap();
let sqlite_schema_cursor_id = program.alloc_cursor_id(
Some(table_name.to_string()),
CursorType::BTreeTable(schema_table.clone()),
);
program.emit_insn(Insn::OpenWriteAsync {
program.emit_insn(Insn::OpenWrite {
cursor_id: sqlite_schema_cursor_id,
root_page: 1,
root_page: 1usize.into(),
});
program.emit_insn(Insn::OpenWriteAwait {});
// 1. Remove all entries from the schema table related to the table we are dropping, except for triggers
// loop to beginning of schema table
program.emit_insn(Insn::RewindAsync {
cursor_id: sqlite_schema_cursor_id,
});
let end_metadata_label = program.allocate_label();
program.emit_insn(Insn::RewindAwait {
let metadata_loop = program.allocate_label();
program.emit_insn(Insn::Rewind {
cursor_id: sqlite_schema_cursor_id,
pc_if_empty: end_metadata_label,
});
program.preassign_label_to_next_insn(metadata_loop);
// start loop on schema table
let metadata_loop = program.allocate_label();
program.resolve_label(metadata_loop, program.offset());
program.emit_insn(Insn::Column {
cursor_id: sqlite_schema_cursor_id,
column: 2,
@ -625,22 +645,16 @@ pub fn translate_drop_table(
cursor_id: sqlite_schema_cursor_id,
dest: row_id_reg,
});
program.emit_insn(Insn::DeleteAsync {
cursor_id: sqlite_schema_cursor_id,
});
program.emit_insn(Insn::DeleteAwait {
program.emit_insn(Insn::Delete {
cursor_id: sqlite_schema_cursor_id,
});
program.resolve_label(next_label, program.offset());
program.emit_insn(Insn::NextAsync {
cursor_id: sqlite_schema_cursor_id,
});
program.emit_insn(Insn::NextAwait {
program.emit_insn(Insn::Next {
cursor_id: sqlite_schema_cursor_id,
pc_if_next: metadata_loop,
});
program.resolve_label(end_metadata_label, program.offset());
program.preassign_label_to_next_insn(end_metadata_label);
// end of loop on schema table
// 2. Destroy the indices within a loop
@ -663,11 +677,31 @@ pub fn translate_drop_table(
}
// 3. Destroy the table structure
program.emit_insn(Insn::Destroy {
root: table.root_page,
former_root_reg: 0, // no autovacuum (https://www.sqlite.org/opcode.html#Destroy)
is_temp: 0,
});
match table.as_ref() {
Table::BTree(table) => {
program.emit_insn(Insn::Destroy {
root: table.root_page,
former_root_reg: 0, // no autovacuum (https://www.sqlite.org/opcode.html#Destroy)
is_temp: 0,
});
}
Table::Virtual(vtab) => {
// From what I see, TableValuedFunction is not stored in the schema as a table.
// But this line here below is a safeguard in case this behavior changes in the future
// And mirrors what SQLite does.
if matches!(vtab.kind, limbo_ext::VTabKind::TableValuedFunction) {
return Err(crate::LimboError::ParseError(format!(
"table {} may not be dropped",
vtab.name
)));
}
program.emit_insn(Insn::VDestroy {
table_name: vtab.name.clone(),
db: 0, // TODO change this for multiple databases
});
}
Table::Pseudo(..) => unimplemented!(),
};
let r6 = program.alloc_register();
let r7 = program.alloc_register();
@ -689,7 +723,7 @@ pub fn translate_drop_table(
// end of the program
program.emit_halt();
program.resolve_label(init_label, program.offset());
program.preassign_label_to_next_insn(init_label);
program.emit_transaction(true);
program.emit_constant_insns();

View file

@ -3,7 +3,7 @@ use super::plan::{select_star, Operation, Search, SelectQueryType};
use super::planner::Scope;
use crate::function::{AggFunc, ExtFunc, Func};
use crate::translate::optimizer::optimize_plan;
use crate::translate::plan::{Aggregate, Direction, GroupBy, Plan, ResultSetColumn, SelectPlan};
use crate::translate::plan::{Aggregate, GroupBy, Plan, ResultSetColumn, SelectPlan};
use crate::translate::planner::{
bind_column_references, break_predicate_at_and_boundaries, parse_from, parse_limit,
parse_where, resolve_aggregates,
@ -104,12 +104,17 @@ pub fn prepare_select_plan<'a>(
match column {
ResultColumn::Star => {
select_star(&plan.table_references, &mut plan.result_columns);
for table in plan.table_references.iter_mut() {
for idx in 0..table.columns().len() {
table.mark_column_used(idx);
}
}
}
ResultColumn::TableStar(name) => {
let name_normalized = normalize_ident(name.0.as_str());
let referenced_table = plan
.table_references
.iter()
.iter_mut()
.enumerate()
.find(|(_, t)| t.identifier == name_normalized);
@ -117,23 +122,29 @@ pub fn prepare_select_plan<'a>(
crate::bail_parse_error!("Table {} not found", name.0);
}
let (table_index, table) = referenced_table.unwrap();
for (idx, col) in table.columns().iter().enumerate() {
let num_columns = table.columns().len();
for idx in 0..num_columns {
let is_rowid_alias = {
let columns = table.columns();
columns[idx].is_rowid_alias
};
plan.result_columns.push(ResultSetColumn {
expr: ast::Expr::Column {
database: None, // TODO: support different databases
table: table_index,
column: idx,
is_rowid_alias: col.is_rowid_alias,
is_rowid_alias,
},
alias: None,
contains_aggregates: false,
});
table.mark_column_used(idx);
}
}
ResultColumn::Expr(ref mut expr, maybe_alias) => {
bind_column_references(
expr,
&plan.table_references,
&mut plan.table_references,
Some(&plan.result_columns),
)?;
match expr {
@ -293,7 +304,7 @@ pub fn prepare_select_plan<'a>(
// Parse the actual WHERE clause and add its conditions to the plan WHERE clause that already contains the join conditions.
parse_where(
where_clause,
&plan.table_references,
&mut plan.table_references,
Some(&plan.result_columns),
&mut plan.where_clause,
)?;
@ -303,7 +314,7 @@ pub fn prepare_select_plan<'a>(
replace_column_number_with_copy_of_column_expr(expr, &plan.result_columns)?;
bind_column_references(
expr,
&plan.table_references,
&mut plan.table_references,
Some(&plan.result_columns),
)?;
}
@ -316,7 +327,7 @@ pub fn prepare_select_plan<'a>(
for expr in predicates.iter_mut() {
bind_column_references(
expr,
&plan.table_references,
&mut plan.table_references,
Some(&plan.result_columns),
)?;
let contains_aggregates =
@ -352,25 +363,19 @@ pub fn prepare_select_plan<'a>(
bind_column_references(
&mut o.expr,
&plan.table_references,
&mut plan.table_references,
Some(&plan.result_columns),
)?;
resolve_aggregates(&o.expr, &mut plan.aggregates);
key.push((
o.expr,
o.order.map_or(Direction::Ascending, |o| match o {
ast::SortOrder::Asc => Direction::Ascending,
ast::SortOrder::Desc => Direction::Descending,
}),
));
key.push((o.expr, o.order.unwrap_or(ast::SortOrder::Asc)));
}
plan.order_by = Some(key);
}
// Parse the LIMIT/OFFSET clause
(plan.limit, plan.offset) =
select.limit.map_or(Ok((None, None)), |l| parse_limit(*l))?;
select.limit.map_or(Ok((None, None)), |l| parse_limit(&l))?;
// Return the unoptimized query plan
Ok(Plan::Select(plan))
@ -411,8 +416,8 @@ fn count_plan_required_cursors(plan: &SelectPlan) -> usize {
.map(|t| match &t.op {
Operation::Scan { .. } => 1,
Operation::Search(search) => match search {
Search::RowidEq { .. } | Search::RowidSearch { .. } => 1,
Search::IndexSearch { .. } => 2, // btree cursor and index cursor
Search::RowidEq { .. } => 1,
Search::Seek { index, .. } => 1 + index.is_some() as usize,
},
Operation::Subquery { plan, .. } => count_plan_required_cursors(plan),
})

View file

@ -52,7 +52,7 @@ pub fn emit_subquery<'a>(
t_ctx: &mut TranslateCtx<'a>,
) -> Result<usize> {
let yield_reg = program.alloc_register();
let coroutine_implementation_start_offset = program.offset().add(1u32);
let coroutine_implementation_start_offset = program.allocate_label();
match &mut plan.query_type {
SelectQueryType::Subquery {
yield_reg: y,
@ -75,6 +75,7 @@ pub fn emit_subquery<'a>(
meta_left_joins: (0..plan.table_references.len()).map(|_| None).collect(),
meta_sort: None,
reg_agg_start: None,
reg_nonagg_emit_once_flag: None,
reg_result_cols_start: None,
result_column_indexes_in_orderby_sorter: (0..plan.result_columns.len()).collect(),
result_columns_to_skip_in_orderby_sorter: None,
@ -82,6 +83,7 @@ pub fn emit_subquery<'a>(
reg_offset: plan.offset.map(|_| program.alloc_register()),
reg_limit_offset_sum: plan.offset.map(|_| program.alloc_register()),
resolver: Resolver::new(t_ctx.resolver.symbol_table),
omit_predicates: Vec::new(),
};
let subquery_body_end_label = program.allocate_label();
program.emit_insn(Insn::InitCoroutine {
@ -89,6 +91,7 @@ pub fn emit_subquery<'a>(
jump_on_definition: subquery_body_end_label,
start_offset: coroutine_implementation_start_offset,
});
program.preassign_label_to_next_insn(coroutine_implementation_start_offset);
// Normally we mark each LIMIT value as a constant insn that is emitted only once, but in the case of a subquery,
// we need to initialize it every time the subquery is run; otherwise subsequent runs of the subquery will already
// have the LIMIT counter at 0, and will never return rows.
@ -101,6 +104,6 @@ pub fn emit_subquery<'a>(
let result_column_start_reg = emit_query(program, plan, &mut metadata)?;
program.resolve_label(end_coroutine_label, program.offset());
program.emit_insn(Insn::EndCoroutine { yield_reg });
program.resolve_label(subquery_body_end_label, program.offset());
program.preassign_label_to_next_insn(subquery_body_end_label);
Ok(result_column_start_reg)
}

View file

@ -33,7 +33,7 @@ pub fn translate_tx_begin(
}
}
program.emit_halt();
program.resolve_label(init_label, program.offset());
program.preassign_label_to_next_insn(init_label);
program.emit_goto(start_offset);
Ok(program)
}
@ -52,7 +52,7 @@ pub fn translate_tx_commit(_tx_name: Option<Name>) -> Result<ProgramBuilder> {
rollback: false,
});
program.emit_halt();
program.resolve_label(init_label, program.offset());
program.preassign_label_to_next_insn(init_label);
program.emit_goto(start_offset);
Ok(program)
}

View file

@ -11,9 +11,10 @@ use limbo_sqlite3_parser::ast::{self, Expr, ResultColumn, SortOrder, Update};
use super::emitter::emit_program;
use super::optimizer::optimize_plan;
use super::plan::{
Direction, IterationDirection, Plan, ResultSetColumn, TableReference, UpdatePlan,
ColumnUsedMask, IterationDirection, Plan, ResultSetColumn, TableReference, UpdatePlan,
};
use super::planner::{bind_column_references, parse_limit, parse_where};
use super::planner::bind_column_references;
use super::planner::{parse_limit, parse_where};
/*
* Update is simple. By default we scan the table, and for each row, we check the WHERE
@ -64,35 +65,50 @@ pub fn translate_update(
}
pub fn prepare_update_plan(schema: &Schema, body: &mut Update) -> crate::Result<Plan> {
if body.with.is_some() {
bail_parse_error!("WITH clause is not supported");
}
if body.or_conflict.is_some() {
bail_parse_error!("ON CONFLICT clause is not supported");
}
let table_name = &body.tbl_name.name;
let table = match schema.get_table(table_name.0.as_str()) {
Some(table) => table,
None => bail_parse_error!("Parse error: no such table: {}", table_name),
};
let Some(btree_table) = table.btree() else {
bail_parse_error!("Error: {} is not a btree table", table_name);
};
let iter_dir: Option<IterationDirection> = body.order_by.as_ref().and_then(|order_by| {
order_by.first().and_then(|ob| {
ob.order.map(|o| match o {
SortOrder::Asc => IterationDirection::Forwards,
SortOrder::Desc => IterationDirection::Backwards,
let iter_dir = body
.order_by
.as_ref()
.and_then(|order_by| {
order_by.first().and_then(|ob| {
ob.order.map(|o| match o {
SortOrder::Asc => IterationDirection::Forwards,
SortOrder::Desc => IterationDirection::Backwards,
})
})
})
});
let table_references = vec![TableReference {
table: Table::BTree(btree_table.clone()),
.unwrap_or(IterationDirection::Forwards);
let mut table_references = vec![TableReference {
table: match table.as_ref() {
Table::Virtual(vtab) => Table::Virtual(vtab.clone()),
Table::BTree(btree_table) => Table::BTree(btree_table.clone()),
_ => unreachable!(),
},
identifier: table_name.0.clone(),
op: Operation::Scan { iter_dir },
op: Operation::Scan {
iter_dir,
index: None,
},
join_info: None,
col_used_mask: ColumnUsedMask::new(),
}];
let set_clauses = body
.sets
.iter_mut()
.map(|set| {
let ident = normalize_ident(set.col_names[0].0.as_str());
let col_index = btree_table
.columns
let col_index = table
.columns()
.iter()
.enumerate()
.find_map(|(i, col)| {
@ -108,7 +124,7 @@ pub fn prepare_update_plan(schema: &Schema, body: &mut Update) -> crate::Result<
))
})?;
let _ = bind_column_references(&mut set.expr, &table_references, None);
let _ = bind_column_references(&mut set.expr, &mut table_references, None);
Ok((col_index, set.expr.clone()))
})
.collect::<Result<Vec<(usize, Expr)>, crate::LimboError>>()?;
@ -118,7 +134,7 @@ pub fn prepare_update_plan(schema: &Schema, body: &mut Update) -> crate::Result<
if let Some(returning) = &mut body.returning {
for rc in returning.iter_mut() {
if let ResultColumn::Expr(expr, alias) = rc {
bind_column_references(expr, &table_references, None)?;
bind_column_references(expr, &mut table_references, None)?;
result_columns.push(ResultSetColumn {
expr: expr.clone(),
alias: alias.as_ref().and_then(|a| {
@ -138,30 +154,39 @@ pub fn prepare_update_plan(schema: &Schema, body: &mut Update) -> crate::Result<
let order_by = body.order_by.as_ref().map(|order| {
order
.iter()
.map(|o| {
(
o.expr.clone(),
o.order
.map(|s| match s {
SortOrder::Asc => Direction::Ascending,
SortOrder::Desc => Direction::Descending,
})
.unwrap_or(Direction::Ascending),
)
})
.map(|o| (o.expr.clone(), o.order.unwrap_or(SortOrder::Asc)))
.collect()
});
// Parse the WHERE clause
parse_where(
body.where_clause.as_ref().map(|w| *w.clone()),
&table_references,
&mut table_references,
Some(&result_columns),
&mut where_clause,
)?;
let limit = if let Some(Ok((limit, _))) = body.limit.as_ref().map(|l| parse_limit(*l.clone())) {
limit
} else {
None
};
// Parse the LIMIT/OFFSET clause
let (limit, offset) = body
.limit
.as_ref()
.map(|l| parse_limit(l))
.unwrap_or(Ok((None, None)))?;
// Check what indexes will need to be updated by checking set_clauses and see
// if a column is contained in an index.
let indexes = schema.get_indices(&table_name.0);
let indexes_to_update = indexes
.iter()
.filter(|index| {
index.columns.iter().any(|index_column| {
set_clauses
.iter()
.any(|(set_index_column, _)| index_column.pos_in_table == *set_index_column)
})
})
.cloned()
.collect();
Ok(Plan::Update(UpdatePlan {
table_references,
set_clauses,
@ -169,6 +194,8 @@ pub fn prepare_update_plan(schema: &Schema, body: &mut Update) -> crate::Result<
returning: Some(result_columns),
order_by,
limit,
offset,
contains_constant_false_condition: false,
indexes_to_update,
}))
}

View file

@ -1,10 +1,13 @@
use limbo_ext::{AggCtx, FinalizeFunction, StepFunction};
use limbo_sqlite3_parser::ast::SortOrder;
use crate::error::LimboError;
use crate::ext::{ExtValue, ExtValueType};
use crate::pseudo::PseudoCursor;
use crate::schema::Index;
use crate::storage::btree::BTreeCursor;
use crate::storage::sqlite3_ondisk::write_varint;
use crate::translate::plan::IterationDirection;
use crate::vdbe::sorter::Sorter;
use crate::vdbe::{Register, VTabOpaqueCursor};
use crate::Result;
@ -22,6 +25,20 @@ pub enum OwnedValueType {
Error,
}
impl Display for OwnedValueType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let value = match self {
Self::Null => "NULL",
Self::Integer => "INT",
Self::Float => "REAL",
Self::Blob => "BLOB",
Self::Text => "TEXT",
Self::Error => "ERROR",
};
write!(f, "{}", value)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum TextSubtype {
Text,
@ -69,6 +86,15 @@ impl Text {
}
}
impl From<String> for Text {
fn from(value: String) -> Self {
Text {
value: value.into_bytes(),
subtype: TextSubtype::Text,
}
}
}
impl TextRef {
pub fn as_str(&self) -> &str {
unsafe { std::str::from_utf8_unchecked(self.value.to_slice()) }
@ -145,13 +171,13 @@ impl OwnedValue {
OwnedValue::Null => {}
OwnedValue::Integer(i) => {
let serial_type = SerialType::from(self);
match serial_type {
SerialType::I8 => out.extend_from_slice(&(*i as i8).to_be_bytes()),
SerialType::I16 => out.extend_from_slice(&(*i as i16).to_be_bytes()),
SerialType::I24 => out.extend_from_slice(&(*i as i32).to_be_bytes()[1..]), // remove most significant byte
SerialType::I32 => out.extend_from_slice(&(*i as i32).to_be_bytes()),
SerialType::I48 => out.extend_from_slice(&i.to_be_bytes()[2..]), // remove 2 most significant bytes
SerialType::I64 => out.extend_from_slice(&i.to_be_bytes()),
match serial_type.kind() {
SerialTypeKind::I8 => out.extend_from_slice(&(*i as i8).to_be_bytes()),
SerialTypeKind::I16 => out.extend_from_slice(&(*i as i16).to_be_bytes()),
SerialTypeKind::I24 => out.extend_from_slice(&(*i as i32).to_be_bytes()[1..]), // remove most significant byte
SerialTypeKind::I32 => out.extend_from_slice(&(*i as i32).to_be_bytes()),
SerialTypeKind::I48 => out.extend_from_slice(&i.to_be_bytes()[2..]), // remove 2 most significant bytes
SerialTypeKind::I64 => out.extend_from_slice(&i.to_be_bytes()),
_ => unreachable!(),
}
}
@ -197,6 +223,12 @@ impl Display for OwnedValue {
}
Self::Float(fl) => {
let fl = *fl;
if fl == f64::INFINITY {
return write!(f, "Inf");
}
if fl == f64::NEG_INFINITY {
return write!(f, "-Inf");
}
if fl.is_nan() {
return write!(f, "");
}
@ -732,6 +764,10 @@ impl ImmutableRecord {
&self.values[idx]
}
pub fn get_value_opt(&self, idx: usize) -> Option<&RefValue> {
self.values.get(idx)
}
pub fn len(&self) -> usize {
self.values.len()
}
@ -750,18 +786,7 @@ impl ImmutableRecord {
let n = write_varint(&mut serial_type_buf[0..], serial_type.into());
serials.push((serial_type_buf, n));
let value_size = match serial_type {
SerialType::Null => 0,
SerialType::I8 => 1,
SerialType::I16 => 2,
SerialType::I24 => 3,
SerialType::I32 => 4,
SerialType::I48 => 6,
SerialType::I64 => 8,
SerialType::F64 => 8,
SerialType::Text { content_size } => content_size,
SerialType::Blob { content_size } => content_size,
};
let value_size = serial_type.size();
size_header += n;
size_values += value_size;
@ -808,16 +833,17 @@ impl ImmutableRecord {
OwnedValue::Integer(i) => {
values.push(RefValue::Integer(*i));
let serial_type = SerialType::from(value);
match serial_type {
SerialType::I8 => writer.extend_from_slice(&(*i as i8).to_be_bytes()),
SerialType::I16 => writer.extend_from_slice(&(*i as i16).to_be_bytes()),
SerialType::I24 => {
match serial_type.kind() {
SerialTypeKind::ConstInt0 | SerialTypeKind::ConstInt1 => {}
SerialTypeKind::I8 => writer.extend_from_slice(&(*i as i8).to_be_bytes()),
SerialTypeKind::I16 => writer.extend_from_slice(&(*i as i16).to_be_bytes()),
SerialTypeKind::I24 => {
writer.extend_from_slice(&(*i as i32).to_be_bytes()[1..])
} // remove most significant byte
SerialType::I32 => writer.extend_from_slice(&(*i as i32).to_be_bytes()),
SerialType::I48 => writer.extend_from_slice(&i.to_be_bytes()[2..]), // remove 2 most significant bytes
SerialType::I64 => writer.extend_from_slice(&i.to_be_bytes()),
_ => unreachable!(),
SerialTypeKind::I32 => writer.extend_from_slice(&(*i as i32).to_be_bytes()),
SerialTypeKind::I48 => writer.extend_from_slice(&i.to_be_bytes()[2..]), // remove 2 most significant bytes
SerialTypeKind::I64 => writer.extend_from_slice(&i.to_be_bytes()),
other => panic!("Serial type is not an integer: {:?}", other),
}
}
OwnedValue::Float(f) => {
@ -877,6 +903,26 @@ impl ImmutableRecord {
}
}
impl Display for ImmutableRecord {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for value in &self.values {
match value {
RefValue::Null => write!(f, "NULL")?,
RefValue::Integer(i) => write!(f, "Integer({})", *i)?,
RefValue::Float(flo) => write!(f, "Float({})", *flo)?,
RefValue::Text(text_ref) => write!(f, "Text({})", text_ref.as_str())?,
RefValue::Blob(raw_slice) => {
write!(f, "Blob({})", String::from_utf8_lossy(raw_slice.to_slice()))?
}
}
if value != self.values.last().unwrap() {
write!(f, ", ")?;
}
}
Ok(())
}
}
impl Clone for ImmutableRecord {
fn clone(&self) -> Self {
let mut new_values = Vec::new();
@ -1009,8 +1055,66 @@ impl PartialOrd<RefValue> for RefValue {
}
}
pub fn compare_immutable(l: &[RefValue], r: &[RefValue]) -> std::cmp::Ordering {
l.partial_cmp(r).unwrap()
/// A bitfield that represents the comparison spec for index keys.
/// Since indexed columns can individually specify ASC/DESC, each key must
/// be compared differently.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[repr(transparent)]
pub struct IndexKeySortOrder(u64);
impl IndexKeySortOrder {
pub fn get_sort_order_for_col(&self, column_idx: usize) -> SortOrder {
assert!(column_idx < 64, "column index out of range: {}", column_idx);
match self.0 & (1 << column_idx) {
0 => SortOrder::Asc,
_ => SortOrder::Desc,
}
}
pub fn from_index(index: &Index) -> Self {
let mut spec = 0;
for (i, column) in index.columns.iter().enumerate() {
spec |= ((column.order == SortOrder::Desc) as u64) << i;
}
IndexKeySortOrder(spec)
}
pub fn from_list(order: &[SortOrder]) -> Self {
let mut spec = 0;
for (i, order) in order.iter().enumerate() {
spec |= ((*order == SortOrder::Desc) as u64) << i;
}
IndexKeySortOrder(spec)
}
pub fn default() -> Self {
Self(0)
}
}
impl Default for IndexKeySortOrder {
fn default() -> Self {
Self::default()
}
}
pub fn compare_immutable(
l: &[RefValue],
r: &[RefValue],
index_key_sort_order: IndexKeySortOrder,
) -> std::cmp::Ordering {
assert_eq!(l.len(), r.len());
for (i, (l, r)) in l.iter().zip(r).enumerate() {
let column_order = index_key_sort_order.get_sort_order_for_col(i);
let cmp = l.partial_cmp(r).unwrap();
if !cmp.is_eq() {
return match column_order {
SortOrder::Asc => cmp,
SortOrder::Desc => cmp.reverse(),
};
}
}
std::cmp::Ordering::Equal
}
const I8_LOW: i64 = -128;
@ -1027,7 +1131,11 @@ const I48_HIGH: i64 = 140737488355327;
/// Sqlite Serial Types
/// https://www.sqlite.org/fileformat.html#record_format
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum SerialType {
#[repr(transparent)]
pub struct SerialType(u64);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum SerialTypeKind {
Null,
I8,
I16,
@ -1036,47 +1144,154 @@ enum SerialType {
I48,
I64,
F64,
Text { content_size: usize },
Blob { content_size: usize },
ConstInt0,
ConstInt1,
Text,
Blob,
}
impl SerialType {
#[inline(always)]
pub fn u64_is_valid_serial_type(n: u64) -> bool {
n != 10 && n != 11
}
const NULL: Self = Self(0);
const I8: Self = Self(1);
const I16: Self = Self(2);
const I24: Self = Self(3);
const I32: Self = Self(4);
const I48: Self = Self(5);
const I64: Self = Self(6);
const F64: Self = Self(7);
const CONST_INT0: Self = Self(8);
const CONST_INT1: Self = Self(9);
pub fn null() -> Self {
Self::NULL
}
pub fn i8() -> Self {
Self::I8
}
pub fn i16() -> Self {
Self::I16
}
pub fn i24() -> Self {
Self::I24
}
pub fn i32() -> Self {
Self::I32
}
pub fn i48() -> Self {
Self::I48
}
pub fn i64() -> Self {
Self::I64
}
pub fn f64() -> Self {
Self::F64
}
pub fn const_int0() -> Self {
Self::CONST_INT0
}
pub fn const_int1() -> Self {
Self::CONST_INT1
}
pub fn blob(size: u64) -> Self {
Self(12 + size * 2)
}
pub fn text(size: u64) -> Self {
Self(13 + size * 2)
}
pub fn kind(&self) -> SerialTypeKind {
match self.0 {
0 => SerialTypeKind::Null,
1 => SerialTypeKind::I8,
2 => SerialTypeKind::I16,
3 => SerialTypeKind::I24,
4 => SerialTypeKind::I32,
5 => SerialTypeKind::I48,
6 => SerialTypeKind::I64,
7 => SerialTypeKind::F64,
8 => SerialTypeKind::ConstInt0,
9 => SerialTypeKind::ConstInt1,
n if n >= 12 => match n % 2 {
0 => SerialTypeKind::Blob,
1 => SerialTypeKind::Text,
_ => unreachable!(),
},
_ => unreachable!(),
}
}
pub fn size(&self) -> usize {
match self.kind() {
SerialTypeKind::Null => 0,
SerialTypeKind::I8 => 1,
SerialTypeKind::I16 => 2,
SerialTypeKind::I24 => 3,
SerialTypeKind::I32 => 4,
SerialTypeKind::I48 => 6,
SerialTypeKind::I64 => 8,
SerialTypeKind::F64 => 8,
SerialTypeKind::ConstInt0 => 0,
SerialTypeKind::ConstInt1 => 0,
SerialTypeKind::Text => (self.0 as usize - 13) / 2,
SerialTypeKind::Blob => (self.0 as usize - 12) / 2,
}
}
}
impl From<&OwnedValue> for SerialType {
fn from(value: &OwnedValue) -> Self {
match value {
OwnedValue::Null => SerialType::Null,
OwnedValue::Null => SerialType::null(),
OwnedValue::Integer(i) => match i {
i if *i >= I8_LOW && *i <= I8_HIGH => SerialType::I8,
i if *i >= I16_LOW && *i <= I16_HIGH => SerialType::I16,
i if *i >= I24_LOW && *i <= I24_HIGH => SerialType::I24,
i if *i >= I32_LOW && *i <= I32_HIGH => SerialType::I32,
i if *i >= I48_LOW && *i <= I48_HIGH => SerialType::I48,
_ => SerialType::I64,
},
OwnedValue::Float(_) => SerialType::F64,
OwnedValue::Text(t) => SerialType::Text {
content_size: t.value.len(),
},
OwnedValue::Blob(b) => SerialType::Blob {
content_size: b.len(),
0 => SerialType::const_int0(),
1 => SerialType::const_int1(),
i if *i >= I8_LOW && *i <= I8_HIGH => SerialType::i8(),
i if *i >= I16_LOW && *i <= I16_HIGH => SerialType::i16(),
i if *i >= I24_LOW && *i <= I24_HIGH => SerialType::i24(),
i if *i >= I32_LOW && *i <= I32_HIGH => SerialType::i32(),
i if *i >= I48_LOW && *i <= I48_HIGH => SerialType::i48(),
_ => SerialType::i64(),
},
OwnedValue::Float(_) => SerialType::f64(),
OwnedValue::Text(t) => SerialType::text(t.value.len() as u64),
OwnedValue::Blob(b) => SerialType::blob(b.len() as u64),
}
}
}
impl From<SerialType> for u64 {
fn from(serial_type: SerialType) -> Self {
match serial_type {
SerialType::Null => 0,
SerialType::I8 => 1,
SerialType::I16 => 2,
SerialType::I24 => 3,
SerialType::I32 => 4,
SerialType::I48 => 5,
SerialType::I64 => 6,
SerialType::F64 => 7,
SerialType::Text { content_size } => (content_size * 2 + 13) as u64,
SerialType::Blob { content_size } => (content_size * 2 + 12) as u64,
serial_type.0
}
}
impl TryFrom<u64> for SerialType {
type Error = LimboError;
fn try_from(uint: u64) -> Result<Self> {
if uint == 10 || uint == 11 {
return Err(LimboError::Corrupt(format!(
"Invalid serial type: {}",
uint
)));
}
Ok(SerialType(uint))
}
}
@ -1104,13 +1319,15 @@ impl Record {
OwnedValue::Null => {}
OwnedValue::Integer(i) => {
let serial_type = SerialType::from(value);
match serial_type {
SerialType::I8 => buf.extend_from_slice(&(*i as i8).to_be_bytes()),
SerialType::I16 => buf.extend_from_slice(&(*i as i16).to_be_bytes()),
SerialType::I24 => buf.extend_from_slice(&(*i as i32).to_be_bytes()[1..]), // remove most significant byte
SerialType::I32 => buf.extend_from_slice(&(*i as i32).to_be_bytes()),
SerialType::I48 => buf.extend_from_slice(&i.to_be_bytes()[2..]), // remove 2 most significant bytes
SerialType::I64 => buf.extend_from_slice(&i.to_be_bytes()),
match serial_type.kind() {
SerialTypeKind::I8 => buf.extend_from_slice(&(*i as i8).to_be_bytes()),
SerialTypeKind::I16 => buf.extend_from_slice(&(*i as i16).to_be_bytes()),
SerialTypeKind::I24 => {
buf.extend_from_slice(&(*i as i32).to_be_bytes()[1..])
} // remove most significant byte
SerialTypeKind::I32 => buf.extend_from_slice(&(*i as i32).to_be_bytes()),
SerialTypeKind::I48 => buf.extend_from_slice(&i.to_be_bytes()[2..]), // remove 2 most significant bytes
SerialTypeKind::I64 => buf.extend_from_slice(&i.to_be_bytes()),
_ => unreachable!(),
}
}
@ -1193,11 +1410,43 @@ pub enum CursorResult<T> {
IO,
}
#[derive(Clone, PartialEq, Debug)]
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
/// The match condition of a table/index seek.
pub enum SeekOp {
EQ,
GE,
GT,
LE,
LT,
}
impl SeekOp {
/// A given seek op implies an iteration direction.
///
/// For example, a seek with SeekOp::GT implies:
/// Find the first table/index key that compares greater than the seek key
/// -> used in forwards iteration.
///
/// A seek with SeekOp::LE implies:
/// Find the last table/index key that compares less than or equal to the seek key
/// -> used in backwards iteration.
#[inline(always)]
pub fn iteration_direction(&self) -> IterationDirection {
match self {
SeekOp::EQ | SeekOp::GE | SeekOp::GT => IterationDirection::Forwards,
SeekOp::LE | SeekOp::LT => IterationDirection::Backwards,
}
}
pub fn reverse(&self) -> Self {
match self {
SeekOp::EQ => SeekOp::EQ,
SeekOp::GE => SeekOp::LE,
SeekOp::GT => SeekOp::LT,
SeekOp::LE => SeekOp::GE,
SeekOp::LT => SeekOp::GT,
}
}
}
#[derive(Clone, PartialEq, Debug)]
@ -1234,7 +1483,7 @@ mod tests {
// First byte should be header size
assert_eq!(header[0], header_length as u8);
// Second byte should be serial type for NULL
assert_eq!(header[1] as u64, u64::from(SerialType::Null));
assert_eq!(header[1] as u64, u64::from(SerialType::null()));
// Check that the buffer is empty after the header
assert_eq!(buf.len(), header_length);
}
@ -1258,12 +1507,12 @@ mod tests {
assert_eq!(header[0], header_length as u8); // Header should be larger than number of values
// Check that correct serial types were chosen
assert_eq!(header[1] as u64, u64::from(SerialType::I8));
assert_eq!(header[2] as u64, u64::from(SerialType::I16));
assert_eq!(header[3] as u64, u64::from(SerialType::I24));
assert_eq!(header[4] as u64, u64::from(SerialType::I32));
assert_eq!(header[5] as u64, u64::from(SerialType::I48));
assert_eq!(header[6] as u64, u64::from(SerialType::I64));
assert_eq!(header[1] as u64, u64::from(SerialType::i8()));
assert_eq!(header[2] as u64, u64::from(SerialType::i16()));
assert_eq!(header[3] as u64, u64::from(SerialType::i24()));
assert_eq!(header[4] as u64, u64::from(SerialType::i32()));
assert_eq!(header[5] as u64, u64::from(SerialType::i48()));
assert_eq!(header[6] as u64, u64::from(SerialType::i64()));
// test that the bytes after the header can be interpreted as the correct values
let mut cur_offset = header_length;
@ -1326,7 +1575,7 @@ mod tests {
// First byte should be header size
assert_eq!(header[0], header_length as u8);
// Second byte should be serial type for FLOAT
assert_eq!(header[1] as u64, u64::from(SerialType::F64));
assert_eq!(header[1] as u64, u64::from(SerialType::f64()));
// Check that the bytes after the header can be interpreted as the float
let float_bytes = &buf[header_length..header_length + size_of::<f64>()];
let float = f64::from_be_bytes(float_bytes.try_into().unwrap());
@ -1390,11 +1639,11 @@ mod tests {
// First byte should be header size
assert_eq!(header[0], header_length as u8);
// Second byte should be serial type for NULL
assert_eq!(header[1] as u64, u64::from(SerialType::Null));
assert_eq!(header[1] as u64, u64::from(SerialType::null()));
// Third byte should be serial type for I8
assert_eq!(header[2] as u64, u64::from(SerialType::I8));
assert_eq!(header[2] as u64, u64::from(SerialType::i8()));
// Fourth byte should be serial type for F64
assert_eq!(header[3] as u64, u64::from(SerialType::F64));
assert_eq!(header[3] as u64, u64::from(SerialType::f64()));
// Fifth byte should be serial type for TEXT, which is (len * 2 + 13)
assert_eq!(header[4] as u64, (4 * 2 + 13) as u64);

View file

@ -2,6 +2,7 @@ use limbo_sqlite3_parser::ast::{self, CreateTableBody, Expr, FunctionTail, Liter
use std::{rc::Rc, sync::Arc};
use crate::{
function::Func,
schema::{self, Column, Schema, Type},
types::{OwnedValue, OwnedValueType},
LimboError, OpenFlags, Result, Statement, StepResult, SymbolTable, IO,
@ -36,6 +37,21 @@ pub fn normalize_ident(identifier: &str) -> String {
pub const PRIMARY_KEY_AUTOMATIC_INDEX_NAME_PREFIX: &str = "sqlite_autoindex_";
enum UnparsedIndex {
/// CREATE INDEX idx ON table_name(sql)
FromSql {
table_name: String,
root_page: usize,
sql: String,
},
/// Implicitly created index due to primary key constraints (or UNIQUE, but not implemented)
FromConstraint {
name: String,
table_name: String,
root_page: usize,
},
}
pub fn parse_schema_rows(
rows: Option<Statement>,
schema: &mut Schema,
@ -45,7 +61,7 @@ pub fn parse_schema_rows(
) -> Result<()> {
if let Some(mut rows) = rows {
rows.set_mv_tx_id(mv_tx_id);
let mut automatic_indexes = Vec::new();
let mut unparsed_indexes = Vec::with_capacity(10);
loop {
match rows.step()? {
StepResult::Row => {
@ -58,9 +74,37 @@ pub fn parse_schema_rows(
"table" => {
let root_page: i64 = row.get::<i64>(3)?;
let sql: &str = row.get::<&str>(4)?;
if root_page == 0 && sql.to_lowercase().contains("virtual") {
if root_page == 0 && sql.to_lowercase().contains("create virtual") {
let name: &str = row.get::<&str>(1)?;
let vtab = syms.vtabs.get(name).unwrap().clone();
// a virtual table is found in the sqlite_schema, but it's no
// longer in the in-memory schema. We need to recreate it if
// the module is loaded in the symbol table.
let vtab = if let Some(vtab) = syms.vtabs.get(name) {
vtab.clone()
} else {
let mod_name = module_name_from_sql(sql)?;
if let Some(vmod) = syms.vtab_modules.get(mod_name) {
if let limbo_ext::VTabKind::VirtualTable = vmod.module_kind
{
crate::VirtualTable::from_args(
Some(name),
mod_name,
module_args_from_sql(sql)?,
syms,
vmod.module_kind,
None,
)?
} else {
return Err(LimboError::Corrupt("Table valued function: {name} registered as virtual table in schema".to_string()));
}
} else {
// the extension isn't loaded, so we emit a warning.
return Err(LimboError::ExtensionError(format!(
"Virtual table module '{}' not found\nPlease load extension",
&mod_name
)));
}
};
schema.add_virtual_table(vtab);
} else {
let table = schema::BTreeTable::from_sql(sql, root_page as usize)?;
@ -71,21 +115,24 @@ pub fn parse_schema_rows(
let root_page: i64 = row.get::<i64>(3)?;
match row.get::<&str>(4) {
Ok(sql) => {
let index = schema::Index::from_sql(sql, root_page as usize)?;
schema.add_index(Arc::new(index));
unparsed_indexes.push(UnparsedIndex::FromSql {
table_name: row.get::<&str>(2)?.to_string(),
root_page: root_page as usize,
sql: sql.to_string(),
});
}
_ => {
// Automatic index on primary key, e.g.
// table|foo|foo|2|CREATE TABLE foo (a text PRIMARY KEY, b)
// index|sqlite_autoindex_foo_1|foo|3|
let index_name = row.get::<&str>(1)?;
let table_name = row.get::<&str>(2)?;
let index_name = row.get::<&str>(1)?.to_string();
let table_name = row.get::<&str>(2)?.to_string();
let root_page = row.get::<i64>(3)?;
automatic_indexes.push((
index_name.to_string(),
table_name.to_string(),
root_page,
));
unparsed_indexes.push(UnparsedIndex::FromConstraint {
name: index_name,
table_name,
root_page: root_page as usize,
});
}
}
}
@ -102,12 +149,31 @@ pub fn parse_schema_rows(
StepResult::Busy => break,
}
}
for (index_name, table_name, root_page) in automatic_indexes {
// We need to process these after all tables are loaded into memory due to the schema.get_table() call
let table = schema.get_btree_table(&table_name).unwrap();
let index =
schema::Index::automatic_from_primary_key(&table, &index_name, root_page as usize)?;
schema.add_index(Arc::new(index));
for unparsed_index in unparsed_indexes {
match unparsed_index {
UnparsedIndex::FromSql {
table_name,
root_page,
sql,
} => {
let table = schema.get_btree_table(&table_name).unwrap();
let index = schema::Index::from_sql(&sql, root_page as usize, table.as_ref())?;
schema.add_index(Arc::new(index));
}
UnparsedIndex::FromConstraint {
name,
table_name,
root_page,
} => {
let table = schema.get_btree_table(&table_name).unwrap();
let index = schema::Index::automatic_from_primary_key(
table.as_ref(),
&name,
root_page as usize,
)?;
schema.add_index(Arc::new(index));
}
}
}
}
Ok(())
@ -132,6 +198,99 @@ pub fn check_ident_equivalency(ident1: &str, ident2: &str) -> bool {
strip_quotes(ident1).eq_ignore_ascii_case(strip_quotes(ident2))
}
fn module_name_from_sql(sql: &str) -> Result<&str> {
if let Some(start) = sql.find("USING") {
let start = start + 6;
// stop at the first space, semicolon, or parenthesis
let end = sql[start..]
.find(|c: char| c.is_whitespace() || c == ';' || c == '(')
.unwrap_or(sql.len() - start)
+ start;
Ok(sql[start..end].trim())
} else {
Err(LimboError::InvalidArgument(
"Expected 'USING' in module name".to_string(),
))
}
}
// CREATE VIRTUAL TABLE table_name USING module_name(arg1, arg2, ...);
// CREATE VIRTUAL TABLE table_name USING module_name;
fn module_args_from_sql(sql: &str) -> Result<Vec<limbo_ext::Value>> {
if !sql.contains('(') {
return Ok(vec![]);
}
let start = sql.find('(').ok_or_else(|| {
LimboError::InvalidArgument("Expected '(' in module argument list".to_string())
})? + 1;
let end = sql.rfind(')').ok_or_else(|| {
LimboError::InvalidArgument("Expected ')' in module argument list".to_string())
})?;
let mut args = Vec::new();
let mut current_arg = String::new();
let mut chars = sql[start..end].chars().peekable();
let mut in_quotes = false;
while let Some(c) = chars.next() {
match c {
'\'' => {
if in_quotes {
if chars.peek() == Some(&'\'') {
// Escaped quote
current_arg.push('\'');
chars.next();
} else {
in_quotes = false;
args.push(limbo_ext::Value::from_text(current_arg.trim().to_string()));
current_arg.clear();
// Skip until comma or end
while let Some(&nc) = chars.peek() {
if nc == ',' {
chars.next(); // Consume comma
break;
} else if nc.is_whitespace() {
chars.next();
} else {
return Err(LimboError::InvalidArgument(
"Unexpected characters after quoted argument".to_string(),
));
}
}
}
} else {
in_quotes = true;
}
}
',' => {
if !in_quotes {
if !current_arg.trim().is_empty() {
args.push(limbo_ext::Value::from_text(current_arg.trim().to_string()));
current_arg.clear();
}
} else {
current_arg.push(c);
}
}
_ => {
current_arg.push(c);
}
}
}
if !current_arg.trim().is_empty() && !in_quotes {
args.push(limbo_ext::Value::from_text(current_arg.trim().to_string()));
}
if in_quotes {
return Err(LimboError::InvalidArgument(
"Unterminated string literal in module arguments".to_string(),
));
}
Ok(args)
}
pub fn check_literal_equivalency(lhs: &Literal, rhs: &Literal) -> bool {
match (lhs, rhs) {
(Literal::Numeric(n1), Literal::Numeric(n2)) => cmp_numeric_strings(n1, n2),
@ -278,7 +437,11 @@ pub fn exprs_are_equivalent(expr1: &Expr, expr2: &Expr) -> bool {
(Expr::Unary(op1, expr1), Expr::Unary(op2, expr2)) => {
op1 == op2 && exprs_are_equivalent(expr1, expr2)
}
(Expr::Variable(var1), Expr::Variable(var2)) => var1 == var2,
// Variables that are not bound to a specific value, are treated as NULL
// https://sqlite.org/lang_expr.html#varparam
(Expr::Variable(var), Expr::Variable(var2)) if var == "" && var2 == "" => false,
// Named variables can be compared by their name
(Expr::Variable(val), Expr::Variable(val2)) => val == val2,
(Expr::Parenthesized(exprs1), Expr::Parenthesized(exprs2)) => {
exprs1.len() == exprs2.len()
&& exprs1
@ -403,6 +566,39 @@ pub fn columns_from_create_table_body(body: &ast::CreateTableBody) -> crate::Res
.collect::<Vec<_>>())
}
/// This function checks if a given expression is a constant value that can be pushed down to the database engine.
/// It is expected to be called with the other half of a binary expression with an Expr::Column
pub fn can_pushdown_predicate(expr: &Expr, table_idx: usize) -> bool {
match expr {
Expr::Literal(_) => true,
Expr::Column { table, .. } => *table <= table_idx,
Expr::Binary(lhs, _, rhs) => {
can_pushdown_predicate(lhs, table_idx) && can_pushdown_predicate(rhs, table_idx)
}
Expr::Parenthesized(exprs) => can_pushdown_predicate(exprs.first().unwrap(), table_idx),
Expr::Unary(_, expr) => can_pushdown_predicate(expr, table_idx),
Expr::FunctionCall { args, name, .. } => {
let function = crate::function::Func::resolve_function(
&name.0,
args.as_ref().map_or(0, |a| a.len()),
);
// is deterministic
matches!(function, Ok(Func::Scalar(_)))
}
Expr::Like { lhs, rhs, .. } => {
can_pushdown_predicate(lhs, table_idx) && can_pushdown_predicate(rhs, table_idx)
}
Expr::Between {
lhs, start, end, ..
} => {
can_pushdown_predicate(lhs, table_idx)
&& can_pushdown_predicate(start, table_idx)
&& can_pushdown_predicate(end, table_idx)
}
_ => false,
}
}
#[derive(Debug, Default, PartialEq)]
pub struct OpenOptions<'a> {
/// The authority component of the URI. may be 'localhost' or empty
@ -716,11 +912,10 @@ fn parse_numeric_str(text: &str) -> Result<(OwnedValueType, &str), ()> {
let text = text.trim();
let bytes = text.as_bytes();
if bytes.is_empty()
|| bytes[0] == b'e'
|| bytes[0] == b'E'
|| (bytes[0] == b'.' && (bytes[1] == b'e' || bytes[1] == b'E'))
{
if matches!(
bytes,
[] | [b'e', ..] | [b'E', ..] | [b'.', b'e' | b'E', ..]
) {
return Err(());
}
@ -824,6 +1019,24 @@ pub mod tests {
assert_eq!(normalize_ident("\"foo\""), "foo");
}
#[test]
fn test_anonymous_variable_comparison() {
let expr1 = Expr::Variable("".to_string());
let expr2 = Expr::Variable("".to_string());
assert!(!exprs_are_equivalent(&expr1, &expr2));
}
#[test]
fn test_named_variable_comparison() {
let expr1 = Expr::Variable("1".to_string());
let expr2 = Expr::Variable("1".to_string());
assert!(exprs_are_equivalent(&expr1, &expr2));
let expr1 = Expr::Variable("1".to_string());
let expr2 = Expr::Variable("2".to_string());
assert!(!exprs_are_equivalent(&expr1, &expr2));
}
#[test]
fn test_basic_addition_exprs_are_equivalent() {
let expr1 = Expr::Binary(
@ -1632,4 +1845,88 @@ pub mod tests {
Ok((OwnedValueType::Float, "1.23e4"))
);
}
#[test]
fn test_module_name_basic() {
let sql = "CREATE VIRTUAL TABLE x USING y;";
assert_eq!(module_name_from_sql(sql).unwrap(), "y");
}
#[test]
fn test_module_name_with_args() {
let sql = "CREATE VIRTUAL TABLE x USING modname('a', 'b');";
assert_eq!(module_name_from_sql(sql).unwrap(), "modname");
}
#[test]
fn test_module_name_missing_using() {
let sql = "CREATE VIRTUAL TABLE x (a, b);";
assert!(module_name_from_sql(sql).is_err());
}
#[test]
fn test_module_name_no_semicolon() {
let sql = "CREATE VIRTUAL TABLE x USING limbo(a, b)";
assert_eq!(module_name_from_sql(sql).unwrap(), "limbo");
}
#[test]
fn test_module_name_no_semicolon_or_args() {
let sql = "CREATE VIRTUAL TABLE x USING limbo";
assert_eq!(module_name_from_sql(sql).unwrap(), "limbo");
}
#[test]
fn test_module_args_none() {
let sql = "CREATE VIRTUAL TABLE x USING modname;";
let args = module_args_from_sql(sql).unwrap();
assert_eq!(args.len(), 0);
}
#[test]
fn test_module_args_basic() {
let sql = "CREATE VIRTUAL TABLE x USING modname('arg1', 'arg2');";
let args = module_args_from_sql(sql).unwrap();
assert_eq!(args.len(), 2);
assert_eq!("arg1", args[0].to_text().unwrap());
assert_eq!("arg2", args[1].to_text().unwrap());
for arg in args {
unsafe { arg.__free_internal_type() }
}
}
#[test]
fn test_module_args_with_escaped_quote() {
let sql = "CREATE VIRTUAL TABLE x USING modname('a''b', 'c');";
let args = module_args_from_sql(sql).unwrap();
assert_eq!(args.len(), 2);
assert_eq!(args[0].to_text().unwrap(), "a'b");
assert_eq!(args[1].to_text().unwrap(), "c");
for arg in args {
unsafe { arg.__free_internal_type() }
}
}
#[test]
fn test_module_args_unterminated_string() {
let sql = "CREATE VIRTUAL TABLE x USING modname('arg1, 'arg2');";
assert!(module_args_from_sql(sql).is_err());
}
#[test]
fn test_module_args_extra_garbage_after_quote() {
let sql = "CREATE VIRTUAL TABLE x USING modname('arg1'x);";
assert!(module_args_from_sql(sql).is_err());
}
#[test]
fn test_module_args_trailing_comma() {
let sql = "CREATE VIRTUAL TABLE x USING modname('arg1',);";
let args = module_args_from_sql(sql).unwrap();
assert_eq!(args.len(), 1);
assert_eq!("arg1", args[0].to_text().unwrap());
for arg in args {
unsafe { arg.__free_internal_type() }
}
}
}

View file

@ -1,6 +1,6 @@
use std::{
cell::Cell,
collections::HashMap,
cmp::Ordering,
rc::{Rc, Weak},
sync::Arc,
};
@ -16,24 +16,25 @@ use crate::{
Connection, VirtualTable,
};
use super::{BranchOffset, CursorID, Insn, InsnFunction, InsnReference, Program};
use super::{BranchOffset, CursorID, Insn, InsnFunction, InsnReference, JumpTarget, Program};
#[allow(dead_code)]
pub struct ProgramBuilder {
next_free_register: usize,
next_free_cursor_id: usize,
insns: Vec<(Insn, InsnFunction)>,
// for temporarily storing instructions that will be put after Transaction opcode
constant_insns: Vec<(Insn, InsnFunction)>,
// Vector of labels which must be assigned to next emitted instruction
next_insn_labels: Vec<BranchOffset>,
/// Instruction, the function to execute it with, and its original index in the vector.
insns: Vec<(Insn, InsnFunction, usize)>,
/// A span of instructions from (offset_start_inclusive, offset_end_exclusive),
/// that are deemed to be compile-time constant and can be hoisted out of loops
/// so that they get evaluated only once at the start of the program.
pub constant_spans: Vec<(usize, usize)>,
// Cursors that are referenced by the program. Indexed by CursorID.
pub cursor_ref: Vec<(Option<String>, CursorType)>,
/// A vector where index=label number, value=resolved offset. Resolved in build().
label_to_resolved_offset: Vec<Option<InsnReference>>,
label_to_resolved_offset: Vec<Option<(InsnReference, JumpTarget)>>,
// Bitmask of cursors that have emitted a SeekRowid instruction.
seekrowid_emitted_bitmask: u64,
// map of instruction index to manual comment (used in EXPLAIN only)
comments: Option<HashMap<InsnReference, &'static str>>,
comments: Option<Vec<(InsnReference, &'static str)>>,
pub parameters: Parameters,
pub result_columns: Vec<ResultSetColumn>,
pub table_references: Vec<TableReference>,
@ -82,13 +83,12 @@ impl ProgramBuilder {
next_free_register: 1,
next_free_cursor_id: 0,
insns: Vec::with_capacity(opts.approx_num_insns),
next_insn_labels: Vec::with_capacity(2),
cursor_ref: Vec::with_capacity(opts.num_cursors),
constant_insns: Vec::new(),
constant_spans: Vec::new(),
label_to_resolved_offset: Vec::with_capacity(opts.approx_num_labels),
seekrowid_emitted_bitmask: 0,
comments: if opts.query_mode == QueryMode::Explain {
Some(HashMap::new())
Some(Vec::new())
} else {
None
},
@ -98,6 +98,56 @@ impl ProgramBuilder {
}
}
/// Start a new constant span. The next instruction to be emitted will be the first
/// instruction in the span.
pub fn constant_span_start(&mut self) -> usize {
let span = self.constant_spans.len();
let start = self.insns.len();
self.constant_spans.push((start, usize::MAX));
span
}
/// End the current constant span. The last instruction that was emitted is the last
/// instruction in the span.
pub fn constant_span_end(&mut self, span_idx: usize) {
let span = &mut self.constant_spans[span_idx];
if span.1 == usize::MAX {
span.1 = self.insns.len().saturating_sub(1);
}
}
/// End all constant spans that are currently open. This is used to handle edge cases
/// where we think a parent expression is constant, but we decide during the evaluation
/// of one of its children that it is not.
pub fn constant_span_end_all(&mut self) {
for span in self.constant_spans.iter_mut() {
if span.1 == usize::MAX {
span.1 = self.insns.len().saturating_sub(1);
}
}
}
/// Check if there is a constant span that is currently open.
pub fn constant_span_is_open(&self) -> bool {
self.constant_spans
.last()
.map_or(false, |(_, end)| *end == usize::MAX)
}
/// Get the index of the next constant span.
/// Used in [crate::translate::expr::translate_expr_no_constant_opt()] to invalidate
/// all constant spans after the given index.
pub fn constant_spans_next_idx(&self) -> usize {
self.constant_spans.len()
}
/// Invalidate all constant spans after the given index. This is used when we want to
/// be sure that constant optimization is never used for translating a given expression.
/// See [crate::translate::expr::translate_expr_no_constant_opt()] for more details.
pub fn constant_spans_invalidate_after(&mut self, idx: usize) {
self.constant_spans.truncate(idx);
}
pub fn alloc_register(&mut self) -> usize {
let reg = self.next_free_register;
self.next_free_register += 1;
@ -123,12 +173,14 @@ impl ProgramBuilder {
}
pub fn emit_insn(&mut self, insn: Insn) {
for label in self.next_insn_labels.drain(..) {
self.label_to_resolved_offset[label.to_label_value() as usize] =
Some(self.insns.len() as InsnReference);
}
let function = insn.to_function();
self.insns.push((insn, function));
self.insns.push((insn, function, self.insns.len()));
}
pub fn close_cursors(&mut self, cursors: &[CursorID]) {
for cursor in cursors {
self.emit_insn(Insn::Close { cursor_id: *cursor });
}
}
pub fn emit_string8(&mut self, value: String, dest: usize) {
@ -194,20 +246,69 @@ impl ProgramBuilder {
pub fn add_comment(&mut self, insn_index: BranchOffset, comment: &'static str) {
if let Some(comments) = &mut self.comments {
comments.insert(insn_index.to_offset_int(), comment);
comments.push((insn_index.to_offset_int(), comment));
}
}
// Emit an instruction that will be put at the end of the program (after Transaction statement).
// This is useful for instructions that otherwise will be unnecessarily repeated in a loop.
// Example: In `SELECT * from users where name='John'`, it is unnecessary to set r[1]='John' as we SCAN users table.
// We could simply set it once before the SCAN started.
pub fn mark_last_insn_constant(&mut self) {
self.constant_insns.push(self.insns.pop().unwrap());
if self.constant_span_is_open() {
// no need to mark this insn as constant as the surrounding parent expression is already constant
return;
}
let prev = self.insns.len().saturating_sub(1);
self.constant_spans.push((prev, prev));
}
pub fn emit_constant_insns(&mut self) {
self.insns.append(&mut self.constant_insns);
// move compile-time constant instructions to the end of the program, where they are executed once after Init jumps to it.
// any label_to_resolved_offset that points to an instruction within any moved constant span should be updated to point to the new location.
// the instruction reordering can be done by sorting the insns, so that the ordering is:
// 1. if insn not in any constant span, it stays where it is
// 2. if insn is in a constant span, it is after other insns, except those that are in a later constant span
// 3. within a single constant span the order is preserver
self.insns.sort_by(|(_, _, index_a), (_, _, index_b)| {
let a_span = self
.constant_spans
.iter()
.find(|span| span.0 <= *index_a && span.1 >= *index_a);
let b_span = self
.constant_spans
.iter()
.find(|span| span.0 <= *index_b && span.1 >= *index_b);
if a_span.is_some() && b_span.is_some() {
a_span.unwrap().0.cmp(&b_span.unwrap().0)
} else if a_span.is_some() {
Ordering::Greater
} else if b_span.is_some() {
Ordering::Less
} else {
Ordering::Equal
}
});
for resolved_offset in self.label_to_resolved_offset.iter_mut() {
if let Some((old_offset, target)) = resolved_offset {
let new_offset = self
.insns
.iter()
.position(|(_, _, index)| *old_offset == *index as u32)
.unwrap() as u32;
*resolved_offset = Some((new_offset, *target));
}
}
// Fix comments to refer to new locations
if let Some(comments) = &mut self.comments {
for (old_offset, _) in comments.iter_mut() {
let new_offset = self
.insns
.iter()
.position(|(_, _, index)| *old_offset == *index as u32)
.expect("comment must exist") as u32;
*old_offset = new_offset;
}
}
}
pub fn offset(&self) -> BranchOffset {
@ -220,18 +321,42 @@ impl ProgramBuilder {
BranchOffset::Label(label_n as u32)
}
// Effectively a GOTO <next insn> without the need to emit an explicit GOTO instruction.
// Useful when you know you need to jump to "the next part", but the exact offset is unknowable
// at the time of emitting the instruction.
/// Resolve a label to whatever instruction follows the one that was
/// last emitted.
///
/// Use this when your use case is: "the program should jump to whatever instruction
/// follows the one that was previously emitted", and you don't care exactly
/// which instruction that is. Examples include "the start of a loop", or
/// "after the loop ends".
///
/// It is important to handle those cases this way, because the precise
/// instruction that follows any given instruction might change due to
/// reordering the emitted instructions.
#[inline]
pub fn preassign_label_to_next_insn(&mut self, label: BranchOffset) {
self.next_insn_labels.push(label);
assert!(label.is_label(), "BranchOffset {:?} is not a label", label);
self._resolve_label(label, self.offset().sub(1u32), JumpTarget::AfterThisInsn);
}
/// Resolve a label to exactly the instruction that was last emitted.
///
/// Use this when your use case is: "the program should jump to the exact instruction
/// that was last emitted", and you don't care WHERE exactly that ends up being
/// once the order of the bytecode of the program is finalized. Examples include
/// "jump to the Halt instruction", or "jump to the Next instruction of a loop".
#[inline]
pub fn resolve_label(&mut self, label: BranchOffset, to_offset: BranchOffset) {
self._resolve_label(label, to_offset, JumpTarget::ExactlyThisInsn);
}
fn _resolve_label(&mut self, label: BranchOffset, to_offset: BranchOffset, target: JumpTarget) {
assert!(matches!(label, BranchOffset::Label(_)));
assert!(matches!(to_offset, BranchOffset::Offset(_)));
self.label_to_resolved_offset[label.to_label_value() as usize] =
Some(to_offset.to_offset_int());
let BranchOffset::Label(label_number) = label else {
unreachable!("Label is not a label");
};
self.label_to_resolved_offset[label_number as usize] =
Some((to_offset.to_offset_int(), target));
}
/// Resolve unresolved labels to a specific offset in the instruction list.
@ -242,19 +367,25 @@ impl ProgramBuilder {
pub fn resolve_labels(&mut self) {
let resolve = |pc: &mut BranchOffset, insn_name: &str| {
if let BranchOffset::Label(label) = pc {
let to_offset = self
.label_to_resolved_offset
.get(*label as usize)
.unwrap_or_else(|| {
panic!("Reference to undefined label in {}: {}", insn_name, label)
});
let Some(Some((to_offset, target))) =
self.label_to_resolved_offset.get(*label as usize)
else {
panic!(
"Reference to undefined or unresolved label in {}: {}",
insn_name, label
);
};
*pc = BranchOffset::Offset(
to_offset
.unwrap_or_else(|| panic!("Unresolved label in {}: {}", insn_name, label)),
+ if *target == JumpTarget::ExactlyThisInsn {
0
} else {
1
},
);
}
};
for (insn, _) in self.insns.iter_mut() {
for (insn, _, _) in self.insns.iter_mut() {
match insn {
Insn::Init { target_pc } => {
resolve(target_pc, "Init");
@ -321,17 +452,11 @@ impl ProgramBuilder {
} => {
resolve(target_pc, "IfNot");
}
Insn::RewindAwait {
cursor_id: _cursor_id,
pc_if_empty,
} => {
resolve(pc_if_empty, "RewindAwait");
Insn::Rewind { pc_if_empty, .. } => {
resolve(pc_if_empty, "Rewind");
}
Insn::LastAwait {
cursor_id: _cursor_id,
pc_if_empty,
} => {
resolve(pc_if_empty, "LastAwait");
Insn::Last { pc_if_empty, .. } => {
resolve(pc_if_empty, "Last");
}
Insn::Goto { target_pc } => {
resolve(target_pc, "Goto");
@ -360,18 +485,25 @@ impl ProgramBuilder {
Insn::IfPos { target_pc, .. } => {
resolve(target_pc, "IfPos");
}
Insn::NextAwait { pc_if_next, .. } => {
resolve(pc_if_next, "NextAwait");
Insn::Next { pc_if_next, .. } => {
resolve(pc_if_next, "Next");
}
Insn::PrevAwait { pc_if_next, .. } => {
resolve(pc_if_next, "PrevAwait");
Insn::Once {
target_pc_when_reentered,
..
} => {
resolve(target_pc_when_reentered, "Once");
}
Insn::Prev { pc_if_prev, .. } => {
resolve(pc_if_prev, "Prev");
}
Insn::InitCoroutine {
yield_reg: _,
jump_on_definition,
start_offset: _,
start_offset,
} => {
resolve(jump_on_definition, "InitCoroutine");
resolve(start_offset, "InitCoroutine");
}
Insn::NotExists {
cursor: _,
@ -407,6 +539,12 @@ impl ProgramBuilder {
Insn::SeekGT { target_pc, .. } => {
resolve(target_pc, "SeekGT");
}
Insn::SeekLE { target_pc, .. } => {
resolve(target_pc, "SeekLE");
}
Insn::SeekLT { target_pc, .. } => {
resolve(target_pc, "SeekLT");
}
Insn::IdxGE { target_pc, .. } => {
resolve(target_pc, "IdxGE");
}
@ -428,6 +566,9 @@ impl ProgramBuilder {
Insn::VFilter { pc_if_empty, .. } => {
resolve(pc_if_empty, "VFilter");
}
Insn::NoConflict { target_pc, .. } => {
resolve(target_pc, "NoConflict");
}
_ => {}
}
}
@ -435,15 +576,17 @@ impl ProgramBuilder {
}
// translate table to cursor id
pub fn resolve_cursor_id_safe(&self, table_identifier: &str) -> Option<CursorID> {
self.cursor_ref.iter().position(|(t_ident, _)| {
t_ident
.as_ref()
.is_some_and(|ident| ident == table_identifier)
})
}
pub fn resolve_cursor_id(&self, table_identifier: &str) -> CursorID {
self.cursor_ref
.iter()
.position(|(t_ident, _)| {
t_ident
.as_ref()
.is_some_and(|ident| ident == table_identifier)
})
.unwrap()
self.resolve_cursor_id_safe(table_identifier)
.unwrap_or_else(|| panic!("Cursor not found: {}", table_identifier))
}
pub fn build(
@ -453,15 +596,15 @@ impl ProgramBuilder {
change_cnt_on: bool,
) -> Program {
self.resolve_labels();
assert!(
self.constant_insns.is_empty(),
"constant_insns is not empty when build() is called, did you forget to call emit_constant_insns()?"
);
self.parameters.list.dedup();
Program {
max_registers: self.next_free_register,
insns: self.insns,
insns: self
.insns
.into_iter()
.map(|(insn, function, _)| (insn, function))
.collect(),
cursor_ref: self.cursor_ref,
database_header,
comments: self.comments,

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,6 @@
use crate::vdbe::builder::CursorType;
use limbo_sqlite3_parser::ast::SortOrder;
use crate::vdbe::{builder::CursorType, insn::RegisterOrLiteral};
use super::{Insn, InsnReference, OwnedValue, Program};
use crate::function::{Func, ScalarFunc};
@ -336,11 +338,11 @@ pub fn insn_to_str(
0,
format!("if !r[{}] goto {}", reg, target_pc.to_debug_int()),
),
Insn::OpenReadAsync {
Insn::OpenRead {
cursor_id,
root_page,
} => (
"OpenReadAsync",
"OpenRead",
*cursor_id as i32,
*root_page as i32,
0,
@ -355,17 +357,8 @@ pub fn insn_to_str(
root_page
),
),
Insn::OpenReadAwait => (
"OpenReadAwait",
0,
0,
0,
OwnedValue::build_text(""),
0,
"".to_string(),
),
Insn::VOpenAsync { cursor_id } => (
"VOpenAsync",
Insn::VOpen { cursor_id } => (
"VOpen",
*cursor_id as i32,
0,
0,
@ -373,15 +366,6 @@ pub fn insn_to_str(
0,
"".to_string(),
),
Insn::VOpenAwait => (
"VOpenAwait",
0,
0,
0,
OwnedValue::build_text(""),
0,
"".to_string(),
),
Insn::VCreate {
table_name,
module_name,
@ -449,6 +433,15 @@ pub fn insn_to_str(
0,
"".to_string(),
),
Insn::VDestroy { db, table_name } => (
"VDestroy",
*db as i32,
0,
0,
OwnedValue::build_text(table_name),
0,
"".to_string(),
),
Insn::OpenPseudo {
cursor_id,
content_reg,
@ -462,27 +455,18 @@ pub fn insn_to_str(
0,
format!("{} columns in r[{}]", num_fields, content_reg),
),
Insn::RewindAsync { cursor_id } => (
"RewindAsync",
*cursor_id as i32,
0,
0,
OwnedValue::build_text(""),
0,
"".to_string(),
),
Insn::RewindAwait {
Insn::Rewind {
cursor_id,
pc_if_empty,
} => (
"RewindAwait",
"Rewind",
*cursor_id as i32,
pc_if_empty.to_debug_int(),
0,
OwnedValue::build_text(""),
0,
format!(
"Rewind table {}",
"Rewind {}",
program.cursor_ref[*cursor_id]
.0
.as_ref()
@ -528,6 +512,20 @@ pub fn insn_to_str(
),
)
}
Insn::TypeCheck {
start_reg,
count,
check_generated,
..
} => (
"TypeCheck",
*start_reg as i32,
*count as i32,
*check_generated as i32,
OwnedValue::build_text(""),
0,
String::from(""),
),
Insn::MakeRecord {
start_reg,
count,
@ -559,20 +557,11 @@ pub fn insn_to_str(
format!("output=r[{}..{}]", start_reg, start_reg + count - 1)
},
),
Insn::NextAsync { cursor_id } => (
"NextAsync",
*cursor_id as i32,
0,
0,
OwnedValue::build_text(""),
0,
"".to_string(),
),
Insn::NextAwait {
Insn::Next {
cursor_id,
pc_if_next,
} => (
"NextAwait",
"Next",
*cursor_id as i32,
pc_if_next.to_debug_int(),
0,
@ -582,13 +571,13 @@ pub fn insn_to_str(
),
Insn::Halt {
err_code,
description: _,
description,
} => (
"Halt",
*err_code as i32,
0,
0,
OwnedValue::build_text(""),
OwnedValue::build_text(&description),
0,
"".to_string(),
),
@ -697,6 +686,22 @@ pub fn insn_to_str(
.unwrap_or(&format!("cursor {}", cursor_id))
),
),
Insn::IdxRowId { cursor_id, dest } => (
"IdxRowId",
*cursor_id as i32,
*dest as i32,
0,
OwnedValue::build_text(""),
0,
format!(
"r[{}]={}.rowid",
dest,
&program.cursor_ref[*cursor_id]
.0
.as_ref()
.unwrap_or(&format!("cursor {}", cursor_id))
),
),
Insn::SeekRowid {
cursor_id,
src_reg,
@ -734,87 +739,105 @@ pub fn insn_to_str(
is_index: _,
cursor_id,
start_reg,
num_regs: _,
num_regs,
target_pc,
}
| Insn::SeekGE {
is_index: _,
cursor_id,
start_reg,
num_regs,
target_pc,
}
| Insn::SeekLE {
is_index: _,
cursor_id,
start_reg,
num_regs,
target_pc,
}
| Insn::SeekLT {
is_index: _,
cursor_id,
start_reg,
num_regs,
target_pc,
} => (
"SeekGT",
match insn {
Insn::SeekGT { .. } => "SeekGT",
Insn::SeekGE { .. } => "SeekGE",
Insn::SeekLE { .. } => "SeekLE",
Insn::SeekLT { .. } => "SeekLT",
_ => unreachable!(),
},
*cursor_id as i32,
target_pc.to_debug_int(),
*start_reg as i32,
OwnedValue::build_text(""),
0,
format!("key=[{}..{}]", start_reg, start_reg + num_regs - 1),
),
Insn::SeekEnd { cursor_id } => (
"SeekEnd",
*cursor_id as i32,
0,
0,
OwnedValue::build_text(""),
0,
"".to_string(),
),
Insn::SeekGE {
is_index: _,
Insn::IdxInsert {
cursor_id,
start_reg,
num_regs: _,
target_pc,
record_reg,
unpacked_start,
flags,
..
} => (
"SeekGE",
"IdxInsert",
*cursor_id as i32,
target_pc.to_debug_int(),
*start_reg as i32,
*record_reg as i32,
unpacked_start.unwrap_or(0) as i32,
OwnedValue::build_text(""),
0,
"".to_string(),
flags.0 as u16,
format!("key=r[{}]", record_reg),
),
Insn::IdxGT {
cursor_id,
start_reg,
num_regs: _,
num_regs,
target_pc,
} => (
"IdxGT",
*cursor_id as i32,
target_pc.to_debug_int(),
*start_reg as i32,
OwnedValue::build_text(""),
0,
"".to_string(),
),
Insn::IdxGE {
}
| Insn::IdxGE {
cursor_id,
start_reg,
num_regs: _,
num_regs,
target_pc,
} => (
"IdxGE",
*cursor_id as i32,
target_pc.to_debug_int(),
*start_reg as i32,
OwnedValue::build_text(""),
0,
"".to_string(),
),
Insn::IdxLT {
}
| Insn::IdxLE {
cursor_id,
start_reg,
num_regs: _,
num_regs,
target_pc,
} => (
"IdxLT",
*cursor_id as i32,
target_pc.to_debug_int(),
*start_reg as i32,
OwnedValue::build_text(""),
0,
"".to_string(),
),
Insn::IdxLE {
}
| Insn::IdxLT {
cursor_id,
start_reg,
num_regs: _,
num_regs,
target_pc,
} => (
"IdxLE",
match insn {
Insn::IdxGT { .. } => "IdxGT",
Insn::IdxGE { .. } => "IdxGE",
Insn::IdxLE { .. } => "IdxLE",
Insn::IdxLT { .. } => "IdxLT",
_ => unreachable!(),
},
*cursor_id as i32,
target_pc.to_debug_int(),
*start_reg as i32,
OwnedValue::build_text(""),
0,
"".to_string(),
format!("key=[{}..{}]", start_reg, start_reg + num_regs - 1),
),
Insn::DecrJumpZero { reg, target_pc } => (
"DecrJumpZero",
@ -855,17 +878,10 @@ pub fn insn_to_str(
} => {
let _p4 = String::new();
let to_print: Vec<String> = order
.get_values()
.iter()
.map(|v| match v {
OwnedValue::Integer(i) => {
if *i == 0 {
"B".to_string()
} else {
"-B".to_string()
}
}
_ => unreachable!(),
SortOrder::Asc => "B".to_string(),
SortOrder::Desc => "-B".to_string(),
})
.collect();
(
@ -993,13 +1009,13 @@ pub fn insn_to_str(
0,
"".to_string(),
),
Insn::InsertAsync {
Insn::Insert {
cursor,
key_reg,
record_reg,
flag,
} => (
"InsertAsync",
"Insert",
*cursor as i32,
*record_reg as i32,
*key_reg as i32,
@ -1007,8 +1023,8 @@ pub fn insn_to_str(
*flag as u16,
"".to_string(),
),
Insn::InsertAwait { cursor_id } => (
"InsertAwait",
Insn::Delete { cursor_id } => (
"Delete",
*cursor_id as i32,
0,
0,
@ -1016,20 +1032,15 @@ pub fn insn_to_str(
0,
"".to_string(),
),
Insn::DeleteAsync { cursor_id } => (
"DeleteAsync",
Insn::IdxDelete {
cursor_id,
start_reg,
num_regs,
} => (
"IdxDelete",
*cursor_id as i32,
0,
0,
OwnedValue::build_text(""),
0,
"".to_string(),
),
Insn::DeleteAwait { cursor_id } => (
"DeleteAwait",
*cursor_id as i32,
0,
0,
*start_reg as i32,
*num_regs as i32,
OwnedValue::build_text(""),
0,
"".to_string(),
@ -1065,6 +1076,20 @@ pub fn insn_to_str(
0,
"".to_string(),
),
Insn::NoConflict {
cursor_id,
target_pc,
record_reg,
num_regs,
} => (
"NoConflict",
*cursor_id as i32,
target_pc.to_debug_int(),
*record_reg as i32,
OwnedValue::build_text(&format!("{num_regs}")),
0,
format!("key=r[{}]", record_reg),
),
Insn::NotExists {
cursor,
rowid_reg,
@ -1094,22 +1119,17 @@ pub fn insn_to_str(
limit_reg, combined_reg, limit_reg, offset_reg, combined_reg
),
),
Insn::OpenWriteAsync {
Insn::OpenWrite {
cursor_id,
root_page,
..
} => (
"OpenWriteAsync",
"OpenWrite",
*cursor_id as i32,
*root_page as i32,
0,
OwnedValue::build_text(""),
0,
"".to_string(),
),
Insn::OpenWriteAwait {} => (
"OpenWriteAwait",
0,
0,
match root_page {
RegisterOrLiteral::Literal(i) => *i as _,
RegisterOrLiteral::Register(i) => *i as _,
},
0,
OwnedValue::build_text(""),
0,
@ -1132,10 +1152,10 @@ pub fn insn_to_str(
"CreateBtree",
*db as i32,
*root as i32,
*flags as i32,
flags.get_flags() as i32,
OwnedValue::build_text(""),
0,
format!("r[{}]=root iDb={} flags={}", root, db, flags),
format!("r[{}]=root iDb={} flags={}", root, db, flags.get_flags()),
),
Insn::Destroy {
root,
@ -1176,10 +1196,13 @@ pub fn insn_to_str(
0,
"".to_string(),
),
Insn::LastAsync { .. } => (
"LastAsync",
0,
0,
Insn::Last {
cursor_id,
pc_if_empty,
} => (
"Last",
*cursor_id as i32,
pc_if_empty.to_debug_int(),
0,
OwnedValue::build_text(""),
0,
@ -1203,28 +1226,13 @@ pub fn insn_to_str(
0,
where_clause.clone(),
),
Insn::LastAwait { .. } => (
"LastAwait",
0,
0,
0,
OwnedValue::build_text(""),
0,
"".to_string(),
),
Insn::PrevAsync { .. } => (
"PrevAsync",
0,
0,
0,
OwnedValue::build_text(""),
0,
"".to_string(),
),
Insn::PrevAwait { .. } => (
"PrevAwait",
0,
0,
Insn::Prev {
cursor_id,
pc_if_prev,
} => (
"Prev",
*cursor_id as i32,
pc_if_prev.to_debug_int(),
0,
OwnedValue::build_text(""),
0,
@ -1344,6 +1352,93 @@ pub fn insn_to_str(
0,
format!("auto_commit={}, rollback={}", auto_commit, rollback),
),
Insn::OpenEphemeral {
cursor_id,
is_table,
} => (
"OpenEphemeral",
*cursor_id as i32,
*is_table as i32,
0,
OwnedValue::build_text(""),
0,
format!(
"cursor={} is_table={}",
cursor_id,
if *is_table { "true" } else { "false" }
),
),
Insn::OpenAutoindex { cursor_id } => (
"OpenAutoindex",
*cursor_id as i32,
0,
0,
OwnedValue::build_text(""),
0,
format!("cursor={}", cursor_id),
),
Insn::Once {
target_pc_when_reentered,
} => (
"Once",
target_pc_when_reentered.to_debug_int(),
0,
0,
OwnedValue::build_text(""),
0,
format!("goto {}", target_pc_when_reentered.to_debug_int()),
),
Insn::BeginSubrtn { dest, dest_end } => (
"BeginSubrtn",
*dest as i32,
dest_end.map_or(0, |end| end as i32),
0,
OwnedValue::build_text(""),
0,
dest_end.map_or(format!("r[{}]=NULL", dest), |end| {
format!("r[{}..{}]=NULL", dest, end)
}),
),
Insn::NotFound {
cursor_id,
target_pc,
record_reg,
..
} => (
"NotFound",
*cursor_id as i32,
target_pc.to_debug_int(),
*record_reg as i32,
OwnedValue::build_text(""),
0,
format!(
"if (r[{}] != NULL) goto {}",
record_reg,
target_pc.to_debug_int()
),
),
Insn::Affinity {
start_reg,
count,
affinities,
} => (
"Affinity",
*start_reg as i32,
count.get() as i32,
0,
OwnedValue::build_text(""),
0,
format!(
"r[{}..{}] = {}",
start_reg,
start_reg + count.get(),
affinities
.chars()
.map(|a| a.to_string())
.collect::<Vec<_>>()
.join(", ")
),
),
};
format!(
"{:<4} {:<17} {:<4} {:<4} {:<4} {:<13} {:<2} {}",

File diff suppressed because it is too large Load diff

View file

@ -24,37 +24,61 @@ pub mod insn;
pub mod likeop;
pub mod sorter;
use crate::error::LimboError;
use crate::fast_lock::SpinLock;
use crate::function::{AggFunc, FuncCtx};
use crate::storage::sqlite3_ondisk::DatabaseHeader;
use crate::storage::{btree::BTreeCursor, pager::Pager};
use crate::translate::plan::{ResultSetColumn, TableReference};
use crate::types::{
AggContext, Cursor, CursorResult, ImmutableRecord, OwnedValue, SeekKey, SeekOp,
use crate::{
error::LimboError,
fast_lock::SpinLock,
function::{AggFunc, FuncCtx},
storage::sqlite3_ondisk::SmallVec,
};
use crate::{
storage::{btree::BTreeCursor, pager::Pager, sqlite3_ondisk::DatabaseHeader},
translate::plan::{ResultSetColumn, TableReference},
types::{AggContext, Cursor, CursorResult, ImmutableRecord, OwnedValue, SeekKey, SeekOp},
vdbe::{builder::CursorType, insn::Insn},
};
use crate::util::cast_text_to_numeric;
use crate::vdbe::builder::CursorType;
use crate::vdbe::insn::Insn;
use crate::CheckpointStatus;
#[cfg(feature = "json")]
use crate::json::JsonCacheCell;
use crate::{Connection, MvStore, Result, TransactionState};
use execute::{InsnFunction, InsnFunctionStepResult};
use execute::{InsnFunction, InsnFunctionStepResult, OpIdxDeleteState};
use rand::distributions::{Distribution, Uniform};
use rand::Rng;
use rand::{
distributions::{Distribution, Uniform},
Rng,
};
use regex::Regex;
use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::ffi::c_void;
use std::num::NonZero;
use std::ops::Deref;
use std::rc::{Rc, Weak};
use std::sync::Arc;
use std::{
cell::{Cell, RefCell},
collections::HashMap,
ffi::c_void,
num::NonZero,
ops::Deref,
rc::{Rc, Weak},
sync::Arc,
};
/// We use labels to indicate that we want to jump to whatever the instruction offset
/// will be at runtime, because the offset cannot always be determined when the jump
/// instruction is created.
///
/// In some cases, we want to jump to EXACTLY a specific instruction.
/// - Example: a condition is not met, so we want to jump to wherever Halt is.
/// In other cases, we don't care what the exact instruction is, but we know that we
/// want to jump to whatever comes AFTER a certain instruction.
/// - Example: a Next instruction will want to jump to "whatever the start of the loop is",
/// but it doesn't care what instruction that is.
///
/// The reason this distinction is important is that we might reorder instructions that are
/// constant at compile time, and when we do that, we need to change the offsets of any impacted
/// jump instructions, so the instruction that comes immediately after "next Insn" might have changed during the reordering.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum JumpTarget {
ExactlyThisInsn,
AfterThisInsn,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
/// Represents a target for a jump instruction.
@ -91,15 +115,6 @@ impl BranchOffset {
}
}
/// Returns the label value. Panics if the branch offset is an offset or placeholder.
pub fn to_label_value(&self) -> u32 {
match self {
BranchOffset::Label(v) => *v,
BranchOffset::Offset(_) => unreachable!("Offset cannot be converted to label value"),
BranchOffset::Placeholder => unreachable!("Unresolved placeholder"),
}
}
/// Returns the branch offset as a signed integer.
/// Used in explain output, where we don't want to panic in case we have an unresolved
/// label or placeholder.
@ -117,6 +132,10 @@ impl BranchOffset {
pub fn add<N: Into<u32>>(self, n: N) -> BranchOffset {
BranchOffset::Offset(self.to_offset_int() + n.into())
}
pub fn sub<N: Into<u32>>(self, n: N) -> BranchOffset {
BranchOffset::Offset(self.to_offset_int() - n.into())
}
}
pub type CursorID = usize;
@ -229,6 +248,8 @@ pub struct ProgramState {
last_compare: Option<std::cmp::Ordering>,
deferred_seek: Option<(CursorID, CursorID)>,
ended_coroutine: Bitfield<4>, // flag to indicate that a coroutine has ended (key is the yield register. currently we assume that the yield register is always between 0-255, YOLO)
/// Indicate whether an [Insn::Once] instruction at a given program counter position has already been executed, well, once.
once: SmallVec<u32, 4>,
regex_cache: RegexCache,
pub(crate) mv_tx_id: Option<crate::mvcc::database::TxID>,
interrupted: bool,
@ -236,6 +257,7 @@ pub struct ProgramState {
halt_state: Option<HaltState>,
#[cfg(feature = "json")]
json_cache: JsonCacheCell,
op_idx_delete_state: Option<OpIdxDeleteState>,
}
impl ProgramState {
@ -251,6 +273,7 @@ impl ProgramState {
last_compare: None,
deferred_seek: None,
ended_coroutine: Bitfield::new(),
once: SmallVec::<u32, 4>::new(),
regex_cache: RegexCache::new(),
mv_tx_id: None,
interrupted: false,
@ -258,6 +281,7 @@ impl ProgramState {
halt_state: None,
#[cfg(feature = "json")]
json_cache: JsonCacheCell::new(),
op_idx_delete_state: None,
}
}
@ -281,8 +305,11 @@ impl ProgramState {
self.parameters.insert(index, value);
}
pub fn get_parameter(&self, index: NonZero<usize>) -> Option<&OwnedValue> {
self.parameters.get(&index)
pub fn get_parameter(&self, index: NonZero<usize>) -> OwnedValue {
self.parameters
.get(&index)
.cloned()
.unwrap_or(OwnedValue::Null)
}
pub fn reset(&mut self) {
@ -342,7 +369,7 @@ pub struct Program {
pub insns: Vec<(Insn, InsnFunction)>,
pub cursor_ref: Vec<(Option<String>, CursorType)>,
pub database_header: Arc<SpinLock<DatabaseHeader>>,
pub comments: Option<HashMap<InsnReference, &'static str>>,
pub comments: Option<Vec<(InsnReference, &'static str)>>,
pub parameters: crate::parameters::Parameters,
pub connection: Weak<Connection>,
pub n_change: Cell<i64>,
@ -386,7 +413,7 @@ impl Program {
) -> Result<StepResult> {
if let Some(mv_store) = mv_store {
let conn = self.connection.upgrade().unwrap();
let auto_commit = *conn.auto_commit.borrow();
let auto_commit = conn.auto_commit.get();
if auto_commit {
let mut mv_transactions = conn.mv_transactions.borrow_mut();
for tx_id in mv_transactions.iter() {
@ -394,13 +421,13 @@ impl Program {
}
mv_transactions.clear();
}
return Ok(StepResult::Done);
Ok(StepResult::Done)
} else {
let connection = self
.connection
.upgrade()
.expect("only weak ref to connection?");
let auto_commit = *connection.auto_commit.borrow();
let auto_commit = connection.auto_commit.get();
tracing::trace!("Halt auto_commit {}", auto_commit);
assert!(
program_state.halt_state.is_none()
@ -408,30 +435,28 @@ impl Program {
);
if program_state.halt_state.is_some() {
self.step_end_write_txn(&pager, &mut program_state.halt_state, connection.deref())
} else {
if auto_commit {
let current_state = connection.transaction_state.borrow().clone();
match current_state {
TransactionState::Write => self.step_end_write_txn(
&pager,
&mut program_state.halt_state,
connection.deref(),
),
TransactionState::Read => {
connection.transaction_state.replace(TransactionState::None);
pager.end_read_tx()?;
Ok(StepResult::Done)
}
TransactionState::None => Ok(StepResult::Done),
} else if auto_commit {
let current_state = connection.transaction_state.get();
match current_state {
TransactionState::Write => self.step_end_write_txn(
&pager,
&mut program_state.halt_state,
connection.deref(),
),
TransactionState::Read => {
connection.transaction_state.replace(TransactionState::None);
pager.end_read_tx()?;
Ok(StepResult::Done)
}
} else {
if self.change_cnt_on {
if let Some(conn) = self.connection.upgrade() {
conn.set_changes(self.n_change.get());
}
}
Ok(StepResult::Done)
TransactionState::None => Ok(StepResult::Done),
}
} else {
if self.change_cnt_on {
if let Some(conn) = self.connection.upgrade() {
conn.set_changes(self.n_change.get());
}
}
Ok(StepResult::Done)
}
}
}
@ -534,10 +559,11 @@ fn trace_insn(program: &Program, addr: InsnReference, insn: &Insn) {
addr,
insn,
String::new(),
program
.comments
.as_ref()
.and_then(|comments| comments.get(&{ addr }).copied())
program.comments.as_ref().and_then(|comments| comments
.iter()
.find(|(offset, _)| *offset == addr)
.map(|(_, comment)| comment)
.copied())
)
);
}
@ -548,10 +574,13 @@ fn print_insn(program: &Program, addr: InsnReference, insn: &Insn, indent: Strin
addr,
insn,
indent,
program
.comments
.as_ref()
.and_then(|comments| comments.get(&{ addr }).copied()),
program.comments.as_ref().and_then(|comments| {
comments
.iter()
.find(|(offset, _)| *offset == addr)
.map(|(_, comment)| comment)
.copied()
}),
);
w.push_str(&s);
}
@ -559,11 +588,14 @@ fn print_insn(program: &Program, addr: InsnReference, insn: &Insn, indent: Strin
fn get_indent_count(indent_count: usize, curr_insn: &Insn, prev_insn: Option<&Insn>) -> usize {
let indent_count = if let Some(insn) = prev_insn {
match insn {
Insn::RewindAwait { .. }
| Insn::LastAwait { .. }
Insn::Rewind { .. }
| Insn::Last { .. }
| Insn::SorterSort { .. }
| Insn::SeekGE { .. }
| Insn::SeekGT { .. } => indent_count + 1,
| Insn::SeekGT { .. }
| Insn::SeekLE { .. }
| Insn::SeekLT { .. } => indent_count + 1,
_ => indent_count,
}
} else {
@ -571,9 +603,7 @@ fn get_indent_count(indent_count: usize, curr_insn: &Insn, prev_insn: Option<&In
};
match curr_insn {
Insn::NextAsync { .. } | Insn::SorterNext { .. } | Insn::PrevAsync { .. } => {
indent_count - 1
}
Insn::Next { .. } | Insn::SorterNext { .. } | Insn::Prev { .. } => indent_count - 1,
_ => indent_count,
}
}
@ -593,6 +623,15 @@ impl<'a> FromValueRow<'a> for i64 {
}
}
impl<'a> FromValueRow<'a> for f64 {
fn from_value(value: &'a OwnedValue) -> Result<Self> {
match value {
OwnedValue::Float(f) => Ok(*f),
_ => Err(LimboError::ConversionError("Expected integer value".into())),
}
}
}
impl<'a> FromValueRow<'a> for String {
fn from_value(value: &'a OwnedValue) -> Result<Self> {
match value {
@ -629,11 +668,10 @@ impl Row {
pub fn get_value<'a>(&'a self, idx: usize) -> &'a OwnedValue {
let value = unsafe { self.values.add(idx).as_ref().unwrap() };
let value = match value {
match value {
Register::OwnedValue(owned_value) => owned_value,
_ => unreachable!("a row should be formed of values only"),
};
value
}
}
pub fn get_values(&self) -> impl Iterator<Item = &OwnedValue> {

View file

@ -1,18 +1,21 @@
use crate::types::ImmutableRecord;
use std::cmp::Ordering;
use limbo_sqlite3_parser::ast::SortOrder;
use crate::types::{compare_immutable, ImmutableRecord, IndexKeySortOrder};
pub struct Sorter {
records: Vec<ImmutableRecord>,
current: Option<ImmutableRecord>,
order: Vec<bool>,
order: IndexKeySortOrder,
key_len: usize,
}
impl Sorter {
pub fn new(order: Vec<bool>) -> Self {
pub fn new(order: &[SortOrder]) -> Self {
Self {
records: Vec::new(),
current: None,
order,
key_len: order.len(),
order: IndexKeySortOrder::from_list(order),
}
}
pub fn is_empty(&self) -> bool {
@ -26,24 +29,11 @@ impl Sorter {
// We do the sorting here since this is what is called by the SorterSort instruction
pub fn sort(&mut self) {
self.records.sort_by(|a, b| {
let cmp_by_idx = |idx: usize, ascending: bool| {
let a = &a.get_value(idx);
let b = &b.get_value(idx);
if ascending {
a.cmp(b)
} else {
b.cmp(a)
}
};
let mut cmp_ret = Ordering::Equal;
for (idx, &is_asc) in self.order.iter().enumerate() {
cmp_ret = cmp_by_idx(idx, is_asc);
if cmp_ret != Ordering::Equal {
break;
}
}
cmp_ret
compare_immutable(
&a.values[..self.key_len],
&b.values[..self.key_len],
self.order,
)
});
self.records.reverse();
self.next()

BIN
db.sqlite Normal file

Binary file not shown.

28
dist-workspace.toml Normal file
View file

@ -0,0 +1,28 @@
[workspace]
members = ["cargo:."]
# Config for 'dist'
[dist]
# The preferred dist version to use in CI (Cargo.toml SemVer syntax)
cargo-dist-version = "0.28.3"
# CI backends to support
ci = "github"
# The installers to generate for each app
installers = ["shell", "powershell"]
# Target platforms to build apps for (Rust target-triple syntax)
targets = [
"aarch64-apple-darwin",
"x86_64-apple-darwin",
"x86_64-unknown-linux-gnu",
"x86_64-pc-windows-msvc",
]
# Which actions to run on pull requests
pr-run-mode = "plan"
# Path that installers should place binaries in
install-path = "~/.limbo"
# Whether to install an updater program
install-updater = true
# Whether to consider the binaries in a package for distribution (defaults true)
dist = false
# Whether to enable GitHub Attestations
github-attestations = true

140
docs/testing.md Normal file
View file

@ -0,0 +1,140 @@
# Testing in Limbo
Limbo supports a comprehensive testing system to ensure correctness, performance, and compatibility with SQLite.
## 1. Compatibility Tests
The `make test` target is the main entry point.
Most compatibility tests live in the testing/ directory and are written in SQLites TCL test format. These tests ensure that Limbo matches SQLites behavior exactly. The database used during these tests is located at testing/testing.db, which includes the following schema:
```sql
CREATE TABLE users (
id INTEGER PRIMARY KEY,
first_name TEXT,
last_name TEXT,
email TEXT,
phone_number TEXT,
address TEXT,
city TEXT,
state TEXT,
zipcode TEXT,
age INTEGER
);
CREATE TABLE products (
id INTEGER PRIMARY KEY,
name TEXT,
price REAL
);
CREATE INDEX age_idx ON users (age);
```
You can freely write queries against these tables during compatibility testing.
### Shell and Python-based Tests
For cases where output or behavior differs intentionally from SQLite (e.g. due to new features or limitations), tests should be placed in the testing/cli_tests/ directory and written in Python.
These tests use the TestLimboShell class:
```python
from cli_tests.common import TestLimboShell
def test_uuid():
limbo = TestLimboShell()
limbo.run_test_fn("SELECT uuid4_str();", lambda res: len(res) == 36)
limbo.quit()
```
You can use run_test, run_test_fn, or debug_print to interact with the shell and validate results.
The constructor takes an optional argument with the `sql` you want to initiate the tests with. You can also enable blob testing or override the executable and flags.
Use these Python-based tests for validating:
- Output formatting
- Shell commands and .dot interactions
- Limbo-specific extensions in `testing/cli_tests/extensions.py`
- Any known divergence from SQLite behavior
> Logging and Tracing
If you wish to trace internal events during test execution, you can set the RUST_LOG environment variable before running the test. For example:
```bash
RUST_LOG=none,limbo_core=trace make test
```
This will enable trace-level logs for the limbo_core crate and disable logs elsewhere. Logging all internal traces to the `testing/test.log` file.
**Note:** trace logs can be very verbose—it's not uncommon for a single test run to generate megabytes of logs.
## Deterministic Simulation Testing (DST):
Limbo simulator uses randomized deterministic simulations to test the Limbo database behaviors.
Each simulation begins with a random configurations:
- the database workload distribution(percentages of reads, writes, deletes...),
- database parameters(page size),
- number of reader or writers, etc.
Based on these parameters, we randomly generate **interaction plans**. Interaction plans consist of statements/queries, and assertions that will be executed in order. The building blocks of interaction plans are:
- Randomly generated SQL queries satisfying the workload distribution,
- Properties, which contain multiple matching queries with assertions indicating the expected result.
An example of a property is the following:
```sql
-- begin testing 'Select-Select-Optimizer'
-- ASSUME table marvelous_ideal exists;
SELECT ((devoted_ahmed = -9142609771.541502 AND loving_wicker = -1246708244.164486)) FROM marvelous_ideal WHERE TRUE;
SELECT * FROM marvelous_ideal WHERE (devoted_ahmed = -9142609771.541502 AND loving_wicker = -1246708244.164486);
-- ASSERT select queries should return the same amount of results;
-- end testing 'Select-Select-Optimizer'
```
The simulator starts from an initially empty database, adding random interactions based on the workload distribution. It can
add random queries unrelated to the properties without breaking the property invariants to reach more diverse states and respect the configured workload distribution.
The simulator executes the interaction plans in a loop, and checks the assertions. It can add random queries unrelated to the properties without
breaking the property invariants to reach more diverse states and respect the configured workload distribution.
## Usage
To run the simulator, you can use the following command:
```bash
RUST_LOG=limbo_sim=debug cargo run --bin limbo_sim
```
The simulator CLI has a few configuration options that you can explore via `--help` flag.
```txt
The Limbo deterministic simulator
Usage: limbo_sim [OPTIONS]
Options:
-s, --seed <SEED> set seed for reproducible runs
-d, --doublecheck enable doublechecking, run the simulator with the plan twice and check output equality
-n, --maximum-size <MAXIMUM_SIZE> change the maximum size of the randomly generated sequence of interactions [default: 5000]
-k, --minimum-size <MINIMUM_SIZE> change the minimum size of the randomly generated sequence of interactions [default: 1000]
-t, --maximum-time <MAXIMUM_TIME> change the maximum time of the simulation(in seconds) [default: 3600]
-l, --load <LOAD> load plan from the bug base
-w, --watch enable watch mode that reruns the simulation on file changes
--differential run differential testing between sqlite and Limbo
-h, --help Print help
-V, --version Print version
```
## Fuzzing
TODO!

View file

@ -91,8 +91,8 @@ impl VTabModule for CompletionVTab {
cursor.eof()
}
fn filter(cursor: &mut Self::VCursor, args: &[Value]) -> ResultCode {
if args.len() == 0 || args.len() > 2 {
fn filter(cursor: &mut Self::VCursor, args: &[Value], _: Option<(&str, i32)>) -> ResultCode {
if args.is_empty() || args.len() > 2 {
return ResultCode::InvalidArgs;
}
cursor.reset();

View file

@ -15,7 +15,10 @@ pub use types::{ResultCode, Value, ValueType};
#[cfg(feature = "vfs")]
pub use vfs_modules::{RegisterVfsFn, VfsExtension, VfsFile, VfsFileImpl, VfsImpl, VfsInterface};
use vtabs::RegisterModuleFn;
pub use vtabs::{VTabCursor, VTabKind, VTabModule, VTabModuleImpl};
pub use vtabs::{
ConstraintInfo, ConstraintOp, ConstraintUsage, ExtIndexInfo, IndexInfo, OrderByInfo,
VTabCursor, VTabKind, VTabModule, VTabModuleImpl,
};
pub type ExtResult<T> = std::result::Result<T, ResultCode>;

Some files were not shown because too many files have changed in this diff Show more