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

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

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

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

@ -281,7 +281,7 @@ jobs:
python-version: "3.10"
cache: pip
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: pip install --no-index --find-links=target/wheels/ pyoxigraph
- run: rm -r target/wheels
@ -347,29 +347,18 @@ jobs:
minimize-crashes: true
parallel-fuzzing: true
storage-repo: https://${{ secrets.FULL_ACCESS_TOKEN }}@github.com/oxigraph/clusterfuzzlite-oxigraph.git
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 }}
continue-on-error: true
- uses: google/clusterfuzzlite/actions/run_fuzzers@v1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
fuzz-seconds: 14400
fuzz-seconds: 3600
mode: prune
storage-repo: https://${{ secrets.FULL_ACCESS_TOKEN }}@github.com/oxigraph/clusterfuzzlite-oxigraph.git
continue-on-error: true
fuzz_coverage:
if: github.event_name != 'pull_request'
needs: fuzz_prune
needs: fuzz_repo
runs-on: ubuntu-latest
steps:
- uses: google/clusterfuzzlite/actions/build_fuzzers@v1
@ -379,7 +368,14 @@ jobs:
- uses: google/clusterfuzzlite/actions/run_fuzzers@v1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
fuzz-seconds: 600
fuzz-seconds: 3600
mode: coverage
sanitizer: coverage
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
### Added

113
Cargo.lock generated

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

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

@ -2,6 +2,8 @@
DATASET_SIZE=100000
PARALLELISM=16
set -eu
wget -nc https://github.com/blazegraph/database/releases/download/BLAZEGRAPH_RELEASE_2_1_5/blazegraph.jar
cd bsbm-tools || exit
./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/businessIntelligence/sparql.txt -o "../bsbm.businessIntelligence.blazegraph.2.1.5.${DATASET_SIZE}.${PARALLELISM}.xml" http://localhost:9999/blazegraph/sparql
kill $!
rm blazegraph.jnl
rm "explore-${DATASET_SIZE}.nt"
rm "explore-update-${DATASET_SIZE}.nt"
rm -r td_data
rm -f blazegraph.jnl
rm -f "explore-${DATASET_SIZE}.nt"
rm -f "explore-update-${DATASET_SIZE}.nt"
rm -rf td_data

@ -3,7 +3,8 @@
DATASET_SIZE=100000
PARALLELISM=16
VERSION="9.3.3"
JAVA_HOME=/usr/lib/jvm/java-11-openjdk
set -eu
cd bsbm-tools
./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 &
@ -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
kill $!
sleep 5
rm -r ../graphdb-free-9.3.3/data
rm "explore-${DATASET_SIZE}.nt"
rm "explore-update-${DATASET_SIZE}.nt"
rm -r td_data
rm -rf ../graphdb-free-9.3.3/data
rm -f "explore-${DATASET_SIZE}.nt"
rm -f "explore-update-${DATASET_SIZE}.nt"
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.
PARALLELISM=16
VERSION="4.3.2"
set -eu
wget -nc https://downloads.apache.org/jena/binaries/apache-jena-${VERSION}.zip
cd bsbm-tools || exit
./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/businessIntelligence/sparql.txt -o "../bsbm.businessIntelligence.jena.${VERSION}.${DATASET_SIZE}.${PARALLELISM}.xml" http://localhost:3030/bsbm/query
kill $!
rm "explore-${DATASET_SIZE}.nt"
rm "explore-update-${DATASET_SIZE}.nt"
rm -r td_data
rm -r run
rm -r apache-jena-${VERSION}
rm -r apache-jena-fuseki-${VERSION}
rm -f "explore-${DATASET_SIZE}.nt"
rm -f "explore-update-${DATASET_SIZE}.nt"
rm -rf td_data
rm -rf run
rm -rf apache-jena-${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.
PARALLELISM=16
set -eu
cd bsbm-tools
./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"
@ -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/businessIntelligence/sparql.txt -o "../bsbm.businessIntelligence.${VERSION}.${DATASET_SIZE}.${PARALLELISM}.xml" "http://127.0.0.1:7878/query"
kill $!
rm -r oxigraph_data
rm "explore-${DATASET_SIZE}.nt"
rm "explore-update-${DATASET_SIZE}.nt"
rm -r td_data
rm -rf oxigraph_data
rm -f "explore-${DATASET_SIZE}.nt"
rm -f "explore-update-${DATASET_SIZE}.nt"
rm -rf td_data

@ -4,6 +4,8 @@ DATASET_SIZE=100000
PARALLELISM=16
VERSION="4.2.2"
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 "tomcat-${TOMCAT_VERSION}.zip" "https://dlcdn.apache.org/tomcat/tomcat-9/v${TOMCAT_VERSION}/bin/apache-tomcat-${TOMCAT_VERSION}.zip"
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/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
rm "explore-${DATASET_SIZE}.nt"
rm "explore-update-${DATASET_SIZE}.nt"
rm -r td_data
rm -r "eclipse-rdf4j-${VERSION}"
rm -r "apache-tomcat-${TOMCAT_VERSION}"
rm -f "explore-${DATASET_SIZE}.nt"
rm -f "explore-update-${DATASET_SIZE}.nt"
rm -rf td_data
rm -rf "eclipse-rdf4j-${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.
PARALLELISM=16
VERSION="7.2.5"
set -eu
cd bsbm-tools
./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
@ -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/businessIntelligence/sparql.txt -o "../bsbm.businessIntelligence.virtuoso.${VERSION}.${DATASET_SIZE}.${PARALLELISM}.xml" 'http://localhost:8890/sparql?graph-uri=urn:graph:test'
kill $!
rm -r ../database
rm "explore-${DATASET_SIZE}.nt"
rm "explore-update-${DATASET_SIZE}.nt"
rm -r td_data
rm -rf ../database
rm -f "explore-${DATASET_SIZE}.nt"
rm -f "explore-update-${DATASET_SIZE}.nt"
rm -rf td_data

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

@ -5,10 +5,10 @@
"devDependencies": {
"@rdfjs/data-model": "^2.0.1",
"mocha": "^10.0.0",
"rome": "^11.0.0"
"rome": "^12.0.0"
},
"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",
"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",

@ -6,5 +6,8 @@
},
"linter": {
"ignore": ["pkg"]
},
"organizeImports": {
"enabled": true
}
}

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

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

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

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

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

@ -1,6 +1,6 @@
[package]
name = "oxrdf"
version = "0.1.5"
version = "0.1.6-dev"
authors = ["Tpt <thomas@pellissier-tanon.fr>"]
license = "MIT OR Apache-2.0"
readme = "README.md"
@ -21,7 +21,7 @@ rdf-star = []
rand = "0.8"
oxilangtag = "0.1"
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]
all-features = true

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

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

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

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

