├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .github ├── FUNDING.yml ├── actions-rs │ └── grcov.yml ├── dependabot.yml ├── ranger.yml └── workflows │ └── ci.yml ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── CITATION.cff ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSES ├── Apache-2.0.txt ├── BSD-2-Clause-Patent.txt ├── BlueOak-1.0.0.txt ├── CC-BY-4.0.txt └── CC0-1.0.txt ├── README.md ├── SECURITY.md ├── datom-c ├── .gitignore ├── Cargo.toml ├── cbindgen.toml ├── run_cbindgen.bat ├── run_cbindgen.sh └── src │ ├── connection.rs │ ├── database.rs │ ├── lib.rs │ ├── misc.rs │ ├── sled.rs │ └── structs.rs ├── datom-java ├── .gitattributes ├── .gitignore ├── Cargo.toml ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ ├── gradle-wrapper.jar.license │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── lib │ ├── build.gradle │ └── src │ │ ├── main │ │ └── java │ │ │ └── engineering │ │ │ └── lutris │ │ │ └── datom │ │ │ ├── Connection.java │ │ │ ├── Datom.java │ │ │ ├── DatomJNI.java │ │ │ ├── Fact.java │ │ │ ├── HelloWorld.java │ │ │ └── InvalidPlatformException.java │ │ └── test │ │ └── java │ │ └── engineering │ │ └── lutris │ │ └── datom │ │ ├── ConnectionTest.java │ │ ├── DatomTest.java │ │ ├── FactTest.java │ │ └── HelloWorldTest.java ├── settings.gradle ├── src │ ├── connection.rs │ ├── datom.rs │ ├── fact.rs │ ├── hello_world.rs │ └── lib.rs └── tests │ └── gradle.rs ├── datom-node ├── .gitignore ├── Cargo.toml ├── index.node.d.ts ├── package-lock.json ├── package-lock.json.license ├── package.json ├── package.json.license ├── src │ ├── index.ts │ └── lib.rs ├── tests │ ├── basic.test.js │ └── jest.rs ├── tsconfig.json └── tsconfig.json.license ├── datom ├── Cargo.toml ├── src │ ├── backends │ │ ├── mod.rs │ │ ├── redblacktreeset.rs │ │ ├── sled.rs │ │ └── tiered.rs │ ├── bin │ │ ├── peer_server.rs │ │ └── transactor.rs │ ├── builtin_idents.rs │ ├── lib.rs │ ├── merge_iters.rs │ ├── serial.rs │ ├── storage.rs │ └── types │ │ ├── attribute_iterator.rs │ │ ├── attribute_schema.rs │ │ ├── connection.rs │ │ ├── connection_error.rs │ │ ├── database.rs │ │ ├── datom.rs │ │ ├── datom_iterator.rs │ │ ├── datom_type.rs │ │ ├── eid.rs │ │ ├── entity.rs │ │ ├── entity_result.rs │ │ ├── fact.rs │ │ ├── id.rs │ │ ├── index.rs │ │ ├── lipsum.txt │ │ ├── mod.rs │ │ ├── query_error.rs │ │ ├── storage_error.rs │ │ ├── transaction.rs │ │ ├── transaction_error.rs │ │ ├── transaction_record.rs │ │ ├── transaction_result.rs │ │ └── value.rs └── tests │ ├── base_entity_api.rs │ └── common │ ├── data.rs │ ├── mod.rs │ └── schema.rs └── website └── index.html /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | # SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | # SPDX-FileContributor: Piper McCorkle 4 | 5 | FROM mcr.microsoft.com/vscode/devcontainers/rust:0-1 6 | 7 | RUN rustup default nightly 8 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | { 5 | "name": "Rust", 6 | "build": { 7 | "dockerfile": "Dockerfile" 8 | }, 9 | "runArgs": [ 10 | "--cap-add=SYS_PTRACE", 11 | "--security-opt", 12 | "seccomp=unconfined" 13 | ], 14 | 15 | "settings": { 16 | "lldb.executable": "/usr/bin/lldb", 17 | "files.watcherExclude": { 18 | "**/target/**": true 19 | }, 20 | "rust-analyzer.checkOnSave.command": "clippy" 21 | }, 22 | 23 | "extensions": [ 24 | "vadimcn.vscode-lldb", 25 | "mutantdino.resourcemonitor", 26 | "matklad.rust-analyzer", 27 | "tamasfe.even-better-toml", 28 | "serayuzgur.crates", 29 | "ecmel.vscode-html-css", 30 | "redhat.java" 31 | ], 32 | 33 | "remoteUser": "vscode" 34 | } 35 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | # SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | # SPDX-FileContributor: Piper McCorkle 4 | 5 | github: [LutrisEng] 6 | liberapay: lutriseng 7 | -------------------------------------------------------------------------------- /.github/actions-rs/grcov.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | # SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | # SPDX-FileContributor: Piper McCorkle 4 | 5 | ignore-not-existing: true 6 | llvm: true 7 | ignore: 8 | - "/*" 9 | - "C:/*" 10 | - "../*" 11 | - "datom-c/*" 12 | - "datom-java/*" 13 | - "datom-node/*" 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | # SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | # SPDX-FileContributor: Piper McCorkle 4 | 5 | version: 2 6 | updates: 7 | - package-ecosystem: "cargo" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" 11 | - package-ecosystem: "cargo" 12 | directory: "/datom" 13 | schedule: 14 | interval: "daily" 15 | - package-ecosystem: "cargo" 16 | directory: "/datom-c" 17 | schedule: 18 | interval: "daily" 19 | - package-ecosystem: "cargo" 20 | directory: "/datom-java" 21 | schedule: 22 | interval: "daily" 23 | - package-ecosystem: "gradle" 24 | directory: "/datom-java/lib" 25 | schedule: 26 | interval: "daily" 27 | - package-ecosystem: "cargo" 28 | directory: "/datom-node" 29 | schedule: 30 | interval: "daily" 31 | - package-ecosystem: "npm" 32 | directory: "/datom-node" 33 | schedule: 34 | interval: "daily" 35 | - package-ecosystem: "github-actions" 36 | directory: "/" 37 | schedule: 38 | interval: "daily" 39 | -------------------------------------------------------------------------------- /.github/ranger.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | # SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | # SPDX-FileContributor: Piper McCorkle 4 | 5 | sponsor_labels: 6 | - sponsor 7 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | # SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | # SPDX-FileContributor: Piper McCorkle 4 | 5 | name: CI 6 | 7 | on: 8 | push: 9 | branches: 10 | - main 11 | pull_request: 12 | schedule: 13 | - cron: "0 0 * * *" 14 | 15 | env: 16 | CARGO_TERM_COLOR: always 17 | 18 | jobs: 19 | check: 20 | name: Check 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v3 24 | - uses: actions-rs/toolchain@v1 25 | with: 26 | toolchain: nightly 27 | profile: minimal 28 | override: true 29 | - uses: Swatinem/rust-cache@v2 30 | - uses: actions-rs/cargo@v1 31 | with: 32 | command: check 33 | args: --all 34 | 35 | fmt: 36 | name: Lint 37 | runs-on: ubuntu-latest 38 | steps: 39 | - uses: actions/checkout@v3 40 | - uses: actions-rs/toolchain@v1 41 | with: 42 | toolchain: nightly 43 | components: rustfmt, clippy 44 | profile: minimal 45 | override: true 46 | - uses: Swatinem/rust-cache@v2 47 | - uses: actions-rs/cargo@v1 48 | with: 49 | command: fmt 50 | args: --all -- --check 51 | - uses: actions-rs/clippy-check@v1 52 | with: 53 | token: ${{ secrets.GITHUB_TOKEN }} 54 | args: --all 55 | name: Clippy 56 | 57 | # security_audit: 58 | # name: Security Audit 59 | # runs-on: ubuntu-latest 60 | # steps: 61 | # - uses: actions/checkout@v3 62 | # - uses: actions-rs/audit-check@v1 63 | # with: 64 | # token: ${{ secrets.GITHUB_TOKEN }} 65 | 66 | reuse: 67 | name: REUSE 68 | runs-on: ubuntu-latest 69 | steps: 70 | - uses: actions/checkout@v3 71 | - uses: actions/setup-python@v4 72 | - run: pip install reuse 73 | - run: reuse lint 74 | 75 | validate_citation: 76 | name: Validate CITATION.cff 77 | runs-on: ubuntu-latest 78 | steps: 79 | - uses: actions/checkout@v3 80 | - uses: LutrisEng/validate-cff@v1.0.0 81 | 82 | docs: 83 | name: Build Docs 84 | runs-on: ubuntu-latest 85 | steps: 86 | - uses: actions/checkout@v3 87 | - uses: actions-rs/toolchain@v1 88 | with: 89 | toolchain: nightly 90 | profile: minimal 91 | override: true 92 | - uses: Swatinem/rust-cache@v2 93 | - uses: actions-rs/cargo@v1 94 | with: 95 | command: doc 96 | args: --all 97 | - run: cp -R website/* target/doc/ 98 | - uses: peaceiris/actions-gh-pages@v3 99 | if: github.ref == 'refs/heads/main' 100 | with: 101 | github_token: ${{ secrets.GITHUB_TOKEN }} 102 | publish_dir: ./target/doc 103 | force_orphan: true 104 | user_name: "github-actions[bot]" 105 | user_email: "github-actions[bot]@users.noreply.github.com" 106 | commit_message: Auto-generated documentation 107 | 108 | coverage: 109 | name: Test with coverage 110 | runs-on: ubuntu-latest 111 | steps: 112 | - uses: actions/checkout@v3 113 | - uses: actions-rs/toolchain@v1 114 | with: 115 | toolchain: nightly 116 | profile: minimal 117 | override: true 118 | - uses: Swatinem/rust-cache@v2 119 | with: 120 | key: coverage 121 | - run: cargo test --verbose -p datom -- --nocapture 122 | env: 123 | CARGO_INCREMENTAL: "0" 124 | RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort" 125 | RUSTDOCFLAGS: "-Cpanic=abort" 126 | - id: coverage 127 | uses: actions-rs/grcov@v0.1 128 | - uses: coverallsapp/github-action@master 129 | with: 130 | github-token: ${{ secrets.GITHUB_TOKEN }} 131 | path-to-lcov: ${{ steps.coverage.outputs.report }} 132 | 133 | test: 134 | name: Test on ${{ matrix.os }} with Rust ${{ matrix.rust }} 135 | runs-on: ${{ matrix.os }} 136 | strategy: 137 | matrix: 138 | os: [ubuntu-latest, macos-latest, windows-latest] 139 | rust: 140 | # Ensure nightly doesn't break anything 141 | - nightly 142 | # Ensure nothing breaks in the next Rust release 143 | - beta 144 | # Ensure nothing breaks in the current Rust release 145 | - stable 146 | # Ensure the MSRV doesn't break 147 | - "1.65" 148 | steps: 149 | - uses: actions/checkout@v3 150 | - uses: actions-rs/toolchain@v1 151 | with: 152 | toolchain: ${{ matrix.rust }} 153 | profile: minimal 154 | override: true 155 | - run: rustup target add aarch64-unknown-linux-gnu 156 | - run: rustup target add x86_64-unknown-linux-gnu 157 | - run: rustup target add aarch64-apple-darwin 158 | - run: rustup target add x86_64-apple-darwin 159 | - run: rustup target add x86_64-pc-windows-gnu 160 | - uses: actions/setup-node@v3 161 | with: 162 | node-version: "18" 163 | cache: "npm" 164 | cache-dependency-path: "datom-node/package-lock.json" 165 | - uses: actions/setup-java@v3 166 | with: 167 | distribution: "adopt" 168 | java-version: "17" 169 | cache: "gradle" 170 | - uses: goto-bus-stop/setup-zig@v2 171 | - uses: Swatinem/rust-cache@v2 172 | - run: cargo install cargo-zigbuild 173 | - run: cargo test --all --verbose -- --nocapture 174 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | # SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | # SPDX-FileContributor: Piper McCorkle 4 | 5 | target 6 | roundtrip 7 | testdbs 8 | Cargo.lock 9 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | { 5 | "recommendations": [ 6 | "matklad.rust-analyzer", 7 | "vadimcn.vscode-lldb", 8 | "ecmel.vscode-html-css", 9 | "redhat.java" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "type": "lldb", 10 | "request": "launch", 11 | "name": "Debug unit tests in library 'datom'", 12 | "cargo": { 13 | "args": [ 14 | "test", 15 | "--no-run", 16 | "--lib", 17 | "--package=datom" 18 | ], 19 | "filter": { 20 | "name": "datom", 21 | "kind": "lib" 22 | } 23 | }, 24 | "args": [], 25 | "cwd": "${workspaceFolder}" 26 | }, 27 | { 28 | "type": "lldb", 29 | "request": "launch", 30 | "name": "Debug executable 'peer_server'", 31 | "cargo": { 32 | "args": [ 33 | "build", 34 | "--bin=peer_server", 35 | "--package=datom" 36 | ], 37 | "filter": { 38 | "name": "peer_server", 39 | "kind": "bin" 40 | } 41 | }, 42 | "args": [], 43 | "cwd": "${workspaceFolder}" 44 | }, 45 | { 46 | "type": "lldb", 47 | "request": "launch", 48 | "name": "Debug unit tests in executable 'peer_server'", 49 | "cargo": { 50 | "args": [ 51 | "test", 52 | "--no-run", 53 | "--bin=peer_server", 54 | "--package=datom" 55 | ], 56 | "filter": { 57 | "name": "peer_server", 58 | "kind": "bin" 59 | } 60 | }, 61 | "args": [], 62 | "cwd": "${workspaceFolder}" 63 | }, 64 | { 65 | "type": "lldb", 66 | "request": "launch", 67 | "name": "Debug executable 'transactor'", 68 | "cargo": { 69 | "args": [ 70 | "build", 71 | "--bin=transactor", 72 | "--package=datom" 73 | ], 74 | "filter": { 75 | "name": "transactor", 76 | "kind": "bin" 77 | } 78 | }, 79 | "args": [], 80 | "cwd": "${workspaceFolder}" 81 | }, 82 | { 83 | "type": "lldb", 84 | "request": "launch", 85 | "name": "Debug unit tests in executable 'transactor'", 86 | "cargo": { 87 | "args": [ 88 | "test", 89 | "--no-run", 90 | "--bin=transactor", 91 | "--package=datom" 92 | ], 93 | "filter": { 94 | "name": "transactor", 95 | "kind": "bin" 96 | } 97 | }, 98 | "args": [], 99 | "cwd": "${workspaceFolder}" 100 | }, 101 | { 102 | "type": "lldb", 103 | "request": "launch", 104 | "name": "Debug integration test 'sled_test'", 105 | "cargo": { 106 | "args": [ 107 | "test", 108 | "--no-run", 109 | "--test=sled_test", 110 | "--package=datom" 111 | ], 112 | "filter": { 113 | "name": "sled_test", 114 | "kind": "test" 115 | } 116 | }, 117 | "args": [], 118 | "cwd": "${workspaceFolder}" 119 | }, 120 | { 121 | "type": "lldb", 122 | "request": "launch", 123 | "name": "Debug benchmark 'serial'", 124 | "cargo": { 125 | "args": [ 126 | "test", 127 | "--no-run", 128 | "--bench=serial", 129 | "--package=datom" 130 | ], 131 | "filter": { 132 | "name": "serial", 133 | "kind": "bench" 134 | } 135 | }, 136 | "args": [], 137 | "cwd": "${workspaceFolder}" 138 | } 139 | ] 140 | } 141 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | { 5 | "rust-analyzer.checkOnSave.command": "clippy", 6 | "files.insertFinalNewline": true, 7 | "editor.formatOnSave": true, 8 | "editor.tabSize": 4, 9 | "java.configuration.updateBuildConfiguration": "automatic" 10 | } 11 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | # SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | # SPDX-FileContributor: Piper McCorkle 4 | 5 | cff-version: 1.2.0 6 | message: "If you use this software in your research, please cite it as below." 7 | authors: 8 | - name: "Lutris, Inc" 9 | address: "102 E Alamo St Ste 200A" 10 | city: "Brenham" 11 | region: "TX" 12 | post-code: 77833 13 | country: "US" 14 | email: "contact@lutris.engineering" 15 | website: "https://lutris.engineering" 16 | title: "datom-rs" 17 | version: 0.1.1-pre4 18 | date-released: 2021-08-20 19 | url: "https://os.lutris.engineering/datom-rs" 20 | repository-code: "https://github.com/LutrisEng/datom-rs" 21 | license: 22 | - BlueOak-1.0.0 23 | - BSD-2-Clause-Patent 24 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # Contributor Covenant Code of Conduct 5 | 6 | ## Our Pledge 7 | 8 | We as members, contributors, and leaders pledge to make participation in our 9 | community a harassment-free experience for everyone, regardless of age, body 10 | size, visible or invisible disability, ethnicity, sex characteristics, gender 11 | identity and expression, level of experience, education, socio-economic status, 12 | nationality, personal appearance, race, caste, color, religion, or sexual identity 13 | and orientation. 14 | 15 | We pledge to act and interact in ways that contribute to an open, welcoming, 16 | diverse, inclusive, and healthy community. 17 | 18 | ## Our Standards 19 | 20 | Examples of behavior that contributes to a positive environment for our 21 | community include: 22 | 23 | * Demonstrating empathy and kindness toward other people 24 | * Being respectful of differing opinions, viewpoints, and experiences 25 | * Giving and gracefully accepting constructive feedback 26 | * Accepting responsibility and apologizing to those affected by our mistakes, 27 | and learning from the experience 28 | * Focusing on what is best not just for us as individuals, but for the 29 | overall community 30 | 31 | Examples of unacceptable behavior include: 32 | 33 | * The use of sexualized language or imagery, and sexual attention or 34 | advances of any kind 35 | * Trolling, insulting or derogatory comments, and personal or political attacks 36 | * Public or private harassment 37 | * Publishing others' private information, such as a physical or email 38 | address, without their explicit permission 39 | * Other conduct which could reasonably be considered inappropriate in a 40 | professional setting 41 | 42 | ## Enforcement Responsibilities 43 | 44 | Community leaders are responsible for clarifying and enforcing our standards of 45 | acceptable behavior and will take appropriate and fair corrective action in 46 | response to any behavior that they deem inappropriate, threatening, offensive, 47 | or harmful. 48 | 49 | Community leaders have the right and responsibility to remove, edit, or reject 50 | comments, commits, code, wiki edits, issues, and other contributions that are 51 | not aligned to this Code of Conduct, and will communicate reasons for moderation 52 | decisions when appropriate. 53 | 54 | ## Scope 55 | 56 | This Code of Conduct applies within all community spaces, and also applies when 57 | an individual is officially representing the community in public spaces. 58 | Examples of representing our community include using an official e-mail address, 59 | posting via an official social media account, or acting as an appointed 60 | representative at an online or offline event. 61 | 62 | ## Enforcement 63 | 64 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 65 | reported to the community leaders responsible for enforcement at 66 | contact@lutris.engineering. 67 | All complaints will be reviewed and investigated promptly and fairly. 68 | 69 | All community leaders are obligated to respect the privacy and security of the 70 | reporter of any incident. 71 | 72 | ## Enforcement Guidelines 73 | 74 | Community leaders will follow these Community Impact Guidelines in determining 75 | the consequences for any action they deem in violation of this Code of Conduct: 76 | 77 | ### 1. Correction 78 | 79 | **Community Impact**: Use of inappropriate language or other behavior deemed 80 | unprofessional or unwelcome in the community. 81 | 82 | **Consequence**: A private, written warning from community leaders, providing 83 | clarity around the nature of the violation and an explanation of why the 84 | behavior was inappropriate. A public apology may be requested. 85 | 86 | ### 2. Warning 87 | 88 | **Community Impact**: A violation through a single incident or series 89 | of actions. 90 | 91 | **Consequence**: A warning with consequences for continued behavior. No 92 | interaction with the people involved, including unsolicited interaction with 93 | those enforcing the Code of Conduct, for a specified period of time. This 94 | includes avoiding interactions in community spaces as well as external channels 95 | like social media. Violating these terms may lead to a temporary or 96 | permanent ban. 97 | 98 | ### 3. Temporary Ban 99 | 100 | **Community Impact**: A serious violation of community standards, including 101 | sustained inappropriate behavior. 102 | 103 | **Consequence**: A temporary ban from any sort of interaction or public 104 | communication with the community for a specified period of time. No public or 105 | private interaction with the people involved, including unsolicited interaction 106 | with those enforcing the Code of Conduct, is allowed during this period. 107 | Violating these terms may lead to a permanent ban. 108 | 109 | ### 4. Permanent Ban 110 | 111 | **Community Impact**: Demonstrating a pattern of violation of community 112 | standards, including sustained inappropriate behavior, harassment of an 113 | individual, or aggression toward or disparagement of classes of individuals. 114 | 115 | **Consequence**: A permanent ban from any sort of public interaction within 116 | the community. 117 | 118 | ## Attribution 119 | 120 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 121 | version 2.1, available at 122 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 123 | 124 | Community Impact Guidelines were inspired by 125 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 126 | 127 | For answers to common questions about this code of conduct, see the FAQ at 128 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available 129 | at [https://www.contributor-covenant.org/translations][translations]. 130 | 131 | [homepage]: https://www.contributor-covenant.org 132 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 133 | [Mozilla CoC]: https://github.com/mozilla/diversity 134 | [FAQ]: https://www.contributor-covenant.org/faq 135 | [translations]: https://www.contributor-covenant.org/translations 136 | 137 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | datom-rs is a project of Lutris, Inc. 6 | 7 | Before you contribute to a Lutris project, you must sign the 8 | Contributor Certificate of Origin. The CCO simply certifies that you 9 | either own your contribution or have the rights to contribute it under 10 | the project's license. 11 | 12 | You can read the CCO on its [wiki page](https://wiki.lutris.engineering/wiki/Contributor_Certificate_of_Origin). 13 | A maintainer will get in touch with you with instructions on how to sign 14 | the CCO if your contribution is accepted into the project. 15 | 16 | In addition, you must add an `SPDX-FileContributor` comment to every 17 | file you modify. For example, if your name is John Doe, you contributed 18 | personally (not on behalf of an organization), and your email address is 19 | johndoe@mail.com, you would add the following comment: 20 | 21 | ```rs 22 | // SPDX-FileContributor: John Doe 23 | ``` 24 | 25 | If your name is John Doe, you contributed on behalf of Software Company, 26 | Inc., and your email address is jdoe@software.com, you would add the 27 | following comment: 28 | 29 | ```rs 30 | // SPDX-FileContributor: Software Company, Inc. / John Doe 31 | ``` 32 | 33 | If your name is John Doe, you contributed on behalf of Lutris 34 | Engineering, Inc., and your email address is john@lutris.engineering, 35 | you would add the following comment: 36 | 37 | ```rs 38 | // SPDX-FileContributor: John Doe 39 | ``` 40 | 41 | Contributions on behalf of Lutris are the only contributions 42 | exempt from adding the company's name to the `SPDX-FileContributor` 43 | comment. The `lutris.engineering` email domain name is considered 44 | "well-known" to refer to Lutris, Inc. 45 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | # SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | # SPDX-FileContributor: Piper McCorkle 4 | 5 | [workspace] 6 | members = [ 7 | "datom", 8 | "datom-c", 9 | "datom-java", 10 | "datom-node", 11 | ] 12 | 13 | [profile.release] 14 | lto = true 15 | -------------------------------------------------------------------------------- /LICENSES/Apache-2.0.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. 10 | 11 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. 12 | 13 | "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 14 | 15 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. 16 | 17 | "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. 18 | 19 | "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. 20 | 21 | "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). 22 | 23 | "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. 24 | 25 | "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." 26 | 27 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 28 | 29 | 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 30 | 31 | 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 32 | 33 | 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: 34 | 35 | (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and 36 | 37 | (b) You must cause any modified files to carry prominent notices stating that You changed the files; and 38 | 39 | (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and 40 | 41 | (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. 42 | 43 | You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 44 | 45 | 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 46 | 47 | 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 48 | 49 | 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 50 | 51 | 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 52 | 53 | 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. 54 | 55 | END OF TERMS AND CONDITIONS 56 | 57 | APPENDIX: How to apply the Apache License to your work. 58 | 59 | To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. 60 | 61 | Copyright [yyyy] [name of copyright owner] 62 | 63 | Licensed under the Apache License, Version 2.0 (the "License"); 64 | you may not use this file except in compliance with the License. 65 | You may obtain a copy of the License at 66 | 67 | http://www.apache.org/licenses/LICENSE-2.0 68 | 69 | Unless required by applicable law or agreed to in writing, software 70 | distributed under the License is distributed on an "AS IS" BASIS, 71 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 72 | See the License for the specific language governing permissions and 73 | limitations under the License. 74 | -------------------------------------------------------------------------------- /LICENSES/BSD-2-Clause-Patent.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | Subject to the terms and conditions of this license, each copyright holder and contributor hereby grants to those receiving rights under this license a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except for failure to satisfy the conditions of this license) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer this software, where such license applies only to those patent claims, already acquired or hereafter acquired, licensable by such copyright holder or contributor that are necessarily infringed by: 10 | 11 | (a) their Contribution(s) (the licensed copyrights of copyright holders and non-copyrightable additions of contributors, in source or binary form) alone; or 12 | 13 | (b) combination of their Contribution(s) with the work of authorship to which such Contribution(s) was added by such copyright holder or contributor, if, at the time the Contribution is added, such addition causes such combination to be necessarily infringed. The patent license shall not apply to any other combinations which include the Contribution. 14 | 15 | Except as expressly stated above, no rights or licenses from any copyright holder or contributor is granted under this license, whether expressly, by implication, estoppel or otherwise. 16 | 17 | DISCLAIMER 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 20 | -------------------------------------------------------------------------------- /LICENSES/BlueOak-1.0.0.txt: -------------------------------------------------------------------------------- 1 | # Blue Oak Model License 2 | 3 | Version 1.0.0 4 | 5 | ## Purpose 6 | 7 | This license gives everyone as much permission to work with 8 | this software as possible, while protecting contributors 9 | from liability. 10 | 11 | ## Acceptance 12 | 13 | In order to receive this license, you must agree to its 14 | rules. The rules of this license are both obligations 15 | under that agreement and conditions to your license. 16 | You must not do anything with this software that triggers 17 | a rule that you cannot or will not follow. 18 | 19 | ## Copyright 20 | 21 | Each contributor licenses you to do everything with this 22 | software that would otherwise infringe that contributor's 23 | copyright in it. 24 | 25 | ## Notices 26 | 27 | You must ensure that everyone who gets a copy of 28 | any part of this software from you, with or without 29 | changes, also gets the text of this license or a link to 30 | . 31 | 32 | ## Excuse 33 | 34 | If anyone notifies you in writing that you have not 35 | complied with [Notices](#notices), you can keep your 36 | license by taking all practical steps to comply within 30 37 | days after the notice. If you do not do so, your license 38 | ends immediately. 39 | 40 | ## Patent 41 | 42 | Each contributor licenses you to do everything with this 43 | software that would otherwise infringe any patent claims 44 | they can license or become able to license. 45 | 46 | ## Reliability 47 | 48 | No contributor can revoke this license. 49 | 50 | ## No Liability 51 | 52 | ***As far as the law allows, this software comes as is, 53 | without any warranty or condition, and no contributor 54 | will be liable to anyone for any damages related to this 55 | software or this license, under any kind of legal claim.*** 56 | -------------------------------------------------------------------------------- /LICENSES/CC-BY-4.0.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LutrisEng/datom-rs/6ab6e1b9d1277311af9f1191c1913a61fa3cc9c2/LICENSES/CC-BY-4.0.txt -------------------------------------------------------------------------------- /LICENSES/CC0-1.0.txt: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | # datom-rs 6 | 7 | 8 | CI status 10 | 11 | 12 | Coverage Status 13 | 14 | 15 | Version 16 | 17 | 18 | Libraries.io dependency status for latest release 20 | 21 | 22 | FOSSA Status 24 | 25 | 26 | License 27 | 28 | 29 | Matrix: #datom-rs:lutris.engineering 31 | 32 | 33 | IRC: ##datom.rs on libera.chat 35 | 36 | 37 | GitHub Sponsors 38 | 39 | 40 | Contributor Covenant 42 | 43 | 44 | ## An open-source database inspired by Datomic 45 | 46 | [Documentation](https://os.lutris.engineering/datom-rs/datom) 47 | 48 | datom-rs is currently under pre-release development. 49 | 50 | ## Installation 51 | 52 | Add the following to the `[dependencies]` section of your `Cargo.toml`: 53 | 54 | ```toml 55 | datom = "0.1" 56 | ``` 57 | 58 | ## MSRV 59 | 60 | datom-rs is supported with `rustc` 1.65 and newer. 61 | 62 | ## Sponsors 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 81 | 82 | 83 | 84 |
SponsorContribution
74 | 76 | Lutris, Inc 79 | 80 | Lutris runs the datom-rs project.
85 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | # Security Policy 6 | 7 | ## Supported Versions 8 | 9 | Since datom-rs is in alpha, we currently only support the latest published version. 10 | 11 | ## Reporting a Vulnerability 12 | 13 | You may report security vulnerabilities to the project's lead, Piper McCorkle, at piper@lutris.engineering. 14 | -------------------------------------------------------------------------------- /datom-c/.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | # SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | # SPDX-FileContributor: Piper McCorkle 4 | 5 | datom.h 6 | -------------------------------------------------------------------------------- /datom-c/Cargo.toml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | # SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | # SPDX-FileContributor: Piper McCorkle 4 | 5 | [package] 6 | name = "datom-c" 7 | version = "0.1.1-pre4" 8 | authors = ["Lutris, Inc "] 9 | edition = "2021" 10 | rust-version = "1.65" 11 | description = "C bindings for an open-source database inspired by Datomic" 12 | readme = "../README.md" 13 | homepage = "https://os.lutris.engineering/datom-rs/" 14 | repository = "https://github.com/LutrisEng/datom-rs" 15 | license = "BlueOak-1.0.0 OR BSD-2-Clause-Patent" 16 | keywords = ["database", "datomic"] 17 | categories = ["database"] 18 | publish = false 19 | 20 | [lib] 21 | crate-type = ["cdylib", "staticlib"] 22 | 23 | [dependencies] 24 | datom = { path = "../datom" } 25 | libc = "0.2" 26 | 27 | [build-dependencies] 28 | cbindgen = "0.24" 29 | -------------------------------------------------------------------------------- /datom-c/cbindgen.toml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | # SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | # SPDX-FileContributor: Piper McCorkle 4 | 5 | language = "C" 6 | -------------------------------------------------------------------------------- /datom-c/run_cbindgen.bat: -------------------------------------------------------------------------------- 1 | @rem SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | @rem SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | @rem SPDX-FileContributor: Piper McCorkle 4 | 5 | cbindgen --config cbindgen.toml --crate datom-c --output datom.h 6 | -------------------------------------------------------------------------------- /datom-c/run_cbindgen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # SPDX-FileCopyrightText: 2022 Lutris, Inc 3 | # SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 4 | # SPDX-FileContributor: Piper McCorkle 5 | 6 | set -euxo pipefail 7 | 8 | cbindgen --config cbindgen.toml --crate datom-c --output datom.h 9 | -------------------------------------------------------------------------------- /datom-c/src/connection.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | use datom::{new_dynamic_connection, ConnectionError, TransactionError}; 6 | 7 | use crate::structs::{Connection, Database, Storage, Transaction, TransactionResult}; 8 | 9 | #[no_mangle] 10 | pub extern "C" fn datom_connect(storage: Box) -> Box { 11 | Box::new(new_dynamic_connection(storage.s).into()) 12 | } 13 | 14 | #[no_mangle] 15 | pub extern "C" fn datom_disconnect(_: Box) {} 16 | 17 | #[no_mangle] 18 | pub extern "C" fn datom_db(conn: &'_ Connection) -> Option>> { 19 | let res: Result, ConnectionError> = (|| { 20 | let db = conn.c.db()?; 21 | Ok(Box::new(db.into())) 22 | })(); 23 | match res { 24 | Ok(d) => Some(d), 25 | Err(_) => { 26 | // update_last_connection_error(e.into()) 27 | None 28 | } 29 | } 30 | } 31 | 32 | #[no_mangle] 33 | pub extern "C" fn datom_as_of(conn: &'_ Connection, t: u64) -> Option>> { 34 | let res: Result, ConnectionError> = (|| { 35 | let db = conn.c.as_of(t)?; 36 | Ok(Box::new(db.into())) 37 | })(); 38 | match res { 39 | Ok(d) => Some(d), 40 | Err(_) => { 41 | // update_last_connection_error(e.into()) 42 | None 43 | } 44 | } 45 | } 46 | 47 | #[no_mangle] 48 | pub extern "C" fn datom_latest_t(conn: &Connection) -> u64 { 49 | let res: Result = conn.c.latest_t(); 50 | match res { 51 | Ok(t) => t, 52 | Err(_) => { 53 | // update_last_connection_error(e.into()) 54 | u64::MAX 55 | } 56 | } 57 | } 58 | 59 | #[no_mangle] 60 | pub extern "C" fn datom_transact( 61 | conn: &Connection, 62 | tx: Box, 63 | ) -> Option> { 64 | let res: Result, TransactionError> = (|| { 65 | let r = conn.c.transact_tx(tx.t)?; 66 | Ok(Box::new(r.into())) 67 | })(); 68 | match res { 69 | Ok(r) => Some(r), 70 | Err(_) => { 71 | // update_last_transaction_error(e.into()) 72 | None 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /datom-c/src/database.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | use datom::QueryError; 6 | 7 | use crate::structs::{Database, Datoms, Entity, Index, ID}; 8 | 9 | #[no_mangle] 10 | pub extern "C" fn datom_datoms<'s>( 11 | database: &'s Database, 12 | index: Index, 13 | ) -> Option>> { 14 | let res: Result>, QueryError> = (|| { 15 | let iter = database.d.datoms(index.into())?; 16 | Ok(Box::new(iter.into())) 17 | })(); 18 | match res { 19 | Ok(d) => Some(d), 20 | Err(_) => { 21 | // update_last_query_error(e.into()) 22 | None 23 | } 24 | } 25 | } 26 | 27 | #[no_mangle] 28 | pub extern "C" fn datom_entity<'s>( 29 | database: &'s Database, 30 | entity: Box, 31 | ) -> Option>> { 32 | let res: Result>, QueryError> = (|| { 33 | let e = database.d.entity(entity.i.into())?; 34 | Ok(Box::new(e.into())) 35 | })(); 36 | match res { 37 | Ok(d) => Some(d), 38 | Err(_) => { 39 | // update_last_query_error(e.into()) 40 | None 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /datom-c/src/lib.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | use std::ffi::CString; 6 | 7 | #[allow(clippy::upper_case_acronyms)] 8 | pub mod connection; 9 | pub mod database; 10 | pub mod misc; 11 | pub mod sled; 12 | pub mod structs; 13 | 14 | #[no_mangle] 15 | pub extern "C" fn datom_version() -> *mut i8 { 16 | CString::new(datom::version()) 17 | .expect("Version contained an unexpected null") 18 | .into_raw() 19 | } 20 | 21 | /// # Safety 22 | /// This takes a raw pointer and interprets it as a CString 23 | #[no_mangle] 24 | pub unsafe extern "C" fn datom_free_version(version: *mut i8) { 25 | drop(CString::from_raw(version)); 26 | } 27 | -------------------------------------------------------------------------------- /datom-c/src/misc.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | use std::ffi::CStr; 6 | 7 | use libc::c_char; 8 | 9 | use crate::structs::Str; 10 | 11 | /// # Safety 12 | /// str must be a valid, NULL-terminated string. 13 | #[no_mangle] 14 | pub unsafe extern "C" fn datom_string(str: *const c_char) -> Option> { 15 | let cstr = CStr::from_ptr(str); 16 | cstr.to_str() 17 | .ok() 18 | .map(|s| Box::new(Str { s: s.to_string() })) 19 | } 20 | -------------------------------------------------------------------------------- /datom-c/src/sled.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | use datom::{backends::SledStorage, StorageError}; 6 | 7 | use crate::structs::{Storage, Str}; 8 | 9 | #[no_mangle] 10 | pub extern "C" fn datom_sled_connect(path: Box) -> Option> { 11 | let res: Result, StorageError> = (|| { 12 | let storage = SledStorage::connect(&path.s)?; 13 | Ok(Box::new(storage.into())) 14 | })(); 15 | match res { 16 | Ok(s) => Some(s), 17 | Err(_) => { 18 | // update_last_storage_error(e.into()) 19 | None 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /datom-c/src/structs.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | use datom::DynamicConnection; 6 | 7 | pub struct Str { 8 | pub(crate) s: String, 9 | } 10 | 11 | impl From for Str { 12 | fn from(s: String) -> Self { 13 | Str { s } 14 | } 15 | } 16 | 17 | impl From for String { 18 | fn from(s: Str) -> Self { 19 | s.s 20 | } 21 | } 22 | 23 | impl<'a> From<&'a Str> for &'a str { 24 | fn from(s: &'a Str) -> Self { 25 | s.s.as_str() 26 | } 27 | } 28 | 29 | pub struct Storage { 30 | pub(crate) s: Box, 31 | } 32 | 33 | impl From for Storage { 34 | fn from(s: T) -> Self { 35 | Storage { s: Box::new(s) } 36 | } 37 | } 38 | 39 | impl From for Box { 40 | fn from(s: Storage) -> Self { 41 | s.s 42 | } 43 | } 44 | 45 | impl<'a> From<&'a Storage> for &'a dyn datom::storage::Storage { 46 | fn from(s: &'a Storage) -> Self { 47 | &s.s 48 | } 49 | } 50 | 51 | pub struct Connection { 52 | pub(crate) c: DynamicConnection, 53 | } 54 | 55 | impl From for Connection { 56 | fn from(c: DynamicConnection) -> Self { 57 | Connection { c } 58 | } 59 | } 60 | 61 | impl From for DynamicConnection { 62 | fn from(c: Connection) -> Self { 63 | c.c 64 | } 65 | } 66 | 67 | impl<'a> From<&'a Connection> for &'a DynamicConnection { 68 | fn from(c: &'a Connection) -> Self { 69 | &c.c 70 | } 71 | } 72 | 73 | pub struct Database<'c> { 74 | pub(crate) d: datom::Database<'c, Box>, 75 | } 76 | 77 | impl<'c> From>> for Database<'c> { 78 | fn from(d: datom::Database<'c, Box>) -> Self { 79 | Database { d } 80 | } 81 | } 82 | 83 | impl<'c> From> for datom::Database<'c, Box> { 84 | fn from(d: Database<'c>) -> Self { 85 | d.d 86 | } 87 | } 88 | 89 | impl<'c, 'a> From<&'a Database<'c>> for &'a datom::Database<'c, Box> { 90 | fn from(d: &'a Database<'c>) -> Self { 91 | &d.d 92 | } 93 | } 94 | 95 | pub struct Transaction { 96 | pub(crate) t: datom::Transaction, 97 | } 98 | 99 | impl From for Transaction { 100 | fn from(t: datom::Transaction) -> Self { 101 | Transaction { t } 102 | } 103 | } 104 | 105 | impl From for datom::Transaction { 106 | fn from(t: Transaction) -> Self { 107 | t.t 108 | } 109 | } 110 | 111 | impl<'a> From<&'a Transaction> for &'a datom::Transaction { 112 | fn from(t: &'a Transaction) -> Self { 113 | &t.t 114 | } 115 | } 116 | 117 | pub struct TransactionResult<'s> { 118 | pub(crate) r: datom::TransactionResult<'s, Box>, 119 | } 120 | 121 | impl<'s> From>> 122 | for TransactionResult<'s> 123 | { 124 | fn from(r: datom::TransactionResult<'s, Box>) -> Self { 125 | TransactionResult { r } 126 | } 127 | } 128 | 129 | impl<'s> From> 130 | for datom::TransactionResult<'s, Box> 131 | { 132 | fn from(r: TransactionResult<'s>) -> Self { 133 | r.r 134 | } 135 | } 136 | 137 | impl<'s, 'a> From<&'a TransactionResult<'s>> 138 | for &'a datom::TransactionResult<'s, Box> 139 | { 140 | fn from(r: &'a TransactionResult<'s>) -> Self { 141 | &r.r 142 | } 143 | } 144 | 145 | pub struct Datoms<'s> { 146 | pub(crate) d: datom::DatomIterator<'s>, 147 | } 148 | 149 | impl<'s> From> for Datoms<'s> { 150 | fn from(d: datom::DatomIterator<'s>) -> Self { 151 | Datoms { d } 152 | } 153 | } 154 | 155 | impl<'s> From> for datom::DatomIterator<'s> { 156 | fn from(d: Datoms<'s>) -> Self { 157 | d.d 158 | } 159 | } 160 | 161 | impl<'s, 'a> From<&'a Datoms<'s>> for &'a datom::DatomIterator<'s> { 162 | fn from(d: &'a Datoms<'s>) -> Self { 163 | &d.d 164 | } 165 | } 166 | 167 | #[repr(C)] 168 | pub enum Index { 169 | EAVT, 170 | AEVT, 171 | AVET, 172 | VAET, 173 | } 174 | 175 | impl From for Index { 176 | fn from(i: datom::Index) -> Self { 177 | use datom::Index as DIndex; 178 | match i { 179 | DIndex::EAVT => Index::EAVT, 180 | DIndex::AEVT => Index::AEVT, 181 | DIndex::AVET => Index::AVET, 182 | DIndex::VAET => Index::VAET, 183 | } 184 | } 185 | } 186 | 187 | impl From for datom::Index { 188 | fn from(i: Index) -> Self { 189 | use datom::Index as DIndex; 190 | match i { 191 | Index::EAVT => DIndex::EAVT, 192 | Index::AEVT => DIndex::AEVT, 193 | Index::AVET => DIndex::AVET, 194 | Index::VAET => DIndex::VAET, 195 | } 196 | } 197 | } 198 | 199 | pub struct Entity<'c> { 200 | pub(crate) e: datom::Entity<'c, Box>, 201 | } 202 | 203 | impl<'c> From>> for Entity<'c> { 204 | fn from(e: datom::Entity<'c, Box>) -> Self { 205 | Entity { e } 206 | } 207 | } 208 | 209 | impl<'c> From> for datom::Entity<'c, Box> { 210 | fn from(e: Entity<'c>) -> Self { 211 | e.e 212 | } 213 | } 214 | 215 | impl<'c, 'a> From<&'a Entity<'c>> for &'a datom::Entity<'c, Box> { 216 | fn from(e: &'a Entity<'c>) -> Self { 217 | &e.e 218 | } 219 | } 220 | 221 | pub struct ID { 222 | pub(crate) i: datom::ID, 223 | } 224 | 225 | impl From for ID { 226 | fn from(i: datom::ID) -> Self { 227 | ID { i } 228 | } 229 | } 230 | 231 | impl From for datom::ID { 232 | fn from(i: ID) -> Self { 233 | i.i 234 | } 235 | } 236 | 237 | impl<'a> From<&'a ID> for &'a datom::ID { 238 | fn from(i: &'a ID) -> Self { 239 | &i.i 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /datom-java/.gitattributes: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | # SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | # SPDX-FileContributor: Piper McCorkle 4 | *.bat text eol=crlf 5 | 6 | -------------------------------------------------------------------------------- /datom-java/.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | # SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | # SPDX-FileContributor: Piper McCorkle 4 | 5 | # Ignore Gradle project-specific cache directory 6 | .gradle 7 | 8 | # Ignore Gradle build output directory 9 | build 10 | 11 | .settings 12 | .project 13 | .classpath 14 | 15 | lib/bin 16 | -------------------------------------------------------------------------------- /datom-java/Cargo.toml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | # SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | # SPDX-FileContributor: Piper McCorkle 4 | 5 | [package] 6 | name = "datom-java" 7 | version = "0.1.1-pre4" 8 | authors = ["Lutris, Inc "] 9 | edition = "2021" 10 | rust-version = "1.65" 11 | description = "Java bindings for an open-source database inspired by Datomic" 12 | readme = "../README.md" 13 | homepage = "https://os.lutris.engineering/datom-rs/" 14 | repository = "https://github.com/LutrisEng/datom-rs" 15 | license = "BlueOak-1.0.0 OR BSD-2-Clause-Patent" 16 | keywords = ["database", "datomic"] 17 | categories = ["database"] 18 | publish = false 19 | 20 | [lib] 21 | crate-type = ["cdylib"] 22 | 23 | [dependencies] 24 | datom = { path = "../datom" } 25 | jni = "0.20" 26 | -------------------------------------------------------------------------------- /datom-java/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LutrisEng/datom-rs/6ab6e1b9d1277311af9f1191c1913a61fa3cc9c2/datom-java/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /datom-java/gradle/wrapper/gradle-wrapper.jar.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2021 Gradle project 2 | SPDX-License-Identifier: Apache-2.0 3 | SPDX-FileContributor: Piper McCorkle 4 | -------------------------------------------------------------------------------- /datom-java/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Gradle project 2 | # SPDX-License-Identifier: Apache-2.0 3 | # SPDX-FileContributor: Piper McCorkle 4 | distributionBase=GRADLE_USER_HOME 5 | distributionPath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip 7 | zipStoreBase=GRADLE_USER_HOME 8 | zipStorePath=wrapper/dists 9 | -------------------------------------------------------------------------------- /datom-java/gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # SPDX-FileCopyrightText: 2015-2021 the original authors. 4 | # SPDX-License-Identifier: Apache-2.0 5 | # SPDX-FileContributor: Piper McCorkle 6 | # 7 | # Copyright � 2015-2021 the original authors. 8 | # 9 | # Licensed under the Apache License, Version 2.0 (the "License"); 10 | # you may not use this file except in compliance with the License. 11 | # You may obtain a copy of the License at 12 | # 13 | # https://www.apache.org/licenses/LICENSE-2.0 14 | # 15 | # Unless required by applicable law or agreed to in writing, software 16 | # distributed under the License is distributed on an "AS IS" BASIS, 17 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | # See the License for the specific language governing permissions and 19 | # limitations under the License. 20 | # 21 | 22 | ############################################################################## 23 | # 24 | # Gradle start up script for POSIX generated by Gradle. 25 | # 26 | # Important for running: 27 | # 28 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 29 | # noncompliant, but you have some other compliant shell such as ksh or 30 | # bash, then to run this script, type that shell name before the whole 31 | # command line, like: 32 | # 33 | # ksh Gradle 34 | # 35 | # Busybox and similar reduced shells will NOT work, because this script 36 | # requires all of these POSIX shell features: 37 | # * functions; 38 | # * expansions �$var�, �${var}�, �${var:-default}�, �${var+SET}�, 39 | # �${var#prefix}�, �${var%suffix}�, and �$( cmd )�; 40 | # * compound commands having a testable exit status, especially �case�; 41 | # * various built-in commands including �command�, �set�, and �ulimit�. 42 | # 43 | # Important for patching: 44 | # 45 | # (2) This script targets any POSIX shell, so it avoids extensions provided 46 | # by Bash, Ksh, etc; in particular arrays are avoided. 47 | # 48 | # The "traditional" practice of packing multiple parameters into a 49 | # space-separated string is a well documented source of bugs and security 50 | # problems, so this is (mostly) avoided, by progressively accumulating 51 | # options in "$@", and eventually passing that to Java. 52 | # 53 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 54 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 55 | # see the in-line comments for details. 56 | # 57 | # There are tweaks for specific operating systems such as AIX, CygWin, 58 | # Darwin, MinGW, and NonStop. 59 | # 60 | # (3) This script is generated from the Groovy template 61 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 62 | # within the Gradle project. 63 | # 64 | # You can find Gradle at https://github.com/gradle/gradle/. 65 | # 66 | ############################################################################## 67 | 68 | # Attempt to set APP_HOME 69 | 70 | # Resolve links: $0 may be a link 71 | app_path=$0 72 | 73 | # Need this for daisy-chained symlinks. 74 | while 75 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 76 | [ -h "$app_path" ] 77 | do 78 | ls=$( ls -ld "$app_path" ) 79 | link=${ls#*' -> '} 80 | case $link in #( 81 | /*) app_path=$link ;; #( 82 | *) app_path=$APP_HOME$link ;; 83 | esac 84 | done 85 | 86 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 87 | 88 | APP_NAME="Gradle" 89 | APP_BASE_NAME=${0##*/} 90 | 91 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 92 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 93 | 94 | # Use the maximum available, or set MAX_FD != -1 to use that value. 95 | MAX_FD=maximum 96 | 97 | warn () { 98 | echo "$*" 99 | } >&2 100 | 101 | die () { 102 | echo 103 | echo "$*" 104 | echo 105 | exit 1 106 | } >&2 107 | 108 | # OS specific support (must be 'true' or 'false'). 109 | cygwin=false 110 | msys=false 111 | darwin=false 112 | nonstop=false 113 | case "$( uname )" in #( 114 | CYGWIN* ) cygwin=true ;; #( 115 | Darwin* ) darwin=true ;; #( 116 | MSYS* | MINGW* ) msys=true ;; #( 117 | NONSTOP* ) nonstop=true ;; 118 | esac 119 | 120 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 121 | 122 | 123 | # Determine the Java command to use to start the JVM. 124 | if [ -n "$JAVA_HOME" ] ; then 125 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 126 | # IBM's JDK on AIX uses strange locations for the executables 127 | JAVACMD=$JAVA_HOME/jre/sh/java 128 | else 129 | JAVACMD=$JAVA_HOME/bin/java 130 | fi 131 | if [ ! -x "$JAVACMD" ] ; then 132 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 133 | 134 | Please set the JAVA_HOME variable in your environment to match the 135 | location of your Java installation." 136 | fi 137 | else 138 | JAVACMD=java 139 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 140 | 141 | Please set the JAVA_HOME variable in your environment to match the 142 | location of your Java installation." 143 | fi 144 | 145 | # Increase the maximum file descriptors if we can. 146 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 147 | case $MAX_FD in #( 148 | max*) 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | ulimit -n "$MAX_FD" || 156 | warn "Could not set maximum file descriptor limit to $MAX_FD" 157 | esac 158 | fi 159 | 160 | # Collect all arguments for the java command, stacking in reverse order: 161 | # * args from the command line 162 | # * the main class name 163 | # * -classpath 164 | # * -D...appname settings 165 | # * --module-path (only if needed) 166 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 167 | 168 | # For Cygwin or MSYS, switch paths to Windows format before running java 169 | if "$cygwin" || "$msys" ; then 170 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 171 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 172 | 173 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 174 | 175 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 176 | for arg do 177 | if 178 | case $arg in #( 179 | -*) false ;; # don't mess with options #( 180 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 181 | [ -e "$t" ] ;; #( 182 | *) false ;; 183 | esac 184 | then 185 | arg=$( cygpath --path --ignore --mixed "$arg" ) 186 | fi 187 | # Roll the args list around exactly as many times as the number of 188 | # args, so each arg winds up back in the position where it started, but 189 | # possibly modified. 190 | # 191 | # NB: a `for` loop captures its iteration list before it begins, so 192 | # changing the positional parameters here affects neither the number of 193 | # iterations, nor the values presented in `arg`. 194 | shift # remove old arg 195 | set -- "$@" "$arg" # push replacement arg 196 | done 197 | fi 198 | 199 | # Collect all arguments for the java command; 200 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 201 | # shell script including quotes and variable substitutions, so put them in 202 | # double quotes to make sure that they get re-expanded; and 203 | # * put everything else in single quotes, so that it's not re-expanded. 204 | 205 | set -- \ 206 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 207 | -classpath "$CLASSPATH" \ 208 | org.gradle.wrapper.GradleWrapperMain \ 209 | "$@" 210 | 211 | # Use "xargs" to parse quoted args. 212 | # 213 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 214 | # 215 | # In Bash we could simply go: 216 | # 217 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 218 | # set -- "${ARGS[@]}" "$@" 219 | # 220 | # but POSIX shell has neither arrays nor command substitution, so instead we 221 | # post-process each arg (as a line of input to sed) to backslash-escape any 222 | # character that might be a shell metacharacter, then use eval to reverse 223 | # that process (while maintaining the separation between arguments), and wrap 224 | # the whole thing up as a single "set" statement. 225 | # 226 | # This will of course break if any of these variables contains a newline or 227 | # an unmatched quote. 228 | # 229 | 230 | eval "set -- $( 231 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 232 | xargs -n1 | 233 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 234 | tr '\n' ' ' 235 | )" '"$@"' 236 | 237 | exec "$JAVACMD" "$@" 238 | -------------------------------------------------------------------------------- /datom-java/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem SPDX-FileCopyrightText: 2015 the original author or authors. 2 | @rem SPDX-License-Identifier: Apache-2.0 3 | @rem SPDX-FileContributor: Piper McCorkle 4 | @rem 5 | @rem Copyright 2015 the original author or authors. 6 | @rem 7 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 8 | @rem you may not use this file except in compliance with the License. 9 | @rem You may obtain a copy of the License at 10 | @rem 11 | @rem https://www.apache.org/licenses/LICENSE-2.0 12 | @rem 13 | @rem Unless required by applicable law or agreed to in writing, software 14 | @rem distributed under the License is distributed on an "AS IS" BASIS, 15 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | @rem See the License for the specific language governing permissions and 17 | @rem limitations under the License. 18 | @rem 19 | 20 | @if "%DEBUG%" == "" @echo off 21 | @rem ########################################################################## 22 | @rem 23 | @rem Gradle startup script for Windows 24 | @rem 25 | @rem ########################################################################## 26 | 27 | @rem Set local scope for the variables with windows NT shell 28 | if "%OS%"=="Windows_NT" setlocal 29 | 30 | set DIRNAME=%~dp0 31 | if "%DIRNAME%" == "" set DIRNAME=. 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if "%ERRORLEVEL%" == "0" goto execute 47 | 48 | echo. 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 50 | echo. 51 | echo Please set the JAVA_HOME variable in your environment to match the 52 | echo location of your Java installation. 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 64 | echo. 65 | echo Please set the JAVA_HOME variable in your environment to match the 66 | echo location of your Java installation. 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if "%ERRORLEVEL%"=="0" goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 87 | exit /b 1 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /datom-java/lib/build.gradle: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | plugins { 6 | id 'java-library' 7 | } 8 | 9 | repositories { 10 | mavenCentral() 11 | } 12 | 13 | // tasks.register('cargo-cross-build-x86_64-pc-windows-gnu', Exec) { 14 | // commandLine 'cargo', 'zigbuild', '--release', '--target', 'x86_64-pc-windows-gnu' 15 | // workingDir rootDir 16 | // } 17 | 18 | // tasks.register('cargo-cross-build-x86_64-pc-windows-gnu-copy', Copy) { 19 | // dependsOn tasks.named('cargo-cross-build-x86_64-pc-windows-gnu') 20 | // from("${rootDir}/../target/x86_64-pc-windows-gnu/release") { 21 | // include 'datom_java.dll' 22 | // } 23 | // into "${buildDir}/native/engineering/lutris/datom/natives/windows/amd64" 24 | // } 25 | 26 | tasks.register('cargo-cross-build-x86_64-unknown-linux-gnu', Exec) { 27 | commandLine 'cargo', 'zigbuild', '--release', '--target', 'x86_64-unknown-linux-gnu' 28 | workingDir rootDir 29 | } 30 | 31 | tasks.register('cargo-cross-build-x86_64-unknown-linux-gnu-copy', Copy) { 32 | dependsOn tasks.named('cargo-cross-build-x86_64-unknown-linux-gnu') 33 | from("${rootDir}/../target/x86_64-unknown-linux-gnu/release") { 34 | include 'libdatom_java.so' 35 | } 36 | into "${buildDir}/native/engineering/lutris/datom/natives/linux/amd64" 37 | } 38 | 39 | tasks.register('cargo-cross-build-aarch64-unknown-linux-gnu', Exec) { 40 | commandLine 'cargo', 'zigbuild', '--release', '--target', 'aarch64-unknown-linux-gnu' 41 | workingDir rootDir 42 | } 43 | 44 | tasks.register('cargo-cross-build-aarch64-unknown-linux-gnu-copy', Copy) { 45 | dependsOn tasks.named('cargo-cross-build-aarch64-unknown-linux-gnu') 46 | from("${rootDir}/../target/aarch64-unknown-linux-gnu/release") { 47 | include 'libdatom_java.so' 48 | } 49 | into "${buildDir}/native/engineering/lutris/datom/natives/linux/aarch64" 50 | } 51 | 52 | tasks.register('cargo-cross-build-x86_64-apple-darwin', Exec) { 53 | commandLine 'cargo', 'zigbuild', '--release', '--target', 'x86_64-apple-darwin' 54 | workingDir rootDir 55 | } 56 | 57 | tasks.register('cargo-cross-build-x86_64-apple-darwin-copy', Copy) { 58 | dependsOn tasks.named('cargo-cross-build-x86_64-apple-darwin') 59 | from("${rootDir}/../target/x86_64-apple-darwin/release") { 60 | include 'libdatom_java.dylib' 61 | } 62 | into "${buildDir}/native/engineering/lutris/datom/natives/macos/amd64" 63 | } 64 | 65 | tasks.register('cargo-cross-build-aarch64-apple-darwin', Exec) { 66 | commandLine 'cargo', 'zigbuild', '--release', '--target', 'aarch64-apple-darwin' 67 | workingDir rootDir 68 | } 69 | 70 | tasks.register('cargo-cross-build-aarch64-apple-darwin-copy', Copy) { 71 | dependsOn tasks.named('cargo-cross-build-aarch64-apple-darwin') 72 | from("${rootDir}/../target/aarch64-apple-darwin/release") { 73 | include 'libdatom_java.dylib' 74 | } 75 | into "${buildDir}/native/engineering/lutris/datom/natives/macos/aarch64" 76 | } 77 | 78 | tasks.register('cargo-native-build', Exec) { 79 | commandLine 'cargo', 'build', '--release' 80 | } 81 | 82 | tasks.register('cargo-native-build-copy', Copy) { 83 | dependsOn tasks.named('cargo-native-build') 84 | from('../../target/release') { 85 | include '*datom_java.so' 86 | include '*datom_java.dylib' 87 | include '*datom_java.dll' 88 | } 89 | into "${buildDir}/native/engineering/lutris/datom/natives/native" 90 | } 91 | 92 | sourceSets { 93 | main { 94 | resources.srcDirs += "${buildDir}/native" 95 | } 96 | } 97 | 98 | processResources { 99 | // dependsOn tasks.named('cargo-cross-build-x86_64-pc-windows-gnu-copy') 100 | // dependsOn tasks.named('cargo-cross-build-x86_64-unknown-linux-gnu-copy') 101 | // dependsOn tasks.named('cargo-cross-build-aarch64-unknown-linux-gnu-copy') 102 | // dependsOn tasks.named('cargo-cross-build-x86_64-apple-darwin-copy') 103 | // dependsOn tasks.named('cargo-cross-build-aarch64-apple-darwin-copy') 104 | dependsOn tasks.named('cargo-native-build-copy') 105 | } 106 | 107 | compileJava { 108 | options.compilerArgs += ["-h", file("${buildDir}/headers")] 109 | } 110 | 111 | dependencies { 112 | implementation 'org.apache.commons:commons-lang3:3.12.0' 113 | 114 | testImplementation 'org.junit.jupiter:junit-jupiter:5.9.1' 115 | } 116 | 117 | test { 118 | useJUnitPlatform() 119 | systemProperty 'engineering.lutris.datom.nativeResourcePath', 'natives/native' 120 | } 121 | -------------------------------------------------------------------------------- /datom-java/lib/src/main/java/engineering/lutris/datom/Connection.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | package engineering.lutris.datom; 6 | 7 | public class Connection implements AutoCloseable { 8 | private static class JNI { 9 | static { 10 | DatomJNI.ensureLoaded(); 11 | } 12 | 13 | private static native long create(); 14 | 15 | private static native void destroy(long connection); 16 | 17 | private static native long latestT(long connection); 18 | } 19 | 20 | private long impl = 0; 21 | 22 | public Connection() { 23 | impl = JNI.create(); 24 | } 25 | 26 | public long latestT() { 27 | return JNI.latestT(impl); 28 | } 29 | 30 | @Override 31 | public void close() { 32 | JNI.destroy(impl); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /datom-java/lib/src/main/java/engineering/lutris/datom/Datom.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | package engineering.lutris.datom; 6 | 7 | public class Datom { 8 | private static class JNI { 9 | static { 10 | DatomJNI.ensureLoaded(); 11 | } 12 | 13 | private static native String version(); 14 | } 15 | 16 | public static String VERSION = JNI.version(); 17 | } 18 | -------------------------------------------------------------------------------- /datom-java/lib/src/main/java/engineering/lutris/datom/DatomJNI.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | package engineering.lutris.datom; 6 | 7 | import java.io.File; 8 | import java.io.FileOutputStream; 9 | import java.io.FileWriter; 10 | import java.io.IOException; 11 | import java.io.InputStream; 12 | 13 | import org.apache.commons.lang3.SystemUtils; 14 | 15 | class DatomJNI { 16 | private static class MissingNativesException extends Exception { 17 | MissingNativesException(String libraryName) { 18 | super(String.format("Missing native library named %s in classpath", libraryName)); 19 | } 20 | } 21 | 22 | private static boolean loaded = false; 23 | 24 | private static String getLibraryExtension() throws InvalidPlatformException { 25 | if (SystemUtils.IS_OS_WINDOWS) { 26 | return "dll"; 27 | } else if (SystemUtils.IS_OS_MAC) { 28 | return "dylib"; 29 | } else if (SystemUtils.IS_OS_LINUX) { 30 | return "so"; 31 | } else { 32 | throw new InvalidPlatformException(System.getProperty("os.name")); 33 | } 34 | } 35 | 36 | private static String getLibraryBasename() throws InvalidPlatformException { 37 | if (SystemUtils.IS_OS_WINDOWS) { 38 | return "datom_java"; 39 | } else if (SystemUtils.IS_OS_MAC || SystemUtils.IS_OS_LINUX) { 40 | return "libdatom_java"; 41 | } else { 42 | throw new InvalidPlatformException(System.getProperty("os.name")); 43 | } 44 | } 45 | 46 | private static String getLibraryName() throws InvalidPlatformException { 47 | return String.format("%s.%s", getLibraryBasename(), getLibraryExtension()); 48 | } 49 | 50 | private static String getPlatformName() throws InvalidPlatformException { 51 | if (SystemUtils.IS_OS_WINDOWS) { 52 | return "windows"; 53 | } else if (SystemUtils.IS_OS_MAC) { 54 | return "macos"; 55 | } else if (SystemUtils.IS_OS_LINUX) { 56 | return "linux"; 57 | } else { 58 | throw new InvalidPlatformException(System.getProperty("os.name")); 59 | } 60 | } 61 | 62 | private static String getLibraryPath() throws InvalidPlatformException { 63 | String fromProperty = System.getProperty("engineering.lutris.datom.nativeResourcePath"); 64 | if (fromProperty != null) { 65 | return String.format("%s/%s", fromProperty, getLibraryName()); 66 | } else { 67 | return String.format("natives/%s/%s/%s", getPlatformName(), System.getProperty("os.arch"), 68 | getLibraryName()); 69 | } 70 | } 71 | 72 | private static void load() throws IOException, InvalidPlatformException, MissingNativesException { 73 | String libraryPath = getLibraryPath(); 74 | String libraryBasename = getLibraryBasename(); 75 | String libraryExtension = getLibraryExtension(); 76 | File nativeTempFile = File.createTempFile(libraryBasename, libraryExtension); 77 | try (InputStream nativeInput = DatomJNI.class.getResourceAsStream(libraryPath)) { 78 | if (nativeInput == null) { 79 | throw new MissingNativesException(libraryPath); 80 | } 81 | try (FileOutputStream nativeOutput = new FileOutputStream(nativeTempFile)) { 82 | nativeInput.transferTo(nativeOutput); 83 | } 84 | } 85 | System.load(nativeTempFile.getAbsolutePath()); 86 | } 87 | 88 | static synchronized void ensureLoaded() { 89 | if (!loaded) { 90 | try { 91 | load(); 92 | } catch (Exception e) { 93 | System.err.println("Failed to load engineering.lutris.datom native library"); 94 | e.printStackTrace(); 95 | System.exit(1); 96 | } 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /datom-java/lib/src/main/java/engineering/lutris/datom/Fact.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | package engineering.lutris.datom; 6 | 7 | public class Fact implements AutoCloseable { 8 | private static class JNI { 9 | static { 10 | DatomJNI.ensureLoaded(); 11 | } 12 | 13 | private static native long fromEdn(String edn); 14 | 15 | private static native void destroy(long fact); 16 | 17 | private static native String toEdn(long fact); 18 | } 19 | 20 | private long impl = 0; 21 | 22 | private Fact(long impl) { 23 | this.impl = impl; 24 | } 25 | 26 | public static Fact fromEdn(String edn) { 27 | return new Fact(JNI.fromEdn(edn)); 28 | } 29 | 30 | public String toEdn() { 31 | return JNI.toEdn(this.impl); 32 | } 33 | 34 | @Override 35 | public void close() { 36 | JNI.destroy(impl); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /datom-java/lib/src/main/java/engineering/lutris/datom/HelloWorld.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | package engineering.lutris.datom; 6 | 7 | public class HelloWorld { 8 | private static class JNI { 9 | static { 10 | DatomJNI.ensureLoaded(); 11 | } 12 | 13 | private static native String hello(String world); 14 | } 15 | 16 | public static String hello(String world) { 17 | return JNI.hello(world); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /datom-java/lib/src/main/java/engineering/lutris/datom/InvalidPlatformException.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | package engineering.lutris.datom; 6 | 7 | public class InvalidPlatformException extends Exception { 8 | InvalidPlatformException(String platform) { 9 | super(String.format("The current platform (%s) isn't supported by engineering.lutris.datom.", platform)); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /datom-java/lib/src/test/java/engineering/lutris/datom/ConnectionTest.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | package engineering.lutris.datom; 6 | 7 | import org.junit.jupiter.api.Test; 8 | import static org.junit.jupiter.api.Assertions.*; 9 | 10 | class ConnectionTest { 11 | @Test 12 | void canCreateConnectionAndGetLatestT() { 13 | try (Connection conn = new Connection()) { 14 | assertEquals(0, conn.latestT()); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /datom-java/lib/src/test/java/engineering/lutris/datom/DatomTest.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | package engineering.lutris.datom; 6 | 7 | import org.junit.jupiter.api.Test; 8 | import static org.junit.jupiter.api.Assertions.*; 9 | 10 | class DatomTest { 11 | @Test 12 | void versionWorks() { 13 | assertEquals(Datom.VERSION, "0.1.1-pre4"); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /datom-java/lib/src/test/java/engineering/lutris/datom/FactTest.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | package engineering.lutris.datom; 6 | 7 | import org.junit.jupiter.api.Test; 8 | import static org.junit.jupiter.api.Assertions.*; 9 | 10 | class FactTest { 11 | @Test 12 | void ednRoundtrip() { 13 | String edn = "[:entity, :attribute, \"value\", ]"; 14 | try (Fact fact = Fact.fromEdn(edn)) { 15 | assertEquals(edn, fact.toEdn()); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /datom-java/lib/src/test/java/engineering/lutris/datom/HelloWorldTest.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | package engineering.lutris.datom; 6 | 7 | import org.junit.jupiter.api.Test; 8 | import static org.junit.jupiter.api.Assertions.*; 9 | 10 | class HelloWorldTest { 11 | @Test 12 | void helloWorks() { 13 | assertEquals(HelloWorld.hello("world"), "Hello, world!"); 14 | assertEquals(HelloWorld.hello("other place"), "Hello, other place!"); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /datom-java/settings.gradle: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | rootProject.name = 'datom-java' 6 | include('lib') 7 | -------------------------------------------------------------------------------- /datom-java/src/connection.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | use datom::{backends::RedBlackTreeSetStorage, DynamicConnection}; 6 | use jni::{objects::JClass, sys::jlong, JNIEnv}; 7 | 8 | #[no_mangle] 9 | pub extern "system" fn Java_engineering_lutris_datom_Connection_00024JNI_create( 10 | _: JNIEnv, 11 | _: JClass, 12 | ) -> jlong { 13 | let connection = DynamicConnection::new(Box::new(RedBlackTreeSetStorage::new())); 14 | Box::into_raw(Box::new(connection)) as jlong 15 | } 16 | 17 | /// # Safety 18 | /// This function interprets `connection_ptr` as a *mut [DynamicConnection]. 19 | #[no_mangle] 20 | pub unsafe extern "system" fn Java_engineering_lutris_datom_Connection_00024JNI_destroy( 21 | _: JNIEnv, 22 | _: JClass, 23 | connection_ptr: jlong, 24 | ) { 25 | drop(Box::from_raw(connection_ptr as *mut DynamicConnection)) 26 | } 27 | 28 | /// # Safety 29 | /// This function interprets `connection_ptr` as a *mut [DynamicConnection]. 30 | #[no_mangle] 31 | pub unsafe extern "system" fn Java_engineering_lutris_datom_Connection_00024JNI_latestT( 32 | _: JNIEnv, 33 | _: JClass, 34 | connection_ptr: jlong, 35 | ) -> jlong { 36 | let connection = connection_ptr as *mut DynamicConnection; 37 | (*connection).latest_t().expect("Couldn't get latest T!") as jlong 38 | } 39 | -------------------------------------------------------------------------------- /datom-java/src/datom.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | use jni::{objects::JClass, sys::jstring, JNIEnv}; 6 | 7 | #[no_mangle] 8 | pub extern "system" fn Java_engineering_lutris_datom_Datom_00024JNI_version( 9 | env: JNIEnv, 10 | _: JClass, 11 | ) -> jstring { 12 | let output = env 13 | .new_string(datom::version()) 14 | .expect("Couldn't create java string!"); 15 | 16 | output.into_raw() 17 | } 18 | -------------------------------------------------------------------------------- /datom-java/src/fact.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | use datom::Fact; 6 | use jni::{ 7 | objects::{JClass, JString}, 8 | sys::{jlong, jstring}, 9 | JNIEnv, 10 | }; 11 | 12 | #[no_mangle] 13 | pub extern "system" fn Java_engineering_lutris_datom_Fact_00024JNI_fromEdn( 14 | env: JNIEnv, 15 | _: JClass, 16 | edn: JString, 17 | ) -> jlong { 18 | let edn: String = env 19 | .get_string(edn) 20 | .expect("Couldn't get Java string!") 21 | .into(); 22 | let parsed = datom::parse_edn(&edn).expect("Invalid edn"); 23 | let fact = Fact::from_edn(parsed).expect("Invalid fact edn"); 24 | Box::into_raw(Box::new(fact)) as jlong 25 | } 26 | 27 | /// # Safety 28 | /// This function interprets `fact_ptr` as a *mut [Fact]. 29 | #[no_mangle] 30 | pub unsafe extern "system" fn Java_engineering_lutris_datom_Fact_00024JNI_destroy( 31 | _: JNIEnv, 32 | _: JClass, 33 | fact_ptr: jlong, 34 | ) { 35 | drop(Box::from_raw(fact_ptr as *mut Fact)) 36 | } 37 | 38 | /// # Safety 39 | /// This function interprets `fact_ptr` as a *mut [Fact]. 40 | #[no_mangle] 41 | pub unsafe extern "system" fn Java_engineering_lutris_datom_Fact_00024JNI_toEdn( 42 | env: JNIEnv, 43 | _: JClass, 44 | fact_ptr: jlong, 45 | ) -> jstring { 46 | let fact = fact_ptr as *mut Fact; 47 | env.new_string((*fact).to_edn()) 48 | .expect("Failed to create string") 49 | .into_raw() 50 | } 51 | -------------------------------------------------------------------------------- /datom-java/src/hello_world.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | use jni::{ 6 | objects::{JClass, JString}, 7 | sys::jstring, 8 | JNIEnv, 9 | }; 10 | 11 | #[no_mangle] 12 | pub extern "system" fn Java_engineering_lutris_datom_HelloWorld_00024JNI_hello( 13 | env: JNIEnv, 14 | _: JClass, 15 | input: JString, 16 | ) -> jstring { 17 | let input: String = env 18 | .get_string(input) 19 | .expect("Couldn't get java string!") 20 | .into(); 21 | 22 | let output = env 23 | .new_string(format!("Hello, {}!", input)) 24 | .expect("Couldn't create java string!"); 25 | 26 | output.into_raw() 27 | } 28 | -------------------------------------------------------------------------------- /datom-java/src/lib.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | pub mod connection; 6 | pub mod datom; 7 | pub mod fact; 8 | pub mod hello_world; 9 | -------------------------------------------------------------------------------- /datom-java/tests/gradle.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | use std::process::Command; 6 | 7 | #[test] 8 | fn gradle() -> Result<(), Box> { 9 | if cfg!(windows) { 10 | let status = Command::new(".\\gradlew.bat").arg("test").status()?; 11 | assert!(status.success()); 12 | } else { 13 | let status = Command::new("./gradlew").arg("test").status()?; 14 | assert!(status.success()); 15 | } 16 | Ok(()) 17 | } 18 | -------------------------------------------------------------------------------- /datom-node/.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | # SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | # SPDX-FileContributor: Piper McCorkle 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | .pnpm-debug.log* 13 | 14 | # Diagnostic reports (https://nodejs.org/api/report.html) 15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | *.lcov 29 | 30 | # nyc test coverage 31 | .nyc_output 32 | 33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 34 | .grunt 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (https://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directories 46 | node_modules/ 47 | jspm_packages/ 48 | 49 | # Snowpack dependency directory (https://snowpack.dev/) 50 | web_modules/ 51 | 52 | # TypeScript cache 53 | *.tsbuildinfo 54 | 55 | # Optional npm cache directory 56 | .npm 57 | 58 | # Optional eslint cache 59 | .eslintcache 60 | 61 | # Microbundle cache 62 | .rpt2_cache/ 63 | .rts2_cache_cjs/ 64 | .rts2_cache_es/ 65 | .rts2_cache_umd/ 66 | 67 | # Optional REPL history 68 | .node_repl_history 69 | 70 | # Output of 'npm pack' 71 | *.tgz 72 | 73 | # Yarn Integrity file 74 | .yarn-integrity 75 | 76 | # dotenv environment variables file 77 | .env 78 | .env.test 79 | .env.production 80 | 81 | # parcel-bundler cache (https://parceljs.org/) 82 | .cache 83 | .parcel-cache 84 | 85 | # Next.js build output 86 | .next 87 | out 88 | 89 | # Nuxt.js build / generate output 90 | .nuxt 91 | dist 92 | 93 | # Gatsby files 94 | .cache/ 95 | # Comment in the public line in if your project uses Gatsby and not Next.js 96 | # https://nextjs.org/blog/next-9-1#public-directory-support 97 | # public 98 | 99 | # vuepress build output 100 | .vuepress/dist 101 | 102 | # Serverless directories 103 | .serverless/ 104 | 105 | # FuseBox cache 106 | .fusebox/ 107 | 108 | # DynamoDB Local files 109 | .dynamodb/ 110 | 111 | # TernJS port file 112 | .tern-port 113 | 114 | # Stores VSCode versions used for testing VSCode extensions 115 | .vscode-test 116 | 117 | # yarn v2 118 | .yarn/cache 119 | .yarn/unplugged 120 | .yarn/build-state.yml 121 | .yarn/install-state.gz 122 | .pnp.* 123 | 124 | *.node 125 | -------------------------------------------------------------------------------- /datom-node/Cargo.toml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | # SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | # SPDX-FileContributor: Piper McCorkle 4 | 5 | [package] 6 | name = "datom-node" 7 | version = "0.1.1-pre4" 8 | authors = ["Lutris, Inc "] 9 | edition = "2021" 10 | rust-version = "1.65" 11 | description = "Node.js bindings for an open-source database inspired by Datomic" 12 | readme = "../README.md" 13 | homepage = "https://os.lutris.engineering/datom-rs/" 14 | repository = "https://github.com/LutrisEng/datom-rs" 15 | license = "BlueOak-1.0.0 OR BSD-2-Clause-Patent" 16 | keywords = ["database", "datomic"] 17 | categories = ["database"] 18 | exclude = ["index.node"] 19 | publish = false 20 | 21 | [lib] 22 | crate-type = ["cdylib"] 23 | 24 | [dependencies] 25 | datom = { path = "../datom" } 26 | 27 | [dependencies.neon] 28 | version = "0.10" 29 | default-features = false 30 | features = ["napi-6"] 31 | -------------------------------------------------------------------------------- /datom-node/index.node.d.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | import Opaque from "ts-opaque"; 6 | 7 | export type ConnectionImpl = Opaque<'datom-connection'>; 8 | export type FactImpl = Opaque<'datom-fact'>; 9 | 10 | export function hello(): 'hello node'; 11 | export function version(): string; 12 | export function new_connection(): ConnectionImpl; 13 | export function connection_latest_t(connection: ConnectionImpl): number; 14 | export function fact_from_edn(edn: string): FactImpl; 15 | export function fact_to_edn(fact: FactImpl): string; 16 | -------------------------------------------------------------------------------- /datom-node/package-lock.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | SPDX-FileContributor: Piper McCorkle 4 | -------------------------------------------------------------------------------- /datom-node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "datom-rs", 3 | "version": "0.1.1-pre4", 4 | "description": "Node.js bindings for an open-source database inspired by Datomic", 5 | "main": "./dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "readme": "../README.md", 8 | "scripts": { 9 | "build": "cargo-cp-artifact -ac datom-node index.node -- cargo build --release --message-format=json-render-diagnostics && tsc", 10 | "install": "npm run build", 11 | "test": "cargo test", 12 | "jest": "jest" 13 | }, 14 | "author": "Lutris, Inc ", 15 | "license": "BlueOak-1.0.0 OR BSD-2-Clause-Patent", 16 | "devDependencies": { 17 | "@types/jest": "^29.2.2", 18 | "@types/node": "^18.11.9", 19 | "cargo-cp-artifact": "^0.1", 20 | "jest": "^29", 21 | "typescript": "^4.8.4" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/LutrisEng/datom-rs.git" 26 | }, 27 | "keywords": [ 28 | "database", 29 | "datomic" 30 | ], 31 | "bugs": { 32 | "url": "https://github.com/LutrisEng/datom-rs/issues" 33 | }, 34 | "homepage": "https://github.com/LutrisEng/datom-rs#readme", 35 | "dependencies": { 36 | "ts-opaque": "^3.0.1" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /datom-node/package.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | SPDX-FileContributor: Piper McCorkle 4 | -------------------------------------------------------------------------------- /datom-node/src/index.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | import * as datom from '../index.node'; 6 | 7 | export class Connection { 8 | #impl: datom.ConnectionImpl 9 | 10 | constructor() { 11 | this.#impl = datom.new_connection(); 12 | } 13 | 14 | latestT(): number { 15 | return datom.connection_latest_t(this.#impl); 16 | } 17 | } 18 | 19 | export class Fact { 20 | #impl: datom.FactImpl 21 | 22 | private constructor(impl: datom.FactImpl) { 23 | this.#impl = impl; 24 | } 25 | 26 | static fromEdn(edn: string): Fact { 27 | return new Fact(datom.fact_from_edn(edn)); 28 | } 29 | 30 | toEdn(): string { 31 | return datom.fact_to_edn(this.#impl); 32 | } 33 | } 34 | 35 | export function hello(): string { 36 | return datom.hello(); 37 | } 38 | 39 | export const VERSION = datom.version(); 40 | -------------------------------------------------------------------------------- /datom-node/src/lib.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | use datom::{backends::RedBlackTreeSetStorage, parse_edn, Connection, DynamicConnection, Fact}; 6 | use neon::prelude::*; 7 | 8 | struct BoxableConnection(DynamicConnection); 9 | 10 | impl Finalize for BoxableConnection {} 11 | 12 | struct BoxableFact(Fact); 13 | 14 | impl Finalize for BoxableFact {} 15 | 16 | fn hello(mut cx: FunctionContext) -> JsResult { 17 | Ok(cx.string("hello node")) 18 | } 19 | 20 | fn version(mut cx: FunctionContext) -> JsResult { 21 | Ok(cx.string(datom::version())) 22 | } 23 | 24 | fn new_connection(mut cx: FunctionContext) -> JsResult> { 25 | Ok(cx.boxed(BoxableConnection(Connection::new(Box::new( 26 | RedBlackTreeSetStorage::new(), 27 | ))))) 28 | } 29 | 30 | fn connection_latest_t(mut cx: FunctionContext) -> JsResult { 31 | let arg: Handle> = cx.argument(0)?; 32 | Ok(cx.number(arg.0.latest_t().expect("Failed to get latest t") as f64)) 33 | } 34 | 35 | fn fact_from_edn(mut cx: FunctionContext) -> JsResult> { 36 | let arg: Handle = cx.argument(0)?; 37 | let parsed = parse_edn(&arg.value(&mut cx)).expect("Failed to parse edn"); 38 | let fact = Fact::from_edn(parsed); 39 | Ok(cx.boxed(BoxableFact(fact.expect("Invalid fact edn")))) 40 | } 41 | 42 | fn fact_to_edn(mut cx: FunctionContext) -> JsResult { 43 | let arg: Handle> = cx.argument(0)?; 44 | Ok(cx.string(arg.0.to_edn())) 45 | } 46 | 47 | #[neon::main] 48 | fn main(mut cx: ModuleContext) -> NeonResult<()> { 49 | cx.export_function("hello", hello)?; 50 | cx.export_function("version", version)?; 51 | cx.export_function("new_connection", new_connection)?; 52 | cx.export_function("connection_latest_t", connection_latest_t)?; 53 | cx.export_function("fact_from_edn", fact_from_edn)?; 54 | cx.export_function("fact_to_edn", fact_to_edn)?; 55 | Ok(()) 56 | } 57 | -------------------------------------------------------------------------------- /datom-node/tests/basic.test.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | const datom = require('../dist/index'); 6 | 7 | test('can require the module', () => { 8 | require('../dist/index'); 9 | }); 10 | 11 | test('hello world works properly', () => { 12 | expect(datom.hello()).toBe('hello node'); 13 | }); 14 | 15 | test('version returns the current version', () => { 16 | expect(datom.VERSION).toBe('0.1.1-pre4'); 17 | }); 18 | 19 | test('can create a connection', () => { 20 | const connection = new datom.Connection(); 21 | expect(connection.latestT()).toBe(0); 22 | }); 23 | 24 | test('can roundtrip a fact', () => { 25 | const edn = '[:entity, :attribute, "value", ]'; 26 | const fact = datom.Fact.fromEdn(edn); 27 | expect(fact.toEdn()).toBe(edn); 28 | }) 29 | -------------------------------------------------------------------------------- /datom-node/tests/jest.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | use std::process::Command; 6 | 7 | fn command_wrapper() -> Command { 8 | if cfg!(windows) { 9 | let mut command = Command::new("powershell"); 10 | command.arg("-Command"); 11 | command 12 | } else { 13 | let mut command = Command::new("sh"); 14 | command.arg("-c"); 15 | command 16 | } 17 | } 18 | 19 | fn run_command(command: &str) { 20 | assert!( 21 | command_wrapper().arg(command).status().unwrap().success(), 22 | "Command {:#?} failed", 23 | command 24 | ); 25 | } 26 | 27 | #[test] 28 | fn jest() { 29 | run_command("npm ci"); 30 | run_command("npm run jest"); 31 | } 32 | -------------------------------------------------------------------------------- /datom-node/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": ["@types/node", "@types/jest"], 4 | "outDir": "dist", 5 | "declaration": true, 6 | "target": "ES2022", 7 | "moduleResolution": "node", 8 | "strict": true, 9 | "module": "commonjs" 10 | }, 11 | "include": [ 12 | "src/**/*.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /datom-node/tsconfig.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | SPDX-FileContributor: Piper McCorkle 4 | -------------------------------------------------------------------------------- /datom/Cargo.toml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | # SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | # SPDX-FileContributor: Piper McCorkle 4 | 5 | [package] 6 | name = "datom" 7 | version = "0.1.1-pre4" 8 | authors = ["Lutris, Inc "] 9 | edition = "2021" 10 | rust-version = "1.65" 11 | description = "datom-rs: an open-source database inspired by Datomic" 12 | readme = "../README.md" 13 | homepage = "https://os.lutris.engineering/datom-rs/" 14 | repository = "https://github.com/LutrisEng/datom-rs" 15 | license = "BlueOak-1.0.0 OR BSD-2-Clause-Patent" 16 | keywords = ["database", "datomic"] 17 | categories = ["database-implementations", "database"] 18 | 19 | [features] 20 | default = ["redblacktreeset", "sled"] 21 | redblacktreeset = ["rpds", "arc-swap"] 22 | 23 | [dependencies] 24 | uuid = { version = "1", features = ["v4"] } 25 | num-bigint = "0.4" 26 | datom-bigdecimal = "0.3.0" 27 | chrono = "0.4" 28 | once_cell = "1" 29 | thiserror = "1" 30 | miette = "5" 31 | edn-rs = "0.17" 32 | 33 | # sled storage backend 34 | sled = { version = "0.34", optional = true } 35 | 36 | # redblacktreeset storage backend 37 | rpds = { version = "0.12", optional = true } 38 | arc-swap = { version = "1", optional = true } 39 | 40 | [dev-dependencies.cargo-husky] 41 | version = "1" 42 | default-features = false 43 | features = ["precommit-hook", "run-for-all", "run-cargo-check", "run-cargo-clippy", "run-cargo-fmt"] 44 | -------------------------------------------------------------------------------- /datom/src/backends/mod.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | #[cfg(feature = "sled")] 6 | mod sled; 7 | #[cfg(feature = "sled")] 8 | pub use self::sled::SledStorage; 9 | 10 | #[cfg(feature = "redblacktreeset")] 11 | mod redblacktreeset; 12 | #[cfg(feature = "redblacktreeset")] 13 | pub use self::redblacktreeset::RedBlackTreeSetStorage; 14 | 15 | mod tiered; 16 | pub use self::tiered::TieredStorage; 17 | -------------------------------------------------------------------------------- /datom/src/backends/redblacktreeset.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | use std::{ops::Range, sync::Arc}; 6 | 7 | use arc_swap::ArcSwap; 8 | use rpds::RedBlackTreeSetSync; 9 | 10 | use crate::{ 11 | storage::{Item, ItemIterator, Storage}, 12 | StorageError, ID, 13 | }; 14 | 15 | /// A storage backend backed by a [RedBlackTreeSetSync] 16 | pub struct RedBlackTreeSetStorage { 17 | set: ArcSwap>, 18 | id: ID, 19 | } 20 | 21 | struct RedBlackTreeSetRangeIter { 22 | set: RedBlackTreeSetSync, 23 | last_front: Option, 24 | last_back: Option, 25 | start: Item, 26 | end: Item, 27 | } 28 | 29 | fn add_one(mut v: Vec) -> Vec { 30 | v.push(0); 31 | v 32 | } 33 | 34 | impl Iterator for RedBlackTreeSetRangeIter { 35 | type Item = Result; 36 | 37 | fn next(&mut self) -> Option { 38 | let start = self 39 | .last_front 40 | .clone() 41 | .map(add_one) 42 | .unwrap_or_else(|| self.start.clone()); 43 | let end = self.last_back.clone().unwrap_or_else(|| self.end.clone()); 44 | if start > end { 45 | None 46 | } else { 47 | let range = start..end; 48 | let mut it = self.set.range(range); 49 | let item = it.next()?.to_owned(); 50 | self.last_front = Some(item.clone()); 51 | Some(Ok(item)) 52 | } 53 | } 54 | } 55 | 56 | impl DoubleEndedIterator for RedBlackTreeSetRangeIter { 57 | fn next_back(&mut self) -> Option { 58 | let start = self 59 | .last_front 60 | .clone() 61 | .map(add_one) 62 | .unwrap_or_else(|| self.start.clone()); 63 | let end = self.last_back.clone().unwrap_or_else(|| self.end.clone()); 64 | if start > end { 65 | None 66 | } else { 67 | let range = start..end; 68 | let mut it = self.set.range(range).rev(); 69 | let item = it.next()?.to_owned(); 70 | self.last_back = Some(item.clone()); 71 | Some(Ok(item)) 72 | } 73 | } 74 | } 75 | 76 | impl Storage for RedBlackTreeSetStorage { 77 | fn range(&self, r: Range<&[u8]>) -> Result, StorageError> { 78 | let set = (*self.set.load_full()).clone(); 79 | Ok(Box::new(RedBlackTreeSetRangeIter { 80 | set, 81 | last_front: None, 82 | last_back: None, 83 | start: r.start.to_vec(), 84 | end: r.end.to_vec(), 85 | })) 86 | } 87 | 88 | fn insert(&self, is: &[Item]) -> Result<(), StorageError> { 89 | let set = (*self.set.load_full()).clone(); 90 | let set = is.iter().fold(set, |s, i| s.insert(i.to_owned())); 91 | self.set.swap(Arc::new(set)); 92 | Ok(()) 93 | } 94 | 95 | fn id(&self) -> ID { 96 | self.id 97 | } 98 | } 99 | 100 | impl RedBlackTreeSetStorage { 101 | /// Create a new empty [RedBlackTreeSetStorage] 102 | pub fn new() -> Self { 103 | Self { 104 | set: ArcSwap::from_pointee(RedBlackTreeSetSync::new_sync()), 105 | id: ID::new(), 106 | } 107 | } 108 | } 109 | 110 | impl Default for RedBlackTreeSetStorage { 111 | fn default() -> Self { 112 | Self::new() 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /datom/src/backends/sled.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | use std::{env::temp_dir, ops::Range}; 6 | 7 | use sled::{transaction::ConflictableTransactionError, Config, Db}; 8 | use uuid::Uuid; 9 | 10 | use crate::{ 11 | storage::{DurableStorage, Item, ItemIterator, Storage}, 12 | StorageError, ID, 13 | }; 14 | 15 | impl From for StorageError { 16 | fn from(e: sled::Error) -> Self { 17 | Self::Miscellaneous(Box::new(e)) 18 | } 19 | } 20 | 21 | impl From for StorageError { 22 | fn from(e: sled::transaction::TransactionError) -> Self { 23 | Self::Miscellaneous(Box::new(e)) 24 | } 25 | } 26 | 27 | /// A storage backend backed by a [sled] database 28 | pub struct SledStorage { 29 | db: Db, 30 | id: ID, 31 | } 32 | 33 | impl Storage for SledStorage { 34 | fn range(&self, r: Range<&[u8]>) -> Result, StorageError> { 35 | Ok(Box::new( 36 | self.db 37 | .range(r) 38 | .map(|r| r.map_err(|e| e.into()).map(|(k, _)| k.to_vec())), 39 | )) 40 | } 41 | 42 | fn insert(&self, is: &[Item]) -> Result<(), StorageError> { 43 | self.db.transaction(|t| { 44 | for i in is { 45 | t.insert(i.as_slice(), vec![])?; 46 | } 47 | Ok::<(), ConflictableTransactionError>(()) 48 | })?; 49 | Ok(()) 50 | } 51 | 52 | fn id(&self) -> ID { 53 | self.id 54 | } 55 | } 56 | 57 | impl DurableStorage for SledStorage {} 58 | 59 | impl SledStorage { 60 | /// Create a connection to a temporary database. When the 61 | /// [SledStorage] is dropped, the temporary database will be 62 | /// removed from the disk. This is useful for tests. 63 | pub fn connect_temp() -> Result { 64 | let mut path = temp_dir(); 65 | path.push(Uuid::new_v4().to_string()); 66 | path.set_extension("db"); 67 | let cfg = Config::new().path(path).temporary(true); 68 | let db = cfg.open()?; 69 | Ok(Self { db, id: ID::new() }) 70 | } 71 | 72 | /// Create a connection to a database. 73 | pub fn connect(uri: &str) -> Result { 74 | let cfg = Config::new().path(uri); 75 | let db = cfg.open()?; 76 | Ok(Self { db, id: ID::new() }) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /datom/src/backends/tiered.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | use std::{cmp::Ordering, ops::Range}; 6 | 7 | use crate::{ 8 | merge_iters::MergeIters, 9 | storage::{DurableStorage, Item, ItemIterator, Storage}, 10 | StorageError, ID, 11 | }; 12 | 13 | /// A storage backend backed by two other storage backends 14 | /// Inserts are sent to both backends. 15 | /// Reads come from both backends, and any items which are read from B 16 | /// but not found in A will persisted to A in a future update. 17 | pub struct TieredStorage { 18 | a: A, 19 | b: B, 20 | id: ID, 21 | } 22 | 23 | impl TieredStorage { 24 | /// Create a new tiered storage from two other storages 25 | pub fn new(a: A, b: B) -> Self { 26 | Self { 27 | a, 28 | b, 29 | id: ID::new(), 30 | } 31 | } 32 | } 33 | 34 | impl PartialEq for StorageError { 35 | fn eq(&self, other: &Self) -> bool { 36 | match (self, other) { 37 | (Self::Miscellaneous(_), Self::Miscellaneous(_)) => true, 38 | _ => core::mem::discriminant(self) == core::mem::discriminant(other), 39 | } 40 | } 41 | } 42 | 43 | impl Eq for StorageError {} 44 | 45 | impl PartialOrd for StorageError { 46 | fn partial_cmp(&self, other: &Self) -> Option { 47 | Some(self.cmp(other)) 48 | } 49 | } 50 | 51 | impl Ord for StorageError { 52 | fn cmp(&self, other: &Self) -> std::cmp::Ordering { 53 | let s = match *self { 54 | Self::ConcurrencyError => 0, 55 | Self::IOError(_) => 1, 56 | Self::Miscellaneous(_) => 2, 57 | }; 58 | let o = match *other { 59 | Self::ConcurrencyError => 0, 60 | Self::IOError(_) => 1, 61 | Self::Miscellaneous(_) => 2, 62 | }; 63 | s.cmp(&o) 64 | } 65 | } 66 | 67 | impl Storage for TieredStorage { 68 | fn range(&self, r: Range<&[u8]>) -> Result, StorageError> { 69 | let merged = MergeIters::new(self.a.range(r.clone())?, self.b.range(r.clone())?); 70 | Ok(Box::new(merged.map(|x| x.0))) 71 | } 72 | 73 | fn insert(&self, is: &[Item]) -> Result<(), StorageError> { 74 | self.a.insert(is)?; 75 | self.b.insert(is)?; 76 | Ok(()) 77 | } 78 | 79 | fn id(&self) -> ID { 80 | self.id 81 | } 82 | } 83 | 84 | impl DurableStorage for TieredStorage {} 85 | -------------------------------------------------------------------------------- /datom/src/bin/peer_server.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | fn main() { 6 | eprintln!("This binary is a stub and has yet to be implemented."); 7 | } 8 | -------------------------------------------------------------------------------- /datom/src/bin/transactor.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | fn main() { 6 | eprintln!("This binary is a stub and has yet to be implemented."); 7 | } 8 | -------------------------------------------------------------------------------- /datom/src/builtin_idents.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | use once_cell::sync::Lazy; 6 | use std::collections::HashMap; 7 | 8 | use crate::{Value, ID as TID}; 9 | 10 | /// An entity's [ID]. This is a virtual attribute, and isn't actually 11 | /// stored. 12 | pub const ID: TID = TID::from_u128(329992551406372030633500533120122732713u128); 13 | 14 | /// An entity's alias, usually used for the attribute schema. 15 | pub const IDENT: TID = TID::from_u128(265682209113858765770461024079827500234u128); 16 | 17 | /// Whether an attribute associates one value or multiple values with an 18 | /// entity 19 | pub const CARDINALITY: TID = TID::from_u128(110064945635332807503383834157761461043u128); 20 | 21 | /// The intended type for an attribute. Note that this is not 22 | /// (currently) strictly checked. 23 | pub const VALUE_TYPE: TID = TID::from_u128(276059213908560386420175049892299151374u128); 24 | 25 | /// A documentation string for an entity 26 | pub const DOC: TID = TID::from_u128(303289866496710859530474533904741988829u128); 27 | 28 | /// Whether there can only be one entity per value for this attribute 29 | pub const UNIQUE: TID = TID::from_u128(307615836394596470679724073561969695989); 30 | 31 | /// Whether the entity referred to in this [TYPE_REF] attribute is a 32 | /// sub-component. When you retract an entity, all sub-components will 33 | /// also be retracted. 34 | pub const IS_COMPONENT: TID = TID::from_u128(308724514559417715856375983930347810391u128); 35 | 36 | /// A value for the [CARDINALITY](self::CARDINALITY) attribute 37 | pub const CARDINALITY_ONE: TID = TID::from_u128(143444949937465711736574828873158396909u128); 38 | 39 | /// A value for the [CARDINALITY](self::CARDINALITY) attribute 40 | 41 | pub const CARDINALITY_MANY: TID = TID::from_u128(11338831660433813835424721536043447369u128); 42 | 43 | /// A value for the [VALUE_TYPE](self::VALUE_TYPE) attribute 44 | 45 | pub const TYPE_STRING: TID = TID::from_u128(301439516182801820546961599694687577507u128); 46 | 47 | /// A value for the [VALUE_TYPE](self::VALUE_TYPE) attribute 48 | pub const TYPE_INTEGER: TID = TID::from_u128(183876393307651966214416059730593095u128); 49 | 50 | /// A value for the [VALUE_TYPE](self::VALUE_TYPE) attribute 51 | pub const TYPE_DECIMAL: TID = TID::from_u128(297077785792417755741249562058415972414u128); 52 | 53 | /// A value for the [VALUE_TYPE](self::VALUE_TYPE) attribute 54 | pub const TYPE_ID: TID = TID::from_u128(339681506578292470250558610134765439055u128); 55 | 56 | /// A value for the [VALUE_TYPE](self::VALUE_TYPE) attribute 57 | pub const TYPE_REF: TID = TID::from_u128(149893903729185565330222631892178876560u128); 58 | 59 | /// A value for the [VALUE_TYPE](self::VALUE_TYPE) attribute 60 | pub const TYPE_BOOLEAN: TID = TID::from_u128(149893903729185565330222631892178876560u128); 61 | 62 | /// The data behind a built-in entity 63 | pub type BuiltinEntity = HashMap; 64 | 65 | /// The type of the data behind all built-in entities 66 | pub type BuiltinEntities = HashMap; 67 | 68 | /// The type of the data behind all built-in entities, by ident 69 | pub type BuiltinEntitiesByIdent = HashMap; 70 | 71 | /// The data behind all built-in entities 72 | pub static BUILTIN_ENTITIES: Lazy = Lazy::new(|| { 73 | let mut entities = BuiltinEntities::new(); 74 | entities.insert(ID, { 75 | let mut entity = BuiltinEntity::new(); 76 | entity.insert(ID, ID.into()); 77 | entity.insert(IDENT, Value::from("db/id")); 78 | entity.insert(VALUE_TYPE, Value::from(TYPE_ID)); 79 | entity.insert(CARDINALITY, Value::from(CARDINALITY_ONE)); 80 | entity 81 | }); 82 | entities.insert(IDENT, { 83 | let mut entity = BuiltinEntity::new(); 84 | entity.insert(ID, IDENT.into()); 85 | entity.insert(IDENT, Value::from("db/ident")); 86 | entity.insert(UNIQUE, Value::from(true)); 87 | entity.insert(VALUE_TYPE, Value::from(TYPE_STRING)); 88 | entity.insert(CARDINALITY, Value::from(CARDINALITY_ONE)); 89 | entity 90 | }); 91 | entities.insert(CARDINALITY, { 92 | let mut entity = BuiltinEntity::new(); 93 | entity.insert(ID, CARDINALITY.into()); 94 | entity.insert(IDENT, Value::from("db/cardinality")); 95 | entity.insert(VALUE_TYPE, Value::from(TYPE_REF)); 96 | entity.insert(CARDINALITY, Value::from(CARDINALITY_ONE)); 97 | entity 98 | }); 99 | entities.insert(VALUE_TYPE, { 100 | let mut entity = BuiltinEntity::new(); 101 | entity.insert(ID, VALUE_TYPE.into()); 102 | entity.insert(IDENT, Value::from("db/value-type")); 103 | entity.insert(VALUE_TYPE, Value::from(TYPE_REF)); 104 | entity.insert(CARDINALITY, Value::from(CARDINALITY_ONE)); 105 | entity 106 | }); 107 | entities.insert(DOC, { 108 | let mut entity = BuiltinEntity::new(); 109 | entity.insert(ID, DOC.into()); 110 | entity.insert(IDENT, Value::from("db/doc")); 111 | entity.insert(VALUE_TYPE, Value::from(TYPE_STRING)); 112 | entity.insert(CARDINALITY, Value::from(CARDINALITY_ONE)); 113 | entity 114 | }); 115 | entities.insert(UNIQUE, { 116 | let mut entity = BuiltinEntity::new(); 117 | entity.insert(ID, UNIQUE.into()); 118 | entity.insert(IDENT, Value::from("db/unique")); 119 | entity.insert(VALUE_TYPE, Value::from(TYPE_BOOLEAN)); 120 | entity.insert(CARDINALITY, Value::from(CARDINALITY_ONE)); 121 | entity 122 | }); 123 | entities.insert(IS_COMPONENT, { 124 | let mut entity = BuiltinEntity::new(); 125 | entity.insert(ID, IS_COMPONENT.into()); 126 | entity.insert(IDENT, Value::from("db/is-component")); 127 | entity.insert(VALUE_TYPE, Value::from(TYPE_BOOLEAN)); 128 | entity.insert(CARDINALITY, Value::from(CARDINALITY_ONE)); 129 | entity 130 | }); 131 | entities.insert(CARDINALITY_ONE, { 132 | let mut entity = BuiltinEntity::new(); 133 | entity.insert(ID, CARDINALITY_ONE.into()); 134 | entity.insert(IDENT, Value::from("db.cardinality/one")); 135 | entity 136 | }); 137 | entities.insert(CARDINALITY_MANY, { 138 | let mut entity = BuiltinEntity::new(); 139 | entity.insert(ID, CARDINALITY_MANY.into()); 140 | entity.insert(IDENT, Value::from("db.cardinality/many")); 141 | entity 142 | }); 143 | entities.insert(TYPE_STRING, { 144 | let mut entity = BuiltinEntity::new(); 145 | entity.insert(ID, TYPE_STRING.into()); 146 | entity.insert(IDENT, Value::from("db.type/string")); 147 | entity 148 | }); 149 | entities.insert(TYPE_INTEGER, { 150 | let mut entity = BuiltinEntity::new(); 151 | entity.insert(ID, TYPE_INTEGER.into()); 152 | entity.insert(IDENT, Value::from("db.type/integer")); 153 | entity 154 | }); 155 | entities.insert(TYPE_DECIMAL, { 156 | let mut entity = BuiltinEntity::new(); 157 | entity.insert(ID, TYPE_DECIMAL.into()); 158 | entity.insert(IDENT, Value::from("db.type/decimal")); 159 | entity 160 | }); 161 | entities.insert(TYPE_ID, { 162 | let mut entity = BuiltinEntity::new(); 163 | entity.insert(ID, TYPE_ID.into()); 164 | entity.insert(IDENT, Value::from("db.type/id")); 165 | entity 166 | }); 167 | entities.insert(TYPE_REF, { 168 | let mut entity = BuiltinEntity::new(); 169 | entity.insert(ID, TYPE_REF.into()); 170 | entity.insert(IDENT, Value::from("db.type/ref")); 171 | entity 172 | }); 173 | entities.insert(TYPE_BOOLEAN, { 174 | let mut entity = BuiltinEntity::new(); 175 | entity.insert(ID, TYPE_BOOLEAN.into()); 176 | entity.insert(IDENT, Value::from("db.type/boolean")); 177 | entity 178 | }); 179 | entities 180 | }); 181 | 182 | /// The data behind all built-in entities, by ident 183 | pub static BUILTIN_ENTITIES_BY_IDENT: Lazy = Lazy::new(|| { 184 | let mut m = HashMap::new(); 185 | for e in BUILTIN_ENTITIES.values() { 186 | if let Some(Value::String(ident)) = e.get(&IDENT) { 187 | m.insert(ident.to_owned(), e.to_owned()); 188 | } 189 | } 190 | m 191 | }); 192 | 193 | #[cfg(test)] 194 | mod tests { 195 | use std::collections::HashSet; 196 | 197 | use super::*; 198 | 199 | #[test] 200 | fn no_duplicates() { 201 | let mut set = HashSet::new(); 202 | let ids = [ 203 | CARDINALITY, 204 | CARDINALITY_MANY, 205 | CARDINALITY_ONE, 206 | DOC, 207 | IDENT, 208 | IS_COMPONENT, 209 | TYPE_DECIMAL, 210 | TYPE_ID, 211 | TYPE_INTEGER, 212 | TYPE_REF, 213 | TYPE_STRING, 214 | UNIQUE, 215 | VALUE_TYPE, 216 | ]; 217 | for id in ids { 218 | assert!(!set.contains(&id)); 219 | set.insert(id); 220 | } 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /datom/src/lib.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | #![deny(missing_docs)] 6 | #![warn(clippy::nursery)] 7 | #![doc( 8 | html_logo_url = "https://avatars.githubusercontent.com/u/85201395?s=200&v=4", 9 | html_favicon_url = "https://lutris.engineering/favicon-32x32.png" 10 | )] 11 | 12 | //! An open-source database inspired by Datomic. 13 | //! 14 | //! ```text 15 | //! // SPDX-FileCopyrightText: 2022 Lutris, Inc 16 | //! // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 17 | //! // SPDX-FileContributor: Piper McCorkle 18 | //! ``` 19 | //! 20 | //! Currently built on top of [sled], a modern embedded database, and is 21 | //! only suitable for embedded usage. Future multi-peer support is 22 | //! planned. 23 | //! 24 | //! If you aren't already familiar with Datomic, it might help to look 25 | //! over [Datomic's excellent documentation]. To the user, datom-rs 26 | //! functions extremely similarly to Datomic. 27 | //! 28 | //! ``` 29 | //! use datom::{backends::SledStorage, Connection, EntityResult, Transaction, Value, ID}; 30 | //! 31 | //! // Use the sled storage backend to create a temporary database 32 | //! let storage = SledStorage::connect_temp()?; 33 | //! 34 | //! // Create a connection from that backend 35 | //! let conn = Connection::new(storage); 36 | //! 37 | //! // Create an ID to use for the username attribute 38 | //! let username = ID::new(); 39 | //! // Create an ID to use for the user's entity 40 | //! let user = ID::new(); 41 | //! 42 | //! // Create a transaction setting the username attribute on the user 43 | //! // entity to "pmc" 44 | //! let mut tx = Transaction::new(); 45 | //! tx.add(user.into(), username.into(), "pmc".into()); 46 | //! // Execute the transaction using the connection 47 | //! conn.transact(tx)?; 48 | //! 49 | //! // Get a view of the database in the current point in time 50 | //! let db = conn.db()?; 51 | //! // Get the value of the username attribute on the user entity 52 | //! if let EntityResult::Value(Value::String(u)) = db.entity(user.into())?.get(username.into())? { 53 | //! println!("The user's username is {}.", u); 54 | //! } 55 | //! # assert_eq!(db.entity(user.into())?.get(username.into())?, EntityResult::Value("pmc".into())); 56 | //! # Ok::<(), Box>(()) 57 | //! ``` 58 | //! 59 | //! [sled]: https://sled.rs 60 | //! [Datomic's excellent documentation]: https://docs.datomic.com/on-prem/overview/architecture.html 61 | 62 | mod types; 63 | use std::str::FromStr; 64 | 65 | use edn_rs::Edn; 66 | pub use types::*; 67 | 68 | /// Serialization/deserialization functions 69 | pub mod serial; 70 | 71 | /// IDs for the built-in attributes and other idents 72 | pub mod builtin_idents; 73 | 74 | mod merge_iters; 75 | 76 | /// API for storage backends 77 | pub mod storage; 78 | 79 | /// Storage backends 80 | pub mod backends; 81 | 82 | /// Get the version of this datom build 83 | pub const fn version() -> &'static str { 84 | env!("CARGO_PKG_VERSION") 85 | } 86 | 87 | /// Parse an EDN string to an [Edn] object 88 | pub fn parse_edn(edn: &str) -> Option { 89 | Edn::from_str(edn).ok() 90 | } 91 | -------------------------------------------------------------------------------- /datom/src/merge_iters.rs: -------------------------------------------------------------------------------- 1 | use std::mem; 2 | 3 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 4 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 5 | // SPDX-FileContributor: Piper McCorkle 6 | 7 | pub enum OriginIter { 8 | A, 9 | B, 10 | } 11 | 12 | pub struct MergeIters, B: Iterator> { 13 | a: A, 14 | b: B, 15 | a_front: Option>, 16 | a_back: Option>, 17 | b_front: Option>, 18 | b_back: Option>, 19 | } 20 | 21 | impl, B: Iterator> Iterator for MergeIters { 22 | type Item = (T, OriginIter); 23 | 24 | fn next(&mut self) -> Option { 25 | let a = if let Some(a_val) = mem::replace(&mut self.a_front, None) { 26 | a_val 27 | } else { 28 | self.a.next() 29 | }; 30 | let b = if let Some(b_val) = mem::replace(&mut self.b_front, None) { 31 | b_val 32 | } else { 33 | self.b.next() 34 | }; 35 | let (res, a, b) = if let Some(a) = a { 36 | if let Some(b) = b { 37 | if a < b { 38 | (Some((a, OriginIter::A)), self.a.next(), Some(b)) 39 | } else { 40 | (Some((b, OriginIter::B)), Some(a), self.b.next()) 41 | } 42 | } else { 43 | (Some((a, OriginIter::A)), self.a.next(), None) 44 | } 45 | } else if let Some(b) = b { 46 | (Some((b, OriginIter::B)), None, self.b.next()) 47 | } else { 48 | (None, None, None) 49 | }; 50 | self.a_front = Some(a); 51 | self.b_front = Some(b); 52 | res 53 | } 54 | } 55 | 56 | impl< 57 | T: Ord, 58 | A: Iterator + DoubleEndedIterator, 59 | B: Iterator + DoubleEndedIterator, 60 | > DoubleEndedIterator for MergeIters 61 | { 62 | fn next_back(&mut self) -> Option { 63 | let a = if let Some(a_val) = mem::replace(&mut self.a_back, None) { 64 | a_val 65 | } else { 66 | self.a.next_back() 67 | }; 68 | let b = if let Some(b_val) = mem::replace(&mut self.b_back, None) { 69 | b_val 70 | } else { 71 | self.b.next_back() 72 | }; 73 | let (res, a, b) = if let Some(a) = a { 74 | if let Some(b) = b { 75 | if a > b { 76 | (Some((a, OriginIter::A)), self.a.next_back(), Some(b)) 77 | } else { 78 | (Some((b, OriginIter::B)), Some(a), self.b.next_back()) 79 | } 80 | } else { 81 | (Some((a, OriginIter::A)), self.a.next_back(), None) 82 | } 83 | } else if let Some(b) = b { 84 | (Some((b, OriginIter::B)), None, self.b.next_back()) 85 | } else { 86 | (None, None, None) 87 | }; 88 | self.a_back = Some(a); 89 | self.b_back = Some(b); 90 | res 91 | } 92 | } 93 | 94 | impl, B: Iterator> MergeIters { 95 | /// Merge n sorted iterators 96 | pub const fn new(a: A, b: B) -> Self { 97 | Self { 98 | a, 99 | b, 100 | a_front: None, 101 | a_back: None, 102 | b_front: None, 103 | b_back: None, 104 | } 105 | } 106 | } 107 | 108 | #[cfg(test)] 109 | mod tests { 110 | use super::MergeIters; 111 | 112 | #[test] 113 | fn basic_test() { 114 | let first: [u64; 4] = [1, 76, 2754, 53326]; 115 | let second: [u64; 3] = [53, 62, 431]; 116 | let merged = MergeIters::new(first.iter().cloned(), second.iter().cloned()); 117 | let results: Vec = merged.map(|x| x.0).collect(); 118 | assert_eq!(vec![1, 53, 62, 76, 431, 2754, 53326], results); 119 | } 120 | 121 | #[test] 122 | fn basic_reverse_test() { 123 | let first: [u64; 4] = [1, 76, 2754, 53326]; 124 | let second: [u64; 3] = [53, 62, 431]; 125 | let merged = MergeIters::new(first.iter().cloned(), second.iter().cloned()); 126 | let results: Vec = merged.rev().map(|x| x.0).collect(); 127 | assert_eq!(vec![53326, 2754, 431, 76, 62, 53, 1], results); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /datom/src/storage.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | use std::ops::Range; 6 | 7 | use crate::{StorageError, ID}; 8 | 9 | /// A serialized datom 10 | pub type Item = Vec; 11 | 12 | /// An iterator over a sorted set of datoms 13 | pub type ItemIterator<'s> = Box> + 's>; 14 | 15 | /// A [std::collections::BTreeSet]-like storage backend 16 | pub trait Storage: Send + Sync { 17 | /// Get all items within this range 18 | fn range(&self, r: Range<&[u8]>) -> Result, StorageError>; 19 | 20 | /// Insert many new items into the backend (in one transaction, if possible) 21 | fn insert(&self, is: &[Item]) -> Result<(), StorageError>; 22 | 23 | /// Get a unique ID for this instance 24 | fn id(&self) -> ID; 25 | } 26 | 27 | impl Storage for Box { 28 | fn range(&self, r: Range<&[u8]>) -> Result, StorageError> { 29 | (**self).range(r) 30 | } 31 | 32 | fn insert(&self, is: &[Item]) -> Result<(), StorageError> { 33 | (**self).insert(is) 34 | } 35 | 36 | fn id(&self) -> ID { 37 | (**self).id() 38 | } 39 | } 40 | 41 | /// A storage backend which persists its state. This may take the form 42 | /// of a file on disk, a remote database, or something else. 43 | pub trait DurableStorage: Storage {} 44 | -------------------------------------------------------------------------------- /datom/src/types/attribute_iterator.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | use std::collections::HashSet; 6 | 7 | use crate::{DatomIterator, DatomType, QueryError, ID}; 8 | 9 | /// An iterator over attributes in a sled-backed database 10 | pub struct AttributeIterator<'d> { 11 | iter: DatomIterator<'d>, 12 | seen: HashSet, 13 | } 14 | 15 | impl<'d> AttributeIterator<'d> { 16 | pub(crate) fn new(iter: DatomIterator<'d>) -> Result { 17 | Ok(Self { 18 | iter, 19 | seen: HashSet::new(), 20 | }) 21 | } 22 | } 23 | 24 | impl<'s> Iterator for AttributeIterator<'s> { 25 | type Item = ID; 26 | 27 | fn next(&mut self) -> Option { 28 | for datom in (&mut self.iter).rev() { 29 | let attr = datom.attribute; 30 | if !self.seen.contains(&attr) { 31 | self.seen.insert(attr); 32 | if datom.datom_type == DatomType::Addition { 33 | return Some(attr); 34 | } 35 | } 36 | } 37 | None 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /datom/src/types/attribute_schema.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | use crate::{builtin_idents, Transactable, Transaction, Value, ID}; 6 | 7 | /// The type of an attribute's values 8 | #[derive(Clone, Copy)] 9 | pub enum AttributeType { 10 | /// A [Value::String](crate::Value::String) 11 | String, 12 | /// A [Value::Integer](crate::Value::Integer) 13 | Integer, 14 | /// A [Value::Decimal](crate::Value::Decimal) 15 | Decimal, 16 | /// A [Value::ID](crate::Value::ID) 17 | ID, 18 | /// A [Value::ID](crate::Value::ID) referring to an entity 19 | Ref, 20 | /// A [Value::Boolean](crate::Value::Boolean) 21 | Boolean, 22 | } 23 | 24 | impl From for ID { 25 | fn from(t: AttributeType) -> Self { 26 | use builtin_idents::*; 27 | match t { 28 | AttributeType::String => TYPE_STRING, 29 | AttributeType::Integer => TYPE_INTEGER, 30 | AttributeType::Decimal => TYPE_DECIMAL, 31 | AttributeType::ID => TYPE_ID, 32 | AttributeType::Ref => TYPE_REF, 33 | AttributeType::Boolean => TYPE_BOOLEAN, 34 | } 35 | } 36 | } 37 | 38 | /// An imperative way to generate an attribute's schema 39 | #[derive(Clone)] 40 | pub struct AttributeSchema { 41 | /// The attribute's ID 42 | pub id: ID, 43 | /// The attribute's unique identifier 44 | pub ident: Option, 45 | /// Whether this attribute can store multiple values for an entity 46 | pub many: bool, 47 | /// What type values should be in this attribute 48 | pub value_type: Option, 49 | /// A docstring for this attribute 50 | pub doc: Option, 51 | /// Whether there can only be one entity of each value for this 52 | /// attribute 53 | pub unique: bool, 54 | /// Whether this attribute refers to a component 55 | pub component: bool, 56 | } 57 | 58 | impl AttributeSchema { 59 | /// Start generating an attribute's schema 60 | pub fn new() -> Self { 61 | Self { 62 | id: ID::new(), 63 | ident: None, 64 | many: false, 65 | value_type: None, 66 | doc: None, 67 | unique: false, 68 | component: false, 69 | } 70 | } 71 | 72 | /// Set a specific ID for an attribute 73 | pub const fn set_id(mut self, id: ID) -> Self { 74 | self.id = id; 75 | self 76 | } 77 | 78 | /// Set the attribute's ident 79 | #[allow(clippy::missing_const_for_fn)] 80 | pub fn ident(mut self, ident: String) -> Self { 81 | self.ident = Some(ident); 82 | self 83 | } 84 | 85 | /// Set the attribute's cardinality to many 86 | pub const fn many(mut self) -> Self { 87 | self.many = true; 88 | self 89 | } 90 | 91 | /// Set the attribute's value type 92 | pub const fn value_type(mut self, t: AttributeType) -> Self { 93 | self.value_type = Some(t); 94 | self 95 | } 96 | 97 | /// Set the attribute's docstring 98 | #[allow(clippy::missing_const_for_fn)] 99 | pub fn doc(mut self, doc: String) -> Self { 100 | self.doc = Some(doc); 101 | self 102 | } 103 | 104 | /// Set the attribute as unique 105 | pub const fn unique(mut self) -> Self { 106 | self.unique = true; 107 | self 108 | } 109 | 110 | /// Set the attribute as being a component reference 111 | pub const fn component(mut self) -> Self { 112 | self.value_type = Some(AttributeType::Ref); 113 | self.component = true; 114 | self 115 | } 116 | } 117 | 118 | impl Default for AttributeSchema { 119 | /// ``` 120 | /// datom::AttributeSchema::default(); 121 | /// ``` 122 | fn default() -> Self { 123 | Self::new() 124 | } 125 | } 126 | 127 | impl Transactable for AttributeSchema { 128 | fn tx(&self) -> Transaction { 129 | let mut tx = Transaction::new(); 130 | tx.add(self.id.into(), builtin_idents::ID.into(), self.id.into()); 131 | if let Some(ident) = self.ident.clone() { 132 | tx.add(self.id.into(), builtin_idents::IDENT.into(), ident.into()); 133 | } 134 | if self.many { 135 | tx.add( 136 | self.id.into(), 137 | builtin_idents::CARDINALITY.into(), 138 | builtin_idents::CARDINALITY_MANY.into(), 139 | ) 140 | } 141 | if let Some(t) = self.value_type { 142 | tx.add( 143 | self.id.into(), 144 | builtin_idents::VALUE_TYPE.into(), 145 | Value::ID(t.into()), 146 | ); 147 | } 148 | if let Some(doc) = self.doc.clone() { 149 | tx.add(self.id.into(), builtin_idents::DOC.into(), doc.into()); 150 | } 151 | if self.unique { 152 | tx.add(self.id.into(), builtin_idents::UNIQUE.into(), true.into()); 153 | } 154 | if self.component { 155 | tx.add( 156 | self.id.into(), 157 | builtin_idents::IS_COMPONENT.into(), 158 | true.into(), 159 | ); 160 | } 161 | tx 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /datom/src/types/connection.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | use std::fmt::Debug; 6 | 7 | use chrono::Utc; 8 | 9 | use crate::{ 10 | builtin_idents, 11 | serial::{ 12 | deserialize_tr, serialize_aevt, serialize_avet, serialize_eavt, serialize_tr, 13 | serialize_vaet, tr_range, vec_range_slice, 14 | }, 15 | storage::Storage, 16 | ConnectionError, Database, Datom, EntityResult, Index, Transactable, Transaction, 17 | TransactionError, TransactionRecord, TransactionResult, Value, ID, 18 | }; 19 | 20 | /// A persistent connection to a database 21 | pub struct Connection { 22 | pub(crate) storage: S, 23 | pub(crate) id: ID, 24 | } 25 | 26 | impl PartialEq for Connection { 27 | /// ``` 28 | /// use datom::{backends::SledStorage, Connection}; 29 | /// let storage1 = SledStorage::connect_temp()?; 30 | /// let storage2 = SledStorage::connect_temp()?; 31 | /// let conn1 = Connection::new(storage1); 32 | /// let conn2 = Connection::new(storage2); 33 | /// let conn1r = &conn1; 34 | /// let conn2r = &conn2; 35 | /// 36 | /// assert_eq!(&conn1, &conn1); 37 | /// assert_eq!(&conn1, conn1r); 38 | /// assert_eq!(conn1r, &conn1); 39 | /// assert_eq!(conn1r, conn1r); 40 | /// 41 | /// assert_ne!(&conn1, &conn2); 42 | /// assert_ne!(&conn1, conn2r); 43 | /// assert_ne!(conn1r, &conn2); 44 | /// assert_ne!(conn1r, conn2r); 45 | /// assert_ne!(&conn2, &conn1); 46 | /// assert_ne!(&conn2, conn1r); 47 | /// assert_ne!(conn2r, &conn1); 48 | /// assert_ne!(conn2r, conn1r); 49 | /// 50 | /// # Ok::<(), Box>(()) 51 | /// ``` 52 | fn eq(&self, other: &Self) -> bool { 53 | self.id == other.id 54 | } 55 | } 56 | 57 | impl Eq for Connection {} 58 | 59 | /// A connection which uses a dynamically dispatched storage backend 60 | pub type DynamicConnection = Connection>; 61 | 62 | /// Create a new connection which uses a dynamically dispatched storage 63 | /// backend 64 | pub fn new_dynamic_connection(storage: S) -> DynamicConnection { 65 | Connection::new(Box::new(storage)) 66 | } 67 | 68 | impl Connection { 69 | /// Create a new connection from a storage backend 70 | pub fn new(storage: S) -> Self { 71 | Self { 72 | storage, 73 | id: ID::new(), 74 | } 75 | } 76 | 77 | /// Fetch the t-value for the latest transaction 78 | pub fn latest_t(&self) -> Result { 79 | if let Some(res) = self.storage.range(vec_range_slice(&tr_range()))?.last() { 80 | let bytes = res?; 81 | deserialize_tr(&bytes).map_or(Err(ConnectionError::InvalidData), |tx| Ok(tx.t)) 82 | } else { 83 | Ok(0) 84 | } 85 | } 86 | 87 | /// Fetch the t-value for the latest transaction 88 | pub const fn as_of(&self, t: u64) -> Result, ConnectionError> { 89 | Ok(Database { 90 | connection: self, 91 | t, 92 | }) 93 | } 94 | 95 | /// Get a [database](crate::Database) for the current 96 | /// point in time 97 | pub fn db(&self) -> Result, ConnectionError> { 98 | self.as_of(self.latest_t()?) 99 | } 100 | 101 | /// Run a transaction on the database 102 | pub fn transact_tx( 103 | &self, 104 | tx: Transaction, 105 | ) -> Result, TransactionError> { 106 | let t_before = self.latest_t()?; 107 | let t = t_before + 1; 108 | let before = self.as_of(t_before)?; 109 | let data = tx.datoms(t, &before)?; 110 | let mut items: Vec> = vec![]; 111 | for datom in data.iter() { 112 | items.push(serialize_eavt(datom)); 113 | items.push(serialize_aevt(datom)); 114 | let attr_entity = before.entity(datom.attribute.into())?; 115 | let unique_value = 116 | attr_entity.get_with_options(builtin_idents::UNIQUE.into(), true, true)?; 117 | let type_value = 118 | attr_entity.get_with_options(builtin_idents::VALUE_TYPE.into(), true, true)?; 119 | let is_unique = { 120 | if let EntityResult::Value(Value::Boolean(x)) = unique_value { 121 | x 122 | } else { 123 | false 124 | } 125 | }; 126 | let is_ref = { 127 | if let EntityResult::Value(Value::ID(id)) = type_value { 128 | id == builtin_idents::TYPE_REF 129 | } else { 130 | false 131 | } 132 | }; 133 | if is_unique { 134 | items.push(serialize_avet(datom)); 135 | } 136 | if is_ref { 137 | items.push(serialize_vaet(datom)); 138 | } 139 | } 140 | items.push(serialize_tr(&TransactionRecord { 141 | t, 142 | timestamp: Utc::now(), 143 | })); 144 | self.storage.insert(&items).map_err(ConnectionError::from)?; 145 | Ok(TransactionResult { 146 | connection: self, 147 | before, 148 | after: self.as_of(t)?, 149 | data, 150 | }) 151 | } 152 | 153 | /// Transact a transactable on the database 154 | pub fn transact( 155 | &self, 156 | txable: T, 157 | ) -> Result, TransactionError> { 158 | self.transact_tx(txable.tx()) 159 | } 160 | } 161 | 162 | impl Debug for Connection { 163 | /// ``` 164 | /// use datom::{Connection, backends::SledStorage}; 165 | /// 166 | /// let storage = SledStorage::connect_temp()?; 167 | /// let conn = Connection::new(storage); 168 | /// println!("{:#?}", conn); 169 | /// # Ok::<(), Box>(()) 170 | /// ``` 171 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 172 | f.debug_struct("Connection") 173 | .field("id", &self.id) 174 | .field( 175 | "EAVT", 176 | &(|| { 177 | Ok::, Box>( 178 | self.db()?.datoms(Index::EAVT)?.collect(), 179 | ) 180 | })(), 181 | ) 182 | .field( 183 | "AEVT", 184 | &(|| { 185 | Ok::, Box>( 186 | self.db()?.datoms(Index::AEVT)?.collect(), 187 | ) 188 | })(), 189 | ) 190 | .field( 191 | "AVET", 192 | &(|| { 193 | Ok::, Box>( 194 | self.db()?.datoms(Index::AVET)?.collect(), 195 | ) 196 | })(), 197 | ) 198 | .field( 199 | "VAET", 200 | &(|| { 201 | Ok::, Box>( 202 | self.db()?.datoms(Index::VAET)?.collect(), 203 | ) 204 | })(), 205 | ) 206 | .finish() 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /datom/src/types/connection_error.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | #![allow(missing_docs)] 6 | 7 | use miette::Diagnostic; 8 | use thiserror::Error; 9 | 10 | use crate::StorageError; 11 | 12 | /// Network/disk errors 13 | #[derive(Error, Debug, Diagnostic)] 14 | pub enum ConnectionError { 15 | #[error("there was invalid data in the data store")] 16 | #[diagnostic(code(datom::connection::invalid_data), url(docsrs))] 17 | InvalidData, 18 | 19 | #[error("there was an error in the underlying storage backend")] 20 | #[diagnostic(code(datom::storage), url(docsrs))] 21 | Storage(#[from] StorageError), 22 | } 23 | -------------------------------------------------------------------------------- /datom/src/types/database.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | use crate::{ 6 | serial::{ 7 | avet_attribute_value_range, eavt_entity_attribute_range, eavt_entity_range, index_range, 8 | range_slice, vaet_value_attribute_range, vec_range_slice, 9 | }, 10 | storage::Storage, 11 | Connection, DatomIterator, Entity, Index, QueryError, Value, EID, ID, 12 | }; 13 | 14 | /// A view of a database at a specific point in time 15 | #[derive(Debug)] 16 | pub struct Database<'connection, S: Storage> { 17 | pub(crate) connection: &'connection Connection, 18 | pub(crate) t: u64, 19 | } 20 | 21 | impl<'connection, S: Storage> Database<'connection, S> { 22 | /// Get all [datoms](crate::Datom) in the given index 23 | pub fn datoms(&self, index: Index) -> Result, QueryError> { 24 | Ok(DatomIterator::new( 25 | self.connection 26 | .storage 27 | .range(range_slice(&index_range(index)))?, 28 | self.t, 29 | )) 30 | } 31 | 32 | /// Get all [datoms](crate::Datom) in the 33 | /// [EAVT index](crate::Index::EAVT) for the given entity 34 | pub fn datoms_for_entity(&self, entity: ID) -> Result, QueryError> { 35 | Ok(DatomIterator::new( 36 | self.connection 37 | .storage 38 | .range(range_slice(&eavt_entity_range(entity)))?, 39 | self.t, 40 | )) 41 | } 42 | 43 | /// Get all [datoms](crate::Datom) in the 44 | /// [EAVT index](crate::Index::EAVT) for the given entity and 45 | /// attribute 46 | pub fn datoms_for_entity_attribute( 47 | &self, 48 | entity: ID, 49 | attribute: ID, 50 | ) -> Result, QueryError> { 51 | Ok(DatomIterator::new( 52 | self.connection 53 | .storage 54 | .range(range_slice(&eavt_entity_attribute_range(entity, attribute)))?, 55 | self.t, 56 | )) 57 | } 58 | 59 | /// Get all [datoms](crate::Datom) in the 60 | /// [AVET index](crate::Index::AVET) for the given attribute and 61 | /// value 62 | pub fn datoms_for_attribute_value( 63 | &self, 64 | attribute: ID, 65 | value: Value, 66 | ) -> Result, QueryError> { 67 | Ok(DatomIterator::new( 68 | self.connection 69 | .storage 70 | .range(vec_range_slice(&avet_attribute_value_range( 71 | attribute, value, 72 | )))?, 73 | self.t, 74 | )) 75 | } 76 | 77 | /// Get all [datoms](crate::Datom) in the 78 | /// [VAET index](crate::Index::VAET) for the given value and 79 | /// attribute 80 | pub fn datoms_for_value_attribute( 81 | &self, 82 | value: Value, 83 | attribute: ID, 84 | ) -> Result, QueryError> { 85 | Ok(DatomIterator::new( 86 | self.connection 87 | .storage 88 | .range(vec_range_slice(&vaet_value_attribute_range( 89 | value, attribute, 90 | )))?, 91 | self.t, 92 | )) 93 | } 94 | 95 | /// Get an entity 96 | pub fn entity(&self, entity: EID) -> Result, QueryError> { 97 | let entity = entity.resolve(self)?; 98 | Ok(Entity { 99 | connection: self.connection, 100 | t: self.t, 101 | id: entity, 102 | }) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /datom/src/types/datom.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | use crate::{DatomType, Value, ID}; 6 | 7 | /** 8 | A _datom_, or a single fact at a single point in time. Short for 9 | _data atom_. 10 | */ 11 | #[derive(Clone, Debug, PartialEq, Eq)] 12 | pub struct Datom { 13 | /// The entity this [Datom] is attached to 14 | pub entity: ID, 15 | /// The attribute this [Datom] is setting on the entity 16 | pub attribute: ID, 17 | /// The value for the attribute 18 | pub value: Value, 19 | /// The t-value for the transaction which introduced this [Datom] 20 | pub t: u64, 21 | /// Whether this [Datom] is adding or retracting data 22 | pub datom_type: DatomType, 23 | } 24 | -------------------------------------------------------------------------------- /datom/src/types/datom_iterator.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | use crate::{serial::deserialize_unknown, storage::ItemIterator, Datom}; 6 | 7 | /// An iterator over [Datom]s 8 | pub struct DatomIterator<'s> { 9 | iter: ItemIterator<'s>, 10 | t: u64, 11 | } 12 | 13 | impl<'s> DatomIterator<'s> { 14 | pub(crate) fn new(iter: ItemIterator<'s>, t: u64) -> Self { 15 | Self { iter, t } 16 | } 17 | } 18 | 19 | impl<'s> Iterator for DatomIterator<'s> { 20 | type Item = Datom; 21 | 22 | fn next(&mut self) -> Option { 23 | loop { 24 | match self.iter.next() { 25 | None => return None, 26 | Some(Err(_)) => continue, 27 | Some(Ok(k)) => { 28 | let bytes: &[u8] = &k; 29 | let (datom, _) = deserialize_unknown(bytes)?; 30 | if datom.t <= self.t { 31 | return Some(datom); 32 | } 33 | } 34 | } 35 | } 36 | } 37 | } 38 | 39 | impl<'s> DoubleEndedIterator for DatomIterator<'s> { 40 | fn next_back(&mut self) -> Option { 41 | loop { 42 | match self.iter.next_back() { 43 | None => return None, 44 | Some(Err(_)) => continue, 45 | Some(Ok(k)) => { 46 | let bytes: &[u8] = &k; 47 | let (datom, _) = deserialize_unknown(bytes)?; 48 | if datom.t <= self.t { 49 | return Some(datom); 50 | } 51 | } 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /datom/src/types/datom_type.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | /// Whether a [datom](crate::Datom) is showing an addition or a retraction 6 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 7 | pub enum DatomType { 8 | /// Adding an attribute value to an entity 9 | Addition, 10 | /// Removing an attribute value from an entity 11 | Retraction, 12 | } 13 | 14 | impl DatomType { 15 | /// Map the [DatomType] to its byte representation 16 | pub const fn byte(&self) -> u8 { 17 | match self { 18 | Self::Addition => 0, 19 | Self::Retraction => 1, 20 | } 21 | } 22 | 23 | /// Map byte representation to a [DatomType] 24 | pub const fn from_byte(b: u8) -> Self { 25 | match b { 26 | 0 => Self::Addition, 27 | 1 => Self::Retraction, 28 | _ => panic!("invalid datom type"), 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /datom/src/types/eid.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | use std::cmp::Ordering; 6 | 7 | use crate::{builtin_idents, storage::Storage, Database, Datom, QueryError, Value, ID}; 8 | 9 | /** 10 | An un-resolved entity [ID], which can be used to resolve entities by 11 | [ident](crate::builtin_idents::IDENT) or 12 | [unique](crate::builtin_idents::UNIQUE) attribute 13 | */ 14 | #[derive(Clone, Debug, PartialEq, Eq, Hash)] 15 | pub enum EID { 16 | /// A resolved entity [ID] 17 | Resolved(ID), 18 | /// Resolve an entity by its [ident](crate::builtin_idents::IDENT) 19 | Ident(String), 20 | /// Resolve an entity by a static 21 | /// [ident](crate::builtin_idents::IDENT) 22 | InternedIdent(&'static str), 23 | /// Resolve an entity by a [unique](crate::builtin_idents::UNIQUE) 24 | /// attribute 25 | Unique( 26 | /// [unique](crate::builtin_idents::UNIQUE) attribute 27 | Box, 28 | /// Value 29 | Value, 30 | ), 31 | } 32 | 33 | fn by_t(a: &Datom, b: &Datom) -> Ordering { 34 | a.t.cmp(&b.t) 35 | } 36 | 37 | impl EID { 38 | /** 39 | Create an [EID] to resolve an entity by a 40 | [unique](crate::builtin_idents::UNIQUE) attribute 41 | */ 42 | pub fn unique(eid: Self, val: Value) -> Self { 43 | Self::Unique(eid.into(), val) 44 | } 45 | 46 | /// Resolve this [EID] into its [ID] according to a [Database] 47 | pub fn resolve<'c, S: Storage>(&self, db: &Database<'c, S>) -> Result { 48 | match self { 49 | Self::Resolved(id) => Ok(*id), 50 | Self::Ident(ident_str) => { 51 | if let Some(entity) = builtin_idents::BUILTIN_ENTITIES_BY_IDENT.get(ident_str) { 52 | if let Some(Value::ID(id)) = entity.get(&builtin_idents::ID) { 53 | return Ok(id.to_owned()); 54 | } 55 | } 56 | let ident_val = Value::from(ident_str.as_str()); 57 | db.datoms_for_attribute_value(builtin_idents::IDENT, ident_val)? 58 | .max_by(by_t) 59 | .map(|datom| datom.entity) 60 | .ok_or_else(|| QueryError::UnresolvedEID(self.clone())) 61 | } 62 | Self::InternedIdent(ident_str) => Self::Ident(ident_str.to_string()).resolve(db), 63 | Self::Unique(attr_eid, val) => { 64 | let attr_id = attr_eid.resolve(db)?; 65 | db.datoms_for_attribute_value(attr_id, val.to_owned())? 66 | .max_by(by_t) 67 | .map(|datom| datom.entity) 68 | .ok_or_else(|| QueryError::UnresolvedEID(self.clone())) 69 | } 70 | } 71 | } 72 | } 73 | 74 | impl From for EID { 75 | fn from(id: ID) -> Self { 76 | Self::Resolved(id) 77 | } 78 | } 79 | 80 | impl From for EID { 81 | fn from(ident: String) -> Self { 82 | Self::Ident(ident) 83 | } 84 | } 85 | 86 | impl From<&'static str> for EID { 87 | fn from(ident: &'static str) -> Self { 88 | Self::InternedIdent(ident) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /datom/src/types/entity.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | use std::{collections::HashSet, hash::Hash}; 6 | 7 | use crate::{ 8 | builtin_idents, storage::Storage, AttributeIterator, Connection, Datom, DatomType, 9 | EntityResult, QueryError, Value, EID, ID, 10 | }; 11 | 12 | /// An entity in a database 13 | #[derive(Debug, Eq, Clone, Copy)] 14 | pub struct Entity<'connection, S: Storage> { 15 | pub(crate) connection: &'connection Connection, 16 | pub(crate) t: u64, 17 | pub(crate) id: ID, 18 | } 19 | 20 | impl<'connection, S: Storage> PartialEq for Entity<'connection, S> { 21 | fn eq(&self, other: &Self) -> bool { 22 | self.connection == other.connection && self.t == other.t && self.id == other.id 23 | } 24 | } 25 | 26 | impl<'connection, S: Storage> Entity<'connection, S> { 27 | /// Get the ID of this entity 28 | pub const fn id(&self) -> &ID { 29 | &self.id 30 | } 31 | 32 | /// Get the value of an attribute on this entity, with options 33 | pub fn get_with_options( 34 | &self, 35 | attribute: EID, 36 | skip_cardinality: bool, 37 | skip_type: bool, 38 | ) -> Result, QueryError> { 39 | let db = self.connection.as_of(self.t)?; 40 | let attribute = attribute.resolve(&db)?; 41 | if attribute == builtin_idents::ID { 42 | return Ok(Value::from(self.id).into()); 43 | } 44 | let attribute_ent = db.entity(attribute.into())?; 45 | let is_repeated = !skip_cardinality 46 | && attribute_ent 47 | .get_with_options(builtin_idents::CARDINALITY.into(), true, false)? 48 | .is_ref_to(&builtin_idents::CARDINALITY_MANY); 49 | let attribute_type = { 50 | if skip_type { 51 | None 52 | } else { 53 | let attribute_type = attribute_ent.get_with_options( 54 | builtin_idents::VALUE_TYPE.into(), 55 | true, 56 | true, 57 | )?; 58 | if let EntityResult::Value(Value::ID(t)) = attribute_type { 59 | Some(t) 60 | } else { 61 | None 62 | } 63 | } 64 | }; 65 | let result = if is_repeated { 66 | let datoms = db.datoms_for_entity_attribute(self.id, attribute)?; 67 | // The index is sorted in EAVT order, so for a given value 68 | // all additions and retractions will be in time-order. 69 | let mut values = HashSet::new(); 70 | for datom in datoms { 71 | if datom.datom_type == DatomType::Retraction { 72 | values.remove(&datom.value); 73 | } else { 74 | values.insert(datom.value); 75 | } 76 | } 77 | let res: Result>, QueryError> = values 78 | .into_iter() 79 | .map(|v| { 80 | if attribute_type == Some(builtin_idents::TYPE_REF) { 81 | if let Value::ID(id) = v { 82 | Ok(EntityResult::Ref(db.entity(id.into())?)) 83 | } else { 84 | Ok(EntityResult::Value(v)) 85 | } 86 | } else { 87 | Ok(EntityResult::from(v)) 88 | } 89 | }) 90 | .collect(); 91 | EntityResult::Repeated(res?) 92 | } else { 93 | db.datoms_for_entity_attribute(self.id, attribute)? 94 | .max_by(|a, b| a.t.cmp(&b.t)) 95 | .map(|x| -> Result, QueryError> { 96 | if x.datom_type == DatomType::Retraction { 97 | Ok(EntityResult::NotFound) 98 | } else if attribute_type == Some(builtin_idents::TYPE_REF) { 99 | if let Value::ID(id) = x.value { 100 | Ok(EntityResult::Ref(db.entity(id.into())?)) 101 | } else { 102 | Ok(EntityResult::Value(x.value)) 103 | } 104 | } else { 105 | Ok(EntityResult::Value(x.value)) 106 | } 107 | }) 108 | .unwrap_or(Ok(EntityResult::NotFound))? 109 | }; 110 | if result == EntityResult::NotFound { 111 | let builtin = builtin_idents::BUILTIN_ENTITIES.get(&self.id); 112 | builtin 113 | .and_then(|builtin| builtin.get(&attribute)) 114 | .map_or(Ok(EntityResult::NotFound), |val| { 115 | Ok(EntityResult::Value(val.to_owned())) 116 | }) 117 | } else { 118 | Ok(result) 119 | } 120 | } 121 | 122 | /// Get the value of an attribute on this entity 123 | pub fn get(&self, attribute: EID) -> Result, QueryError> { 124 | self.get_with_options(attribute, false, false) 125 | } 126 | 127 | /// Get the entities with this entity as a value on an attribute 128 | /// (reverse lookup) 129 | pub fn reverse_get(&self, attribute: EID) -> Result, QueryError> { 130 | let db = self.connection.as_of(self.t)?; 131 | let attribute = attribute.resolve(&db)?; 132 | let datoms = db.datoms_for_value_attribute(self.id().to_owned().into(), attribute)?; 133 | let datoms: Vec = datoms.collect(); 134 | // The index is sorted in AVET order, so for a given entity 135 | // all additions and retractions will be in time-order. 136 | let mut entities = HashSet::new(); 137 | for datom in datoms { 138 | if datom.datom_type == DatomType::Retraction { 139 | entities.remove(&datom.entity); 140 | } else { 141 | entities.insert(datom.entity); 142 | } 143 | } 144 | let res: Result>, QueryError> = entities 145 | .into_iter() 146 | .map(|id| Ok(EntityResult::Ref(db.entity(id.into())?))) 147 | .collect(); 148 | Ok(EntityResult::Repeated(res?)) 149 | } 150 | 151 | /// Get the attributes on this entity 152 | pub fn attributes(&self) -> Result, QueryError> { 153 | let iter = self.connection.as_of(self.t)?.datoms_for_entity(self.id)?; 154 | AttributeIterator::new(iter) 155 | } 156 | } 157 | 158 | impl<'connection, S: Storage> Hash for Entity<'connection, S> { 159 | fn hash(&self, state: &mut H) { 160 | self.t.hash(state); 161 | self.id.hash(state); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /datom/src/types/entity_result.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | use std::{ 6 | fmt::{self, Debug, Formatter}, 7 | hash::Hash, 8 | }; 9 | 10 | use crate::{storage::Storage, Entity, Value, ID}; 11 | 12 | #[derive(Clone)] 13 | /// The result of getting an attribute on an entity 14 | pub enum EntityResult<'connection, S: Storage> { 15 | /// A value for that attribute wasn't found on this entity 16 | NotFound, 17 | /// A value for that attribute was found on this entity 18 | Value(Value), 19 | /// A value for that attribute was found on this entity, and it 20 | /// refers to another entity. 21 | Ref(Entity<'connection, S>), 22 | /// Possibly multiple values for that attribute were found on this 23 | /// entity 24 | Repeated(Vec), 25 | } 26 | 27 | impl<'connection, S: Storage> Debug for EntityResult<'connection, S> { 28 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 29 | match self { 30 | Self::NotFound => write!(f, "NotFound")?, 31 | Self::Value(v) => { 32 | write!(f, "Value(")?; 33 | v.fmt(f)?; 34 | write!(f, ")")?; 35 | } 36 | Self::Ref(e) => { 37 | write!(f, "Ref(")?; 38 | e.id().fmt(f)?; 39 | write!(f, ")")?; 40 | } 41 | Self::Repeated(xs) => { 42 | write!(f, "Repeated(")?; 43 | for x in xs { 44 | x.fmt(f)?; 45 | write!(f, ", ")?; 46 | } 47 | write!(f, ")")?; 48 | } 49 | } 50 | Ok(()) 51 | } 52 | } 53 | 54 | impl<'connection, S: Storage> EntityResult<'connection, S> { 55 | /// Check if the result is a reference to a specific ID 56 | pub fn is_ref_to(&self, id: &ID) -> bool { 57 | if let Self::Ref(e) = self { 58 | e.id() == id 59 | } else { 60 | false 61 | } 62 | } 63 | } 64 | 65 | impl<'connection, S: Storage> PartialEq for EntityResult<'connection, S> { 66 | fn eq(&self, other: &Self) -> bool { 67 | match (self, other) { 68 | (Self::Value(l0), Self::Value(r0)) => l0 == r0, 69 | (Self::Ref(l0), Self::Ref(r0)) => l0 == r0, 70 | (Self::Repeated(l0), Self::Repeated(r0)) => l0 == r0, 71 | _ => core::mem::discriminant(self) == core::mem::discriminant(other), 72 | } 73 | } 74 | } 75 | 76 | impl<'connection, S: Storage> Eq for EntityResult<'connection, S> {} 77 | 78 | impl<'connection, S: Storage> Hash for EntityResult<'connection, S> { 79 | fn hash(&self, state: &mut H) { 80 | core::mem::discriminant(self).hash(state); 81 | } 82 | } 83 | 84 | impl<'connection, S: Storage> PartialEq for EntityResult<'connection, S> { 85 | fn eq(&self, other: &Value) -> bool { 86 | match self { 87 | Self::Value(s) => s == other, 88 | _ => false, 89 | } 90 | } 91 | } 92 | 93 | impl<'connection, S: Storage + PartialEq> PartialEq> 94 | for EntityResult<'connection, S> 95 | { 96 | fn eq(&self, other: &Entity<'connection, S>) -> bool { 97 | match self { 98 | Self::Ref(s) => s == other, 99 | _ => false, 100 | } 101 | } 102 | } 103 | 104 | impl<'connection, S: Storage> PartialEq for EntityResult<'connection, S> { 105 | fn eq(&self, other: &ID) -> bool { 106 | self.is_ref_to(other) 107 | } 108 | } 109 | 110 | impl<'connection, S: Storage + PartialEq> PartialEq> for EntityResult<'connection, S> { 111 | fn eq(&self, other: &Vec) -> bool { 112 | match self { 113 | Self::Repeated(s) => s == other, 114 | _ => false, 115 | } 116 | } 117 | } 118 | 119 | impl<'connection, S: Storage> From for EntityResult<'connection, S> { 120 | fn from(v: Value) -> Self { 121 | Self::Value(v) 122 | } 123 | } 124 | 125 | impl<'connection, S: Storage> From> for EntityResult<'connection, S> { 126 | fn from(e: Entity<'connection, S>) -> Self { 127 | Self::Ref(e) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /datom/src/types/fact.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | use std::str::FromStr; 6 | 7 | use datom_bigdecimal::BigDecimal; 8 | use edn_rs::Edn; 9 | use num_bigint::BigInt; 10 | 11 | use crate::{ 12 | storage::Storage, Database, Datom, DatomType, EntityResult, TransactionError, Value, EID, 13 | }; 14 | 15 | /** 16 | A fact which hasn't yet been converted to a [Datom] (or set of 17 | [Datom]s) 18 | */ 19 | #[derive(Clone, Debug)] 20 | pub enum Fact { 21 | /// Adding an attribute value to an entity 22 | Add( 23 | /// Entity ID 24 | EID, 25 | /// Attribute ID 26 | EID, 27 | /// Value 28 | Value, 29 | ), 30 | /// Retracting a specific attribute value from an entity 31 | RetractValue( 32 | /// Entity ID 33 | EID, 34 | /// Attribute ID 35 | EID, 36 | /// Value 37 | Value, 38 | ), 39 | /// Retracting an attribute from an entity, no matter its value 40 | Retract( 41 | /// Entity ID 42 | EID, 43 | /// Attribute ID 44 | EID, 45 | ), 46 | } 47 | 48 | impl Fact { 49 | /// Convert this [Fact] into a [Datom], given a [Database] 50 | pub fn datom( 51 | self, 52 | t: u64, 53 | db: &Database<'_, S>, 54 | ) -> Result { 55 | match self { 56 | Self::Add(entity, attribute, value) => Ok(Datom { 57 | entity: entity.resolve(db)?, 58 | attribute: attribute.resolve(db)?, 59 | value, 60 | t, 61 | datom_type: DatomType::Addition, 62 | }), 63 | Self::RetractValue(entity, attribute, value) => Ok(Datom { 64 | entity: entity.resolve(db)?, 65 | attribute: attribute.resolve(db)?, 66 | value, 67 | t, 68 | datom_type: DatomType::Retraction, 69 | }), 70 | Self::Retract(entity, attribute) => { 71 | let entity = entity.resolve(db)?; 72 | let attribute = attribute.resolve(db)?; 73 | let value = db.entity(entity.into())?.get(attribute.into())?; 74 | if let EntityResult::Value(value) = value { 75 | Ok(Datom { 76 | entity, 77 | attribute, 78 | value, 79 | t, 80 | datom_type: DatomType::Retraction, 81 | }) 82 | } else { 83 | Err(TransactionError::FailedToRetractRepeatedAttribute( 84 | entity, attribute, 85 | )) 86 | } 87 | } 88 | } 89 | } 90 | 91 | /// Create a fact from an EDN fact representation 92 | pub fn from_edn(edn: Edn) -> Result> { 93 | let Edn::Vector(parts) = edn else { 94 | todo!("error"); 95 | }; 96 | let vec = parts.to_vec(); 97 | if vec.len() != 3 { 98 | todo!("error"); 99 | } 100 | let mut it = vec.into_iter(); 101 | 102 | let entity_edn = it.next(); 103 | let attribute_edn = it.next(); 104 | let value_edn = it.next(); 105 | 106 | let Some(Edn::Key(entity_keyword)) = entity_edn else { 107 | todo!("error"); 108 | }; 109 | let Some(Edn::Key(attribute_keyword)) = attribute_edn else { 110 | todo!("error"); 111 | }; 112 | 113 | let entity = EID::Ident(entity_keyword); 114 | let attribute = EID::Ident(attribute_keyword); 115 | 116 | let value = match value_edn { 117 | Some(Edn::Str(s)) => Value::String(s), 118 | Some(Edn::Int(i)) => Value::Integer(BigInt::from(i)), 119 | Some(Edn::Double(d)) => Value::Decimal(BigDecimal::from_str(&d.to_string())?), 120 | Some(Edn::Bool(b)) => Value::Boolean(b), 121 | _ => todo!("error"), 122 | }; 123 | 124 | Ok(Self::Add(entity, attribute, value)) 125 | } 126 | 127 | /// Generate an EDN representation from a fact 128 | pub fn to_edn(&self) -> String { 129 | let Self::Add(entity_eid, attribute_eid, value) = self else { 130 | todo!("error"); 131 | }; 132 | let EID::Ident(entity) = entity_eid else { 133 | todo!("error"); 134 | }; 135 | let EID::Ident(attribute) = attribute_eid else { 136 | todo!("error"); 137 | }; 138 | Edn::Vector(edn_rs::Vector::new(vec![ 139 | Edn::Key(entity.to_string()), 140 | Edn::Key(attribute.to_string()), 141 | value.to_owned().into_edn(), 142 | ])) 143 | .to_string() 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /datom/src/types/id.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | use std::{ 6 | str::FromStr, 7 | time::{SystemTime, UNIX_EPOCH}, 8 | }; 9 | 10 | use uuid::Uuid; 11 | 12 | /** 13 | An entity ID 14 | 15 | These IDs are better known as Squuids - a [UUID](Uuid) v4 with the most 16 | significant 32 bits overwritten with seconds since epoch. Squuids 17 | allow for more efficient indexing of the IDs, since they will end up 18 | approximately monotonically increasing on a large scale. 19 | 20 | Since attributes are entities themselves, these are also attribute 21 | IDs. 22 | */ 23 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] 24 | pub struct ID(Uuid); 25 | 26 | impl ID { 27 | /// Generate a new [ID] from the current time. 28 | pub fn new() -> Self { 29 | let now = SystemTime::now(); 30 | let since_epoch = now 31 | .duration_since(UNIX_EPOCH) 32 | .expect("Must be run after 1970 (large ask, I know)") 33 | .as_secs() as u32; 34 | let since_epoch_be = since_epoch.to_be_bytes(); 35 | let mut uuid_bytes = Uuid::new_v4().as_bytes().to_owned(); 36 | uuid_bytes[..since_epoch_be.len()].clone_from_slice(&since_epoch_be[..]); 37 | Self(Uuid::from_bytes(uuid_bytes)) 38 | } 39 | 40 | /// Get the null [UUID](Uuid), which is all zeroes. 41 | pub const fn null() -> Self { 42 | Self(Uuid::nil()) 43 | } 44 | 45 | /// A const equivalent to [From::from] 46 | /// 47 | /// ``` 48 | /// use datom::ID; 49 | /// assert_eq!(ID::from_u128(0), ID::null()); 50 | /// ``` 51 | pub const fn from_u128(x: u128) -> Self { 52 | Self(Uuid::from_u128(x)) 53 | } 54 | } 55 | 56 | impl Default for ID { 57 | /// ``` 58 | /// datom::ID::default(); 59 | /// ``` 60 | fn default() -> Self { 61 | Self::new() 62 | } 63 | } 64 | 65 | impl FromStr for ID { 66 | type Err = Box; 67 | 68 | fn from_str(s: &str) -> Result { 69 | Ok(Self(Uuid::from_str(s)?)) 70 | } 71 | } 72 | 73 | impl ToString for ID { 74 | fn to_string(&self) -> String { 75 | self.0.to_string() 76 | } 77 | } 78 | 79 | impl From for ID { 80 | /// ``` 81 | /// use datom::ID; 82 | /// assert_eq!(ID::from_u128(0), ID::null()); 83 | /// ``` 84 | fn from(n: u128) -> Self { 85 | Self(Uuid::from_u128(n)) 86 | } 87 | } 88 | 89 | impl From for u128 { 90 | /// ``` 91 | /// use datom::ID; 92 | /// assert_eq!(0u128, ID::null().into()); 93 | /// ``` 94 | fn from(val: ID) -> Self { 95 | val.0.as_u128() 96 | } 97 | } 98 | 99 | impl From<[u8; 16]> for ID { 100 | fn from(bytes: [u8; 16]) -> Self { 101 | Self(Uuid::from_bytes(bytes)) 102 | } 103 | } 104 | 105 | impl From<&ID> for [u8; 16] { 106 | fn from(id: &ID) -> Self { 107 | id.0.as_bytes().to_owned() 108 | } 109 | } 110 | 111 | impl From for [u8; 16] { 112 | fn from(id: ID) -> Self { 113 | <[u8; 16]>::from(&id) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /datom/src/types/index.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | /** 6 | The four indices used in the underlying data store. The names refer 7 | to the serialization order, and by extension the sort order. 8 | 9 | These indices are the same as are used in Datomic, and are described 10 | in [Datomic's documentation] in great detail. 11 | 12 | [Datomic's documentation]: https://docs.datomic.com/on-prem/query/indexes.html 13 | */ 14 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 15 | #[repr(C)] 16 | pub enum Index { 17 | /** 18 | Entity-Attribute-Value-T index 19 | 20 | Provides information on an entity-by-entity basis. 21 | */ 22 | EAVT, 23 | /** 24 | Attribute-Entity-Value-T index 25 | 26 | Provides access to all instances of each attribute. 27 | */ 28 | AEVT, 29 | /** 30 | Attribute-Value-Entity-T index 31 | 32 | Provides efficient access to unique entities 33 | */ 34 | AVET, 35 | /** 36 | Value-Attribute-Entity-T index 37 | 38 | Also known as the reverse index. Provides relationship access in 39 | reverse. For example, if a page has its owner as an attribute 40 | but not vice versa, this index allows a query to walk backwards 41 | from owner to page. 42 | */ 43 | VAET, 44 | } 45 | 46 | impl Index { 47 | /// Map the [Index] to its byte representation 48 | pub const fn byte(&self) -> u8 { 49 | match self { 50 | Self::EAVT => 0, 51 | Self::AEVT => 1, 52 | Self::AVET => 2, 53 | Self::VAET => 3, 54 | } 55 | } 56 | 57 | /// Map byte representation to an [Index] 58 | pub const fn from_byte(b: u8) -> Self { 59 | match b { 60 | 0 => Self::EAVT, 61 | 1 => Self::AEVT, 62 | 2 => Self::AVET, 63 | 3 => Self::VAET, 64 | _ => panic!("invalid index"), 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /datom/src/types/mod.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | // For clarity's sake, this is in alphabetical order. 6 | 7 | mod attribute_iterator; 8 | pub use self::attribute_iterator::*; 9 | 10 | mod attribute_schema; 11 | pub use self::attribute_schema::*; 12 | 13 | mod connection_error; 14 | pub use self::connection_error::*; 15 | 16 | mod connection; 17 | pub use self::connection::*; 18 | 19 | mod database; 20 | pub use self::database::*; 21 | 22 | mod datom_iterator; 23 | pub use self::datom_iterator::*; 24 | 25 | mod datom_type; 26 | pub use self::datom_type::*; 27 | 28 | mod datom; 29 | pub use self::datom::*; 30 | 31 | mod eid; 32 | pub use self::eid::*; 33 | 34 | mod entity_result; 35 | pub use self::entity_result::*; 36 | 37 | mod entity; 38 | pub use self::entity::*; 39 | 40 | mod fact; 41 | pub use self::fact::*; 42 | 43 | mod id; 44 | pub use self::id::*; 45 | 46 | mod index; 47 | pub use self::index::*; 48 | 49 | mod query_error; 50 | pub use self::query_error::*; 51 | 52 | mod storage_error; 53 | pub use self::storage_error::*; 54 | 55 | mod transaction_error; 56 | pub use self::transaction_error::*; 57 | 58 | mod transaction_record; 59 | pub use self::transaction_record::*; 60 | 61 | mod transaction_result; 62 | pub use self::transaction_result::*; 63 | 64 | mod transaction; 65 | pub use self::transaction::*; 66 | 67 | mod value; 68 | pub use self::value::*; 69 | -------------------------------------------------------------------------------- /datom/src/types/query_error.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | #![allow(missing_docs)] 6 | 7 | use miette::Diagnostic; 8 | use thiserror::Error; 9 | 10 | use crate::{ConnectionError, StorageError, EID}; 11 | 12 | /// Errors during a [Database](crate::Database) query 13 | #[derive(Error, Debug, Diagnostic)] 14 | pub enum QueryError { 15 | #[error("the given EID `{0:?}` doesn't resolve to an entity")] 16 | #[diagnostic(code(datom::query::unresolved_eid), url(docsrs))] 17 | UnresolvedEID(EID), 18 | 19 | #[error("there was an error with the underlying connection")] 20 | #[diagnostic(code(datom::connection), url(docsrs))] 21 | ConnectionError(#[from] ConnectionError), 22 | } 23 | 24 | impl From for QueryError { 25 | fn from(se: StorageError) -> Self { 26 | Self::ConnectionError(se.into()) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /datom/src/types/storage_error.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | #![allow(missing_docs)] 6 | 7 | use std::{error, io}; 8 | 9 | use miette::Diagnostic; 10 | use thiserror::Error; 11 | 12 | /// An error in the underlying storage backend 13 | #[derive(Error, Debug, Diagnostic)] 14 | pub enum StorageError { 15 | #[error("an error occurred related to concurrency")] 16 | #[diagnostic(code(datom::storage::concurrency), url(docsrs))] 17 | ConcurrencyError, 18 | 19 | #[error("an I/O error occurred")] 20 | #[diagnostic(code(datom::storage::io), url(docsrs))] 21 | IOError(#[from] io::Error), 22 | 23 | #[error("an unknown error occurred")] 24 | #[diagnostic(code(datom::storage::misc), url(docsrs))] 25 | Miscellaneous(#[from] Box), 26 | } 27 | -------------------------------------------------------------------------------- /datom/src/types/transaction.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | use std::collections::HashMap; 6 | 7 | use edn_rs::Edn; 8 | 9 | use crate::{storage::Storage, Database, Datom, Fact, TransactionError, Value, EID}; 10 | 11 | /// A type which can be appended to a transaction 12 | pub trait Transactable { 13 | /// Get a transaction to add this object to the database 14 | fn tx(&self) -> Transaction; 15 | } 16 | 17 | /// A set of facts which can be transacted into a database connection 18 | #[derive(Clone, Debug)] 19 | pub struct Transaction { 20 | facts: Vec, 21 | } 22 | 23 | impl Transaction { 24 | /// Create a new empty [Transaction] 25 | pub const fn new() -> Self { 26 | Self { facts: vec![] } 27 | } 28 | 29 | /// Add a raw [Fact] 30 | pub fn push_fact(&mut self, fact: Fact) { 31 | self.facts.push(fact) 32 | } 33 | 34 | /// Add an attribute value to an entity 35 | pub fn add(&mut self, entity: EID, attribute: EID, value: Value) { 36 | self.push_fact(Fact::Add(entity, attribute, value)); 37 | } 38 | 39 | /// Add many attribute values to an entity 40 | pub fn add_many(&mut self, entity: EID, attr_value_pairs: HashMap) { 41 | for (attr, val) in attr_value_pairs { 42 | self.push_fact(Fact::Add(entity.clone(), attr, val)); 43 | } 44 | } 45 | 46 | /// Retract a specific attribute value from an entity 47 | pub fn retract_value(&mut self, entity: EID, attribute: EID, value: Value) { 48 | self.push_fact(Fact::RetractValue(entity, attribute, value)); 49 | } 50 | 51 | /// Retract an attribute from an entity, ignoring its value 52 | pub fn retract(&mut self, entity: EID, attribute: EID) { 53 | self.push_fact(Fact::Retract(entity, attribute)) 54 | } 55 | 56 | /// Append a transactable to this transaction 57 | pub fn append(&mut self, txable: T) { 58 | self.facts.append(&mut txable.tx().facts); 59 | } 60 | 61 | /// Convert the [Transaction] to a set of [Datom]s 62 | pub fn datoms<'c, S: Storage>( 63 | &self, 64 | t: u64, 65 | db: &Database<'c, S>, 66 | ) -> Result, TransactionError> { 67 | self.facts 68 | .iter() 69 | .map(|f| f.to_owned().datom(t, db)) 70 | .collect() 71 | } 72 | 73 | /// Create a transaction from an EDN list of facts 74 | pub fn from_edn(edn: Edn) -> Result> { 75 | let Edn::Vector(facts) = edn else { 76 | todo!("error"); 77 | }; 78 | let facts = facts 79 | .to_vec() 80 | .into_iter() 81 | .map(Fact::from_edn) 82 | .collect::, _>>()?; 83 | Ok(Self { facts }) 84 | } 85 | } 86 | 87 | impl Default for Transaction { 88 | fn default() -> Self { 89 | Self::new() 90 | } 91 | } 92 | 93 | impl Transactable for Transaction { 94 | fn tx(&self) -> Transaction { 95 | self.to_owned() 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /datom/src/types/transaction_error.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | #![allow(missing_docs)] 6 | 7 | use miette::Diagnostic; 8 | use thiserror::Error; 9 | 10 | use crate::{ConnectionError, QueryError, EID, ID}; 11 | 12 | /// Errors during a [Transaction](crate::Transaction) 13 | #[derive(Error, Debug, Diagnostic)] 14 | pub enum TransactionError { 15 | #[error( 16 | "the given attribute {1:?} doesn't have a value for entity {0:?}, and cannot be retracted" 17 | )] 18 | #[diagnostic(code(datom::transaction::retract_nonexistent_attribute), url(docsrs))] 19 | FailedToRetractNonexistentAttribute(ID, ID), 20 | 21 | #[error("the given attribute {1:?} is repeated, and cannot be retracted without specifying a specific value to retract")] 22 | #[diagnostic(code(datom::transaction::retract_repeated_attribute), url(docsrs))] 23 | FailedToRetractRepeatedAttribute(ID, ID), 24 | 25 | #[error("the given EID {0:?} doesn't resolve to an entity")] 26 | #[diagnostic(code(datom::transaction::unresolved_eid), url(docsrs))] 27 | UnresolvedEID(EID), 28 | 29 | #[error("a query executed during this transaction failed")] 30 | #[diagnostic(code(datom::query), url(docsrs))] 31 | QueryError(#[from] QueryError), 32 | 33 | #[error("there was an error with the underlying connection")] 34 | #[diagnostic(code(datom::connection), url(docsrs))] 35 | ConnectionError(#[from] ConnectionError), 36 | } 37 | -------------------------------------------------------------------------------- /datom/src/types/transaction_record.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | use chrono::{DateTime, Utc}; 6 | 7 | /// The record of a past transaction 8 | pub struct TransactionRecord { 9 | /// The t-value of this transaction 10 | pub t: u64, 11 | /// When this transaction was transacted 12 | pub timestamp: DateTime, 13 | } 14 | -------------------------------------------------------------------------------- /datom/src/types/transaction_result.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | use crate::{storage::Storage, Connection, Database, Datom}; 6 | 7 | /** 8 | The result of running a [Transaction](crate::Transaction) on a 9 | [Connection] 10 | */ 11 | pub struct TransactionResult<'connection, S: Storage> { 12 | /** 13 | The [Connection] the [Transaction](crate::Transaction) was run 14 | on 15 | */ 16 | pub connection: &'connection Connection, 17 | /// The [Database] before the transaction 18 | pub before: Database<'connection, S>, 19 | /// The [Database] after the transaction 20 | pub after: Database<'connection, S>, 21 | /// The [Datom]s added to the database in the transaction 22 | pub data: Vec, 23 | } 24 | -------------------------------------------------------------------------------- /datom/tests/base_entity_api.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | mod common; 6 | 7 | use common::{ 8 | data::{db_users_transacted_properly, transact_users, users_transacted_properly}, 9 | schema::{schema_transacted_properly, with_connection}, 10 | }; 11 | use datom::{EntityResult, Transaction, TransactionError, EID}; 12 | use miette::Result; 13 | 14 | #[test] 15 | fn schema_only() -> Result<()> { 16 | with_connection(|conn| schema_transacted_properly(&conn)) 17 | } 18 | 19 | #[test] 20 | fn users() -> Result<()> { 21 | with_connection(|conn| { 22 | schema_transacted_properly(&conn)?; 23 | transact_users(&conn)?; 24 | users_transacted_properly(&conn)?; 25 | Ok(()) 26 | }) 27 | } 28 | 29 | #[test] 30 | fn retract_repeated_value() -> Result<()> { 31 | with_connection(|conn| { 32 | schema_transacted_properly(&conn)?; 33 | transact_users(&conn)?; 34 | users_transacted_properly(&conn)?; 35 | 36 | let mut tx = Transaction::new(); 37 | tx.retract_value( 38 | EID::unique("user/username".into(), "pmc".into()), 39 | "user/repeated-numbers".into(), 40 | 5678i32.into(), 41 | ); 42 | conn.transact(tx)?; 43 | 44 | let db = conn.db()?; 45 | let user = db.entity(EID::unique("user/username".into(), "pmc".into()))?; 46 | assert_eq!( 47 | user.get("user/repeated-numbers".into())?, 48 | EntityResult::Repeated(vec![EntityResult::Value(1234i32.into())]) 49 | ); 50 | 51 | let mut tx = Transaction::new(); 52 | tx.retract( 53 | EID::unique("user/username".into(), "pmc".into()), 54 | "user/repeated-numbers".into(), 55 | ); 56 | if let Err(TransactionError::FailedToRetractRepeatedAttribute(_, _)) = conn.transact(tx) { 57 | // Good, we should get this error! 58 | } else { 59 | panic!(); 60 | } 61 | 62 | Ok(()) 63 | }) 64 | } 65 | 66 | #[test] 67 | fn database_is_persistent() -> Result<()> { 68 | with_connection(|conn| { 69 | schema_transacted_properly(&conn)?; 70 | transact_users(&conn)?; 71 | users_transacted_properly(&conn)?; 72 | 73 | let before = conn.db()?; 74 | db_users_transacted_properly(&before)?; 75 | 76 | let user = before.entity(EID::unique("user/username".into(), "pmc".into()))?; 77 | assert_eq!( 78 | user.get("user/admin?".into())?, 79 | EntityResult::Value(true.into()) 80 | ); 81 | 82 | let mut tx = Transaction::new(); 83 | tx.add( 84 | EID::unique("user/username".into(), "pmc".into()), 85 | "user/admin?".into(), 86 | false.into(), 87 | ); 88 | conn.transact(tx)?; 89 | 90 | let after = conn.db()?; 91 | db_users_transacted_properly(&before)?; 92 | 93 | let user = before.entity(EID::unique("user/username".into(), "pmc".into()))?; 94 | assert_eq!( 95 | user.get("user/admin?".into())?, 96 | EntityResult::Value(true.into()) 97 | ); 98 | 99 | let user = after.entity(EID::unique("user/username".into(), "pmc".into()))?; 100 | assert_eq!( 101 | user.get("user/admin?".into())?, 102 | EntityResult::Value(false.into()) 103 | ); 104 | 105 | Ok(()) 106 | }) 107 | } 108 | -------------------------------------------------------------------------------- /datom/tests/common/data.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | use std::collections::HashSet; 6 | 7 | use datom::{ 8 | builtin_idents, storage::Storage, Connection, Database, DynamicConnection, EntityResult, 9 | Transactable, Transaction, Value, ID, 10 | }; 11 | use miette::Result; 12 | use num_bigint::BigInt; 13 | use once_cell::sync::Lazy; 14 | 15 | struct User { 16 | id: ID, 17 | username: Option, 18 | admin: Option, 19 | stripe_customer: Option, 20 | friends: Vec, 21 | repeated_numbers: Vec, 22 | } 23 | 24 | impl Transactable for &User { 25 | fn tx(&self) -> Transaction { 26 | let mut tx = Transaction::new(); 27 | let id = self.id; 28 | if let Some(username) = &self.username { 29 | tx.add(id.into(), "user/username".into(), username.as_str().into()); 30 | } 31 | if let Some(admin) = self.admin { 32 | tx.add(id.into(), "user/admin?".into(), admin.into()); 33 | } 34 | if let Some(customer) = self.stripe_customer { 35 | tx.add(id.into(), "user/stripe-customer".into(), customer.into()); 36 | } 37 | for friend in self.friends.iter() { 38 | tx.add(id.into(), "user/friends".into(), friend.to_owned()); 39 | } 40 | for number in self.repeated_numbers.iter() { 41 | tx.add(id.into(), "user/repeated-numbers".into(), number.into()); 42 | } 43 | tx 44 | } 45 | } 46 | 47 | static USERS: Lazy> = Lazy::new(|| { 48 | [User { 49 | id: ID::new(), 50 | username: Some("pmc".into()), 51 | admin: Some(true), 52 | stripe_customer: None, 53 | friends: vec![], 54 | repeated_numbers: vec![1234.into(), 5678.into()], 55 | }] 56 | .into() 57 | }); 58 | 59 | pub fn transact_users(conn: &DynamicConnection) -> Result<()> { 60 | let mut tx = Transaction::new(); 61 | for user in USERS.iter() { 62 | tx.append(user); 63 | } 64 | conn.transact(tx)?; 65 | Ok(()) 66 | } 67 | 68 | pub fn db_users_transacted_properly(db: &Database<'_, S>) -> Result<()> { 69 | for user in USERS.iter() { 70 | let user_ent = db.entity(user.id.into())?; 71 | assert_eq!(user_ent.id(), &user.id); 72 | assert_eq!( 73 | user_ent.get(builtin_idents::ID.into())?, 74 | EntityResult::Value(user.id.into()) 75 | ); 76 | if let Some(username) = &user.username { 77 | assert_eq!( 78 | user_ent.get("user/username".into())?, 79 | EntityResult::Value(username.as_str().into()) 80 | ); 81 | } 82 | if let Some(admin) = user.admin { 83 | assert_eq!( 84 | user_ent.get("user/admin?".into())?, 85 | EntityResult::Value(admin.into()) 86 | ); 87 | } 88 | if let Some(stripe_customer) = user.stripe_customer { 89 | assert!(user_ent 90 | .get("user/stripe-customer".into())? 91 | .is_ref_to(&stripe_customer)); 92 | } 93 | let mut friends = HashSet::new(); 94 | if let EntityResult::Repeated(results) = user_ent.get("user/friends".into())? { 95 | for res in results.into_iter() { 96 | friends.insert(res); 97 | } 98 | } else { 99 | panic!(); 100 | } 101 | for friend in user.friends.iter() { 102 | if let Value::ID(id) = friend { 103 | assert!(friends.contains(&EntityResult::Ref(db.entity(id.to_owned().into())?))); 104 | } else { 105 | assert!(friends.contains(&EntityResult::Value(friend.to_owned()))); 106 | } 107 | } 108 | let mut numbers = HashSet::new(); 109 | if let EntityResult::Repeated(results) = user_ent.get("user/repeated-numbers".into())? { 110 | for res in results.into_iter() { 111 | numbers.insert(res); 112 | } 113 | } else { 114 | panic!(); 115 | } 116 | for number in user.repeated_numbers.iter() { 117 | assert!(numbers.contains(&EntityResult::Value(number.into()))); 118 | } 119 | } 120 | Ok(()) 121 | } 122 | 123 | pub fn users_transacted_properly(conn: &Connection) -> Result<()> { 124 | let db = conn.db()?; 125 | db_users_transacted_properly(&db) 126 | } 127 | -------------------------------------------------------------------------------- /datom/tests/common/mod.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | #[allow(dead_code)] 6 | pub mod data; 7 | #[allow(dead_code)] 8 | pub mod schema; 9 | -------------------------------------------------------------------------------- /datom/tests/common/schema.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Lutris, Inc 2 | // SPDX-License-Identifier: BlueOak-1.0.0 OR BSD-2-Clause-Patent 3 | // SPDX-FileContributor: Piper McCorkle 4 | 5 | #[cfg(feature = "redblacktreeset")] 6 | use datom::backends::RedBlackTreeSetStorage; 7 | #[cfg(feature = "sled")] 8 | use datom::backends::SledStorage; 9 | use datom::{ 10 | backends::TieredStorage, builtin_idents, new_dynamic_connection, AttributeSchema, 11 | AttributeType, DynamicConnection, EntityResult, Transaction, 12 | }; 13 | use miette::Result; 14 | use once_cell::sync::Lazy; 15 | 16 | static ATTRIBUTES: Lazy> = Lazy::new(|| { 17 | [ 18 | AttributeSchema::new() 19 | .ident("user/username".into()) 20 | .value_type(AttributeType::String) 21 | .doc("The user's unique username".into()) 22 | .unique(), 23 | AttributeSchema::new() 24 | .ident("user/admin?".into()) 25 | .value_type(AttributeType::Boolean), 26 | AttributeSchema::new() 27 | .ident("user/stripe-customer".into()) 28 | .value_type(AttributeType::Ref) 29 | .component() 30 | .unique(), 31 | AttributeSchema::new() 32 | .ident("user/friends".into()) 33 | .value_type(AttributeType::Ref) 34 | .many(), 35 | AttributeSchema::new() 36 | .ident("user/repeated-numbers".into()) 37 | .value_type(AttributeType::Integer) 38 | .many(), 39 | ] 40 | .into() 41 | }); 42 | 43 | pub fn transact_schema(conn: &DynamicConnection) -> Result<()> { 44 | let mut tx = Transaction::new(); 45 | for attr in ATTRIBUTES.iter() { 46 | tx.append(attr.to_owned()); 47 | } 48 | conn.transact(tx)?; 49 | Ok(()) 50 | } 51 | 52 | #[cfg(feature = "sled")] 53 | pub fn sled_connection_with_schema() -> Result { 54 | use miette::IntoDiagnostic; 55 | 56 | let storage = SledStorage::connect_temp().into_diagnostic()?; 57 | let conn = new_dynamic_connection(storage); 58 | transact_schema(&conn)?; 59 | Ok(conn) 60 | } 61 | 62 | #[cfg(feature = "redblacktreeset")] 63 | pub fn redblacktreeset_connection_with_schema() -> Result { 64 | let storage = RedBlackTreeSetStorage::new(); 65 | let conn = new_dynamic_connection(storage); 66 | transact_schema(&conn)?; 67 | Ok(conn) 68 | } 69 | 70 | #[cfg(all(feature = "sled", feature = "redblacktreeset"))] 71 | pub fn tiered_connection_with_schema() -> Result { 72 | use miette::IntoDiagnostic; 73 | 74 | let a = SledStorage::connect_temp().into_diagnostic()?; 75 | let b = RedBlackTreeSetStorage::new(); 76 | let storage = TieredStorage::new(a, b); 77 | let conn = new_dynamic_connection(storage); 78 | transact_schema(&conn)?; 79 | Ok(conn) 80 | } 81 | 82 | pub fn with_connection Result<()>>(f: F) -> Result<()> { 83 | #[cfg(feature = "sled")] 84 | f(sled_connection_with_schema()?)?; 85 | #[cfg(feature = "redblacktreeset")] 86 | f(redblacktreeset_connection_with_schema()?)?; 87 | #[cfg(all(feature = "sled", feature = "redblacktreeset"))] 88 | f(tiered_connection_with_schema()?)?; 89 | Ok(()) 90 | } 91 | 92 | pub fn schema_transacted_properly(conn: &DynamicConnection) -> Result<()> { 93 | let db = conn.db()?; 94 | for attr in ATTRIBUTES.iter() { 95 | let attr_ent = db.entity(attr.id.into())?; 96 | assert_eq!(attr_ent.id(), &attr.id); 97 | assert_eq!( 98 | attr_ent.get(builtin_idents::ID.into())?, 99 | EntityResult::Value(attr.id.into()) 100 | ); 101 | if let Some(ident) = &attr.ident { 102 | assert_eq!( 103 | attr_ent.get(builtin_idents::IDENT.into())?, 104 | EntityResult::Value(ident.as_str().into()) 105 | ); 106 | } 107 | if attr.many { 108 | assert!(attr_ent 109 | .get(builtin_idents::CARDINALITY.into())? 110 | .is_ref_to(&builtin_idents::CARDINALITY_MANY)); 111 | } 112 | if let Some(t) = attr.value_type { 113 | assert!(attr_ent 114 | .get(builtin_idents::VALUE_TYPE.into())? 115 | .is_ref_to(&t.into())); 116 | } 117 | if let Some(doc) = &attr.doc { 118 | assert_eq!( 119 | attr_ent.get(builtin_idents::DOC.into())?, 120 | EntityResult::Value(doc.as_str().into()) 121 | ); 122 | } 123 | if attr.unique { 124 | assert_eq!( 125 | attr_ent.get(builtin_idents::UNIQUE.into())?, 126 | EntityResult::Value(true.into()) 127 | ); 128 | } 129 | if attr.component { 130 | assert_eq!( 131 | attr_ent.get(builtin_idents::IS_COMPONENT.into())?, 132 | EntityResult::Value(true.into()) 133 | ); 134 | } 135 | } 136 | Ok(()) 137 | } 138 | --------------------------------------------------------------------------------