diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 000000000..151ee791c --- /dev/null +++ b/.github/workflows/go.yml @@ -0,0 +1,43 @@ +name: Go Tests + +on: + push: + branches: + - main + tags: + - v* + pull_request: + branches: + - main + +env: + working-directory: bindings/go + +jobs: + test: + runs-on: ubuntu-latest + + defaults: + run: + working-directory: ${{ env.working-directory }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Rust(stable) + uses: dtolnay/rust-toolchain@stable + + - name: Set up go + uses: actions/setup-go@v4 + with: + go-version: "1.23" + + - name: build Go bindings library + run: cargo build --package limbo-go + + - name: run Go tests + env: + LD_LIBRARY_PATH: ${{ github.workspace }}/target/debug:$LD_LIBRARY_PATH + run: go test + diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index d6e02a69f..f7dc35258 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -15,19 +15,28 @@ env: PIP_DISABLE_PIP_VERSION_CHECK: "true" jobs: + configure-strategy: + runs-on: ubuntu-latest + outputs: + python-versions: ${{ steps.gen-matrix.outputs.python-versions }} + steps: + - id: gen-matrix + run: | + if [ ${{ github.event_name }} == "pull_request" ]; then + echo "python-versions=[\"3.13\"]" >> $GITHUB_OUTPUT + else + echo "python-versions=[\"3.9\",\"3.10\",\"3.11\",\"3.12\",\"3.13\"]" >> $GITHUB_OUTPUT + fi + test: + needs: configure-strategy strategy: matrix: os: - ubuntu-latest - macos-latest - windows-latest - python-version: - - "3.9" - - "3.10" - - "3.11" - - "3.12" - - "3.13" + python-version: ${{ fromJson(needs.configure-strategy.outputs.python-versions) }} runs-on: ${{ matrix.os }} defaults: @@ -123,7 +132,7 @@ jobs: - name: Upload wheels uses: actions/upload-artifact@v4 with: - name: linux-wheels + name: wheels-linux path: bindings/python/dist macos-x86_64: @@ -152,7 +161,7 @@ jobs: - name: Upload wheels uses: actions/upload-artifact@v4 with: - name: macos-x86-wheels + name: wheels-macos-x86 path: bindings/python/dist macos-arm64: @@ -181,7 +190,7 @@ jobs: - name: Upload wheels uses: actions/upload-artifact@v4 with: - name: macos-arm64-wheels + name: wheels-macos-arm64 path: bindings/python/dist sdist: @@ -200,7 +209,7 @@ jobs: - name: Upload sdist uses: actions/upload-artifact@v4 with: - name: sdist-wheels + name: wheels-sdist path: bindings/python/dist release: @@ -209,18 +218,11 @@ jobs: if: "startsWith(github.ref, 'refs/tags/')" needs: [linux, macos-arm64, macos-x86_64, sdist] steps: - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: - name: linux-wheels - - uses: actions/download-artifact@v3 - with: - name: macos-x86-wheels - - uses: actions/download-artifact@v3 - with: - name: macos-arm64-wheels - - uses: actions/download-artifact@v3 - with: - name: sdist-wheels + path: bindings/python/dist + pattern: wheels-* + merge-multiple: true - name: Publish to PyPI uses: PyO3/maturin-action@v1 env: diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index ad9d1bbe7..44fb4cff3 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -14,7 +14,7 @@ jobs: - name: Close stale pull requests uses: actions/stale@v6 with: - repo-token: ${{ secrets.STALE_GH_TOKEN }} + repo-token: ${{ secrets.GH_TOKEN }} operations-per-run: 1000 ascending: true stale-pr-message: 'This pull request has been marked as stale due to inactivity. It will be closed in 7 days if no further activity occurs.' diff --git a/.gitignore b/.gitignore index 1f1406ceb..c7c56a7ee 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,7 @@ dist/ .DS_Store # Javascript -**/node_modules/ \ No newline at end of file +**/node_modules/ + +# testing +testing/limbo_output.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 9186ec8a5..fee38e12c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,79 @@ # Changelog +## 0.0.14 - 2025-02-04 + +### Added + +**Core:** + +* Improve changes() and total_changes() functions and add tests (Ben Li) +* Add support for `json_object` function (Jorge Hermo) +* Implemented json_valid function (Harin) +* Implement Not (Vrishabh) +* Initial support for wal_checkpoint pragma (Sonny) +* Implement Or and And bytecodes (Diego Reis) +* Implement strftime function (Pedro Muniz) +* implement sqlite_source_id function (Glauber Costa) +* json_patch() function implementation (Ihor Andrianov) +* json_remove() function implementation (Ihor Andrianov) +* Implement isnull / not null for filter expressions (Glauber Costa) +* Add support for offset in select queries (Ben Li) +* Support returning column names from prepared statement (Preston Thorpe) +* Implement Concat opcode (Harin) +* Table info (Glauber Costa) +* Pragma list (Glauber Costa) +* Implement Noop bytecode (Pedro Muniz) +* implement is and is not where constraints (Glauber Costa) +* Pagecount (Glauber Costa) +* Support column aliases in GROUP BY, ORDER BY and HAVING (Jussi Saurio) +* Implement json_pretty (Pedro Muniz) + +**Extensions:** + +* Initial pass on vector extension (Pekka Enberg) +* Enable static linking for 'built-in' extensions (Preston Thorpe) + +**Go Bindings:** + +* Initial support for Go database/sql driver (Preston Thorpe) +* Avoid potentially expensive operations on prepare' (Glauber Costa) + +**Java Bindings:** + +* Implement JDBC `ResultSet` (Kim Seon Woo) +* Implement LimboConnection `close()` (Kim Seon Woo) +* Implement close() for `LimboStatement` and `LimboResultSet` (Kim Seon Woo) +* Implement methods in `JDBC4ResultSet` (Kim Seon Woo) +* Load native library from Jar (Kim Seon Woo) +* Change logger dependency (Kim Seon Woo) +* Log driver loading error (Pekka Enberg) + +**Simulator:** + +* Implement `--load` and `--watch` flags (Alperen Keleş) + +**Build system and CI:** + +* Add Nyrkiö change point detection to 'cargo bench' workflow (Henrik Ingo) + +### Fixed + +* Fix `select X'1';` causes limbo to go in infinite loop (Krishna Vishal) +* Fix rowid search codegen (Nikita Sivukhin) +* Fix logical codegen (Nikita Sivukhin) +* Fix parser panic when duplicate column names are given to `CREATE TABLE` (Krishna Vishal) +* Fix panic when double quoted strings are used for column names. (Krishna Vishal) +* Fix `SELECT -9223372036854775808` result differs from SQLite (Krishna Vishal) +* Fix `SELECT ABS(-9223372036854775808)` causes limbo to panic. (Krishna Vishal) +* Fix memory leaks, make extension types more efficient (Preston Thorpe) +* Fix table with single column PRIMARY KEY to not create extra btree (Krishna Vishal) +* Fix null cmp codegen (Nikita Sivukhin) +* Fix null expr codegen (Nikita Sivukhin) +* Fix rowid generation (Nikita Sivukhin) +* Fix shr instruction (Nikita Sivukhin) +* Fix strftime function compatibility problems (Pedro Muniz) +* Dont fsync the WAL on read queries (Jussi Saurio) + ## 0.0.13 - 2025-01-19 ### Added diff --git a/COMPAT.md b/COMPAT.md index d7c8b0a2c..1c2b9b227 100644 --- a/COMPAT.md +++ b/COMPAT.md @@ -19,9 +19,12 @@ This document describes the compatibility of Limbo with SQLite. - [JSON functions](#json-functions) - [SQLite C API](#sqlite-c-api) - [SQLite VDBE opcodes](#sqlite-vdbe-opcodes) + - [SQLite journaling modes](#sqlite-journaling-modes) - [Extensions](#extensions) - [UUID](#uuid) - [regexp](#regexp) + - [Vector](#vector) + - [Time](#time) ## Features @@ -38,229 +41,229 @@ The current status of Limbo is: ### Statements -| Statement | Status | Comment | -| ------------------------- | ------- | ------- | -| ALTER TABLE | No | | -| ANALYZE | No | | -| ATTACH DATABASE | No | | -| BEGIN TRANSACTION | No | | -| COMMIT TRANSACTION | No | | -| CREATE INDEX | No | | -| CREATE TABLE | Partial | | -| CREATE TRIGGER | No | | -| CREATE VIEW | No | | -| CREATE VIRTUAL TABLE | No | | -| DELETE | No | | -| DETACH DATABASE | No | | -| DROP INDEX | No | | -| DROP TABLE | No | | -| DROP TRIGGER | No | | -| DROP VIEW | No | | -| END TRANSACTION | No | | -| EXPLAIN | Yes | | -| INDEXED BY | No | | -| INSERT | Partial | | -| ON CONFLICT clause | No | | -| REINDEX | No | | -| RELEASE SAVEPOINT | No | | -| REPLACE | No | | -| RETURNING clause | No | | -| ROLLBACK TRANSACTION | No | | -| SAVEPOINT | No | | -| SELECT | Yes | | -| SELECT ... WHERE | Yes | | -| SELECT ... WHERE ... LIKE | Yes | | -| SELECT ... LIMIT | Yes | | -| SELECT ... ORDER BY | Yes | | -| SELECT ... GROUP BY | Yes | | -| SELECT ... HAVING | Yes | | -| SELECT ... JOIN | Yes | | +| Statement | Status | Comment | +|---------------------------|---------|-----------------------------------------------------------------------------------| +| ALTER TABLE | No | | +| ANALYZE | No | | +| ATTACH DATABASE | No | | +| BEGIN TRANSACTION | No | | +| COMMIT TRANSACTION | No | | +| CREATE INDEX | No | | +| CREATE TABLE | Partial | | +| CREATE TRIGGER | No | | +| CREATE VIEW | No | | +| CREATE VIRTUAL TABLE | No | | +| DELETE | No | | +| DETACH DATABASE | No | | +| DROP INDEX | No | | +| DROP TABLE | No | | +| DROP TRIGGER | No | | +| DROP VIEW | No | | +| END TRANSACTION | No | | +| EXPLAIN | Yes | | +| INDEXED BY | No | | +| INSERT | Partial | | +| ON CONFLICT clause | No | | +| REINDEX | No | | +| RELEASE SAVEPOINT | No | | +| REPLACE | No | | +| RETURNING clause | No | | +| ROLLBACK TRANSACTION | No | | +| SAVEPOINT | No | | +| SELECT | Yes | | +| SELECT ... WHERE | Yes | | +| SELECT ... WHERE ... LIKE | Yes | | +| SELECT ... LIMIT | Yes | | +| SELECT ... ORDER BY | Yes | | +| SELECT ... GROUP BY | Yes | | +| SELECT ... HAVING | Yes | | +| SELECT ... JOIN | Yes | | | SELECT ... CROSS JOIN | Yes | SQLite CROSS JOIN means "do not reorder joins". We don't support that yet anyway. | -| SELECT ... INNER JOIN | Yes | | -| SELECT ... OUTER JOIN | Partial | no RIGHT JOIN | -| SELECT ... JOIN USING | Yes | | -| SELECT ... NATURAL JOIN | Yes | | -| UPDATE | No | | -| UPSERT | No | | -| VACUUM | No | | -| WITH clause | No | | +| SELECT ... INNER JOIN | Yes | | +| SELECT ... OUTER JOIN | Partial | no RIGHT JOIN | +| SELECT ... JOIN USING | Yes | | +| SELECT ... NATURAL JOIN | Yes | | +| UPDATE | No | | +| UPSERT | No | | +| VACUUM | No | | +| WITH clause | No | | #### [PRAGMA](https://www.sqlite.org/pragma.html) -| Statement | Status | Comment | -|----------------------------------|------------|-------------------------------------------------| -| PRAGMA analysis_limit | No | | -| PRAGMA application_id | No | | -| PRAGMA auto_vacuum | No | | -| PRAGMA automatic_index | No | | -| PRAGMA busy_timeout | No | | -| PRAGMA busy_timeout | No | | -| PRAGMA cache_size | Yes | | -| PRAGMA cache_spill | No | | -| PRAGMA case_sensitive_like | Not Needed | deprecated in SQLite | -| PRAGMA cell_size_check | No | | -| PRAGMA checkpoint_fullsync | No | | -| PRAGMA collation_list | No | | -| PRAGMA compile_options | No | | -| PRAGMA count_changes | Not Needed | deprecated in SQLite | -| PRAGMA data_store_directory | Not Needed | deprecated in SQLite | -| PRAGMA data_version | No | | -| PRAGMA database_list | No | | -| PRAGMA default_cache_size | Not Needed | deprecated in SQLite | -| PRAGMA defer_foreign_keys | No | | -| PRAGMA empty_result_callbacks | Not Needed | deprecated in SQLite | -| PRAGMA encoding | No | | -| PRAGMA foreign_key_check | No | | -| PRAGMA foreign_key_list | No | | -| PRAGMA foreign_keys | No | | -| PRAGMA freelist_count | No | | -| PRAGMA full_column_names | Not Needed | deprecated in SQLite | -| PRAGMA fullsync | No | | -| PRAGMA function_list | No | | -| PRAGMA hard_heap_limit | No | | -| PRAGMA ignore_check_constraints | No | | -| PRAGMA incremental_vacuum | No | | -| PRAGMA index_info | No | | -| PRAGMA index_list | No | | -| PRAGMA index_xinfo | No | | -| PRAGMA integrity_check | No | | -| PRAGMA journal_mode | No | | -| PRAGMA journal_size_limit | No | | -| PRAGMA legacy_alter_table | No | | -| PRAGMA legacy_file_format | No | | -| PRAGMA locking_mode | No | | -| PRAGMA max_page_count | No | | -| PRAGMA mmap_size | No | | -| PRAGMA module_list | No | | -| PRAGMA optimize | No | | -| PRAGMA page_count | No | | -| PRAGMA page_size | No | | -| PRAGMA parser_trace | No | | -| PRAGMA pragma_list | No | | -| PRAGMA query_only | No | | -| PRAGMA quick_check | No | | -| PRAGMA read_uncommitted | No | | -| PRAGMA recursive_triggers | No | | -| PRAGMA reverse_unordered_selects | No | | -| PRAGMA schema_version | No | | -| PRAGMA secure_delete | No | | -| PRAGMA short_column_names | Not Needed | deprecated in SQLite | -| PRAGMA shrink_memory | No | | -| PRAGMA soft_heap_limit | No | | -| PRAGMA stats | No | Used for testing in SQLite | -| PRAGMA synchronous | No | | -| PRAGMA table_info | No | | -| PRAGMA table_list | No | | -| PRAGMA table_xinfo | No | | -| PRAGMA temp_store | No | | -| PRAGMA temp_store_directory | Not Needed | deprecated in SQLite | -| PRAGMA threads | No | | -| PRAGMA trusted_schema | No | | -| PRAGMA user_version | No | | -| PRAGMA vdbe_addoptrace | No | | -| PRAGMA vdbe_debug | No | | -| PRAGMA vdbe_listing | No | | -| PRAGMA vdbe_trace | No | | -| PRAGMA wal_autocheckpoint | No | | -| PRAGMA wal_checkpoint | Partial | Not supported calling with param (pragma-value) | -| PRAGMA writable_schema | No | | +| Statement | Status | Comment | +|----------------------------------|------------|----------------------------------------------| +| PRAGMA analysis_limit | No | | +| PRAGMA application_id | No | | +| PRAGMA auto_vacuum | No | | +| PRAGMA automatic_index | No | | +| PRAGMA busy_timeout | No | | +| PRAGMA busy_timeout | No | | +| PRAGMA cache_size | Yes | | +| PRAGMA cache_spill | No | | +| PRAGMA case_sensitive_like | Not Needed | deprecated in SQLite | +| PRAGMA cell_size_check | No | | +| PRAGMA checkpoint_fullsync | No | | +| PRAGMA collation_list | No | | +| PRAGMA compile_options | No | | +| PRAGMA count_changes | Not Needed | deprecated in SQLite | +| PRAGMA data_store_directory | Not Needed | deprecated in SQLite | +| PRAGMA data_version | No | | +| PRAGMA database_list | No | | +| PRAGMA default_cache_size | Not Needed | deprecated in SQLite | +| PRAGMA defer_foreign_keys | No | | +| PRAGMA empty_result_callbacks | Not Needed | deprecated in SQLite | +| PRAGMA encoding | No | | +| PRAGMA foreign_key_check | No | | +| PRAGMA foreign_key_list | No | | +| PRAGMA foreign_keys | No | | +| PRAGMA freelist_count | No | | +| PRAGMA full_column_names | Not Needed | deprecated in SQLite | +| PRAGMA fullsync | No | | +| PRAGMA function_list | No | | +| PRAGMA hard_heap_limit | No | | +| PRAGMA ignore_check_constraints | No | | +| PRAGMA incremental_vacuum | No | | +| PRAGMA index_info | No | | +| PRAGMA index_list | No | | +| PRAGMA index_xinfo | No | | +| PRAGMA integrity_check | No | | +| PRAGMA journal_mode | Yes | | +| PRAGMA journal_size_limit | No | | +| PRAGMA legacy_alter_table | No | | +| PRAGMA legacy_file_format | No | | +| PRAGMA locking_mode | No | | +| PRAGMA max_page_count | No | | +| PRAGMA mmap_size | No | | +| PRAGMA module_list | No | | +| PRAGMA optimize | No | | +| PRAGMA page_count | Yes | | +| PRAGMA page_size | No | | +| PRAGMA parser_trace | No | | +| PRAGMA pragma_list | Yes | | +| PRAGMA query_only | No | | +| PRAGMA quick_check | No | | +| PRAGMA read_uncommitted | No | | +| PRAGMA recursive_triggers | No | | +| PRAGMA reverse_unordered_selects | No | | +| PRAGMA schema_version | No | | +| PRAGMA secure_delete | No | | +| PRAGMA short_column_names | Not Needed | deprecated in SQLite | +| PRAGMA shrink_memory | No | | +| PRAGMA soft_heap_limit | No | | +| PRAGMA stats | No | Used for testing in SQLite | +| PRAGMA synchronous | No | | +| PRAGMA table_info | Yes | | +| PRAGMA table_list | No | | +| PRAGMA table_xinfo | No | | +| PRAGMA temp_store | No | | +| PRAGMA temp_store_directory | Not Needed | deprecated in SQLite | +| PRAGMA threads | No | | +| PRAGMA trusted_schema | No | | +| PRAGMA user_version | No | | +| PRAGMA vdbe_addoptrace | No | | +| PRAGMA vdbe_debug | No | | +| PRAGMA vdbe_listing | No | | +| PRAGMA vdbe_trace | No | | +| PRAGMA wal_autocheckpoint | No | | +| PRAGMA wal_checkpoint | Partial | Not Needed calling with param (pragma-value) | +| PRAGMA writable_schema | No | | ### Expressions Feature support of [sqlite expr syntax](https://www.sqlite.org/lang_expr.html). -| Syntax | Status | Comment | -|------------------------------|---------|---------| -| literals | Yes | | -| schema.table.column | Partial | Schemas aren't supported | -| unary operator | Yes | | -| binary operator | Partial | Only `%`, `!<`, and `!>` are unsupported | -| agg() FILTER (WHERE ...) | No | Is incorrectly ignored | -| ... OVER (...) | No | Is incorrectly ignored | -| (expr) | Yes | | -| CAST (expr AS type) | Yes | | -| COLLATE | No | | -| (NOT) LIKE | No | | -| (NOT) GLOB | No | | -| (NOT) REGEXP | No | | -| (NOT) MATCH | No | | -| IS (NOT) | No | | -| IS (NOT) DISTINCT FROM | No | | -| (NOT) BETWEEN ... AND ... | No | | -| (NOT) IN (subquery) | No | | -| (NOT) EXISTS (subquery) | No | | -| CASE WHEN THEN ELSE END | Yes | | -| RAISE | No | | +| Syntax | Status | Comment | +|---------------------------|---------|------------------------------------------| +| literals | Yes | | +| schema.table.column | Partial | Schemas aren't supported | +| unary operator | Yes | | +| binary operator | Partial | Only `%`, `!<`, and `!>` are unsupported | +| agg() FILTER (WHERE ...) | No | Is incorrectly ignored | +| ... OVER (...) | No | Is incorrectly ignored | +| (expr) | Yes | | +| CAST (expr AS type) | Yes | | +| COLLATE | No | | +| (NOT) LIKE | Yes | | +| (NOT) GLOB | Yes | | +| (NOT) REGEXP | No | | +| (NOT) MATCH | No | | +| IS (NOT) | Yes | | +| IS (NOT) DISTINCT FROM | Yes | | +| (NOT) BETWEEN ... AND ... | No | | +| (NOT) IN (subquery) | No | | +| (NOT) EXISTS (subquery) | No | | +| CASE WHEN THEN ELSE END | Yes | | +| RAISE | No | | ### SQL functions #### Scalar functions -| Function | Status | Comment | -|------------------------------|--------|---------| -| abs(X) | Yes | | -| changes() | Partial| Still need to support update statements and triggers | -| char(X1,X2,...,XN) | Yes | | -| coalesce(X,Y,...) | Yes | | -| concat(X,...) | Yes | | -| concat_ws(SEP,X,...) | Yes | | -| format(FORMAT,...) | No | | -| glob(X,Y) | Yes | | -| hex(X) | Yes | | -| ifnull(X,Y) | Yes | | -| iif(X,Y,Z) | Yes | | -| instr(X,Y) | Yes | | -| last_insert_rowid() | Yes | | -| length(X) | Yes | | -| like(X,Y) | Yes | | -| like(X,Y,Z) | Yes | | -| likelihood(X,Y) | No | | -| likely(X) | No | | -| load_extension(X) | Yes | sqlite3 extensions not yet supported | -| load_extension(X,Y) | No | | -| lower(X) | Yes | | -| ltrim(X) | Yes | | -| ltrim(X,Y) | Yes | | -| max(X,Y,...) | Yes | | -| min(X,Y,...) | Yes | | -| nullif(X,Y) | Yes | | -| octet_length(X) | Yes | | -| printf(FORMAT,...) | No | | -| quote(X) | Yes | | -| random() | Yes | | -| randomblob(N) | Yes | | -| replace(X,Y,Z) | Yes | | -| round(X) | Yes | | -| round(X,Y) | Yes | | -| rtrim(X) | Yes | | -| rtrim(X,Y) | Yes | | -| sign(X) | Yes | | -| soundex(X) | Yes | | -| sqlite_compileoption_get(N) | No | | -| sqlite_compileoption_used(X) | No | | -| sqlite_offset(X) | No | | -| sqlite_source_id() | No | | -| sqlite_version() | Yes | | -| substr(X,Y,Z) | Yes | | -| substr(X,Y) | Yes | | -| substring(X,Y,Z) | Yes | | -| substring(X,Y) | Yes | | -| total_changes() | Partial| Still need to support update statements and triggers | -| trim(X) | Yes | | -| trim(X,Y) | Yes | | -| typeof(X) | Yes | | -| unhex(X) | Yes | | -| unhex(X,Y) | Yes | | -| unicode(X) | Yes | | -| unlikely(X) | No | | -| upper(X) | Yes | | -| zeroblob(N) | Yes | | +| Function | Status | Comment | +|------------------------------|---------|------------------------------------------------------| +| abs(X) | Yes | | +| changes() | Partial | Still need to support update statements and triggers | +| char(X1,X2,...,XN) | Yes | | +| coalesce(X,Y,...) | Yes | | +| concat(X,...) | Yes | | +| concat_ws(SEP,X,...) | Yes | | +| format(FORMAT,...) | No | | +| glob(X,Y) | Yes | | +| hex(X) | Yes | | +| ifnull(X,Y) | Yes | | +| iif(X,Y,Z) | Yes | | +| instr(X,Y) | Yes | | +| last_insert_rowid() | Yes | | +| length(X) | Yes | | +| like(X,Y) | Yes | | +| like(X,Y,Z) | Yes | | +| likelihood(X,Y) | No | | +| likely(X) | No | | +| load_extension(X) | Yes | sqlite3 extensions not yet supported | +| load_extension(X,Y) | No | | +| lower(X) | Yes | | +| ltrim(X) | Yes | | +| ltrim(X,Y) | Yes | | +| max(X,Y,...) | Yes | | +| min(X,Y,...) | Yes | | +| nullif(X,Y) | Yes | | +| octet_length(X) | Yes | | +| printf(FORMAT,...) | Yes | Still need support additional modifiers | +| quote(X) | Yes | | +| random() | Yes | | +| randomblob(N) | Yes | | +| replace(X,Y,Z) | Yes | | +| round(X) | Yes | | +| round(X,Y) | Yes | | +| rtrim(X) | Yes | | +| rtrim(X,Y) | Yes | | +| sign(X) | Yes | | +| soundex(X) | Yes | | +| sqlite_compileoption_get(N) | No | | +| sqlite_compileoption_used(X) | No | | +| sqlite_offset(X) | No | | +| sqlite_source_id() | Yes | | +| sqlite_version() | Yes | | +| substr(X,Y,Z) | Yes | | +| substr(X,Y) | Yes | | +| substring(X,Y,Z) | Yes | | +| substring(X,Y) | Yes | | +| total_changes() | Partial | Still need to support update statements and triggers | +| trim(X) | Yes | | +| trim(X,Y) | Yes | | +| typeof(X) | Yes | | +| unhex(X) | Yes | | +| unhex(X,Y) | Yes | | +| unicode(X) | Yes | | +| unlikely(X) | No | | +| upper(X) | Yes | | +| zeroblob(N) | Yes | | #### Mathematical functions | Function | Status | Comment | -| ---------- | ------ | ------- | +|------------|--------|---------| | acos(X) | Yes | | | acosh(X) | Yes | | | asin(X) | Yes | | @@ -348,7 +351,7 @@ Modifiers: #### JSON functions | Function | Status | Comment | -|------------------------------------|---------|----------------------------------------------------------------------------------------------------------------------------------------------| +| ---------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | json(json) | Partial | | | jsonb(json) | | | | json_array(value1,value2,...) | Yes | | @@ -364,14 +367,14 @@ Modifiers: | jsonb_insert(json,path,value,...) | | | | json_object(label1,value1,...) | Yes | When keys are duplicated, only the last one processed is returned. This differs from sqlite, where the keys in the output can be duplicated | | jsonb_object(label1,value1,...) | | | -| json_patch(json1,json2) | | | +| json_patch(json1,json2) | Yes | | | jsonb_patch(json1,json2) | | | -| json_pretty(json) | | | -| json_remove(json,path,...) | | | +| json_pretty(json) | Partial | Shares same json(val) limitations. Also, when passing blobs for indentation, conversion is not exactly the same as in SQLite | +| json_remove(json,path,...) | Partial | Uses same json path parser as json_extract so shares same limitations. | | jsonb_remove(json,path,...) | | | | json_replace(json,path,value,...) | | | | jsonb_replace(json,path,value,...) | | | -| json_set(json,path,value,...) | | | +| json_set(json,path,value,...) | Yes | | | jsonb_set(json,path,value,...) | | | | json_type(json) | Yes | | | json_type(json,path) | Yes | | @@ -400,178 +403,193 @@ Modifiers: ## SQLite VDBE opcodes -| Opcode | Status | -|----------------|--------| -| Add | Yes | -| AddImm | No | -| Affinity | No | -| AggFinal | Yes | -| AggStep | Yes | -| AggStep | Yes | -| And | Yes | -| AutoCommit | No | -| BitAnd | Yes | -| BitNot | Yes | -| BitOr | Yes | -| Blob | Yes | -| Checkpoint | No | -| Clear | No | -| Close | No | -| CollSeq | No | -| Column | Yes | -| Compare | Yes | -| Concat | Yes | -| Copy | Yes | -| Count | No | -| CreateIndex | No | -| CreateTable | No | -| DecrJumpZero | Yes | -| Delete | No | -| Destroy | No | -| Divide | Yes | -| DropIndex | No | -| DropTable | No | -| DropTrigger | No | -| EndCoroutine | Yes | -| Eq | Yes | -| Expire | No | -| Explain | No | -| FkCounter | No | -| FkIfZero | No | -| Found | No | -| Function | Yes | -| Ge | Yes | -| Gosub | Yes | -| Goto | Yes | -| Gt | Yes | -| Halt | Yes | -| HaltIfNull | No | -| IdxDelete | No | -| IdxGE | Yes | -| IdxInsert | No | -| IdxLT | No | -| IdxRowid | No | -| If | Yes | -| IfNeg | No | -| IfNot | Yes | -| IfPos | Yes | -| IfZero | No | -| IncrVacuum | No | -| Init | Yes | -| InitCoroutine | Yes | -| Insert | No | -| InsertAsync | Yes | -| InsertAwait | Yes | -| InsertInt | No | -| Int64 | No | -| Integer | Yes | -| IntegrityCk | No | -| IsNull | Yes | -| IsUnique | No | -| JournalMode | No | -| Jump | Yes | -| Last | No | -| Le | Yes | -| LoadAnalysis | No | -| Lt | Yes | -| MakeRecord | Yes | -| MaxPgcnt | No | -| MemMax | No | -| Move | No | -| Multiply | Yes | -| MustBeInt | Yes | -| Ne | Yes | -| NewRowid | Yes | -| Next | No | -| NextAsync | Yes | -| NextAwait | Yes | -| Noop | No | -| Not | Yes | -| NotExists | Yes | -| NotFound | No | -| NotNull | Yes | -| Null | Yes | -| NullRow | Yes | -| Once | No | -| OpenAutoindex | No | -| OpenEphemeral | No | -| OpenPseudo | Yes | -| OpenRead | Yes | -| OpenReadAsync | Yes | -| OpenWrite | No | -| OpenWriteAsync | Yes | -| OpenWriteAwait | Yes | -| Or | Yes | -| Pagecount | No | -| Param | No | -| ParseSchema | No | -| Permutation | No | -| Prev | No | -| PrevAsync | Yes | -| PrevAwait | Yes | -| Program | No | -| ReadCookie | No | -| Real | Yes | -| RealAffinity | Yes | -| Remainder | Yes | -| ResetCount | No | -| ResultRow | Yes | -| Return | Yes | -| Rewind | Yes | -| RewindAsync | Yes | -| RewindAwait | Yes | -| RowData | No | -| RowId | Yes | -| RowKey | No | -| RowSetAdd | No | -| RowSetRead | No | -| RowSetTest | No | -| Rowid | Yes | -| SCopy | No | -| Savepoint | No | -| Seek | No | -| SeekGe | Yes | -| SeekGt | Yes | -| SeekLe | No | -| SeekLt | No | -| SeekRowid | Yes | -| Sequence | No | -| SetCookie | No | -| ShiftLeft | Yes | -| ShiftRight | Yes | -| SoftNull | Yes | -| Sort | No | -| SorterCompare | No | -| SorterData | Yes | -| SorterInsert | Yes | -| SorterNext | Yes | -| SorterOpen | Yes | -| SorterSort | Yes | -| String | No | -| String8 | Yes | -| Subtract | Yes | -| TableLock | No | -| ToBlob | No | -| ToInt | No | -| ToNumeric | No | -| ToReal | No | -| ToText | No | -| Trace | No | -| Transaction | Yes | -| VBegin | No | -| VColumn | No | -| VCreate | No | -| VDestroy | No | -| VFilter | No | -| VNext | No | -| VOpen | No | -| VRename | No | -| VUpdate | No | -| Vacuum | No | -| Variable | No | -| VerifyCookie | No | -| Yield | Yes | -| ZeroOrNull | Yes | +| Opcode | Status | Comment | +|----------------|--------|---------| +| Add | Yes | | +| AddImm | No | | +| Affinity | No | | +| AggFinal | Yes | | +| AggStep | Yes | | +| AggStep | Yes | | +| And | Yes | | +| AutoCommit | No | | +| BitAnd | Yes | | +| BitNot | Yes | | +| BitOr | Yes | | +| Blob | Yes | | +| Checkpoint | No | | +| Clear | No | | +| Close | No | | +| CollSeq | No | | +| Column | Yes | | +| Compare | Yes | | +| Concat | Yes | | +| Copy | Yes | | +| Count | No | | +| CreateBTree | Partial| no temp databases | +| CreateTable | No | | +| CreateTable | No | | +| DecrJumpZero | Yes | | +| Delete | No | | +| Destroy | No | | +| Divide | Yes | | +| DropIndex | No | | +| DropTable | No | | +| DropTrigger | No | | +| EndCoroutine | Yes | | +| Eq | Yes | | +| Expire | No | | +| Explain | No | | +| FkCounter | No | | +| FkIfZero | No | | +| Found | No | | +| Function | Yes | | +| Ge | Yes | | +| Gosub | Yes | | +| Goto | Yes | | +| Gt | Yes | | +| Halt | Yes | | +| HaltIfNull | No | | +| IdxDelete | No | | +| IdxGE | Yes | | +| IdxInsert | No | | +| IdxLT | No | | +| IdxRowid | No | | +| If | Yes | | +| IfNeg | No | | +| IfNot | Yes | | +| IfPos | Yes | | +| IfZero | No | | +| IncrVacuum | No | | +| Init | Yes | | +| InitCoroutine | Yes | | +| Insert | No | | +| InsertAsync | Yes | | +| InsertAwait | Yes | | +| InsertInt | No | | +| Int64 | No | | +| Integer | Yes | | +| IntegrityCk | No | | +| IsNull | Yes | | +| IsUnique | No | | +| JournalMode | No | | +| Jump | Yes | | +| Last | No | | +| Le | Yes | | +| LoadAnalysis | No | | +| Lt | Yes | | +| MakeRecord | Yes | | +| MaxPgcnt | No | | +| MemMax | No | | +| Move | No | | +| Multiply | Yes | | +| MustBeInt | Yes | | +| Ne | Yes | | +| NewRowid | Yes | | +| Next | No | | +| NextAsync | Yes | | +| NextAwait | Yes | | +| Noop | Yes | | +| Not | Yes | | +| NotExists | Yes | | +| NotFound | No | | +| NotNull | Yes | | +| Null | Yes | | +| NullRow | Yes | | +| Once | No | | +| OpenAutoindex | No | | +| OpenEphemeral | No | | +| OpenPseudo | Yes | | +| OpenRead | Yes | | +| OpenReadAsync | Yes | | +| OpenWrite | No | | +| OpenWriteAsync | Yes | | +| OpenWriteAwait | Yes | | +| Or | Yes | | +| Pagecount | Partial| no temp databases | +| Param | No | | +| ParseSchema | No | | +| Permutation | No | | +| Prev | No | | +| PrevAsync | Yes | | +| PrevAwait | Yes | | +| Program | No | | +| ReadCookie | No | | +| Real | Yes | | +| RealAffinity | Yes | | +| Remainder | Yes | | +| ResetCount | No | | +| ResultRow | Yes | | +| Return | Yes | | +| Rewind | Yes | | +| RewindAsync | Yes | | +| RewindAwait | Yes | | +| RowData | No | | +| RowId | Yes | | +| RowKey | No | | +| RowSetAdd | No | | +| RowSetRead | No | | +| RowSetTest | No | | +| Rowid | Yes | | +| SCopy | No | | +| Savepoint | No | | +| Seek | No | | +| SeekGe | Yes | | +| SeekGt | Yes | | +| SeekLe | No | | +| SeekLt | No | | +| SeekRowid | Yes | | +| Sequence | No | | +| SetCookie | No | | +| ShiftLeft | Yes | | +| ShiftRight | Yes | | +| SoftNull | Yes | | +| Sort | No | | +| SorterCompare | No | | +| SorterData | Yes | | +| SorterInsert | Yes | | +| SorterNext | Yes | | +| SorterOpen | Yes | | +| SorterSort | Yes | | +| String | No | | +| String8 | Yes | | +| Subtract | Yes | | +| TableLock | No | | +| ToBlob | No | | +| ToInt | No | | +| ToNumeric | No | | +| ToReal | No | | +| ToText | No | | +| Trace | No | | +| Transaction | Yes | | +| VBegin | No | | +| VColumn | No | | +| VCreate | No | | +| VDestroy | No | | +| VFilter | No | | +| VNext | No | | +| VOpen | No | | +| VRename | No | | +| VUpdate | No | | +| Vacuum | No | | +| Variable | No | | +| VerifyCookie | No | | +| Yield | Yes | | +| ZeroOrNull | Yes | | + +## [SQLite journaling modes](https://www.sqlite.org/pragma.html#pragma_journal_mode) + +We currently don't have plan to support the rollback journal mode as it locks the database file during writes. +Therefore, all rollback-type modes (delete, truncate, persist, memory) are marked are `Not Needed` below. + +| Journal mode | Status | Comment | +|--------------|------------|--------------------------------| +| wal | Yes | | +| wal2 | No | experimental feature in sqlite | +| delete | Not Needed | | +| truncate | Not Needed | | +| persist | Not Needed | | +| memory | Not Needed | | ## Extensions @@ -581,7 +599,7 @@ Limbo has in-tree extensions. UUID's in Limbo are `blobs` by default. -| Function | Status | Comment | +| Function | Status | Comment | |-----------------------|--------|---------------------------------------------------------------| | uuid4() | Yes | UUID version 4 | | uuid4_str() | Yes | UUID v4 string alias `gen_random_uuid()` for PG compatibility | @@ -594,10 +612,75 @@ UUID's in Limbo are `blobs` by default. The `regexp` extension is compatible with [sqlean-regexp](https://github.com/nalgeon/sqlean/blob/main/docs/regexp.md). -| Function | Status | Comment | +| Function | Status | Comment | |------------------------------------------------|--------|---------| | regexp(pattern, source) | Yes | | | regexp_like(source, pattern) | Yes | | | regexp_substr(source, pattern) | Yes | | | regexp_capture(source, pattern[, n]) | No | | | regexp_replace(source, pattern, replacement) | No | | + +### Vector + +The `vector` extension is compatible with libSQL native vector search. + +| Function | Status | Comment | +|------------------------------------------------|--------|---------| +| vector(x) | Yes | | +| vector32(x) | Yes | | +| vector64(x) | Yes | | +| vector_extract(x) | Yes | | +| vector_distance_cos(x, y) | Yes | | + +### Time + +The `time` extension is compatible with [sqlean-time](https://github.com/nalgeon/sqlean/blob/main/docs/time.md). + + +| Function | Status | Comment | +| ------------------------------------------------------------------- | ------ | ---------------------------- | +| time_now() | Yes | | +| time_date(year, month, day[, hour, min, sec[, nsec[, offset_sec]]]) | Yes | offset_sec is not normalized | +| time_get_year(t) | Yes | | +| time_get_month(t) | Yes | | +| time_get_day(t) | Yes | | +| time_get_hour(t) | Yes | | +| time_get_minute(t) | Yes | | +| time_get_second(t) | Yes | | +| time_get_nano(t) | Yes | | +| time_get_weekday(t) | Yes | | +| time_get_yearday(t) | Yes | | +| time_get_isoyear(t) | Yes | | +| time_get_isoweek(t) | Yes | | +| time_get(t, field) | Yes | | +| time_unix(sec[, nsec]) | Yes | | +| time_milli(msec) | Yes | | +| time_micro(usec) | Yes | | +| time_nano(nsec) | Yes | | +| time_to_unix(t) | Yes | | +| time_to_milli(t) | Yes | | +| time_to_micro(t) | Yes | | +| time_to_nano(t) | Yes | | +| time_after(t, u) | Yes | | +| time_before(t, u) | Yes | | +| time_compare(t, u) | Yes | | +| time_equal(t, u) | Yes | | +| time_add(t, d) | Yes | | +| time_add_date(t, years[, months[, days]]) | Yes | | +| time_sub(t, u) | Yes | | +| time_since(t) | Yes | | +| time_until(t) | Yes | | +| time_trunc(t, field) | Yes | | +| time_trunc(t, d) | Yes | | +| time_round(t, d) | Yes | | +| time_fmt_iso(t[, offset_sec]) | Yes | | +| time_fmt_datetime(t[, offset_sec]) | Yes | | +| time_fmt_date(t[, offset_sec]) | Yes | | +| time_fmt_time(t[, offset_sec]) | Yes | | +| time_parse(s) | Yes | | +| dur_ns() | Yes | | +| dur_us() | Yes | | +| dur_ms() | Yes | | +| dur_s() | Yes | | +| dur_m() | Yes | | +| dur_h() | Yes | | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2f2b99922..cad33a8db 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,7 @@ We'd love to have you contribute to Limbo! This document is a quick helper to get you going. -## Getting started +## Getting Started Limbo is a rewrite of SQLite in Rust. If you are new to SQLite, the following articles and books are a good starting point: @@ -19,7 +19,47 @@ If you are new to Rust, the following books are recommended reading: Examples of contributing -* [How to contribute a SQL function implementation](docs/internals/functions.md) +* [How to contribute a SQL function implementation](docs/contributing/contributing_functions.md) + +To build and run `limbo` cli: + +```shell +cargo run --package limbo --bin limbo database.db +``` + +Run tests: + +```console +cargo test +``` + +Test coverage report: + +``` +cargo tarpaulin -o html +``` + +> [!NOTE] +> Generation of coverage report requires [tarpaulin](https://github.com/xd009642/tarpaulin) binary to be installed. +> You can install it with `cargo install cargo-tarpaulin` + +[//]: # (TODO remove the below tip when the bug is solved) + +> [!TIP] +> If coverage fails with "Test failed during run" error and all of the tests passed it might be the result of tarpaulin [bug](https://github.com/xd009642/tarpaulin/issues/1642). You can temporarily set [dynamic libraries linking manually](https://doc.rust-lang.org/cargo/reference/environment-variables.html#dynamic-library-paths) as a workaround, e.g. for linux `LD_LIBRARY_PATH="$(rustc --print=target-libdir)" cargo tarpaulin -o html`. + +Run benchmarks: + +```console +cargo bench +``` + +Run benchmarks and generate flamegraphs: + +```console +echo -1 | sudo tee /proc/sys/kernel/perf_event_paranoid +cargo bench --bench benchmark -- --profile-time=5 +``` ## Finding things to work on diff --git a/Cargo.lock b/Cargo.lock index 1fda860f2..a412add52 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,10 +24,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", - "getrandom", + "getrandom 0.2.15", "once_cell", "version_check", - "zerocopy", + "zerocopy 0.7.35", ] [[package]] @@ -60,7 +60,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "18a1e15a87b13ae79e04e07b3714fc41d5f6993dff11662fdbe0b207c6ad0fe0" dependencies = [ - "rand", + "rand 0.8.5", ] [[package]] @@ -224,6 +224,16 @@ dependencies = [ "serde", ] +[[package]] +name = "built" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c360505aed52b7ec96a3636c3f039d99103c37d1d9b4f7a8c743d3ea9ffcd03b" +dependencies = [ + "chrono", + "git2", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -260,6 +270,8 @@ version = "1.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8293772165d9345bdaaa39b45b2109591e63fe5e6fbc23c6ff930a048aa310b" dependencies = [ + "jobserver", + "libc", "shlex", ] @@ -451,7 +463,7 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "core_tester" -version = "0.0.13" +version = "0.0.14" dependencies = [ "anyhow", "assert_cmd", @@ -460,6 +472,8 @@ dependencies = [ "env_logger 0.10.2", "limbo_core", "log", + "rand 0.9.0", + "rand_chacha 0.9.0", "rexpect", "rusqlite", "rustyline", @@ -540,6 +554,16 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-skiplist" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df29de440c58ca2cc6e587ec3d22347551a32435fbde9d2bff64e78a9ffa151b" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -639,6 +663,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + [[package]] name = "doc-comment" version = "0.3.3" @@ -672,6 +707,16 @@ dependencies = [ "log", ] +[[package]] +name = "env_logger" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" +dependencies = [ + "log", + "regex", +] + [[package]] name = "env_logger" version = "0.10.2" @@ -806,6 +851,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + [[package]] name = "fragile" version = "2.0.0" @@ -935,16 +989,41 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.13.3+wasi-0.2.2", + "windows-targets 0.52.6", +] + [[package]] name = "gimli" version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[package]] +name = "git2" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724" +dependencies = [ + "bitflags 2.8.0", + "libc", + "libgit2-sys", + "log", + "url", +] + [[package]] name = "glob" version = "0.3.2" @@ -1042,6 +1121,145 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "indexmap" version = "2.7.0" @@ -1147,7 +1365,7 @@ checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "java-limbo" -version = "0.0.13" +version = "0.0.14" dependencies = [ "jni", "limbo_core", @@ -1176,6 +1394,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + [[package]] name = "js-sys" version = "0.3.77" @@ -1197,7 +1424,7 @@ dependencies = [ "itoa", "nom", "ordered-float", - "rand", + "rand 0.8.5", "ryu", "serde_json", ] @@ -1243,6 +1470,18 @@ version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +[[package]] +name = "libgit2-sys" +version = "0.17.0+1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10472326a8a6477c3c20a64547b0059e4b0d086869eee31e6d7da728a8eb7224" +dependencies = [ + "cc", + "libc", + "libz-sys", + "pkg-config", +] + [[package]] name = "libloading" version = "0.8.6" @@ -1285,6 +1524,18 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "libz-sys" +version = "1.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2d16453e800a8cf6dd2fc3eb4bc99b786a9b90c663b8559a5b1a041bf89e472" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "limbo" version = "0.0.13" @@ -1303,14 +1554,14 @@ dependencies = [ [[package]] name = "limbo-go" -version = "0.0.13" +version = "0.0.14" dependencies = [ "limbo_core", ] [[package]] name = "limbo-wasm" -version = "0.0.13" +version = "0.0.14" dependencies = [ "console_error_panic_hook", "js-sys", @@ -1322,14 +1573,16 @@ dependencies = [ [[package]] name = "limbo_core" -version = "0.0.13" +version = "0.0.14" dependencies = [ + "built", "bumpalo", "cfg_block", "chrono", "criterion", + "crossbeam-skiplist", "fallible-iterator 0.3.0", - "getrandom", + "getrandom 0.2.15", "hex", "indexmap", "io-uring", @@ -1339,15 +1592,21 @@ dependencies = [ "libloading", "limbo_ext", "limbo_macros", + "limbo_percentile", + "limbo_regexp", + "limbo_time", + "limbo_uuid", + "limbo_vector", "log", "miette", "mimalloc", "mockall", + "parking_lot", "pest", "pest_derive", "polling", "pprof", - "rand", + "rand 0.8.5", "regex", "regex-syntax", "rstest", @@ -1356,14 +1615,15 @@ dependencies = [ "serde", "sieve-cache", "sqlite3-parser", + "strum", "tempfile", "thiserror 1.0.69", - "uuid", + "tracing", ] [[package]] name = "limbo_ext" -version = "0.0.13" +version = "0.0.14" dependencies = [ "limbo_macros", "log", @@ -1371,7 +1631,7 @@ dependencies = [ [[package]] name = "limbo_libsql" -version = "0.0.13" +version = "0.0.14" dependencies = [ "limbo_core", "thiserror 2.0.11", @@ -1380,7 +1640,7 @@ dependencies = [ [[package]] name = "limbo_macros" -version = "0.0.13" +version = "0.0.14" dependencies = [ "proc-macro2", "quote", @@ -1389,23 +1649,25 @@ dependencies = [ [[package]] name = "limbo_percentile" -version = "0.0.13" +version = "0.0.14" dependencies = [ "limbo_ext", + "mimalloc", ] [[package]] name = "limbo_regexp" -version = "0.0.13" +version = "0.0.14" dependencies = [ "limbo_ext", "log", + "mimalloc", "regex", ] [[package]] name = "limbo_sim" -version = "0.0.13" +version = "0.0.14" dependencies = [ "anarchist-readable-name-generator-lib", "clap", @@ -1413,10 +1675,8 @@ dependencies = [ "limbo_core", "log", "notify", - "rand", - "rand_chacha", - "regex", - "regex-syntax", + "rand 0.8.5", + "rand_chacha 0.3.1", "serde", "serde_json", "tempfile", @@ -1424,7 +1684,7 @@ dependencies = [ [[package]] name = "limbo_sqlite3" -version = "0.0.13" +version = "0.0.14" dependencies = [ "env_logger 0.11.6", "libc", @@ -1432,21 +1692,49 @@ dependencies = [ "log", ] +[[package]] +name = "limbo_time" +version = "0.0.14" +dependencies = [ + "chrono", + "limbo_ext", + "mimalloc", + "strum", + "strum_macros", + "thiserror 2.0.11", +] + [[package]] name = "limbo_uuid" -version = "0.0.13" +version = "0.0.14" dependencies = [ "limbo_ext", - "log", + "mimalloc", "uuid", ] +[[package]] +name = "limbo_vector" +version = "0.0.14" +dependencies = [ + "limbo_ext", + "quickcheck", + "quickcheck_macros", + "rand 0.8.5", +] + [[package]] name = "linux-raw-sys" version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +[[package]] +name = "litemap" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" + [[package]] name = "lock_api" version = "0.4.12" @@ -1550,7 +1838,7 @@ checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", "log", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] @@ -1742,6 +2030,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + [[package]] name = "pest" version = "2.7.15" @@ -1813,7 +2107,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", - "rand", + "rand 0.8.5", ] [[package]] @@ -1922,7 +2216,7 @@ version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" dependencies = [ - "zerocopy", + "zerocopy 0.7.35", ] [[package]] @@ -1963,7 +2257,7 @@ dependencies = [ [[package]] name = "py-limbo" -version = "0.0.13" +version = "0.0.14" dependencies = [ "anyhow", "limbo_core", @@ -2045,6 +2339,28 @@ dependencies = [ "memchr", ] +[[package]] +name = "quickcheck" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" +dependencies = [ + "env_logger 0.8.4", + "log", + "rand 0.8.5", +] + +[[package]] +name = "quickcheck_macros" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b22a693222d716a9587786f37ac3f6b4faedb5b80c23914e7303ff5a1d8016e9" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "quote" version = "1.0.38" @@ -2071,8 +2387,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.0", + "zerocopy 0.8.14", ] [[package]] @@ -2082,7 +2409,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.0", ] [[package]] @@ -2091,7 +2428,17 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.15", +] + +[[package]] +name = "rand_core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b08f3c9802962f7e1b25113931d94f43ed9725bebc59db9d0c3e9a23b67e15ff" +dependencies = [ + "getrandom 0.3.1", + "zerocopy 0.8.14", ] [[package]] @@ -2129,7 +2476,7 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ - "getrandom", + "getrandom 0.2.15", "libredox", "thiserror 1.0.69", ] @@ -2429,6 +2776,8 @@ dependencies = [ "phf", "phf_codegen", "phf_shared", + "strum", + "strum_macros", "uncased", ] @@ -2456,6 +2805,28 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.96", +] + [[package]] name = "supports-color" version = "3.0.2" @@ -2522,6 +2893,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + [[package]] name = "target-lexicon" version = "0.12.16" @@ -2536,7 +2918,7 @@ checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" dependencies = [ "cfg-if", "fastrand", - "getrandom", + "getrandom 0.2.15", "once_cell", "rustix", "windows-sys 0.59.0", @@ -2617,6 +2999,16 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tinytemplate" version = "1.2.1" @@ -2663,14 +3055,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + [[package]] name = "tracing-core" version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", +] [[package]] name = "typenum" @@ -2723,6 +3130,29 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -2735,7 +3165,7 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "744018581f9a3454a9e15beb8a33b017183f1e7c0cd170232a2d1453b23a51c4" dependencies = [ - "getrandom", + "getrandom 0.2.15", ] [[package]] @@ -2775,6 +3205,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.13.3+wasi-0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -3110,6 +3549,51 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags 2.8.0", +] + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.7.35" @@ -3117,7 +3601,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "byteorder", - "zerocopy-derive", + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a367f292d93d4eab890745e75a778da40909cab4d6ff8173693812f79c4a2468" +dependencies = [ + "zerocopy-derive 0.8.14", ] [[package]] @@ -3130,3 +3623,57 @@ dependencies = [ "quote", "syn 2.0.96", ] + +[[package]] +name = "zerocopy-derive" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3931cb58c62c13adec22e38686b559c86a30565e16ad6e8510a337cedc611e1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", + "synstructure", +] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] diff --git a/Cargo.toml b/Cargo.toml index 0ffbdf6ac..85b8c46d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,11 +18,13 @@ members = [ "sqlite3", "tests", "extensions/percentile", + "extensions/vector", + "extensions/time", ] exclude = ["perf/latency/limbo"] [workspace.package] -version = "0.0.13" +version = "0.0.14" authors = ["the Limbo authors"] edition = "2021" license = "MIT" diff --git a/Makefile b/Makefile index 109a3f147..e1fdf6d29 100644 --- a/Makefile +++ b/Makefile @@ -62,11 +62,11 @@ limbo-wasm: cargo build --package limbo-wasm --target wasm32-wasi .PHONY: limbo-wasm -test: limbo test-compat test-sqlite3 test-shell test-extensions +test: limbo test-compat test-vector test-sqlite3 test-shell test-extensions .PHONY: test test-extensions: limbo - cargo build --package limbo_uuid + cargo build --package limbo_regexp ./testing/extensions.py .PHONY: test-extensions @@ -78,6 +78,14 @@ test-compat: SQLITE_EXEC=$(SQLITE_EXEC) ./testing/all.test .PHONY: test-compat +test-vector: + SQLITE_EXEC=$(SQLITE_EXEC) ./testing/vector.test +.PHONY: test-vector + +test-time: + SQLITE_EXEC=$(SQLITE_EXEC) ./testing/time.test +.PHONY: test-time + test-sqlite3: limbo-c LIBS="$(SQLITE_LIB)" HEADERS="$(SQLITE_LIB_HEADERS)" make -C sqlite3/tests test .PHONY: test-sqlite3 diff --git a/README.md b/README.md index 85fb2f1ea..654ba3193 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@
-
-
+ - Limbo is a work-in-progress, in-process OLTP database management system, compatible with SQLite. + Limbo is a project to build the modern evolution of SQLite.
--- ## Features -Limbo is an in-process OLTP database engine library that has: +Limbo is a _work-in-progress_, in-process OLTP database engine library written in Rust that has: * **Asynchronous I/O** support on Linux with `io_uring` * **SQLite compatibility** [[doc](COMPAT.md)] for SQL dialect, file formats, and the C API -* **Language bindings** for JavaScript/WebAssembly, Rust, Python, and Java +* **Language bindings** for JavaScript/WebAssembly, Rust, Go, Python, and [Java](bindings/java) * **OS support** for Linux, macOS, and Windows ## Getting Started -### CLI +### 💻 Command Line -Install `limbo` with: +You can install the latest `limbo` release with: ```shell curl --proto '=https' --tlsv1.2 -LsSf \ https://github.com/tursodatabase/limbo/releases/latest/download/limbo-installer.sh | sh ``` -Then use the SQL shell to create and query a database: +Then launch the shell to execute SQL statements: ```console -$ limbo database.db -Limbo v0.0.6 +Limbo Enter ".help" for usage hints. +Connected to a transient in-memory database. +Use ".open FILENAME" to reopen on a persistent database limbo> CREATE TABLE users (id INT PRIMARY KEY, username TEXT); limbo> INSERT INTO users VALUES (1, 'alice'); limbo> INSERT INTO users VALUES (2, 'bob'); @@ -56,7 +57,13 @@ limbo> SELECT * FROM users; 2|bob ``` -### JavaScript (wip) +You can also build and run the latest development version with: + +```shell +cargo run +``` + +### ✨ JavaScript (wip) Installation: @@ -75,7 +82,7 @@ const users = stmt.all(); console.log(users); ``` -### Python (wip) +### 🐍 Python (wip) ```console pip install pylimbo @@ -92,63 +99,60 @@ res = cur.execute("SELECT * FROM users") print(res.fetchone()) ``` -## Developing +### 🐹 Go (wip) -Build and run `limbo` cli: - -```shell -cargo run --package limbo --bin limbo database.db +1. Clone the repository +2. Build the library and set your LD_LIBRARY_PATH to include limbo's target directory +```console +cargo build --package limbo-go +export LD_LIBRARY_PATH=/path/to/limbo/target/debug:$LD_LIBRARY_PATH ``` - -Run tests: +3. Use the driver ```console -cargo test +go get github.com/tursodatabase/limbo +go install github.com/tursodatabase/limbo ``` -Test coverage report: +Example usage: +```go +import ( + "database/sql" + _"github.com/tursodatabase/limbo" +) -``` -cargo tarpaulin -o html +conn, _ = sql.Open("sqlite3", "sqlite.db") +defer conn.Close() + +stmt, _ := conn.Prepare("select * from users") +defer stmt.Close() + +rows, _ = stmt.Query() +for rows.Next() { + var id int + var username string + _ := rows.Scan(&id, &username) + fmt.Printf("User: ID: %d, Username: %s\n", id, username) +} ``` -> [!NOTE] -> Generation of coverage report requires [tarpaulin](https://github.com/xd009642/tarpaulin) binary to be installed. -> You can install it with `cargo install cargo-tarpaulin` +## Contributing -[//]: # (TODO remove the below tip when the bug is solved) - -> [!TIP] -> If coverage fails with "Test failed during run" error and all of the tests passed it might be the result of tarpaulin [bug](https://github.com/xd009642/tarpaulin/issues/1642). You can temporarily set [dynamic libraries linking manually](https://doc.rust-lang.org/cargo/reference/environment-variables.html#dynamic-library-paths) as a workaround, e.g. for linux `LD_LIBRARY_PATH="$(rustc --print=target-libdir)" cargo tarpaulin -o html`. - -Run benchmarks: - -```console -cargo bench -``` - -Run benchmarks and generate flamegraphs: - -```console -echo -1 | sudo tee /proc/sys/kernel/perf_event_paranoid -cargo bench --bench benchmark -- --profile-time=5 -``` +We'd love to have you contribute to Limbo! Please check out the [contribution guide] to get started. ## FAQ -### How is Limbo different from libSQL? +### How is Limbo different from Turso's libSQL? -Limbo is a research project to build a SQLite compatible in-process database in Rust with native async support. The libSQL project, on the other hand, is an open source, open contribution fork of SQLite, with focus on production features such as replication, backups, encryption, and so on. There is no hard dependency between the two projects. Of course, if Limbo becomes widely successful, we might consider merging with libSQL, but that is something that will be decided in the future. +Limbo is a project to build the modern evolution of SQLite in Rust, with a strong open contribution focus and features like native async support, vector search, and more. The libSQL project is also an attempt to evolve SQLite in a similar direction, but through a fork rather than a rewrite. + +Rewriting SQLite in Rust started as an unassuming experiment, and due to its incredible success, replaces libSQL as our intended direction. At this point, libSQL is production ready, Limbo is not - although it is evolving rapidly. As the project start to near production readiness, we plan to rename it to just "Turso". More details [here](https://turso.tech/blog/we-will-rewrite-sqlite-and-we-are-going-all-in). ## Publications * Pekka Enberg, Sasu Tarkoma, Jon Crowcroft Ashwin Rao (2024). Serverless Runtime / Database Co-Design With Asynchronous I/O. In _EdgeSys ‘24_. [[PDF]](https://penberg.org/papers/penberg-edgesys24.pdf) * Pekka Enberg, Sasu Tarkoma, and Ashwin Rao (2023). Towards Database and Serverless Runtime Co-Design. In _CoNEXT-SW ’23_. [[PDF](https://penberg.org/papers/penberg-conext-sw-23.pdf)] [[Slides](https://penberg.org/papers/penberg-conext-sw-23-slides.pdf)] -## Contributing - -We'd love to have you contribute to Limbo! Check out the [contribution guide] to get started. - ## License This project is licensed under the [MIT license]. diff --git a/bindings/go/README.md b/bindings/go/README.md new file mode 100644 index 000000000..ab50140bb --- /dev/null +++ b/bindings/go/README.md @@ -0,0 +1,71 @@ +# Limbo driver for Go's `database/sql` library + + +**NOTE:** this is currently __heavily__ W.I.P and is not yet in a usable state. + +This driver uses the awesome [purego](https://github.com/ebitengine/purego) library to call C (in this case Rust with C ABI) functions from Go without the use of `CGO`. + + +## To use: (_UNSTABLE_ testing or development purposes only) + +### Linux | MacOS + +_All commands listed are relative to the bindings/go directory in the limbo repository_ + +``` +cargo build --package limbo-go + +# Your LD_LIBRARY_PATH environment variable must include limbo's `target/debug` directory + +export LD_LIBRARY_PATH="/path/to/limbo/target/debug:$LD_LIBRARY_PATH" + +``` + +## Windows + +``` +cargo build --package limbo-go + +# You must add limbo's `target/debug` directory to your PATH +# or you could built + copy the .dll to a location in your PATH +# or just the CWD of your go module + +cp path\to\limbo\target\debug\lib_limbo_go.dll . + +go test + +``` +**Temporarily** you may have to clone the limbo repository and run: + +`go mod edit -replace github.com/tursodatabase/limbo=/path/to/limbo/bindings/go` + +```go +import ( + "fmt" + "database/sql" + _"github.com/tursodatabase/limbo" +) + +func main() { + conn, err := sql.Open("sqlite3", ":memory:") + if err != nil { + fmt.Printf("Error: %v\n", err) + os.Exit(1) + } + sql := "CREATE table go_limbo (foo INTEGER, bar TEXT)" + _ = conn.Exec(sql) + + sql = "INSERT INTO go_limbo (foo, bar) values (?, ?)" + stmt, _ := conn.Prepare(sql) + defer stmt.Close() + _ = stmt.Exec(42, "limbo") + rows, _ := conn.Query("SELECT * from go_limbo") + defer rows.Close() + for rows.Next() { + var a int + var b string + _ = rows.Scan(&a, &b) + fmt.Printf("%d, %s", a, b) + } +} +``` diff --git a/bindings/go/connection.go b/bindings/go/connection.go new file mode 100644 index 000000000..2d7a27e8b --- /dev/null +++ b/bindings/go/connection.go @@ -0,0 +1,142 @@ +package limbo + +import ( + "database/sql" + "database/sql/driver" + "errors" + "fmt" + "sync" + + "github.com/ebitengine/purego" +) + +func init() { + err := ensureLibLoaded() + if err != nil { + panic(err) + } + sql.Register(driverName, &limboDriver{}) +} + +type limboDriver struct { + sync.Mutex +} + +var ( + libOnce sync.Once + limboLib uintptr + loadErr error + dbOpen func(string) uintptr + dbClose func(uintptr) uintptr + connPrepare func(uintptr, string) uintptr + connGetError func(uintptr) uintptr + freeBlobFunc func(uintptr) + freeStringFunc func(uintptr) + rowsGetColumns func(uintptr) int32 + rowsGetColumnName func(uintptr, int32) uintptr + rowsGetValue func(uintptr, int32) uintptr + rowsGetError func(uintptr) uintptr + closeRows func(uintptr) uintptr + rowsNext func(uintptr) uintptr + stmtQuery func(stmtPtr uintptr, argsPtr uintptr, argCount uint64) uintptr + stmtExec func(stmtPtr uintptr, argsPtr uintptr, argCount uint64, changes uintptr) int32 + stmtParamCount func(uintptr) int32 + stmtGetError func(uintptr) uintptr + stmtClose func(uintptr) int32 +) + +// Register all the symbols on library load +func ensureLibLoaded() error { + libOnce.Do(func() { + limboLib, loadErr = loadLibrary() + if loadErr != nil { + return + } + purego.RegisterLibFunc(&dbOpen, limboLib, FfiDbOpen) + purego.RegisterLibFunc(&dbClose, limboLib, FfiDbClose) + purego.RegisterLibFunc(&connPrepare, limboLib, FfiDbPrepare) + purego.RegisterLibFunc(&connGetError, limboLib, FfiDbGetError) + purego.RegisterLibFunc(&freeBlobFunc, limboLib, FfiFreeBlob) + purego.RegisterLibFunc(&freeStringFunc, limboLib, FfiFreeCString) + purego.RegisterLibFunc(&rowsGetColumns, limboLib, FfiRowsGetColumns) + purego.RegisterLibFunc(&rowsGetColumnName, limboLib, FfiRowsGetColumnName) + purego.RegisterLibFunc(&rowsGetValue, limboLib, FfiRowsGetValue) + purego.RegisterLibFunc(&closeRows, limboLib, FfiRowsClose) + purego.RegisterLibFunc(&rowsNext, limboLib, FfiRowsNext) + purego.RegisterLibFunc(&rowsGetError, limboLib, FfiDbGetError) + purego.RegisterLibFunc(&stmtQuery, limboLib, FfiStmtQuery) + purego.RegisterLibFunc(&stmtExec, limboLib, FfiStmtExec) + purego.RegisterLibFunc(&stmtParamCount, limboLib, FfiStmtParameterCount) + purego.RegisterLibFunc(&stmtGetError, limboLib, FfiDbGetError) + purego.RegisterLibFunc(&stmtClose, limboLib, FfiStmtClose) + }) + return loadErr +} + +func (d *limboDriver) Open(name string) (driver.Conn, error) { + d.Lock() + conn, err := openConn(name) + d.Unlock() + if err != nil { + return nil, err + } + return conn, nil +} + +type limboConn struct { + sync.Mutex + ctx uintptr +} + +func openConn(dsn string) (*limboConn, error) { + ctx := dbOpen(dsn) + if ctx == 0 { + return nil, fmt.Errorf("failed to open database for dsn=%q", dsn) + } + return &limboConn{ + sync.Mutex{}, + ctx, + }, loadErr +} + +func (c *limboConn) Close() error { + if c.ctx == 0 { + return nil + } + c.Lock() + dbClose(c.ctx) + c.Unlock() + c.ctx = 0 + return nil +} + +func (c *limboConn) getError() error { + if c.ctx == 0 { + return errors.New("connection closed") + } + err := connGetError(c.ctx) + if err == 0 { + return nil + } + defer freeStringFunc(err) + cpy := fmt.Sprintf("%s", GoString(err)) + return errors.New(cpy) +} + +func (c *limboConn) Prepare(query string) (driver.Stmt, error) { + if c.ctx == 0 { + return nil, errors.New("connection closed") + } + c.Lock() + defer c.Unlock() + stmtPtr := connPrepare(c.ctx, query) + if stmtPtr == 0 { + return nil, c.getError() + } + return newStmt(stmtPtr, query), nil +} + +// begin is needed to implement driver.Conn.. for now not implemented +func (c *limboConn) Begin() (driver.Tx, error) { + return nil, errors.New("transactions not implemented") +} diff --git a/bindings/go/go.mod b/bindings/go/go.mod index 589b9a0e3..a9145591b 100644 --- a/bindings/go/go.mod +++ b/bindings/go/go.mod @@ -1,8 +1,8 @@ -module limbo +module github.com/tursodatabase/limbo go 1.23.4 require ( github.com/ebitengine/purego v0.8.2 - golang.org/x/sys/windows v0.29.0 + golang.org/x/sys v0.29.0 ) diff --git a/bindings/go/limbo.go b/bindings/go/limbo.go deleted file mode 100644 index 4011fb1ac..000000000 --- a/bindings/go/limbo.go +++ /dev/null @@ -1,141 +0,0 @@ -package limbo - -import ( - "database/sql" - "database/sql/driver" - "errors" - "fmt" - "log/slog" - "os" - "runtime" - "sync" - "unsafe" - - "github.com/ebitengine/purego" - "golang.org/x/sys/windows" -) - -const limbo = "../../target/debug/lib_limbo_go" -const driverName = "limbo" - -var limboLib uintptr - -func getSystemLibrary() error { - switch runtime.GOOS { - case "darwin": - slib, err := purego.Dlopen(fmt.Sprintf("%s.dylib", limbo), purego.RTLD_LAZY) - if err != nil { - return err - } - limboLib = slib - case "linux": - slib, err := purego.Dlopen(fmt.Sprintf("%s.so", limbo), purego.RTLD_LAZY) - if err != nil { - return err - } - limboLib = slib - case "windows": - slib, err := windows.LoadLibrary(fmt.Sprintf("%s.dll", limbo)) - if err != nil { - return err - } - limboLib = slib - default: - panic(fmt.Errorf("GOOS=%s is not supported", runtime.GOOS)) - } - return nil -} - -func init() { - err := getSystemLibrary() - if err != nil { - slog.Error("Error opening limbo library: ", err) - os.Exit(1) - } - sql.Register(driverName, &limboDriver{}) -} - -type limboDriver struct{} - -func (d limboDriver) Open(name string) (driver.Conn, error) { - return openConn(name) -} - -func toCString(s string) uintptr { - b := append([]byte(s), 0) - return uintptr(unsafe.Pointer(&b[0])) -} - -// helper to register an FFI function in the lib_limbo_go library -func getFfiFunc(ptr interface{}, name string) { - purego.RegisterLibFunc(&ptr, limboLib, name) -} - -type limboConn struct { - ctx uintptr - sync.Mutex - prepare func(uintptr, uintptr) uintptr -} - -func newConn(ctx uintptr) *limboConn { - var prepare func(uintptr, uintptr) uintptr - getFfiFunc(&prepare, FfiDbPrepare) - return &limboConn{ - ctx, - sync.Mutex{}, - prepare, - } -} - -func openConn(dsn string) (*limboConn, error) { - var dbOpen func(uintptr) uintptr - getFfiFunc(&dbOpen, FfiDbOpen) - - cStr := toCString(dsn) - defer freeCString(cStr) - - ctx := dbOpen(cStr) - if ctx == 0 { - return nil, fmt.Errorf("failed to open database for dsn=%q", dsn) - } - return &limboConn{ctx: ctx}, nil -} - -func (c *limboConn) Close() error { - if c.ctx == 0 { - return nil - } - var dbClose func(uintptr) uintptr - getFfiFunc(&dbClose, FfiDbClose) - - dbClose(c.ctx) - c.ctx = 0 - return nil -} - -func (c *limboConn) Prepare(query string) (driver.Stmt, error) { - if c.ctx == 0 { - return nil, errors.New("connection closed") - } - if c.prepare == nil { - var dbPrepare func(uintptr, uintptr) uintptr - getFfiFunc(&dbPrepare, FfiDbPrepare) - c.prepare = dbPrepare - } - qPtr := toCString(query) - stmtPtr := c.prepare(c.ctx, qPtr) - freeCString(qPtr) - - if stmtPtr == 0 { - return nil, fmt.Errorf("prepare failed: %q", query) - } - return &limboStmt{ - ctx: stmtPtr, - sql: query, - }, nil -} - -// begin is needed to implement driver.Conn.. for now not implemented -func (c *limboConn) Begin() (driver.Tx, error) { - return nil, errors.New("transactions not implemented") -} diff --git a/bindings/go/limbo_test.go b/bindings/go/limbo_test.go new file mode 100644 index 000000000..31b1fc3b4 --- /dev/null +++ b/bindings/go/limbo_test.go @@ -0,0 +1,323 @@ +package limbo_test + +import ( + "database/sql" + "fmt" + "log" + "testing" + + _ "github.com/tursodatabase/limbo" +) + +var conn *sql.DB +var connErr error + +func TestMain(m *testing.M) { + conn, connErr = sql.Open("sqlite3", ":memory:") + if connErr != nil { + panic(connErr) + } + defer conn.Close() + err := createTable(conn) + if err != nil { + log.Fatalf("Error creating table: %v", err) + } + m.Run() +} + +func TestInsertData(t *testing.T) { + err := insertData(conn) + if err != nil { + t.Fatalf("Error inserting data: %v", err) + } +} + +func TestQuery(t *testing.T) { + query := "SELECT * FROM test;" + stmt, err := conn.Prepare(query) + if err != nil { + t.Fatalf("Error preparing query: %v", err) + } + defer stmt.Close() + + rows, err := stmt.Query() + if err != nil { + t.Fatalf("Error executing query: %v", err) + } + defer rows.Close() + + expectedCols := []string{"foo", "bar", "baz"} + cols, err := rows.Columns() + if err != nil { + t.Fatalf("Error getting columns: %v", err) + } + if len(cols) != len(expectedCols) { + t.Fatalf("Expected %d columns, got %d", len(expectedCols), len(cols)) + } + for i, col := range cols { + if col != expectedCols[i] { + t.Errorf("Expected column %d to be %s, got %s", i, expectedCols[i], col) + } + } + var i = 1 + for rows.Next() { + var a int + var b string + var c []byte + err = rows.Scan(&a, &b, &c) + if err != nil { + t.Fatalf("Error scanning row: %v", err) + } + if a != i || b != rowsMap[i] || !slicesAreEq(c, []byte(rowsMap[i])) { + t.Fatalf("Expected %d, %s, %s, got %d, %s, %s", i, rowsMap[i], rowsMap[i], a, b, string(c)) + } + fmt.Println("RESULTS: ", a, b, string(c)) + i++ + } + + if err = rows.Err(); err != nil { + t.Fatalf("Row iteration error: %v", err) + } + +} + +func TestFunctions(t *testing.T) { + insert := "INSERT INTO test (foo, bar, baz) VALUES (?, ?, zeroblob(?));" + stmt, err := conn.Prepare(insert) + if err != nil { + t.Fatalf("Error preparing statement: %v", err) + } + _, err = stmt.Exec(60, "TestFunction", 400) + if err != nil { + t.Fatalf("Error executing statment with arguments: %v", err) + } + stmt.Close() + stmt, err = conn.Prepare("SELECT baz FROM test where foo = ?") + if err != nil { + t.Fatalf("Error preparing select stmt: %v", err) + } + defer stmt.Close() + rows, err := stmt.Query(60) + if err != nil { + t.Fatalf("Error executing select stmt: %v", err) + } + defer rows.Close() + for rows.Next() { + var b []byte + err = rows.Scan(&b) + if err != nil { + t.Fatalf("Error scanning row: %v", err) + } + if len(b) != 400 { + t.Fatalf("Expected 100 bytes, got %d", len(b)) + } + } + sql := "SELECT uuid4_str();" + stmt, err = conn.Prepare(sql) + if err != nil { + t.Fatalf("Error preparing statement: %v", err) + } + defer stmt.Close() + rows, err = stmt.Query() + if err != nil { + t.Fatalf("Error executing query: %v", err) + } + defer rows.Close() + var i int + for rows.Next() { + var b string + err = rows.Scan(&b) + if err != nil { + t.Fatalf("Error scanning row: %v", err) + } + if len(b) != 36 { + t.Fatalf("Expected 36 bytes, got %d", len(b)) + } + i++ + fmt.Printf("uuid: %s\n", b) + } + if i != 1 { + t.Fatalf("Expected 1 row, got %d", i) + } + fmt.Println("zeroblob + uuid functions passed") +} + +func TestDuplicateConnection(t *testing.T) { + newConn, err := sql.Open("sqlite3", ":memory:") + if err != nil { + t.Fatalf("Error opening new connection: %v", err) + } + err = createTable(newConn) + if err != nil { + t.Fatalf("Error creating table: %v", err) + } + err = insertData(newConn) + if err != nil { + t.Fatalf("Error inserting data: %v", err) + } + query := "SELECT * FROM test;" + rows, err := newConn.Query(query) + if err != nil { + t.Fatalf("Error executing query: %v", err) + } + defer rows.Close() + for rows.Next() { + var a int + var b string + var c []byte + err = rows.Scan(&a, &b, &c) + if err != nil { + t.Fatalf("Error scanning row: %v", err) + } + fmt.Println("RESULTS: ", a, b, string(c)) + } +} + +func TestDuplicateConnection2(t *testing.T) { + newConn, err := sql.Open("sqlite3", ":memory:") + if err != nil { + t.Fatalf("Error opening new connection: %v", err) + } + sql := "CREATE TABLE test (foo INTEGER, bar INTEGER, baz BLOB);" + newConn.Exec(sql) + sql = "INSERT INTO test (foo, bar, baz) VALUES (?, ?, uuid4());" + stmt, err := newConn.Prepare(sql) + stmt.Exec(242345, 2342434) + defer stmt.Close() + query := "SELECT * FROM test;" + rows, err := newConn.Query(query) + if err != nil { + t.Fatalf("Error executing query: %v", err) + } + defer rows.Close() + for rows.Next() { + var a int + var b int + var c []byte + err = rows.Scan(&a, &b, &c) + if err != nil { + t.Fatalf("Error scanning row: %v", err) + } + fmt.Println("RESULTS: ", a, b, string(c)) + if len(c) != 16 { + t.Fatalf("Expected 16 bytes, got %d", len(c)) + } + } +} + +func TestConnectionError(t *testing.T) { + newConn, err := sql.Open("sqlite3", ":memory:") + if err != nil { + t.Fatalf("Error opening new connection: %v", err) + } + sql := "CREATE TABLE test (foo INTEGER, bar INTEGER, baz BLOB);" + newConn.Exec(sql) + sql = "INSERT INTO test (foo, bar, baz) VALUES (?, ?, notafunction(?));" + _, err = newConn.Prepare(sql) + if err == nil { + t.Fatalf("Expected error, got nil") + } + expectedErr := "Parse error: unknown function notafunction" + if err.Error() != expectedErr { + t.Fatalf("Error test failed, expected: %s, found: %v", expectedErr, err) + } + fmt.Println("Connection error test passed") +} + +func TestStatementError(t *testing.T) { + newConn, err := sql.Open("sqlite3", ":memory:") + if err != nil { + t.Fatalf("Error opening new connection: %v", err) + } + sql := "CREATE TABLE test (foo INTEGER, bar INTEGER, baz BLOB);" + newConn.Exec(sql) + sql = "INSERT INTO test (foo, bar, baz) VALUES (?, ?, ?);" + stmt, err := newConn.Prepare(sql) + if err != nil { + t.Fatalf("Error preparing statement: %v", err) + } + _, err = stmt.Exec(1, 2) + if err == nil { + t.Fatalf("Expected error, got nil") + } + if err.Error() != "sql: expected 3 arguments, got 2" { + t.Fatalf("Unexpected : %v\n", err) + } + fmt.Println("Statement error test passed") +} + +func TestDriverRowsErrorMessages(t *testing.T) { + db, err := sql.Open("sqlite3", ":memory:") + if err != nil { + t.Fatalf("failed to open database: %v", err) + } + defer db.Close() + + _, err = db.Exec("CREATE TABLE test (id INTEGER, name TEXT)") + if err != nil { + t.Fatalf("failed to create table: %v", err) + } + + _, err = db.Exec("INSERT INTO test (id, name) VALUES (?, ?)", 1, "Alice") + if err != nil { + t.Fatalf("failed to insert row: %v", err) + } + + rows, err := db.Query("SELECT id, name FROM test") + if err != nil { + t.Fatalf("failed to query table: %v", err) + } + + if !rows.Next() { + t.Fatalf("expected at least one row") + } + var id int + var name string + err = rows.Scan(&name, &id) + if err == nil { + t.Fatalf("expected error scanning wrong type: %v", err) + } + t.Log("Rows error behavior test passed") +} + +func slicesAreEq(a, b []byte) bool { + if len(a) != len(b) { + fmt.Printf("LENGTHS NOT EQUAL: %d != %d\n", len(a), len(b)) + return false + } + for i := range a { + if a[i] != b[i] { + fmt.Printf("SLICES NOT EQUAL: %v != %v\n", a, b) + return false + } + } + return true +} + +var rowsMap = map[int]string{1: "hello", 2: "world", 3: "foo", 4: "bar", 5: "baz"} + +func createTable(conn *sql.DB) error { + insert := "CREATE TABLE test (foo INT, bar TEXT, baz BLOB);" + stmt, err := conn.Prepare(insert) + if err != nil { + return err + } + defer stmt.Close() + _, err = stmt.Exec() + return err +} + +func insertData(conn *sql.DB) error { + for i := 1; i <= 5; i++ { + insert := "INSERT INTO test (foo, bar, baz) VALUES (?, ?, ?);" + stmt, err := conn.Prepare(insert) + if err != nil { + return err + } + defer stmt.Close() + if _, err = stmt.Exec(i, rowsMap[i], []byte(rowsMap[i])); err != nil { + return err + } + } + return nil +} diff --git a/bindings/go/limbo_unix.go b/bindings/go/limbo_unix.go new file mode 100644 index 000000000..8ab911416 --- /dev/null +++ b/bindings/go/limbo_unix.go @@ -0,0 +1,45 @@ +//go:build linux || darwin + +package limbo + +import ( + "fmt" + "os" + "path/filepath" + "runtime" + "strings" + + "github.com/ebitengine/purego" +) + +func loadLibrary() (uintptr, error) { + var libraryName string + switch runtime.GOOS { + case "darwin": + libraryName = fmt.Sprintf("%s.dylib", libName) + case "linux": + libraryName = fmt.Sprintf("%s.so", libName) + default: + return 0, fmt.Errorf("GOOS=%s is not supported", runtime.GOOS) + } + + libPath := os.Getenv("LD_LIBRARY_PATH") + paths := strings.Split(libPath, ":") + cwd, err := os.Getwd() + if err != nil { + return 0, err + } + paths = append(paths, cwd) + + for _, path := range paths { + libPath := filepath.Join(path, libraryName) + if _, err := os.Stat(libPath); err == nil { + slib, dlerr := purego.Dlopen(libPath, purego.RTLD_NOW|purego.RTLD_GLOBAL) + if dlerr != nil { + return 0, fmt.Errorf("failed to load library at %s: %w", libPath, dlerr) + } + return slib, nil + } + } + return 0, fmt.Errorf("%s library not found in LD_LIBRARY_PATH or CWD", libName) +} diff --git a/bindings/go/limbo_windows.go b/bindings/go/limbo_windows.go new file mode 100644 index 000000000..d56381176 --- /dev/null +++ b/bindings/go/limbo_windows.go @@ -0,0 +1,36 @@ +//go:build windows + +package limbo + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "golang.org/x/sys/windows" +) + +func loadLibrary() (uintptr, error) { + libName := fmt.Sprintf("%s.dll", libName) + pathEnv := os.Getenv("PATH") + paths := strings.Split(pathEnv, ";") + + cwd, err := os.Getwd() + if err != nil { + return 0, err + } + paths = append(paths, cwd) + for _, path := range paths { + dllPath := filepath.Join(path, libName) + if _, err := os.Stat(dllPath); err == nil { + slib, loadErr := windows.LoadLibrary(dllPath) + if loadErr != nil { + return 0, fmt.Errorf("failed to load library at %s: %w", dllPath, loadErr) + } + return uintptr(slib), nil + } + } + + return 0, fmt.Errorf("library %s not found in PATH or CWD", libName) +} diff --git a/bindings/go/rows.go b/bindings/go/rows.go new file mode 100644 index 000000000..1d14e0d0c --- /dev/null +++ b/bindings/go/rows.go @@ -0,0 +1,121 @@ +package limbo + +import ( + "database/sql/driver" + "errors" + "fmt" + "io" + "sync" +) + +type limboRows struct { + mu sync.Mutex + ctx uintptr + columns []string + err error + closed bool +} + +func newRows(ctx uintptr) *limboRows { + return &limboRows{ + mu: sync.Mutex{}, + ctx: ctx, + columns: nil, + err: nil, + closed: false, + } +} + +func (r *limboRows) isClosed() bool { + if r.ctx == 0 || r.closed { + return true + } + return false +} + +func (r *limboRows) Columns() []string { + if r.isClosed() { + return nil + } + if r.columns == nil { + r.mu.Lock() + count := rowsGetColumns(r.ctx) + if count > 0 { + columns := make([]string, 0, count) + for i := 0; i < int(count); i++ { + cstr := rowsGetColumnName(r.ctx, int32(i)) + columns = append(columns, fmt.Sprintf("%s", GoString(cstr))) + freeCString(cstr) + } + r.mu.Unlock() + r.columns = columns + } + } + return r.columns +} + +func (r *limboRows) Close() error { + r.err = errors.New(RowsClosedErr) + if r.isClosed() { + return r.err + } + r.mu.Lock() + r.closed = true + closeRows(r.ctx) + r.ctx = 0 + r.mu.Unlock() + return nil +} + +func (r *limboRows) Err() error { + if r.err == nil { + r.mu.Lock() + defer r.mu.Unlock() + r.getError() + } + return r.err +} + +func (r *limboRows) Next(dest []driver.Value) error { + r.mu.Lock() + defer r.mu.Unlock() + if r.isClosed() { + return r.err + } + for { + status := rowsNext(r.ctx) + switch ResultCode(status) { + case Row: + for i := range dest { + valPtr := rowsGetValue(r.ctx, int32(i)) + val := toGoValue(valPtr) + if val == nil { + r.getError() + } + dest[i] = val + } + return nil + case Io: + continue + case Done: + return io.EOF + default: + return r.getError() + } + } +} + +// mutex will already be locked. this is always called after FFI +func (r *limboRows) getError() error { + if r.isClosed() { + return r.err + } + err := rowsGetError(r.ctx) + if err == 0 { + return nil + } + defer freeCString(err) + cpy := fmt.Sprintf("%s", GoString(err)) + r.err = errors.New(cpy) + return r.err +} diff --git a/bindings/go/rs_src/lib.rs b/bindings/go/rs_src/lib.rs index 199ed10c0..6ef640580 100644 --- a/bindings/go/rs_src/lib.rs +++ b/bindings/go/rs_src/lib.rs @@ -26,7 +26,6 @@ pub unsafe extern "C" fn db_open(path: *const c_char) -> *mut c_void { let db = Database::open_file(io.clone(), &db_options.path.to_string()); match db { Ok(db) => { - println!("Opened database: {}", path); let conn = db.connect(); return LimboConn::new(conn, io).to_ptr(); } @@ -43,23 +42,51 @@ pub unsafe extern "C" fn db_open(path: *const c_char) -> *mut c_void { struct LimboConn { conn: RcThis method attempts to load the native library named "_limbo_java" from the system's + * library path. If the library is successfully loaded, the `isLoaded` flag is set to true. + * + * @return true if the library was successfully loaded, false otherwise. + */ + private static boolean loadFromSystemPath() { try { System.loadLibrary("_limbo_java"); - } finally { - isLoaded = true; + return true; + } catch (Throwable t) { + logger.info("Unable to load from default path: {}", String.valueOf(t)); } + + return false; + } + + /** + * Load the native library from the JAR file. + * + *
By default, native libraries are packaged within the JAR file. This method extracts the
+ * appropriate native library for the current operating system and architecture from the JAR and
+ * loads it.
+ *
+ * @return true if the library was successfully loaded, false otherwise.
+ */
+ private static boolean loadFromJar() {
+ Architecture arch = Architecture.detect();
+ if (arch == Architecture.UNSUPPORTED) {
+ logger.info("Unsupported OS or architecture");
+ return false;
+ }
+
+ try {
+ InputStream is = LimboDB.class.getClassLoader().getResourceAsStream(arch.getLibPath());
+ assert is != null;
+ File file = convertInputStreamToFile(is, arch);
+ System.load(file.getPath());
+ return true;
+ } catch (Throwable t) {
+ logger.info("Unable to load from jar: {}", String.valueOf(t));
+ }
+
+ return false;
+ }
+
+ private static File convertInputStreamToFile(InputStream is, Architecture arch)
+ throws IOException {
+ File tempFile = File.createTempFile("lib", arch.getFileExtension());
+ tempFile.deleteOnExit();
+
+ try (FileOutputStream os = new FileOutputStream(tempFile)) {
+ int read;
+ byte[] bytes = new byte[1024];
+
+ while ((read = is.read(bytes)) != -1) {
+ os.write(bytes, 0, read);
+ }
+ }
+
+ return tempFile;
}
/**
@@ -56,8 +179,6 @@ public final class LimboDB extends AbstractDB {
super(url, filePath);
}
- // WRAPPER FUNCTIONS ////////////////////////////////////////////
-
// TODO: add support for JNI
@Override
protected native long openUtf8(byte[] file, int openFlags) throws SQLException;
@@ -66,9 +187,6 @@ public final class LimboDB extends AbstractDB {
@Override
protected native void close0() throws SQLException;
- // TODO: add support for JNI
- native int execUtf8(byte[] sqlUtf8) throws SQLException;
-
// TODO: add support for JNI
@Override
public native void interrupt();
diff --git a/bindings/java/src/main/java/org/github/tursodatabase/core/LimboResultSet.java b/bindings/java/src/main/java/org/github/tursodatabase/core/LimboResultSet.java
index c6cb8d00e..1884d4786 100644
--- a/bindings/java/src/main/java/org/github/tursodatabase/core/LimboResultSet.java
+++ b/bindings/java/src/main/java/org/github/tursodatabase/core/LimboResultSet.java
@@ -1,5 +1,6 @@
package org.github.tursodatabase.core;
+import java.sql.ResultSet;
import java.sql.SQLException;
import org.github.tursodatabase.annotations.Nullable;
import org.slf4j.Logger;
@@ -39,6 +40,20 @@ public class LimboResultSet {
this.statement = statement;
}
+ /**
+ * Consumes all the rows in this {@link ResultSet} until the {@link #next()} method returns
+ * `false`.
+ *
+ * @throws SQLException if the result set is not open or if an error occurs while iterating.
+ */
+ public void consumeAll() throws SQLException {
+ if (!open) {
+ throw new SQLException("The result set is not open");
+ }
+
+ while (next()) {}
+ }
+
/**
* Moves the cursor forward one row from its current position. A {@link LimboResultSet} cursor is
* initially positioned before the first fow; the first call to the method next makes
@@ -50,7 +65,11 @@ public class LimboResultSet {
* cursor can only move forward.
*/
public boolean next() throws SQLException {
- if (!open || isEmptyResultSet || pastLastRow) {
+ if (!open) {
+ throw new SQLException("The resultSet is not open");
+ }
+
+ if (isEmptyResultSet || pastLastRow) {
return false; // completed ResultSet
}
@@ -70,9 +89,6 @@ public class LimboResultSet {
}
pastLastRow = lastStepResult.isDone();
- if (pastLastRow) {
- open = false;
- }
return !pastLastRow;
}
@@ -97,6 +113,29 @@ public class LimboResultSet {
}
}
+ public void close() throws SQLException {
+ this.open = false;
+ }
+
+ // Note that columnIndex starts from 1
+ @Nullable
+ public Object get(int columnIndex) throws SQLException {
+ if (!this.isOpen()) {
+ throw new SQLException("ResultSet is not open");
+ }
+
+ if (this.lastStepResult == null || this.lastStepResult.getResult() == null) {
+ throw new SQLException("ResultSet is null");
+ }
+
+ final Object[] resultSet = this.lastStepResult.getResult();
+ if (columnIndex > resultSet.length || columnIndex < 0) {
+ throw new SQLException("columnIndex out of bound");
+ }
+
+ return resultSet[columnIndex - 1];
+ }
+
@Override
public String toString() {
return "LimboResultSet{"
diff --git a/bindings/java/src/main/java/org/github/tursodatabase/core/LimboStatement.java b/bindings/java/src/main/java/org/github/tursodatabase/core/LimboStatement.java
index c749e27cc..fa660b67c 100644
--- a/bindings/java/src/main/java/org/github/tursodatabase/core/LimboStatement.java
+++ b/bindings/java/src/main/java/org/github/tursodatabase/core/LimboStatement.java
@@ -21,6 +21,8 @@ public class LimboStatement {
private final long statementPointer;
private final LimboResultSet resultSet;
+ private boolean closed;
+
// TODO: what if the statement we ran was DDL, update queries and etc. Should we still create a
// resultSet?
public LimboStatement(String sql, long statementPointer) {
@@ -53,6 +55,11 @@ public class LimboStatement {
return result;
}
+ /**
+ * Because Limbo supports async I/O, it is possible to return a {@link LimboStepResult} with
+ * {@link LimboStepResult#STEP_RESULT_ID_ROW}. However, this is handled by the native side, so you
+ * can expect that this method will not return a {@link LimboStepResult#STEP_RESULT_ID_ROW}.
+ */
@Nullable
private native LimboStepResult step(long stmtPointer) throws SQLException;
@@ -67,6 +74,30 @@ public class LimboStatement {
LimboExceptionUtils.throwLimboException(errorCode, errorMessageBytes);
}
+ /**
+ * Closes the current statement and releases any resources associated with it. This method calls
+ * the native `_close` method to perform the actual closing operation.
+ */
+ public void close() throws SQLException {
+ if (closed) {
+ return;
+ }
+ this.resultSet.close();
+ _close(statementPointer);
+ closed = true;
+ }
+
+ private native void _close(long statementPointer);
+
+ /**
+ * Checks if the statement is closed.
+ *
+ * @return true if the statement is closed, false otherwise.
+ */
+ public boolean isClosed() {
+ return closed;
+ }
+
@Override
public String toString() {
return "LimboStatement{"
diff --git a/bindings/java/src/main/java/org/github/tursodatabase/core/LimboStepResult.java b/bindings/java/src/main/java/org/github/tursodatabase/core/LimboStepResult.java
index 93a1878aa..b82750b9a 100644
--- a/bindings/java/src/main/java/org/github/tursodatabase/core/LimboStepResult.java
+++ b/bindings/java/src/main/java/org/github/tursodatabase/core/LimboStepResult.java
@@ -47,6 +47,11 @@ public class LimboStepResult {
|| stepResultId == STEP_RESULT_ID_ERROR;
}
+ @Nullable
+ public Object[] getResult() {
+ return result;
+ }
+
@Override
public String toString() {
return "LimboStepResult{"
diff --git a/bindings/java/src/main/java/org/github/tursodatabase/jdbc4/JDBC4Connection.java b/bindings/java/src/main/java/org/github/tursodatabase/jdbc4/JDBC4Connection.java
index b17c6af36..3c32ffaf2 100644
--- a/bindings/java/src/main/java/org/github/tursodatabase/jdbc4/JDBC4Connection.java
+++ b/bindings/java/src/main/java/org/github/tursodatabase/jdbc4/JDBC4Connection.java
@@ -83,13 +83,12 @@ public class JDBC4Connection extends LimboConnection {
@Override
public void close() throws SQLException {
- // TODO
+ super.close();
}
@Override
public boolean isClosed() throws SQLException {
- // TODO
- return false;
+ return super.isClosed();
}
@Override
diff --git a/bindings/java/src/main/java/org/github/tursodatabase/jdbc4/JDBC4ResultSet.java b/bindings/java/src/main/java/org/github/tursodatabase/jdbc4/JDBC4ResultSet.java
index 867b2688e..ad3720b8c 100644
--- a/bindings/java/src/main/java/org/github/tursodatabase/jdbc4/JDBC4ResultSet.java
+++ b/bindings/java/src/main/java/org/github/tursodatabase/jdbc4/JDBC4ResultSet.java
@@ -3,10 +3,26 @@ package org.github.tursodatabase.jdbc4;
import java.io.InputStream;
import java.io.Reader;
import java.math.BigDecimal;
+import java.math.RoundingMode;
import java.net.URL;
-import java.sql.*;
+import java.sql.Array;
+import java.sql.Blob;
+import java.sql.Clob;
+import java.sql.Date;
+import java.sql.NClob;
+import java.sql.Ref;
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.RowId;
+import java.sql.SQLException;
+import java.sql.SQLWarning;
+import java.sql.SQLXML;
+import java.sql.Statement;
+import java.sql.Time;
+import java.sql.Timestamp;
import java.util.Calendar;
import java.util.Map;
+import org.github.tursodatabase.annotations.Nullable;
import org.github.tursodatabase.annotations.SkipNullableCheck;
import org.github.tursodatabase.core.LimboResultSet;
@@ -25,7 +41,7 @@ public class JDBC4ResultSet implements ResultSet {
@Override
public void close() throws SQLException {
- // TODO
+ resultSet.close();
}
@Override
@@ -35,64 +51,99 @@ public class JDBC4ResultSet implements ResultSet {
}
@Override
+ @Nullable
public String getString(int columnIndex) throws SQLException {
- // TODO
- return "";
+ final Object result = resultSet.get(columnIndex);
+ if (result == null) {
+ return null;
+ }
+ return wrapTypeConversion(() -> (String) result);
}
@Override
public boolean getBoolean(int columnIndex) throws SQLException {
- // TODO
- return false;
+ final Object result = resultSet.get(columnIndex);
+ if (result == null) {
+ return false;
+ }
+ return wrapTypeConversion(() -> (Long) result != 0);
}
@Override
public byte getByte(int columnIndex) throws SQLException {
- // TODO
- return 0;
+ final Object result = resultSet.get(columnIndex);
+ if (result == null) {
+ return 0;
+ }
+ return wrapTypeConversion(() -> ((Long) result).byteValue());
}
@Override
public short getShort(int columnIndex) throws SQLException {
- // TODO
- return 0;
+ final Object result = resultSet.get(columnIndex);
+ if (result == null) {
+ return 0;
+ }
+ return wrapTypeConversion(() -> ((Long) result).shortValue());
}
@Override
public int getInt(int columnIndex) throws SQLException {
- // TODO
- return 0;
+ final Object result = resultSet.get(columnIndex);
+ if (result == null) {
+ return 0;
+ }
+ return wrapTypeConversion(() -> ((Long) result).intValue());
}
@Override
public long getLong(int columnIndex) throws SQLException {
- // TODO
- return 0;
+ final Object result = resultSet.get(columnIndex);
+ if (result == null) {
+ return 0;
+ }
+ return wrapTypeConversion(() -> (long) result);
}
@Override
public float getFloat(int columnIndex) throws SQLException {
- // TODO
- return 0;
+ final Object result = resultSet.get(columnIndex);
+ if (result == null) {
+ return 0;
+ }
+ return wrapTypeConversion(() -> ((Double) result).floatValue());
}
@Override
public double getDouble(int columnIndex) throws SQLException {
- // TODO
- return 0;
+ final Object result = resultSet.get(columnIndex);
+ if (result == null) {
+ return 0;
+ }
+ return wrapTypeConversion(() -> (double) result);
}
+ // TODO: customize rounding mode?
@Override
- @SkipNullableCheck
+ @Nullable
public BigDecimal getBigDecimal(int columnIndex, int scale) throws SQLException {
- // TODO
- return null;
+ final Object result = resultSet.get(columnIndex);
+ if (result == null) {
+ return null;
+ }
+ final double doubleResult = wrapTypeConversion(() -> (double) result);
+ final BigDecimal bigDecimalResult = BigDecimal.valueOf(doubleResult);
+ return bigDecimalResult.setScale(scale, RoundingMode.HALF_UP);
}
@Override
+ @Nullable
public byte[] getBytes(int columnIndex) throws SQLException {
- // TODO
- return new byte[0];
+ final Object result = resultSet.get(columnIndex);
+ if (result == null) {
+ return null;
+ }
+ return wrapTypeConversion(() -> (byte[]) result);
}
@Override
@@ -300,10 +351,14 @@ public class JDBC4ResultSet implements ResultSet {
}
@Override
- @SkipNullableCheck
+ @Nullable
public BigDecimal getBigDecimal(int columnIndex) throws SQLException {
- // TODO
- return null;
+ final Object result = resultSet.get(columnIndex);
+ if (result == null) {
+ return null;
+ }
+ final double doubleResult = wrapTypeConversion(() -> (double) result);
+ return BigDecimal.valueOf(doubleResult);
}
@Override
@@ -866,8 +921,7 @@ public class JDBC4ResultSet implements ResultSet {
@Override
public boolean isClosed() throws SQLException {
- // TODO
- return false;
+ return !resultSet.isOpen();
}
@Override
@@ -1127,7 +1181,16 @@ public class JDBC4ResultSet implements ResultSet {
return false;
}
- private SQLException throwNotSupportedException() {
- return new SQLFeatureNotSupportedException("Not implemented by the driver");
+ @FunctionalInterface
+ public interface ResultSetSupplier(pairs: &Vec<(String, Val)>, serializer: S) -> Result