@ -43,14 +43,14 @@ impl From<Decimal> for Boolean {
impl From<Float> for Boolean {
#[inline]
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 {
#[inline]
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 crate::parser::{
g_day_lexical_rep, g_month_day_lexical_rep, g_month_lexical_rep, g_year_lexical_rep,
g_year_month_lexical_rep,
parse_date, parse_date_time, parse_g_day, parse_g_month, parse_g_month_day, parse_g_year,
parse_g_year_month, parse_time,
};
use std::cmp::{min, Ordering};
use std::error::Error;
@ -44,6 +43,7 @@ impl DateTime {
})
}
/// [fn:current-dateTime](https://www.w3.org/TR/xpath-functions/#func-current-dateTime)
#[inline]
pub fn now() -> Result<Self, DateTimeError> {
Ok(Self {
@ -187,7 +187,10 @@ impl DateTime {
self.checked_sub_day_time_duration(rhs)
} else {
Some(Self {
timestamp: Timestamp::new(&date_time_plus_duration(-rhs, &self.properties())?)
timestamp: Timestamp::new(&date_time_plus_duration(
rhs.checked_neg()?,
&self.properties(),
)?)
.ok()?,
})
}
@ -230,7 +233,7 @@ impl FromStr for DateTime {
type Err = 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)
#[inline]
pub fn hour(&self) -> u8 {
@ -435,7 +444,7 @@ impl FromStr for Time {
type Err = 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)
#[inline]
pub fn year(&self) -> i64 {
@ -632,7 +647,7 @@ impl FromStr for Date {
type Err = 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;
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;
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;
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;
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;
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::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_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)
///
@ -22,10 +21,9 @@ pub struct Decimal {
impl Decimal {
/// Constructs the decimal i / 10^n
#[allow(clippy::cast_possible_truncation)]
#[inline]
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)
.ok_or(DecimalOverflowError)?;
Ok(Self {
@ -66,29 +64,69 @@ impl Decimal {
/// [op:numeric-multiply](https://www.w3.org/TR/xpath-functions/#func-numeric-multiply)
#[inline]
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 {
value: self
.value
.checked_div(DECIMAL_PART_HALF_POW)?
.checked_mul(rhs.into().value.checked_div(DECIMAL_PART_HALF_POW)?)?,
value: left
.checked_mul(right)?
.checked_mul(10_i128.checked_pow(shift)?)?,
})
}
/// [op:numeric-divide](https://www.w3.org/TR/xpath-functions/#func-numeric-divide)
#[inline]
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 {
value: self
.value
.checked_mul(DECIMAL_PART_HALF_POW)?
.checked_div(rhs.into().value)?
.checked_mul(DECIMAL_PART_HALF_POW)?,
value: left
.checked_div(right)?
.checked_div(10_i128.checked_pow(shift)?)?,
})
}
/// TODO: XSD? is well defined for not integer
/// [op:numeric-mod](https://www.w3.org/TR/xpath-functions/#func-numeric-mod)
#[inline]
pub fn checked_rem(&self, rhs: impl Into<Self>) -> Option<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)
#[inline]
pub const fn abs(&self) -> Self {
@ -174,9 +220,7 @@ impl Decimal {
pub const MAX: Self = Self { value: i128::MAX };
#[cfg(test)]
pub(super) const fn step() -> Self {
Self { value: 1 }
}
pub const STEP: Self = Self { value: 1 };
}
impl From<bool> for Decimal {
@ -316,13 +360,10 @@ impl TryFrom<Double> for Decimal {
#[inline]
#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
fn try_from(value: Double) -> Result<Self, DecimalOverflowError> {
let shifted = value * Double::from(DECIMAL_PART_POW as f64);
if shifted.is_finite()
&& Double::from(i128::MIN as f64) <= shifted
&& shifted <= Double::from(i128::MAX as f64)
{
let shifted = f64::from(value) * (DECIMAL_PART_POW as f64);
if shifted.is_finite() && (i128::MIN as f64) <= shifted && shifted <= (i128::MAX as f64) {
Ok(Self {
value: f64::from(shifted) as i128,
value: shifted as i128,
})
} else {
Err(DecimalOverflowError)
@ -334,7 +375,7 @@ impl From<Decimal> for Float {
#[inline]
#[allow(clippy::cast_precision_loss)]
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]
#[allow(clippy::cast_precision_loss)]
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() {
Some(b'+') => (1, &input[1..]),
Some(b'-') => (-1, &input[1..]),
Some(b'+') => (1_i128, &input[1..]),
Some(b'-') => (-1_i128, &input[1..]),
_ => (1, input),
};
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() {
if c.is_ascii_digit() {
value = value
.checked_mul(10)
.ok_or(PARSE_OVERFLOW)?
.checked_add((*c - b'0').into())
.checked_add(sign * i128::from(*c - b'0'))
.ok_or(PARSE_OVERFLOW)?;
input = &input[1..];
} else {
@ -414,7 +466,7 @@ impl FromStr for Decimal {
value = value
.checked_mul(10)
.ok_or(PARSE_OVERFLOW)?
.checked_add((*c - b'0').into())
.checked_add(sign * i128::from(*c - b'0'))
.ok_or(PARSE_OVERFLOW)?;
input = &input[1..];
} else {
@ -431,11 +483,7 @@ impl FromStr for Decimal {
}
Ok(Self {
value: value
.checked_mul(sign)
.ok_or(PARSE_OVERFLOW)?
.checked_mul(exp)
.ok_or(PARSE_OVERFLOW)?,
value: value.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) })
.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() {
if self.value.is_negative() {
width -= 1;
}
if last_non_zero - DECIMAL_PART_DIGITS + 1 < width {
DECIMAL_PART_DIGITS + width
if last_non_zero - decimal_part_digits + 1 < width {
decimal_part_digits + width
} else {
last_non_zero + 1
}
} else {
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))?;
}
} else {
f.write_char('0')?
}
if DECIMAL_PART_DIGITS > first_non_zero {
if decimal_part_digits > first_non_zero {
f.write_char('.')?;
let start = if let Some(precision) = f.precision() {
if DECIMAL_PART_DIGITS - first_non_zero > precision {
DECIMAL_PART_DIGITS - precision
if decimal_part_digits - first_non_zero > precision {
decimal_part_digits - precision
} else {
first_non_zero
}
} else {
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))?;
}
}
@ -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(&Decimal::MAX.to_string())?, Decimal::MAX);
assert_eq!(
Decimal::from_str(
&Decimal::MIN
.checked_add(Decimal::step())
.unwrap()
.to_string()
)?,
Decimal::MIN.checked_add(Decimal::step()).unwrap()
);
assert_eq!(Decimal::from_str(&Decimal::MIN.to_string())?, Decimal::MIN);
assert!(Decimal::from_str("0.0000000000000000001").is_err());
assert!(Decimal::from_str("1000000000000000000000").is_err());
assert_eq!(
@ -663,58 +704,101 @@ mod tests {
#[test]
fn add() {
assert!(Decimal::MIN.checked_add(Decimal::step()).is_some());
assert!(Decimal::MAX.checked_add(Decimal::step()).is_none());
assert!(Decimal::MIN.checked_add(Decimal::STEP).is_some());
assert!(Decimal::MAX.checked_add(Decimal::STEP).is_none());
assert_eq!(
Decimal::MAX.checked_add(Decimal::MIN),
Some(-Decimal::step())
Decimal::STEP.checked_neg()
);
}
#[test]
fn sub() {
assert!(Decimal::MIN.checked_sub(Decimal::step()).is_none());
assert!(Decimal::MAX.checked_sub(Decimal::step()).is_some());
assert!(Decimal::MIN.checked_sub(Decimal::STEP).is_none());
assert!(Decimal::MAX.checked_sub(Decimal::STEP).is_some());
}
#[test]
fn mul() -> Result<(), ParseDecimalError> {
assert_eq!(Decimal::from(1).checked_mul(-1), Some(Decimal::from(-1)));
assert_eq!(
Decimal::from_str("1")?.checked_mul(Decimal::from_str("-1")?),
Some(Decimal::from_str("-1")?)
);
assert_eq!(
Decimal::from_str("1000")?.checked_mul(Decimal::from_str("1000")?),
Some(Decimal::from_str("1000000")?)
Decimal::from(1000).checked_mul(1000),
Some(Decimal::from(1_000_000))
);
assert_eq!(
Decimal::from_str("0.1")?.checked_mul(Decimal::from_str("0.01")?),
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(())
}
#[test]
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!(
Decimal::from_str("1")?.checked_div(Decimal::from_str("1")?),
Some(Decimal::from_str("1")?)
Decimal::from(10).checked_div(100),
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!(
Decimal::from_str("100")?.checked_div(Decimal::from_str("10")?),
Some(Decimal::from_str("10")?)
Decimal::MAX.checked_div(-1),
Some(Decimal::MIN.checked_add(Decimal::STEP).unwrap())
);
assert_eq!(Decimal::MIN.checked_div(-1), None);
assert_eq!(
Decimal::from_str("10")?.checked_div(Decimal::from_str("100")?),
Some(Decimal::from_str("0.1")?)
Decimal::MIN
.checked_add(Decimal::STEP)
.unwrap()
.checked_div(-1),
Some(Decimal::MAX)
);
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]
fn round() -> Result<(), ParseDecimalError> {
assert_eq!(Decimal::from_str("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(-10).round(), Decimal::from(-10));
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.5")?.round(), Decimal::from(-2));
@ -725,8 +809,8 @@ mod tests {
#[test]
fn ceil() -> Result<(), ParseDecimalError> {
assert_eq!(Decimal::from_str("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(-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(-10));
assert_eq!(Decimal::from(i64::MIN).ceil(), Decimal::from(i64::MIN));
@ -736,8 +820,8 @@ mod tests {
#[test]
fn floor() -> Result<(), ParseDecimalError> {
assert_eq!(Decimal::from_str("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(-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(-11));
assert_eq!(Decimal::from(i64::MIN).floor(), Decimal::from(i64::MIN));
@ -780,11 +864,11 @@ mod tests {
fn from_float() -> Result<(), ParseDecimalError> {
assert_eq!(
Decimal::try_from(Float::from(0.)).ok(),
Some(Decimal::from_str("0")?)
Some(Decimal::from(0))
);
assert_eq!(
Decimal::try_from(Float::from(-0.)).ok(),
Some(Decimal::from_str("0.")?)
Some(Decimal::from(0))
);
assert_eq!(
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::MAX)).is_err());
assert!(
Decimal::try_from(Float::from(1_672_507_302_466.))
Decimal::try_from(Float::from(1_672_507_300_000.))
.unwrap()
.checked_sub(Decimal::from_str("1672507302466")?)
.checked_sub(Decimal::from(1_672_507_293_696_i64))
.unwrap()
.abs()
< Decimal::from(1_000_000)
< Decimal::from(1)
);
Ok(())
}
@ -810,11 +894,11 @@ mod tests {
fn from_double() -> Result<(), ParseDecimalError> {
assert_eq!(
Decimal::try_from(Double::from(0.)).ok(),
Some(Decimal::from_str("0")?)
Some(Decimal::from(0))
);
assert_eq!(
Decimal::try_from(Double::from(-0.)).ok(),
Some(Decimal::from_str("0")?)
Some(Decimal::from(0))
);
assert_eq!(
Decimal::try_from(Double::from(-123.1)).ok(),
@ -823,7 +907,7 @@ mod tests {
assert!(
Decimal::try_from(Double::from(1_672_507_302_466.))
.unwrap()
.checked_sub(Decimal::from_str("1672507302466")?)
.checked_sub(Decimal::from(1_672_507_302_466_i64))
.unwrap()
.abs()
< Decimal::from(1)
@ -836,6 +920,34 @@ mod tests {
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]
fn minimally_conformant() -> Result<(), ParseDecimalError> {
// 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()
}
#[inline]
pub fn is_nan(self) -> bool {
self.value.is_nan()
}
#[deprecated(note = "Use .is_nan()")]
#[inline]
pub fn is_naan(self) -> bool {
self.value.is_nan()
@ -68,6 +74,20 @@ impl Double {
pub fn is_identical_with(&self, other: &Self) -> bool {
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 {
@ -243,7 +263,7 @@ mod tests {
#[test]
fn eq() {
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.));
}
@ -254,18 +274,15 @@ mod tests {
Some(Ordering::Equal)
);
assert_eq!(
Double::from(f64::INFINITY).partial_cmp(&Double::from(f64::MAX)),
Double::INFINITY.partial_cmp(&Double::MAX),
Some(Ordering::Greater)
);
assert_eq!(
Double::from(f64::NEG_INFINITY).partial_cmp(&Double::from(f64::MIN)),
Double::NEG_INFINITY.partial_cmp(&Double::MIN),
Some(Ordering::Less)
);
assert_eq!(Double::from(f64::NAN).partial_cmp(&Double::from(0.)), None);
assert_eq!(
Double::from(f64::NAN).partial_cmp(&Double::from(f64::NAN)),
None
);
assert_eq!(Double::NAN.partial_cmp(&Double::from(0.)), None);
assert_eq!(Double::NAN.partial_cmp(&Double::NAN), None);
assert_eq!(
Double::from(0.).partial_cmp(&Double::from(-0.)),
Some(Ordering::Equal)
@ -275,7 +292,7 @@ mod tests {
#[test]
fn is_identical_with() {
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.)));
}
@ -297,11 +314,11 @@ mod tests {
assert_eq!(Double::from_str("-1.")?.to_string(), "-1");
assert_eq!(
Double::from_str(&f64::MIN.to_string()).unwrap(),
Double::from(f64::MIN)
Double::MIN
);
assert_eq!(
Double::from_str(&f64::MAX.to_string()).unwrap(),
Double::from(f64::MAX)
Double::MAX
);
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).
#[inline]
pub fn is_identical_with(&self, other: &Self) -> bool {
@ -127,7 +135,7 @@ impl FromStr for Duration {
type Err = 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 m = (s_int % 3600) / 60;
let s = ss
.checked_sub(Decimal::try_from(d * 86400 + h * 3600 + m * 60).unwrap())
.unwrap(); //could not fail
.checked_sub(
Decimal::try_from(d * 86400 + h * 3600 + m * 60).map_err(|_| fmt::Error)?,
)
.ok_or(fmt::Error)?;
if d != 0 {
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).
#[inline]
pub fn is_identical_with(&self, other: &Self) -> bool {
@ -333,7 +350,7 @@ impl FromStr for YearMonthDuration {
type Err = 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).
#[inline]
pub fn is_identical_with(&self, other: &Self) -> bool {
@ -513,7 +537,7 @@ impl FromStr for DayTimeDuration {
type Err = 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> {
let min = Duration::new(
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);

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

@ -58,6 +58,7 @@ impl Integer {
})
}
/// [op:numeric-mod](https://www.w3.org/TR/xpath-functions/#func-numeric-mod)
#[inline]
pub fn checked_rem(&self, rhs: impl Into<Self>) -> Option<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)
#[inline]
pub const fn abs(&self) -> Self {
@ -95,6 +104,10 @@ impl Integer {
pub fn is_identical_with(&self, other: &Self) -> bool {
self == other
}
pub const MIN: Self = Self { value: i64::MIN };
pub const MAX: Self = Self { value: i64::MAX };
}
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::MAX)).is_err());
assert!(
Integer::try_from(Float::from(1_672_507_302_466.))
Integer::try_from(Float::from(1_672_507_300_000.))
.unwrap()
.checked_sub(Integer::from_str("1672507302466")?)
.checked_sub(Integer::from_str("1672507300000")?)
.unwrap()
.abs()
< Integer::from(1_000_000)
@ -283,12 +296,12 @@ mod tests {
Some(Integer::from_str("-123")?)
);
assert!(
Integer::try_from(Double::from(1_672_507_302_466.))
Integer::try_from(Double::from(1_672_507_300_000.))
.unwrap()
.checked_sub(Integer::from_str("1672507302466").unwrap())
.checked_sub(Integer::from_str("1672507300000").unwrap())
.unwrap()
.abs()
< Integer::from(1)
< Integer::from(10)
);
assert!(Integer::try_from(Double::from(f64::NAN)).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());
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")]
#![deny(unsafe_code)]
#![doc(test(attr(deny(warnings))))]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![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")]
#![allow(clippy::return_self_not_must_use)]
mod boolean;
mod date_time;

@ -2,15 +2,6 @@ use super::date_time::{DateTimeError, GDay, GMonth, GMonthDay, GYear, GYearMonth
use super::decimal::ParseDecimalError;
use super::duration::{DayTimeDuration, YearMonthDuration};
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::fmt;
use std::num::ParseIntError;
@ -24,46 +15,35 @@ pub struct XsdParseError {
#[derive(Debug, Clone)]
enum XsdParseErrorKind {
NomKind(ErrorKind),
NomChar(char),
MissingData(Needed),
TooMuchData { count: usize },
Overflow,
ParseInt(ParseIntError),
ParseDecimal(ParseDecimalError),
OutOfIntegerRange { value: u8, min: u8, max: u8 },
DateTime(DateTimeError),
Message(&'static str),
}
const OVERFLOW_ERROR: XsdParseError = XsdParseError {
kind: XsdParseErrorKind::Message("Overflow error"),
};
impl fmt::Display for XsdParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
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) => {
write!(f, "Error while parsing integer: {error}")
}
XsdParseErrorKind::ParseDecimal(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::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::ParseDecimal(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 {
fn from(error: ParseIntError) -> Self {
Self {
@ -125,412 +83,538 @@ impl From<DateTimeError> for XsdParseError {
}
}
impl From<Err<Self>> for XsdParseError {
fn from(err: Err<Self>) -> Self {
match err {
Err::Incomplete(needed) => Self {
kind: XsdParseErrorKind::MissingData(needed),
},
Err::Error(e) | Err::Failure(e) => e,
}
}
}
type XsdResult<'a, T> = IResult<&'a str, T, XsdParseError>;
const OVERFLOW_ERROR: XsdParseError = XsdParseError {
kind: XsdParseErrorKind::Overflow,
};
pub fn parse_value<'a, T>(
mut f: impl FnMut(&'a str) -> XsdResult<'a, T>,
input: &'a str,
) -> Result<T, XsdParseError> {
let (left, result) = f(input)?;
if left.is_empty() {
Ok(result)
} else {
Err(XsdParseError {
kind: XsdParseErrorKind::TooMuchData { count: left.len() },
})
}
}
//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'
fn du_month_frag(input: &str) -> XsdResult<'_, i64> {
terminated(unsigned_no_decimal_pt_numeral, char('M'))(input)
}
// [8] duDayFrag ::= unsignedNoDecimalPtNumeral 'D'
fn du_day_frag(input: &str) -> XsdResult<'_, i64> {
terminated(unsigned_no_decimal_pt_numeral, char('D'))(input)
}
// [9] duHourFrag ::= unsignedNoDecimalPtNumeral 'H'
fn du_hour_frag(input: &str) -> XsdResult<'_, i64> {
terminated(unsigned_no_decimal_pt_numeral, char('H'))(input)
}
// [10] duMinuteFrag ::= unsignedNoDecimalPtNumeral 'M'
fn du_minute_frag(input: &str) -> XsdResult<'_, i64> {
terminated(unsigned_no_decimal_pt_numeral, char('M'))(input)
}
// [11] duSecondFrag ::= (unsignedNoDecimalPtNumeral | unsignedDecimalPtNumeral) 'S'
fn du_second_frag(input: &str) -> XsdResult<'_, Decimal> {
terminated(
map_res(
recognize(tuple((digit0, opt(preceded(char('.'), digit0))))),
Decimal::from_str,
),
char('S'),
)(input)
}
// [12] duYearMonthFrag ::= (duYearFrag duMonthFrag?) | duMonthFrag
fn du_year_month_frag(input: &str) -> XsdResult<'_, i64> {
alt((
map(tuple((du_year_frag, opt(du_month_frag))), |(y, m)| {
12 * y + m.unwrap_or(0)
}),
du_month_frag,
))(input)
}
// [13] duTimeFrag ::= 'T' ((duHourFrag duMinuteFrag? duSecondFrag?) | (duMinuteFrag duSecondFrag?) | duSecondFrag)
fn du_time_frag(input: &str) -> XsdResult<'_, Decimal> {
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)
// [14] duDayTimeFrag ::= (duDayFrag duTimeFrag?) | duTimeFrag
// [15] durationLexicalRep ::= '-'? 'P' ((duYearMonthFrag duDayTimeFrag?) | duDayTimeFrag)
struct DurationParts {
year_month: Option<i64>,
day_time: Option<Decimal>,
}
fn duration_parts(input: &str) -> Result<(DurationParts, &str), XsdParseError> {
// States
const START: u32 = 0;
const AFTER_YEAR: u32 = 1;
const AFTER_MONTH: u32 = 2;
const AFTER_DAY: u32 = 3;
const AFTER_T: u32 = 4;
const AFTER_HOUR: u32 = 5;
const AFTER_MINUTE: u32 = 6;
const AFTER_SECOND: u32 = 7;
let (negative, input) = if let Some(left) = input.strip_prefix('-') {
(true, left)
} else {
(false, input)
};
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..];
}
}
Ok((
DurationParts {
year_month: if let Some(v) = year_month {
Some(if negative {
v.checked_neg().ok_or(OVERFLOW_ERROR)?
} else {
v
})
} else {
None
},
),
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)
day_time: if let Some(v) = day_time {
Some(if negative {
v.checked_neg().ok_or(OVERFLOW_ERROR)?
} else {
v
})
} else {
None
},
},
input,
))
}
// [14] duDayTimeFrag ::= (duDayFrag duTimeFrag?) | duTimeFrag
fn du_day_time_frag(input: &str) -> XsdResult<'_, Decimal> {
alt((
map_res(tuple((du_day_frag, opt(du_time_frag))), |(d, t)| {
Decimal::from(d)
.checked_mul(Decimal::from(86400))
.ok_or(OVERFLOW_ERROR)?
.checked_add(t.unwrap_or_default())
.ok_or(OVERFLOW_ERROR)
}),
du_time_frag,
))(input)
pub fn parse_duration(input: &str) -> Result<Duration, XsdParseError> {
let parts = ensure_complete(input, duration_parts)?;
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(),
))
}
// [15] durationLexicalRep ::= '-'? 'P' ((duYearMonthFrag duDayTimeFrag?) | duDayTimeFrag)
pub fn duration_lexical_rep(input: &str) -> XsdResult<'_, Duration> {
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
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",
));
}
},
)(input)
Ok(YearMonthDuration::new(parts.year_month.ok_or(
XsdParseError::msg("No year and month values found"),
)?))
}
pub fn parse_day_time_duration(input: &str) -> Result<DayTimeDuration, XsdParseError> {
let parts = ensure_complete(input, duration_parts)?;
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"),
)?))
}
// [16] dateTimeLexicalRep ::= yearFrag '-' monthFrag '-' dayFrag 'T' ((hourFrag ':' minuteFrag ':' secondFrag) | endOfDayFrag) timezoneFrag?
pub fn date_time_lexical_rep(input: &str) -> XsdResult<'_, DateTime> {
map_res(
tuple((
year_frag,
char('-'),
month_frag,
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)
fn date_time_lexical_rep(input: &str) -> Result<(DateTime, &str), XsdParseError> {
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,
))
}
pub fn parse_date_time(input: &str) -> Result<DateTime, XsdParseError> {
ensure_complete(input, date_time_lexical_rep)
}
// [17] timeLexicalRep ::= ((hourFrag ':' minuteFrag ':' secondFrag) | endOfDayFrag) timezoneFrag?
pub fn time_lexical_rep(input: &str) -> XsdResult<'_, Time> {
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)
fn time_lexical_rep(input: &str) -> Result<(Time, &str), XsdParseError> {
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((Time::new(hour, minute, second, timezone_offset)?, input))
}
pub fn parse_time(input: &str) -> Result<Time, XsdParseError> {
ensure_complete(input, time_lexical_rep)
}
// [18] dateLexicalRep ::= yearFrag '-' monthFrag '-' dayFrag timezoneFrag? Constraint: Day-of-month Representations
pub fn date_lexical_rep(input: &str) -> XsdResult<'_, Date> {
map_res(
tuple((
year_frag,
char('-'),
month_frag,
char('-'),
day_frag,
opt(timezone_frag),
)),
|(year, _, month, _, day, timezone)| Date::new(year, month, day, timezone),
)(input)
fn date_lexical_rep(input: &str) -> Result<(Date, &str), XsdParseError> {
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 (timezone_offset, input) = optional_end(input, timezone_frag)?;
Ok((Date::new(year, month, day, timezone_offset)?, input))
}
pub fn parse_date(input: &str) -> Result<Date, XsdParseError> {
ensure_complete(input, date_lexical_rep)
}
// [19] gYearMonthLexicalRep ::= yearFrag '-' monthFrag timezoneFrag?
pub fn g_year_month_lexical_rep(input: &str) -> XsdResult<'_, GYearMonth> {
map_res(
tuple((year_frag, char('-'), month_frag, opt(timezone_frag))),
|(year, _, month, timezone)| GYearMonth::new(year, month, timezone),
)(input)
fn g_year_month_lexical_rep(input: &str) -> Result<(GYearMonth, &str), XsdParseError> {
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 (timezone_offset, input) = optional_end(input, timezone_frag)?;
Ok((GYearMonth::new(year, month, timezone_offset)?, input))
}
pub fn parse_g_year_month(input: &str) -> Result<GYearMonth, XsdParseError> {
ensure_complete(input, g_year_month_lexical_rep)
}
// [20] gYearLexicalRep ::= yearFrag timezoneFrag?
pub fn g_year_lexical_rep(input: &str) -> XsdResult<'_, GYear> {
map_res(
tuple((year_frag, opt(timezone_frag))),
|(year, timezone)| GYear::new(year, timezone),
)(input)
fn g_year_lexical_rep(input: &str) -> Result<(GYear, &str), XsdParseError> {
let (year, input) = year_frag(input)?;
let (timezone_offset, input) = optional_end(input, timezone_frag)?;
Ok((GYear::new(year, timezone_offset)?, input))
}
pub fn parse_g_year(input: &str) -> Result<GYear, XsdParseError> {
ensure_complete(input, g_year_lexical_rep)
}
// [21] gMonthDayLexicalRep ::= '--' monthFrag '-' dayFrag timezoneFrag? Constraint: Day-of-month Representations
pub fn g_month_day_lexical_rep(input: &str) -> XsdResult<'_, GMonthDay> {
map_res(
tuple((
char('-'),
char('-'),
month_frag,
char('-'),
day_frag,
opt(timezone_frag),
)),
|(_, _, month, _, day, timezone)| GMonthDay::new(month, day, timezone),
)(input)
fn g_month_day_lexical_rep(input: &str) -> Result<(GMonthDay, &str), XsdParseError> {
let input = expect_char(input, '-', "gMonthDay values must start with '--'")?;
let input = expect_char(input, '-', "gMonthDay values must start with '--'")?;
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 (timezone_offset, input) = optional_end(input, timezone_frag)?;
Ok((GMonthDay::new(month, day, timezone_offset)?, input))
}
pub fn parse_g_month_day(input: &str) -> Result<GMonthDay, XsdParseError> {
ensure_complete(input, g_month_day_lexical_rep)
}
// [22] gDayLexicalRep ::= '---' dayFrag timezoneFrag?
pub fn g_day_lexical_rep(input: &str) -> XsdResult<'_, GDay> {
map_res(
tuple((
char('-'),
char('-'),
char('-'),
day_frag,
opt(timezone_frag),
)),
|(_, _, _, day, timezone)| GDay::new(day, timezone),
)(input)
fn g_day_lexical_rep(input: &str) -> Result<(GDay, &str), XsdParseError> {
let input = expect_char(input, '-', "gDay values must start with '---'")?;
let input = expect_char(input, '-', "gDay values must start with '---'")?;
let input = expect_char(input, '-', "gDay values must start with '---'")?;
let (day, input) = day_frag(input)?;
let (timezone_offset, input) = optional_end(input, timezone_frag)?;
Ok((GDay::new(day, timezone_offset)?, input))
}
// [23] gMonthLexicalRep ::= '--' monthFrag timezoneFrag?
pub fn g_month_lexical_rep(input: &str) -> XsdResult<'_, GMonth> {
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)
pub fn parse_g_day(input: &str) -> Result<GDay, XsdParseError> {
ensure_complete(input, g_day_lexical_rep)
}
// [43] dayTimeDurationLexicalRep ::= '-'? 'P' duDayTimeFrag
pub fn day_time_duration_lexical_rep(input: &str) -> XsdResult<'_, DayTimeDuration> {
map(
tuple((opt(char('-')), preceded(char('P'), du_day_time_frag))),
|(sign, duration)| {
DayTimeDuration::new(if sign == Some('-') {
-duration
} else {
duration
})
},
)(input)
// [23] gMonthLexicalRep ::= '--' monthFrag timezoneFrag?
fn g_month_lexical_rep(input: &str) -> Result<(GMonth, &str), XsdParseError> {
let input = expect_char(input, '-', "gMonth values must start with '--'")?;
let input = expect_char(input, '-', "gMonth values must start with '--'")?;
let (month, input) = month_frag(input)?;
let (timezone_offset, input) = optional_end(input, timezone_frag)?;
Ok((GMonth::new(month, timezone_offset)?, input))
}
// [46] unsignedNoDecimalPtNumeral ::= digit+
fn unsigned_no_decimal_pt_numeral(input: &str) -> XsdResult<'_, i64> {
map_res(digit1, i64::from_str)(input)
pub fn parse_g_month(input: &str) -> Result<GMonth, XsdParseError> {
ensure_complete(input, g_month_lexical_rep)
}
// [56] yearFrag ::= '-'? (([1-9] digit digit digit+)) | ('0' digit digit digit))
fn year_frag(input: &str) -> XsdResult<'_, i64> {
map_res(
recognize(tuple((
opt(char('-')),
take_while_m_n(4, usize::MAX, |c: char| c.is_ascii_digit()),
))),
i64::from_str,
)(input)
fn year_frag(input: &str) -> Result<(i64, &str), XsdParseError> {
let (sign, input) = if let Some(left) = input.strip_prefix('-') {
(-1, left)
} else {
(1, input)
};
let (number_str, input) = integer_prefix(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])
fn month_frag(input: &str) -> XsdResult<'_, u8> {
map_res(take_while_m_n(2, 2, |c: char| c.is_ascii_digit()), |v| {
parsed_u8_range(v, 1, 12)
})(input)
fn month_frag(input: &str) -> Result<(u8, &str), XsdParseError> {
let (number_str, input) = integer_prefix(input);
if number_str.len() != 2 {
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])
fn day_frag(input: &str) -> XsdResult<'_, u8> {
map_res(take_while_m_n(2, 2, |c: char| c.is_ascii_digit()), |v| {
parsed_u8_range(v, 1, 31)
})(input)
fn day_frag(input: &str) -> Result<(u8, &str), XsdParseError> {
let (number_str, input) = integer_prefix(input);
if number_str.len() != 2 {
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])
fn hour_frag(input: &str) -> XsdResult<'_, u8> {
map_res(take_while_m_n(2, 2, |c: char| c.is_ascii_digit()), |v| {
parsed_u8_range(v, 0, 23)
})(input)
// We also allow 24 for ease of parsing
fn hour_frag(input: &str) -> Result<(u8, &str), XsdParseError> {
let (number_str, input) = integer_prefix(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
fn minute_frag(input: &str) -> XsdResult<'_, u8> {
map_res(take_while_m_n(2, 2, |c: char| c.is_ascii_digit()), |v| {
parsed_u8_range(v, 0, 59)
})(input)
fn minute_frag(input: &str) -> Result<(u8, &str), XsdParseError> {
let (number_str, input) = integer_prefix(input);
if number_str.len() != 2 {
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+)?
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn second_frag(input: &str) -> XsdResult<'_, Decimal> {
map_res(
recognize(tuple((
take_while_m_n(2, 2, |c: char| c.is_ascii_digit()),
opt(preceded(
char('.'),
take_while(|c: char| c.is_ascii_digit()),
)),
))),
|v| {
let value = Decimal::from_str(v)?;
if Decimal::from(0) <= value && value < Decimal::from(60) {
Ok(value)
fn second_frag(input: &str) -> Result<(Decimal, &str), XsdParseError> {
let (number_str, input) = decimal_prefix(input);
let (before_dot_str, _) = number_str.split_once('.').unwrap_or((number_str, ""));
if before_dot_str.len() != 2 {
return Err(XsdParseError::msg(
"Seconds must be encoded with two digits",
));
}
let number = Decimal::from_str(number_str)?;
if number < Decimal::from(0) || number >= Decimal::from(60) {
return Err(XsdParseError::msg("Seconds must be between 00 and 60"));
}
if number_str.ends_with('.') {
return Err(XsdParseError::msg(
"Seconds are not allowed to end with a dot",
));
}
Ok((number, input))
}
// [63] timezoneFrag ::= 'Z' | ('+' | '-') (('0' digit | '1' [0-3]) ':' minuteFrag | '14:00')
fn timezone_frag(input: &str) -> Result<(TimezoneOffset, &str), XsdParseError> {
if let Some(left) = input.strip_prefix('Z') {
return Ok((TimezoneOffset::UTC, left));
}
let (sign, input) = if let Some(left) = input.strip_prefix('-') {
(-1, left)
} else if let Some(left) = input.strip_prefix('+') {
(1, left)
} else {
Err(XsdParseError {
kind: XsdParseErrorKind::OutOfIntegerRange {
value: value.as_i128() as u8,
min: 0,
max: 60,
},
})
(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",
));
}
},
)(input)
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",
));
}
// [62] endOfDayFrag ::= '24:00:00' ('.' '0'+)?
fn end_of_day_frag(input: &str) -> XsdResult<'_, (u8, u8, Decimal)> {
map(
recognize(tuple((
tag("24:00:00"),
opt(preceded(char('.'), many1(char('0')))),
))),
|_| (24, 0, 0.into()),
)(input)
Ok((
TimezoneOffset::new(sign * (hours * 60 + i16::from(minutes)))?,
input,
))
}
// [63] timezoneFrag ::= 'Z' | ('+' | '-') (('0' digit | '1' [0-3]) ':' minuteFrag | '14:00')
fn timezone_frag(input: &str) -> XsdResult<'_, TimezoneOffset> {
alt((
map(char('Z'), |_| TimezoneOffset::UTC),
map_res(
tuple((
alt((map(char('+'), |_| 1), map(char('-'), |_| -1))),
alt((
map(
tuple((
map_res(take_while_m_n(2, 2, |c: char| c.is_ascii_digit()), |v| {
parsed_u8_range(v, 0, 13)
}),
char(':'),
minute_frag,
)),
|(hours, _, minutes)| i16::from(hours) * 60 + i16::from(minutes),
),
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)
fn ensure_complete<T>(
input: &str,
parse: impl FnOnce(&str) -> Result<(T, &str), XsdParseError>,
) -> Result<T, XsdParseError> {
let (result, left) = parse(input)?;
if !left.is_empty() {
return Err(XsdParseError::msg("Unrecognized value suffix"));
}
Ok(result)
}
fn expect_char<'a>(
input: &'a str,
constant: char,
error_message: &'static str,
) -> Result<&'a str, XsdParseError> {
if let Some(left) = input.strip_prefix(constant) {
Ok(left)
} else {
Err(XsdParseError {
kind: XsdParseErrorKind::OutOfIntegerRange { value, min, max },
})
Err(XsdParseError::msg(error_message))
}
}
fn map_res<'a, O1, O2, E2: Into<XsdParseError>>(
mut first: impl FnMut(&'a str) -> XsdResult<'a, O1>,
mut second: impl FnMut(O1) -> Result<O2, E2>,
) -> impl FnMut(&'a str) -> XsdResult<'a, O2> {
move |input| {
let (input, o1) = first(input)?;
Ok((input, second(o1).map_err(|e| Err::Error(e.into()))?))
fn integer_prefix(input: &str) -> (&str, &str) {
let mut end = input.len();
for (i, c) in input.char_indices() {
if !c.is_ascii_digit() {
end = i;
break;
}
}
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]
name = "sparesults"
version = "0.1.7"
version = "0.1.8-dev"
authors = ["Tpt <thomas@pellissier-tanon.fr>"]
license = "MIT OR Apache-2.0"
readme = "README.md"
@ -19,7 +19,7 @@ rdf-star = ["oxrdf/rdf-star"]
[dependencies]
json-event-parser = "0.1"
oxrdf = { version = "0.1.5", path="../oxrdf" }
oxrdf = { version = "0.1.6-dev", path="../oxrdf" }
quick-xml = "0.28"
[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();
if let Some(language) = literal.language() {
write_tsv_quoted_str(value, sink)?;
write!(sink, "@{}", language)
write!(sink, "@{language}")
} else {
match literal.datatype() {
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"-") {
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 {
@ -227,7 +227,7 @@ fn is_turtle_decimal(value: &str) -> bool {
} else if let Some(v) = value.strip_prefix(b"-") {
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..];
}
if let Some(v) = value.strip_prefix(b".") {
@ -235,7 +235,7 @@ fn is_turtle_decimal(value: &str) -> bool {
} else {
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 {
@ -248,14 +248,14 @@ fn is_turtle_double(value: &str) -> bool {
value = v;
}
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..];
with_before = true;
}
let mut with_after = false;
if let Some(v) = value.strip_prefix(b".") {
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..];
with_after = true;
}
@ -272,7 +272,7 @@ fn is_turtle_double(value: &str) -> bool {
} else if let Some(v) = value.strip_prefix(b"-") {
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> {

@ -1,5 +1,4 @@
#![doc = include_str!("../README.md")]
#![deny(unsafe_code)]
#![doc(test(attr(deny(warnings))))]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![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(())
/// ```
#[allow(clippy::rc_buffer)]
pub struct SolutionsReader<R: BufRead> {
variables: Rc<Vec<Variable>>,
solutions: SolutionsReaderKind<R>,
@ -318,7 +318,7 @@ impl<R: BufRead> Iterator for SolutionsReader<R> {
SolutionsReaderKind::Tsv(reader) => reader.read_next(),
}
.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(1), None); // Get the value of the second column if it exists (here no).
/// ```
#[allow(clippy::rc_buffer)]
pub struct QuerySolution {
variables: Rc<Vec<Variable>>,
values: Vec<Option<Term>>,
@ -69,7 +70,7 @@ impl QuerySolution {
/// ```
#[inline]
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.

@ -186,6 +186,7 @@ impl<R: BufRead> XmlQueryResultsReader<R> {
//Read header
loop {
buffer.clear();
let event = reader.read_event_into(&mut buffer)?;
match event {
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()),
_ => (),
}
buffer.clear();
}
}
}
@ -315,6 +315,7 @@ impl<R: BufRead> XmlSolutionsReader<R> {
let mut lang = None;
let mut datatype = None;
loop {
self.buffer.clear();
let event = self.reader.read_event_into(&mut self.buffer)?;
match event {
Event::Start(event) => match state {
@ -482,20 +483,31 @@ impl<R: BufRead> XmlSolutionsReader<R> {
}
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 => {
if term.is_none() {
//We default to a random bnode
term = Some(BlankNode::default().into())
}
state = self.stack.pop().unwrap()
state = self
.stack
.pop()
.ok_or_else(|| SyntaxError::msg("Empty stack"))?
}
State::Literal => {
if term.is_none() {
//We default to the empty literal
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 => {
#[cfg(feature = "rdf-star")]
@ -530,7 +542,10 @@ impl<R: BufRead> XmlSolutionsReader<R> {
)
.into(),
);
state = self.stack.pop().unwrap();
state = self
.stack
.pop()
.ok_or_else(|| SyntaxError::msg("Empty stack"))?;
} else {
return Err(
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),
_ => (),
}
self.buffer.clear();
}
}
}

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

@ -1,5 +1,4 @@
#![doc = include_str!("../README.md")]
#![deny(unsafe_code)]
#![doc(test(attr(deny(warnings))))]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![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),
Literal(Literal),
Variable(Variable),
#[cfg(feature = "rdf-star")]
Triple(Box<GroundTriplePattern>),
}
@ -587,6 +588,7 @@ impl GroundTermPattern {
Self::NamedNode(term) => write!(f, "{term}"),
Self::Literal(term) => write!(f, "{term}"),
Self::Variable(var) => write!(f, "{var}"),
#[cfg(feature = "rdf-star")]
Self::Triple(triple) => triple.fmt_sse(f),
}
}
@ -599,6 +601,7 @@ impl fmt::Display for GroundTermPattern {
Self::NamedNode(term) => term.fmt(f),
Self::Literal(term) => term.fmt(f),
Self::Variable(var) => var.fmt(f),
#[cfg(feature = "rdf-star")]
Self::Triple(triple) => write!(f, "<<{triple}>>"),
}
}
@ -618,6 +621,7 @@ impl From<Literal> for GroundTermPattern {
}
}
#[cfg(feature = "rdf-star")]
impl From<GroundTriplePattern> for GroundTermPattern {
#[inline]
fn from(triple: GroundTriplePattern) -> Self {
@ -818,6 +822,7 @@ pub struct GroundTriplePattern {
impl GroundTriplePattern {
/// 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 {
write!(f, "(triple ")?;
self.subject.fmt_sse(f)?;

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

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

@ -45,12 +45,14 @@ impl Error for ParseError {
}
}
#[allow(clippy::fallible_impl_from)]
impl From<TurtleError> for ParseError {
#[inline]
fn from(error: TurtleError) -> Self {
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 {
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 {
#[inline]
fn from(error: RdfXmlError) -> Self {
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 {
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")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![doc(test(attr(deny(warnings))))]
#![deny(unsafe_code)]
#![allow(clippy::return_self_not_must_use)]
pub mod io;
pub mod sparql;

@ -40,7 +40,7 @@ impl DatasetView {
) -> impl Iterator<Item = Result<EncodedQuad, EvaluationError>> + 'static {
self.reader
.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)]

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(())
/// ```
#[allow(clippy::rc_buffer)]
pub struct QuerySolutionIter {
variables: Rc<Vec<Variable>>,
iter: Box<dyn Iterator<Item = Result<QuerySolution, EvaluationError>>>,
@ -171,8 +172,10 @@ impl QuerySolutionIter {
iter: impl Iterator<Item = Result<Vec<Option<Term>>, EvaluationError>> + 'static,
) -> Self {
Self {
variables: variables.clone(),
iter: Box::new(iter.map(move |t| t.map(|values| (variables.clone(), values).into()))),
variables: Rc::clone(&variables),
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::{fmt, io};
#[derive(Debug)]
#[derive(Debug, Clone)]
pub enum PlanNode {
StaticBindings {
encoded_tuples: Vec<EncodedTuple>,
@ -22,7 +22,7 @@ pub enum PlanNode {
},
Service {
service_name: PatternValue,
variables: Rc<Vec<Variable>>,
variables: Rc<[Variable]>,
child: Rc<Self>,
graph_pattern: Rc<GraphPattern>,
silent: bool,
@ -71,7 +71,7 @@ pub enum PlanNode {
ForLoopLeftJoin {
left: 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 {
child: Rc<Self>,
@ -99,13 +99,13 @@ pub enum PlanNode {
},
Project {
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 {
// By definition the group by key are the range 0..key_mapping.len()
child: Rc<Self>,
key_variables: Rc<Vec<PlanVariable>>,
aggregates: Rc<Vec<(PlanAggregation, PlanVariable)>>,
key_variables: Rc<[PlanVariable]>,
aggregates: Rc<[(PlanAggregation, PlanVariable)]>,
},
}
@ -236,7 +236,10 @@ impl PlanNode {
match self {
Self::StaticBindings { encoded_tuples, .. } => {
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 key in 0..max_tuple_length {
match variables.entry(key) {
@ -444,8 +447,8 @@ pub enum PlanExpression {
Literal(PlanTerm<Literal>),
Variable(PlanVariable),
Exists(Rc<PlanNode>),
Or(Box<Self>, Box<Self>),
And(Box<Self>, Box<Self>),
Or(Vec<Self>),
And(Vec<Self>),
Equal(Box<Self>, Box<Self>),
Greater(Box<Self>, Box<Self>),
GreaterOrEqual(Box<Self>, Box<Self>),
@ -594,9 +597,7 @@ impl PlanExpression {
| Self::YearMonthDurationCast(e)
| Self::DayTimeDurationCast(e)
| Self::StringCast(e) => e.lookup_used_variables(callback),
Self::Or(a, b)
| Self::And(a, b)
| Self::Equal(a, b)
Self::Equal(a, b)
| Self::Greater(a, b)
| Self::GreaterOrEqual(a, b)
| Self::Less(a, b)
@ -636,7 +637,11 @@ impl PlanExpression {
c.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 {
e.lookup_used_variables(callback);
}
@ -649,6 +654,7 @@ impl PlanExpression {
}
impl fmt::Display for PlanExpression {
#[allow(clippy::many_single_char_names)]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Variable(v) => {
@ -719,8 +725,26 @@ impl fmt::Display for PlanExpression {
Self::YearMonthDurationCast(e) => write!(f, "YearMonthDurationCast({e})"),
Self::DayTimeDurationCast(e) => write!(f, "DayTimeDurationCast({e})"),
Self::StringCast(e) => write!(f, "StringCast({e})"),
Self::Or(a, b) => write!(f, "Or({a}, {b})"),
Self::And(a, b) => write!(f, "And({a}, {b})"),
Self::Or(es) => {
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::Greater(a, b) => write!(f, "Greater({a}, {b})"),
Self::GreaterOrEqual(a, b) => write!(f, "GreaterOrEqual({a}, {b})"),
@ -838,7 +862,7 @@ pub enum PlanAggregationFunction {
Max,
Avg,
Sample,
GroupConcat { separator: Rc<String> },
GroupConcat { separator: Rc<str> },
}
#[derive(Debug, Clone)]
@ -850,7 +874,7 @@ pub enum PlanPropertyPath {
ZeroOrMore(Rc<Self>),
OneOrMore(Rc<Self>),
ZeroOrOne(Rc<Self>),
NegatedPropertySet(Rc<Vec<PlanTerm<NamedNode>>>),
NegatedPropertySet(Rc<[PlanTerm<NamedNode>]>),
}
impl fmt::Display for PlanPropertyPath {
@ -1046,7 +1070,7 @@ impl PlanNodeWithStats {
"Aggregate({})",
key_variables
.iter()
.map(|c| c.to_string())
.map(ToString::to_string)
.chain(aggregates.iter().map(|(agg, v)| format!("{agg} -> {v}")))
.collect::<Vec<_>>()
.join(", ")
@ -1107,7 +1131,7 @@ impl PlanNodeWithStats {
format!(
"Sort({})",
by.iter()
.map(|c| c.to_string())
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ")
)
@ -1117,7 +1141,7 @@ impl PlanNodeWithStats {
"StaticBindings({})",
variables
.iter()
.map(|v| v.to_string())
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ")
)

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

@ -71,7 +71,14 @@ impl<'a, 'b: 'a> SimpleUpdateEvaluator<'a, 'b> {
insert,
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 {
silent,
source,
@ -119,14 +126,14 @@ impl<'a, 'b: 'a> SimpleUpdateEvaluator<'a, 'b> {
) -> Result<(), EvaluationError> {
let dataset = Rc::new(DatasetView::new(self.transaction.reader(), using));
let (plan, variables) = PlanBuilder::build(
dataset.as_ref(),
&dataset,
algebra,
false,
&self.options.query_options.custom_functions,
!self.options.query_options.without_optimizations,
)?;
let evaluator = SimpleEvaluator::new(
dataset.clone(),
Rc::clone(&dataset),
self.base_iri.clone(),
self.options.query_options.service_handler(),
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::Triple(triple) => {
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)
.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::Triple(triple) => {
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)
.map(|node| dataset.decode_term(&node))

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

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

@ -1,3 +1,4 @@
#![allow(clippy::same_name_method)]
#[cfg(not(target_family = "wasm"))]
use crate::model::Quad;
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> {
let this = Self {
#[cfg(not(target_family = "wasm"))]
@ -1305,7 +1306,7 @@ impl StorageBulkLoader {
let mut buffer_to_load = Vec::with_capacity(batch_size);
swap(buffer, &mut buffer_to_load);
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 || {
FileBulkLoader::new(storage, batch_size).load(buffer_to_load, &done_counter_clone)
}));

@ -565,7 +565,7 @@ impl Store {
&self,
quads: impl IntoIterator<Item = impl Into<Quad>>,
) -> 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))
}
@ -1569,7 +1569,7 @@ impl BulkLoader {
quads: impl IntoIterator<Item = Result<impl Into<Quad>, EI>>,
) -> Result<(), EO> {
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]
#[cfg(not(target_family = "wasm"))]
fn test_bulk_load_dataset() -> Result<(), Box<dyn Error>> {
let store = Store::new().unwrap();
let store = Store::new()?;
store
.bulk_loader()
.load_dataset(Cursor::new(GRAPH_DATA), DatasetFormat::TriG, None)?;

@ -1,6 +1,6 @@
[package]
name = "oxrocksdb-sys"
version = "0.3.16-dev"
version = "0.3.17-dev"
authors = ["Tpt <thomas@pellissier-tanon.fr>"]
license = "GPL-2.0 OR Apache-2.0"
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
// 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;
fn link(name: &str, bundled: bool) {
@ -88,8 +88,6 @@ fn build_rocksdb() {
config.define("HAVE_LZCNT", Some("1"));
config.flag_if_supported("-mlzcnt");
}
} else if target.contains("aarch64") {
lib_sources.push("util/crc32c_arm64.cc")
}
if target.contains("apple-ios") {
@ -100,11 +98,13 @@ fn build_rocksdb() {
config.define("NPERF_CONTEXT", None);
config.define("ROCKSDB_PLATFORM_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");
} else if target.contains("darwin") {
config.define("OS_MACOSX", None);
config.define("ROCKSDB_PLATFORM_POSIX", None);
config.define("ROCKSDB_LIB_IO_POSIX", None);
remove_var("SDKROOT"); // We override SDKROOT for cross-compilation
} else if target.contains("android") {
config.define("OS_ANDROID", None);
config.define("ROCKSDB_PLATFORM_POSIX", None);
@ -179,6 +179,7 @@ fn build_rocksdb() {
}
config.file(&format!("rocksdb/{file}"));
}
config.compile("rocksdb");
}

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

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

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

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

@ -8,7 +8,9 @@ use oxigraph::io::{
use pyo3::exceptions::{PyIOError, PySyntaxError, PyValueError};
use pyo3::prelude::*;
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::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/"))
/// [<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]
#[pyo3(text_signature = "(input, mime_type, *, base_iri = None)")]
#[pyo3(signature = (input, mime_type, *, base_iri = None))]
pub fn parse(
input: PyObject,
mime_type: &str,
@ -281,21 +283,22 @@ impl Write for PyWritable {
pub struct PyIo(PyObject);
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| {
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
.0
.call_method(py, "read", (buf.len(),), None)
.as_ref(py)
.call_method1(intern!(py, "read"), (to_read,))
.map_err(to_io_err)?;
let bytes = read
.extract::<&[u8]>(py)
.or_else(|e| {
read.extract::<&str>(py)
.map(|s| s.as_bytes())
.map_err(|_| e)
})
.extract::<&[u8]>()
.or_else(|e| read.extract::<&str>().map(str::as_bytes).map_err(|_| e))
.map_err(to_io_err)?;
buf.write_all(bytes)?;
buf[..bytes.len()].copy_from_slice(bytes);
Ok(bytes.len())
})
}
@ -305,16 +308,17 @@ impl Write for PyIo {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
Python::with_gil(|py| {
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)?
.extract::<usize>(py)
.extract::<usize>()
.map_err(to_io_err)
})
}
fn flush(&mut self) -> io::Result<()> {
Python::with_gil(|py| {
self.0.call_method(py, "flush", (), None)?;
self.0.as_ref(py).call_method0(intern!(py, "flush"))?;
Ok(())
})
}
@ -325,7 +329,10 @@ fn to_io_err(error: impl Into<PyErr>) -> io::Error {
}
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()
} else {
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 model;
mod sparql;

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

@ -88,7 +88,7 @@ pub fn query_results_to_python(py: Python<'_>, results: QueryResults) -> PyObjec
/// >>> s, p, o = solution
/// >>> s
/// <NamedNode value=http://example.com>
#[pyclass(unsendable, name = "QuerySolution", module = "pyoxigraph")]
#[pyclass(frozen, unsendable, name = "QuerySolution", module = "pyoxigraph")]
pub struct PyQuerySolution {
inner: QuerySolution,
}
@ -225,7 +225,7 @@ impl PyQueryTriples {
Ok(allow_threads_unsafe(|| self.inner.next())
.transpose()
.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')))
/// >>> str(store)
/// '<http://example.com> <http://example.com/p> "1" <http://example.com/g> .\n'
#[pyclass(name = "Store", module = "pyoxigraph")]
#[pyo3(text_signature = "(path = None)")]
#[pyclass(frozen, name = "Store", module = "pyoxigraph")]
#[derive(Clone)]
pub struct PyStore {
inner: Store,
@ -94,7 +93,7 @@ impl PyStore {
/// :rtype: Store
/// :raises IOError: if the target directories contain invalid data or could not be accessed.
#[staticmethod]
#[pyo3(signature = (primary_path, secondary_path = None), text_signature = "(primary_path, secondary_path = None)")]
#[pyo3(signature = (primary_path, secondary_path = None))]
fn secondary(
primary_path: &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')))
/// >>> 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>>]
#[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(
&self,
subject: &PyAny,
@ -228,10 +227,10 @@ impl PyStore {
extract_quads_pattern(subject, predicate, object, graph_name)?;
Ok(QuadIter {
inner: self.inner.quads_for_pattern(
subject.as_ref().map(|p| p.into()),
predicate.as_ref().map(|p| p.into()),
object.as_ref().map(|p| p.into()),
graph_name.as_ref().map(|p| p.into()),
subject.as_ref().map(Into::into),
predicate.as_ref().map(Into::into),
object.as_ref().map(Into::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.query('ASK { ?s ?p ?o }')
/// True
#[pyo3(
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)"
)]
#[pyo3(signature = (query, *, base_iri = None, use_default_graph_as_union = false, default_graph = None, named_graphs = None))]
fn query(
&self,
query: &str,
@ -332,7 +328,7 @@ impl PyStore {
/// >>> store.update('DELETE WHERE { <http://example.com> ?p ?o }')
/// >>> 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<()> {
py.allow_threads(|| {
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"))
/// >>> 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>>]
#[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(
&self,
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"))
/// >>> 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>>]
#[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(
&self,
input: PyObject,
@ -537,7 +533,7 @@ impl PyStore {
/// >>> store.dump(output, "text/turtle", from_graph=NamedNode("http://example.com/g"))
/// >>> output.getvalue()
/// 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(
&self,
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.
///
/// :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
/// :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
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(
NamedNode("http://example.com/foo"),
@ -18,7 +20,7 @@ EXAMPLE_QUAD = Quad(
class TestParse(unittest.TestCase):
def test_parse_file(self) -> None:
with NamedTemporaryFile() as fp:
fp.write(b'<foo> <p> "1" .')
fp.write('<foo> <p> "éù" .'.encode())
fp.flush()
self.assertEqual(
list(parse(fp.name, "text/turtle", base_iri="http://example.com/")),
@ -33,7 +35,7 @@ class TestParse(unittest.TestCase):
self.assertEqual(
list(
parse(
StringIO('<foo> <p> "1" .'),
StringIO('<foo> <p> "éù" .'),
"text/turtle",
base_iri="http://example.com/",
)
@ -41,11 +43,23 @@ class TestParse(unittest.TestCase):
[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:
self.assertEqual(
list(
parse(
BytesIO(b'<foo> <p> "1" .'),
BytesIO('<foo> <p> "éù" .'.encode()),
"text/turtle",
base_iri="http://example.com/",
)
@ -75,15 +89,16 @@ class TestSerialize(unittest.TestCase):
output = BytesIO()
serialize([EXAMPLE_TRIPLE], output, "text/turtle")
self.assertEqual(
output.getvalue(),
b'<http://example.com/foo> <http://example.com/p> "1" .\n',
output.getvalue().decode(),
'<http://example.com/foo> <http://example.com/p> "éù" .\n',
)
def test_serialize_to_file(self) -> None:
with NamedTemporaryFile() as fp:
serialize([EXAMPLE_TRIPLE], fp.name, "text/turtle")
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:

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

@ -17,9 +17,13 @@ use sparesults::{QueryResultsFormat, QueryResultsSerializer};
use std::borrow::Cow;
use std::cell::RefCell;
use std::cmp::{max, min};
#[cfg(target_os = "linux")]
use std::env;
use std::ffi::OsStr;
use std::fs::File;
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::rc::Rc;
use std::str::FromStr;
@ -591,7 +595,7 @@ pub fn main() -> anyhow::Result<()> {
let mut file = BufWriter::new(File::create(&explain_file)?);
match explain_file
.extension()
.and_then(|e| e.to_str()) {
.and_then(OsStr::to_str) {
Some("json") => {
explanation.write_in_json(file)?;
},
@ -734,7 +738,7 @@ fn format_from_path<T>(
path: &Path,
from_extension: impl FnOnce(&str) -> 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| {
e.context(format!(
"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_server_name(concat!("Oxigraph/", env!("CARGO_PKG_VERSION")))?;
#[cfg(target_os = "linux")]
systemd_notify_ready()?;
eprintln!("Listening for requests at http://{}", &bind);
server.listen(bind)?;
Ok(())
@ -1531,19 +1537,24 @@ fn web_load_graph(
format: GraphFormat,
to_graph_name: GraphNameRef<'_>,
) -> 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() {
web_bulk_loader(store, request).load_graph(
BufReader::new(request.body_mut()),
format,
to_graph_name,
None,
base_iri,
)
} else {
store.load_graph(
BufReader::new(request.body_mut()),
format,
to_graph_name,
None,
base_iri,
)
}
.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> {
let buffer = Rc::new(RefCell::new(Vec::new()));
let state = initial_state_builder(ReadForWriteWriter {
buffer: buffer.clone(),
buffer: Rc::clone(&buffer),
})
.map_err(internal_server_error)?;
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)]
mod tests {
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]
fn graph_store_protocol() -> Result<()> {
// 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"?>
<sparql xmlns="http://www.w3.org/2005/sparql-results#">
<head>
<variable name="v"/>
<variable name="r"/>
</head>
<results>
<result></result>
</results>
</sparql>

@ -23,17 +23,14 @@
:values_in_filter_not_exists
:subquery_in_filter_not_exists
:cmp_langString
:halloween_problem
:nested_path
:nested_expression
:order_terms
:nested_anonymous
:unbound_variable_in_subquery
:values_too_many
:values_too_few
:values_property_path_all
:one_or_more_shared
:one_or_more_star
:in_empty_error
) .
:small_unicode_escape_with_multibytes_char rdf:type mf:NegativeSyntaxTest ;
@ -105,11 +102,6 @@
mf:action [ qt:query <cmp_langString.rq> ] ;
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 ;
mf:name "A very nested property path" ;
mf:action <nested_path.rq> .
@ -136,20 +128,6 @@
[ qt:query <unbound_variable_in_subquery.rq> ] ;
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 ;
mf:name "SPARQL one or more with shared variable" ;
mf:action
@ -163,3 +141,8 @@
[ qt:query <one_or_more_star.rq> ;
qt:data <one_or_more_star.ttl> ] ;
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
.graph
.object_for_subject_predicate(current, rdf::FIRST)
.map(|v| v.into_owned());
.map(TermRef::into_owned);
self.current_node =
match self.graph.object_for_subject_predicate(current, rdf::REST) {
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> {
if url.ends_with(".srx") {
StaticQueryResults::from_query_results(
QueryResults::read(read_file(url)?, QueryResultsFormat::Xml)?,
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,
)
if let Some(format) = url
.rsplit_once('.')
.and_then(|(_, extension)| QueryResultsFormat::from_extension(extension))
{
StaticQueryResults::from_query_results(QueryResults::read(read_file(url)?, format)?, false)
} else {
StaticQueryResults::from_graph(&load_graph(url, guess_graph_format(url)?)?)
}
@ -505,7 +495,7 @@ impl StaticQueryResults {
fn from_graph(graph: &Graph) -> Result<Self> {
// Hack to normalize literals
let store = Store::new().unwrap();
let store = Store::new()?;
for t in graph.iter() {
store
.insert(t.in_graph(GraphNameRef::DefaultGraph))
@ -617,12 +607,12 @@ fn results_diff(expected: StaticQueryResults, actual: StaticQueryResults) -> Str
format_diff(
&expected_variables
.iter()
.map(|v| v.to_string())
.map(ToString::to_string)
.collect::<Vec<_>>()
.join("\n"),
&actual_variables
.iter()
.map(|v| v.to_string())
.map(ToString::to_string)
.collect::<Vec<_>>()
.join("\n"),
"variables",

Loading…
Cancel
Save