Merge branch 'oxigraph:main' into yamdan-canonical-escaping

pull/476/head
Dan Yamamoto 2 years ago committed by GitHub
commit 6caef63a2e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 68
      .cargo/config.toml
  2. 9
      .clusterfuzzlite/build.sh
  3. 5
      .github/DEPENDABOT.yml
  4. 4
      .github/workflows/artifacts.yml
  5. 30
      .github/workflows/tests.yml
  6. 3
      .mailmap
  7. 9
      CHANGELOG.md
  8. 113
      Cargo.lock
  9. 1
      Cargo.toml
  10. 10
      bench/bsbm_blazegraph.sh
  11. 11
      bench/bsbm_graphdb.sh
  12. 14
      bench/bsbm_jena.sh
  13. 10
      bench/bsbm_oxigraph.sh
  14. 12
      bench/bsbm_rdf4j.sh
  15. 10
      bench/bsbm_virtuoso.sh
  16. 4
      js/Cargo.toml
  17. 4
      js/package.json
  18. 21
      js/rome.json
  19. 2
      js/src/model.rs
  20. 8
      js/src/store.rs
  21. 2
      js/test/model.mjs
  22. 2
      js/test/store.mjs
  23. 12
      lib/Cargo.toml
  24. 4
      lib/oxrdf/Cargo.toml
  25. 4
      lib/oxrdf/src/dataset.rs
  26. 9
      lib/oxrdf/src/interning.rs
  27. 1
      lib/oxrdf/src/lib.rs
  28. 5
      lib/oxsdatatypes/Cargo.toml
  29. 4
      lib/oxsdatatypes/src/boolean.rs
  30. 41
      lib/oxsdatatypes/src/date_time.rs
  31. 282
      lib/oxsdatatypes/src/decimal.rs
  32. 39
      lib/oxsdatatypes/src/double.rs
  33. 36
      lib/oxsdatatypes/src/duration.rs
  34. 45
      lib/oxsdatatypes/src/float.rs
  35. 59
      lib/oxsdatatypes/src/integer.rs
  36. 2
      lib/oxsdatatypes/src/lib.rs
  37. 880
      lib/oxsdatatypes/src/parser.rs
  38. 4
      lib/sparesults/Cargo.toml
  39. 14
      lib/sparesults/src/csv.rs
  40. 4
      lib/sparesults/src/lib.rs
  41. 3
      lib/sparesults/src/solution.rs
  42. 26
      lib/sparesults/src/xml.rs
  43. 4
      lib/spargebra/Cargo.toml
  44. 1
      lib/spargebra/src/lib.rs
  45. 321
      lib/spargebra/src/parser.rs
  46. 5
      lib/spargebra/src/term.rs
  47. 4
      lib/sparql-smith/Cargo.toml
  48. 45
      lib/sparql-smith/src/lib.rs
  49. 12
      lib/src/io/error.rs
  50. 2
      lib/src/lib.rs
  51. 2
      lib/src/sparql/dataset.rs
  52. 697
      lib/src/sparql/eval.rs
  53. 7
      lib/src/sparql/model.rs
  54. 64
      lib/src/sparql/plan.rs
  55. 294
      lib/src/sparql/plan_builder.rs
  56. 17
      lib/src/sparql/update.rs
  57. 27
      lib/src/storage/backend/fallback.rs
  58. 62
      lib/src/storage/backend/rocksdb.rs
  59. 5
      lib/src/storage/mod.rs
  60. 4
      lib/src/store.rs
  61. 2
      lib/tests/store.rs
  62. 2
      oxrocksdb-sys/Cargo.toml
  63. 7
      oxrocksdb-sys/build.rs
  64. 2
      oxrocksdb-sys/rocksdb
  65. 6
      python/Cargo.toml
  66. 2
      python/pyproject.toml
  67. 2
      python/requirements.dev.txt
  68. 37
      python/src/io.rs
  69. 6
      python/src/lib.rs
  70. 21
      python/src/model.rs
  71. 4
      python/src/sparql.rs
  72. 51
      python/src/store.rs
  73. 29
      python/tests/test_io.py
  74. 6
      server/Cargo.toml
  75. 19
      server/README.md
  76. 76
      server/src/main.rs
  77. 3
      testsuite/oxigraph-tests/sparql/halloween_problem.ru
  78. 6
      testsuite/oxigraph-tests/sparql/halloween_problem_result.ttl
  79. 1
      testsuite/oxigraph-tests/sparql/in_empty_error.rq
  80. 3
      testsuite/oxigraph-tests/sparql/in_empty_error.srx
  81. 29
      testsuite/oxigraph-tests/sparql/manifest.ttl
  82. 4
      testsuite/oxigraph-tests/sparql/values_property_path_all.rq
  83. 1
      testsuite/oxigraph-tests/sparql/values_too_few.rq
  84. 1
      testsuite/oxigraph-tests/sparql/values_too_many.rq
  85. 2
      testsuite/rdf-tests
  86. 2
      testsuite/src/manifest.rs
  87. 26
      testsuite/src/sparql_evaluator.rs

