├── .env
├── .gitattributes
├── .github
├── FUNDING.yml
└── workflows
│ ├── c-tests.yaml
│ ├── c-valgrind.yaml
│ ├── publish.yaml
│ ├── py-tests.yaml
│ ├── rs-tests.yml
│ └── rs-valgrind.yml
├── .gitignore
├── .gitmodules
├── .prettierignore
├── .prettierrc
├── LICENSE
├── Makefile
├── README.md
├── core
├── .clang-format
├── .gitignore
├── CHANGELOG.md
├── Makefile
├── README.md
├── all-ios-loadable.sh
├── all-ios-static.sh
├── nodejs-helper.d.ts
├── nodejs-helper.js
├── nodejs-install-helper.js
├── package.json
├── pnpm-lock.yaml
├── rs
│ ├── bundle
│ │ ├── Cargo.lock
│ │ ├── Cargo.toml
│ │ ├── Cargo.toml.integration-test
│ │ ├── README.md
│ │ ├── keep-stack-sizes.x
│ │ ├── rust-toolchain.toml
│ │ └── src
│ │ │ └── lib.rs
│ ├── bundle_static
│ │ ├── Cargo.lock
│ │ ├── Cargo.toml
│ │ ├── rust-toolchain.toml
│ │ └── src
│ │ │ └── lib.rs
│ ├── core
│ │ ├── Cargo.lock
│ │ ├── Cargo.toml
│ │ ├── README.md
│ │ ├── explore.sql
│ │ ├── rust-toolchain.toml
│ │ └── src
│ │ │ ├── alter.rs
│ │ │ ├── automigrate.rs
│ │ │ ├── backfill.rs
│ │ │ ├── bootstrap.rs
│ │ │ ├── c.rs
│ │ │ ├── changes_vtab.rs
│ │ │ ├── changes_vtab_read.rs
│ │ │ ├── changes_vtab_write.rs
│ │ │ ├── compare_values.rs
│ │ │ ├── config.rs
│ │ │ ├── consts.rs
│ │ │ ├── create_cl_set_vtab.rs
│ │ │ ├── create_crr.rs
│ │ │ ├── db_version.rs
│ │ │ ├── ext_data.rs
│ │ │ ├── is_crr.rs
│ │ │ ├── lib.rs
│ │ │ ├── local_writes
│ │ │ ├── after_delete.rs
│ │ │ ├── after_insert.rs
│ │ │ ├── after_update.rs
│ │ │ └── mod.rs
│ │ │ ├── pack_columns.rs
│ │ │ ├── sha.rs
│ │ │ ├── stmt_cache.rs
│ │ │ ├── tableinfo.rs
│ │ │ ├── teardown.rs
│ │ │ ├── test_exports.rs
│ │ │ ├── triggers.rs
│ │ │ ├── unpack_columns_vtab.rs
│ │ │ └── util.rs
│ ├── fractindex-core
│ │ ├── Cargo.lock
│ │ ├── Cargo.toml
│ │ ├── README.md
│ │ ├── explore.sql
│ │ ├── rust-toolchain.toml
│ │ └── src
│ │ │ ├── as_ordered.rs
│ │ │ ├── fractindex.rs
│ │ │ ├── fractindex_view.rs
│ │ │ ├── lib.rs
│ │ │ └── util.rs
│ └── integration_check
│ │ ├── Cargo.lock
│ │ ├── Cargo.toml
│ │ ├── notes.md
│ │ ├── rust-toolchain.toml
│ │ └── src
│ │ ├── lib.rs
│ │ └── t
│ │ ├── automigrate.rs
│ │ ├── backfill.rs
│ │ ├── fract.rs
│ │ ├── mod.rs
│ │ ├── pack_columns.rs
│ │ ├── pk_only_tables.rs
│ │ ├── pk_update.rs
│ │ ├── sync_bit_honored.rs
│ │ ├── tableinfo.rs
│ │ ├── teardown.rs
│ │ ├── test_cl_set_vtab.rs
│ │ └── test_db_version.rs
└── src
│ ├── changes-vtab-rowid.test.c
│ ├── changes-vtab.c
│ ├── changes-vtab.h
│ ├── changes-vtab.test.c
│ ├── consts.h
│ ├── core_init.c
│ ├── crsqlite.c
│ ├── crsqlite.h
│ ├── crsqlite.test.c
│ ├── ext-data.c
│ ├── ext-data.h
│ ├── ext-data.test.c
│ ├── ext.h
│ ├── fuzzer.cc
│ ├── is-crr.test.c
│ ├── rows-impacted.test.c
│ ├── rs-fract.test.c
│ ├── rust.h
│ ├── sandbox.test.c
│ ├── sqlite
│ ├── Makefile
│ ├── shell.c
│ ├── sqlite3.c
│ ├── sqlite3.h
│ └── sqlite3ext.h
│ ├── tests.c
│ └── util.h
├── libsql-sync.sh
├── notes.md
└── py
├── README.md
├── correctness
├── .gitignore
├── CHANGELOG.md
├── install-and-test.sh
├── notes.md
├── package.json
├── prior-dbs
│ ├── v0.12.0.prior-db
│ ├── v0.13.0.prior-db
│ └── v0.15.0.prior-db
├── pyproject.toml
├── requirements.txt
├── spec.md
├── src
│ ├── crsql_correctness.egg-info
│ │ ├── PKG-INFO
│ │ ├── SOURCES.txt
│ │ ├── dependency_links.txt
│ │ └── top_level.txt
│ └── crsql_correctness
│ │ └── __init__.py
├── test.sh
└── tests
│ ├── test_as_ordered.py
│ ├── test_cl_merging.py
│ ├── test_cl_triggers.py
│ ├── test_commit_alter_perf.py
│ ├── test_config.py
│ ├── test_crsql_changes_filters.py
│ ├── test_dbversion.py
│ ├── test_insert_new_rows.py
│ ├── test_lookaside_key_creation.py
│ ├── test_prior_versions.py
│ ├── test_sandbox.py
│ ├── test_schema_modification.py
│ ├── test_sentinel_omission.py
│ ├── test_seq.py
│ ├── test_site_id_lookaside.py
│ ├── test_siteid.py
│ ├── test_sync.py
│ ├── test_sync_bit.py
│ ├── test_sync_prop.py
│ └── test_update_rows.py
└── perf
├── .gitignore
├── notes.md
└── perf.ipynb
/.env:
--------------------------------------------------------------------------------
1 | PYTHONPATH=./py/correctness/src
2 | CRSQLITE_NOPREBUILD=1
3 | CRSQLITE_COMMIT_SHA=dev
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | core/src/sqlite/* linguist-vendored
2 | py/perf/* linguist-documentation
3 |
4 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [tantaman]
4 |
--------------------------------------------------------------------------------
/.github/workflows/c-tests.yaml:
--------------------------------------------------------------------------------
1 | on:
2 | pull_request:
3 | name: "c-tests"
4 | jobs:
5 | build:
6 | name: Testing on ${{ matrix.os }}
7 | runs-on: ${{ matrix.os }}
8 | strategy:
9 | matrix:
10 | include:
11 | - os: ubuntu-latest
12 | - os: windows-2022
13 | - os: macos-latest
14 | steps:
15 | - uses: actions/checkout@v3
16 | with:
17 | submodules: true
18 |
19 | - name: Load .env file
20 | uses: xom9ikk/dotenv@v2
21 | with:
22 | path: ./
23 |
24 | - name: Windows rust toolchain
25 | if: runner.os == 'Windows'
26 | run: |
27 | rm core/rs/integration_check/rust-toolchain.toml
28 | rustup component add rust-src --toolchain nightly-2023-10-05-x86_64-pc-windows-gnu
29 | rustup default nightly-2023-10-05-x86_64-pc-windows-gnu
30 |
31 | - name: Test
32 | run: |
33 | cd core
34 | make test
35 |
--------------------------------------------------------------------------------
/.github/workflows/c-valgrind.yaml:
--------------------------------------------------------------------------------
1 | on:
2 | pull_request:
3 | name: "c-valgrind"
4 | jobs:
5 | build:
6 | name: Testing on ${{ matrix.os }}
7 | runs-on: ${{ matrix.os }}
8 | strategy:
9 | matrix:
10 | include:
11 | - os: ubuntu-latest
12 | #- os: windows-2022
13 | #- os: macos-latest
14 | steps:
15 | - uses: actions/checkout@v3
16 | with:
17 | submodules: true
18 |
19 | - name: Load .env file
20 | uses: xom9ikk/dotenv@v2
21 | with:
22 | path: ./
23 |
24 | - name: Install valgrind
25 | run: sudo apt update && sudo apt install -y valgrind
26 |
27 | - name: Valgrind Test
28 | run: |
29 | cd core
30 | make test
31 | valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes -s dist/test
32 |
--------------------------------------------------------------------------------
/.github/workflows/py-tests.yaml:
--------------------------------------------------------------------------------
1 | on:
2 | pull_request:
3 | name: "py-tests"
4 | jobs:
5 | build:
6 | name: Testing on ${{ matrix.os }}
7 | runs-on: ${{ matrix.os }}
8 | defaults:
9 | run:
10 | shell: bash -el {0}
11 | strategy:
12 | matrix:
13 | include:
14 | - os: ubuntu-latest
15 | - os: macos-latest
16 | steps:
17 | - uses: actions/checkout@v3
18 | with:
19 | submodules: true
20 |
21 | - name: Load .env file
22 | uses: xom9ikk/dotenv@v2
23 | with:
24 | path: ./
25 |
26 | - name: Make loadable
27 | run: |
28 | cd core
29 | make loadable
30 |
31 | - uses: conda-incubator/setup-miniconda@v2
32 | with:
33 | auto-update-conda: true
34 | auto-activate-base: true
35 | activate-environment: anaconda-client-env
36 | python-version: "3.10"
37 |
38 | - name: Install SQLite
39 | run: conda install sqlite
40 |
41 | - name: Check SQLite Version
42 | run: echo "import sqlite3; print(sqlite3.sqlite_version)" | python
43 |
44 | # - name: Install Python
45 | # uses: actions/setup-python@v4
46 | # with:
47 | # python-version: "3.10"
48 |
49 | - name: Install pip
50 | run: |
51 | python -m pip install --upgrade pip
52 |
53 | - name: Test
54 | run: cd py/correctness && ./install-and-test.sh
55 |
--------------------------------------------------------------------------------
/.github/workflows/rs-tests.yml:
--------------------------------------------------------------------------------
1 | on:
2 | pull_request:
3 | name: "rs-tests"
4 | jobs:
5 | build:
6 | name: Testing on ${{ matrix.os }}
7 | runs-on: ${{ matrix.os }}
8 | strategy:
9 | matrix:
10 | include:
11 | - os: ubuntu-latest
12 | # - os: windows-2022 # Windows is complete nonsense. If someone wants to figure out how to make tests run there go ahead.
13 | - os: macos-latest
14 | steps:
15 | - uses: actions/checkout@v3
16 | with:
17 | submodules: true
18 |
19 | - name: Load .env file
20 | uses: xom9ikk/dotenv@v2
21 | with:
22 | path: ./
23 |
24 | - name: Windows rust toolchain
25 | if: runner.os == 'Windows'
26 | run: |
27 | find . -name rust-toolchain.toml -exec echo rm {} \;
28 | rustup component add rust-src --toolchain nightly-2023-10-05-x86_64-pc-windows-gnu
29 | rustup default nightly-2023-10-05-x86_64-pc-windows-gnu
30 |
31 | - name: Test Fractindex
32 | run: |
33 | cd core/rs/fractindex-core
34 | cargo test --features=loadable_extension
35 |
36 | - name: Test Core
37 | run: |
38 | cd core/rs/core
39 | cargo test --features=loadable_extension
40 |
--------------------------------------------------------------------------------
/.github/workflows/rs-valgrind.yml:
--------------------------------------------------------------------------------
1 | on:
2 | pull_request:
3 | name: "rs-valgrind"
4 | jobs:
5 | build:
6 | name: Testing on ${{ matrix.os }}
7 | runs-on: ${{ matrix.os }}
8 | strategy:
9 | matrix:
10 | include:
11 | - os: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v3
14 | with:
15 | submodules: true
16 |
17 | - name: Load .env file
18 | uses: xom9ikk/dotenv@v2
19 | with:
20 | path: ./
21 |
22 | - name: Install valgrind
23 | run: sudo apt update && sudo apt install -y valgrind
24 |
25 | - name: Install Cargo Valgrind
26 | run: |
27 | cargo install cargo-valgrind
28 |
29 | - name: Test Fractindex
30 | run: |
31 | cd core/rs/fractindex-core
32 | cargo valgrind test --features=loadable_extension
33 |
34 | - name: Test Core
35 | run: |
36 | cd core/rs/core
37 | cargo valgrind test --features=loadable_extension
38 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | out.db
3 | .vscode
4 | a.out
5 | dist/
6 | bench/
7 | .ipynb_checkpoints/
8 | *.db
9 | __pycache__/
10 | node_modules/
11 | build/
12 | tsconfig.tsbuildinfo
13 | .turbo/
14 | target/
15 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "core/rs/sqlite-rs-embedded"]
2 | path = core/rs/sqlite-rs-embedded
3 | url = git@github.com:vlcn-io/sqlite-rs-embedded.git
4 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | LICENSE
2 | spec.md
3 | deps/
4 | wa-sqlite/
5 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "tabWidth": 2,
4 | "singleQuote": false
5 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 One Law LLC
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | git-deps = core/rs/sqlite-rs-embedded
2 |
3 | .EXPORT_ALL_VARIABLES:
4 | CRSQLITE_NOPREBUILD = 1
5 |
6 | all: crsqlite
7 |
8 | $(git-deps):
9 | git submodule update --init --recursive
10 |
11 |
12 | crsqlite: $(git-deps)
13 | cd core; \
14 | make loadable
15 |
16 | clean:
17 | cd core && make clean
18 |
19 | .PHONY: crsqlite all clean
20 |
--------------------------------------------------------------------------------
/core/.clang-format:
--------------------------------------------------------------------------------
1 | # Use the Google style in this project.
2 | BasedOnStyle: Google
3 |
4 | # Some folks prefer to write "int& foo" while others prefer "int &foo". The
5 | # Google Style Guide only asks for consistency within a project, we chose
6 | # "int &foo" for this project:
7 | DerivePointerAlignment: false
8 | PointerAlignment: Right
9 |
--------------------------------------------------------------------------------
/core/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | target/
3 | dist/
4 | dbg/
5 | dist-ios/
6 | dist-ios-sim/
7 | dist-macos/
8 | test_leak_condition
9 |
--------------------------------------------------------------------------------
/core/README.md:
--------------------------------------------------------------------------------
1 | See main readme: https://github.com/vlcn-io/cr-sqlite/blob/main/README.md
2 |
--------------------------------------------------------------------------------
/core/all-ios-loadable.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 |
3 | # a hacky script to make all the various ios targets.
4 | # once we have something consistently working we'll streamline all of this.
5 |
6 | BUILD_DIR=./build
7 | DIST_PACKAGE_DIR=./dist
8 |
9 | function createXcframework() {
10 | plist=$(cat << EOF
11 |
12 |
13 |
14 |
15 | CFBundleDevelopmentRegion
16 | en
17 | CFBundleExecutable
18 | crsqlite
19 | CFBundleIdentifier
20 | io.vlcn.crsqlite
21 | CFBundleInfoDictionaryVersion
22 | 6.0
23 | CFBundlePackageType
24 | FMWK
25 | CFBundleSignature
26 | ????
27 | CFBundleVersion
28 | 1.0.0
29 | CFBundleShortVersionString
30 | 1.0.0
31 | MinimumOSVersion
32 | 8.0
33 |
34 |
35 | EOF
36 | )
37 | printf "\n\n\t\t===================== create ios device framework =====================\n\n"
38 | mkdir -p "${BUILD_DIR}/ios-arm64/crsqlite.framework"
39 | echo "${plist}" > "${BUILD_DIR}/ios-arm64/crsqlite.framework/Info.plist"
40 | cp -f "./dist-ios/crsqlite-aarch64-apple-ios.dylib" "${BUILD_DIR}/ios-arm64/crsqlite.framework/crsqlite"
41 | install_name_tool -id "@rpath/crsqlite.framework/crsqlite" "${BUILD_DIR}/ios-arm64/crsqlite.framework/crsqlite"
42 |
43 | printf "\n\n\t\t===================== create ios simulator framework =====================\n\n"
44 | mkdir -p "${BUILD_DIR}/ios-arm64_x86_64-simulator/crsqlite.framework"
45 | echo "${plist}" > "${BUILD_DIR}/ios-arm64_x86_64-simulator/crsqlite.framework/Info.plist"
46 | cp -p "./dist-ios-sim/crsqlite-universal-ios-sim.dylib" "${BUILD_DIR}/ios-arm64_x86_64-simulator/crsqlite.framework/crsqlite"
47 | install_name_tool -id "@rpath/crsqlite.framework/crsqlite" "${BUILD_DIR}/ios-arm64_x86_64-simulator/crsqlite.framework/crsqlite"
48 |
49 | printf "\n\n\t\t===================== create ios xcframework =====================\n\n"
50 | rm -rf "${BUILD_DIR}/crsqlite.xcframework"
51 | xcodebuild -create-xcframework -framework "${BUILD_DIR}/ios-arm64/crsqlite.framework" -framework "${BUILD_DIR}/ios-arm64_x86_64-simulator/crsqlite.framework" -output "${BUILD_DIR}/crsqlite.xcframework"
52 |
53 | mkdir -p ${DIST_PACKAGE_DIR}
54 | cp -Rf "${BUILD_DIR}/crsqlite.xcframework" "${DIST_PACKAGE_DIR}/crsqlite.xcframework"
55 | cd ${DIST_PACKAGE_DIR}
56 | tar -czvf crsqlite-ios-dylib.xcframework.tar.gz crsqlite.xcframework
57 | rm -rf ${BUILD_DIR}
58 | }
59 |
60 | # Make all the non-simulator libs
61 | # Package into a universal ios lib
62 | mkdir -p ./dist-ios
63 |
64 | # TODO: fix things up to not require a clean before each target.
65 | make clean
66 | export IOS_TARGET=aarch64-apple-ios; make loadable
67 | cp ./dist/crsqlite.dylib ./dist-ios/crsqlite-aarch64-apple-ios.dylib
68 |
69 | mkdir -p ./dist-ios-sim
70 |
71 | make clean
72 | export IOS_TARGET=aarch64-apple-ios-sim; make loadable
73 | cp ./dist/crsqlite.dylib ./dist-ios-sim/crsqlite-aarch64-apple-ios-sim.dylib
74 |
75 | make clean
76 | export IOS_TARGET=x86_64-apple-ios; make loadable
77 | cp ./dist/crsqlite.dylib ./dist-ios-sim/crsqlite-x86_64-apple-ios-sim.dylib
78 |
79 | cd ./dist-ios-sim
80 | lipo crsqlite-aarch64-apple-ios-sim.dylib crsqlite-x86_64-apple-ios-sim.dylib -create -output crsqlite-universal-ios-sim.dylib
81 | cd ..
82 |
83 | createXcframework
84 |
--------------------------------------------------------------------------------
/core/all-ios-static.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 |
3 | # a hacky script to make all the various ios targets.
4 | # once we have something consistently working we'll streamline all of this.
5 |
6 | # Make all the non-simulator libs
7 | # Package into a universal ios lib
8 | mkdir -p ./dist-ios
9 |
10 | # TODO: fix things up to not require a clean before each target.
11 | make clean
12 | export IOS_TARGET=aarch64-apple-ios; make static
13 | cp ./dist/crsqlite-aarch64-apple-ios.a ./dist-ios
14 |
15 | make clean
16 | export IOS_TARGET=armv7-apple-ios; make static
17 | cp ./dist/crsqlite-armv7-apple-ios.a ./dist-ios
18 |
19 | make clean
20 | export IOS_TARGET=armv7s-apple-ios; make static
21 | cp ./dist/crsqlite-armv7s-apple-ios.a ./dist-ios
22 |
23 | cd ./dist-ios
24 | lipo crsqlite-aarch64-apple-ios.a crsqlite-armv7-apple-ios.a crsqlite-armv7s-apple-ios.a -create -output crsqlite-universal-ios.a
25 |
26 | cd ..
27 | # ===
28 |
29 | # Make the simlator libs
30 | # Package into a universal ios sim lib
31 | mkdir -p ./dist-ios-sim
32 |
33 | make clean
34 | export IOS_TARGET=aarch64-apple-ios-sim; make static
35 | cp ./dist/crsqlite-aarch64-apple-ios-sim.a ./dist-ios-sim
36 |
37 | make clean
38 | export IOS_TARGET=x86_64-apple-ios; make static
39 | cp ./dist/crsqlite-x86_64-apple-ios.a ./dist-ios-sim
40 |
41 | cd ./dist-ios-sim
42 | lipo crsqlite-aarch64-apple-ios-sim.a crsqlite-x86_64-apple-ios.a -create -output crsqlite-universal-ios-sim.a
43 |
44 | cd ..
45 | # ===
46 |
47 | # Make the macos static lib
48 | mkdir -p ./dist-macos
49 | make clean
50 | unset IOS_TARGET
51 | export CI_MAYBE_TARGET="aarch64-apple-darwin"; make static
52 |
53 | cp ./dist/crsqlite-aarch64-apple-darwin.a ./dist-macos
54 |
55 |
--------------------------------------------------------------------------------
/core/nodejs-helper.d.ts:
--------------------------------------------------------------------------------
1 | export declare const extensionPath: string;
2 |
--------------------------------------------------------------------------------
/core/nodejs-helper.js:
--------------------------------------------------------------------------------
1 | // Exports the path to the extension for those using
2 | // crsqlite in a Node.js environment.
3 | import * as url from "url";
4 | import { join } from "path";
5 | const __dirname = url.fileURLToPath(new URL(".", import.meta.url));
6 |
7 | export const extensionPath = join(__dirname, "dist", "crsqlite");
8 |
--------------------------------------------------------------------------------
/core/nodejs-install-helper.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 1. Checks the current OS and CPU architecture
3 | * 2. Copies pre-built binaries from the `binaries` directory to the `dist` directory if one exists
4 | * 3. Otherwise, lets the standard install process via `make` take over
5 | */
6 | import { join } from "path";
7 | import fs from "fs";
8 | import https from "https";
9 | import pkg from "./package.json" with { type: "json" };
10 | import { exec } from "child_process";
11 | let { version } = pkg;
12 |
13 | let arch = process.arch;
14 | let os = process.platform;
15 | let ext = "unknown";
16 | version = "v" + version;
17 |
18 | if (process.env.CRSQLITE_NOPREBUILD) {
19 | console.log("CRSQLITE_NOPREBUILD env variable is set. Building from source.");
20 | buildFromSource();
21 | } else {
22 | // todo: check msys?
23 | if (["win32", "cygwin"].includes(process.platform)) {
24 | os = "win";
25 | }
26 |
27 | // manual ovverides for testing
28 | // arch = "x86_64";
29 | // os = "linux";
30 | // version = "prebuild-test.11";
31 |
32 | switch (os) {
33 | case "darwin":
34 | ext = "dylib";
35 | break;
36 | case "linux":
37 | ext = "so";
38 | break;
39 | case "win":
40 | ext = "dll";
41 | break;
42 | }
43 |
44 | switch (arch) {
45 | case "x64":
46 | arch = "x86_64";
47 | break;
48 | case "arm64":
49 | arch = "aarch64";
50 | break;
51 | }
52 |
53 | const binaryUrl = `https://github.com/vlcn-io/cr-sqlite/releases/download/${version}/crsqlite-${os}-${arch}.zip`;
54 | console.log(`Look for prebuilt binary from ${binaryUrl}`);
55 | const distPath = join("dist", `crsqlite.${ext}`);
56 |
57 | if (!fs.existsSync(join(".", "dist"))) {
58 | fs.mkdirSync(join(".", "dist"));
59 | }
60 |
61 | if (fs.existsSync(distPath)) {
62 | console.log("Binary already present and installed.");
63 | process.exit(0);
64 | }
65 |
66 | // download the file at the url, if it exists
67 | let redirectCount = 0;
68 | function get(url, cb) {
69 | https.get(url, (res) => {
70 | if (res.statusCode === 302 || res.statusCode === 301) {
71 | ++redirectCount;
72 | if (redirectCount > 5) {
73 | throw new Error("Too many redirects");
74 | }
75 | get(res.headers.location, cb);
76 | } else if (res.statusCode === 200) {
77 | cb(res);
78 | } else {
79 | cb(null);
80 | }
81 | });
82 | }
83 |
84 | get(binaryUrl, (res) => {
85 | if (res == null) {
86 | console.log("No prebuilt binary available. Building from source.");
87 | buildFromSource();
88 | return;
89 | }
90 |
91 | const file = fs.createWriteStream(join("dist", "crsqlite.zip"));
92 | res.pipe(file);
93 | file.on("finish", () => {
94 | file.close();
95 | console.log("Prebuilt binary downloaded");
96 | process.chdir(join(".", "dist"));
97 | exec("unzip crsqlite.zip", (err, stdout, stderr) => {
98 | if (err) {
99 | console.log("Error extracting");
100 | console.log(err.message);
101 | process.exit(1);
102 | }
103 | if (stderr) {
104 | console.log(stderr);
105 | }
106 | console.log("Prebuilt binary extracted");
107 | process.exit(0);
108 | });
109 | });
110 | // unzipper incorrectly unzips the file -- it becomes unloadable by sqlite.
111 | // res.pipe(unzipper.Extract({ path: join(".", "dist") })).on("close", () => {
112 | // console.log("Prebuilt binary downloaded");
113 | // process.exit(0);
114 | // });
115 | });
116 | }
117 |
118 | function buildFromSource() {
119 | console.log("Building from source");
120 | exec("make loadable", (err, stdout, stderr) => {
121 | if (err) {
122 | console.log("Error building from source");
123 | console.log(err.message);
124 | process.exit(1);
125 | }
126 | if (stderr) {
127 | console.log(stderr);
128 | }
129 | console.log("Built from source");
130 | console.log(stdout);
131 | process.exit(0);
132 | });
133 | }
134 |
--------------------------------------------------------------------------------
/core/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@vlcn.io/crsqlite",
3 | "version": "0.16.3",
4 | "description": "CR-SQLite loadable extension",
5 | "homepage": "https://vlcn.io",
6 | "repository": {
7 | "type": "git",
8 | "url": "git://github.com/vlcn-io/cr-sqlite"
9 | },
10 | "files": [
11 | "Makefile",
12 | "src/**",
13 | "rs/**/*.rs",
14 | "rs/**/*.toml",
15 | "rs/**/*.lock",
16 | "rs/**/*.h",
17 | "nodejs-helper.d.ts",
18 | "nodejs-helper.js",
19 | "nodejs-install-helper.js"
20 | ],
21 | "type": "module",
22 | "main": "nodejs-helper.js",
23 | "types": "nodejs-helper.d.ts",
24 | "scripts": {
25 | "install": "node ./nodejs-install-helper.js",
26 | "test": "make test",
27 | "deep-clean": "rm -rf ./dbg || true && rm -rf ./dist || true"
28 | },
29 | "license": "Apache 2",
30 | "keywords": [
31 | "sql",
32 | "sqlite",
33 | "sqlite3",
34 | "crdt"
35 | ]
36 | }
37 |
--------------------------------------------------------------------------------
/core/rs/bundle/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "crsql_bundle"
3 | version = "0.1.0"
4 | edition = "2021"
5 | authors = ["Matt Wonlaw"]
6 | keywords = ["sqlite", "cr-sqlite"]
7 | license = "Apache 2"
8 |
9 | [lib]
10 | name = "crsql_bundle"
11 | crate-type = ["rlib"]
12 |
13 | # "cdylib" <-- only enable cdylib if loadable_extension is enabled
14 |
15 | [dependencies]
16 | crsql_fractindex_core = {path="../fractindex-core"}
17 | crsql_core = { path="../core" }
18 | sqlite_nostd = { path="../sqlite-rs-embedded/sqlite_nostd" }
19 |
20 | [profile.dev]
21 | panic = "abort"
22 |
23 | [profile.release]
24 | panic = "abort"
25 |
26 | [features]
27 | test = ["crsql_core/test"]
28 | libsql = ["crsql_core/libsql"]
29 | loadable_extension = [
30 | "sqlite_nostd/loadable_extension",
31 | "crsql_fractindex_core/loadable_extension",
32 | "crsql_core/loadable_extension"
33 | ]
34 | static = [
35 | "sqlite_nostd/static",
36 | "crsql_fractindex_core/static",
37 | "crsql_core/static"
38 | ]
39 | omit_load_extension = [
40 | "sqlite_nostd/omit_load_extension",
41 | "crsql_fractindex_core/omit_load_extension",
42 | "crsql_core/omit_load_extension"
43 | ]
44 |
--------------------------------------------------------------------------------
/core/rs/bundle/Cargo.toml.integration-test:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "crsql_bundle"
3 | version = "0.1.0"
4 | edition = "2021"
5 | authors = ["Matt Wonlaw"]
6 | keywords = ["sqlite", "cr-sqlite"]
7 | license = "Apache 2"
8 |
9 | [lib]
10 | name = "crsql_bundle"
11 | crate-type = ["rlib"]
12 |
13 | # "cdylib" <-- only enable cdylib if loadable_extension is enabled
14 |
15 | [dependencies]
16 | crsql_fractindex_core = {path="../fractindex-core"}
17 | crsql_core = { path="../core" }
18 | sqlite_nostd = { path="../sqlite-rs-embedded/sqlite_nostd" }
19 |
20 | [profile.dev]
21 | panic = "abort"
22 |
23 | [profile.release]
24 | panic = "abort"
25 |
26 | [features]
27 | test = []
28 | loadable_extension = [
29 | "sqlite_nostd/loadable_extension",
30 | "crsql_fractindex_core/loadable_extension",
31 | "crsql_core/loadable_extension"
32 | ]
33 | static = [
34 | "sqlite_nostd/static",
35 | "crsql_fractindex_core/static",
36 | "crsql_core/static"
37 | ]
38 | omit_load_extension = [
39 | "sqlite_nostd/omit_load_extension",
40 | "crsql_fractindex_core/omit_load_extension",
41 | "crsql_core/omit_load_extension"
42 | ]
43 |
--------------------------------------------------------------------------------
/core/rs/bundle/README.md:
--------------------------------------------------------------------------------
1 | # bundle
2 |
3 | Using feature flags, bundles selected rust extensions into a single runtime loadable or statically linkable SQLite extension.
4 |
5 | - feature web : wasm support via web crate inclusion <-- just make this a target cfg
6 | - freature fract-index
7 | - feature auto-migrate
8 |
--------------------------------------------------------------------------------
/core/rs/bundle/keep-stack-sizes.x:
--------------------------------------------------------------------------------
1 | /* file: keep-stack-sizes.x */
2 | SECTIONS
3 | {
4 | /* `INFO` makes the section not allocatable so it won't be loaded into memory */
5 | .stack_sizes (INFO) :
6 | {
7 | KEEP(*(.stack_sizes));
8 | }
9 | }
10 | /* RUSTFLAGS="-Z emit-stack-sizes" cargo rustc --release -- -C link-arg=-Wl,-Tkeep-stack-sizes.x -C link-arg=-N */
11 | /* https://doc.rust-lang.org/nightly/unstable-book/compiler-flags/emit-stack-sizes.html */
12 |
--------------------------------------------------------------------------------
/core/rs/bundle/rust-toolchain.toml:
--------------------------------------------------------------------------------
1 | [toolchain]
2 | channel = "nightly-2023-10-05"
3 | components = [ "rust-src", "rustfmt", "clippy" ]
4 |
--------------------------------------------------------------------------------
/core/rs/bundle/src/lib.rs:
--------------------------------------------------------------------------------
1 | #![no_std]
2 | #![feature(core_intrinsics)]
3 | #![feature(lang_items)]
4 |
5 | extern crate alloc;
6 |
7 | use core::alloc::GlobalAlloc;
8 | use core::ffi::c_char;
9 | use core::panic::PanicInfo;
10 | use crsql_core;
11 | use crsql_core::sqlite3_crsqlcore_init;
12 | #[cfg(feature = "test")]
13 | pub use crsql_core::test_exports;
14 | use crsql_fractindex_core::sqlite3_crsqlfractionalindex_init;
15 | use sqlite_nostd as sqlite;
16 | use sqlite_nostd::SQLite3Allocator;
17 |
18 | // This must be our allocator so we can transfer ownership of memory to SQLite and have SQLite free that memory for us.
19 | // This drastically reduces copies when passing strings and blobs back and forth between Rust and C.
20 | #[global_allocator]
21 | static ALLOCATOR: SQLite3Allocator = SQLite3Allocator {};
22 |
23 | // This must be our panic handler for WASM builds. For simplicity, we make it our panic handler for
24 | // all builds. Abort is also more portable than unwind, enabling us to go to more embedded use cases.
25 | #[panic_handler]
26 | fn panic(_info: &PanicInfo) -> ! {
27 | core::intrinsics::abort()
28 | }
29 |
30 | #[cfg(not(target_family = "wasm"))]
31 | #[lang = "eh_personality"]
32 | extern "C" fn eh_personality() {}
33 |
34 | #[cfg(target_family = "wasm")]
35 | #[no_mangle]
36 | pub fn __rust_alloc_error_handler(_: Layout) -> ! {
37 | core::intrinsics::abort()
38 | }
39 |
40 | #[no_mangle]
41 | pub extern "C" fn sqlite3_crsqlrustbundle_init(
42 | db: *mut sqlite::sqlite3,
43 | err_msg: *mut *mut c_char,
44 | api: *mut sqlite::api_routines,
45 | ) -> *mut ::core::ffi::c_void {
46 | sqlite::EXTENSION_INIT2(api);
47 |
48 | let rc = sqlite3_crsqlfractionalindex_init(db, err_msg, api);
49 | if rc != 0 {
50 | return core::ptr::null_mut();
51 | }
52 |
53 | sqlite3_crsqlcore_init(db, err_msg, api)
54 | }
55 |
--------------------------------------------------------------------------------
/core/rs/bundle_static/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "crsql_bundle_static"
3 | version = "0.1.0"
4 | edition = "2021"
5 | authors = ["Matt Wonlaw"]
6 | keywords = ["sqlite", "cr-sqlite"]
7 | license = "Apache 2"
8 |
9 | [lib]
10 | name = "crsql_bundle_static"
11 | crate-type = ["staticlib"]
12 |
13 | [dependencies]
14 | crsql_bundle = {path="../bundle"}
15 |
16 | [profile.dev]
17 | panic = "abort"
18 |
19 | [profile.release]
20 | panic = "abort"
21 |
22 | [features]
23 | libsql = ["crsql_bundle/libsql"]
24 | test = [
25 | "crsql_bundle/test"
26 | ]
27 | loadable_extension = [
28 | "crsql_bundle/loadable_extension"
29 | ]
30 | static = [
31 | "crsql_bundle/static"
32 | ]
33 | omit_load_extension = [
34 | "crsql_bundle/omit_load_extension"
35 | ]
36 |
--------------------------------------------------------------------------------
/core/rs/bundle_static/rust-toolchain.toml:
--------------------------------------------------------------------------------
1 | [toolchain]
2 | channel = "nightly-2023-10-05"
3 |
4 |
--------------------------------------------------------------------------------
/core/rs/bundle_static/src/lib.rs:
--------------------------------------------------------------------------------
1 | #![no_std]
2 |
3 | pub use crsql_bundle;
4 |
--------------------------------------------------------------------------------
/core/rs/core/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "crsql_core"
3 | version = "0.1.0"
4 | edition = "2021"
5 | authors = ["Matt Wonlaw"]
6 | keywords = ["sqlite", "cr-sqlite", "crdt"]
7 | license = "Apache 2"
8 |
9 | [lib]
10 | name = "crsql_core"
11 | crate-type = ["rlib"]
12 |
13 | [dependencies]
14 | sqlite_nostd = { path="../sqlite-rs-embedded/sqlite_nostd" }
15 | bytes = { version = "1.5", default-features = false }
16 | num-traits = { version = "0.2.17", default-features = false }
17 | num-derive = "0.4.1"
18 |
19 | [dev-dependencies]
20 |
21 | [profile.dev]
22 | panic = "abort"
23 |
24 | [profile.release]
25 | panic = "abort"
26 |
27 | [features]
28 | test = []
29 | libsql = []
30 | loadable_extension = ["sqlite_nostd/loadable_extension"]
31 | static = ["sqlite_nostd/static"]
32 | omit_load_extension = ["sqlite_nostd/omit_load_extension"]
33 |
--------------------------------------------------------------------------------
/core/rs/core/README.md:
--------------------------------------------------------------------------------
1 | # core
2 |
3 | Eventually all the `c` code will be re-written and Rust and said functionality will be present here.
4 |
5 | Until then, `core` will slowly gobble different bits.
--------------------------------------------------------------------------------
/core/rs/core/explore.sql:
--------------------------------------------------------------------------------
1 | -- backfill
2 |
3 | INSERT INTO x__crsql_clock VALUES (SELECT * FROM x);
4 |
5 | -- ^-- you need to unroll the columns
--------------------------------------------------------------------------------
/core/rs/core/rust-toolchain.toml:
--------------------------------------------------------------------------------
1 | [toolchain]
2 | channel = "nightly-2023-10-05"
--------------------------------------------------------------------------------
/core/rs/core/src/alter.rs:
--------------------------------------------------------------------------------
1 | // Not yet fully migrated from `crsqlite.c`
2 |
3 | use alloc::boxed::Box;
4 | use alloc::format;
5 | use alloc::string::String;
6 | use alloc::vec::Vec;
7 | use core::ffi::{c_char, c_int, CStr};
8 | use core::mem;
9 | #[cfg(not(feature = "std"))]
10 | use num_traits::FromPrimitive;
11 | use sqlite_nostd::{sqlite3, Connection, ResultCode, StrRef};
12 |
13 | use crate::c::crsql_ExtData;
14 | use crate::db_version::fill_db_version_if_needed;
15 | use crate::tableinfo::{crsql_ensure_table_infos_are_up_to_date, TableInfo};
16 |
17 | #[no_mangle]
18 | pub unsafe extern "C" fn crsql_compact_post_alter(
19 | db: *mut sqlite3,
20 | tbl_name: *const c_char,
21 | ext_data: *mut crsql_ExtData,
22 | errmsg: *mut *mut c_char,
23 | ) -> c_int {
24 | match compact_post_alter(db, tbl_name, ext_data, errmsg) {
25 | Ok(rc) | Err(rc) => rc as c_int,
26 | }
27 | }
28 |
29 | unsafe fn compact_post_alter(
30 | db: *mut sqlite3,
31 | tbl_name: *const c_char,
32 | ext_data: *mut crsql_ExtData,
33 | errmsg: *mut *mut c_char,
34 | ) -> Result {
35 | let tbl_name_str = CStr::from_ptr(tbl_name).to_str()?;
36 | fill_db_version_if_needed(db, ext_data).or_else(|msg| {
37 | errmsg.set(&msg);
38 | Err(ResultCode::ERROR)
39 | })?;
40 | let current_db_version = (*ext_data).dbVersion;
41 |
42 | // If primary key columns change (in the schema)
43 | // We need to drop, re-create and backfill
44 | // the clock table.
45 | // A change in pk columns means a change in all identities
46 | // of all rows.
47 | // We can determine this by comparing unique index on lookaside table vs
48 | // pks on source table
49 | let stmt = db.prepare_v2(&format!(
50 | "SELECT count(name) FROM (
51 | SELECT name FROM pragma_table_info('{table_name}')
52 | WHERE pk > 0 AND name NOT IN
53 | (SELECT name FROM pragma_index_info('{table_name}__crsql_pks_pks'))
54 | UNION SELECT name FROM pragma_index_info('{table_name}__crsql_pks_pks') WHERE name NOT IN
55 | (SELECT name FROM pragma_table_info('{table_name}') WHERE pk > 0) AND name != 'col_name'
56 | );",
57 | table_name = crate::util::escape_ident_as_value(tbl_name_str),
58 | ))?;
59 | stmt.step()?;
60 |
61 | let pk_diff = stmt.column_int(0);
62 | // immediately drop stmt, otherwise clock table is considered locked.
63 | drop(stmt);
64 |
65 | if pk_diff > 0 {
66 | // drop the clock table so we can re-create it
67 | db.exec_safe(&format!(
68 | "DROP TABLE \"{table_name}__crsql_clock\";
69 | DROP TABLE \"{table_name}__crsql_pks\";",
70 | table_name = crate::util::escape_ident(tbl_name_str),
71 | ))?;
72 | } else {
73 | // clock table is still relevant but needs compacting
74 | // in case columns were removed during the migration
75 |
76 | // First delete entries that no longer have a column
77 | let sql = format!(
78 | "DELETE FROM \"{tbl_name_ident}__crsql_clock\" WHERE \"col_name\" NOT IN (
79 | SELECT name FROM pragma_table_info('{tbl_name_val}') UNION SELECT '{cl_sentinel}'
80 | )",
81 | tbl_name_ident = crate::util::escape_ident(tbl_name_str),
82 | tbl_name_val = crate::util::escape_ident_as_value(tbl_name_str),
83 | cl_sentinel = crate::c::DELETE_SENTINEL,
84 | );
85 | db.exec_safe(&sql)?;
86 |
87 | // Next delete entries that no longer have a row but keeping tombstones
88 | // TODO: if we move the sentinel metadata to the lookaside this becomes much simpler
89 | let mut sql = String::from(
90 | format!(
91 | "DELETE FROM \"{tbl_name}__crsql_clock\" WHERE (col_name != '-1' OR (col_name = '-1' AND col_version % 2 != 0))
92 | AND NOT EXISTS (SELECT 1 FROM \"{tbl_name}\" JOIN \"{tbl_name}__crsql_pks\" ON ",
93 | tbl_name = crate::util::escape_ident(tbl_name_str),
94 | ),
95 | );
96 | let c_rc = crsql_ensure_table_infos_are_up_to_date(db, ext_data, errmsg);
97 | if c_rc != ResultCode::OK as c_int {
98 | if let Some(rc) = ResultCode::from_i32(c_rc) {
99 | return Err(rc);
100 | }
101 | return Err(ResultCode::ERROR);
102 | }
103 | let table_infos =
104 | mem::ManuallyDrop::new(Box::from_raw((*ext_data).tableInfos as *mut Vec));
105 | let table_info = table_infos.iter().find(|x| x.tbl_name == tbl_name_str);
106 | if table_info.is_none() {
107 | return Err(ResultCode::ERROR);
108 | }
109 | // TODO: safe since we checked above but make more idiomatic
110 | let table_info = table_info.unwrap();
111 |
112 | // for each pk col, append \"%w\".\"%w\" = \"%w__crsql_pks\".\"%w\"
113 | // to the where clause then close the statement.
114 | for (i, col) in table_info.pks.iter().enumerate() {
115 | if i > 0 {
116 | sql.push_str(" AND ");
117 | }
118 |
119 | sql.push_str(&format!(
120 | "\"{tbl_name}\".\"{col_name}\" = \"{tbl_name}__crsql_pks\".\"{col_name}\"",
121 | tbl_name = crate::util::escape_ident(tbl_name_str),
122 | col_name = &col.name,
123 | ));
124 | }
125 | sql.push_str(
126 | &format!(
127 | " WHERE \"{tbl_name}__crsql_clock\".key = \"{tbl_name}__crsql_pks\".__crsql_key LIMIT 1)",
128 | tbl_name = crate::util::escape_ident(tbl_name_str)
129 | )
130 | );
131 | db.exec_safe(&sql)?;
132 |
133 | // now delete pk lookasides that no longer map to anything in the clock tables
134 | let sql = format!(
135 | "DELETE FROM \"{tbl_name}__crsql_pks\" WHERE __crsql_key NOT IN (
136 | SELECT key FROM \"{tbl_name}__crsql_clock\"
137 | )",
138 | tbl_name = crate::util::escape_ident(tbl_name_str),
139 | );
140 | db.exec_safe(&sql)?;
141 | }
142 |
143 | let stmt = db.prepare_v2(
144 | "INSERT OR REPLACE INTO crsql_master (key, value) VALUES ('pre_compact_dbversion', ?)",
145 | )?;
146 | stmt.bind_int64(1, current_db_version)?;
147 | stmt.step()?;
148 | Ok(ResultCode::OK)
149 | }
150 |
--------------------------------------------------------------------------------
/core/rs/core/src/changes_vtab_read.rs:
--------------------------------------------------------------------------------
1 | extern crate alloc;
2 | use crate::tableinfo::TableInfo;
3 | use alloc::format;
4 | use alloc::string::String;
5 | use alloc::vec;
6 | use alloc::vec::Vec;
7 | use sqlite::ResultCode;
8 |
9 | use sqlite_nostd as sqlite;
10 |
11 | fn crsql_changes_query_for_table(table_info: &TableInfo) -> Result {
12 | if table_info.pks.len() == 0 {
13 | // no primary keys? We can't get changes for a table w/o primary keys...
14 | // this should be an impossible case.
15 | return Err(ResultCode::ABORT);
16 | }
17 |
18 | let pk_list = crate::util::as_identifier_list(&table_info.pks, Some("pk_tbl."))?;
19 | // TODO: we can remove the self join if we put causal length in the primary key table
20 |
21 | // We LEFT JOIN and COALESCE the causal length
22 | // since we incorporated an optimization to not store causal length records
23 | // until they're required. I.e., do not store them until a delete
24 | // is actually issued. This cuts data weight quite a bit for
25 | // rows that never get removed.
26 | Ok(format!(
27 | "SELECT
28 | '{table_name_val}' as tbl,
29 | crsql_pack_columns({pk_list}) as pks,
30 | t1.col_name as cid,
31 | t1.col_version as col_vrsn,
32 | t1.db_version as db_vrsn,
33 | site_tbl.site_id as site_id,
34 | t1.key,
35 | t1.seq as seq,
36 | COALESCE(t2.col_version, 1) as cl
37 | FROM \"{table_name_ident}__crsql_clock\" AS t1
38 | JOIN \"{table_name_ident}__crsql_pks\" AS pk_tbl ON t1.key = pk_tbl.__crsql_key
39 | LEFT JOIN crsql_site_id AS site_tbl ON t1.site_id = site_tbl.ordinal
40 | LEFT JOIN \"{table_name_ident}__crsql_clock\" AS t2 ON
41 | t1.key = t2.key AND t2.col_name = '{sentinel}'",
42 | table_name_val = crate::util::escape_ident_as_value(&table_info.tbl_name),
43 | pk_list = pk_list,
44 | table_name_ident = crate::util::escape_ident(&table_info.tbl_name),
45 | sentinel = crate::c::INSERT_SENTINEL
46 | ))
47 | }
48 |
49 | pub fn changes_union_query(
50 | table_infos: &Vec,
51 | idx_str: &str,
52 | ) -> Result {
53 | let mut sub_queries = vec![];
54 |
55 | for table_info in table_infos {
56 | let query_part = crsql_changes_query_for_table(&table_info)?;
57 | sub_queries.push(query_part);
58 | }
59 |
60 | // Manually null-terminate the string so we don't have to copy it to create a CString.
61 | // We can just extract the raw bytes of the Rust string.
62 | return Ok(format!(
63 | "SELECT tbl, pks, cid, col_vrsn, db_vrsn, site_id, key, seq, cl FROM ({unions}) {idx_str}\0",
64 | unions = sub_queries.join(" UNION ALL "),
65 | idx_str = idx_str,
66 | ));
67 | }
68 |
--------------------------------------------------------------------------------
/core/rs/core/src/compare_values.rs:
--------------------------------------------------------------------------------
1 | use alloc::format;
2 | use alloc::string::String;
3 | use core::ffi::c_int;
4 | use sqlite::value;
5 | use sqlite::Value;
6 | use sqlite_nostd as sqlite;
7 |
8 | // TODO: add an integration test that ensures NULL == NULL!
9 | pub fn crsql_compare_sqlite_values(l: *mut sqlite::value, r: *mut sqlite::value) -> c_int {
10 | let l_type = l.value_type();
11 | let r_type = r.value_type();
12 |
13 | if l_type != r_type {
14 | // We swap the compare since we want null to be _less than_ all things
15 | // and null is assigned to ordinal 5 (greatest thing).
16 | return (r_type as i32) - (l_type as i32);
17 | }
18 |
19 | match l_type {
20 | sqlite::ColumnType::Blob => l.blob().cmp(r.blob()) as c_int,
21 | sqlite::ColumnType::Float => {
22 | let l_double = l.double();
23 | let r_double = r.double();
24 | if l_double < r_double {
25 | return -1;
26 | } else if l_double > r_double {
27 | return 1;
28 | }
29 | return 0;
30 | }
31 | sqlite::ColumnType::Integer => {
32 | let l_int = l.int64();
33 | let r_int = r.int64();
34 | // no subtraction since that could overflow the c_int return type
35 | if l_int < r_int {
36 | return -1;
37 | } else if l_int > r_int {
38 | return 1;
39 | }
40 | return 0;
41 | }
42 | sqlite::ColumnType::Null => 0,
43 | sqlite::ColumnType::Text => l.text().cmp(r.text()) as c_int,
44 | }
45 | }
46 |
47 | pub fn any_value_changed(left: &[*mut value], right: &[*mut value]) -> Result {
48 | if left.len() != right.len() {
49 | return Err(format!(
50 | "left and right values must have the same length: {} != {}",
51 | left.len(),
52 | right.len()
53 | ));
54 | }
55 |
56 | for (l, r) in left.iter().zip(right.iter()) {
57 | if crsql_compare_sqlite_values(*l, *r) != 0 {
58 | return Ok(true);
59 | }
60 | }
61 |
62 | Ok(false)
63 | }
64 |
--------------------------------------------------------------------------------
/core/rs/core/src/config.rs:
--------------------------------------------------------------------------------
1 | use alloc::format;
2 |
3 | use sqlite::{Connection, Context};
4 | use sqlite_nostd as sqlite;
5 | use sqlite_nostd::{ResultCode, Value};
6 |
7 | use crate::c::crsql_ExtData;
8 |
9 | pub const MERGE_EQUAL_VALUES: &str = "merge-equal-values";
10 |
11 | pub extern "C" fn crsql_config_set(
12 | ctx: *mut sqlite::context,
13 | argc: i32,
14 | argv: *mut *mut sqlite::value,
15 | ) {
16 | let args = sqlite::args!(argc, argv);
17 |
18 | let name = args[0].text();
19 |
20 | let value = match name {
21 | MERGE_EQUAL_VALUES => {
22 | let value = args[1];
23 | let ext_data = ctx.user_data() as *mut crsql_ExtData;
24 | unsafe { (*ext_data).mergeEqualValues = value.int() };
25 | value
26 | }
27 | _ => {
28 | ctx.result_error("Unknown setting name");
29 | ctx.result_error_code(ResultCode::ERROR);
30 | return;
31 | }
32 | };
33 |
34 | let db = ctx.db_handle();
35 | match insert_config_setting(db, name, value) {
36 | Ok(value) => {
37 | ctx.result_value(value);
38 | }
39 | Err(rc) => {
40 | ctx.result_error("Could not persist config in database");
41 | ctx.result_error_code(rc);
42 | return;
43 | }
44 | }
45 | }
46 |
47 | fn insert_config_setting(
48 | db: *mut sqlite_nostd::sqlite3,
49 | name: &str,
50 | value: *mut sqlite::value,
51 | ) -> Result<*mut sqlite::value, ResultCode> {
52 | let stmt =
53 | db.prepare_v2("INSERT OR REPLACE INTO crsql_master VALUES (?, ?) RETURNING value")?;
54 |
55 | stmt.bind_text(1, &format!("config.{name}"), sqlite::Destructor::TRANSIENT)?;
56 | stmt.bind_value(2, value)?;
57 |
58 | if let ResultCode::ROW = stmt.step()? {
59 | stmt.column_value(0)
60 | } else {
61 | Err(ResultCode::ERROR)
62 | }
63 | }
64 |
65 | pub extern "C" fn crsql_config_get(
66 | ctx: *mut sqlite::context,
67 | argc: i32,
68 | argv: *mut *mut sqlite::value,
69 | ) {
70 | let args = sqlite::args!(argc, argv);
71 |
72 | let name = args[0].text();
73 |
74 | match name {
75 | MERGE_EQUAL_VALUES => {
76 | let ext_data = ctx.user_data() as *mut crsql_ExtData;
77 | ctx.result_int(unsafe { (*ext_data).mergeEqualValues });
78 | }
79 | _ => {
80 | ctx.result_error("Unknown setting name");
81 | ctx.result_error_code(ResultCode::ERROR);
82 | return;
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/core/rs/core/src/consts.rs:
--------------------------------------------------------------------------------
1 | pub const TBL_SITE_ID: &'static str = "crsql_site_id";
2 | pub const TBL_SCHEMA: &'static str = "crsql_master";
3 | // pub const CRSQLITE_VERSION_0_15_0: i32 = 15_00_00;
4 | // pub const CRSQLITE_VERSION_0_13_0: i32 = 13_00_00;
5 | // MM_mm_pp_xx
6 | // so a 1.0.0 release is:
7 | // 01_00_00_00 -> 1000000
8 | // a 0.5 release is:
9 | // 00_05_00_00 -> 50000
10 | // a 0.5.1 is:
11 | // 00_05_01_00
12 | // and, if we ever need it, we can track individual builds of a patch release
13 | // 00_05_01_01
14 | pub const CRSQLITE_VERSION: i32 = 16_03_00;
15 | pub const CRSQLITE_VERSION_STR: &'static str = "0.16.3";
16 | pub const CRSQLITE_VERSION_0_15_0: i32 = 15_00_00;
17 |
18 | pub const SITE_ID_LEN: i32 = 16;
19 | pub const ROWID_SLAB_SIZE: i64 = 10000000000000;
20 | // db version is a signed 64bit int since sqlite doesn't support saving and
21 | // retrieving unsigned 64bit ints. (2^64 / 2) is a big enough number to write 1
22 | // million entries per second for 3,000 centuries.
23 | pub const MIN_POSSIBLE_DB_VERSION: i64 = 0;
24 | pub const MAX_TBL_NAME_LEN: i32 = 2048;
25 |
--------------------------------------------------------------------------------
/core/rs/core/src/create_crr.rs:
--------------------------------------------------------------------------------
1 | use core::ffi::c_char;
2 | use sqlite_nostd as sqlite;
3 | use sqlite_nostd::ResultCode;
4 |
5 | use crate::bootstrap::create_clock_table;
6 | use crate::tableinfo::{is_table_compatible, pull_table_info};
7 | use crate::triggers::create_triggers;
8 | use crate::{backfill_table, is_crr, remove_crr_triggers_if_exist};
9 |
10 | /**
11 | * Create a new crr --
12 | * all triggers, views, tables
13 | */
14 | pub fn create_crr(
15 | db: *mut sqlite::sqlite3,
16 | _schema: &str,
17 | table: &str,
18 | is_commit_alter: bool,
19 | no_tx: bool,
20 | err: *mut *mut c_char,
21 | ) -> Result {
22 | if !is_table_compatible(db, table, err)? {
23 | return Err(ResultCode::ERROR);
24 | }
25 | if is_crr(db, table)? {
26 | return Ok(ResultCode::OK);
27 | }
28 |
29 | // We do not / can not pull this from the cached set of table infos
30 | // since nothing would exist in it for a table not yet made into a crr.
31 | // TODO: Note: we can optimize out our `ensureTableInfosAreUpToDate` by mutating our ext data
32 | // when upgrading stuff to CRRs
33 | let table_info = pull_table_info(db, table, err)?;
34 |
35 | create_clock_table(db, &table_info, err)?;
36 | remove_crr_triggers_if_exist(db, table)?;
37 | create_triggers(db, &table_info, err)?;
38 |
39 | backfill_table(
40 | db,
41 | table,
42 | &table_info.pks,
43 | &table_info.non_pks,
44 | is_commit_alter,
45 | no_tx,
46 | )?;
47 |
48 | Ok(ResultCode::OK)
49 | }
50 |
--------------------------------------------------------------------------------
/core/rs/core/src/db_version.rs:
--------------------------------------------------------------------------------
1 | use core::ptr;
2 |
3 | use crate::alloc::string::ToString;
4 | use alloc::format;
5 | use alloc::string::String;
6 | use core::ffi::{c_char, c_int};
7 | use sqlite::ResultCode;
8 | use sqlite::StrRef;
9 | use sqlite::{sqlite3, Stmt};
10 | use sqlite_nostd as sqlite;
11 |
12 | use crate::c::crsql_ExtData;
13 | use crate::c::crsql_fetchPragmaDataVersion;
14 | use crate::c::crsql_fetchPragmaSchemaVersion;
15 | use crate::c::DB_VERSION_SCHEMA_VERSION;
16 | use crate::consts::MIN_POSSIBLE_DB_VERSION;
17 | use crate::ext_data::recreate_db_version_stmt;
18 |
19 | #[no_mangle]
20 | pub extern "C" fn crsql_fill_db_version_if_needed(
21 | db: *mut sqlite3,
22 | ext_data: *mut crsql_ExtData,
23 | errmsg: *mut *mut c_char,
24 | ) -> c_int {
25 | match fill_db_version_if_needed(db, ext_data) {
26 | Ok(rc) => rc as c_int,
27 | Err(msg) => {
28 | errmsg.set(&msg);
29 | ResultCode::ERROR as c_int
30 | }
31 | }
32 | }
33 |
34 | #[no_mangle]
35 | pub extern "C" fn crsql_next_db_version(
36 | db: *mut sqlite3,
37 | ext_data: *mut crsql_ExtData,
38 | merging_version: sqlite::int64,
39 | errmsg: *mut *mut c_char,
40 | ) -> sqlite::int64 {
41 | match next_db_version(db, ext_data, Some(merging_version)) {
42 | Ok(version) => version,
43 | Err(msg) => {
44 | errmsg.set(&msg);
45 | -1
46 | }
47 | }
48 | }
49 |
50 | /**
51 | * Given this needs to do a pragma check, invoke it as little as possible.
52 | * TODO: We could optimize to only do a pragma check once per transaction.
53 | * Need to save some bit that states we checked the pragma already and reset on tx commit or rollback.
54 | */
55 | pub fn next_db_version(
56 | db: *mut sqlite3,
57 | ext_data: *mut crsql_ExtData,
58 | merging_version: Option,
59 | ) -> Result {
60 | fill_db_version_if_needed(db, ext_data)?;
61 |
62 | let mut ret = unsafe { (*ext_data).dbVersion + 1 };
63 | if ret < unsafe { (*ext_data).pendingDbVersion } {
64 | ret = unsafe { (*ext_data).pendingDbVersion };
65 | }
66 | if let Some(merging_version) = merging_version {
67 | if ret < merging_version {
68 | ret = merging_version;
69 | }
70 | }
71 | unsafe {
72 | (*ext_data).pendingDbVersion = ret;
73 | }
74 | Ok(ret)
75 | }
76 |
77 | pub fn fill_db_version_if_needed(
78 | db: *mut sqlite3,
79 | ext_data: *mut crsql_ExtData,
80 | ) -> Result {
81 | unsafe {
82 | let rc = crsql_fetchPragmaDataVersion(db, ext_data);
83 | if rc == -1 {
84 | return Err("failed to fetch PRAGMA data_version".to_string());
85 | }
86 | if (*ext_data).dbVersion != -1 && rc == 0 {
87 | return Ok(ResultCode::OK);
88 | }
89 | fetch_db_version_from_storage(db, ext_data)
90 | }
91 | }
92 |
93 | pub fn fetch_db_version_from_storage(
94 | db: *mut sqlite3,
95 | ext_data: *mut crsql_ExtData,
96 | ) -> Result {
97 | unsafe {
98 | let schema_changed = if (*ext_data).pDbVersionStmt == ptr::null_mut() {
99 | 1 as c_int
100 | } else {
101 | crsql_fetchPragmaSchemaVersion(db, ext_data, DB_VERSION_SCHEMA_VERSION)
102 | };
103 |
104 | if schema_changed < 0 {
105 | return Err("failed to fetch the pragma schema version".to_string());
106 | }
107 |
108 | if schema_changed > 0 {
109 | match recreate_db_version_stmt(db, ext_data) {
110 | Ok(ResultCode::DONE) => {
111 | // this means there are no clock tables / this is a clean db
112 | (*ext_data).dbVersion = 0;
113 | return Ok(ResultCode::OK);
114 | }
115 | Ok(_) => {}
116 | Err(rc) => return Err(format!("failed to recreate db version stmt: {}", rc)),
117 | }
118 | }
119 |
120 | let db_version_stmt = (*ext_data).pDbVersionStmt;
121 | let rc = db_version_stmt.step();
122 | match rc {
123 | // no rows? We're a fresh db with the min starting version
124 | Ok(ResultCode::DONE) => {
125 | db_version_stmt.reset().or_else(|rc| {
126 | Err(format!(
127 | "failed to reset db version stmt after DONE: {}",
128 | rc
129 | ))
130 | })?;
131 | (*ext_data).dbVersion = MIN_POSSIBLE_DB_VERSION;
132 | Ok(ResultCode::OK)
133 | }
134 | // got a row? It is our db version.
135 | Ok(ResultCode::ROW) => {
136 | (*ext_data).dbVersion = db_version_stmt.column_int64(0);
137 | db_version_stmt
138 | .reset()
139 | .or_else(|rc| Err(format!("failed to reset db version stmt after ROW: {}", rc)))
140 | }
141 | // Not row or done? Something went wrong.
142 | Ok(rc) | Err(rc) => {
143 | db_version_stmt.reset().or_else(|rc| {
144 | Err(format!("failed to reset db version stmt after ROW: {}", rc))
145 | })?;
146 | Err(format!("failed to step db version stmt: {}", rc))
147 | }
148 | }
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/core/rs/core/src/ext_data.rs:
--------------------------------------------------------------------------------
1 | extern crate alloc;
2 | use crate::alloc::string::ToString;
3 | use alloc::vec;
4 | use core::ffi::c_int;
5 | use core::ptr::null_mut;
6 |
7 | use sqlite::{sqlite3, Connection, ResultCode, Stmt};
8 | use sqlite_nostd as sqlite;
9 |
10 | use crate::{c::crsql_ExtData, util::get_db_version_union_query};
11 |
12 | #[no_mangle]
13 | pub extern "C" fn crsql_recreate_db_version_stmt(
14 | db: *mut sqlite3,
15 | ext_data: *mut crsql_ExtData,
16 | ) -> c_int {
17 | match recreate_db_version_stmt(db, ext_data) {
18 | Ok(ResultCode::DONE) => -1, // negative 1 means no clock tables exist and there is nothing to fetch
19 | Ok(rc) | Err(rc) => rc as c_int,
20 | }
21 | }
22 |
23 | pub fn recreate_db_version_stmt(
24 | db: *mut sqlite3,
25 | ext_data: *mut crsql_ExtData,
26 | ) -> Result {
27 | let clock_tables_stmt = unsafe { (*ext_data).pSelectClockTablesStmt };
28 | let db_version_stmt = unsafe { (*ext_data).pDbVersionStmt };
29 |
30 | db_version_stmt.finalize()?;
31 | unsafe {
32 | (*ext_data).pDbVersionStmt = null_mut();
33 | }
34 |
35 | let mut clock_tbl_names = vec![];
36 | loop {
37 | match clock_tables_stmt.step() {
38 | Ok(ResultCode::DONE) => {
39 | clock_tables_stmt.reset()?;
40 | if clock_tbl_names.len() == 0 {
41 | return Ok(ResultCode::DONE);
42 | }
43 | break;
44 | }
45 | Ok(ResultCode::ROW) => {
46 | clock_tbl_names.push(clock_tables_stmt.column_text(0).to_string());
47 | }
48 | Ok(rc) | Err(rc) => {
49 | clock_tables_stmt.reset()?;
50 | return Err(rc);
51 | }
52 | }
53 | }
54 |
55 | let union = get_db_version_union_query(&clock_tbl_names);
56 |
57 | let db_version_stmt = db.prepare_v3(&union, sqlite::PREPARE_PERSISTENT)?;
58 | unsafe {
59 | (*ext_data).pDbVersionStmt = db_version_stmt.into_raw();
60 | }
61 |
62 | Ok(ResultCode::OK)
63 | }
64 |
--------------------------------------------------------------------------------
/core/rs/core/src/is_crr.rs:
--------------------------------------------------------------------------------
1 | use alloc::format;
2 | use sqlite::Connection;
3 | use sqlite_nostd as sqlite;
4 | use sqlite_nostd::ResultCode;
5 |
6 | /**
7 | * Given a table name, returns whether or not it has already
8 | * been upgraded to a CRR.
9 | */
10 | pub fn is_crr(db: *mut sqlite::sqlite3, table: &str) -> Result {
11 | let stmt =
12 | db.prepare_v2("SELECT count(*) FROM sqlite_master WHERE type = 'trigger' AND name = ?")?;
13 | stmt.bind_text(
14 | 1,
15 | &format!("{}__crsql_itrig", table),
16 | sqlite::Destructor::TRANSIENT,
17 | )?;
18 | stmt.step()?;
19 | let count = stmt.column_int(0);
20 |
21 | if count == 0 {
22 | Ok(false)
23 | } else {
24 | Ok(true)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/core/rs/core/src/local_writes/after_delete.rs:
--------------------------------------------------------------------------------
1 | use alloc::string::String;
2 | use core::ffi::c_int;
3 | use sqlite::sqlite3;
4 | use sqlite::value;
5 | use sqlite::Context;
6 | use sqlite::ResultCode;
7 | use sqlite_nostd as sqlite;
8 |
9 | use crate::{c::crsql_ExtData, tableinfo::TableInfo};
10 |
11 | use super::bump_seq;
12 | use super::trigger_fn_preamble;
13 |
14 | /**
15 | * crsql_after_delete("table", old_pk_values...)
16 | */
17 | pub unsafe extern "C" fn x_crsql_after_delete(
18 | ctx: *mut sqlite::context,
19 | argc: c_int,
20 | argv: *mut *mut sqlite::value,
21 | ) {
22 | let result = trigger_fn_preamble(ctx, argc, argv, |table_info, values, ext_data| {
23 | after_delete(ctx.db_handle(), ext_data, table_info, &values[1..])
24 | });
25 |
26 | match result {
27 | Ok(_) => {
28 | ctx.result_int64(0);
29 | }
30 | Err(msg) => {
31 | ctx.result_error(&msg);
32 | }
33 | }
34 | }
35 |
36 | fn after_delete(
37 | db: *mut sqlite3,
38 | ext_data: *mut crsql_ExtData,
39 | tbl_info: &TableInfo,
40 | pks_old: &[*mut value],
41 | ) -> Result {
42 | let db_version = crate::db_version::next_db_version(db, ext_data, None)?;
43 | let seq = bump_seq(ext_data);
44 | let key = tbl_info
45 | .get_or_create_key_via_raw_values(db, pks_old)
46 | .or_else(|_| Err("failed geteting or creating lookaside key"))?;
47 |
48 | let mark_locally_deleted_stmt_ref = tbl_info
49 | .get_mark_locally_deleted_stmt(db)
50 | .or_else(|_e| Err("failed to get mark_locally_deleted_stmt"))?;
51 | let mark_locally_deleted_stmt = mark_locally_deleted_stmt_ref
52 | .as_ref()
53 | .ok_or("Failed to deref sentinel stmt")?;
54 | mark_locally_deleted_stmt
55 | .bind_int64(1, key)
56 | .and_then(|_| mark_locally_deleted_stmt.bind_int64(2, db_version))
57 | .and_then(|_| mark_locally_deleted_stmt.bind_int(3, seq))
58 | .and_then(|_| mark_locally_deleted_stmt.bind_int64(4, db_version))
59 | .and_then(|_| mark_locally_deleted_stmt.bind_int(5, seq))
60 | .or_else(|_| Err("failed binding to mark locally deleted stmt"))?;
61 | super::step_trigger_stmt(mark_locally_deleted_stmt)?;
62 |
63 | // now actually delete the row metadata
64 | let drop_clocks_stmt_ref = tbl_info
65 | .get_merge_delete_drop_clocks_stmt(db)
66 | .or_else(|_e| Err("failed to get mark_locally_deleted_stmt"))?;
67 | let drop_clocks_stmt = drop_clocks_stmt_ref
68 | .as_ref()
69 | .ok_or("Failed to deref sentinel stmt")?;
70 |
71 | drop_clocks_stmt
72 | .bind_int64(1, key)
73 | .or_else(|_e| Err("failed to bind pks to drop_clocks_stmt"))?;
74 | super::step_trigger_stmt(drop_clocks_stmt)
75 | }
76 |
--------------------------------------------------------------------------------
/core/rs/core/src/local_writes/after_insert.rs:
--------------------------------------------------------------------------------
1 | use alloc::string::String;
2 | use core::ffi::c_int;
3 | use sqlite::sqlite3;
4 | use sqlite::value;
5 | use sqlite::Context;
6 | use sqlite::ResultCode;
7 | use sqlite_nostd as sqlite;
8 |
9 | use crate::{c::crsql_ExtData, tableinfo::TableInfo};
10 |
11 | use super::bump_seq;
12 | use super::trigger_fn_preamble;
13 |
14 | /**
15 | * crsql_after_insert("table", pk_values...)
16 | */
17 | pub unsafe extern "C" fn x_crsql_after_insert(
18 | ctx: *mut sqlite::context,
19 | argc: c_int,
20 | argv: *mut *mut sqlite::value,
21 | ) {
22 | let result = trigger_fn_preamble(ctx, argc, argv, |table_info, values, ext_data| {
23 | after_insert(ctx.db_handle(), ext_data, table_info, &values[1..])
24 | });
25 |
26 | match result {
27 | Ok(_) => {
28 | ctx.result_int64(0);
29 | }
30 | Err(msg) => {
31 | ctx.result_error(&msg);
32 | }
33 | }
34 | }
35 |
36 | fn after_insert(
37 | db: *mut sqlite3,
38 | ext_data: *mut crsql_ExtData,
39 | tbl_info: &TableInfo,
40 | pks_new: &[*mut value],
41 | ) -> Result {
42 | let db_version = crate::db_version::next_db_version(db, ext_data, None)?;
43 | let (create_record_existed, key_new) = tbl_info
44 | .get_or_create_key_for_insert(db, pks_new)
45 | .or_else(|_| Err("failed geteting or creating lookaside key"))?;
46 | if tbl_info.non_pks.len() == 0 {
47 | let seq = bump_seq(ext_data);
48 | // just a sentinel record
49 | return super::mark_new_pk_row_created(db, tbl_info, key_new, db_version, seq);
50 | } else if create_record_existed {
51 | // update the create record since it already exists.
52 | let seq = bump_seq(ext_data);
53 | update_create_record(db, tbl_info, key_new, db_version, seq)?;
54 | }
55 |
56 | // now for each non-pk column, create or update the column record
57 | for col in tbl_info.non_pks.iter() {
58 | let seq = bump_seq(ext_data);
59 | super::mark_locally_updated(db, tbl_info, key_new, col, db_version, seq)?;
60 | }
61 | Ok(ResultCode::OK)
62 | }
63 |
64 | fn update_create_record(
65 | db: *mut sqlite3,
66 | tbl_info: &TableInfo,
67 | new_key: sqlite::int64,
68 | db_version: sqlite::int64,
69 | seq: i32,
70 | ) -> Result {
71 | let update_create_record_stmt_ref = tbl_info
72 | .get_maybe_mark_locally_reinserted_stmt(db)
73 | .or_else(|_e| Err("failed to get update_create_record_stmt"))?;
74 | let update_create_record_stmt = update_create_record_stmt_ref
75 | .as_ref()
76 | .ok_or("Failed to deref update_create_record_stmt")?;
77 |
78 | update_create_record_stmt
79 | .bind_int64(1, db_version)
80 | .and_then(|_| update_create_record_stmt.bind_int(2, seq))
81 | .and_then(|_| update_create_record_stmt.bind_int64(3, new_key))
82 | .and_then(|_| {
83 | update_create_record_stmt.bind_text(
84 | 4,
85 | crate::c::INSERT_SENTINEL,
86 | sqlite::Destructor::STATIC,
87 | )
88 | })
89 | .or_else(|_e| Err("failed binding to update_create_record_stmt"))?;
90 |
91 | super::step_trigger_stmt(update_create_record_stmt)
92 | }
93 |
--------------------------------------------------------------------------------
/core/rs/core/src/local_writes/mod.rs:
--------------------------------------------------------------------------------
1 | use core::ffi::{c_char, c_int};
2 | use core::mem::ManuallyDrop;
3 |
4 | use crate::alloc::string::ToString;
5 | use crate::c::crsql_ExtData;
6 | use crate::stmt_cache::reset_cached_stmt;
7 | use alloc::boxed::Box;
8 | use alloc::format;
9 | use alloc::string::String;
10 | use alloc::vec::Vec;
11 | use sqlite::sqlite3;
12 | use sqlite::{Context, ManagedStmt, Value};
13 | use sqlite_nostd as sqlite;
14 | use sqlite_nostd::ResultCode;
15 |
16 | use crate::tableinfo::{crsql_ensure_table_infos_are_up_to_date, ColumnInfo, TableInfo};
17 |
18 | pub mod after_delete;
19 | pub mod after_insert;
20 | pub mod after_update;
21 |
22 | fn trigger_fn_preamble(
23 | ctx: *mut sqlite::context,
24 | argc: c_int,
25 | argv: *mut *mut sqlite::value,
26 | f: F,
27 | ) -> Result
28 | where
29 | F: Fn(&TableInfo, &[*mut sqlite::value], *mut crsql_ExtData) -> Result,
30 | {
31 | if argc < 1 {
32 | return Err("expected at least 1 argument".to_string());
33 | }
34 |
35 | let values = sqlite::args!(argc, argv);
36 | let ext_data = sqlite::user_data(ctx) as *mut crsql_ExtData;
37 | let mut inner_err: *mut c_char = core::ptr::null_mut();
38 | let outer_err: *mut *mut c_char = &mut inner_err;
39 |
40 | let rc = crsql_ensure_table_infos_are_up_to_date(ctx.db_handle(), ext_data, outer_err);
41 | if rc != ResultCode::OK as c_int {
42 | return Err(format!(
43 | "failed to ensure table infos are up to date: {}",
44 | rc
45 | ));
46 | }
47 |
48 | let table_infos =
49 | unsafe { ManuallyDrop::new(Box::from_raw((*ext_data).tableInfos as *mut Vec)) };
50 | let table_name = values[0].text();
51 | let table_info = match table_infos.iter().find(|t| &(t.tbl_name) == table_name) {
52 | Some(t) => t,
53 | None => {
54 | return Err(format!("table {} not found", table_name));
55 | }
56 | };
57 |
58 | f(table_info, &values, ext_data)
59 | }
60 |
61 | fn step_trigger_stmt(stmt: &ManagedStmt) -> Result {
62 | match stmt.step() {
63 | Ok(ResultCode::DONE) => {
64 | reset_cached_stmt(stmt.stmt)
65 | .or_else(|_e| Err("done -- unable to reset cached trigger stmt"))?;
66 | Ok(ResultCode::OK)
67 | }
68 | Ok(code) | Err(code) => {
69 | reset_cached_stmt(stmt.stmt)
70 | .or_else(|_e| Err("error -- unable to reset cached trigger stmt"))?;
71 | Err(format!(
72 | "unexpected result code from tigger_stmt.step: {}",
73 | code
74 | ))
75 | }
76 | }
77 | }
78 |
79 | fn mark_new_pk_row_created(
80 | db: *mut sqlite3,
81 | tbl_info: &TableInfo,
82 | key_new: sqlite::int64,
83 | db_version: i64,
84 | seq: i32,
85 | ) -> Result {
86 | let mark_locally_created_stmt_ref = tbl_info
87 | .get_mark_locally_created_stmt(db)
88 | .or_else(|_e| Err("failed to get mark_locally_created_stmt"))?;
89 | let mark_locally_created_stmt = mark_locally_created_stmt_ref
90 | .as_ref()
91 | .ok_or("Failed to deref sentinel stmt")?;
92 |
93 | mark_locally_created_stmt
94 | .bind_int64(1, key_new)
95 | .and_then(|_| mark_locally_created_stmt.bind_int64(2, db_version))
96 | .and_then(|_| mark_locally_created_stmt.bind_int(3, seq))
97 | .and_then(|_| mark_locally_created_stmt.bind_int64(4, db_version))
98 | .and_then(|_| mark_locally_created_stmt.bind_int(5, seq))
99 | .or_else(|_| Err("failed binding to mark_locally_created_stmt"))?;
100 | step_trigger_stmt(mark_locally_created_stmt)
101 | }
102 |
103 | fn bump_seq(ext_data: *mut crsql_ExtData) -> c_int {
104 | unsafe {
105 | (*ext_data).seq += 1;
106 | (*ext_data).seq - 1
107 | }
108 | }
109 |
110 | #[allow(non_snake_case)]
111 | fn mark_locally_updated(
112 | db: *mut sqlite3,
113 | tbl_info: &TableInfo,
114 | new_key: sqlite::int64,
115 | col_info: &ColumnInfo,
116 | db_version: sqlite::int64,
117 | seq: i32,
118 | ) -> Result {
119 | let mark_locally_updated_stmt_ref = tbl_info
120 | .get_mark_locally_updated_stmt(db)
121 | .or_else(|_e| Err("failed to get mark_locally_updated_stmt"))?;
122 | let mark_locally_updated_stmt = mark_locally_updated_stmt_ref
123 | .as_ref()
124 | .ok_or("Failed to deref sentinel stmt")?;
125 |
126 | mark_locally_updated_stmt
127 | .bind_int64(1, new_key)
128 | .and_then(|_| {
129 | mark_locally_updated_stmt.bind_text(2, &col_info.name, sqlite::Destructor::STATIC)
130 | })
131 | .and_then(|_| mark_locally_updated_stmt.bind_int64(3, db_version))
132 | .and_then(|_| mark_locally_updated_stmt.bind_int(4, seq))
133 | .and_then(|_| mark_locally_updated_stmt.bind_int64(5, db_version))
134 | .and_then(|_| mark_locally_updated_stmt.bind_int(6, seq))
135 | .or_else(|_| Err("failed binding to mark_locally_updated_stmt"))?;
136 | step_trigger_stmt(mark_locally_updated_stmt)
137 | }
138 |
--------------------------------------------------------------------------------
/core/rs/core/src/sha.rs:
--------------------------------------------------------------------------------
1 | // The sha of the commit that this version of crsqlite was built from.
2 | pub const SHA: &'static str = core::env!("CRSQLITE_COMMIT_SHA");
3 |
--------------------------------------------------------------------------------
/core/rs/core/src/stmt_cache.rs:
--------------------------------------------------------------------------------
1 | extern crate alloc;
2 | use alloc::vec::Vec;
3 | use core::mem::ManuallyDrop;
4 |
5 | use alloc::boxed::Box;
6 | use sqlite::Stmt;
7 | use sqlite_nostd as sqlite;
8 | use sqlite_nostd::ResultCode;
9 |
10 | use crate::c::crsql_ExtData;
11 | use crate::tableinfo::TableInfo;
12 |
13 | // Finalize prepared statements attached to table infos.
14 | // Do not drop the table infos.
15 | // We do this explicitly since `drop` cannot return an error and we want to
16 | // return the error / not panic.
17 | #[no_mangle]
18 | pub extern "C" fn crsql_clear_stmt_cache(ext_data: *mut crsql_ExtData) {
19 | let tbl_infos =
20 | unsafe { ManuallyDrop::new(Box::from_raw((*ext_data).tableInfos as *mut Vec)) };
21 | for tbl_info in tbl_infos.iter() {
22 | // TODO: return an error.
23 | let _ = tbl_info.clear_stmts();
24 | }
25 | }
26 |
27 | pub fn reset_cached_stmt(stmt: *mut sqlite::stmt) -> Result {
28 | if stmt.is_null() {
29 | return Ok(ResultCode::OK);
30 | }
31 | stmt.clear_bindings()?;
32 | stmt.reset()
33 | }
34 |
--------------------------------------------------------------------------------
/core/rs/core/src/teardown.rs:
--------------------------------------------------------------------------------
1 | use sqlite_nostd as sqlite;
2 | use sqlite_nostd::{Connection, ResultCode};
3 | extern crate alloc;
4 | use alloc::format;
5 |
6 | pub fn remove_crr_clock_table_if_exists(
7 | db: *mut sqlite::sqlite3,
8 | table: &str,
9 | ) -> Result {
10 | let escaped_table = crate::util::escape_ident(table);
11 | db.exec_safe(&format!(
12 | "DROP TABLE IF EXISTS \"{table}__crsql_clock\"",
13 | table = escaped_table
14 | ))?;
15 | db.exec_safe(&format!(
16 | "DROP TABLE IF EXISTS \"{table}__crsql_pks\"",
17 | table = escaped_table
18 | ))
19 | }
20 |
21 | pub fn remove_crr_triggers_if_exist(
22 | db: *mut sqlite::sqlite3,
23 | table: &str,
24 | ) -> Result {
25 | let escaped_table = crate::util::escape_ident(table);
26 |
27 | db.exec_safe(&format!(
28 | "DROP TRIGGER IF EXISTS \"{table}__crsql_itrig\"",
29 | table = escaped_table
30 | ))?;
31 |
32 | db.exec_safe(&format!(
33 | "DROP TRIGGER IF EXISTS \"{table}__crsql_utrig\"",
34 | table = escaped_table
35 | ))?;
36 |
37 | // get all columns of table
38 | // iterate pk cols
39 | // drop triggers against those pk cols
40 | let stmt = db.prepare_v2("SELECT name FROM pragma_table_info(?) WHERE pk > 0")?;
41 | stmt.bind_text(1, table, sqlite::Destructor::STATIC)?;
42 | while stmt.step()? == ResultCode::ROW {
43 | let col_name = stmt.column_text(0)?;
44 | db.exec_safe(&format!(
45 | "DROP TRIGGER IF EXISTS \"{tbl_name}_{col_name}__crsql_utrig\"",
46 | tbl_name = crate::util::escape_ident(table),
47 | col_name = crate::util::escape_ident(col_name),
48 | ))?;
49 | }
50 |
51 | db.exec_safe(&format!(
52 | "DROP TRIGGER IF EXISTS \"{table}__crsql_dtrig\"",
53 | table = escaped_table
54 | ))
55 | }
56 |
--------------------------------------------------------------------------------
/core/rs/core/src/test_exports.rs:
--------------------------------------------------------------------------------
1 | pub use crate::bootstrap;
2 | pub use crate::c;
3 | pub use crate::db_version;
4 | pub use crate::pack_columns;
5 | pub use crate::tableinfo;
6 |
--------------------------------------------------------------------------------
/core/rs/core/src/triggers.rs:
--------------------------------------------------------------------------------
1 | extern crate alloc;
2 | use alloc::format;
3 | use sqlite::Connection;
4 |
5 | use core::ffi::c_char;
6 |
7 | use sqlite::{sqlite3, ResultCode};
8 | use sqlite_nostd as sqlite;
9 |
10 | use crate::tableinfo::TableInfo;
11 |
12 | pub fn create_triggers(
13 | db: *mut sqlite3,
14 | table_info: &TableInfo,
15 | err: *mut *mut c_char,
16 | ) -> Result {
17 | create_insert_trigger(db, table_info, err)?;
18 | create_update_trigger(db, table_info, err)?;
19 | create_delete_trigger(db, table_info, err)
20 | }
21 |
22 | fn create_insert_trigger(
23 | db: *mut sqlite3,
24 | table_info: &TableInfo,
25 | _err: *mut *mut c_char,
26 | ) -> Result {
27 | let create_trigger_sql = format!(
28 | "CREATE TRIGGER IF NOT EXISTS \"{table_name}__crsql_itrig\"
29 | AFTER INSERT ON \"{table_name}\" WHEN crsql_internal_sync_bit() = 0
30 | BEGIN
31 | VALUES (crsql_after_insert('{table_name}', {pk_new_list}));
32 | END;",
33 | table_name = crate::util::escape_ident_as_value(&table_info.tbl_name),
34 | pk_new_list = crate::util::as_identifier_list(&table_info.pks, Some("NEW."))?
35 | );
36 |
37 | db.exec_safe(&create_trigger_sql)
38 | }
39 |
40 | fn create_update_trigger(
41 | db: *mut sqlite3,
42 | table_info: &TableInfo,
43 | _err: *mut *mut c_char,
44 | ) -> Result {
45 | let table_name = &table_info.tbl_name;
46 | let pk_columns = &table_info.pks;
47 | let non_pk_columns = &table_info.non_pks;
48 | let pk_new_list = crate::util::as_identifier_list(pk_columns, Some("NEW."))?;
49 | let pk_old_list = crate::util::as_identifier_list(pk_columns, Some("OLD."))?;
50 |
51 | let trigger_body = if non_pk_columns.is_empty() {
52 | format!(
53 | "VALUES (crsql_after_update('{table_name}', {pk_new_list}, {pk_old_list}))",
54 | table_name = crate::util::escape_ident_as_value(table_name),
55 | pk_new_list = pk_new_list,
56 | pk_old_list = pk_old_list,
57 | )
58 | } else {
59 | format!(
60 | "VALUES (crsql_after_update('{table_name}', {pk_new_list}, {pk_old_list}, {non_pk_new_list}, {non_pk_old_list}))",
61 | table_name = crate::util::escape_ident_as_value(table_name),
62 | pk_new_list = pk_new_list,
63 | pk_old_list = pk_old_list,
64 | non_pk_new_list = crate::util::as_identifier_list(non_pk_columns, Some("NEW."))?,
65 | non_pk_old_list = crate::util::as_identifier_list(non_pk_columns, Some("OLD."))?
66 | )
67 | };
68 | db.exec_safe(&format!(
69 | "CREATE TRIGGER IF NOT EXISTS \"{table_name}__crsql_utrig\"
70 | AFTER UPDATE ON \"{table_name}\" WHEN crsql_internal_sync_bit() = 0
71 | BEGIN
72 | {trigger_body};
73 | END;",
74 | table_name = crate::util::escape_ident(table_name),
75 | ))
76 | }
77 |
78 | fn create_delete_trigger(
79 | db: *mut sqlite3,
80 | table_info: &TableInfo,
81 | _err: *mut *mut c_char,
82 | ) -> Result {
83 | let table_name = &table_info.tbl_name;
84 | let pk_columns = &table_info.pks;
85 | let pk_old_list = crate::util::as_identifier_list(pk_columns, Some("OLD."))?;
86 |
87 | let create_trigger_sql = format!(
88 | "CREATE TRIGGER IF NOT EXISTS \"{table_name}__crsql_dtrig\"
89 | AFTER DELETE ON \"{table_name}\" WHEN crsql_internal_sync_bit() = 0
90 | BEGIN
91 | VALUES (crsql_after_delete('{table_name}', {pk_old_list}));
92 | END;",
93 | table_name = crate::util::escape_ident(table_name),
94 | pk_old_list = pk_old_list
95 | );
96 |
97 | db.exec_safe(&create_trigger_sql)
98 | }
99 |
--------------------------------------------------------------------------------
/core/rs/core/src/util.rs:
--------------------------------------------------------------------------------
1 | extern crate alloc;
2 |
3 | use crate::{alloc::string::ToString, tableinfo::ColumnInfo};
4 | use alloc::format;
5 | use alloc::string::String;
6 | use alloc::vec;
7 | use alloc::vec::Vec;
8 | use core::str::Utf8Error;
9 | use sqlite::{sqlite3, ColumnType, Connection, ResultCode};
10 | use sqlite_nostd as sqlite;
11 |
12 | pub fn get_dflt_value(
13 | db: *mut sqlite3,
14 | table: &str,
15 | col: &str,
16 | ) -> Result