@ -2,94 +2,124 @@
rustflags = [ rustflags = [
"-Wtrivial-casts", "-Wtrivial-casts",
"-Wtrivial-numeric-casts", "-Wtrivial-numeric-casts",
"-Wunsafe_code", "-Wunsafe-code",
"-Wunused-lifetimes", "-Wunused-lifetimes",
"-Wunused-qualifications", "-Wunused-qualifications",
# TODO: 1.63+ "-Wclippy::as-underscore",
# TODO: 1.65+ ""-Wclippy::bool-to-int-with-if",
"-Wclippy::borrow-as-ptr",
"-Wclippy::case-sensitive-file-extension-comparisons",
"-Wclippy::cast-lossless", "-Wclippy::cast-lossless",
"-Wclippy::cast-possible-truncation", "-Wclippy::cast-possible-truncation",
"-Wclippy::cast-possible-wrap", "-Wclippy::cast-possible-wrap",
"-Wclippy::cast-precision-loss", "-Wclippy::cast-precision-loss",
"-Wclippy::cast-ptr-alignment",
"-Wclippy::cast-sign-loss", "-Wclippy::cast-sign-loss",
"-Wclippy::checked-conversions", "-Wclippy::checked-conversions",
"-Wclippy::clone-on-ref-ptr",
"-Wclippy::cloned-instead-of-copied", "-Wclippy::cloned-instead-of-copied",
"-Wclippy::copy-iterator", "-Wclippy::copy-iterator",
"-Wclippy::dbg-macro", "-Wclippy::dbg-macro",
"-Wclippy::debug-assert-with-mut-call",
"-Wclippy::decimal-literal-representation", "-Wclippy::decimal-literal-representation",
"-Wclippy::empty-line-after-outer-attr", "-Wclippy::default-trait-access",
"-Wclippy::default-union-representation",
# TODO: 1.61+ "-Wclippy::deref-by-slicing",
# TODO: 1.63+ "-Wclippy::doc-link-with-quotes",
# TODO: 1.62+ "-Wclippy::empty-drop",
"-Wclippy::empty-enum", "-Wclippy::empty-enum",
# TODO: on major version "-Wclippy::empty-structs-with-brackets",
"-Wclippy::enum-glob-use", "-Wclippy::enum-glob-use",
"-Wclippy::exit",
"-Wclippy::expect-used", "-Wclippy::expect-used",
"-Wclippy::expl-impl-clone-on-copy", "-Wclippy::expl-impl-clone-on-copy",
"-Wclippy::explicit-deref-methods", "-Wclippy::explicit-deref-methods",
"-Wclippy::explicit-into-iter-loop", "-Wclippy::explicit-into-iter-loop",
"-Wclippy::explicit-iter-loop", "-Wclippy::explicit-iter-loop",
"-Wclippy::fallible-impl-from",
"-Wclippy::filter-map-next", "-Wclippy::filter-map-next",
"-Wclippy::flat-map-option", "-Wclippy::flat-map-option",
"-Wclippy::fn-to-numeric-cast-any",
# TODO: 1.62+ "-Wclippy::format-push-string",
"-Wclippy::from-iter-instead-of-collect", "-Wclippy::from-iter-instead-of-collect",
"-Wclippy::get-unwrap", "-Wclippy::get-unwrap",
"-Wclippy::if-not-else", "-Wclippy::if-not-else",
"-Wclippy::if-then-some-else-none",
"-Wclippy::implicit-clone", "-Wclippy::implicit-clone",
"-Wclippy::implicit-saturating-sub",
"-Wclippy::imprecise-flops",
"-Wclippy::inconsistent-struct-constructor", "-Wclippy::inconsistent-struct-constructor",
"-Wclippy::index-refutable-slice",
"-Wclippy::inefficient-to-string", "-Wclippy::inefficient-to-string",
"-Wclippy::inline-always", "-Wclippy::inline-always",
"-Wclippy::inline-asm-x86-att-syntax",
"-Wclippy::inline-asm-x86-intel-syntax",
"-Wclippy::invalid-upcast-comparisons", "-Wclippy::invalid-upcast-comparisons",
"-Wclippy::items-after-statements", "-Wclippy::items-after-statements",
"-Wclippy::large-digit-groups", "-Wclippy::large-digit-groups",
# TODO: 1.68+ "-Wclippy::large-futures",
"-Wclippy::large-stack-arrays", "-Wclippy::large-stack-arrays",
"-Wclippy::large-types-passed-by-value", "-Wclippy::large-types-passed-by-value",
"-Wclippy::let-underscore-must-use", "-Wclippy::let-underscore-must-use",
"-Wclippy::let-unit-value", "-Wclippy::let-unit-value",
"-Wclippy::linkedlist", "-Wclippy::linkedlist",
"-Wclippy::lossy-float-literal",
"-Wclippy::macro-use-imports", "-Wclippy::macro-use-imports",
"-Wclippy::manual-assert",
# TODO: 1.65+ "-Wclippy::manual-instant-elapsed",
# TODO: 1.67+ "-Wclippy::manual-let-else",
"-Wclippy::manual-ok-or", "-Wclippy::manual-ok-or",
"-Wclippy::map-flatten", # TODO: 1.65+ "-Wclippy::manual-string-new",
"-Wclippy::many-single-char-names",
"-Wclippy::map-unwrap-or", "-Wclippy::map-unwrap-or",
"-Wclippy::match-bool", "-Wclippy::match-bool",
"-Wclippy::match-same-arms", "-Wclippy::match-same-arms",
"-Wclippy::match-wildcard-for-single-variants", "-Wclippy::match-wildcard-for-single-variants",
"-Wclippy::maybe-infinite-iter", "-Wclippy::maybe-infinite-iter",
"-Wclippy::mem-forget", "-Wclippy::mem-forget",
# TODO: 1.63+ "-Wclippy::mismatching-type-param-order",
"-Wclippy::multiple-inherent-impl", "-Wclippy::multiple-inherent-impl",
"-Wclippy::mut-mut", "-Wclippy::mut-mut",
"-Wclippy::mutex-integer", "-Wclippy::mutex-atomic",
"-Wclippy::naive-bytecount", "-Wclippy::naive-bytecount",
"-Wclippy::needless-bitwise-bool", "-Wclippy::needless-bitwise-bool",
"-Wclippy::needless-continue", "-Wclippy::needless-continue",
"-Wclippy::needless-pass-by-value", "-Wclippy::needless-pass-by-value",
"-Wclippy::no-effect-underscore-binding",
# TODO: 1.69+ "-Wclippy::no-mangle-with-rust-abi",
"-Wclippy::non-ascii-literal", "-Wclippy::non-ascii-literal",
"-Wclippy::nonstandard-macro-braces",
"-Wclippy::path-buf-push-overwrite",
"-Wclippy::print-stderr", "-Wclippy::print-stderr",
"-Wclippy::print-stdout", "-Wclippy::print-stdout",
"-Wclippy::ptr-as-ptr",
"-Wclippy::range-minus-one", "-Wclippy::range-minus-one",
"-Wclippy::range-plus-one", "-Wclippy::range-plus-one",
"-Wclippy::rc-buffer",
"-Wclippy::rc-mutex", "-Wclippy::rc-mutex",
"-Wclippy::enum-variant-names", "-Wclippy::redundant-closure-for-method-calls",
"-Wclippy::redundant-else", "-Wclippy::redundant-else",
"-Wclippy::redundant-pub-crate", "-Wclippy::redundant-feature-names",
"-Wclippy::ref-binding-to-reference", "-Wclippy::ref-binding-to-reference",
"-Wclippy::ref-option-ref", "-Wclippy::ref-option-ref",
"-Wclippy::rest-pat-in-fully-bound-structs", "-Wclippy::rest-pat-in-fully-bound-structs",
"-Wclippy::return-self-not-must-use",
"-Wclippy::same-functions-in-if-condition", "-Wclippy::same-functions-in-if-condition",
# TODO: strange failure on 1.60 "-Wclippy::same-name-method",
# TODO: 1.68+ "-Wclippy::semicolon-outside-block",
"-Wclippy::single-match-else",
"-Wclippy::stable-sort-primitive",
"-Wclippy::str-to-string", "-Wclippy::str-to-string",
"-Wclippy::string-add", "-Wclippy::string-add",
"-Wclippy::string-add-assign", "-Wclippy::string-add-assign",
"-Wclippy::string-lit-as-bytes", "-Wclippy::string-lit-as-bytes",
"-Wclippy::string-to-string", "-Wclippy::string-to-string",
"-Wclippy::suboptimal-flops", # TODO: 1.67+ "-Wclippy::suspicious-xor-used-as-pow",
"-Wclippy::suspicious-operation-groupings",
"-Wclippy::todo", "-Wclippy::todo",
"-Wclippy::trait-duplication-in-bounds",
"-Wclippy::transmute-ptr-to-ptr", "-Wclippy::transmute-ptr-to-ptr",
"-Wclippy::trivial-regex",
"-Wclippy::trivially-copy-pass-by-ref", "-Wclippy::trivially-copy-pass-by-ref",
"-Wclippy::type-repetition-in-bounds", "-Wclippy::try-err",
"-Wclippy::unicode-not-nfc", "-Wclippy::unicode-not-nfc",
"-Wclippy::unimplemented", "-Wclippy::unimplemented",
# TODO: 1.66+ "-Wclippy::uninlined-format-args",
# TODO: 1.70+ "-Wclippy::unnecessary-box-returns",
# TODO: 1.61+ "-Wclippy::unnecessary-join",
# TODO: 1.67+ "-Wclippy::unnecessary-safety-comment",
# TODO: 1.67+ "-Wclippy::unnecessary-safety-doc",
"-Wclippy::unnecessary-self-imports", "-Wclippy::unnecessary-self-imports",
"-Wclippy::unnecessary-wraps", "-Wclippy::unnecessary-wraps",
"-Wclippy::unneeded-field-pattern", "-Wclippy::unneeded-field-pattern",
@ -99,13 +129,9 @@ rustflags = [
"-Wclippy::unused-async", "-Wclippy::unused-async",
"-Wclippy::unused-self", "-Wclippy::unused-self",
"-Wclippy::use-debug", "-Wclippy::use-debug",
"-Wclippy::use-self",
"-Wclippy::used-underscore-binding", "-Wclippy::used-underscore-binding",
"-Wclippy::useless-let-if-seq",
"-Wclippy::useless-transmute",
"-Wclippy::verbose-bit-mask", "-Wclippy::verbose-bit-mask",
"-Wclippy::verbose-file-reads", "-Wclippy::verbose-file-reads",
"-Wclippy::wildcard-dependencies", "-Wclippy::wildcard-dependencies",
"-Wclippy::zero-sized-map-values", "-Wclippy::zero-sized-map-values",
"-Wclippy::wrong-self-convention",
] ]

@ -5,7 +5,7 @@ function build_seed_corpus() {
mkdir "/tmp/oxigraph_$1" mkdir "/tmp/oxigraph_$1"
for file in **/*."$2" for file in **/*."$2"
do do
hash=($(sha256sum "$file")) hash=$(sha256sum "$file" | awk '{print $1;}')
cp "$file" "/tmp/oxigraph_$1/$hash" cp "$file" "/tmp/oxigraph_$1/$hash"
done done
zip "$1_seed_corpus.zip" /tmp/"oxigraph_$1"/* zip "$1_seed_corpus.zip" /tmp/"oxigraph_$1"/*
@ -15,9 +15,10 @@ function build_seed_corpus() {
cd "$SRC"/oxigraph cd "$SRC"/oxigraph
cargo fuzz build -O --debug-assertions cargo fuzz build -O --debug-assertions
for TARGET in sparql_eval # sparql_results_json sparql_results_tsv for TARGET in sparql_eval sparql_results_json sparql_results_tsv sparql_results_xml
do do
cp fuzz/target/x86_64-unknown-linux-gnu/release/$TARGET "$OUT"/ cp fuzz/target/x86_64-unknown-linux-gnu/release/$TARGET "$OUT"/
done done
# build_seed_corpus sparql_results_json json build_seed_corpus sparql_results_json srj
# build_seed_corpus sparql_results_tsv tsv build_seed_corpus sparql_results_tsv tsv
build_seed_corpus sparql_results_xml srx

@ -9,3 +9,8 @@ updates:
versioning-strategy: increase-if-necessary versioning-strategy: increase-if-necessary
schedule: schedule:
interval: weekly interval: weekly
- package-ecosystem: "npm"
directory: "/js/"
versioning-strategy: increase-if-necessary
schedule:
interval: weekly

@ -21,8 +21,8 @@ jobs:
submodules: true submodules: true
- run: rustup update && rustup target add aarch64-unknown-linux-gnu - run: rustup update && rustup target add aarch64-unknown-linux-gnu
- run: | - run: |
sudo apt install -y g++-aarch64-linux-gnu sudo apt update && sudo apt install -y g++-aarch64-linux-gnu
echo -e "[target.aarch64-unknown-linux-gnu]\nlinker = \"aarch64-linux-gnu-gcc\"" >> .cargo/config.toml echo -e "\n\n[target.aarch64-unknown-linux-gnu]\nlinker = \"aarch64-linux-gnu-gcc\"" >> .cargo/config.toml
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
- run: cargo build --release - run: cargo build --release
working-directory: ./server working-directory: ./server

@ -281,7 +281,7 @@ jobs:
python-version: "3.10" python-version: "3.10"
cache: pip cache: pip
cache-dependency-path: '**/requirements.dev.txt' cache-dependency-path: '**/requirements.dev.txt'
- run: pip install "maturin~=0.14.0" - run: pip install "maturin~=0.15.0"
- run: maturin build -m python/Cargo.toml - run: maturin build -m python/Cargo.toml
- run: pip install --no-index --find-links=target/wheels/ pyoxigraph - run: pip install --no-index --find-links=target/wheels/ pyoxigraph
- run: rm -r target/wheels - run: rm -r target/wheels
@ -347,29 +347,18 @@ jobs:
minimize-crashes: true minimize-crashes: true
parallel-fuzzing: true parallel-fuzzing: true
storage-repo: https://${{ secrets.FULL_ACCESS_TOKEN }}@github.com/oxigraph/clusterfuzzlite-oxigraph.git storage-repo: https://${{ secrets.FULL_ACCESS_TOKEN }}@github.com/oxigraph/clusterfuzzlite-oxigraph.git
continue-on-error: true
fuzz_prune:
if: github.event_name != 'pull_request'
needs: fuzz_repo
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
submodules: true
- uses: google/clusterfuzzlite/actions/build_fuzzers@v1
with:
language: rust
github-token: ${{ secrets.GITHUB_TOKEN }}
- uses: google/clusterfuzzlite/actions/run_fuzzers@v1 - uses: google/clusterfuzzlite/actions/run_fuzzers@v1
with: with:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
fuzz-seconds: 14400 fuzz-seconds: 3600
mode: prune mode: prune
storage-repo: https://${{ secrets.FULL_ACCESS_TOKEN }}@github.com/oxigraph/clusterfuzzlite-oxigraph.git storage-repo: https://${{ secrets.FULL_ACCESS_TOKEN }}@github.com/oxigraph/clusterfuzzlite-oxigraph.git
continue-on-error: true
fuzz_coverage: fuzz_coverage:
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
needs: fuzz_prune needs: fuzz_repo
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: google/clusterfuzzlite/actions/build_fuzzers@v1 - uses: google/clusterfuzzlite/actions/build_fuzzers@v1
@ -379,7 +368,14 @@ jobs:
- uses: google/clusterfuzzlite/actions/run_fuzzers@v1 - uses: google/clusterfuzzlite/actions/run_fuzzers@v1
with: with:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
fuzz-seconds: 600 fuzz-seconds: 3600
mode: coverage mode: coverage
sanitizer: coverage sanitizer: coverage
storage-repo: https://${{ secrets.FULL_ACCESS_TOKEN }}@github.com/oxigraph/clusterfuzzlite-oxigraph.git storage-repo: https://${{ secrets.FULL_ACCESS_TOKEN }}@github.com/oxigraph/clusterfuzzlite-oxigraph.git
shellcheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: sudo apt install -y shellcheck
- run: git grep -l '^#\( *shellcheck \|!\(/bin/\|/usr/bin/env \)\(sh\|bash\|dash\|ksh\)\)' | xargs shellcheck

@ -0,0 +1,3 @@
Thomas Tanon <thomas@pellissier-tanon.fr> <thomaspt@hotmail.fr> <Tpt@users.noreply.github.com>
Thomas Tanon <thomas@pellissier-tanon.fr>
Thomas Tanon <thomas.pellissier-tanon@helsing.ai>

@ -1,3 +1,12 @@
## [0.3.16] - 2023-04-29
### Changed
- Fixes flush and compaction on the GSPO index. It might improve Oxigraph performances and storage space.
- SPARQL: fixes some optimizations in presence quoted triples with nested variables.
- SPARQL profiler: adds EXISTS operation to the explanation and profiling tree.
- Upgrades RocksDB to 8.1.1.
## [0.3.15] - 2023-04-18 ## [0.3.15] - 2023-04-18
### Added ### Added

113
Cargo.lock generated

@ -17,6 +17,15 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "aho-corasick"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "anes" name = "anes"
version = "0.1.6" version = "0.1.6"
@ -141,9 +150,9 @@ dependencies = [
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.12.0" version = "3.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" checksum = "9b1ce199063694f33ffb7dd4e0ee620741495c32833cde5aa08f02a0bf96f0c8"
[[package]] [[package]]
name = "cast" name = "cast"
@ -215,9 +224,9 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "3.2.23" version = "3.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5" checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"clap_lex 0.2.4", "clap_lex 0.2.4",
@ -299,9 +308,9 @@ checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
[[package]] [[package]]
name = "cpufeatures" name = "cpufeatures"
version = "0.2.6" version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "280a9f2d8b3a38871a3c8a46fb80db65e5e5ed97da80c4d08bf27fb63e35e181" checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58"
dependencies = [ dependencies = [
"libc", "libc",
] ]
@ -325,7 +334,7 @@ dependencies = [
"atty", "atty",
"cast", "cast",
"ciborium", "ciborium",
"clap 3.2.23", "clap 3.2.25",
"criterion-plot", "criterion-plot",
"itertools", "itertools",
"lazy_static", "lazy_static",
@ -381,7 +390,7 @@ dependencies = [
"autocfg", "autocfg",
"cfg-if", "cfg-if",
"crossbeam-utils", "crossbeam-utils",
"memoffset", "memoffset 0.8.0",
"scopeguard", "scopeguard",
] ]
@ -487,9 +496,9 @@ dependencies = [
[[package]] [[package]]
name = "flate2" name = "flate2"
version = "1.0.25" version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743"
dependencies = [ dependencies = [
"crc32fast", "crc32fast",
"miniz_oxide", "miniz_oxide",
@ -563,7 +572,7 @@ version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc" checksum = "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick 0.7.20",
"bstr", "bstr",
"fnv", "fnv",
"log", "log",
@ -774,9 +783,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.141" version = "0.2.142"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5" checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317"
[[package]] [[package]]
name = "libloading" name = "libloading"
@ -790,9 +799,9 @@ dependencies = [
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.3.3" version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b085a4f2cde5781fc4b1717f2e86c62f5cda49de7ba99a7c2eae02b61c9064c" checksum = "b64f40e5e03e0d54f03845c8197d0291253cdbedfb1cb46b13c2c117554a9f4c"
[[package]] [[package]]
name = "lock_api" name = "lock_api"
@ -837,6 +846,15 @@ dependencies = [
"autocfg", "autocfg",
] ]
[[package]]
name = "memoffset"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c"
dependencies = [
"autocfg",
]
[[package]] [[package]]
name = "minimal-lexical" name = "minimal-lexical"
version = "0.2.1" version = "0.2.1"
@ -845,9 +863,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.6.2" version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"
dependencies = [ dependencies = [
"adler", "adler",
] ]
@ -927,7 +945,7 @@ dependencies = [
[[package]] [[package]]
name = "oxigraph" name = "oxigraph"
version = "0.3.16-dev" version = "0.3.17-dev"
dependencies = [ dependencies = [
"criterion", "criterion",
"digest", "digest",
@ -959,7 +977,7 @@ dependencies = [
[[package]] [[package]]
name = "oxigraph_js" name = "oxigraph_js"
version = "0.3.16-dev" version = "0.3.17-dev"
dependencies = [ dependencies = [
"console_error_panic_hook", "console_error_panic_hook",
"js-sys", "js-sys",
@ -969,7 +987,7 @@ dependencies = [
[[package]] [[package]]
name = "oxigraph_server" name = "oxigraph_server"
version = "0.3.16-dev" version = "0.3.17-dev"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"assert_cmd", "assert_cmd",
@ -1017,7 +1035,7 @@ checksum = "bb175ec8981211357b7b379869c2f8d555881c55ea62311428ec0de46d89bd5c"
[[package]] [[package]]
name = "oxrdf" name = "oxrdf"
version = "0.1.5" version = "0.1.6-dev"
dependencies = [ dependencies = [
"oxilangtag", "oxilangtag",
"oxiri", "oxiri",
@ -1027,7 +1045,7 @@ dependencies = [
[[package]] [[package]]
name = "oxrocksdb-sys" name = "oxrocksdb-sys"
version = "0.3.16-dev" version = "0.3.17-dev"
dependencies = [ dependencies = [
"bindgen", "bindgen",
"cc", "cc",
@ -1036,10 +1054,9 @@ dependencies = [
[[package]] [[package]]
name = "oxsdatatypes" name = "oxsdatatypes"
version = "0.1.1" version = "0.1.2-dev"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"nom",
] ]
[[package]] [[package]]
@ -1219,14 +1236,14 @@ dependencies = [
[[package]] [[package]]
name = "pyo3" name = "pyo3"
version = "0.18.3" version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3b1ac5b3731ba34fdaa9785f8d74d17448cd18f30cf19e0c7e7b1fdb5272109" checksum = "cffef52f74ec3b1a1baf295d9b8fcc3070327aefc39a6d00656b13c1d0b8885c"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"indoc", "indoc",
"libc", "libc",
"memoffset", "memoffset 0.9.0",
"parking_lot", "parking_lot",
"pyo3-build-config", "pyo3-build-config",
"pyo3-ffi", "pyo3-ffi",
@ -1236,9 +1253,9 @@ dependencies = [
[[package]] [[package]]
name = "pyo3-build-config" name = "pyo3-build-config"
version = "0.18.3" version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cb946f5ac61bb61a5014924910d936ebd2b23b705f7a4a3c40b05c720b079a3" checksum = "713eccf888fb05f1a96eb78c0dbc51907fee42b3377272dc902eb38985f418d5"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"target-lexicon", "target-lexicon",
@ -1246,9 +1263,9 @@ dependencies = [
[[package]] [[package]]
name = "pyo3-ffi" name = "pyo3-ffi"
version = "0.18.3" version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd4d7c5337821916ea2a1d21d1092e8443cf34879e53a0ac653fbb98f44ff65c" checksum = "5b2ecbdcfb01cbbf56e179ce969a048fd7305a66d4cdf3303e0da09d69afe4c3"
dependencies = [ dependencies = [
"libc", "libc",
"pyo3-build-config", "pyo3-build-config",
@ -1256,9 +1273,9 @@ dependencies = [
[[package]] [[package]]
name = "pyo3-macros" name = "pyo3-macros"
version = "0.18.3" version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d39c55dab3fc5a4b25bbd1ac10a2da452c4aca13bb450f22818a002e29648d" checksum = "b78fdc0899f2ea781c463679b20cb08af9247febc8d052de941951024cd8aea0"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"pyo3-macros-backend", "pyo3-macros-backend",
@ -1268,9 +1285,9 @@ dependencies = [
[[package]] [[package]]
name = "pyo3-macros-backend" name = "pyo3-macros-backend"
version = "0.18.3" version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97daff08a4c48320587b5224cc98d609e3c27b6d437315bd40b605c98eeb5918" checksum = "60da7b84f1227c3e2fe7593505de274dcf4c8928b4e0a1c23d551a14e4e80a0f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1279,7 +1296,7 @@ dependencies = [
[[package]] [[package]]
name = "pyoxigraph" name = "pyoxigraph"
version = "0.3.16-dev" version = "0.3.17-dev"
dependencies = [ dependencies = [
"oxigraph", "oxigraph",
"pyo3", "pyo3",
@ -1375,11 +1392,11 @@ dependencies = [
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.7.3" version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d" checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick 1.0.1",
"memchr", "memchr",
"regex-syntax", "regex-syntax",
] ]
@ -1392,9 +1409,9 @@ checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
[[package]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.6.29" version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c"
[[package]] [[package]]
name = "ring" name = "ring"
@ -1448,9 +1465,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "0.37.13" version = "0.37.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f79bef90eb6d984c72722595b5b1348ab39275a5e5123faca6863bf07d75a4e0" checksum = "a0661814f891c57c930a610266415528da53c4933e6dea5fb350cbfe048a9ece"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"errno", "errno",
@ -1629,7 +1646,7 @@ checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
[[package]] [[package]]
name = "sparesults" name = "sparesults"
version = "0.1.7" version = "0.1.8-dev"
dependencies = [ dependencies = [
"json-event-parser", "json-event-parser",
"oxrdf", "oxrdf",
@ -1638,7 +1655,7 @@ dependencies = [
[[package]] [[package]]
name = "spargebra" name = "spargebra"
version = "0.2.7" version = "0.2.8-dev"
dependencies = [ dependencies = [
"oxilangtag", "oxilangtag",
"oxiri", "oxiri",
@ -1649,7 +1666,7 @@ dependencies = [
[[package]] [[package]]
name = "sparql-smith" name = "sparql-smith"
version = "0.1.0-alpha.3" version = "0.1.0-alpha.4-dev"
dependencies = [ dependencies = [
"arbitrary", "arbitrary",
] ]
@ -1690,9 +1707,9 @@ dependencies = [
[[package]] [[package]]
name = "target-lexicon" name = "target-lexicon"
version = "0.12.6" version = "0.12.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ae9980cab1db3fceee2f6c6f643d5d8de2997c58ee8d25fb0cc8a9e9e7348e5" checksum = "fd1ba337640d60c3e96bc6f0638a939b9c9a7f2c316a1598c279828b3d1dc8c5"
[[package]] [[package]]
name = "tempfile" name = "tempfile"

@ -12,6 +12,7 @@ members = [
"server", "server",
"testsuite" "testsuite"
] ]
resolver = "2"
[profile.release] [profile.release]
lto = true lto = true

@ -2,6 +2,8 @@
DATASET_SIZE=100000 DATASET_SIZE=100000
PARALLELISM=16 PARALLELISM=16
set -eu
wget -nc https://github.com/blazegraph/database/releases/download/BLAZEGRAPH_RELEASE_2_1_5/blazegraph.jar wget -nc https://github.com/blazegraph/database/releases/download/BLAZEGRAPH_RELEASE_2_1_5/blazegraph.jar
cd bsbm-tools || exit cd bsbm-tools || exit
./generate -fc -pc ${DATASET_SIZE} -s nt -fn "explore-${DATASET_SIZE}" -ud -ufn "explore-update-${DATASET_SIZE}" ./generate -fc -pc ${DATASET_SIZE} -s nt -fn "explore-${DATASET_SIZE}" -ud -ufn "explore-update-${DATASET_SIZE}"
@ -12,7 +14,7 @@ curl -f -X POST -H 'Content-Type:text/turtle' -T "explore-${DATASET_SIZE}.nt" ht
./testdriver -mt ${PARALLELISM} -ucf usecases/exploreAndUpdate/sparql.txt -o "../bsbm.exploreAndUpdate.blazegraph.2.1.5.${DATASET_SIZE}.${PARALLELISM}.xml" http://localhost:9999/blazegraph/sparql -u http://localhost:9999/blazegraph/sparql -udataset "explore-update-${DATASET_SIZE}.nt" ./testdriver -mt ${PARALLELISM} -ucf usecases/exploreAndUpdate/sparql.txt -o "../bsbm.exploreAndUpdate.blazegraph.2.1.5.${DATASET_SIZE}.${PARALLELISM}.xml" http://localhost:9999/blazegraph/sparql -u http://localhost:9999/blazegraph/sparql -udataset "explore-update-${DATASET_SIZE}.nt"
#./testdriver -mt ${PARALLELISM} -ucf usecases/businessIntelligence/sparql.txt -o "../bsbm.businessIntelligence.blazegraph.2.1.5.${DATASET_SIZE}.${PARALLELISM}.xml" http://localhost:9999/blazegraph/sparql #./testdriver -mt ${PARALLELISM} -ucf usecases/businessIntelligence/sparql.txt -o "../bsbm.businessIntelligence.blazegraph.2.1.5.${DATASET_SIZE}.${PARALLELISM}.xml" http://localhost:9999/blazegraph/sparql
kill $! kill $!
rm blazegraph.jnl rm -f blazegraph.jnl
rm "explore-${DATASET_SIZE}.nt" rm -f "explore-${DATASET_SIZE}.nt"
rm "explore-update-${DATASET_SIZE}.nt" rm -f "explore-update-${DATASET_SIZE}.nt"
rm -r td_data rm -rf td_data

@ -3,7 +3,8 @@
DATASET_SIZE=100000 DATASET_SIZE=100000
PARALLELISM=16 PARALLELISM=16
VERSION="9.3.3" VERSION="9.3.3"
JAVA_HOME=/usr/lib/jvm/java-11-openjdk
set -eu
cd bsbm-tools cd bsbm-tools
./generate -fc -pc ${DATASET_SIZE} -s nt -fn "explore-${DATASET_SIZE}" -ud -ufn "explore-update-${DATASET_SIZE}" ./generate -fc -pc ${DATASET_SIZE} -s nt -fn "explore-${DATASET_SIZE}" -ud -ufn "explore-update-${DATASET_SIZE}"
../graphdb-free-9.3.3/bin/graphdb -s -Dgraphdb.logger.root.level=WARN & ../graphdb-free-9.3.3/bin/graphdb -s -Dgraphdb.logger.root.level=WARN &
@ -17,7 +18,7 @@ curl -f -X PUT -H 'Content-Type:application/n-triples' -T "explore-${DATASET_SIZ
#./testdriver -mt ${PARALLELISM} -ucf usecases/businessIntelligence/sparql.txt -o "../bsbm.businessIntelligence.graphdb.${VERSION}.${DATASET_SIZE}.${PARALLELISM}.xml" http://localhost:7200/repositories/bsbm #./testdriver -mt ${PARALLELISM} -ucf usecases/businessIntelligence/sparql.txt -o "../bsbm.businessIntelligence.graphdb.${VERSION}.${DATASET_SIZE}.${PARALLELISM}.xml" http://localhost:7200/repositories/bsbm
kill $! kill $!
sleep 5 sleep 5
rm -r ../graphdb-free-9.3.3/data rm -rf ../graphdb-free-9.3.3/data
rm "explore-${DATASET_SIZE}.nt" rm -f "explore-${DATASET_SIZE}.nt"
rm "explore-update-${DATASET_SIZE}.nt" rm -f "explore-update-${DATASET_SIZE}.nt"
rm -r td_data rm -rf td_data

@ -3,6 +3,8 @@
DATASET_SIZE=100000 # number of products in the dataset. There is around 350 triples generated by product. DATASET_SIZE=100000 # number of products in the dataset. There is around 350 triples generated by product.
PARALLELISM=16 PARALLELISM=16
VERSION="4.3.2" VERSION="4.3.2"
set -eu
wget -nc https://downloads.apache.org/jena/binaries/apache-jena-${VERSION}.zip wget -nc https://downloads.apache.org/jena/binaries/apache-jena-${VERSION}.zip
cd bsbm-tools || exit cd bsbm-tools || exit
./generate -fc -pc ${DATASET_SIZE} -s nt -fn "explore-${DATASET_SIZE}" -ud -ufn "explore-update-${DATASET_SIZE}" ./generate -fc -pc ${DATASET_SIZE} -s nt -fn "explore-${DATASET_SIZE}" -ud -ufn "explore-update-${DATASET_SIZE}"
@ -18,9 +20,9 @@ sleep 10
./testdriver -mt ${PARALLELISM} -ucf usecases/exploreAndUpdate/sparql.txt -o "../bsbm.exploreAndUpdate.jena.${VERSION}.${DATASET_SIZE}.${PARALLELISM}.xml" http://localhost:3030/bsbm/query -u http://localhost:3030/bsbm/update -udataset "explore-update-${DATASET_SIZE}.nt" ./testdriver -mt ${PARALLELISM} -ucf usecases/exploreAndUpdate/sparql.txt -o "../bsbm.exploreAndUpdate.jena.${VERSION}.${DATASET_SIZE}.${PARALLELISM}.xml" http://localhost:3030/bsbm/query -u http://localhost:3030/bsbm/update -udataset "explore-update-${DATASET_SIZE}.nt"
#./testdriver -mt ${PARALLELISM} -ucf usecases/businessIntelligence/sparql.txt -o "../bsbm.businessIntelligence.jena.${VERSION}.${DATASET_SIZE}.${PARALLELISM}.xml" http://localhost:3030/bsbm/query #./testdriver -mt ${PARALLELISM} -ucf usecases/businessIntelligence/sparql.txt -o "../bsbm.businessIntelligence.jena.${VERSION}.${DATASET_SIZE}.${PARALLELISM}.xml" http://localhost:3030/bsbm/query
kill $! kill $!
rm "explore-${DATASET_SIZE}.nt" rm -f "explore-${DATASET_SIZE}.nt"
rm "explore-update-${DATASET_SIZE}.nt" rm -f "explore-update-${DATASET_SIZE}.nt"
rm -r td_data rm -rf td_data
rm -r run rm -rf run
rm -r apache-jena-${VERSION} rm -rf apache-jena-${VERSION}
rm -r apache-jena-fuseki-${VERSION} rm -rf apache-jena-fuseki-${VERSION}

@ -2,6 +2,8 @@
DATASET_SIZE=100000 # number of products in the dataset. There is around 350 triples generated by product. DATASET_SIZE=100000 # number of products in the dataset. There is around 350 triples generated by product.
PARALLELISM=16 PARALLELISM=16
set -eu
cd bsbm-tools cd bsbm-tools
./generate -fc -pc ${DATASET_SIZE} -s nt -fn "explore-${DATASET_SIZE}" -ud -ufn "explore-update-${DATASET_SIZE}" ./generate -fc -pc ${DATASET_SIZE} -s nt -fn "explore-${DATASET_SIZE}" -ud -ufn "explore-update-${DATASET_SIZE}"
cargo build --release --manifest-path="../../server/Cargo.toml" cargo build --release --manifest-path="../../server/Cargo.toml"
@ -13,7 +15,7 @@ sleep 1
./testdriver -mt ${PARALLELISM} -ucf usecases/exploreAndUpdate/sparql.txt -o "../bsbm.exploreAndUpdate.oxigraph.${VERSION}.${DATASET_SIZE}.${PARALLELISM}.xml" http://127.0.0.1:7878/query -u http://127.0.0.1:7878/update -udataset "explore-update-${DATASET_SIZE}.nt" ./testdriver -mt ${PARALLELISM} -ucf usecases/exploreAndUpdate/sparql.txt -o "../bsbm.exploreAndUpdate.oxigraph.${VERSION}.${DATASET_SIZE}.${PARALLELISM}.xml" http://127.0.0.1:7878/query -u http://127.0.0.1:7878/update -udataset "explore-update-${DATASET_SIZE}.nt"
#./testdriver -mt ${PARALLELISM} -ucf usecases/businessIntelligence/sparql.txt -o "../bsbm.businessIntelligence.${VERSION}.${DATASET_SIZE}.${PARALLELISM}.xml" "http://127.0.0.1:7878/query" #./testdriver -mt ${PARALLELISM} -ucf usecases/businessIntelligence/sparql.txt -o "../bsbm.businessIntelligence.${VERSION}.${DATASET_SIZE}.${PARALLELISM}.xml" "http://127.0.0.1:7878/query"
kill $! kill $!
rm -r oxigraph_data rm -rf oxigraph_data
rm "explore-${DATASET_SIZE}.nt" rm -f "explore-${DATASET_SIZE}.nt"
rm "explore-update-${DATASET_SIZE}.nt" rm -f "explore-update-${DATASET_SIZE}.nt"
rm -r td_data rm -rf td_data

@ -4,6 +4,8 @@ DATASET_SIZE=100000
PARALLELISM=16 PARALLELISM=16
VERSION="4.2.2" VERSION="4.2.2"
TOMCAT_VERSION="9.0.71" TOMCAT_VERSION="9.0.71"
set -eu
wget -nc -O "rdf4j-${VERSION}.zip" "https://www.eclipse.org/downloads/download.php?file=/rdf4j/eclipse-rdf4j-${VERSION}-sdk.zip&mirror_id=1" wget -nc -O "rdf4j-${VERSION}.zip" "https://www.eclipse.org/downloads/download.php?file=/rdf4j/eclipse-rdf4j-${VERSION}-sdk.zip&mirror_id=1"
wget -nc -O "tomcat-${TOMCAT_VERSION}.zip" "https://dlcdn.apache.org/tomcat/tomcat-9/v${TOMCAT_VERSION}/bin/apache-tomcat-${TOMCAT_VERSION}.zip" wget -nc -O "tomcat-${TOMCAT_VERSION}.zip" "https://dlcdn.apache.org/tomcat/tomcat-9/v${TOMCAT_VERSION}/bin/apache-tomcat-${TOMCAT_VERSION}.zip"
cd bsbm-tools || exit cd bsbm-tools || exit
@ -40,8 +42,8 @@ curl -f -X PUT -H 'Content-Type:application/n-triples' -T "explore-${DATASET_SIZ
./testdriver -mt ${PARALLELISM} -ucf usecases/exploreAndUpdate/sparql.txt -o "../bsbm.exploreAndUpdate.rdf4j-lmdb.${VERSION}.${DATASET_SIZE}.${PARALLELISM}.xml" http://localhost:8080/rdf4j-server/repositories/bsbm -u http://localhost:8080/rdf4j-server/repositories/bsbm/statements -udataset "explore-update-${DATASET_SIZE}.nt" ./testdriver -mt ${PARALLELISM} -ucf usecases/exploreAndUpdate/sparql.txt -o "../bsbm.exploreAndUpdate.rdf4j-lmdb.${VERSION}.${DATASET_SIZE}.${PARALLELISM}.xml" http://localhost:8080/rdf4j-server/repositories/bsbm -u http://localhost:8080/rdf4j-server/repositories/bsbm/statements -udataset "explore-update-${DATASET_SIZE}.nt"
#./testdriver -mt ${PARALLELISM} -ucf usecases/businessIntelligence/sparql.txt -o "../bsbm.businessIntelligence.rdf4j-lmdb.${VERSION}.${DATASET_SIZE}.${PARALLELISM}.xml" http://localhost:8080/rdf4j-server/repositories/bsbm #./testdriver -mt ${PARALLELISM} -ucf usecases/businessIntelligence/sparql.txt -o "../bsbm.businessIntelligence.rdf4j-lmdb.${VERSION}.${DATASET_SIZE}.${PARALLELISM}.xml" http://localhost:8080/rdf4j-server/repositories/bsbm
"${CATALINA_HOME}"/bin/shutdown.sh "${CATALINA_HOME}"/bin/shutdown.sh
rm "explore-${DATASET_SIZE}.nt" rm -f "explore-${DATASET_SIZE}.nt"
rm "explore-update-${DATASET_SIZE}.nt" rm -f "explore-update-${DATASET_SIZE}.nt"
rm -r td_data rm -rf td_data
rm -r "eclipse-rdf4j-${VERSION}" rm -rf "eclipse-rdf4j-${VERSION}"
rm -r "apache-tomcat-${TOMCAT_VERSION}" rm -rf "apache-tomcat-${TOMCAT_VERSION}"

@ -3,6 +3,8 @@
DATASET_SIZE=100000 # number of products in the dataset. There is around 350 triples generated by product. DATASET_SIZE=100000 # number of products in the dataset. There is around 350 triples generated by product.
PARALLELISM=16 PARALLELISM=16
VERSION="7.2.5" VERSION="7.2.5"
set -eu
cd bsbm-tools cd bsbm-tools
./generate -fc -pc ${DATASET_SIZE} -s nt -fn "explore-${DATASET_SIZE}" -ud -ufn "explore-update-${DATASET_SIZE}" ./generate -fc -pc ${DATASET_SIZE} -s nt -fn "explore-${DATASET_SIZE}" -ud -ufn "explore-update-${DATASET_SIZE}"
cp ../virtuoso-opensource/database/virtuoso.ini.sample virtuoso.ini cp ../virtuoso-opensource/database/virtuoso.ini.sample virtuoso.ini
@ -18,7 +20,7 @@ EOF
# ./testdriver -mt ${PARALLELISM} -ucf usecases/exploreAndUpdate/sparql.txt -o "../bsbm.exploreAndUpdate.virtuoso.${DATASET_SIZE}.${PARALLELISM}.${PARALLELISM}.${VERSION}.xml" 'http://localhost:8890/sparql?graph-uri=urn:graph:test' -u 'http://dba:dba@localhost:8890/sparql-auth?graph-uri=urn:graph:test' -udataset "explore-update-${DATASET_SIZE}.nt" # ./testdriver -mt ${PARALLELISM} -ucf usecases/exploreAndUpdate/sparql.txt -o "../bsbm.exploreAndUpdate.virtuoso.${DATASET_SIZE}.${PARALLELISM}.${PARALLELISM}.${VERSION}.xml" 'http://localhost:8890/sparql?graph-uri=urn:graph:test' -u 'http://dba:dba@localhost:8890/sparql-auth?graph-uri=urn:graph:test' -udataset "explore-update-${DATASET_SIZE}.nt"
# ./testdriver -mt ${PARALLELISM} -ucf usecases/businessIntelligence/sparql.txt -o "../bsbm.businessIntelligence.virtuoso.${VERSION}.${DATASET_SIZE}.${PARALLELISM}.xml" 'http://localhost:8890/sparql?graph-uri=urn:graph:test' # ./testdriver -mt ${PARALLELISM} -ucf usecases/businessIntelligence/sparql.txt -o "../bsbm.businessIntelligence.virtuoso.${VERSION}.${DATASET_SIZE}.${PARALLELISM}.xml" 'http://localhost:8890/sparql?graph-uri=urn:graph:test'
kill $! kill $!
rm -r ../database rm -rf ../database
rm "explore-${DATASET_SIZE}.nt" rm -f "explore-${DATASET_SIZE}.nt"
rm "explore-update-${DATASET_SIZE}.nt" rm -f "explore-update-${DATASET_SIZE}.nt"
rm -r td_data rm -rf td_data

@ -1,6 +1,6 @@
[package] [package]
name = "oxigraph_js" name = "oxigraph_js"
version = "0.3.16-dev" version = "0.3.17-dev"
authors = ["Tpt <thomas@pellissier-tanon.fr>"] authors = ["Tpt <thomas@pellissier-tanon.fr>"]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
readme = "README.md" readme = "README.md"
@ -14,7 +14,7 @@ crate-type = ["cdylib"]
name = "oxigraph" name = "oxigraph"
[dependencies] [dependencies]
oxigraph = { version = "0.3.16-dev", path="../lib" } oxigraph = { version = "0.3.17-dev", path="../lib" }
wasm-bindgen = "0.2" wasm-bindgen = "0.2"
js-sys = "0.3" js-sys = "0.3"
console_error_panic_hook = "0.1" console_error_panic_hook = "0.1"

@ -5,10 +5,10 @@
"devDependencies": { "devDependencies": {
"@rdfjs/data-model": "^2.0.1", "@rdfjs/data-model": "^2.0.1",
"mocha": "^10.0.0", "mocha": "^10.0.0",
"rome": "^11.0.0" "rome": "^12.0.0"
}, },
"scripts": { "scripts": {
"fmt": "rome format . --write && rome check . --apply-suggested", "fmt": "rome format . --write && rome check . --apply-unsafe",
"test": "rome ci . && wasm-pack build --debug --target nodejs && mocha", "test": "rome ci . && wasm-pack build --debug --target nodejs && mocha",
"build": "rm -rf pkg && wasm-pack build --release --target web --out-name web && mv pkg pkg-web && wasm-pack build --release --target nodejs --out-name node && mv pkg pkg-node && node build_package.js && rm -r pkg-web && rm -r pkg-node", "build": "rm -rf pkg && wasm-pack build --release --target web --out-name web && mv pkg pkg-web && wasm-pack build --release --target nodejs --out-name node && mv pkg pkg-node && node build_package.js && rm -r pkg-web && rm -r pkg-node",
"release": "npm run build && npm publish ./pkg", "release": "npm run build && npm publish ./pkg",

@ -1,10 +1,13 @@
{ {
"formatter": { "formatter": {
"indentStyle": "space", "indentStyle": "space",
"indentSize": 4, "indentSize": 4,
"lineWidth": 100 "lineWidth": 100
}, },
"linter": { "linter": {
"ignore": ["pkg"] "ignore": ["pkg"]
} },
} "organizeImports": {
"enabled": true
}
}

@ -19,7 +19,7 @@ thread_local! {
#[wasm_bindgen(js_name = namedNode)] #[wasm_bindgen(js_name = namedNode)]
pub fn named_node(value: String) -> Result<JsNamedNode, JsValue> { pub fn named_node(value: String) -> Result<JsNamedNode, JsValue> {
NamedNode::new(value) NamedNode::new(value)
.map(|v| v.into()) .map(Into::into)
.map_err(|v| UriError::new(&v.to_string()).into()) .map_err(|v| UriError::new(&v.to_string()).into())
} }

@ -76,28 +76,28 @@ impl JsStore {
None None
} }
.as_ref() .as_ref()
.map(|t: &NamedOrBlankNode| t.into()), .map(<&Subject>::into),
if let Some(predicate) = FROM_JS.with(|c| c.to_optional_term(predicate))? { if let Some(predicate) = FROM_JS.with(|c| c.to_optional_term(predicate))? {
Some(NamedNode::try_from(predicate)?) Some(NamedNode::try_from(predicate)?)
} else { } else {
None None
} }
.as_ref() .as_ref()
.map(|t: &NamedNode| t.into()), .map(<&NamedNode>::into),
if let Some(object) = FROM_JS.with(|c| c.to_optional_term(object))? { if let Some(object) = FROM_JS.with(|c| c.to_optional_term(object))? {
Some(object.try_into()?) Some(object.try_into()?)
} else { } else {
None None
} }
.as_ref() .as_ref()
.map(|t: &Term| t.into()), .map(<&Term>::into),
if let Some(graph_name) = FROM_JS.with(|c| c.to_optional_term(graph_name))? { if let Some(graph_name) = FROM_JS.with(|c| c.to_optional_term(graph_name))? {
Some(graph_name.try_into()?) Some(graph_name.try_into()?)
} else { } else {
None None
} }
.as_ref() .as_ref()
.map(|t: &GraphName| t.into()), .map(<&GraphName>::into),
) )
.map(|v| v.map(|v| JsQuad::from(v).into())) .map(|v| v.map(|v| JsQuad::from(v).into()))
.collect::<Result<Vec<_>, _>>() .collect::<Result<Vec<_>, _>>()

@ -1,8 +1,8 @@
/* global describe, it */ /* global describe, it */
import runTests from "../node_modules/@rdfjs/data-model/test/index.js";
import oxigraph from "../pkg/oxigraph.js"; import oxigraph from "../pkg/oxigraph.js";
import assert from "assert"; import assert from "assert";
import runTests from "../node_modules/@rdfjs/data-model/test/index.js";
runTests({ factory: oxigraph }); runTests({ factory: oxigraph });

@ -1,8 +1,8 @@
/* global describe, it */ /* global describe, it */
import { Store } from "../pkg/oxigraph.js"; import { Store } from "../pkg/oxigraph.js";
import assert from "assert";
import dataModel from "@rdfjs/data-model"; import dataModel from "@rdfjs/data-model";
import assert from "assert";
const ex = dataModel.namedNode("http://example.com"); const ex = dataModel.namedNode("http://example.com");
const triple = dataModel.quad( const triple = dataModel.quad(

@ -1,6 +1,6 @@
[package] [package]
name = "oxigraph" name = "oxigraph"
version = "0.3.16-dev" version = "0.3.17-dev"
authors = ["Tpt <thomas@pellissier-tanon.fr>"] authors = ["Tpt <thomas@pellissier-tanon.fr>"]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
readme = "README.md" readme = "README.md"
@ -38,14 +38,14 @@ hex = "0.4"
siphasher = "0.3" siphasher = "0.3"
lazy_static = "1" lazy_static = "1"
json-event-parser = "0.1" json-event-parser = "0.1"
oxrdf = { version = "0.1.5", path="oxrdf", features = ["rdf-star", "oxsdatatypes"] } oxrdf = { version = "0.1.6-dev", path="oxrdf", features = ["rdf-star", "oxsdatatypes"] }
oxsdatatypes = { version = "0.1.1", path="oxsdatatypes" } oxsdatatypes = { version = "0.1.2-dev", path="oxsdatatypes" }
spargebra = { version = "0.2.7", path="spargebra", features = ["rdf-star", "sep-0002", "sep-0006"] } spargebra = { version = "0.2.8-dev", path="spargebra", features = ["rdf-star", "sep-0002", "sep-0006"] }
sparesults = { version = "0.1.7", path="sparesults", features = ["rdf-star"] } sparesults = { version = "0.1.8-dev", path="sparesults", features = ["rdf-star"] }
[target.'cfg(not(target_family = "wasm"))'.dependencies] [target.'cfg(not(target_family = "wasm"))'.dependencies]
libc = "0.2" libc = "0.2"
oxrocksdb-sys = { version = "0.3.16-dev", path="../oxrocksdb-sys" } oxrocksdb-sys = { version = "0.3.17-dev", path="../oxrocksdb-sys" }
oxhttp = { version = "0.1", optional = true } oxhttp = { version = "0.1", optional = true }
[target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies] [target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies]

@ -1,6 +1,6 @@
[package] [package]
name = "oxrdf" name = "oxrdf"
version = "0.1.5" version = "0.1.6-dev"
authors = ["Tpt <thomas@pellissier-tanon.fr>"] authors = ["Tpt <thomas@pellissier-tanon.fr>"]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
readme = "README.md" readme = "README.md"
@ -21,7 +21,7 @@ rdf-star = []
rand = "0.8" rand = "0.8"
oxilangtag = "0.1" oxilangtag = "0.1"
oxiri = "0.2" oxiri = "0.2"
oxsdatatypes = { version = "0.1.1", path="../oxsdatatypes", optional = true } oxsdatatypes = { version = "0.1.2-dev", path="../oxsdatatypes", optional = true }
[package.metadata.docs.rs] [package.metadata.docs.rs]
all-features = true all-features = true

@ -705,9 +705,7 @@ impl Dataset {
InternedTerm, InternedTerm,
InternedGraphName, InternedGraphName,
)> { )> {
let b_prime = partition let b_prime = partition.iter().find_map(|(_, b)| (b.len() > 1).then(|| b));
.iter()
.find_map(|(_, b)| if b.len() > 1 { Some(b) } else { None });
if let Some(b_prime) = b_prime { if let Some(b_prime) = b_prime {
b_prime b_prime
.iter() .iter()

@ -463,11 +463,10 @@ impl InternedTriple {
predicate: InternedNamedNode::encoded_from(triple.predicate, interner)?, predicate: InternedNamedNode::encoded_from(triple.predicate, interner)?,
object: InternedTerm::encoded_from(triple.object, interner)?, object: InternedTerm::encoded_from(triple.object, interner)?,
}; };
if interner.triples.contains_key(&interned_triple) { interner
Some(interned_triple) .triples
} else { .contains_key(&interned_triple)
None .then(|| interned_triple)
}
} }
pub fn next(&self) -> Self { pub fn next(&self) -> Self {

@ -1,5 +1,4 @@
#![doc = include_str!("../README.md")] #![doc = include_str!("../README.md")]
#![deny(unsafe_code)]
#![doc(test(attr(deny(warnings))))] #![doc(test(attr(deny(warnings))))]
#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![doc(html_favicon_url = "https://raw.githubusercontent.com/oxigraph/oxigraph/main/logo.svg")] #![doc(html_favicon_url = "https://raw.githubusercontent.com/oxigraph/oxigraph/main/logo.svg")]

@ -1,6 +1,6 @@
[package] [package]
name = "oxsdatatypes" name = "oxsdatatypes"
version = "0.1.1" version = "0.1.2-dev"
authors = ["Tpt <thomas@pellissier-tanon.fr>"] authors = ["Tpt <thomas@pellissier-tanon.fr>"]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
readme = "README.md" readme = "README.md"
@ -13,9 +13,6 @@ An implementation of some XSD datatypes for SPARQL implementations
edition = "2021" edition = "2021"
rust-version = "1.60" rust-version = "1.60"
[dependencies]
nom = "7"
[target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies] [target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies]
js-sys = "0.3" js-sys = "0.3"

@ -43,14 +43,14 @@ impl From<Decimal> for Boolean {
impl From<Float> for Boolean { impl From<Float> for Boolean {
#[inline] #[inline]
fn from(value: Float) -> Self { fn from(value: Float) -> Self {
(value != Float::from(0.) && !value.is_naan()).into() (value != Float::from(0.) && !value.is_nan()).into()
} }
} }
impl From<Double> for Boolean { impl From<Double> for Boolean {
#[inline] #[inline]
fn from(value: Double) -> Self { fn from(value: Double) -> Self {
(value != Double::from(0.) && !value.is_naan()).into() (value != Double::from(0.) && !value.is_nan()).into()
} }
} }

@ -1,8 +1,7 @@
use super::parser::{date_lexical_rep, date_time_lexical_rep, parse_value, time_lexical_rep};
use super::{DayTimeDuration, Decimal, Duration, XsdParseError, YearMonthDuration}; use super::{DayTimeDuration, Decimal, Duration, XsdParseError, YearMonthDuration};
use crate::parser::{ use crate::parser::{
g_day_lexical_rep, g_month_day_lexical_rep, g_month_lexical_rep, g_year_lexical_rep, parse_date, parse_date_time, parse_g_day, parse_g_month, parse_g_month_day, parse_g_year,
g_year_month_lexical_rep, parse_g_year_month, parse_time,
}; };
use std::cmp::{min, Ordering}; use std::cmp::{min, Ordering};
use std::error::Error; use std::error::Error;
@ -44,6 +43,7 @@ impl DateTime {
}) })
} }
/// [fn:current-dateTime](https://www.w3.org/TR/xpath-functions/#func-current-dateTime)
#[inline] #[inline]
pub fn now() -> Result<Self, DateTimeError> { pub fn now() -> Result<Self, DateTimeError> {
Ok(Self { Ok(Self {
@ -187,8 +187,11 @@ impl DateTime {
self.checked_sub_day_time_duration(rhs) self.checked_sub_day_time_duration(rhs)
} else { } else {
Some(Self { Some(Self {
timestamp: Timestamp::new(&date_time_plus_duration(-rhs, &self.properties())?) timestamp: Timestamp::new(&date_time_plus_duration(
.ok()?, rhs.checked_neg()?,
&self.properties(),
)?)
.ok()?,
}) })
} }
} }
@ -230,7 +233,7 @@ impl FromStr for DateTime {
type Err = XsdParseError; type Err = XsdParseError;
fn from_str(input: &str) -> Result<Self, XsdParseError> { fn from_str(input: &str) -> Result<Self, XsdParseError> {
parse_value(date_time_lexical_rep, input) parse_date_time(input)
} }
} }
@ -303,6 +306,12 @@ impl Time {
} }
} }
/// [fn:current-time](https://www.w3.org/TR/xpath-functions/#func-current-time)
#[inline]
pub fn now() -> Result<Self, DateTimeError> {
DateTime::now()?.try_into()
}
/// [fn:hour-from-time](https://www.w3.org/TR/xpath-functions/#func-hour-from-time) /// [fn:hour-from-time](https://www.w3.org/TR/xpath-functions/#func-hour-from-time)
#[inline] #[inline]
pub fn hour(&self) -> u8 { pub fn hour(&self) -> u8 {
@ -435,7 +444,7 @@ impl FromStr for Time {
type Err = XsdParseError; type Err = XsdParseError;
fn from_str(input: &str) -> Result<Self, XsdParseError> { fn from_str(input: &str) -> Result<Self, XsdParseError> {
parse_value(time_lexical_rep, input) parse_time(input)
} }
} }
@ -498,6 +507,12 @@ impl Date {
} }
} }
/// [fn:current-date](https://www.w3.org/TR/xpath-functions/#func-current-date)
#[inline]
pub fn now() -> Result<Self, DateTimeError> {
DateTime::now()?.try_into()
}
/// [fn:year-from-date](https://www.w3.org/TR/xpath-functions/#func-year-from-date) /// [fn:year-from-date](https://www.w3.org/TR/xpath-functions/#func-year-from-date)
#[inline] #[inline]
pub fn year(&self) -> i64 { pub fn year(&self) -> i64 {
@ -632,7 +647,7 @@ impl FromStr for Date {
type Err = XsdParseError; type Err = XsdParseError;
fn from_str(input: &str) -> Result<Self, XsdParseError> { fn from_str(input: &str) -> Result<Self, XsdParseError> {
parse_value(date_lexical_rep, input) parse_date(input)
} }
} }
@ -754,7 +769,7 @@ impl FromStr for GYearMonth {
type Err = XsdParseError; type Err = XsdParseError;
fn from_str(input: &str) -> Result<Self, XsdParseError> { fn from_str(input: &str) -> Result<Self, XsdParseError> {
parse_value(g_year_month_lexical_rep, input) parse_g_year_month(input)
} }
} }
@ -875,7 +890,7 @@ impl FromStr for GYear {
type Err = XsdParseError; type Err = XsdParseError;
fn from_str(input: &str) -> Result<Self, XsdParseError> { fn from_str(input: &str) -> Result<Self, XsdParseError> {
parse_value(g_year_lexical_rep, input) parse_g_year(input)
} }
} }
@ -997,7 +1012,7 @@ impl FromStr for GMonthDay {
type Err = XsdParseError; type Err = XsdParseError;
fn from_str(input: &str) -> Result<Self, XsdParseError> { fn from_str(input: &str) -> Result<Self, XsdParseError> {
parse_value(g_month_day_lexical_rep, input) parse_g_month_day(input)
} }
} }
@ -1123,7 +1138,7 @@ impl FromStr for GMonth {
type Err = XsdParseError; type Err = XsdParseError;
fn from_str(input: &str) -> Result<Self, XsdParseError> { fn from_str(input: &str) -> Result<Self, XsdParseError> {
parse_value(g_month_lexical_rep, input) parse_g_month(input)
} }
} }
@ -1240,7 +1255,7 @@ impl FromStr for GDay {
type Err = XsdParseError; type Err = XsdParseError;
fn from_str(input: &str) -> Result<Self, XsdParseError> { fn from_str(input: &str) -> Result<Self, XsdParseError> {
parse_value(g_day_lexical_rep, input) parse_g_day(input)
} }
} }

@ -5,10 +5,9 @@ use std::fmt::Write;
use std::ops::Neg; use std::ops::Neg;
use std::str::FromStr; use std::str::FromStr;
const DECIMAL_PART_DIGITS: usize = 18; const DECIMAL_PART_DIGITS: u32 = 18;
const DECIMAL_PART_POW: i128 = 1_000_000_000_000_000_000; const DECIMAL_PART_POW: i128 = 1_000_000_000_000_000_000;
const DECIMAL_PART_POW_MINUS_ONE: i128 = 100_000_000_000_000_000; const DECIMAL_PART_POW_MINUS_ONE: i128 = 100_000_000_000_000_000;
const DECIMAL_PART_HALF_POW: i128 = 1_000_000_000;
/// [XML Schema `decimal` datatype](https://www.w3.org/TR/xmlschema11-2/#decimal) /// [XML Schema `decimal` datatype](https://www.w3.org/TR/xmlschema11-2/#decimal)
/// ///
@ -22,10 +21,9 @@ pub struct Decimal {
impl Decimal { impl Decimal {
/// Constructs the decimal i / 10^n /// Constructs the decimal i / 10^n
#[allow(clippy::cast_possible_truncation)]
#[inline] #[inline]
pub fn new(i: i128, n: u32) -> Result<Self, DecimalOverflowError> { pub fn new(i: i128, n: u32) -> Result<Self, DecimalOverflowError> {
let shift = (DECIMAL_PART_DIGITS as u32) let shift = DECIMAL_PART_DIGITS
.checked_sub(n) .checked_sub(n)
.ok_or(DecimalOverflowError)?; .ok_or(DecimalOverflowError)?;
Ok(Self { Ok(Self {
@ -66,29 +64,69 @@ impl Decimal {
/// [op:numeric-multiply](https://www.w3.org/TR/xpath-functions/#func-numeric-multiply) /// [op:numeric-multiply](https://www.w3.org/TR/xpath-functions/#func-numeric-multiply)
#[inline] #[inline]
pub fn checked_mul(&self, rhs: impl Into<Self>) -> Option<Self> { pub fn checked_mul(&self, rhs: impl Into<Self>) -> Option<Self> {
//TODO: better algorithm to keep precision // Idea: we shift right as much as possible to keep as much precision as possible
// Do the multiplication and do the required left shift
let mut left = self.value;
let mut shift_left = 0_u32;
if left != 0 {
while left % 10 == 0 {
left /= 10;
shift_left += 1;
}
}
let mut right = rhs.into().value;
let mut shift_right = 0_u32;
if right != 0 {
while right % 10 == 0 {
right /= 10;
shift_right += 1;
}
}
// We do multiplication + shift
let shift = (shift_left + shift_right).checked_sub(DECIMAL_PART_DIGITS)?;
Some(Self { Some(Self {
value: self value: left
.value .checked_mul(right)?
.checked_div(DECIMAL_PART_HALF_POW)? .checked_mul(10_i128.checked_pow(shift)?)?,
.checked_mul(rhs.into().value.checked_div(DECIMAL_PART_HALF_POW)?)?,
}) })
} }
/// [op:numeric-divide](https://www.w3.org/TR/xpath-functions/#func-numeric-divide) /// [op:numeric-divide](https://www.w3.org/TR/xpath-functions/#func-numeric-divide)
#[inline] #[inline]
pub fn checked_div(&self, rhs: impl Into<Self>) -> Option<Self> { pub fn checked_div(&self, rhs: impl Into<Self>) -> Option<Self> {
//TODO: better algorithm to keep precision // Idea: we shift the dividend left as much as possible to keep as much precision as possible
// And we shift right the divisor as much as possible
// Do the multiplication and do the required shift
let mut left = self.value;
let mut shift_left = 0_u32;
if left != 0 {
while let Some(r) = left.checked_mul(10) {
assert_eq!(r / 10, left);
left = r;
shift_left += 1;
}
}
let mut right = rhs.into().value;
let mut shift_right = 0_u32;
if right != 0 {
while right % 10 == 0 {
right /= 10;
shift_right += 1;
}
}
// We do division + shift
let shift = (shift_left + shift_right).checked_sub(DECIMAL_PART_DIGITS)?;
Some(Self { Some(Self {
value: self value: left
.value .checked_div(right)?
.checked_mul(DECIMAL_PART_HALF_POW)? .checked_div(10_i128.checked_pow(shift)?)?,
.checked_div(rhs.into().value)?
.checked_mul(DECIMAL_PART_HALF_POW)?,
}) })
} }
/// TODO: XSD? is well defined for not integer /// [op:numeric-mod](https://www.w3.org/TR/xpath-functions/#func-numeric-mod)
#[inline] #[inline]
pub fn checked_rem(&self, rhs: impl Into<Self>) -> Option<Self> { pub fn checked_rem(&self, rhs: impl Into<Self>) -> Option<Self> {
Some(Self { Some(Self {
@ -103,6 +141,14 @@ impl Decimal {
}) })
} }
/// [op:numeric-unary-minus](https://www.w3.org/TR/xpath-functions/#func-numeric-unary-minus)
#[inline]
pub fn checked_neg(&self) -> Option<Self> {
Some(Self {
value: self.value.checked_neg()?,
})
}
/// [fn:abs](https://www.w3.org/TR/xpath-functions/#func-abs) /// [fn:abs](https://www.w3.org/TR/xpath-functions/#func-abs)
#[inline] #[inline]
pub const fn abs(&self) -> Self { pub const fn abs(&self) -> Self {
@ -174,9 +220,7 @@ impl Decimal {
pub const MAX: Self = Self { value: i128::MAX }; pub const MAX: Self = Self { value: i128::MAX };
#[cfg(test)] #[cfg(test)]
pub(super) const fn step() -> Self { pub const STEP: Self = Self { value: 1 };
Self { value: 1 }
}
} }
impl From<bool> for Decimal { impl From<bool> for Decimal {
@ -316,13 +360,10 @@ impl TryFrom<Double> for Decimal {
#[inline] #[inline]
#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)] #[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
fn try_from(value: Double) -> Result<Self, DecimalOverflowError> { fn try_from(value: Double) -> Result<Self, DecimalOverflowError> {
let shifted = value * Double::from(DECIMAL_PART_POW as f64); let shifted = f64::from(value) * (DECIMAL_PART_POW as f64);
if shifted.is_finite() if shifted.is_finite() && (i128::MIN as f64) <= shifted && shifted <= (i128::MAX as f64) {
&& Double::from(i128::MIN as f64) <= shifted
&& shifted <= Double::from(i128::MAX as f64)
{
Ok(Self { Ok(Self {
value: f64::from(shifted) as i128, value: shifted as i128,
}) })
} else { } else {
Err(DecimalOverflowError) Err(DecimalOverflowError)
@ -334,7 +375,7 @@ impl From<Decimal> for Float {
#[inline] #[inline]
#[allow(clippy::cast_precision_loss)] #[allow(clippy::cast_precision_loss)]
fn from(value: Decimal) -> Self { fn from(value: Decimal) -> Self {
((value.value as f32) / (DECIMAL_PART_POW as f32)).into() Double::from(value).into()
} }
} }
@ -342,7 +383,18 @@ impl From<Decimal> for Double {
#[inline] #[inline]
#[allow(clippy::cast_precision_loss)] #[allow(clippy::cast_precision_loss)]
fn from(value: Decimal) -> Self { fn from(value: Decimal) -> Self {
((value.value as f64) / (DECIMAL_PART_POW as f64)).into() let mut value = value.value;
let mut shift = DECIMAL_PART_POW;
// Hack to improve precision
if value != 0 {
while shift != 1 && value % 10 == 0 {
value /= 10;
shift /= 10;
}
}
((value as f64) / (shift as f64)).into()
} }
} }
@ -374,19 +426,19 @@ impl FromStr for Decimal {
} }
let (sign, mut input) = match input.first() { let (sign, mut input) = match input.first() {
Some(b'+') => (1, &input[1..]), Some(b'+') => (1_i128, &input[1..]),
Some(b'-') => (-1, &input[1..]), Some(b'-') => (-1_i128, &input[1..]),
_ => (1, input), _ => (1, input),
}; };
let mut value = 0_i128; let mut value = 0_i128;
let with_before_dot = input.first().map_or(false, |c| c.is_ascii_digit()); let with_before_dot = input.first().map_or(false, u8::is_ascii_digit);
while let Some(c) = input.first() { while let Some(c) = input.first() {
if c.is_ascii_digit() { if c.is_ascii_digit() {
value = value value = value
.checked_mul(10) .checked_mul(10)
.ok_or(PARSE_OVERFLOW)? .ok_or(PARSE_OVERFLOW)?
.checked_add((*c - b'0').into()) .checked_add(sign * i128::from(*c - b'0'))
.ok_or(PARSE_OVERFLOW)?; .ok_or(PARSE_OVERFLOW)?;
input = &input[1..]; input = &input[1..];
} else { } else {
@ -414,7 +466,7 @@ impl FromStr for Decimal {
value = value value = value
.checked_mul(10) .checked_mul(10)
.ok_or(PARSE_OVERFLOW)? .ok_or(PARSE_OVERFLOW)?
.checked_add((*c - b'0').into()) .checked_add(sign * i128::from(*c - b'0'))
.ok_or(PARSE_OVERFLOW)?; .ok_or(PARSE_OVERFLOW)?;
input = &input[1..]; input = &input[1..];
} else { } else {
@ -431,11 +483,7 @@ impl FromStr for Decimal {
} }
Ok(Self { Ok(Self {
value: value value: value.checked_mul(exp).ok_or(PARSE_OVERFLOW)?,
.checked_mul(sign)
.ok_or(PARSE_OVERFLOW)?
.checked_mul(exp)
.ok_or(PARSE_OVERFLOW)?,
}) })
} }
} }
@ -476,37 +524,38 @@ impl fmt::Display for Decimal {
.find_map(|(i, v)| if v == b'0' { None } else { Some(i) }) .find_map(|(i, v)| if v == b'0' { None } else { Some(i) })
.unwrap_or(40); .unwrap_or(40);
if last_non_zero >= DECIMAL_PART_DIGITS { let decimal_part_digits = usize::try_from(DECIMAL_PART_DIGITS).unwrap();
if last_non_zero >= decimal_part_digits {
let end = if let Some(mut width) = f.width() { let end = if let Some(mut width) = f.width() {
if self.value.is_negative() { if self.value.is_negative() {
width -= 1; width -= 1;
} }
if last_non_zero - DECIMAL_PART_DIGITS + 1 < width { if last_non_zero - decimal_part_digits + 1 < width {
DECIMAL_PART_DIGITS + width decimal_part_digits + width
} else { } else {
last_non_zero + 1 last_non_zero + 1
} }
} else { } else {
last_non_zero + 1 last_non_zero + 1
}; };
for c in digits[DECIMAL_PART_DIGITS..end].iter().rev() { for c in digits[decimal_part_digits..end].iter().rev() {
f.write_char(char::from(*c))?; f.write_char(char::from(*c))?;
} }
} else { } else {
f.write_char('0')? f.write_char('0')?
} }
if DECIMAL_PART_DIGITS > first_non_zero { if decimal_part_digits > first_non_zero {
f.write_char('.')?; f.write_char('.')?;
let start = if let Some(precision) = f.precision() { let start = if let Some(precision) = f.precision() {
if DECIMAL_PART_DIGITS - first_non_zero > precision { if decimal_part_digits - first_non_zero > precision {
DECIMAL_PART_DIGITS - precision decimal_part_digits - precision
} else { } else {
first_non_zero first_non_zero
} }
} else { } else {
first_non_zero first_non_zero
}; };
for c in digits[start..DECIMAL_PART_DIGITS].iter().rev() { for c in digits[start..decimal_part_digits].iter().rev() {
f.write_char(char::from(*c))?; f.write_char(char::from(*c))?;
} }
} }
@ -626,15 +675,7 @@ mod tests {
assert_eq!(Decimal::from_str("0")?.to_string(), "0"); assert_eq!(Decimal::from_str("0")?.to_string(), "0");
assert_eq!(Decimal::from_str("-0")?.to_string(), "0"); assert_eq!(Decimal::from_str("-0")?.to_string(), "0");
assert_eq!(Decimal::from_str(&Decimal::MAX.to_string())?, Decimal::MAX); assert_eq!(Decimal::from_str(&Decimal::MAX.to_string())?, Decimal::MAX);
assert_eq!( assert_eq!(Decimal::from_str(&Decimal::MIN.to_string())?, Decimal::MIN);
Decimal::from_str(
&Decimal::MIN
.checked_add(Decimal::step())
.unwrap()
.to_string()
)?,
Decimal::MIN.checked_add(Decimal::step()).unwrap()
);
assert!(Decimal::from_str("0.0000000000000000001").is_err()); assert!(Decimal::from_str("0.0000000000000000001").is_err());
assert!(Decimal::from_str("1000000000000000000000").is_err()); assert!(Decimal::from_str("1000000000000000000000").is_err());
assert_eq!( assert_eq!(
@ -663,58 +704,101 @@ mod tests {
#[test] #[test]
fn add() { fn add() {
assert!(Decimal::MIN.checked_add(Decimal::step()).is_some()); assert!(Decimal::MIN.checked_add(Decimal::STEP).is_some());
assert!(Decimal::MAX.checked_add(Decimal::step()).is_none()); assert!(Decimal::MAX.checked_add(Decimal::STEP).is_none());
assert_eq!( assert_eq!(
Decimal::MAX.checked_add(Decimal::MIN), Decimal::MAX.checked_add(Decimal::MIN),
Some(-Decimal::step()) Decimal::STEP.checked_neg()
); );
} }
#[test] #[test]
fn sub() { fn sub() {
assert!(Decimal::MIN.checked_sub(Decimal::step()).is_none()); assert!(Decimal::MIN.checked_sub(Decimal::STEP).is_none());
assert!(Decimal::MAX.checked_sub(Decimal::step()).is_some()); assert!(Decimal::MAX.checked_sub(Decimal::STEP).is_some());
} }
#[test] #[test]
fn mul() -> Result<(), ParseDecimalError> { fn mul() -> Result<(), ParseDecimalError> {
assert_eq!(Decimal::from(1).checked_mul(-1), Some(Decimal::from(-1)));
assert_eq!( assert_eq!(
Decimal::from_str("1")?.checked_mul(Decimal::from_str("-1")?), Decimal::from(1000).checked_mul(1000),
Some(Decimal::from_str("-1")?) Some(Decimal::from(1_000_000))
);
assert_eq!(
Decimal::from_str("1000")?.checked_mul(Decimal::from_str("1000")?),
Some(Decimal::from_str("1000000")?)
); );
assert_eq!( assert_eq!(
Decimal::from_str("0.1")?.checked_mul(Decimal::from_str("0.01")?), Decimal::from_str("0.1")?.checked_mul(Decimal::from_str("0.01")?),
Some(Decimal::from_str("0.001")?) Some(Decimal::from_str("0.001")?)
); );
assert_eq!(Decimal::from(0).checked_mul(1), Some(Decimal::from(0)));
assert_eq!(Decimal::from(1).checked_mul(0), Some(Decimal::from(0)));
assert_eq!(Decimal::MAX.checked_mul(1), Some(Decimal::MAX));
assert_eq!(Decimal::MIN.checked_mul(1), Some(Decimal::MIN));
assert_eq!(
Decimal::from(1).checked_mul(Decimal::MAX),
Some(Decimal::MAX)
);
assert_eq!(
Decimal::from(1).checked_mul(Decimal::MIN),
Some(Decimal::MIN)
);
assert_eq!(
Decimal::MAX.checked_mul(-1),
Some(Decimal::MIN.checked_add(Decimal::STEP).unwrap())
);
assert_eq!(Decimal::MIN.checked_mul(-1), None);
assert_eq!(
Decimal::MIN
.checked_add(Decimal::STEP)
.unwrap()
.checked_mul(-1),
Some(Decimal::MAX)
);
Ok(()) Ok(())
} }
#[test] #[test]
fn div() -> Result<(), ParseDecimalError> { fn div() -> Result<(), ParseDecimalError> {
assert_eq!(Decimal::from(1).checked_div(1), Some(Decimal::from(1)));
assert_eq!(Decimal::from(100).checked_div(10), Some(Decimal::from(10)));
assert_eq!( assert_eq!(
Decimal::from_str("1")?.checked_div(Decimal::from_str("1")?), Decimal::from(10).checked_div(100),
Some(Decimal::from_str("1")?) Some(Decimal::from_str("0.1")?)
); );
assert_eq!(Decimal::from(1).checked_div(0), None);
assert_eq!(Decimal::from(0).checked_div(1), Some(Decimal::from(0)));
assert_eq!(Decimal::MAX.checked_div(1), Some(Decimal::MAX));
assert_eq!(Decimal::MIN.checked_div(1), Some(Decimal::MIN));
assert_eq!( assert_eq!(
Decimal::from_str("100")?.checked_div(Decimal::from_str("10")?), Decimal::MAX.checked_div(-1),
Some(Decimal::from_str("10")?) Some(Decimal::MIN.checked_add(Decimal::STEP).unwrap())
); );
assert_eq!(Decimal::MIN.checked_div(-1), None);
assert_eq!( assert_eq!(
Decimal::from_str("10")?.checked_div(Decimal::from_str("100")?), Decimal::MIN
Some(Decimal::from_str("0.1")?) .checked_add(Decimal::STEP)
.unwrap()
.checked_div(-1),
Some(Decimal::MAX)
); );
Ok(()) Ok(())
} }
#[test]
fn rem() -> Result<(), ParseDecimalError> {
assert_eq!(Decimal::from(10).checked_rem(3), Some(Decimal::from(1)));
assert_eq!(Decimal::from(6).checked_rem(-2), Some(Decimal::from(0)));
assert_eq!(
Decimal::from_str("4.5")?.checked_rem(Decimal::from_str("1.2")?),
Some(Decimal::from_str("0.9")?)
);
assert_eq!(Decimal::from(1).checked_rem(0), None);
Ok(())
}
#[test] #[test]
fn round() -> Result<(), ParseDecimalError> { fn round() -> Result<(), ParseDecimalError> {
assert_eq!(Decimal::from_str("10")?.round(), Decimal::from(10)); assert_eq!(Decimal::from(10).round(), Decimal::from(10));
assert_eq!(Decimal::from_str("-10")?.round(), Decimal::from(-10)); assert_eq!(Decimal::from(-10).round(), Decimal::from(-10));
assert_eq!(Decimal::from_str("2.5")?.round(), Decimal::from(3)); assert_eq!(Decimal::from_str("2.5")?.round(), Decimal::from(3));
assert_eq!(Decimal::from_str("2.4999")?.round(), Decimal::from(2)); assert_eq!(Decimal::from_str("2.4999")?.round(), Decimal::from(2));
assert_eq!(Decimal::from_str("-2.5")?.round(), Decimal::from(-2)); assert_eq!(Decimal::from_str("-2.5")?.round(), Decimal::from(-2));
@ -725,8 +809,8 @@ mod tests {
#[test] #[test]
fn ceil() -> Result<(), ParseDecimalError> { fn ceil() -> Result<(), ParseDecimalError> {
assert_eq!(Decimal::from_str("10")?.ceil(), Decimal::from(10)); assert_eq!(Decimal::from(10).ceil(), Decimal::from(10));
assert_eq!(Decimal::from_str("-10")?.ceil(), Decimal::from(-10)); assert_eq!(Decimal::from(-10).ceil(), Decimal::from(-10));
assert_eq!(Decimal::from_str("10.5")?.ceil(), Decimal::from(11)); assert_eq!(Decimal::from_str("10.5")?.ceil(), Decimal::from(11));
assert_eq!(Decimal::from_str("-10.5")?.ceil(), Decimal::from(-10)); assert_eq!(Decimal::from_str("-10.5")?.ceil(), Decimal::from(-10));
assert_eq!(Decimal::from(i64::MIN).ceil(), Decimal::from(i64::MIN)); assert_eq!(Decimal::from(i64::MIN).ceil(), Decimal::from(i64::MIN));
@ -736,8 +820,8 @@ mod tests {
#[test] #[test]
fn floor() -> Result<(), ParseDecimalError> { fn floor() -> Result<(), ParseDecimalError> {
assert_eq!(Decimal::from_str("10")?.ceil(), Decimal::from(10)); assert_eq!(Decimal::from(10).ceil(), Decimal::from(10));
assert_eq!(Decimal::from_str("-10")?.ceil(), Decimal::from(-10)); assert_eq!(Decimal::from(-10).ceil(), Decimal::from(-10));
assert_eq!(Decimal::from_str("10.5")?.floor(), Decimal::from(10)); assert_eq!(Decimal::from_str("10.5")?.floor(), Decimal::from(10));
assert_eq!(Decimal::from_str("-10.5")?.floor(), Decimal::from(-11)); assert_eq!(Decimal::from_str("-10.5")?.floor(), Decimal::from(-11));
assert_eq!(Decimal::from(i64::MIN).floor(), Decimal::from(i64::MIN)); assert_eq!(Decimal::from(i64::MIN).floor(), Decimal::from(i64::MIN));
@ -780,11 +864,11 @@ mod tests {
fn from_float() -> Result<(), ParseDecimalError> { fn from_float() -> Result<(), ParseDecimalError> {
assert_eq!( assert_eq!(
Decimal::try_from(Float::from(0.)).ok(), Decimal::try_from(Float::from(0.)).ok(),
Some(Decimal::from_str("0")?) Some(Decimal::from(0))
); );
assert_eq!( assert_eq!(
Decimal::try_from(Float::from(-0.)).ok(), Decimal::try_from(Float::from(-0.)).ok(),
Some(Decimal::from_str("0.")?) Some(Decimal::from(0))
); );
assert_eq!( assert_eq!(
Decimal::try_from(Float::from(-123.5)).ok(), Decimal::try_from(Float::from(-123.5)).ok(),
@ -796,12 +880,12 @@ mod tests {
assert!(Decimal::try_from(Float::from(f32::MIN)).is_err()); assert!(Decimal::try_from(Float::from(f32::MIN)).is_err());
assert!(Decimal::try_from(Float::from(f32::MAX)).is_err()); assert!(Decimal::try_from(Float::from(f32::MAX)).is_err());
assert!( assert!(
Decimal::try_from(Float::from(1_672_507_302_466.)) Decimal::try_from(Float::from(1_672_507_300_000.))
.unwrap() .unwrap()
.checked_sub(Decimal::from_str("1672507302466")?) .checked_sub(Decimal::from(1_672_507_293_696_i64))
.unwrap() .unwrap()
.abs() .abs()
< Decimal::from(1_000_000) < Decimal::from(1)
); );
Ok(()) Ok(())
} }
@ -810,11 +894,11 @@ mod tests {
fn from_double() -> Result<(), ParseDecimalError> { fn from_double() -> Result<(), ParseDecimalError> {
assert_eq!( assert_eq!(
Decimal::try_from(Double::from(0.)).ok(), Decimal::try_from(Double::from(0.)).ok(),
Some(Decimal::from_str("0")?) Some(Decimal::from(0))
); );
assert_eq!( assert_eq!(
Decimal::try_from(Double::from(-0.)).ok(), Decimal::try_from(Double::from(-0.)).ok(),
Some(Decimal::from_str("0")?) Some(Decimal::from(0))
); );
assert_eq!( assert_eq!(
Decimal::try_from(Double::from(-123.1)).ok(), Decimal::try_from(Double::from(-123.1)).ok(),
@ -823,7 +907,7 @@ mod tests {
assert!( assert!(
Decimal::try_from(Double::from(1_672_507_302_466.)) Decimal::try_from(Double::from(1_672_507_302_466.))
.unwrap() .unwrap()
.checked_sub(Decimal::from_str("1672507302466")?) .checked_sub(Decimal::from(1_672_507_302_466_i64))
.unwrap() .unwrap()
.abs() .abs()
< Decimal::from(1) < Decimal::from(1)
@ -836,6 +920,34 @@ mod tests {
Ok(()) Ok(())
} }
#[test]
fn to_float() -> Result<(), ParseDecimalError> {
assert_eq!(Float::from(Decimal::from(0)), Float::from(0.));
assert_eq!(Float::from(Decimal::from(1)), Float::from(1.));
assert_eq!(Float::from(Decimal::from(10)), Float::from(10.));
assert_eq!(Float::from(Decimal::from_str("0.1")?), Float::from(0.1));
assert!((Float::from(Decimal::MAX) - Float::from(1.701_412e20)).abs() < Float::from(1.));
assert!((Float::from(Decimal::MIN) - Float::from(-1.701_412e20)).abs() < Float::from(1.));
Ok(())
}
#[test]
fn to_double() -> Result<(), ParseDecimalError> {
assert_eq!(Double::from(Decimal::from(0)), Double::from(0.));
assert_eq!(Double::from(Decimal::from(1)), Double::from(1.));
assert_eq!(Double::from(Decimal::from(10)), Double::from(10.));
assert_eq!(Double::from(Decimal::from_str("0.1")?), Double::from(0.1));
assert!(
(Double::from(Decimal::MAX) - Double::from(1.701_411_834_604_692_4e20)).abs()
< Double::from(1.)
);
assert!(
(Double::from(Decimal::MIN) - Double::from(-1.701_411_834_604_692_4e20)).abs()
< Double::from(1.)
);
Ok(())
}
#[test] #[test]
fn minimally_conformant() -> Result<(), ParseDecimalError> { fn minimally_conformant() -> Result<(), ParseDecimalError> {
// All minimally conforming processors must support decimal values whose absolute value can be expressed as i / 10^k, // All minimally conforming processors must support decimal values whose absolute value can be expressed as i / 10^k,

@ -53,6 +53,12 @@ impl Double {
self.value.round().into() self.value.round().into()
} }
#[inline]
pub fn is_nan(self) -> bool {
self.value.is_nan()
}
#[deprecated(note = "Use .is_nan()")]
#[inline] #[inline]
pub fn is_naan(self) -> bool { pub fn is_naan(self) -> bool {
self.value.is_nan() self.value.is_nan()
@ -68,6 +74,20 @@ impl Double {
pub fn is_identical_with(&self, other: &Self) -> bool { pub fn is_identical_with(&self, other: &Self) -> bool {
self.value.to_ne_bytes() == other.value.to_ne_bytes() self.value.to_ne_bytes() == other.value.to_ne_bytes()
} }
pub const MIN: Self = Self { value: f64::MIN };
pub const MAX: Self = Self { value: f64::MAX };
pub const INFINITY: Self = Self {
value: f64::INFINITY,
};
pub const NEG_INFINITY: Self = Self {
value: f64::NEG_INFINITY,
};
pub const NAN: Self = Self { value: f64::NAN };
} }
impl From<Double> for f64 { impl From<Double> for f64 {
@ -243,7 +263,7 @@ mod tests {
#[test] #[test]
fn eq() { fn eq() {
assert_eq!(Double::from(0_f64), Double::from(0_f64)); assert_eq!(Double::from(0_f64), Double::from(0_f64));
assert_ne!(Double::from(f64::NAN), Double::from(f64::NAN)); assert_ne!(Double::NAN, Double::NAN);
assert_eq!(Double::from(-0.), Double::from(0.)); assert_eq!(Double::from(-0.), Double::from(0.));
} }
@ -254,18 +274,15 @@ mod tests {
Some(Ordering::Equal) Some(Ordering::Equal)
); );
assert_eq!( assert_eq!(
Double::from(f64::INFINITY).partial_cmp(&Double::from(f64::MAX)), Double::INFINITY.partial_cmp(&Double::MAX),
Some(Ordering::Greater) Some(Ordering::Greater)
); );
assert_eq!( assert_eq!(
Double::from(f64::NEG_INFINITY).partial_cmp(&Double::from(f64::MIN)), Double::NEG_INFINITY.partial_cmp(&Double::MIN),
Some(Ordering::Less) Some(Ordering::Less)
); );
assert_eq!(Double::from(f64::NAN).partial_cmp(&Double::from(0.)), None); assert_eq!(Double::NAN.partial_cmp(&Double::from(0.)), None);
assert_eq!( assert_eq!(Double::NAN.partial_cmp(&Double::NAN), None);
Double::from(f64::NAN).partial_cmp(&Double::from(f64::NAN)),
None
);
assert_eq!( assert_eq!(
Double::from(0.).partial_cmp(&Double::from(-0.)), Double::from(0.).partial_cmp(&Double::from(-0.)),
Some(Ordering::Equal) Some(Ordering::Equal)
@ -275,7 +292,7 @@ mod tests {
#[test] #[test]
fn is_identical_with() { fn is_identical_with() {
assert!(Double::from(0.).is_identical_with(&Double::from(0.))); assert!(Double::from(0.).is_identical_with(&Double::from(0.)));
assert!(Double::from(f64::NAN).is_identical_with(&Double::from(f64::NAN))); assert!(Double::NAN.is_identical_with(&Double::NAN));
assert!(!Double::from(-0.).is_identical_with(&Double::from(0.))); assert!(!Double::from(-0.).is_identical_with(&Double::from(0.)));
} }
@ -297,11 +314,11 @@ mod tests {
assert_eq!(Double::from_str("-1.")?.to_string(), "-1"); assert_eq!(Double::from_str("-1.")?.to_string(), "-1");
assert_eq!( assert_eq!(
Double::from_str(&f64::MIN.to_string()).unwrap(), Double::from_str(&f64::MIN.to_string()).unwrap(),
Double::from(f64::MIN) Double::MIN
); );
assert_eq!( assert_eq!(
Double::from_str(&f64::MAX.to_string()).unwrap(), Double::from_str(&f64::MAX.to_string()).unwrap(),
Double::from(f64::MAX) Double::MAX
); );
Ok(()) Ok(())
} }

@ -107,6 +107,14 @@ impl Duration {
}) })
} }
#[inline]
pub fn checked_neg(&self) -> Option<Self> {
Some(Self {
year_month: self.year_month.checked_neg()?,
day_time: self.day_time.checked_neg()?,
})
}
/// Checks if the two values are [identical](https://www.w3.org/TR/xmlschema11-2/#identity). /// Checks if the two values are [identical](https://www.w3.org/TR/xmlschema11-2/#identity).
#[inline] #[inline]
pub fn is_identical_with(&self, other: &Self) -> bool { pub fn is_identical_with(&self, other: &Self) -> bool {
@ -127,7 +135,7 @@ impl FromStr for Duration {
type Err = XsdParseError; type Err = XsdParseError;
fn from_str(input: &str) -> Result<Self, XsdParseError> { fn from_str(input: &str) -> Result<Self, XsdParseError> {
parse_value(duration_lexical_rep, input) parse_duration(input)
} }
} }
@ -170,8 +178,10 @@ impl fmt::Display for Duration {
let h = (s_int % 86400) / 3600; let h = (s_int % 86400) / 3600;
let m = (s_int % 3600) / 60; let m = (s_int % 3600) / 60;
let s = ss let s = ss
.checked_sub(Decimal::try_from(d * 86400 + h * 3600 + m * 60).unwrap()) .checked_sub(
.unwrap(); //could not fail Decimal::try_from(d * 86400 + h * 3600 + m * 60).map_err(|_| fmt::Error)?,
)
.ok_or(fmt::Error)?;
if d != 0 { if d != 0 {
write!(f, "{d}D")?; write!(f, "{d}D")?;
@ -299,6 +309,13 @@ impl YearMonthDuration {
}) })
} }
#[inline]
pub fn checked_neg(&self) -> Option<Self> {
Some(Self {
months: self.months.checked_neg()?,
})
}
/// Checks if the two values are [identical](https://www.w3.org/TR/xmlschema11-2/#identity). /// Checks if the two values are [identical](https://www.w3.org/TR/xmlschema11-2/#identity).
#[inline] #[inline]
pub fn is_identical_with(&self, other: &Self) -> bool { pub fn is_identical_with(&self, other: &Self) -> bool {
@ -333,7 +350,7 @@ impl FromStr for YearMonthDuration {
type Err = XsdParseError; type Err = XsdParseError;
fn from_str(input: &str) -> Result<Self, XsdParseError> { fn from_str(input: &str) -> Result<Self, XsdParseError> {
parse_value(year_month_duration_lexical_rep, input) parse_year_month_duration(input)
} }
} }
@ -465,6 +482,13 @@ impl DayTimeDuration {
}) })
} }
#[inline]
pub fn checked_neg(&self) -> Option<Self> {
Some(Self {
seconds: self.seconds.checked_neg()?,
})
}
/// Checks if the two values are [identical](https://www.w3.org/TR/xmlschema11-2/#identity). /// Checks if the two values are [identical](https://www.w3.org/TR/xmlschema11-2/#identity).
#[inline] #[inline]
pub fn is_identical_with(&self, other: &Self) -> bool { pub fn is_identical_with(&self, other: &Self) -> bool {
@ -513,7 +537,7 @@ impl FromStr for DayTimeDuration {
type Err = XsdParseError; type Err = XsdParseError;
fn from_str(input: &str) -> Result<Self, XsdParseError> { fn from_str(input: &str) -> Result<Self, XsdParseError> {
parse_value(day_time_duration_lexical_rep, input) parse_day_time_duration(input)
} }
} }
@ -599,7 +623,7 @@ mod tests {
fn from_str() -> Result<(), XsdParseError> { fn from_str() -> Result<(), XsdParseError> {
let min = Duration::new( let min = Duration::new(
i64::MIN + 1, i64::MIN + 1,
Decimal::MIN.checked_add(Decimal::step()).unwrap(), Decimal::MIN.checked_add(Decimal::STEP).unwrap(),
); );
let max = Duration::new(i64::MAX, Decimal::MAX); let max = Duration::new(i64::MAX, Decimal::MAX);

@ -53,11 +53,17 @@ impl Float {
self.value.round().into() self.value.round().into()
} }
#[deprecated(note = "Use .is_nan()")]
#[inline] #[inline]
pub fn is_naan(self) -> bool { pub fn is_naan(self) -> bool {
self.value.is_nan() self.value.is_nan()
} }
#[inline]
pub fn is_nan(self) -> bool {
self.value.is_nan()
}
#[inline] #[inline]
pub fn is_finite(self) -> bool { pub fn is_finite(self) -> bool {
self.value.is_finite() self.value.is_finite()
@ -68,6 +74,20 @@ impl Float {
pub fn is_identical_with(&self, other: &Self) -> bool { pub fn is_identical_with(&self, other: &Self) -> bool {
self.value.to_ne_bytes() == other.value.to_ne_bytes() self.value.to_ne_bytes() == other.value.to_ne_bytes()
} }
pub const MIN: Self = Self { value: f32::MIN };
pub const MAX: Self = Self { value: f32::MAX };
pub const INFINITY: Self = Self {
value: f32::INFINITY,
};
pub const NEG_INFINITY: Self = Self {
value: f32::NEG_INFINITY,
};
pub const NAN: Self = Self { value: f32::NAN };
} }
impl From<Float> for f32 { impl From<Float> for f32 {
@ -233,7 +253,7 @@ mod tests {
#[test] #[test]
fn eq() { fn eq() {
assert_eq!(Float::from(0.), Float::from(0.)); assert_eq!(Float::from(0.), Float::from(0.));
assert_ne!(Float::from(f32::NAN), Float::from(f32::NAN)); assert_ne!(Float::NAN, Float::NAN);
assert_eq!(Float::from(-0.), Float::from(0.)); assert_eq!(Float::from(-0.), Float::from(0.));
} }
@ -244,18 +264,15 @@ mod tests {
Some(Ordering::Equal) Some(Ordering::Equal)
); );
assert_eq!( assert_eq!(
Float::from(f32::INFINITY).partial_cmp(&Float::from(f32::MAX)), Float::INFINITY.partial_cmp(&Float::MAX),
Some(Ordering::Greater) Some(Ordering::Greater)
); );
assert_eq!( assert_eq!(
Float::from(f32::NEG_INFINITY).partial_cmp(&Float::from(f32::MIN)), Float::NEG_INFINITY.partial_cmp(&Float::MIN),
Some(Ordering::Less) Some(Ordering::Less)
); );
assert_eq!(Float::from(f32::NAN).partial_cmp(&Float::from(0.)), None); assert_eq!(Float::NAN.partial_cmp(&Float::from(0.)), None);
assert_eq!( assert_eq!(Float::NAN.partial_cmp(&Float::NAN), None);
Float::from(f32::NAN).partial_cmp(&Float::from(f32::NAN)),
None
);
assert_eq!( assert_eq!(
Float::from(0.).partial_cmp(&Float::from(-0.)), Float::from(0.).partial_cmp(&Float::from(-0.)),
Some(Ordering::Equal) Some(Ordering::Equal)
@ -265,7 +282,7 @@ mod tests {
#[test] #[test]
fn is_identical_with() { fn is_identical_with() {
assert!(Float::from(0.).is_identical_with(&Float::from(0.))); assert!(Float::from(0.).is_identical_with(&Float::from(0.)));
assert!(Float::from(f32::NAN).is_identical_with(&Float::from(f32::NAN))); assert!(Float::NAN.is_identical_with(&Float::NAN));
assert!(!Float::from(-0.).is_identical_with(&Float::from(0.))); assert!(!Float::from(-0.).is_identical_with(&Float::from(0.)));
} }
@ -285,14 +302,8 @@ mod tests {
assert_eq!(Float::from_str("-1")?.to_string(), "-1"); assert_eq!(Float::from_str("-1")?.to_string(), "-1");
assert_eq!(Float::from_str("1.")?.to_string(), "1"); assert_eq!(Float::from_str("1.")?.to_string(), "1");
assert_eq!(Float::from_str("-1.")?.to_string(), "-1"); assert_eq!(Float::from_str("-1.")?.to_string(), "-1");
assert_eq!( assert_eq!(Float::from_str(&f32::MIN.to_string())?, Float::MIN);
Float::from_str(&f32::MIN.to_string())?, assert_eq!(Float::from_str(&f32::MAX.to_string())?, Float::MAX);
Float::from(f32::MIN)
);
assert_eq!(
Float::from_str(&f32::MAX.to_string())?,
Float::from(f32::MAX)
);
Ok(()) Ok(())
} }
} }

@ -58,6 +58,7 @@ impl Integer {
}) })
} }
/// [op:numeric-mod](https://www.w3.org/TR/xpath-functions/#func-numeric-mod)
#[inline] #[inline]
pub fn checked_rem(&self, rhs: impl Into<Self>) -> Option<Self> { pub fn checked_rem(&self, rhs: impl Into<Self>) -> Option<Self> {
Some(Self { Some(Self {
@ -72,6 +73,14 @@ impl Integer {
}) })
} }
/// [op:numeric-unary-minus](https://www.w3.org/TR/xpath-functions/#func-numeric-unary-minus)
#[inline]
pub fn checked_neg(&self) -> Option<Self> {
Some(Self {
value: self.value.checked_neg()?,
})
}
/// [fn:abs](https://www.w3.org/TR/xpath-functions/#func-abs) /// [fn:abs](https://www.w3.org/TR/xpath-functions/#func-abs)
#[inline] #[inline]
pub const fn abs(&self) -> Self { pub const fn abs(&self) -> Self {
@ -95,6 +104,10 @@ impl Integer {
pub fn is_identical_with(&self, other: &Self) -> bool { pub fn is_identical_with(&self, other: &Self) -> bool {
self == other self == other
} }
pub const MIN: Self = Self { value: i64::MIN };
pub const MAX: Self = Self { value: i64::MAX };
} }
impl From<bool> for Integer { impl From<bool> for Integer {
@ -258,9 +271,9 @@ mod tests {
assert!(Integer::try_from(Float::from(f32::MIN)).is_err()); assert!(Integer::try_from(Float::from(f32::MIN)).is_err());
assert!(Integer::try_from(Float::from(f32::MAX)).is_err()); assert!(Integer::try_from(Float::from(f32::MAX)).is_err());
assert!( assert!(
Integer::try_from(Float::from(1_672_507_302_466.)) Integer::try_from(Float::from(1_672_507_300_000.))
.unwrap() .unwrap()
.checked_sub(Integer::from_str("1672507302466")?) .checked_sub(Integer::from_str("1672507300000")?)
.unwrap() .unwrap()
.abs() .abs()
< Integer::from(1_000_000) < Integer::from(1_000_000)
@ -283,12 +296,12 @@ mod tests {
Some(Integer::from_str("-123")?) Some(Integer::from_str("-123")?)
); );
assert!( assert!(
Integer::try_from(Double::from(1_672_507_302_466.)) Integer::try_from(Double::from(1_672_507_300_000.))
.unwrap() .unwrap()
.checked_sub(Integer::from_str("1672507302466").unwrap()) .checked_sub(Integer::from_str("1672507300000").unwrap())
.unwrap() .unwrap()
.abs() .abs()
< Integer::from(1) < Integer::from(10)
); );
assert!(Integer::try_from(Double::from(f64::NAN)).is_err()); assert!(Integer::try_from(Double::from(f64::NAN)).is_err());
assert!(Integer::try_from(Double::from(f64::INFINITY)).is_err()); assert!(Integer::try_from(Double::from(f64::INFINITY)).is_err());
@ -312,4 +325,40 @@ mod tests {
assert!(Integer::try_from(Decimal::MAX).is_err()); assert!(Integer::try_from(Decimal::MAX).is_err());
Ok(()) Ok(())
} }
#[test]
fn add() {
assert_eq!(
Integer::MIN.checked_add(1),
Some(Integer::from(i64::MIN + 1))
);
assert_eq!(Integer::MAX.checked_add(1), None);
}
#[test]
fn sub() {
assert_eq!(Integer::MIN.checked_sub(1), None);
assert_eq!(
Integer::MAX.checked_sub(1),
Some(Integer::from(i64::MAX - 1))
);
}
#[test]
fn mul() {
assert_eq!(Integer::MIN.checked_mul(2), None);
assert_eq!(Integer::MAX.checked_mul(2), None);
}
#[test]
fn div() {
assert_eq!(Integer::from(1).checked_div(0), None);
}
#[test]
fn rem() {
assert_eq!(Integer::from(10).checked_rem(3), Some(Integer::from(1)));
assert_eq!(Integer::from(6).checked_rem(-2), Some(Integer::from(0)));
assert_eq!(Integer::from(1).checked_rem(0), None);
}
} }

@ -1,9 +1,9 @@
#![doc = include_str!("../README.md")] #![doc = include_str!("../README.md")]
#![deny(unsafe_code)]
#![doc(test(attr(deny(warnings))))] #![doc(test(attr(deny(warnings))))]
#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![doc(html_favicon_url = "https://raw.githubusercontent.com/oxigraph/oxigraph/main/logo.svg")] #![doc(html_favicon_url = "https://raw.githubusercontent.com/oxigraph/oxigraph/main/logo.svg")]
#![doc(html_logo_url = "https://raw.githubusercontent.com/oxigraph/oxigraph/main/logo.svg")] #![doc(html_logo_url = "https://raw.githubusercontent.com/oxigraph/oxigraph/main/logo.svg")]
#![allow(clippy::return_self_not_must_use)]
mod boolean; mod boolean;
mod date_time; mod date_time;

@ -2,15 +2,6 @@ use super::date_time::{DateTimeError, GDay, GMonth, GMonthDay, GYear, GYearMonth
use super::decimal::ParseDecimalError; use super::decimal::ParseDecimalError;
use super::duration::{DayTimeDuration, YearMonthDuration}; use super::duration::{DayTimeDuration, YearMonthDuration};
use super::*; use super::*;
use nom::branch::alt;
use nom::bytes::complete::{tag, take_while, take_while_m_n};
use nom::character::complete::{char, digit0, digit1};
use nom::combinator::{map, opt, recognize};
use nom::error::{ErrorKind, ParseError};
use nom::multi::many1;
use nom::sequence::{preceded, terminated, tuple};
use nom::Err;
use nom::{IResult, Needed};
use std::error::Error; use std::error::Error;
use std::fmt; use std::fmt;
use std::num::ParseIntError; use std::num::ParseIntError;
@ -24,46 +15,35 @@ pub struct XsdParseError {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
enum XsdParseErrorKind { enum XsdParseErrorKind {
NomKind(ErrorKind),
NomChar(char),
MissingData(Needed),
TooMuchData { count: usize },
Overflow,
ParseInt(ParseIntError), ParseInt(ParseIntError),
ParseDecimal(ParseDecimalError), ParseDecimal(ParseDecimalError),
OutOfIntegerRange { value: u8, min: u8, max: u8 },
DateTime(DateTimeError), DateTime(DateTimeError),
Message(&'static str),
} }
const OVERFLOW_ERROR: XsdParseError = XsdParseError {
kind: XsdParseErrorKind::Message("Overflow error"),
};
impl fmt::Display for XsdParseError { impl fmt::Display for XsdParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.kind { match &self.kind {
XsdParseErrorKind::NomKind(kind) => {
write!(f, "Invalid XML Schema value: {}", kind.description())
}
XsdParseErrorKind::NomChar(c) => {
write!(f, "Unexpected character in XML Schema value: '{c}'")
}
XsdParseErrorKind::MissingData(Needed::Unknown) => {
write!(f, "Too small XML Schema value")
}
XsdParseErrorKind::MissingData(Needed::Size(size)) => {
write!(f, "Too small XML Schema value: missing {size} chars")
}
XsdParseErrorKind::TooMuchData { count } => {
write!(f, "Too long XML Schema value: {count} extra chars")
}
XsdParseErrorKind::Overflow => write!(f, "Computation overflow or underflow"),
XsdParseErrorKind::ParseInt(error) => { XsdParseErrorKind::ParseInt(error) => {
write!(f, "Error while parsing integer: {error}") write!(f, "Error while parsing integer: {error}")
} }
XsdParseErrorKind::ParseDecimal(error) => { XsdParseErrorKind::ParseDecimal(error) => {
write!(f, "Error while parsing decimal: {error}") write!(f, "Error while parsing decimal: {error}")
} }
XsdParseErrorKind::OutOfIntegerRange { value, min, max } => {
write!(f, "The integer {value} is not between {min} and {max}")
}
XsdParseErrorKind::DateTime(error) => error.fmt(f), XsdParseErrorKind::DateTime(error) => error.fmt(f),
XsdParseErrorKind::Message(msg) => write!(f, "{msg}"),
}
}
}
impl XsdParseError {
const fn msg(message: &'static str) -> Self {
Self {
kind: XsdParseErrorKind::Message(message),
} }
} }
} }
@ -74,33 +54,11 @@ impl Error for XsdParseError {
XsdParseErrorKind::ParseInt(error) => Some(error), XsdParseErrorKind::ParseInt(error) => Some(error),
XsdParseErrorKind::ParseDecimal(error) => Some(error), XsdParseErrorKind::ParseDecimal(error) => Some(error),
XsdParseErrorKind::DateTime(error) => Some(error), XsdParseErrorKind::DateTime(error) => Some(error),
_ => None, XsdParseErrorKind::Message(_) => None,
} }
} }
} }
impl ParseError<&str> for XsdParseError {
fn from_error_kind(_input: &str, kind: ErrorKind) -> Self {
Self {
kind: XsdParseErrorKind::NomKind(kind),
}
}
fn append(_input: &str, _kind: ErrorKind, other: Self) -> Self {
other
}
fn from_char(_input: &str, c: char) -> Self {
Self {
kind: XsdParseErrorKind::NomChar(c),
}
}
fn or(self, other: Self) -> Self {
other
}
}
impl From<ParseIntError> for XsdParseError { impl From<ParseIntError> for XsdParseError {
fn from(error: ParseIntError) -> Self { fn from(error: ParseIntError) -> Self {
Self { Self {
@ -125,412 +83,538 @@ impl From<DateTimeError> for XsdParseError {
} }
} }
impl From<Err<Self>> for XsdParseError { // [6] duYearFrag ::= unsignedNoDecimalPtNumeral 'Y'
fn from(err: Err<Self>) -> Self { // [7] duMonthFrag ::= unsignedNoDecimalPtNumeral 'M'
match err { // [8] duDayFrag ::= unsignedNoDecimalPtNumeral 'D'
Err::Incomplete(needed) => Self { // [9] duHourFrag ::= unsignedNoDecimalPtNumeral 'H'
kind: XsdParseErrorKind::MissingData(needed), // [10] duMinuteFrag ::= unsignedNoDecimalPtNumeral 'M'
}, // [11] duSecondFrag ::= (unsignedNoDecimalPtNumeral | unsignedDecimalPtNumeral) 'S'
Err::Error(e) | Err::Failure(e) => e, // [12] duYearMonthFrag ::= (duYearFrag duMonthFrag?) | duMonthFrag
} // [13] duTimeFrag ::= 'T' ((duHourFrag duMinuteFrag? duSecondFrag?) | (duMinuteFrag duSecondFrag?) | duSecondFrag)
} // [14] duDayTimeFrag ::= (duDayFrag duTimeFrag?) | duTimeFrag
} // [15] durationLexicalRep ::= '-'? 'P' ((duYearMonthFrag duDayTimeFrag?) | duDayTimeFrag)
struct DurationParts {
type XsdResult<'a, T> = IResult<&'a str, T, XsdParseError>; year_month: Option<i64>,
day_time: Option<Decimal>,
const OVERFLOW_ERROR: XsdParseError = XsdParseError { }
kind: XsdParseErrorKind::Overflow,
}; fn duration_parts(input: &str) -> Result<(DurationParts, &str), XsdParseError> {
// States
pub fn parse_value<'a, T>( const START: u32 = 0;
mut f: impl FnMut(&'a str) -> XsdResult<'a, T>, const AFTER_YEAR: u32 = 1;
input: &'a str, const AFTER_MONTH: u32 = 2;
) -> Result<T, XsdParseError> { const AFTER_DAY: u32 = 3;
let (left, result) = f(input)?; const AFTER_T: u32 = 4;
if left.is_empty() { const AFTER_HOUR: u32 = 5;
Ok(result) const AFTER_MINUTE: u32 = 6;
const AFTER_SECOND: u32 = 7;
let (negative, input) = if let Some(left) = input.strip_prefix('-') {
(true, left)
} else { } else {
Err(XsdParseError { (false, input)
kind: XsdParseErrorKind::TooMuchData { count: left.len() }, };
}) let mut input = expect_char(input, 'P', "Durations must start with 'P'")?;
let mut state = START;
let mut year_month: Option<i64> = None;
let mut day_time: Option<Decimal> = None;
while !input.is_empty() {
if let Some(left) = input.strip_prefix('T') {
if state >= AFTER_T {
return Err(XsdParseError::msg("Duplicated time separator 'T'"));
}
state = AFTER_T;
input = left;
} else {
let (number_str, left) = decimal_prefix(input);
match left.chars().next() {
Some('Y') if state < AFTER_YEAR => {
year_month = Some(
year_month
.unwrap_or_default()
.checked_add(
i64::from_str(number_str)?
.checked_mul(12)
.ok_or(OVERFLOW_ERROR)?,
)
.ok_or(OVERFLOW_ERROR)?,
);
state = AFTER_YEAR;
}
Some('M') if state < AFTER_MONTH => {
year_month = Some(
year_month
.unwrap_or_default()
.checked_add(i64::from_str(number_str)?)
.ok_or(OVERFLOW_ERROR)?,
);
state = AFTER_MONTH;
}
Some('D') if state < AFTER_DAY => {
if number_str.contains('.') {
return Err(XsdParseError::msg(
"Decimal numbers are not allowed for days",
));
}
day_time = Some(
day_time
.unwrap_or_default()
.checked_add(
Decimal::from_str(number_str)?
.checked_mul(86400)
.ok_or(OVERFLOW_ERROR)?,
)
.ok_or(OVERFLOW_ERROR)?,
);
state = AFTER_DAY;
}
Some('H') if state == AFTER_T => {
if number_str.contains('.') {
return Err(XsdParseError::msg(
"Decimal numbers are not allowed for hours",
));
}
day_time = Some(
day_time
.unwrap_or_default()
.checked_add(
Decimal::from_str(number_str)?
.checked_mul(3600)
.ok_or(OVERFLOW_ERROR)?,
)
.ok_or(OVERFLOW_ERROR)?,
);
state = AFTER_HOUR;
}
Some('M') if (AFTER_T..AFTER_MINUTE).contains(&state) => {
if number_str.contains('.') {
return Err(XsdParseError::msg(
"Decimal numbers are not allowed for minutes",
));
}
day_time = Some(
day_time
.unwrap_or_default()
.checked_add(
Decimal::from_str(number_str)?
.checked_mul(60)
.ok_or(OVERFLOW_ERROR)?,
)
.ok_or(OVERFLOW_ERROR)?,
);
state = AFTER_MINUTE;
}
Some('S') if (AFTER_T..AFTER_SECOND).contains(&state) => {
day_time = Some(
day_time
.unwrap_or_default()
.checked_add(Decimal::from_str(number_str)?)
.ok_or(OVERFLOW_ERROR)?,
);
state = AFTER_SECOND;
}
Some(_) => return Err(XsdParseError::msg("Unexpected type character")),
None => {
return Err(XsdParseError::msg(
"Numbers in durations must be followed by a type character",
))
}
}
input = &left[1..];
}
} }
}
//TODO: check every computation
// [6] duYearFrag ::= unsignedNoDecimalPtNumeral 'Y'
fn du_year_frag(input: &str) -> XsdResult<'_, i64> {
terminated(unsigned_no_decimal_pt_numeral, char('Y'))(input)
}
// [7] duMonthFrag ::= unsignedNoDecimalPtNumeral 'M' Ok((
fn du_month_frag(input: &str) -> XsdResult<'_, i64> { DurationParts {
terminated(unsigned_no_decimal_pt_numeral, char('M'))(input) year_month: if let Some(v) = year_month {
Some(if negative {
v.checked_neg().ok_or(OVERFLOW_ERROR)?
} else {
v
})
} else {
None
},
day_time: if let Some(v) = day_time {
Some(if negative {
v.checked_neg().ok_or(OVERFLOW_ERROR)?
} else {
v
})
} else {
None
},
},
input,
))
} }
// [8] duDayFrag ::= unsignedNoDecimalPtNumeral 'D' pub fn parse_duration(input: &str) -> Result<Duration, XsdParseError> {
fn du_day_frag(input: &str) -> XsdResult<'_, i64> { let parts = ensure_complete(input, duration_parts)?;
terminated(unsigned_no_decimal_pt_numeral, char('D'))(input) if parts.year_month.is_none() && parts.day_time.is_none() {
return Err(XsdParseError::msg("Empty duration"));
}
Ok(Duration::new(
parts.year_month.unwrap_or(0),
parts.day_time.unwrap_or_default(),
))
}
pub fn parse_year_month_duration(input: &str) -> Result<YearMonthDuration, XsdParseError> {
let parts = ensure_complete(input, duration_parts)?;
if parts.day_time.is_some() {
return Err(XsdParseError::msg(
"There must not be any day or time component in a yearMonthDuration",
));
}
Ok(YearMonthDuration::new(parts.year_month.ok_or(
XsdParseError::msg("No year and month values found"),
)?))
} }
// [9] duHourFrag ::= unsignedNoDecimalPtNumeral 'H' pub fn parse_day_time_duration(input: &str) -> Result<DayTimeDuration, XsdParseError> {
fn du_hour_frag(input: &str) -> XsdResult<'_, i64> { let parts = ensure_complete(input, duration_parts)?;
terminated(unsigned_no_decimal_pt_numeral, char('H'))(input) if parts.year_month.is_some() {
return Err(XsdParseError::msg(
"There must not be any year or month component in a dayTimeDuration",
));
}
Ok(DayTimeDuration::new(parts.day_time.ok_or(
XsdParseError::msg("No day or time values found"),
)?))
} }
// [10] duMinuteFrag ::= unsignedNoDecimalPtNumeral 'M' // [16] dateTimeLexicalRep ::= yearFrag '-' monthFrag '-' dayFrag 'T' ((hourFrag ':' minuteFrag ':' secondFrag) | endOfDayFrag) timezoneFrag?
fn du_minute_frag(input: &str) -> XsdResult<'_, i64> { fn date_time_lexical_rep(input: &str) -> Result<(DateTime, &str), XsdParseError> {
terminated(unsigned_no_decimal_pt_numeral, char('M'))(input) let (year, input) = year_frag(input)?;
let input = expect_char(input, '-', "The year and month must be separated by '-'")?;
let (month, input) = month_frag(input)?;
let input = expect_char(input, '-', "The month and day must be separated by '-'")?;
let (day, input) = day_frag(input)?;
let input = expect_char(input, 'T', "The date and time must be separated by 'T'")?;
let (hour, input) = hour_frag(input)?;
let input = expect_char(input, ':', "The hours and minutes must be separated by ':'")?;
let (minute, input) = minute_frag(input)?;
let input = expect_char(
input,
':',
"The minutes and seconds must be separated by ':'",
)?;
let (second, input) = second_frag(input)?;
// We validate 24:00:00
if hour == 24 && minute != 0 && second != Decimal::from(0) {
return Err(XsdParseError::msg(
"Times are not allowed to be after 24:00:00",
));
}
let (timezone_offset, input) = optional_end(input, timezone_frag)?;
Ok((
DateTime::new(year, month, day, hour, minute, second, timezone_offset)?,
input,
))
} }
// [11] duSecondFrag ::= (unsignedNoDecimalPtNumeral | unsignedDecimalPtNumeral) 'S' pub fn parse_date_time(input: &str) -> Result<DateTime, XsdParseError> {
fn du_second_frag(input: &str) -> XsdResult<'_, Decimal> { ensure_complete(input, date_time_lexical_rep)
terminated(
map_res(
recognize(tuple((digit0, opt(preceded(char('.'), digit0))))),
Decimal::from_str,
),
char('S'),
)(input)
} }
// [12] duYearMonthFrag ::= (duYearFrag duMonthFrag?) | duMonthFrag // [17] timeLexicalRep ::= ((hourFrag ':' minuteFrag ':' secondFrag) | endOfDayFrag) timezoneFrag?
fn du_year_month_frag(input: &str) -> XsdResult<'_, i64> { fn time_lexical_rep(input: &str) -> Result<(Time, &str), XsdParseError> {
alt(( let (hour, input) = hour_frag(input)?;
map(tuple((du_year_frag, opt(du_month_frag))), |(y, m)| { let input = expect_char(input, ':', "The hours and minutes must be separated by ':'")?;
12 * y + m.unwrap_or(0) let (minute, input) = minute_frag(input)?;
}), let input = expect_char(
du_month_frag, input,
))(input) ':',
"The minutes and seconds must be separated by ':'",
)?;
let (second, input) = second_frag(input)?;
// We validate 24:00:00
if hour == 24 && minute != 0 && second != Decimal::from(0) {
return Err(XsdParseError::msg(
"Times are not allowed to be after 24:00:00",
));
}
let (timezone_offset, input) = optional_end(input, timezone_frag)?;
Ok((Time::new(hour, minute, second, timezone_offset)?, input))
} }
// [13] duTimeFrag ::= 'T' ((duHourFrag duMinuteFrag? duSecondFrag?) | (duMinuteFrag duSecondFrag?) | duSecondFrag) pub fn parse_time(input: &str) -> Result<Time, XsdParseError> {
fn du_time_frag(input: &str) -> XsdResult<'_, Decimal> { ensure_complete(input, time_lexical_rep)
preceded(
char('T'),
alt((
map_res(
tuple((du_hour_frag, opt(du_minute_frag), opt(du_second_frag))),
|(h, m, s)| {
Decimal::from(3600 * h + 60 * m.unwrap_or(0))
.checked_add(s.unwrap_or_default())
.ok_or(OVERFLOW_ERROR)
},
),
map_res(tuple((du_minute_frag, opt(du_second_frag))), |(m, s)| {
Decimal::from(m * 60)
.checked_add(s.unwrap_or_default())
.ok_or(OVERFLOW_ERROR)
}),
du_second_frag,
)),
)(input)
} }
// [14] duDayTimeFrag ::= (duDayFrag duTimeFrag?) | duTimeFrag // [18] dateLexicalRep ::= yearFrag '-' monthFrag '-' dayFrag timezoneFrag? Constraint: Day-of-month Representations
fn du_day_time_frag(input: &str) -> XsdResult<'_, Decimal> { fn date_lexical_rep(input: &str) -> Result<(Date, &str), XsdParseError> {
alt(( let (year, input) = year_frag(input)?;
map_res(tuple((du_day_frag, opt(du_time_frag))), |(d, t)| { let input = expect_char(input, '-', "The year and month must be separated by '-'")?;
Decimal::from(d) let (month, input) = month_frag(input)?;
.checked_mul(Decimal::from(86400)) let input = expect_char(input, '-', "The month and day must be separated by '-'")?;
.ok_or(OVERFLOW_ERROR)? let (day, input) = day_frag(input)?;
.checked_add(t.unwrap_or_default()) let (timezone_offset, input) = optional_end(input, timezone_frag)?;
.ok_or(OVERFLOW_ERROR) Ok((Date::new(year, month, day, timezone_offset)?, input))
}),
du_time_frag,
))(input)
} }
// [15] durationLexicalRep ::= '-'? 'P' ((duYearMonthFrag duDayTimeFrag?) | duDayTimeFrag) pub fn parse_date(input: &str) -> Result<Date, XsdParseError> {
pub fn duration_lexical_rep(input: &str) -> XsdResult<'_, Duration> { ensure_complete(input, date_lexical_rep)
map(
tuple((
opt(char('-')),
preceded(
char('P'),
alt((
map(
tuple((du_year_month_frag, opt(du_day_time_frag))),
|(y, d)| Duration::new(y, d.unwrap_or_default()),
),
map(du_day_time_frag, |d| Duration::new(0, d)),
)),
),
)),
|(sign, duration)| {
if sign == Some('-') {
-duration
} else {
duration
}
},
)(input)
} }
// [16] dateTimeLexicalRep ::= yearFrag '-' monthFrag '-' dayFrag 'T' ((hourFrag ':' minuteFrag ':' secondFrag) | endOfDayFrag) timezoneFrag? // [19] gYearMonthLexicalRep ::= yearFrag '-' monthFrag timezoneFrag?
pub fn date_time_lexical_rep(input: &str) -> XsdResult<'_, DateTime> { fn g_year_month_lexical_rep(input: &str) -> Result<(GYearMonth, &str), XsdParseError> {
map_res( let (year, input) = year_frag(input)?;
tuple(( let input = expect_char(input, '-', "The year and month must be separated by '-'")?;
year_frag, let (month, input) = month_frag(input)?;
char('-'), let (timezone_offset, input) = optional_end(input, timezone_frag)?;
month_frag, Ok((GYearMonth::new(year, month, timezone_offset)?, input))
char('-'),
day_frag,
char('T'),
alt((
map(
tuple((hour_frag, char(':'), minute_frag, char(':'), second_frag)),
|(h, _, m, _, s)| (h, m, s),
),
end_of_day_frag,
)),
opt(timezone_frag),
)),
|(year, _, month, _, day, _, (hours, minutes, seconds), timezone)| {
DateTime::new(year, month, day, hours, minutes, seconds, timezone)
},
)(input)
} }
// [17] timeLexicalRep ::= ((hourFrag ':' minuteFrag ':' secondFrag) | endOfDayFrag) timezoneFrag? pub fn parse_g_year_month(input: &str) -> Result<GYearMonth, XsdParseError> {
pub fn time_lexical_rep(input: &str) -> XsdResult<'_, Time> { ensure_complete(input, g_year_month_lexical_rep)
map_res(
tuple((
alt((
map(
tuple((hour_frag, char(':'), minute_frag, char(':'), second_frag)),
|(h, _, m, _, s)| (h, m, s),
),
end_of_day_frag,
)),
opt(timezone_frag),
)),
|((hours, minutes, seconds), timezone)| Time::new(hours, minutes, seconds, timezone),
)(input)
} }
// [18] dateLexicalRep ::= yearFrag '-' monthFrag '-' dayFrag timezoneFrag? Constraint: Day-of-month Representations // [20] gYearLexicalRep ::= yearFrag timezoneFrag?
pub fn date_lexical_rep(input: &str) -> XsdResult<'_, Date> { fn g_year_lexical_rep(input: &str) -> Result<(GYear, &str), XsdParseError> {
map_res( let (year, input) = year_frag(input)?;
tuple(( let (timezone_offset, input) = optional_end(input, timezone_frag)?;
year_frag, Ok((GYear::new(year, timezone_offset)?, input))
char('-'),
month_frag,
char('-'),
day_frag,
opt(timezone_frag),
)),
|(year, _, month, _, day, timezone)| Date::new(year, month, day, timezone),
)(input)
} }
// [19] gYearMonthLexicalRep ::= yearFrag '-' monthFrag timezoneFrag? pub fn parse_g_year(input: &str) -> Result<GYear, XsdParseError> {
pub fn g_year_month_lexical_rep(input: &str) -> XsdResult<'_, GYearMonth> { ensure_complete(input, g_year_lexical_rep)
map_res(
tuple((year_frag, char('-'), month_frag, opt(timezone_frag))),
|(year, _, month, timezone)| GYearMonth::new(year, month, timezone),
)(input)
} }
// [20] gYearLexicalRep ::= yearFrag timezoneFrag? // [21] gMonthDayLexicalRep ::= '--' monthFrag '-' dayFrag timezoneFrag? Constraint: Day-of-month Representations
pub fn g_year_lexical_rep(input: &str) -> XsdResult<'_, GYear> { fn g_month_day_lexical_rep(input: &str) -> Result<(GMonthDay, &str), XsdParseError> {
map_res( let input = expect_char(input, '-', "gMonthDay values must start with '--'")?;
tuple((year_frag, opt(timezone_frag))), let input = expect_char(input, '-', "gMonthDay values must start with '--'")?;
|(year, timezone)| GYear::new(year, timezone), let (month, input) = month_frag(input)?;
)(input) let input = expect_char(input, '-', "The month and day must be separated by '-'")?;
let (day, input) = day_frag(input)?;
let (timezone_offset, input) = optional_end(input, timezone_frag)?;
Ok((GMonthDay::new(month, day, timezone_offset)?, input))
} }
// [21] gMonthDayLexicalRep ::= '--' monthFrag '-' dayFrag timezoneFrag? Constraint: Day-of-month Representations pub fn parse_g_month_day(input: &str) -> Result<GMonthDay, XsdParseError> {
pub fn g_month_day_lexical_rep(input: &str) -> XsdResult<'_, GMonthDay> { ensure_complete(input, g_month_day_lexical_rep)
map_res(
tuple((
char('-'),
char('-'),
month_frag,
char('-'),
day_frag,
opt(timezone_frag),
)),
|(_, _, month, _, day, timezone)| GMonthDay::new(month, day, timezone),
)(input)
} }
// [22] gDayLexicalRep ::= '---' dayFrag timezoneFrag? // [22] gDayLexicalRep ::= '---' dayFrag timezoneFrag?
pub fn g_day_lexical_rep(input: &str) -> XsdResult<'_, GDay> { fn g_day_lexical_rep(input: &str) -> Result<(GDay, &str), XsdParseError> {
map_res( let input = expect_char(input, '-', "gDay values must start with '---'")?;
tuple(( let input = expect_char(input, '-', "gDay values must start with '---'")?;
char('-'), let input = expect_char(input, '-', "gDay values must start with '---'")?;
char('-'), let (day, input) = day_frag(input)?;
char('-'), let (timezone_offset, input) = optional_end(input, timezone_frag)?;
day_frag, Ok((GDay::new(day, timezone_offset)?, input))
opt(timezone_frag),
)),
|(_, _, _, day, timezone)| GDay::new(day, timezone),
)(input)
} }
// [23] gMonthLexicalRep ::= '--' monthFrag timezoneFrag? pub fn parse_g_day(input: &str) -> Result<GDay, XsdParseError> {
pub fn g_month_lexical_rep(input: &str) -> XsdResult<'_, GMonth> { ensure_complete(input, g_day_lexical_rep)
map_res(
tuple((char('-'), char('-'), month_frag, opt(timezone_frag))),
|(_, _, month, timezone)| GMonth::new(month, timezone),
)(input)
}
// [42] yearMonthDurationLexicalRep ::= '-'? 'P' duYearMonthFrag
pub fn year_month_duration_lexical_rep(input: &str) -> XsdResult<'_, YearMonthDuration> {
map(
tuple((opt(char('-')), preceded(char('P'), du_year_month_frag))),
|(sign, duration)| {
YearMonthDuration::new(if sign == Some('-') {
-duration
} else {
duration
})
},
)(input)
} }
// [43] dayTimeDurationLexicalRep ::= '-'? 'P' duDayTimeFrag // [23] gMonthLexicalRep ::= '--' monthFrag timezoneFrag?
pub fn day_time_duration_lexical_rep(input: &str) -> XsdResult<'_, DayTimeDuration> { fn g_month_lexical_rep(input: &str) -> Result<(GMonth, &str), XsdParseError> {
map( let input = expect_char(input, '-', "gMonth values must start with '--'")?;
tuple((opt(char('-')), preceded(char('P'), du_day_time_frag))), let input = expect_char(input, '-', "gMonth values must start with '--'")?;
|(sign, duration)| { let (month, input) = month_frag(input)?;
DayTimeDuration::new(if sign == Some('-') { let (timezone_offset, input) = optional_end(input, timezone_frag)?;
-duration Ok((GMonth::new(month, timezone_offset)?, input))
} else {
duration
})
},
)(input)
} }
// [46] unsignedNoDecimalPtNumeral ::= digit+ pub fn parse_g_month(input: &str) -> Result<GMonth, XsdParseError> {
fn unsigned_no_decimal_pt_numeral(input: &str) -> XsdResult<'_, i64> { ensure_complete(input, g_month_lexical_rep)
map_res(digit1, i64::from_str)(input)
} }
// [56] yearFrag ::= '-'? (([1-9] digit digit digit+)) | ('0' digit digit digit)) // [56] yearFrag ::= '-'? (([1-9] digit digit digit+)) | ('0' digit digit digit))
fn year_frag(input: &str) -> XsdResult<'_, i64> { fn year_frag(input: &str) -> Result<(i64, &str), XsdParseError> {
map_res( let (sign, input) = if let Some(left) = input.strip_prefix('-') {
recognize(tuple(( (-1, left)
opt(char('-')), } else {
take_while_m_n(4, usize::MAX, |c: char| c.is_ascii_digit()), (1, input)
))), };
i64::from_str, let (number_str, input) = integer_prefix(input);
)(input) let number = i64::from_str(number_str)?;
if number < 1000 && number_str.len() != 4 {
return Err(XsdParseError::msg(
"The years below 1000 must be encoded on exactly 4 digits",
));
}
Ok((sign * number, input))
} }
// [57] monthFrag ::= ('0' [1-9]) | ('1' [0-2]) // [57] monthFrag ::= ('0' [1-9]) | ('1' [0-2])
fn month_frag(input: &str) -> XsdResult<'_, u8> { fn month_frag(input: &str) -> Result<(u8, &str), XsdParseError> {
map_res(take_while_m_n(2, 2, |c: char| c.is_ascii_digit()), |v| { let (number_str, input) = integer_prefix(input);
parsed_u8_range(v, 1, 12) if number_str.len() != 2 {
})(input) return Err(XsdParseError::msg("Month must be encoded with two digits"));
}
let number = u8::from_str(number_str)?;
if !(1..=12).contains(&number) {
return Err(XsdParseError::msg("Month must be between 01 and 12"));
}
Ok((number, input))
} }
// [58] dayFrag ::= ('0' [1-9]) | ([12] digit) | ('3' [01]) // [58] dayFrag ::= ('0' [1-9]) | ([12] digit) | ('3' [01])
fn day_frag(input: &str) -> XsdResult<'_, u8> { fn day_frag(input: &str) -> Result<(u8, &str), XsdParseError> {
map_res(take_while_m_n(2, 2, |c: char| c.is_ascii_digit()), |v| { let (number_str, input) = integer_prefix(input);
parsed_u8_range(v, 1, 31) if number_str.len() != 2 {
})(input) return Err(XsdParseError::msg("Day must be encoded with two digits"));
}
let number = u8::from_str(number_str)?;
if !(1..=31).contains(&number) {
return Err(XsdParseError::msg("Day must be between 01 and 31"));
}
Ok((number, input))
} }
// [59] hourFrag ::= ([01] digit) | ('2' [0-3]) // [59] hourFrag ::= ([01] digit) | ('2' [0-3])
fn hour_frag(input: &str) -> XsdResult<'_, u8> { // We also allow 24 for ease of parsing
map_res(take_while_m_n(2, 2, |c: char| c.is_ascii_digit()), |v| { fn hour_frag(input: &str) -> Result<(u8, &str), XsdParseError> {
parsed_u8_range(v, 0, 23) let (number_str, input) = integer_prefix(input);
})(input) if number_str.len() != 2 {
return Err(XsdParseError::msg("Hours must be encoded with two digits"));
}
let number = u8::from_str(number_str)?;
if !(0..=24).contains(&number) {
return Err(XsdParseError::msg("Hours must be between 00 and 24"));
}
Ok((number, input))
} }
// [60] minuteFrag ::= [0-5] digit // [60] minuteFrag ::= [0-5] digit
fn minute_frag(input: &str) -> XsdResult<'_, u8> { fn minute_frag(input: &str) -> Result<(u8, &str), XsdParseError> {
map_res(take_while_m_n(2, 2, |c: char| c.is_ascii_digit()), |v| { let (number_str, input) = integer_prefix(input);
parsed_u8_range(v, 0, 59) if number_str.len() != 2 {
})(input) return Err(XsdParseError::msg(
"Minutes must be encoded with two digits",
));
}
let number = u8::from_str(number_str)?;
if !(0..=59).contains(&number) {
return Err(XsdParseError::msg("Minutes must be between 00 and 59"));
}
Ok((number, input))
} }
// [61] secondFrag ::= ([0-5] digit) ('.' digit+)? // [61] secondFrag ::= ([0-5] digit) ('.' digit+)?
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] fn second_frag(input: &str) -> Result<(Decimal, &str), XsdParseError> {
fn second_frag(input: &str) -> XsdResult<'_, Decimal> { let (number_str, input) = decimal_prefix(input);
map_res( let (before_dot_str, _) = number_str.split_once('.').unwrap_or((number_str, ""));
recognize(tuple(( if before_dot_str.len() != 2 {
take_while_m_n(2, 2, |c: char| c.is_ascii_digit()), return Err(XsdParseError::msg(
opt(preceded( "Seconds must be encoded with two digits",
char('.'), ));
take_while(|c: char| c.is_ascii_digit()), }
)), let number = Decimal::from_str(number_str)?;
))), if number < Decimal::from(0) || number >= Decimal::from(60) {
|v| { return Err(XsdParseError::msg("Seconds must be between 00 and 60"));
let value = Decimal::from_str(v)?; }
if Decimal::from(0) <= value && value < Decimal::from(60) { if number_str.ends_with('.') {
Ok(value) return Err(XsdParseError::msg(
} else { "Seconds are not allowed to end with a dot",
Err(XsdParseError { ));
kind: XsdParseErrorKind::OutOfIntegerRange { }
value: value.as_i128() as u8, Ok((number, input))
min: 0,
max: 60,
},
})
}
},
)(input)
} }
// [62] endOfDayFrag ::= '24:00:00' ('.' '0'+)? // [63] timezoneFrag ::= 'Z' | ('+' | '-') (('0' digit | '1' [0-3]) ':' minuteFrag | '14:00')
fn end_of_day_frag(input: &str) -> XsdResult<'_, (u8, u8, Decimal)> { fn timezone_frag(input: &str) -> Result<(TimezoneOffset, &str), XsdParseError> {
map( if let Some(left) = input.strip_prefix('Z') {
recognize(tuple(( return Ok((TimezoneOffset::UTC, left));
tag("24:00:00"), }
opt(preceded(char('.'), many1(char('0')))), let (sign, input) = if let Some(left) = input.strip_prefix('-') {
))), (-1, left)
|_| (24, 0, 0.into()), } else if let Some(left) = input.strip_prefix('+') {
)(input) (1, left)
} else {
(1, input)
};
let (hour_str, input) = integer_prefix(input);
if hour_str.len() != 2 {
return Err(XsdParseError::msg(
"The timezone hours must be encoded with two digits",
));
}
let hours = i16::from_str(hour_str)?;
let input = expect_char(
input,
':',
"The timezone hours and minutes must be separated by ':'",
)?;
let (minutes, input) = minute_frag(input)?;
if hours > 13 && !(hours == 14 && minutes == 0) {
return Err(XsdParseError::msg(
"The timezone hours must be between 00 and 13",
));
}
Ok((
TimezoneOffset::new(sign * (hours * 60 + i16::from(minutes)))?,
input,
))
} }
// [63] timezoneFrag ::= 'Z' | ('+' | '-') (('0' digit | '1' [0-3]) ':' minuteFrag | '14:00') fn ensure_complete<T>(
fn timezone_frag(input: &str) -> XsdResult<'_, TimezoneOffset> { input: &str,
alt(( parse: impl FnOnce(&str) -> Result<(T, &str), XsdParseError>,
map(char('Z'), |_| TimezoneOffset::UTC), ) -> Result<T, XsdParseError> {
map_res( let (result, left) = parse(input)?;
tuple(( if !left.is_empty() {
alt((map(char('+'), |_| 1), map(char('-'), |_| -1))), return Err(XsdParseError::msg("Unrecognized value suffix"));
alt(( }
map( Ok(result)
tuple(( }
map_res(take_while_m_n(2, 2, |c: char| c.is_ascii_digit()), |v| {
parsed_u8_range(v, 0, 13) fn expect_char<'a>(
}), input: &'a str,
char(':'), constant: char,
minute_frag, error_message: &'static str,
)), ) -> Result<&'a str, XsdParseError> {
|(hours, _, minutes)| i16::from(hours) * 60 + i16::from(minutes), if let Some(left) = input.strip_prefix(constant) {
), Ok(left)
map(tag("14:00"), |_| 14 * 60),
)),
)),
|(sign, value)| TimezoneOffset::new(sign * value),
),
))(input)
}
fn parsed_u8_range(input: &str, min: u8, max: u8) -> Result<u8, XsdParseError> {
let value = u8::from_str(input)?;
if min <= value && value <= max {
Ok(value)
} else { } else {
Err(XsdParseError { Err(XsdParseError::msg(error_message))
kind: XsdParseErrorKind::OutOfIntegerRange { value, min, max },
})
} }
} }
fn map_res<'a, O1, O2, E2: Into<XsdParseError>>( fn integer_prefix(input: &str) -> (&str, &str) {
mut first: impl FnMut(&'a str) -> XsdResult<'a, O1>, let mut end = input.len();
mut second: impl FnMut(O1) -> Result<O2, E2>, for (i, c) in input.char_indices() {
) -> impl FnMut(&'a str) -> XsdResult<'a, O2> { if !c.is_ascii_digit() {
move |input| { end = i;
let (input, o1) = first(input)?; break;
Ok((input, second(o1).map_err(|e| Err::Error(e.into()))?)) }
}
input.split_at(end)
}
fn decimal_prefix(input: &str) -> (&str, &str) {
let mut end = input.len();
let mut dot_seen = false;
for (i, c) in input.char_indices() {
if c.is_ascii_digit() {
// Ok
} else if c == '.' && !dot_seen {
dot_seen = true;
} else {
end = i;
break;
}
} }
input.split_at(end)
}
fn optional_end<T>(
input: &str,
parse: impl FnOnce(&str) -> Result<(T, &str), XsdParseError>,
) -> Result<(Option<T>, &str), XsdParseError> {
Ok(if input.is_empty() {
(None, input)
} else {
let (result, input) = parse(input)?;
(Some(result), input)
})
} }

@ -1,6 +1,6 @@
[package] [package]
name = "sparesults" name = "sparesults"
version = "0.1.7" version = "0.1.8-dev"
authors = ["Tpt <thomas@pellissier-tanon.fr>"] authors = ["Tpt <thomas@pellissier-tanon.fr>"]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
readme = "README.md" readme = "README.md"
@ -19,7 +19,7 @@ rdf-star = ["oxrdf/rdf-star"]
[dependencies] [dependencies]
json-event-parser = "0.1" json-event-parser = "0.1"
oxrdf = { version = "0.1.5", path="../oxrdf" } oxrdf = { version = "0.1.6-dev", path="../oxrdf" }
quick-xml = "0.28" quick-xml = "0.28"
[package.metadata.docs.rs] [package.metadata.docs.rs]

@ -160,7 +160,7 @@ fn write_tsv_term<'a>(term: impl Into<TermRef<'a>>, sink: &mut impl Write) -> io
let value = literal.value(); let value = literal.value();
if let Some(language) = literal.language() { if let Some(language) = literal.language() {
write_tsv_quoted_str(value, sink)?; write_tsv_quoted_str(value, sink)?;
write!(sink, "@{}", language) write!(sink, "@{language}")
} else { } else {
match literal.datatype() { match literal.datatype() {
xsd::BOOLEAN if is_turtle_boolean(value) => sink.write_all(value.as_bytes()), xsd::BOOLEAN if is_turtle_boolean(value) => sink.write_all(value.as_bytes()),
@ -216,7 +216,7 @@ fn is_turtle_integer(value: &str) -> bool {
} else if let Some(v) = value.strip_prefix(b"-") { } else if let Some(v) = value.strip_prefix(b"-") {
value = v; value = v;
} }
!value.is_empty() && value.iter().all(|c| c.is_ascii_digit()) !value.is_empty() && value.iter().all(u8::is_ascii_digit)
} }
fn is_turtle_decimal(value: &str) -> bool { fn is_turtle_decimal(value: &str) -> bool {
@ -227,7 +227,7 @@ fn is_turtle_decimal(value: &str) -> bool {
} else if let Some(v) = value.strip_prefix(b"-") { } else if let Some(v) = value.strip_prefix(b"-") {
value = v; value = v;
} }
while value.first().map_or(false, |c| c.is_ascii_digit()) { while value.first().map_or(false, u8::is_ascii_digit) {
value = &value[1..]; value = &value[1..];
} }
if let Some(v) = value.strip_prefix(b".") { if let Some(v) = value.strip_prefix(b".") {
@ -235,7 +235,7 @@ fn is_turtle_decimal(value: &str) -> bool {
} else { } else {
return false; return false;
} }
!value.is_empty() && value.iter().all(|c| c.is_ascii_digit()) !value.is_empty() && value.iter().all(u8::is_ascii_digit)
} }
fn is_turtle_double(value: &str) -> bool { fn is_turtle_double(value: &str) -> bool {
@ -248,14 +248,14 @@ fn is_turtle_double(value: &str) -> bool {
value = v; value = v;
} }
let mut with_before = false; let mut with_before = false;
while value.first().map_or(false, |c| c.is_ascii_digit()) { while value.first().map_or(false, u8::is_ascii_digit) {
value = &value[1..]; value = &value[1..];
with_before = true; with_before = true;
} }
let mut with_after = false; let mut with_after = false;
if let Some(v) = value.strip_prefix(b".") { if let Some(v) = value.strip_prefix(b".") {
value = v; value = v;
while value.first().map_or(false, |c| c.is_ascii_digit()) { while value.first().map_or(false, u8::is_ascii_digit) {
value = &value[1..]; value = &value[1..];
with_after = true; with_after = true;
} }
@ -272,7 +272,7 @@ fn is_turtle_double(value: &str) -> bool {
} else if let Some(v) = value.strip_prefix(b"-") { } else if let Some(v) = value.strip_prefix(b"-") {
value = v; value = v;
} }
(with_before || with_after) && !value.is_empty() && value.iter().all(|c| c.is_ascii_digit()) (with_before || with_after) && !value.is_empty() && value.iter().all(u8::is_ascii_digit)
} }
pub enum TsvQueryResultsReader<R: BufRead> { pub enum TsvQueryResultsReader<R: BufRead> {

@ -1,5 +1,4 @@
#![doc = include_str!("../README.md")] #![doc = include_str!("../README.md")]
#![deny(unsafe_code)]
#![doc(test(attr(deny(warnings))))] #![doc(test(attr(deny(warnings))))]
#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![doc(html_favicon_url = "https://raw.githubusercontent.com/oxigraph/oxigraph/main/logo.svg")] #![doc(html_favicon_url = "https://raw.githubusercontent.com/oxigraph/oxigraph/main/logo.svg")]
@ -276,6 +275,7 @@ pub enum QueryResultsReader<R: BufRead> {
/// } /// }
/// # Result::<(),sparesults::ParseError>::Ok(()) /// # Result::<(),sparesults::ParseError>::Ok(())
/// ``` /// ```
#[allow(clippy::rc_buffer)]
pub struct SolutionsReader<R: BufRead> { pub struct SolutionsReader<R: BufRead> {
variables: Rc<Vec<Variable>>, variables: Rc<Vec<Variable>>,
solutions: SolutionsReaderKind<R>, solutions: SolutionsReaderKind<R>,
@ -318,7 +318,7 @@ impl<R: BufRead> Iterator for SolutionsReader<R> {
SolutionsReaderKind::Tsv(reader) => reader.read_next(), SolutionsReaderKind::Tsv(reader) => reader.read_next(),
} }
.transpose()? .transpose()?
.map(|values| (self.variables.clone(), values).into()), .map(|values| (Rc::clone(&self.variables), values).into()),
) )
} }
} }

@ -18,6 +18,7 @@ use std::rc::Rc;
/// assert_eq!(solution.get("foo"), Some(&Literal::from(1).into())); // Get the value of the variable ?foo if it exists (here yes). /// assert_eq!(solution.get("foo"), Some(&Literal::from(1).into())); // Get the value of the variable ?foo if it exists (here yes).
/// assert_eq!(solution.get(1), None); // Get the value of the second column if it exists (here no). /// assert_eq!(solution.get(1), None); // Get the value of the second column if it exists (here no).
/// ``` /// ```
#[allow(clippy::rc_buffer)]
pub struct QuerySolution { pub struct QuerySolution {
variables: Rc<Vec<Variable>>, variables: Rc<Vec<Variable>>,
values: Vec<Option<Term>>, values: Vec<Option<Term>>,
@ -69,7 +70,7 @@ impl QuerySolution {
/// ``` /// ```
#[inline] #[inline]
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.values.iter().all(|v| v.is_none()) self.values.iter().all(Option::is_none)
} }
/// Returns an iterator over bound variables. /// Returns an iterator over bound variables.

@ -186,6 +186,7 @@ impl<R: BufRead> XmlQueryResultsReader<R> {
//Read header //Read header
loop { loop {
buffer.clear();
let event = reader.read_event_into(&mut buffer)?; let event = reader.read_event_into(&mut buffer)?;
match event { match event {
Event::Start(event) => match state { Event::Start(event) => match state {
@ -275,7 +276,6 @@ impl<R: BufRead> XmlQueryResultsReader<R> {
Event::Eof => return Err(SyntaxError::msg("Unexpected early file end. All results file should have a <head> and a <result> or <boolean> tag").into()), Event::Eof => return Err(SyntaxError::msg("Unexpected early file end. All results file should have a <head> and a <result> or <boolean> tag").into()),
_ => (), _ => (),
} }
buffer.clear();
} }
} }
} }
@ -315,6 +315,7 @@ impl<R: BufRead> XmlSolutionsReader<R> {
let mut lang = None; let mut lang = None;
let mut datatype = None; let mut datatype = None;
loop { loop {
self.buffer.clear();
let event = self.reader.read_event_into(&mut self.buffer)?; let event = self.reader.read_event_into(&mut self.buffer)?;
match event { match event {
Event::Start(event) => match state { Event::Start(event) => match state {
@ -482,20 +483,31 @@ impl<R: BufRead> XmlSolutionsReader<R> {
} }
state = State::Triple; state = State::Triple;
} }
State::Uri => state = self.stack.pop().unwrap(), State::Uri => {
state = self
.stack
.pop()
.ok_or_else(|| SyntaxError::msg("Empty stack"))?
}
State::BNode => { State::BNode => {
if term.is_none() { if term.is_none() {
//We default to a random bnode //We default to a random bnode
term = Some(BlankNode::default().into()) term = Some(BlankNode::default().into())
} }
state = self.stack.pop().unwrap() state = self
.stack
.pop()
.ok_or_else(|| SyntaxError::msg("Empty stack"))?
} }
State::Literal => { State::Literal => {
if term.is_none() { if term.is_none() {
//We default to the empty literal //We default to the empty literal
term = Some(build_literal("", lang.take(), datatype.take())?.into()) term = Some(build_literal("", lang.take(), datatype.take())?.into())
} }
state = self.stack.pop().unwrap(); state = self
.stack
.pop()
.ok_or_else(|| SyntaxError::msg("Empty stack"))?;
} }
State::Triple => { State::Triple => {
#[cfg(feature = "rdf-star")] #[cfg(feature = "rdf-star")]
@ -530,7 +542,10 @@ impl<R: BufRead> XmlSolutionsReader<R> {
) )
.into(), .into(),
); );
state = self.stack.pop().unwrap(); state = self
.stack
.pop()
.ok_or_else(|| SyntaxError::msg("Empty stack"))?;
} else { } else {
return Err( return Err(
SyntaxError::msg("A <triple> should contain a <subject>, a <predicate> and an <object>").into() SyntaxError::msg("A <triple> should contain a <subject>, a <predicate> and an <object>").into()
@ -549,7 +564,6 @@ impl<R: BufRead> XmlSolutionsReader<R> {
Event::Eof => return Ok(None), Event::Eof => return Ok(None),
_ => (), _ => (),
} }
self.buffer.clear();
} }
} }
} }

@ -1,6 +1,6 @@
[package] [package]
name = "spargebra" name = "spargebra"
version = "0.2.7" version = "0.2.8-dev"
authors = ["Tpt <thomas@pellissier-tanon.fr>"] authors = ["Tpt <thomas@pellissier-tanon.fr>"]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
readme = "README.md" readme = "README.md"
@ -24,7 +24,7 @@ peg = "0.8"
rand = "0.8" rand = "0.8"
oxiri = "0.2" oxiri = "0.2"
oxilangtag = "0.1" oxilangtag = "0.1"
oxrdf = { version = "0.1.5", path="../oxrdf" } oxrdf = { version = "0.1.6-dev", path="../oxrdf" }
[package.metadata.docs.rs] [package.metadata.docs.rs]
all-features = true all-features = true

@ -1,5 +1,4 @@
#![doc = include_str!("../README.md")] #![doc = include_str!("../README.md")]
#![deny(unsafe_code)]
#![doc(test(attr(deny(warnings))))] #![doc(test(attr(deny(warnings))))]
#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![doc(html_favicon_url = "https://raw.githubusercontent.com/oxigraph/oxigraph/main/logo.svg")] #![doc(html_favicon_url = "https://raw.githubusercontent.com/oxigraph/oxigraph/main/logo.svg")]

File diff suppressed because it is too large Load Diff

@ -577,6 +577,7 @@ pub enum GroundTermPattern {
NamedNode(NamedNode), NamedNode(NamedNode),
Literal(Literal), Literal(Literal),
Variable(Variable), Variable(Variable),
#[cfg(feature = "rdf-star")]
Triple(Box<GroundTriplePattern>), Triple(Box<GroundTriplePattern>),
} }
@ -587,6 +588,7 @@ impl GroundTermPattern {
Self::NamedNode(term) => write!(f, "{term}"), Self::NamedNode(term) => write!(f, "{term}"),
Self::Literal(term) => write!(f, "{term}"), Self::Literal(term) => write!(f, "{term}"),
Self::Variable(var) => write!(f, "{var}"), Self::Variable(var) => write!(f, "{var}"),
#[cfg(feature = "rdf-star")]
Self::Triple(triple) => triple.fmt_sse(f), Self::Triple(triple) => triple.fmt_sse(f),
} }
} }
@ -599,6 +601,7 @@ impl fmt::Display for GroundTermPattern {
Self::NamedNode(term) => term.fmt(f), Self::NamedNode(term) => term.fmt(f),
Self::Literal(term) => term.fmt(f), Self::Literal(term) => term.fmt(f),
Self::Variable(var) => var.fmt(f), Self::Variable(var) => var.fmt(f),
#[cfg(feature = "rdf-star")]
Self::Triple(triple) => write!(f, "<<{triple}>>"), Self::Triple(triple) => write!(f, "<<{triple}>>"),
} }
} }
@ -618,6 +621,7 @@ impl From<Literal> for GroundTermPattern {
} }
} }
#[cfg(feature = "rdf-star")]
impl From<GroundTriplePattern> for GroundTermPattern { impl From<GroundTriplePattern> for GroundTermPattern {
#[inline] #[inline]
fn from(triple: GroundTriplePattern) -> Self { fn from(triple: GroundTriplePattern) -> Self {
@ -818,6 +822,7 @@ pub struct GroundTriplePattern {
impl GroundTriplePattern { impl GroundTriplePattern {
/// Formats using the [SPARQL S-Expression syntax](https://jena.apache.org/documentation/notes/sse.html). /// Formats using the [SPARQL S-Expression syntax](https://jena.apache.org/documentation/notes/sse.html).
#[allow(dead_code)]
pub(crate) fn fmt_sse(&self, f: &mut impl Write) -> fmt::Result { pub(crate) fn fmt_sse(&self, f: &mut impl Write) -> fmt::Result {
write!(f, "(triple ")?; write!(f, "(triple ")?;
self.subject.fmt_sse(f)?; self.subject.fmt_sse(f)?;

@ -1,6 +1,6 @@
[package] [package]
name = "sparql-smith" name = "sparql-smith"
version = "0.1.0-alpha.3" version = "0.1.0-alpha.4-dev"
authors = ["Tpt <thomas@pellissier-tanon.fr>"] authors = ["Tpt <thomas@pellissier-tanon.fr>"]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
readme = "README.md" readme = "README.md"
@ -14,6 +14,8 @@ edition = "2021"
[features] [features]
default = [] default = []
limit-offset = ["order"]
order = []
sep-0006 = [] sep-0006 = []
[dependencies] [dependencies]

@ -1,6 +1,5 @@
use arbitrary::{Arbitrary, Result, Unstructured}; use arbitrary::{Arbitrary, Result, Unstructured};
use std::fmt; use std::fmt;
use std::fmt::Debug;
use std::iter::once; use std::iter::once;
use std::ops::ControlFlow; use std::ops::ControlFlow;
@ -30,8 +29,12 @@ const LITERALS: [&str; 11] = [
"1e0", "1e0",
]; ];
#[derive(Arbitrary)]
pub struct Query { pub struct Query {
inner: QueryContent,
}
#[derive(Arbitrary)]
struct QueryContent {
// [1] QueryUnit ::= Query // [1] QueryUnit ::= Query
// [2] Query ::= Prologue ( SelectQuery | ConstructQuery | DescribeQuery | AskQuery ) ValuesClause // [2] Query ::= Prologue ( SelectQuery | ConstructQuery | DescribeQuery | AskQuery ) ValuesClause
variant: QueryVariant, variant: QueryVariant,
@ -44,16 +47,34 @@ enum QueryVariant {
//TODO: Other variants! //TODO: Other variants!
} }
impl<'a> Arbitrary<'a> for Query {
fn arbitrary(u: &mut Unstructured<'a>) -> Result<Self> {
Ok(Self {
inner: QueryContent::arbitrary(u)?,
})
}
fn arbitrary_take_rest(u: Unstructured<'a>) -> Result<Self> {
Ok(Self {
inner: QueryContent::arbitrary_take_rest(u)?,
})
}
fn size_hint(_depth: usize) -> (usize, Option<usize>) {
(20, None)
}
}
impl fmt::Display for Query { impl fmt::Display for Query {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.variant { match &self.inner.variant {
QueryVariant::Select(s) => write!(f, "{s}"), QueryVariant::Select(s) => write!(f, "{s}"),
}?; }?;
write!(f, "{}", self.values_clause) write!(f, "{}", self.inner.values_clause)
} }
} }
impl Debug for Query { impl fmt::Debug for Query {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self, f) fmt::Display::fmt(self, f)
} }
@ -169,7 +190,9 @@ struct SolutionModifier {
// [18] SolutionModifier ::= GroupClause? HavingClause? OrderClause? LimitOffsetClauses? // [18] SolutionModifier ::= GroupClause? HavingClause? OrderClause? LimitOffsetClauses?
group: Option<GroupClause>, group: Option<GroupClause>,
having: Option<HavingClause>, having: Option<HavingClause>,
#[cfg(feature = "order")]
order: Option<OrderClause>, order: Option<OrderClause>,
#[cfg(feature = "limit-offset")]
limit_offset: Option<LimitOffsetClauses>, limit_offset: Option<LimitOffsetClauses>,
} }
@ -181,9 +204,11 @@ impl fmt::Display for SolutionModifier {
if let Some(having) = &self.having { if let Some(having) = &self.having {
write!(f, " {having}")?; write!(f, " {having}")?;
} }
#[cfg(feature = "order")]
if let Some(order) = &self.order { if let Some(order) = &self.order {
write!(f, " {order}")?; write!(f, " {order}")?;
} }
#[cfg(feature = "limit-offset")]
if let Some(limit_offset) = &self.limit_offset { if let Some(limit_offset) = &self.limit_offset {
write!(f, " {limit_offset}")?; write!(f, " {limit_offset}")?;
} }
@ -254,6 +279,7 @@ impl fmt::Display for HavingClause {
// [22] HavingCondition ::= Constraint // [22] HavingCondition ::= Constraint
type HavingCondition = Constraint; type HavingCondition = Constraint;
#[cfg(feature = "order")]
#[derive(Arbitrary)] #[derive(Arbitrary)]
struct OrderClause { struct OrderClause {
// [23] OrderClause ::= 'ORDER' 'BY' OrderCondition+ // [23] OrderClause ::= 'ORDER' 'BY' OrderCondition+
@ -261,6 +287,7 @@ struct OrderClause {
others: Vec<OrderCondition>, others: Vec<OrderCondition>,
} }
#[cfg(feature = "order")]
impl fmt::Display for OrderClause { impl fmt::Display for OrderClause {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "ORDER BY {}", self.start)?; write!(f, "ORDER BY {}", self.start)?;
@ -271,6 +298,7 @@ impl fmt::Display for OrderClause {
} }
} }
#[cfg(feature = "order")]
#[derive(Arbitrary)] #[derive(Arbitrary)]
enum OrderCondition { enum OrderCondition {
// [24] OrderCondition ::= ( ( 'ASC' | 'DESC' ) BrackettedExpression ) | ( Constraint | Var ) // [24] OrderCondition ::= ( ( 'ASC' | 'DESC' ) BrackettedExpression ) | ( Constraint | Var )
@ -282,6 +310,7 @@ enum OrderCondition {
Var(Var), Var(Var),
} }
#[cfg(feature = "order")]
impl fmt::Display for OrderCondition { impl fmt::Display for OrderCondition {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
@ -298,6 +327,7 @@ impl fmt::Display for OrderCondition {
} }
} }
#[cfg(feature = "limit-offset")]
#[derive(Arbitrary)] #[derive(Arbitrary)]
enum LimitOffsetClauses { enum LimitOffsetClauses {
// [25] LimitOffsetClauses ::= LimitClause OffsetClause? | OffsetClause LimitClause? // [25] LimitOffsetClauses ::= LimitClause OffsetClause? | OffsetClause LimitClause?
@ -305,6 +335,7 @@ enum LimitOffsetClauses {
OffsetLimit(OffsetClause, Option<LimitClause>), OffsetLimit(OffsetClause, Option<LimitClause>),
} }
#[cfg(feature = "limit-offset")]
impl fmt::Display for LimitOffsetClauses { impl fmt::Display for LimitOffsetClauses {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
@ -316,24 +347,28 @@ impl fmt::Display for LimitOffsetClauses {
} }
} }
#[cfg(feature = "limit-offset")]
#[derive(Arbitrary)] #[derive(Arbitrary)]
struct LimitClause { struct LimitClause {
// [26] LimitClause ::= 'LIMIT' INTEGER // [26] LimitClause ::= 'LIMIT' INTEGER
value: u8, value: u8,
} }
#[cfg(feature = "limit-offset")]
impl fmt::Display for LimitClause { impl fmt::Display for LimitClause {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "LIMIT {}", self.value) write!(f, "LIMIT {}", self.value)
} }
} }
#[cfg(feature = "limit-offset")]
#[derive(Arbitrary)] #[derive(Arbitrary)]
struct OffsetClause { struct OffsetClause {
// [27] OffsetClause ::= 'OFFSET' INTEGER // [27] OffsetClause ::= 'OFFSET' INTEGER
value: u8, value: u8,
} }
#[cfg(feature = "limit-offset")]
impl fmt::Display for OffsetClause { impl fmt::Display for OffsetClause {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "OFFSET {}", self.value) write!(f, "OFFSET {}", self.value)

@ -45,12 +45,14 @@ impl Error for ParseError {
} }
} }
#[allow(clippy::fallible_impl_from)]
impl From<TurtleError> for ParseError { impl From<TurtleError> for ParseError {
#[inline] #[inline]
fn from(error: TurtleError) -> Self { fn from(error: TurtleError) -> Self {
let error = io::Error::from(error); let error = io::Error::from(error);
if error.get_ref().map_or(false, |e| e.is::<TurtleError>()) { if error.get_ref().map_or(
false,
<(dyn Error + Send + Sync + 'static)>::is::<TurtleError>,
) {
Self::Syntax(SyntaxError { Self::Syntax(SyntaxError {
inner: SyntaxErrorKind::Turtle(*error.into_inner().unwrap().downcast().unwrap()), inner: SyntaxErrorKind::Turtle(*error.into_inner().unwrap().downcast().unwrap()),
}) })
@ -60,12 +62,14 @@ impl From<TurtleError> for ParseError {
} }
} }
#[allow(clippy::fallible_impl_from)]
impl From<RdfXmlError> for ParseError { impl From<RdfXmlError> for ParseError {
#[inline] #[inline]
fn from(error: RdfXmlError) -> Self { fn from(error: RdfXmlError) -> Self {
let error = io::Error::from(error); let error = io::Error::from(error);
if error.get_ref().map_or(false, |e| e.is::<RdfXmlError>()) { if error.get_ref().map_or(
false,
<(dyn Error + Send + Sync + 'static)>::is::<RdfXmlError>,
) {
Self::Syntax(SyntaxError { Self::Syntax(SyntaxError {
inner: SyntaxErrorKind::RdfXml(*error.into_inner().unwrap().downcast().unwrap()), inner: SyntaxErrorKind::RdfXml(*error.into_inner().unwrap().downcast().unwrap()),
}) })

@ -3,7 +3,7 @@
#![doc(html_logo_url = "https://raw.githubusercontent.com/oxigraph/oxigraph/main/logo.svg")] #![doc(html_logo_url = "https://raw.githubusercontent.com/oxigraph/oxigraph/main/logo.svg")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![doc(test(attr(deny(warnings))))] #![doc(test(attr(deny(warnings))))]
#![deny(unsafe_code)] #![allow(clippy::return_self_not_must_use)]
pub mod io; pub mod io;
pub mod sparql; pub mod sparql;

@ -40,7 +40,7 @@ impl DatasetView {
) -> impl Iterator<Item = Result<EncodedQuad, EvaluationError>> + 'static { ) -> impl Iterator<Item = Result<EncodedQuad, EvaluationError>> + 'static {
self.reader self.reader
.quads_for_pattern(subject, predicate, object, graph_name) .quads_for_pattern(subject, predicate, object, graph_name)
.map(|t| t.map_err(|e| e.into())) .map(|t| t.map_err(Into::into))
} }
#[allow(clippy::needless_collect)] #[allow(clippy::needless_collect)]

File diff suppressed because it is too large Load Diff

@ -160,6 +160,7 @@ impl<R: BufRead + 'static> From<QueryResultsReader<R>> for QueryResults {
/// } /// }
/// # Result::<_,Box<dyn std::error::Error>>::Ok(()) /// # Result::<_,Box<dyn std::error::Error>>::Ok(())
/// ``` /// ```
#[allow(clippy::rc_buffer)]
pub struct QuerySolutionIter { pub struct QuerySolutionIter {
variables: Rc<Vec<Variable>>, variables: Rc<Vec<Variable>>,
iter: Box<dyn Iterator<Item = Result<QuerySolution, EvaluationError>>>, iter: Box<dyn Iterator<Item = Result<QuerySolution, EvaluationError>>>,
@ -171,8 +172,10 @@ impl QuerySolutionIter {
iter: impl Iterator<Item = Result<Vec<Option<Term>>, EvaluationError>> + 'static, iter: impl Iterator<Item = Result<Vec<Option<Term>>, EvaluationError>> + 'static,
) -> Self { ) -> Self {
Self { Self {
variables: variables.clone(), variables: Rc::clone(&variables),
iter: Box::new(iter.map(move |t| t.map(|values| (variables.clone(), values).into()))), iter: Box::new(
iter.map(move |t| t.map(|values| (Rc::clone(&variables), values).into())),
),
} }
} }

@ -13,7 +13,7 @@ use std::rc::Rc;
use std::time::Duration; use std::time::Duration;
use std::{fmt, io}; use std::{fmt, io};
#[derive(Debug)] #[derive(Debug, Clone)]
pub enum PlanNode { pub enum PlanNode {
StaticBindings { StaticBindings {
encoded_tuples: Vec<EncodedTuple>, encoded_tuples: Vec<EncodedTuple>,
@ -22,7 +22,7 @@ pub enum PlanNode {
}, },
Service { Service {
service_name: PatternValue, service_name: PatternValue,
variables: Rc<Vec<Variable>>, variables: Rc<[Variable]>,
child: Rc<Self>, child: Rc<Self>,
graph_pattern: Rc<GraphPattern>, graph_pattern: Rc<GraphPattern>,
silent: bool, silent: bool,
@ -71,7 +71,7 @@ pub enum PlanNode {
ForLoopLeftJoin { ForLoopLeftJoin {
left: Rc<Self>, left: Rc<Self>,
right: Rc<Self>, right: Rc<Self>,
possible_problem_vars: Rc<Vec<usize>>, //Variables that should not be part of the entry of the left join possible_problem_vars: Rc<[usize]>, //Variables that should not be part of the entry of the left join
}, },
Extend { Extend {
child: Rc<Self>, child: Rc<Self>,
@ -99,13 +99,13 @@ pub enum PlanNode {
}, },
Project { Project {
child: Rc<Self>, child: Rc<Self>,
mapping: Rc<Vec<(PlanVariable, PlanVariable)>>, // pairs of (variable key in child, variable key in output) mapping: Rc<[(PlanVariable, PlanVariable)]>, // pairs of (variable key in child, variable key in output)
}, },
Aggregate { Aggregate {
// By definition the group by key are the range 0..key_mapping.len() // By definition the group by key are the range 0..key_mapping.len()
child: Rc<Self>, child: Rc<Self>,
key_variables: Rc<Vec<PlanVariable>>, key_variables: Rc<[PlanVariable]>,
aggregates: Rc<Vec<(PlanAggregation, PlanVariable)>>, aggregates: Rc<[(PlanAggregation, PlanVariable)]>,
}, },
} }
@ -236,7 +236,10 @@ impl PlanNode {
match self { match self {
Self::StaticBindings { encoded_tuples, .. } => { Self::StaticBindings { encoded_tuples, .. } => {
let mut variables = BTreeMap::default(); // value true iff always bound let mut variables = BTreeMap::default(); // value true iff always bound
let max_tuple_length = encoded_tuples.iter().map(|t| t.capacity()).fold(0, max); let max_tuple_length = encoded_tuples
.iter()
.map(EncodedTuple::capacity)
.fold(0, max);
for tuple in encoded_tuples { for tuple in encoded_tuples {
for key in 0..max_tuple_length { for key in 0..max_tuple_length {
match variables.entry(key) { match variables.entry(key) {
@ -444,8 +447,8 @@ pub enum PlanExpression {
Literal(PlanTerm<Literal>), Literal(PlanTerm<Literal>),
Variable(PlanVariable), Variable(PlanVariable),
Exists(Rc<PlanNode>), Exists(Rc<PlanNode>),
Or(Box<Self>, Box<Self>), Or(Vec<Self>),
And(Box<Self>, Box<Self>), And(Vec<Self>),
Equal(Box<Self>, Box<Self>), Equal(Box<Self>, Box<Self>),
Greater(Box<Self>, Box<Self>), Greater(Box<Self>, Box<Self>),
GreaterOrEqual(Box<Self>, Box<Self>), GreaterOrEqual(Box<Self>, Box<Self>),
@ -594,9 +597,7 @@ impl PlanExpression {
| Self::YearMonthDurationCast(e) | Self::YearMonthDurationCast(e)
| Self::DayTimeDurationCast(e) | Self::DayTimeDurationCast(e)
| Self::StringCast(e) => e.lookup_used_variables(callback), | Self::StringCast(e) => e.lookup_used_variables(callback),
Self::Or(a, b) Self::Equal(a, b)
| Self::And(a, b)
| Self::Equal(a, b)
| Self::Greater(a, b) | Self::Greater(a, b)
| Self::GreaterOrEqual(a, b) | Self::GreaterOrEqual(a, b)
| Self::Less(a, b) | Self::Less(a, b)
@ -636,7 +637,11 @@ impl PlanExpression {
c.lookup_used_variables(callback); c.lookup_used_variables(callback);
d.lookup_used_variables(callback); d.lookup_used_variables(callback);
} }
Self::Concat(es) | Self::Coalesce(es) | Self::CustomFunction(_, es) => { Self::Or(es)
| Self::And(es)
| Self::Concat(es)
| Self::Coalesce(es)
| Self::CustomFunction(_, es) => {
for e in es { for e in es {
e.lookup_used_variables(callback); e.lookup_used_variables(callback);
} }
@ -649,6 +654,7 @@ impl PlanExpression {
} }
impl fmt::Display for PlanExpression { impl fmt::Display for PlanExpression {
#[allow(clippy::many_single_char_names)]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
Self::Variable(v) => { Self::Variable(v) => {
@ -719,8 +725,26 @@ impl fmt::Display for PlanExpression {
Self::YearMonthDurationCast(e) => write!(f, "YearMonthDurationCast({e})"), Self::YearMonthDurationCast(e) => write!(f, "YearMonthDurationCast({e})"),
Self::DayTimeDurationCast(e) => write!(f, "DayTimeDurationCast({e})"), Self::DayTimeDurationCast(e) => write!(f, "DayTimeDurationCast({e})"),
Self::StringCast(e) => write!(f, "StringCast({e})"), Self::StringCast(e) => write!(f, "StringCast({e})"),
Self::Or(a, b) => write!(f, "Or({a}, {b})"), Self::Or(es) => {
Self::And(a, b) => write!(f, "And({a}, {b})"), write!(f, "Or(")?;
for (i, e) in es.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{e}")?;
}
write!(f, ")")
}
Self::And(es) => {
write!(f, "And(")?;
for (i, e) in es.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{e}")?;
}
write!(f, ")")
}
Self::Equal(a, b) => write!(f, "Equal({a}, {b})"), Self::Equal(a, b) => write!(f, "Equal({a}, {b})"),
Self::Greater(a, b) => write!(f, "Greater({a}, {b})"), Self::Greater(a, b) => write!(f, "Greater({a}, {b})"),
Self::GreaterOrEqual(a, b) => write!(f, "GreaterOrEqual({a}, {b})"), Self::GreaterOrEqual(a, b) => write!(f, "GreaterOrEqual({a}, {b})"),
@ -838,7 +862,7 @@ pub enum PlanAggregationFunction {
Max, Max,
Avg, Avg,
Sample, Sample,
GroupConcat { separator: Rc<String> }, GroupConcat { separator: Rc<str> },
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -850,7 +874,7 @@ pub enum PlanPropertyPath {
ZeroOrMore(Rc<Self>), ZeroOrMore(Rc<Self>),
OneOrMore(Rc<Self>), OneOrMore(Rc<Self>),
ZeroOrOne(Rc<Self>), ZeroOrOne(Rc<Self>),
NegatedPropertySet(Rc<Vec<PlanTerm<NamedNode>>>), NegatedPropertySet(Rc<[PlanTerm<NamedNode>]>),
} }
impl fmt::Display for PlanPropertyPath { impl fmt::Display for PlanPropertyPath {
@ -1046,7 +1070,7 @@ impl PlanNodeWithStats {
"Aggregate({})", "Aggregate({})",
key_variables key_variables
.iter() .iter()
.map(|c| c.to_string()) .map(ToString::to_string)
.chain(aggregates.iter().map(|(agg, v)| format!("{agg} -> {v}"))) .chain(aggregates.iter().map(|(agg, v)| format!("{agg} -> {v}")))
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(", ") .join(", ")
@ -1107,7 +1131,7 @@ impl PlanNodeWithStats {
format!( format!(
"Sort({})", "Sort({})",
by.iter() by.iter()
.map(|c| c.to_string()) .map(ToString::to_string)
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(", ") .join(", ")
) )
@ -1117,7 +1141,7 @@ impl PlanNodeWithStats {
"StaticBindings({})", "StaticBindings({})",
variables variables
.iter() .iter()
.map(|v| v.to_string()) .map(ToString::to_string)
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(", ") .join(", ")
) )

@ -105,10 +105,9 @@ impl<'a> PlanBuilder<'a> {
let left = self.build_for_graph_pattern(left, variables, graph_name)?; let left = self.build_for_graph_pattern(left, variables, graph_name)?;
let right = self.build_for_graph_pattern(right, variables, graph_name)?; let right = self.build_for_graph_pattern(right, variables, graph_name)?;
let mut possible_problem_vars = BTreeSet::new(); if self.with_optimizations && Self::can_use_for_loop_left_join(&right) {
Self::add_left_join_problematic_variables(&right, &mut possible_problem_vars); let mut possible_problem_vars = BTreeSet::new();
if self.with_optimizations { Self::add_left_join_problematic_variables(&right, &mut possible_problem_vars);
// TODO: don't use if SERVICE is inside of for loop
//We add the extra filter if needed //We add the extra filter if needed
let right = if let Some(expr) = expression { let right = if let Some(expr) = expression {
@ -122,7 +121,7 @@ impl<'a> PlanBuilder<'a> {
PlanNode::ForLoopLeftJoin { PlanNode::ForLoopLeftJoin {
left: Rc::new(left), left: Rc::new(left),
right: Rc::new(right), right: Rc::new(right),
possible_problem_vars: Rc::new(possible_problem_vars.into_iter().collect()), possible_problem_vars: possible_problem_vars.into_iter().collect(),
} }
} else { } else {
PlanNode::HashLeftJoin { PlanNode::HashLeftJoin {
@ -191,7 +190,7 @@ impl<'a> PlanBuilder<'a> {
let service_name = self.pattern_value_from_named_node_or_variable(name, variables); let service_name = self.pattern_value_from_named_node_or_variable(name, variables);
PlanNode::Service { PlanNode::Service {
service_name, service_name,
variables: Rc::new(variables.clone()), variables: Rc::from(variables.as_slice()),
child: Rc::new(child), child: Rc::new(child),
graph_pattern: Rc::new(inner.as_ref().clone()), graph_pattern: Rc::new(inner.as_ref().clone()),
silent: *silent, silent: *silent,
@ -203,22 +202,19 @@ impl<'a> PlanBuilder<'a> {
aggregates, aggregates,
} => PlanNode::Aggregate { } => PlanNode::Aggregate {
child: Rc::new(self.build_for_graph_pattern(inner, variables, graph_name)?), child: Rc::new(self.build_for_graph_pattern(inner, variables, graph_name)?),
key_variables: Rc::new( key_variables: by
by.iter() .iter()
.map(|k| build_plan_variable(variables, k)) .map(|k| build_plan_variable(variables, k))
.collect(), .collect(),
), aggregates: aggregates
aggregates: Rc::new( .iter()
aggregates .map(|(v, a)| {
.iter() Ok((
.map(|(v, a)| { self.build_for_aggregate(a, variables, graph_name)?,
Ok(( build_plan_variable(variables, v),
self.build_for_aggregate(a, variables, graph_name)?, ))
build_plan_variable(variables, v), })
)) .collect::<Result<_, EvaluationError>>()?,
})
.collect::<Result<Vec<_>, EvaluationError>>()?,
),
}, },
GraphPattern::Values { GraphPattern::Values {
variables: table_variables, variables: table_variables,
@ -283,21 +279,19 @@ impl<'a> PlanBuilder<'a> {
&mut inner_variables, &mut inner_variables,
&inner_graph_name, &inner_graph_name,
)?), )?),
mapping: Rc::new( mapping: projection
projection .iter()
.iter() .enumerate()
.enumerate() .map(|(new_variable, variable)| {
.map(|(new_variable, variable)| { (
( PlanVariable {
PlanVariable { encoded: new_variable,
encoded: new_variable, plain: variable.clone(),
plain: variable.clone(), },
}, build_plan_variable(variables, variable),
build_plan_variable(variables, variable), )
) })
}) .collect(),
.collect(),
),
} }
} }
GraphPattern::Distinct { inner } => PlanNode::HashDeduplicate { GraphPattern::Distinct { inner } => PlanNode::HashDeduplicate {
@ -378,16 +372,14 @@ impl<'a> PlanBuilder<'a> {
PropertyPathExpression::ZeroOrOne(p) => { PropertyPathExpression::ZeroOrOne(p) => {
PlanPropertyPath::ZeroOrOne(Rc::new(self.build_for_path(p))) PlanPropertyPath::ZeroOrOne(Rc::new(self.build_for_path(p)))
} }
PropertyPathExpression::NegatedPropertySet(p) => { PropertyPathExpression::NegatedPropertySet(p) => PlanPropertyPath::NegatedPropertySet(
PlanPropertyPath::NegatedPropertySet(Rc::new( p.iter()
p.iter() .map(|p| PlanTerm {
.map(|p| PlanTerm { encoded: self.build_term(p),
encoded: self.build_term(p), plain: p.clone(),
plain: p.clone(), })
}) .collect(),
.collect(), ),
))
}
} }
} }
@ -407,14 +399,14 @@ impl<'a> PlanBuilder<'a> {
plain: l.clone(), plain: l.clone(),
}), }),
Expression::Variable(v) => PlanExpression::Variable(build_plan_variable(variables, v)), Expression::Variable(v) => PlanExpression::Variable(build_plan_variable(variables, v)),
Expression::Or(a, b) => PlanExpression::Or( Expression::Or(a, b) => PlanExpression::Or(vec![
Box::new(self.build_for_expression(a, variables, graph_name)?), self.build_for_expression(a, variables, graph_name)?,
Box::new(self.build_for_expression(b, variables, graph_name)?), self.build_for_expression(b, variables, graph_name)?,
), ]),
Expression::And(a, b) => PlanExpression::And( Expression::And(a, b) => PlanExpression::And(vec![
Box::new(self.build_for_expression(a, variables, graph_name)?), self.build_for_expression(a, variables, graph_name)?,
Box::new(self.build_for_expression(b, variables, graph_name)?), self.build_for_expression(b, variables, graph_name)?,
), ]),
Expression::Equal(a, b) => PlanExpression::Equal( Expression::Equal(a, b) => PlanExpression::Equal(
Box::new(self.build_for_expression(a, variables, graph_name)?), Box::new(self.build_for_expression(a, variables, graph_name)?),
Box::new(self.build_for_expression(b, variables, graph_name)?), Box::new(self.build_for_expression(b, variables, graph_name)?),
@ -441,22 +433,31 @@ impl<'a> PlanBuilder<'a> {
), ),
Expression::In(e, l) => { Expression::In(e, l) => {
let e = self.build_for_expression(e, variables, graph_name)?; let e = self.build_for_expression(e, variables, graph_name)?;
l.iter() if l.is_empty() {
.map(|v| { // False except on error
Ok(PlanExpression::Equal( PlanExpression::If(
Box::new(e.clone()), Box::new(e),
Box::new(self.build_for_expression(v, variables, graph_name)?), Box::new(PlanExpression::Literal(PlanTerm {
))
})
.reduce(|a: Result<_, EvaluationError>, b| {
Ok(PlanExpression::Or(Box::new(a?), Box::new(b?)))
})
.unwrap_or_else(|| {
Ok(PlanExpression::Literal(PlanTerm {
encoded: false.into(), encoded: false.into(),
plain: false.into(), plain: false.into(),
})) })),
})? Box::new(PlanExpression::Literal(PlanTerm {
encoded: false.into(),
plain: false.into(),
})),
)
} else {
PlanExpression::Or(
l.iter()
.map(|v| {
Ok(PlanExpression::Equal(
Box::new(e.clone()),
Box::new(self.build_for_expression(v, variables, graph_name)?),
))
})
.collect::<Result<_, EvaluationError>>()?,
)
}
} }
Expression::Add(a, b) => PlanExpression::Add( Expression::Add(a, b) => PlanExpression::Add(
Box::new(self.build_for_expression(a, variables, graph_name)?), Box::new(self.build_for_expression(a, variables, graph_name)?),
@ -1084,7 +1085,7 @@ impl<'a> PlanBuilder<'a> {
separator, separator,
} => Ok(PlanAggregation { } => Ok(PlanAggregation {
function: PlanAggregationFunction::GroupConcat { function: PlanAggregationFunction::GroupConcat {
separator: Rc::new(separator.clone().unwrap_or_else(|| " ".to_owned())), separator: Rc::from(separator.as_deref().unwrap_or(" ")),
}, },
parameter: Some(self.build_for_expression(expr, variables, graph_name)?), parameter: Some(self.build_for_expression(expr, variables, graph_name)?),
distinct: *distinct, distinct: *distinct,
@ -1219,13 +1220,11 @@ impl<'a> PlanBuilder<'a> {
} }
fn convert_plan_variable(from_variable: &PlanVariable, to: &mut Vec<Variable>) -> PlanVariable { fn convert_plan_variable(from_variable: &PlanVariable, to: &mut Vec<Variable>) -> PlanVariable {
let encoded = if let Some(to_id) = to.iter().enumerate().find_map(|(to_id, var)| { let encoded = if let Some(to_id) = to
if *var == from_variable.plain { .iter()
Some(to_id) .enumerate()
} else { .find_map(|(to_id, var)| (*var == from_variable.plain).then(|| to_id))
None {
}
}) {
to_id to_id
} else { } else {
to.push(Variable::new_unchecked(format!("{:x}", random::<u128>()))); to.push(Variable::new_unchecked(format!("{:x}", random::<u128>())));
@ -1237,6 +1236,35 @@ impl<'a> PlanBuilder<'a> {
} }
} }
fn can_use_for_loop_left_join(node: &PlanNode) -> bool {
// We forbid MINUS, SERVICE and everything that affects cardinality in for loop left joins
match node {
PlanNode::StaticBindings { .. }
| PlanNode::QuadPattern { .. }
| PlanNode::PathPattern { .. } => true,
PlanNode::Filter { child, .. }
| PlanNode::Extend { child, .. }
| PlanNode::Sort { child, .. }
| PlanNode::Project { child, .. }
| PlanNode::Aggregate { child, .. } => Self::can_use_for_loop_left_join(child),
PlanNode::Union { children } => {
children.iter().all(|c| Self::can_use_for_loop_left_join(c))
}
PlanNode::HashJoin { left, right }
| PlanNode::ForLoopJoin { left, right }
| PlanNode::ForLoopLeftJoin { left, right, .. }
| PlanNode::HashLeftJoin { left, right, .. } => {
Self::can_use_for_loop_left_join(left) && Self::can_use_for_loop_left_join(right)
}
PlanNode::AntiJoin { .. }
| PlanNode::Service { .. }
| PlanNode::HashDeduplicate { .. }
| PlanNode::Reduced { .. }
| PlanNode::Skip { .. }
| PlanNode::Limit { .. } => false,
}
}
fn add_left_join_problematic_variables(node: &PlanNode, set: &mut BTreeSet<usize>) { fn add_left_join_problematic_variables(node: &PlanNode, set: &mut BTreeSet<usize>) {
match node { match node {
PlanNode::StaticBindings { .. } PlanNode::StaticBindings { .. }
@ -1299,7 +1327,8 @@ impl<'a> PlanBuilder<'a> {
} }
PlanNode::Sort { child, .. } PlanNode::Sort { child, .. }
| PlanNode::HashDeduplicate { child } | PlanNode::HashDeduplicate { child }
| PlanNode::Reduced { child } => { | PlanNode::Reduced { child }
| PlanNode::Project { child, .. } => {
Self::add_left_join_problematic_variables(child, set); Self::add_left_join_problematic_variables(child, set);
} }
PlanNode::Skip { child, .. } | PlanNode::Limit { child, .. } => { PlanNode::Skip { child, .. } | PlanNode::Limit { child, .. } => {
@ -1317,15 +1346,6 @@ impl<'a> PlanBuilder<'a> {
Self::add_left_join_problematic_variables(child, set) Self::add_left_join_problematic_variables(child, set)
} }
} }
PlanNode::Project { mapping, child } => {
let mut child_bound = BTreeSet::new();
Self::add_left_join_problematic_variables(child, &mut child_bound);
for (child_i, output_i) in mapping.iter() {
if child_bound.contains(&child_i.encoded) {
set.insert(output_i.encoded);
}
}
}
PlanNode::Aggregate { PlanNode::Aggregate {
key_variables, key_variables,
aggregates, aggregates,
@ -1341,15 +1361,15 @@ impl<'a> PlanBuilder<'a> {
} }
fn new_join(&self, mut left: PlanNode, mut right: PlanNode) -> PlanNode { fn new_join(&self, mut left: PlanNode, mut right: PlanNode) -> PlanNode {
// We first use VALUES to filter the following patterns evaluation
if matches!(right, PlanNode::StaticBindings { .. }) {
swap(&mut left, &mut right);
}
if self.with_optimizations if self.with_optimizations
&& Self::is_fit_for_for_loop_join(&left)
&& Self::is_fit_for_for_loop_join(&right) && Self::is_fit_for_for_loop_join(&right)
&& Self::has_some_common_variables(&left, &right) && Self::has_some_common_variables(&left, &right)
{ {
// We first use VALUES to filter the following patterns evaluation
if matches!(right, PlanNode::StaticBindings { .. }) {
swap(&mut left, &mut right);
}
PlanNode::ForLoopJoin { PlanNode::ForLoopJoin {
left: Rc::new(left), left: Rc::new(left),
right: Rc::new(right), right: Rc::new(right),
@ -1379,9 +1399,8 @@ impl<'a> PlanBuilder<'a> {
match node { match node {
PlanNode::StaticBindings { .. } PlanNode::StaticBindings { .. }
| PlanNode::QuadPattern { .. } | PlanNode::QuadPattern { .. }
| PlanNode::PathPattern { .. } | PlanNode::PathPattern { .. } => true,
| PlanNode::ForLoopJoin { .. } => true, PlanNode::ForLoopJoin { left, right } | PlanNode::HashJoin { left, right } => {
PlanNode::HashJoin { left, right } => {
Self::is_fit_for_for_loop_join(left) && Self::is_fit_for_for_loop_join(right) Self::is_fit_for_for_loop_join(left) && Self::is_fit_for_for_loop_join(right)
} }
PlanNode::Filter { child, .. } | PlanNode::Extend { child, .. } => { PlanNode::Filter { child, .. } | PlanNode::Extend { child, .. } => {
@ -1411,8 +1430,12 @@ impl<'a> PlanBuilder<'a> {
expression: filter, expression: filter,
}; };
} }
if let PlanExpression::And(f1, f2) = *filter { if let PlanExpression::And(filters) = *filter {
return self.push_filter(Rc::new(self.push_filter(node, f1)), f2); return filters
.into_iter()
.fold((*node.as_ref()).clone(), |acc, f| {
self.push_filter(Rc::new(acc), Box::new(f))
});
} }
let mut filter_variables = BTreeSet::new(); let mut filter_variables = BTreeSet::new();
filter.lookup_used_variables(&mut |v| { filter.lookup_used_variables(&mut |v| {
@ -1423,25 +1446,25 @@ impl<'a> PlanBuilder<'a> {
if filter_variables.iter().all(|v| left.is_variable_bound(*v)) { if filter_variables.iter().all(|v| left.is_variable_bound(*v)) {
if filter_variables.iter().all(|v| right.is_variable_bound(*v)) { if filter_variables.iter().all(|v| right.is_variable_bound(*v)) {
PlanNode::HashJoin { PlanNode::HashJoin {
left: Rc::new(self.push_filter(left.clone(), filter.clone())), left: Rc::new(self.push_filter(Rc::clone(left), filter.clone())),
right: Rc::new(self.push_filter(right.clone(), filter)), right: Rc::new(self.push_filter(Rc::clone(right), filter)),
} }
} else { } else {
PlanNode::HashJoin { PlanNode::HashJoin {
left: Rc::new(self.push_filter(left.clone(), filter)), left: Rc::new(self.push_filter(Rc::clone(left), filter)),
right: right.clone(), right: Rc::clone(right),
} }
} }
} else if filter_variables.iter().all(|v| right.is_variable_bound(*v)) { } else if filter_variables.iter().all(|v| right.is_variable_bound(*v)) {
PlanNode::HashJoin { PlanNode::HashJoin {
left: left.clone(), left: Rc::clone(left),
right: Rc::new(self.push_filter(right.clone(), filter)), right: Rc::new(self.push_filter(Rc::clone(right), filter)),
} }
} else { } else {
PlanNode::Filter { PlanNode::Filter {
child: Rc::new(PlanNode::HashJoin { child: Rc::new(PlanNode::HashJoin {
left: left.clone(), left: Rc::clone(left),
right: right.clone(), right: Rc::clone(right),
}), }),
expression: filter, expression: filter,
} }
@ -1450,20 +1473,20 @@ impl<'a> PlanBuilder<'a> {
PlanNode::ForLoopJoin { left, right } => { PlanNode::ForLoopJoin { left, right } => {
if filter_variables.iter().all(|v| left.is_variable_bound(*v)) { if filter_variables.iter().all(|v| left.is_variable_bound(*v)) {
PlanNode::ForLoopJoin { PlanNode::ForLoopJoin {
left: Rc::new(self.push_filter(left.clone(), filter)), left: Rc::new(self.push_filter(Rc::clone(left), filter)),
right: right.clone(), right: Rc::clone(right),
} }
} else if filter_variables.iter().all(|v| right.is_variable_bound(*v)) { } else if filter_variables.iter().all(|v| right.is_variable_bound(*v)) {
PlanNode::ForLoopJoin { PlanNode::ForLoopJoin {
//TODO: should we do that always? //TODO: should we do that always?
left: left.clone(), left: Rc::clone(left),
right: Rc::new(self.push_filter(right.clone(), filter)), right: Rc::new(self.push_filter(Rc::clone(right), filter)),
} }
} else { } else {
PlanNode::Filter { PlanNode::Filter {
child: Rc::new(PlanNode::HashJoin { child: Rc::new(PlanNode::HashJoin {
left: left.clone(), left: Rc::clone(left),
right: right.clone(), right: Rc::clone(right),
}), }),
expression: filter, expression: filter,
} }
@ -1477,14 +1500,14 @@ impl<'a> PlanBuilder<'a> {
//TODO: handle the case where the filter generates an expression variable //TODO: handle the case where the filter generates an expression variable
if filter_variables.iter().all(|v| child.is_variable_bound(*v)) { if filter_variables.iter().all(|v| child.is_variable_bound(*v)) {
PlanNode::Extend { PlanNode::Extend {
child: Rc::new(self.push_filter(child.clone(), filter)), child: Rc::new(self.push_filter(Rc::clone(child), filter)),
expression: expression.clone(), expression: expression.clone(),
variable: variable.clone(), variable: variable.clone(),
} }
} else { } else {
PlanNode::Filter { PlanNode::Filter {
child: Rc::new(PlanNode::Extend { child: Rc::new(PlanNode::Extend {
child: child.clone(), child: Rc::clone(child),
expression: expression.clone(), expression: expression.clone(),
variable: variable.clone(), variable: variable.clone(),
}), }),
@ -1495,20 +1518,23 @@ impl<'a> PlanBuilder<'a> {
PlanNode::Filter { child, expression } => { PlanNode::Filter { child, expression } => {
if filter_variables.iter().all(|v| child.is_variable_bound(*v)) { if filter_variables.iter().all(|v| child.is_variable_bound(*v)) {
PlanNode::Filter { PlanNode::Filter {
child: Rc::new(self.push_filter(child.clone(), filter)), child: Rc::new(self.push_filter(Rc::clone(child), filter)),
expression: expression.clone(), expression: expression.clone(),
} }
} else { } else {
PlanNode::Filter { PlanNode::Filter {
child: child.clone(), child: Rc::clone(child),
expression: Box::new(PlanExpression::And(expression.clone(), filter)), expression: Box::new(PlanExpression::And(vec![
*expression.clone(),
*filter,
])),
} }
} }
} }
PlanNode::Union { children } => PlanNode::Union { PlanNode::Union { children } => PlanNode::Union {
children: children children: children
.iter() .iter()
.map(|c| Rc::new(self.push_filter(c.clone(), filter.clone()))) .map(|c| Rc::new(self.push_filter(Rc::clone(c), filter.clone())))
.collect(), .collect(),
}, },
_ => PlanNode::Filter { _ => PlanNode::Filter {
@ -1541,12 +1567,11 @@ impl<'a> PlanBuilder<'a> {
} }
fn build_plan_variable(variables: &mut Vec<Variable>, variable: &Variable) -> PlanVariable { fn build_plan_variable(variables: &mut Vec<Variable>, variable: &Variable) -> PlanVariable {
let encoded = match slice_key(variables, variable) { let encoded = if let Some(key) = slice_key(variables, variable) {
Some(key) => key, key
None => { } else {
variables.push(variable.clone()); variables.push(variable.clone());
variables.len() - 1 variables.len() - 1
}
}; };
PlanVariable { PlanVariable {
plain: variable.clone(), plain: variable.clone(),
@ -1555,12 +1580,11 @@ fn build_plan_variable(variables: &mut Vec<Variable>, variable: &Variable) -> Pl
} }
fn bnode_key(blank_nodes: &mut Vec<BlankNode>, blank_node: &BlankNode) -> usize { fn bnode_key(blank_nodes: &mut Vec<BlankNode>, blank_node: &BlankNode) -> usize {
match slice_key(blank_nodes, blank_node) { if let Some(key) = slice_key(blank_nodes, blank_node) {
Some(key) => key, key
None => { } else {
blank_nodes.push(blank_node.clone()); blank_nodes.push(blank_node.clone());
blank_nodes.len() - 1 blank_nodes.len() - 1
}
} }
} }
@ -1673,21 +1697,13 @@ fn compile_static_pattern_if_exists(
options: Option<&Expression>, options: Option<&Expression>,
) -> Option<Regex> { ) -> Option<Regex> {
let static_pattern = if let Expression::Literal(pattern) = pattern { let static_pattern = if let Expression::Literal(pattern) = pattern {
if pattern.datatype() == xsd::STRING { (pattern.datatype() == xsd::STRING).then(|| pattern.value())
Some(pattern.value())
} else {
None
}
} else { } else {
None None
}; };
let static_options = if let Some(options) = options { let static_options = if let Some(options) = options {
if let Expression::Literal(options) = options { if let Expression::Literal(options) = options {
if options.datatype() == xsd::STRING { (options.datatype() == xsd::STRING).then(|| Some(options.value()))
Some(Some(options.value()))
} else {
None
}
} else { } else {
None None
} }

@ -71,7 +71,14 @@ impl<'a, 'b: 'a> SimpleUpdateEvaluator<'a, 'b> {
insert, insert,
pattern, pattern,
.. ..
} => self.eval_delete_insert(delete, insert, using_dataset.as_ref().unwrap(), pattern), } => self.eval_delete_insert(
delete,
insert,
using_dataset
.as_ref()
.ok_or_else(|| EvaluationError::msg("No dataset"))?,
pattern,
),
GraphUpdateOperation::Load { GraphUpdateOperation::Load {
silent, silent,
source, source,
@ -119,14 +126,14 @@ impl<'a, 'b: 'a> SimpleUpdateEvaluator<'a, 'b> {
) -> Result<(), EvaluationError> { ) -> Result<(), EvaluationError> {
let dataset = Rc::new(DatasetView::new(self.transaction.reader(), using)); let dataset = Rc::new(DatasetView::new(self.transaction.reader(), using));
let (plan, variables) = PlanBuilder::build( let (plan, variables) = PlanBuilder::build(
dataset.as_ref(), &dataset,
algebra, algebra,
false, false,
&self.options.query_options.custom_functions, &self.options.query_options.custom_functions,
!self.options.query_options.without_optimizations, !self.options.query_options.without_optimizations,
)?; )?;
let evaluator = SimpleEvaluator::new( let evaluator = SimpleEvaluator::new(
dataset.clone(), Rc::clone(&dataset),
self.base_iri.clone(), self.base_iri.clone(),
self.options.query_options.service_handler(), self.options.query_options.service_handler(),
Rc::new(self.options.query_options.custom_functions.clone()), Rc::new(self.options.query_options.custom_functions.clone()),
@ -374,7 +381,7 @@ impl<'a, 'b: 'a> SimpleUpdateEvaluator<'a, 'b> {
TermPattern::Literal(term) => Some(term.clone().into()), TermPattern::Literal(term) => Some(term.clone().into()),
TermPattern::Triple(triple) => { TermPattern::Triple(triple) => {
Self::convert_triple_pattern(triple, variables, values, dataset, bnodes)? Self::convert_triple_pattern(triple, variables, values, dataset, bnodes)?
.map(|t| t.into()) .map(Into::into)
} }
TermPattern::Variable(v) => Self::lookup_variable(v, variables, values) TermPattern::Variable(v) => Self::lookup_variable(v, variables, values)
.map(|node| dataset.decode_term(&node)) .map(|node| dataset.decode_term(&node))
@ -507,7 +514,7 @@ impl<'a, 'b: 'a> SimpleUpdateEvaluator<'a, 'b> {
GroundTermPattern::Literal(term) => Some(term.clone().into()), GroundTermPattern::Literal(term) => Some(term.clone().into()),
GroundTermPattern::Triple(triple) => { GroundTermPattern::Triple(triple) => {
Self::convert_ground_triple_pattern(triple, variables, values, dataset)? Self::convert_ground_triple_pattern(triple, variables, values, dataset)?
.map(|t| t.into()) .map(Into::into)
} }
GroundTermPattern::Variable(v) => Self::lookup_variable(v, variables, values) GroundTermPattern::Variable(v) => Self::lookup_variable(v, variables, values)
.map(|node| dataset.decode_term(&node)) .map(|node| dataset.decode_term(&node))

@ -29,20 +29,18 @@ impl Db {
Ok(Self(Arc::new(RwLock::new(trees)))) Ok(Self(Arc::new(RwLock::new(trees))))
} }
#[allow(clippy::unwrap_in_result)]
pub fn column_family(&self, name: &'static str) -> Option<ColumnFamily> { pub fn column_family(&self, name: &'static str) -> Option<ColumnFamily> {
let name = ColumnFamily(name); let name = ColumnFamily(name);
if self.0.read().unwrap().contains_key(&name) { (self.0.read().unwrap().contains_key(&name)).then(|| name)
Some(name)
} else {
None
}
} }
#[must_use] #[must_use]
pub fn snapshot(&self) -> Reader { pub fn snapshot(&self) -> Reader {
Reader(InnerReader::Simple(self.0.clone())) Reader(InnerReader::Simple(Arc::clone(&self.0)))
} }
#[allow(clippy::unwrap_in_result)]
pub fn transaction<'a, 'b: 'a, T, E: Error + 'static + From<StorageError>>( pub fn transaction<'a, 'b: 'a, T, E: Error + 'static + From<StorageError>>(
&'b self, &'b self,
f: impl Fn(Transaction<'a>) -> Result<T, E>, f: impl Fn(Transaction<'a>) -> Result<T, E>,
@ -64,6 +62,7 @@ enum InnerReader {
} }
impl Reader { impl Reader {
#[allow(clippy::unwrap_in_result)]
pub fn get( pub fn get(
&self, &self,
column_family: &ColumnFamily, column_family: &ColumnFamily,
@ -90,6 +89,7 @@ impl Reader {
} }
} }
#[allow(clippy::unwrap_in_result)]
pub fn contains_key( pub fn contains_key(
&self, &self,
column_family: &ColumnFamily, column_family: &ColumnFamily,
@ -120,6 +120,7 @@ impl Reader {
self.scan_prefix(column_family, &[]) self.scan_prefix(column_family, &[])
} }
#[allow(clippy::unwrap_in_result)]
pub fn scan_prefix( pub fn scan_prefix(
&self, &self,
column_family: &ColumnFamily, column_family: &ColumnFamily,
@ -176,19 +177,20 @@ impl Reader {
Ok(Iter { iter, current }) Ok(Iter { iter, current })
} }
#[allow(clippy::unwrap_in_result)]
pub fn len(&self, column_family: &ColumnFamily) -> Result<usize, StorageError> { pub fn len(&self, column_family: &ColumnFamily) -> Result<usize, StorageError> {
match &self.0 { match &self.0 {
InnerReader::Simple(reader) => Ok(reader InnerReader::Simple(reader) => Ok(reader
.read() .read()
.unwrap() .unwrap()
.get(column_family) .get(column_family)
.map_or(0, |tree| tree.len())), .map_or(0, BTreeMap::len)),
InnerReader::Transaction(reader) => { InnerReader::Transaction(reader) => {
if let Some(reader) = reader.upgrade() { if let Some(reader) = reader.upgrade() {
Ok((*reader) Ok((*reader)
.borrow() .borrow()
.get(column_family) .get(column_family)
.map_or(0, |tree| tree.len())) .map_or(0, BTreeMap::len))
} else { } else {
Err(StorageError::Other( Err(StorageError::Other(
"The transaction is already ended".into(), "The transaction is already ended".into(),
@ -198,19 +200,20 @@ impl Reader {
} }
} }
#[allow(clippy::unwrap_in_result)]
pub fn is_empty(&self, column_family: &ColumnFamily) -> Result<bool, StorageError> { pub fn is_empty(&self, column_family: &ColumnFamily) -> Result<bool, StorageError> {
match &self.0 { match &self.0 {
InnerReader::Simple(reader) => Ok(reader InnerReader::Simple(reader) => Ok(reader
.read() .read()
.unwrap() .unwrap()
.get(column_family) .get(column_family)
.map_or(true, |tree| tree.is_empty())), .map_or(true, BTreeMap::is_empty)),
InnerReader::Transaction(reader) => { InnerReader::Transaction(reader) => {
if let Some(reader) = reader.upgrade() { if let Some(reader) = reader.upgrade() {
Ok((*reader) Ok((*reader)
.borrow() .borrow()
.get(column_family) .get(column_family)
.map_or(true, |tree| tree.is_empty())) .map_or(true, BTreeMap::is_empty))
} else { } else {
Err(StorageError::Other( Err(StorageError::Other(
"The transaction is already ended".into(), "The transaction is already ended".into(),
@ -246,7 +249,7 @@ impl Transaction<'_> {
.map_or(false, |cf| cf.contains_key(key))) .map_or(false, |cf| cf.contains_key(key)))
} }
#[allow(clippy::unnecessary_wraps)] #[allow(clippy::unnecessary_wraps, clippy::unwrap_in_result)]
pub fn insert( pub fn insert(
&mut self, &mut self,
column_family: &ColumnFamily, column_family: &ColumnFamily,
@ -269,7 +272,7 @@ impl Transaction<'_> {
self.insert(column_family, key, &[]) self.insert(column_family, key, &[])
} }
#[allow(clippy::unnecessary_wraps)] #[allow(clippy::unnecessary_wraps, clippy::unwrap_in_result)]
pub fn remove(&mut self, column_family: &ColumnFamily, key: &[u8]) -> Result<(), StorageError> { pub fn remove(&mut self, column_family: &ColumnFamily, key: &[u8]) -> Result<(), StorageError> {
self.0 self.0
.borrow_mut() .borrow_mut()

@ -4,7 +4,7 @@
use crate::storage::error::{CorruptionError, StorageError}; use crate::storage::error::{CorruptionError, StorageError};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use libc::{self, c_char, c_void, free}; use libc::{self, c_void, free};
use oxrocksdb_sys::*; use oxrocksdb_sys::*;
use rand::random; use rand::random;
use std::borrow::Borrow; use std::borrow::Borrow;
@ -241,7 +241,7 @@ impl Db {
.map(|cf| cf.as_ptr()) .map(|cf| cf.as_ptr())
.collect::<Vec<_>>() .collect::<Vec<_>>()
.as_ptr(), .as_ptr(),
cf_options.as_ptr() as *const *const rocksdb_options_t, cf_options.as_ptr().cast(),
cf_handles.as_mut_ptr(), cf_handles.as_mut_ptr(),
)) ))
.map_err(|e| { .map_err(|e| {
@ -359,7 +359,7 @@ impl Db {
.map(|cf| cf.as_ptr()) .map(|cf| cf.as_ptr())
.collect::<Vec<_>>() .collect::<Vec<_>>()
.as_ptr(), .as_ptr(),
cf_options.as_ptr() as *const *const rocksdb_options_t, cf_options.as_ptr().cast(),
cf_handles.as_mut_ptr(), cf_handles.as_mut_ptr(),
)) ))
.map_err(|e| { .map_err(|e| {
@ -393,11 +393,7 @@ impl Db {
cf_handles, cf_handles,
cf_options, cf_options,
is_secondary: true, is_secondary: true,
path_to_remove: if in_memory { path_to_remove: in_memory.then(|| secondary_path),
Some(secondary_path)
} else {
None
},
})), })),
}) })
} }
@ -424,7 +420,7 @@ impl Db {
.map(|cf| cf.as_ptr()) .map(|cf| cf.as_ptr())
.collect::<Vec<_>>() .collect::<Vec<_>>()
.as_ptr(), .as_ptr(),
cf_options.as_ptr() as *const *const rocksdb_options_t, cf_options.as_ptr().cast(),
cf_handles.as_mut_ptr(), cf_handles.as_mut_ptr(),
0, // false 0, // false
)) ))
@ -580,7 +576,7 @@ impl Db {
} }
let options = rocksdb_readoptions_create_copy(db.read_options); let options = rocksdb_readoptions_create_copy(db.read_options);
Reader { Reader {
inner: InnerReader::PlainDb(db.clone()), inner: InnerReader::PlainDb(Arc::clone(db)),
options, options,
} }
} }
@ -594,7 +590,7 @@ impl Db {
rocksdb_readoptions_set_snapshot(options, snapshot); rocksdb_readoptions_set_snapshot(options, snapshot);
Reader { Reader {
inner: InnerReader::TransactionalSnapshot(Rc::new(TransactionalSnapshot { inner: InnerReader::TransactionalSnapshot(Rc::new(TransactionalSnapshot {
db: db.clone(), db: Arc::clone(db),
snapshot, snapshot,
})), })),
options, options,
@ -632,7 +628,7 @@ impl Db {
let result = f(Transaction { let result = f(Transaction {
transaction: Rc::new(transaction), transaction: Rc::new(transaction),
read_options, read_options,
_lifetime: PhantomData::default(), _lifetime: PhantomData,
}); });
match result { match result {
Ok(result) => { Ok(result) => {
@ -698,7 +694,7 @@ impl Db {
db.db, db.db,
db.read_options, db.read_options,
column_family.0, column_family.0,
key.as_ptr() as *const c_char, key.as_ptr().cast(),
key.len(), key.len(),
)) ))
} }
@ -707,7 +703,7 @@ impl Db {
db.db, db.db,
db.read_options, db.read_options,
column_family.0, column_family.0,
key.as_ptr() as *const c_char, key.as_ptr().cast(),
key.len() key.len()
)) ))
} }
@ -740,9 +736,9 @@ impl Db {
db.db, db.db,
db.write_options, db.write_options,
column_family.0, column_family.0,
key.as_ptr() as *const c_char, key.as_ptr().cast(),
key.len(), key.len(),
value.as_ptr() as *const c_char, value.as_ptr().cast(),
value.len(), value.len(),
)) ))
}?; }?;
@ -940,7 +936,7 @@ impl Reader {
inner.db.db, inner.db.db,
self.options, self.options,
column_family.0, column_family.0,
key.as_ptr() as *const c_char, key.as_ptr().cast(),
key.len() key.len()
)) ))
} }
@ -950,7 +946,7 @@ impl Reader {
*inner, *inner,
self.options, self.options,
column_family.0, column_family.0,
key.as_ptr() as *const c_char, key.as_ptr().cast(),
key.len() key.len()
)) ))
} else { } else {
@ -964,7 +960,7 @@ impl Reader {
inner.db, inner.db,
self.options, self.options,
column_family.0, column_family.0,
key.as_ptr() as *const c_char, key.as_ptr().cast(),
key.len() key.len()
)) ))
} }
@ -1005,11 +1001,7 @@ impl Reader {
break; break;
} }
} }
if found { found.then(|| bound)
Some(bound)
} else {
None
}
}; };
unsafe { unsafe {
@ -1021,7 +1013,7 @@ impl Reader {
if let Some(upper_bound) = &upper_bound { if let Some(upper_bound) = &upper_bound {
rocksdb_readoptions_set_iterate_upper_bound( rocksdb_readoptions_set_iterate_upper_bound(
options, options,
upper_bound.as_ptr() as *const c_char, upper_bound.as_ptr().cast(),
upper_bound.len(), upper_bound.len(),
); );
} }
@ -1046,7 +1038,7 @@ impl Reader {
if prefix.is_empty() { if prefix.is_empty() {
rocksdb_iter_seek_to_first(iter); rocksdb_iter_seek_to_first(iter);
} else { } else {
rocksdb_iter_seek(iter, prefix.as_ptr() as *const c_char, prefix.len()); rocksdb_iter_seek(iter, prefix.as_ptr().cast(), prefix.len());
} }
let is_currently_valid = rocksdb_iter_valid(iter) != 0; let is_currently_valid = rocksdb_iter_valid(iter) != 0;
Ok(Iter { Ok(Iter {
@ -1101,7 +1093,7 @@ impl Transaction<'_> {
*self.transaction, *self.transaction,
self.read_options, self.read_options,
column_family.0, column_family.0,
key.as_ptr() as *const c_char, key.as_ptr().cast(),
key.len() key.len()
))?; ))?;
Ok(if slice.is_null() { Ok(if slice.is_null() {
@ -1130,9 +1122,9 @@ impl Transaction<'_> {
ffi_result!(rocksdb_transaction_put_cf_with_status( ffi_result!(rocksdb_transaction_put_cf_with_status(
*self.transaction, *self.transaction,
column_family.0, column_family.0,
key.as_ptr() as *const c_char, key.as_ptr().cast(),
key.len(), key.len(),
value.as_ptr() as *const c_char, value.as_ptr().cast(),
value.len(), value.len(),
))?; ))?;
} }
@ -1152,7 +1144,7 @@ impl Transaction<'_> {
ffi_result!(rocksdb_transaction_delete_cf_with_status( ffi_result!(rocksdb_transaction_delete_cf_with_status(
*self.transaction, *self.transaction,
column_family.0, column_family.0,
key.as_ptr() as *const c_char, key.as_ptr().cast(),
key.len(), key.len(),
))?; ))?;
} }
@ -1177,7 +1169,7 @@ impl Deref for PinnableSlice {
unsafe { unsafe {
let mut len = 0; let mut len = 0;
let val = rocksdb_pinnableslice_value(self.0, &mut len); let val = rocksdb_pinnableslice_value(self.0, &mut len);
slice::from_raw_parts(val as *const u8, len) slice::from_raw_parts(val.cast(), len)
} }
} }
} }
@ -1208,7 +1200,7 @@ pub struct Buffer {
impl Drop for Buffer { impl Drop for Buffer {
fn drop(&mut self) { fn drop(&mut self) {
unsafe { unsafe {
free(self.base as *mut c_void); free(self.base.cast());
} }
} }
} }
@ -1285,7 +1277,7 @@ impl Iter {
unsafe { unsafe {
let mut len = 0; let mut len = 0;
let val = rocksdb_iter_key(self.iter, &mut len); let val = rocksdb_iter_key(self.iter, &mut len);
Some(slice::from_raw_parts(val as *const u8, len)) Some(slice::from_raw_parts(val.cast(), len))
} }
} else { } else {
None None
@ -1311,9 +1303,9 @@ impl SstFileWriter {
unsafe { unsafe {
ffi_result!(rocksdb_sstfilewriter_put_with_status( ffi_result!(rocksdb_sstfilewriter_put_with_status(
self.writer, self.writer,
key.as_ptr() as *const c_char, key.as_ptr().cast(),
key.len(), key.len(),
value.as_ptr() as *const c_char, value.as_ptr().cast(),
value.len(), value.len(),
))?; ))?;
} }

@ -1,3 +1,4 @@
#![allow(clippy::same_name_method)]
#[cfg(not(target_family = "wasm"))] #[cfg(not(target_family = "wasm"))]
use crate::model::Quad; use crate::model::Quad;
use crate::model::{GraphNameRef, NamedOrBlankNodeRef, QuadRef, TermRef}; use crate::model::{GraphNameRef, NamedOrBlankNodeRef, QuadRef, TermRef};
@ -181,7 +182,7 @@ impl Storage {
] ]
} }
#[allow(clippy::unnecessary_wraps)] #[allow(clippy::unnecessary_wraps, clippy::unwrap_in_result)]
fn setup(db: Db) -> Result<Self, StorageError> { fn setup(db: Db) -> Result<Self, StorageError> {
let this = Self { let this = Self {
#[cfg(not(target_family = "wasm"))] #[cfg(not(target_family = "wasm"))]
@ -1305,7 +1306,7 @@ impl StorageBulkLoader {
let mut buffer_to_load = Vec::with_capacity(batch_size); let mut buffer_to_load = Vec::with_capacity(batch_size);
swap(buffer, &mut buffer_to_load); swap(buffer, &mut buffer_to_load);
let storage = self.storage.clone(); let storage = self.storage.clone();
let done_counter_clone = done_counter.clone(); let done_counter_clone = Arc::clone(done_counter);
threads.push_back(spawn(move || { threads.push_back(spawn(move || {
FileBulkLoader::new(storage, batch_size).load(buffer_to_load, &done_counter_clone) FileBulkLoader::new(storage, batch_size).load(buffer_to_load, &done_counter_clone)
})); }));

@ -565,7 +565,7 @@ impl Store {
&self, &self,
quads: impl IntoIterator<Item = impl Into<Quad>>, quads: impl IntoIterator<Item = impl Into<Quad>>,
) -> Result<(), StorageError> { ) -> Result<(), StorageError> {
let quads = quads.into_iter().map(|q| q.into()).collect::<Vec<_>>(); let quads = quads.into_iter().map(Into::into).collect::<Vec<_>>();
self.transaction(move |mut t| t.extend(&quads)) self.transaction(move |mut t| t.extend(&quads))
} }
@ -1569,7 +1569,7 @@ impl BulkLoader {
quads: impl IntoIterator<Item = Result<impl Into<Quad>, EI>>, quads: impl IntoIterator<Item = Result<impl Into<Quad>, EI>>,
) -> Result<(), EO> { ) -> Result<(), EO> {
self.storage self.storage
.load(quads.into_iter().map(|q| q.map(|q| q.into()))) .load(quads.into_iter().map(|q| q.map(Into::into)))
} }
} }

@ -175,7 +175,7 @@ fn test_load_dataset() -> Result<(), Box<dyn Error>> {
#[test] #[test]
#[cfg(not(target_family = "wasm"))] #[cfg(not(target_family = "wasm"))]
fn test_bulk_load_dataset() -> Result<(), Box<dyn Error>> { fn test_bulk_load_dataset() -> Result<(), Box<dyn Error>> {
let store = Store::new().unwrap(); let store = Store::new()?;
store store
.bulk_loader() .bulk_loader()
.load_dataset(Cursor::new(GRAPH_DATA), DatasetFormat::TriG, None)?; .load_dataset(Cursor::new(GRAPH_DATA), DatasetFormat::TriG, None)?;

@ -1,6 +1,6 @@
[package] [package]
name = "oxrocksdb-sys" name = "oxrocksdb-sys"
version = "0.3.16-dev" version = "0.3.17-dev"
authors = ["Tpt <thomas@pellissier-tanon.fr>"] authors = ["Tpt <thomas@pellissier-tanon.fr>"]
license = "GPL-2.0 OR Apache-2.0" license = "GPL-2.0 OR Apache-2.0"
repository = "https://github.com/oxigraph/oxigraph/tree/main/oxrocksdb-sys" repository = "https://github.com/oxigraph/oxigraph/tree/main/oxrocksdb-sys"

@ -1,7 +1,7 @@
// Code from https://github.com/rust-rocksdb/rust-rocksdb/blob/eb2d302682418b361a80ad8f4dcf335ade60dcf5/librocksdb-sys/build.rs // Code from https://github.com/rust-rocksdb/rust-rocksdb/blob/eb2d302682418b361a80ad8f4dcf335ade60dcf5/librocksdb-sys/build.rs
// License: https://github.com/rust-rocksdb/rust-rocksdb/blob/master/LICENSE // License: https://github.com/rust-rocksdb/rust-rocksdb/blob/master/LICENSE
use std::env::{set_var, var}; use std::env::{remove_var, set_var, var};
use std::path::PathBuf; use std::path::PathBuf;
fn link(name: &str, bundled: bool) { fn link(name: &str, bundled: bool) {
@ -88,8 +88,6 @@ fn build_rocksdb() {
config.define("HAVE_LZCNT", Some("1")); config.define("HAVE_LZCNT", Some("1"));
config.flag_if_supported("-mlzcnt"); config.flag_if_supported("-mlzcnt");
} }
} else if target.contains("aarch64") {
lib_sources.push("util/crc32c_arm64.cc")
} }
if target.contains("apple-ios") { if target.contains("apple-ios") {
@ -100,11 +98,13 @@ fn build_rocksdb() {
config.define("NPERF_CONTEXT", None); config.define("NPERF_CONTEXT", None);
config.define("ROCKSDB_PLATFORM_POSIX", None); config.define("ROCKSDB_PLATFORM_POSIX", None);
config.define("ROCKSDB_LIB_IO_POSIX", None); config.define("ROCKSDB_LIB_IO_POSIX", None);
remove_var("SDKROOT"); // We override SDKROOT for cross-compilation
set_var("IPHONEOS_DEPLOYMENT_TARGET", "11.0"); set_var("IPHONEOS_DEPLOYMENT_TARGET", "11.0");
} else if target.contains("darwin") { } else if target.contains("darwin") {
config.define("OS_MACOSX", None); config.define("OS_MACOSX", None);
config.define("ROCKSDB_PLATFORM_POSIX", None); config.define("ROCKSDB_PLATFORM_POSIX", None);
config.define("ROCKSDB_LIB_IO_POSIX", None); config.define("ROCKSDB_LIB_IO_POSIX", None);
remove_var("SDKROOT"); // We override SDKROOT for cross-compilation
} else if target.contains("android") { } else if target.contains("android") {
config.define("OS_ANDROID", None); config.define("OS_ANDROID", None);
config.define("ROCKSDB_PLATFORM_POSIX", None); config.define("ROCKSDB_PLATFORM_POSIX", None);
@ -179,6 +179,7 @@ fn build_rocksdb() {
} }
config.file(&format!("rocksdb/{file}")); config.file(&format!("rocksdb/{file}"));
} }
config.compile("rocksdb"); config.compile("rocksdb");
} }

@ -1 +1 @@
Subproject commit 740854a7b0c09450e67e5e29d9979e743485aebf Subproject commit 443333d8c059c87db408ec2d11685db00031b30a

@ -1,6 +1,6 @@
[package] [package]
name = "pyoxigraph" name = "pyoxigraph"
version = "0.3.16-dev" version = "0.3.17-dev"
authors = ["Tpt"] authors = ["Tpt"]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
readme = "README.md" readme = "README.md"
@ -19,5 +19,5 @@ doctest = false
abi3 = ["pyo3/abi3-py37"] abi3 = ["pyo3/abi3-py37"]
[dependencies] [dependencies]
oxigraph = { version = "0.3.16-dev", path="../lib", features = ["http_client"] } oxigraph = { version = "0.3.17-dev", path="../lib", features = ["http_client"] }
pyo3 = { version = "0.18", features = ["extension-module"] } pyo3 = { version = "0.19", features = ["extension-module"] }

@ -1,5 +1,5 @@
[build-system] [build-system]
requires = ["maturin~=0.14.0"] requires = ["maturin~=0.15.0"]
build-backend = "maturin" build-backend = "maturin"
[project] [project]

@ -1,6 +1,6 @@
black~=23.1 black~=23.1
furo furo
maturin~=0.14.0 maturin~=0.15.1
mypy~=1.0 mypy~=1.0
ruff~=0.0.255 ruff~=0.0.255
sphinx~=5.3 sphinx~=5.3

@ -8,7 +8,9 @@ use oxigraph::io::{
use pyo3::exceptions::{PyIOError, PySyntaxError, PyValueError}; use pyo3::exceptions::{PyIOError, PySyntaxError, PyValueError};
use pyo3::prelude::*; use pyo3::prelude::*;
use pyo3::types::PyBytes; use pyo3::types::PyBytes;
use pyo3::wrap_pyfunction; use pyo3::{intern, wrap_pyfunction};
use std::cmp::max;
use std::error::Error;
use std::fs::File; use std::fs::File;
use std::io::{self, BufRead, BufReader, BufWriter, Cursor, Read, Write}; use std::io::{self, BufRead, BufReader, BufWriter, Cursor, Read, Write};
@ -46,7 +48,7 @@ pub fn add_to_module(module: &PyModule) -> PyResult<()> {
/// >>> list(parse(input, "text/turtle", base_iri="http://example.com/")) /// >>> list(parse(input, "text/turtle", base_iri="http://example.com/"))
/// [<Triple subject=<NamedNode value=http://example.com/foo> predicate=<NamedNode value=http://example.com/p> object=<Literal value=1 datatype=<NamedNode value=http://www.w3.org/2001/XMLSchema#string>>>] /// [<Triple subject=<NamedNode value=http://example.com/foo> predicate=<NamedNode value=http://example.com/p> object=<Literal value=1 datatype=<NamedNode value=http://www.w3.org/2001/XMLSchema#string>>>]
#[pyfunction] #[pyfunction]
#[pyo3(text_signature = "(input, mime_type, *, base_iri = None)")] #[pyo3(signature = (input, mime_type, *, base_iri = None))]
pub fn parse( pub fn parse(
input: PyObject, input: PyObject,
mime_type: &str, mime_type: &str,
@ -281,21 +283,22 @@ impl Write for PyWritable {
pub struct PyIo(PyObject); pub struct PyIo(PyObject);
impl Read for PyIo { impl Read for PyIo {
fn read(&mut self, mut buf: &mut [u8]) -> io::Result<usize> { fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
Python::with_gil(|py| { Python::with_gil(|py| {
if buf.is_empty() {
return Ok(0);
}
let to_read = max(1, buf.len() / 4); // We divide by 4 because TextIO works with number of characters and not with number of bytes
let read = self let read = self
.0 .0
.call_method(py, "read", (buf.len(),), None) .as_ref(py)
.call_method1(intern!(py, "read"), (to_read,))
.map_err(to_io_err)?; .map_err(to_io_err)?;
let bytes = read let bytes = read
.extract::<&[u8]>(py) .extract::<&[u8]>()
.or_else(|e| { .or_else(|e| read.extract::<&str>().map(str::as_bytes).map_err(|_| e))
read.extract::<&str>(py)
.map(|s| s.as_bytes())
.map_err(|_| e)
})
.map_err(to_io_err)?; .map_err(to_io_err)?;
buf.write_all(bytes)?; buf[..bytes.len()].copy_from_slice(bytes);
Ok(bytes.len()) Ok(bytes.len())
}) })
} }
@ -305,16 +308,17 @@ impl Write for PyIo {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> { fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
Python::with_gil(|py| { Python::with_gil(|py| {
self.0 self.0
.call_method(py, "write", (PyBytes::new(py, buf),), None) .as_ref(py)
.call_method1(intern!(py, "write"), (PyBytes::new(py, buf),))
.map_err(to_io_err)? .map_err(to_io_err)?
.extract::<usize>(py) .extract::<usize>()
.map_err(to_io_err) .map_err(to_io_err)
}) })
} }
fn flush(&mut self) -> io::Result<()> { fn flush(&mut self) -> io::Result<()> {
Python::with_gil(|py| { Python::with_gil(|py| {
self.0.call_method(py, "flush", (), None)?; self.0.as_ref(py).call_method0(intern!(py, "flush"))?;
Ok(()) Ok(())
}) })
} }
@ -325,7 +329,10 @@ fn to_io_err(error: impl Into<PyErr>) -> io::Error {
} }
pub fn map_io_err(error: io::Error) -> PyErr { pub fn map_io_err(error: io::Error) -> PyErr {
if error.get_ref().map_or(false, |s| s.is::<PyErr>()) { if error
.get_ref()
.map_or(false, <(dyn Error + Send + Sync + 'static)>::is::<PyErr>)
{
*error.into_inner().unwrap().downcast().unwrap() *error.into_inner().unwrap().downcast().unwrap()
} else { } else {
PyIOError::new_err(error.to_string()) PyIOError::new_err(error.to_string())

@ -1,9 +1,3 @@
#![allow(
clippy::redundant_pub_crate,
clippy::used_underscore_binding,
clippy::unused_self,
clippy::trivially_copy_pass_by_ref
)]
mod io; mod io;
mod model; mod model;
mod sparql; mod sparql;

@ -20,8 +20,7 @@ use std::vec::IntoIter;
/// ///
/// >>> str(NamedNode('http://example.com')) /// >>> str(NamedNode('http://example.com'))
/// '<http://example.com>' /// '<http://example.com>'
#[pyclass(name = "NamedNode", module = "pyoxigraph")] #[pyclass(frozen, name = "NamedNode", module = "pyoxigraph")]
#[pyo3(text_signature = "(value)")]
#[derive(Eq, PartialEq, Ord, PartialOrd, Debug, Clone, Hash)] #[derive(Eq, PartialEq, Ord, PartialOrd, Debug, Clone, Hash)]
pub struct PyNamedNode { pub struct PyNamedNode {
inner: NamedNode, inner: NamedNode,
@ -144,8 +143,7 @@ impl PyNamedNode {
/// ///
/// >>> str(BlankNode('ex')) /// >>> str(BlankNode('ex'))
/// '_:ex' /// '_:ex'
#[pyclass(name = "BlankNode", module = "pyoxigraph")] #[pyclass(frozen, name = "BlankNode", module = "pyoxigraph")]
#[pyo3(text_signature = "(value = None)")]
#[derive(Eq, PartialEq, Debug, Clone, Hash)] #[derive(Eq, PartialEq, Debug, Clone, Hash)]
pub struct PyBlankNode { pub struct PyBlankNode {
inner: BlankNode, inner: BlankNode,
@ -280,8 +278,7 @@ impl PyBlankNode {
/// '"example"@en' /// '"example"@en'
/// >>> str(Literal('11', datatype=NamedNode('http://www.w3.org/2001/XMLSchema#integer'))) /// >>> str(Literal('11', datatype=NamedNode('http://www.w3.org/2001/XMLSchema#integer')))
/// '"11"^^<http://www.w3.org/2001/XMLSchema#integer>' /// '"11"^^<http://www.w3.org/2001/XMLSchema#integer>'
#[pyclass(name = "Literal", module = "pyoxigraph")] #[pyclass(frozen, name = "Literal", module = "pyoxigraph")]
#[pyo3(text_signature = "(value, *, datatype = None, language = None)")]
#[derive(Eq, PartialEq, Debug, Clone, Hash)] #[derive(Eq, PartialEq, Debug, Clone, Hash)]
pub struct PyLiteral { pub struct PyLiteral {
inner: Literal, inner: Literal,
@ -427,8 +424,7 @@ impl PyLiteral {
} }
/// The RDF `default graph name <https://www.w3.org/TR/rdf11-concepts/#dfn-default-graph>`_. /// The RDF `default graph name <https://www.w3.org/TR/rdf11-concepts/#dfn-default-graph>`_.
#[pyclass(name = "DefaultGraph", module = "pyoxigraph")] #[pyclass(frozen, name = "DefaultGraph", module = "pyoxigraph")]
#[pyo3(text_signature = "()")]
#[derive(Eq, PartialEq, Debug, Clone, Copy, Hash)] #[derive(Eq, PartialEq, Debug, Clone, Copy, Hash)]
pub struct PyDefaultGraph {} pub struct PyDefaultGraph {}
@ -625,9 +621,8 @@ impl IntoPy<PyObject> for PyTerm {
/// A triple could also be easily destructed into its components: /// A triple could also be easily destructed into its components:
/// ///
/// >>> (s, p, o) = Triple(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1')) /// >>> (s, p, o) = Triple(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'))
#[pyclass(name = "Triple", module = "pyoxigraph")] #[pyclass(frozen, name = "Triple", module = "pyoxigraph")]
#[derive(Eq, PartialEq, Debug, Clone, Hash)] #[derive(Eq, PartialEq, Debug, Clone, Hash)]
#[pyo3(text_signature = "(subject, predicate, object)")]
pub struct PyTriple { pub struct PyTriple {
inner: Triple, inner: Triple,
} }
@ -824,8 +819,7 @@ impl IntoPy<PyObject> for PyGraphName {
/// A quad could also be easily destructed into its components: /// A quad could also be easily destructed into its components:
/// ///
/// >>> (s, p, o, g) = Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'), NamedNode('http://example.com/g')) /// >>> (s, p, o, g) = Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'), NamedNode('http://example.com/g'))
#[pyclass(name = "Quad", module = "pyoxigraph")] #[pyclass(frozen, name = "Quad", module = "pyoxigraph")]
#[pyo3(text_signature = "(subject, predicate, object, graph_name = None)")]
#[derive(Eq, PartialEq, Debug, Clone, Hash)] #[derive(Eq, PartialEq, Debug, Clone, Hash)]
pub struct PyQuad { pub struct PyQuad {
inner: Quad, inner: Quad,
@ -1012,8 +1006,7 @@ impl PyQuad {
/// ///
/// >>> str(Variable('foo')) /// >>> str(Variable('foo'))
/// '?foo' /// '?foo'
#[pyclass(name = "Variable", module = "pyoxigraph")] #[pyclass(frozen, name = "Variable", module = "pyoxigraph")]
#[pyo3(text_signature = "(value)")]
#[derive(Eq, PartialEq, Debug, Clone, Hash)] #[derive(Eq, PartialEq, Debug, Clone, Hash)]
pub struct PyVariable { pub struct PyVariable {
inner: Variable, inner: Variable,

@ -88,7 +88,7 @@ pub fn query_results_to_python(py: Python<'_>, results: QueryResults) -> PyObjec
/// >>> s, p, o = solution /// >>> s, p, o = solution
/// >>> s /// >>> s
/// <NamedNode value=http://example.com> /// <NamedNode value=http://example.com>
#[pyclass(unsendable, name = "QuerySolution", module = "pyoxigraph")] #[pyclass(frozen, unsendable, name = "QuerySolution", module = "pyoxigraph")]
pub struct PyQuerySolution { pub struct PyQuerySolution {
inner: QuerySolution, inner: QuerySolution,
} }
@ -225,7 +225,7 @@ impl PyQueryTriples {
Ok(allow_threads_unsafe(|| self.inner.next()) Ok(allow_threads_unsafe(|| self.inner.next())
.transpose() .transpose()
.map_err(map_evaluation_error)? .map_err(map_evaluation_error)?
.map(|t| t.into())) .map(Into::into))
} }
} }

@ -35,8 +35,7 @@ use pyo3::prelude::*;
/// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'), NamedNode('http://example.com/g'))) /// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'), NamedNode('http://example.com/g')))
/// >>> str(store) /// >>> str(store)
/// '<http://example.com> <http://example.com/p> "1" <http://example.com/g> .\n' /// '<http://example.com> <http://example.com/p> "1" <http://example.com/g> .\n'
#[pyclass(name = "Store", module = "pyoxigraph")] #[pyclass(frozen, name = "Store", module = "pyoxigraph")]
#[pyo3(text_signature = "(path = None)")]
#[derive(Clone)] #[derive(Clone)]
pub struct PyStore { pub struct PyStore {
inner: Store, inner: Store,
@ -94,7 +93,7 @@ impl PyStore {
/// :rtype: Store /// :rtype: Store
/// :raises IOError: if the target directories contain invalid data or could not be accessed. /// :raises IOError: if the target directories contain invalid data or could not be accessed.
#[staticmethod] #[staticmethod]
#[pyo3(signature = (primary_path, secondary_path = None), text_signature = "(primary_path, secondary_path = None)")] #[pyo3(signature = (primary_path, secondary_path = None))]
fn secondary( fn secondary(
primary_path: &str, primary_path: &str,
secondary_path: Option<&str>, secondary_path: Option<&str>,
@ -216,7 +215,7 @@ impl PyStore {
/// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'), NamedNode('http://example.com/g'))) /// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'), NamedNode('http://example.com/g')))
/// >>> list(store.quads_for_pattern(NamedNode('http://example.com'), None, None, None)) /// >>> list(store.quads_for_pattern(NamedNode('http://example.com'), None, None, None))
/// [<Quad subject=<NamedNode value=http://example.com> predicate=<NamedNode value=http://example.com/p> object=<Literal value=1 datatype=<NamedNode value=http://www.w3.org/2001/XMLSchema#string>> graph_name=<NamedNode value=http://example.com/g>>] /// [<Quad subject=<NamedNode value=http://example.com> predicate=<NamedNode value=http://example.com/p> object=<Literal value=1 datatype=<NamedNode value=http://www.w3.org/2001/XMLSchema#string>> graph_name=<NamedNode value=http://example.com/g>>]
#[pyo3(signature = (subject, predicate, object, graph_name = None), text_signature = "($self, subject, predicate, object, graph_name = None)")] #[pyo3(signature = (subject, predicate, object, graph_name = None))]
fn quads_for_pattern( fn quads_for_pattern(
&self, &self,
subject: &PyAny, subject: &PyAny,
@ -228,10 +227,10 @@ impl PyStore {
extract_quads_pattern(subject, predicate, object, graph_name)?; extract_quads_pattern(subject, predicate, object, graph_name)?;
Ok(QuadIter { Ok(QuadIter {
inner: self.inner.quads_for_pattern( inner: self.inner.quads_for_pattern(
subject.as_ref().map(|p| p.into()), subject.as_ref().map(Into::into),
predicate.as_ref().map(|p| p.into()), predicate.as_ref().map(Into::into),
object.as_ref().map(|p| p.into()), object.as_ref().map(Into::into),
graph_name.as_ref().map(|p| p.into()), graph_name.as_ref().map(Into::into),
), ),
}) })
} }
@ -273,10 +272,7 @@ impl PyStore {
/// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1'))) /// >>> store.add(Quad(NamedNode('http://example.com'), NamedNode('http://example.com/p'), Literal('1')))
/// >>> store.query('ASK { ?s ?p ?o }') /// >>> store.query('ASK { ?s ?p ?o }')
/// True /// True
#[pyo3( #[pyo3(signature = (query, *, base_iri = None, use_default_graph_as_union = false, default_graph = None, named_graphs = None))]
signature = (query, *, base_iri = None, use_default_graph_as_union = false, default_graph = None, named_graphs = None),
text_signature = "($self, query, *, base_iri = None, use_default_graph_as_union = False, default_graph = None, named_graphs = None)"
)]
fn query( fn query(
&self, &self,
query: &str, query: &str,
@ -332,7 +328,7 @@ impl PyStore {
/// >>> store.update('DELETE WHERE { <http://example.com> ?p ?o }') /// >>> store.update('DELETE WHERE { <http://example.com> ?p ?o }')
/// >>> list(store) /// >>> list(store)
/// [] /// []
#[pyo3(signature = (update, *, base_iri = None), text_signature = "($self, update, *, base_iri = None)")] #[pyo3(signature = (update, *, base_iri = None))]
fn update(&self, update: &str, base_iri: Option<&str>, py: Python<'_>) -> PyResult<()> { fn update(&self, update: &str, base_iri: Option<&str>, py: Python<'_>) -> PyResult<()> {
py.allow_threads(|| { py.allow_threads(|| {
let update = let update =
@ -377,7 +373,7 @@ impl PyStore {
/// >>> store.load(io.BytesIO(b'<foo> <p> "1" .'), "text/turtle", base_iri="http://example.com/", to_graph=NamedNode("http://example.com/g")) /// >>> store.load(io.BytesIO(b'<foo> <p> "1" .'), "text/turtle", base_iri="http://example.com/", to_graph=NamedNode("http://example.com/g"))
/// >>> list(store) /// >>> list(store)
/// [<Quad subject=<NamedNode value=http://example.com/foo> predicate=<NamedNode value=http://example.com/p> object=<Literal value=1 datatype=<NamedNode value=http://www.w3.org/2001/XMLSchema#string>> graph_name=<NamedNode value=http://example.com/g>>] /// [<Quad subject=<NamedNode value=http://example.com/foo> predicate=<NamedNode value=http://example.com/p> object=<Literal value=1 datatype=<NamedNode value=http://www.w3.org/2001/XMLSchema#string>> graph_name=<NamedNode value=http://example.com/g>>]
#[pyo3(signature = (input, mime_type, *, base_iri = None, to_graph = None), text_signature = "($self, input, mime_type, *, base_iri = None, to_graph = None)")] #[pyo3(signature = (input, mime_type, *, base_iri = None, to_graph = None))]
fn load( fn load(
&self, &self,
input: PyObject, input: PyObject,
@ -459,7 +455,7 @@ impl PyStore {
/// >>> store.bulk_load(io.BytesIO(b'<foo> <p> "1" .'), "text/turtle", base_iri="http://example.com/", to_graph=NamedNode("http://example.com/g")) /// >>> store.bulk_load(io.BytesIO(b'<foo> <p> "1" .'), "text/turtle", base_iri="http://example.com/", to_graph=NamedNode("http://example.com/g"))
/// >>> list(store) /// >>> list(store)
/// [<Quad subject=<NamedNode value=http://example.com/foo> predicate=<NamedNode value=http://example.com/p> object=<Literal value=1 datatype=<NamedNode value=http://www.w3.org/2001/XMLSchema#string>> graph_name=<NamedNode value=http://example.com/g>>] /// [<Quad subject=<NamedNode value=http://example.com/foo> predicate=<NamedNode value=http://example.com/p> object=<Literal value=1 datatype=<NamedNode value=http://www.w3.org/2001/XMLSchema#string>> graph_name=<NamedNode value=http://example.com/g>>]
#[pyo3(signature = (input, mime_type, *, base_iri = None, to_graph = None), text_signature = "($self, input, mime_type, *, base_iri = None, to_graph = None)")] #[pyo3(signature = (input, mime_type, *, base_iri = None, to_graph = None))]
fn bulk_load( fn bulk_load(
&self, &self,
input: PyObject, input: PyObject,
@ -537,7 +533,7 @@ impl PyStore {
/// >>> store.dump(output, "text/turtle", from_graph=NamedNode("http://example.com/g")) /// >>> store.dump(output, "text/turtle", from_graph=NamedNode("http://example.com/g"))
/// >>> output.getvalue() /// >>> output.getvalue()
/// b'<http://example.com> <http://example.com/p> "1" .\n' /// b'<http://example.com> <http://example.com/p> "1" .\n'
#[pyo3(signature = (output, mime_type, *, from_graph = None), text_signature = "($self, output, mime_type, *, from_graph = None)")] #[pyo3(signature = (output, mime_type, *, from_graph = None))]
fn dump( fn dump(
&self, &self,
output: PyObject, output: PyObject,
@ -597,10 +593,31 @@ impl PyStore {
} }
} }
/// Returns if the store contains the given named graph.
///
/// :param graph_name: the name of the named graph.
/// :type graph_name: NamedNode or BlankNode or DefaultGraph
/// :rtype: None
/// :raises IOError: if an I/O error happens during the named graph lookup.
///
/// >>> store = Store()
/// >>> store.add_graph(NamedNode('http://example.com/g'))
/// >>> store.contains_named_graph(NamedNode('http://example.com/g'))
/// True
fn contains_named_graph(&self, graph_name: &PyAny) -> PyResult<bool> {
let graph_name = GraphName::from(&PyGraphNameRef::try_from(graph_name)?);
match graph_name {
GraphName::DefaultGraph => Ok(true),
GraphName::NamedNode(graph_name) => self.inner.contains_named_graph(&graph_name),
GraphName::BlankNode(graph_name) => self.inner.contains_named_graph(&graph_name),
}
.map_err(map_storage_error)
}
/// Adds a named graph to the store. /// Adds a named graph to the store.
/// ///
/// :param graph_name: the name of the name graph to add. /// :param graph_name: the name of the name graph to add.
/// :type graph_name: NamedNode or BlankNode /// :type graph_name: NamedNode or BlankNode or DefaultGraph
/// :rtype: None /// :rtype: None
/// :raises IOError: if an I/O error happens during the named graph insertion. /// :raises IOError: if an I/O error happens during the named graph insertion.
/// ///

@ -5,7 +5,9 @@ from tempfile import NamedTemporaryFile, TemporaryFile
from pyoxigraph import Literal, NamedNode, Quad, Triple, parse, serialize from pyoxigraph import Literal, NamedNode, Quad, Triple, parse, serialize
EXAMPLE_TRIPLE = Triple( EXAMPLE_TRIPLE = Triple(
NamedNode("http://example.com/foo"), NamedNode("http://example.com/p"), Literal("1") NamedNode("http://example.com/foo"),
NamedNode("http://example.com/p"),
Literal("éù"),
) )
EXAMPLE_QUAD = Quad( EXAMPLE_QUAD = Quad(
NamedNode("http://example.com/foo"), NamedNode("http://example.com/foo"),
@ -18,7 +20,7 @@ EXAMPLE_QUAD = Quad(
class TestParse(unittest.TestCase): class TestParse(unittest.TestCase):
def test_parse_file(self) -> None: def test_parse_file(self) -> None:
with NamedTemporaryFile() as fp: with NamedTemporaryFile() as fp:
fp.write(b'<foo> <p> "1" .') fp.write('<foo> <p> "éù" .'.encode())
fp.flush() fp.flush()
self.assertEqual( self.assertEqual(
list(parse(fp.name, "text/turtle", base_iri="http://example.com/")), list(parse(fp.name, "text/turtle", base_iri="http://example.com/")),
@ -33,7 +35,7 @@ class TestParse(unittest.TestCase):
self.assertEqual( self.assertEqual(
list( list(
parse( parse(
StringIO('<foo> <p> "1" .'), StringIO('<foo> <p> "éù" .'),
"text/turtle", "text/turtle",
base_iri="http://example.com/", base_iri="http://example.com/",
) )
@ -41,11 +43,23 @@ class TestParse(unittest.TestCase):
[EXAMPLE_TRIPLE], [EXAMPLE_TRIPLE],
) )
def test_parse_long_str_io(self) -> None:
self.assertEqual(
list(
parse(
StringIO('<foo> <p> "éù" .\n' * 1024),
"text/turtle",
base_iri="http://example.com/",
)
),
[EXAMPLE_TRIPLE] * 1024,
)
def test_parse_bytes_io(self) -> None: def test_parse_bytes_io(self) -> None:
self.assertEqual( self.assertEqual(
list( list(
parse( parse(
BytesIO(b'<foo> <p> "1" .'), BytesIO('<foo> <p> "éù" .'.encode()),
"text/turtle", "text/turtle",
base_iri="http://example.com/", base_iri="http://example.com/",
) )
@ -75,15 +89,16 @@ class TestSerialize(unittest.TestCase):
output = BytesIO() output = BytesIO()
serialize([EXAMPLE_TRIPLE], output, "text/turtle") serialize([EXAMPLE_TRIPLE], output, "text/turtle")
self.assertEqual( self.assertEqual(
output.getvalue(), output.getvalue().decode(),
b'<http://example.com/foo> <http://example.com/p> "1" .\n', '<http://example.com/foo> <http://example.com/p> "éù" .\n',
) )
def test_serialize_to_file(self) -> None: def test_serialize_to_file(self) -> None:
with NamedTemporaryFile() as fp: with NamedTemporaryFile() as fp:
serialize([EXAMPLE_TRIPLE], fp.name, "text/turtle") serialize([EXAMPLE_TRIPLE], fp.name, "text/turtle")
self.assertEqual( self.assertEqual(
fp.read(), b'<http://example.com/foo> <http://example.com/p> "1" .\n' fp.read().decode(),
'<http://example.com/foo> <http://example.com/p> "éù" .\n',
) )
def test_serialize_io_error(self) -> None: def test_serialize_io_error(self) -> None:

@ -1,6 +1,6 @@
[package] [package]
name = "oxigraph_server" name = "oxigraph_server"
version = "0.3.16-dev" version = "0.3.17-dev"
authors = ["Tpt <thomas@pellissier-tanon.fr>"] authors = ["Tpt <thomas@pellissier-tanon.fr>"]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
readme = "README.md" readme = "README.md"
@ -17,8 +17,8 @@ anyhow = "1"
oxhttp = { version = "0.1", features = ["rayon"] } oxhttp = { version = "0.1", features = ["rayon"] }
clap = { version = "=4.0", features = ["derive"] } clap = { version = "=4.0", features = ["derive"] }
clap_lex = "=0.3.0" clap_lex = "=0.3.0"
oxigraph = { version = "0.3.16-dev", path = "../lib", features = ["http_client"] } oxigraph = { version = "0.3.17-dev", path = "../lib", features = ["http_client"] }
sparesults = { version = "0.1.7", path = "../lib/sparesults", features = ["rdf-star"] } sparesults = { version = "0.1.8-dev", path = "../lib/sparesults", features = ["rdf-star"] }
rand = "0.8" rand = "0.8"
url = "2" url = "2"
oxiri = "0.2" oxiri = "0.2"

@ -220,6 +220,25 @@ brew install oxigraph
It installs the `oxigraph_server` binary. [See the usage documentation to know how to use it](#usage). It installs the `oxigraph_server` binary. [See the usage documentation to know how to use it](#usage).
## Systemd
It is possible to run Oxigraph in the background using systemd.
For that, you can use the following `oxigraph_server.service` file (it might be inserted into `/etc/systemd/system/` or `$HOME/.config/systemd/user`):
```ini
[Unit]
Description=Oxigraph database server
After=network-online.target
Wants=network-online.target
[Service]
Type=notify
ExecStart=/PATH/TO/oxigraph_server serve --location /PATH/TO/OXIGRAPH/DATA
[Install]
WantedBy=multi-user.target
```
## Migration guide ## Migration guide
### From 0.2 to 0.3 ### From 0.2 to 0.3

@ -17,9 +17,13 @@ use sparesults::{QueryResultsFormat, QueryResultsSerializer};
use std::borrow::Cow; use std::borrow::Cow;
use std::cell::RefCell; use std::cell::RefCell;
use std::cmp::{max, min}; use std::cmp::{max, min};
#[cfg(target_os = "linux")]
use std::env;
use std::ffi::OsStr; use std::ffi::OsStr;
use std::fs::File; use std::fs::File;
use std::io::{self, stdin, stdout, BufRead, BufReader, BufWriter, Read, Write}; use std::io::{self, stdin, stdout, BufRead, BufReader, BufWriter, Read, Write};
#[cfg(target_os = "linux")]
use std::os::unix::net::UnixDatagram;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::rc::Rc; use std::rc::Rc;
use std::str::FromStr; use std::str::FromStr;
@ -591,7 +595,7 @@ pub fn main() -> anyhow::Result<()> {
let mut file = BufWriter::new(File::create(&explain_file)?); let mut file = BufWriter::new(File::create(&explain_file)?);
match explain_file match explain_file
.extension() .extension()
.and_then(|e| e.to_str()) { .and_then(OsStr::to_str) {
Some("json") => { Some("json") => {
explanation.write_in_json(file)?; explanation.write_in_json(file)?;
}, },
@ -734,7 +738,7 @@ fn format_from_path<T>(
path: &Path, path: &Path,
from_extension: impl FnOnce(&str) -> anyhow::Result<T>, from_extension: impl FnOnce(&str) -> anyhow::Result<T>,
) -> anyhow::Result<T> { ) -> anyhow::Result<T> {
if let Some(ext) = path.extension().and_then(|ext| ext.to_str()) { if let Some(ext) = path.extension().and_then(OsStr::to_str) {
from_extension(ext).map_err(|e| { from_extension(ext).map_err(|e| {
e.context(format!( e.context(format!(
"Not able to guess the file format from file name extension '{ext}'" "Not able to guess the file format from file name extension '{ext}'"
@ -776,6 +780,8 @@ fn serve(store: Store, bind: String, read_only: bool, cors: bool) -> anyhow::Res
}; };
server.set_global_timeout(HTTP_TIMEOUT); server.set_global_timeout(HTTP_TIMEOUT);
server.set_server_name(concat!("Oxigraph/", env!("CARGO_PKG_VERSION")))?; server.set_server_name(concat!("Oxigraph/", env!("CARGO_PKG_VERSION")))?;
#[cfg(target_os = "linux")]
systemd_notify_ready()?;
eprintln!("Listening for requests at http://{}", &bind); eprintln!("Listening for requests at http://{}", &bind);
server.listen(bind)?; server.listen(bind)?;
Ok(()) Ok(())
@ -1531,19 +1537,24 @@ fn web_load_graph(
format: GraphFormat, format: GraphFormat,
to_graph_name: GraphNameRef<'_>, to_graph_name: GraphNameRef<'_>,
) -> Result<(), HttpError> { ) -> Result<(), HttpError> {
let base_iri = if let GraphNameRef::NamedNode(graph_name) = to_graph_name {
Some(graph_name.as_str())
} else {
None
};
if url_query_parameter(request, "no_transaction").is_some() { if url_query_parameter(request, "no_transaction").is_some() {
web_bulk_loader(store, request).load_graph( web_bulk_loader(store, request).load_graph(
BufReader::new(request.body_mut()), BufReader::new(request.body_mut()),
format, format,
to_graph_name, to_graph_name,
None, base_iri,
) )
} else { } else {
store.load_graph( store.load_graph(
BufReader::new(request.body_mut()), BufReader::new(request.body_mut()),
format, format,
to_graph_name, to_graph_name,
None, base_iri,
) )
} }
.map_err(loader_to_http_error) .map_err(loader_to_http_error)
@ -1636,7 +1647,7 @@ impl<O: 'static, U: (Fn(O) -> io::Result<Option<O>>) + 'static> ReadForWrite<O,
) -> Result<Response, HttpError> { ) -> Result<Response, HttpError> {
let buffer = Rc::new(RefCell::new(Vec::new())); let buffer = Rc::new(RefCell::new(Vec::new()));
let state = initial_state_builder(ReadForWriteWriter { let state = initial_state_builder(ReadForWriteWriter {
buffer: buffer.clone(), buffer: Rc::clone(&buffer),
}) })
.map_err(internal_server_error)?; .map_err(internal_server_error)?;
Ok(Response::builder(Status::OK) Ok(Response::builder(Status::OK)
@ -1698,6 +1709,14 @@ impl Write for ReadForWriteWriter {
} }
} }
#[cfg(target_os = "linux")]
fn systemd_notify_ready() -> io::Result<()> {
if let Some(path) = env::var_os("NOTIFY_SOCKET") {
UnixDatagram::unbound()?.send_to(b"READY=1", path)?;
}
Ok(())
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -2367,6 +2386,53 @@ mod tests {
) )
} }
#[test]
fn graph_store_base_url() -> Result<()> {
let server = ServerTest::new()?;
// POST
let request = Request::builder(
Method::POST,
"http://localhost/store?graph=http://example.com".parse()?,
)
.with_header(HeaderName::CONTENT_TYPE, "text/turtle")?
.with_body("<> <http://example.com/p> <http://example.com/o1> .");
server.test_status(request, Status::NO_CONTENT)?;
// GET
let request = Request::builder(
Method::GET,
"http://localhost/store?graph=http://example.com".parse()?,
)
.with_header(HeaderName::ACCEPT, "application/n-triples")?
.build();
server.test_body(
request,
"<http://example.com> <http://example.com/p> <http://example.com/o1> .\n",
)?;
// PUT
let request = Request::builder(
Method::PUT,
"http://localhost/store?graph=http://example.com".parse()?,
)
.with_header(HeaderName::CONTENT_TYPE, "text/turtle")?
.with_body("<> <http://example.com/p> <http://example.com/o2> .");
server.test_status(request, Status::NO_CONTENT)?;
// GET
let request = Request::builder(
Method::GET,
"http://localhost/store?graph=http://example.com".parse()?,
)
.with_header(HeaderName::ACCEPT, "application/n-triples")?
.build();
server.test_body(
request,
"<http://example.com> <http://example.com/p> <http://example.com/o2> .\n",
)
}
#[test] #[test]
fn graph_store_protocol() -> Result<()> { fn graph_store_protocol() -> Result<()> {
// Tests from https://www.w3.org/2009/sparql/docs/tests/data-sparql11/http-rdf-update/ // Tests from https://www.w3.org/2009/sparql/docs/tests/data-sparql11/http-rdf-update/

@ -1,3 +0,0 @@
PREFIX ex: <http://example.com/>
INSERT DATA { ex:s ex:salary 1200 . ex:s2 ex:salary 1250 . ex:s3 ex:salary 1280 . ex:boss ex:salary 1600 . };
DELETE { ?s ex:salary ?o } INSERT { ?s ex:salary ?v } WHERE { ?s ex:salary ?o FILTER(?o < 1500) BIND(?o + 100 AS ?v) }

@ -1,6 +0,0 @@
@prefix ex: <http://example.com/> .
ex:s ex:salary 1300 .
ex:s2 ex:salary 1350 .
ex:s3 ex:salary 1380 .
ex:boss ex:salary 1600 .

@ -0,0 +1 @@
SELECT ?r WHERE { BIND((?foo IN ()) AS ?r) }

@ -1,8 +1,9 @@
<?xml version="1.0"?> <?xml version="1.0"?>
<sparql xmlns="http://www.w3.org/2005/sparql-results#"> <sparql xmlns="http://www.w3.org/2005/sparql-results#">
<head> <head>
<variable name="v"/> <variable name="r"/>
</head> </head>
<results> <results>
<result></result>
</results> </results>
</sparql> </sparql>

@ -23,17 +23,14 @@
:values_in_filter_not_exists :values_in_filter_not_exists
:subquery_in_filter_not_exists :subquery_in_filter_not_exists
:cmp_langString :cmp_langString
:halloween_problem
:nested_path :nested_path
:nested_expression :nested_expression
:order_terms :order_terms
:nested_anonymous :nested_anonymous
:unbound_variable_in_subquery :unbound_variable_in_subquery
:values_too_many
:values_too_few
:values_property_path_all
:one_or_more_shared :one_or_more_shared
:one_or_more_star :one_or_more_star
:in_empty_error
) . ) .
:small_unicode_escape_with_multibytes_char rdf:type mf:NegativeSyntaxTest ; :small_unicode_escape_with_multibytes_char rdf:type mf:NegativeSyntaxTest ;
@ -105,11 +102,6 @@
mf:action [ qt:query <cmp_langString.rq> ] ; mf:action [ qt:query <cmp_langString.rq> ] ;
mf:result <cmp_langString.srx> . mf:result <cmp_langString.srx> .
:halloween_problem rdf:type mf:UpdateEvaluationTest ;
mf:name "Halloween Problem: An update operation should not be able to read its own writes" ;
mf:action [ ut:request <halloween_problem.ru> ] ;
mf:result [ ut:data <halloween_problem_result.ttl> ] .
:nested_path rdf:type mf:PositiveSyntaxTest11 ; :nested_path rdf:type mf:PositiveSyntaxTest11 ;
mf:name "A very nested property path" ; mf:name "A very nested property path" ;
mf:action <nested_path.rq> . mf:action <nested_path.rq> .
@ -136,20 +128,6 @@
[ qt:query <unbound_variable_in_subquery.rq> ] ; [ qt:query <unbound_variable_in_subquery.rq> ] ;
mf:result <unbound_variable_in_subquery.srx> . mf:result <unbound_variable_in_subquery.srx> .
:values_too_many rdf:type mf:NegativeSyntaxTest11 ;
mf:name "Too many values in a VALUE clause compared to the number of variable" ;
mf:action <values_too_many.rq> .
:values_too_few rdf:type mf:NegativeSyntaxTest11 ;
mf:name "Too few values in a VALUE clause compared to the number of variable" ;
mf:action <values_too_few.rq> .
:values_property_path_all rdf:type mf:QueryEvaluationTest ;
mf:name "ZeroOrX property paths should only return terms in the graph and not also terms defined in the query" ;
mf:action
[ qt:query <values_property_path_all.rq> ] ;
mf:result <values_property_path_all.srx> .
:one_or_more_shared rdf:type mf:QueryEvaluationTest ; :one_or_more_shared rdf:type mf:QueryEvaluationTest ;
mf:name "SPARQL one or more with shared variable" ; mf:name "SPARQL one or more with shared variable" ;
mf:action mf:action
@ -163,3 +141,8 @@
[ qt:query <one_or_more_star.rq> ; [ qt:query <one_or_more_star.rq> ;
qt:data <one_or_more_star.ttl> ] ; qt:data <one_or_more_star.ttl> ] ;
mf:result <one_or_more_star.srx> . mf:result <one_or_more_star.srx> .
:in_empty_error rdf:type mf:QueryEvaluationTest ;
mf:name "IN should propagate errors on the left side, even on the empty input" ;
mf:action [ qt:query <in_empty_error.rq> ] ;
mf:result <in_empty_error.srx> .

@ -1,4 +0,0 @@
SELECT * WHERE {
VALUES ?v { 1 }
?v <http://example.com/p>? ?v
}

@ -1 +0,0 @@
SELECT * WHERE { VALUES (?a ?b) { (1) } }

@ -1 +0,0 @@
SELECT * WHERE { VALUES (?a ?b) { (1 2 3) } }

@ -1 +1 @@
Subproject commit 52be3f1b99a7890ec1266bac7b52be19a85a720c Subproject commit 9d70ac9298f494bfc3a2becabc8fa8bc3d169685

@ -347,7 +347,7 @@ impl<'a> Iterator for RdfListIterator<'a> {
let result = self let result = self
.graph .graph
.object_for_subject_predicate(current, rdf::FIRST) .object_for_subject_predicate(current, rdf::FIRST)
.map(|v| v.into_owned()); .map(TermRef::into_owned);
self.current_node = self.current_node =
match self.graph.object_for_subject_predicate(current, rdf::REST) { match self.graph.object_for_subject_predicate(current, rdf::REST) {
Some(TermRef::NamedNode(n)) if n == rdf::NIL => None, Some(TermRef::NamedNode(n)) if n == rdf::NIL => None,

@ -287,21 +287,11 @@ fn evaluate_update_evaluation_test(test: &Test) -> Result<()> {
} }
fn load_sparql_query_result(url: &str) -> Result<StaticQueryResults> { fn load_sparql_query_result(url: &str) -> Result<StaticQueryResults> {
if url.ends_with(".srx") { if let Some(format) = url
StaticQueryResults::from_query_results( .rsplit_once('.')
QueryResults::read(read_file(url)?, QueryResultsFormat::Xml)?, .and_then(|(_, extension)| QueryResultsFormat::from_extension(extension))
false, {
) StaticQueryResults::from_query_results(QueryResults::read(read_file(url)?, format)?, false)
} else if url.ends_with(".srj") {
StaticQueryResults::from_query_results(
QueryResults::read(read_file(url)?, QueryResultsFormat::Json)?,
false,
)
} else if url.ends_with(".tsv") {
StaticQueryResults::from_query_results(
QueryResults::read(read_file(url)?, QueryResultsFormat::Tsv)?,
false,
)
} else { } else {
StaticQueryResults::from_graph(&load_graph(url, guess_graph_format(url)?)?) StaticQueryResults::from_graph(&load_graph(url, guess_graph_format(url)?)?)
} }
@ -505,7 +495,7 @@ impl StaticQueryResults {
fn from_graph(graph: &Graph) -> Result<Self> { fn from_graph(graph: &Graph) -> Result<Self> {
// Hack to normalize literals // Hack to normalize literals
let store = Store::new().unwrap(); let store = Store::new()?;
for t in graph.iter() { for t in graph.iter() {
store store
.insert(t.in_graph(GraphNameRef::DefaultGraph)) .insert(t.in_graph(GraphNameRef::DefaultGraph))
@ -617,12 +607,12 @@ fn results_diff(expected: StaticQueryResults, actual: StaticQueryResults) -> Str
format_diff( format_diff(
&expected_variables &expected_variables
.iter() .iter()
.map(|v| v.to_string()) .map(ToString::to_string)
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join("\n"), .join("\n"),
&actual_variables &actual_variables
.iter() .iter()
.map(|v| v.to_string()) .map(ToString::to_string)
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join("\n"), .join("\n"),
"variables", "variables",

Loading…
Cancel
Save