diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a7884855..b2814580 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -114,5 +114,5 @@ jobs: working-directory: ./python - run: python -m mypy.stubtest pyoxigraph --allowlist=mypy_allowlist.txt working-directory: ./python - - run: python -m mypy generate_stubs.py tests + - run: python -m mypy generate_stubs.py tests --strict working-directory: ./python diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a639965..b77490e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,22 @@ +## [0.3.9] - 2022-12-07 + +## Added +- Server: The `/store` endpoints now has a `no_transaction` HTTP option for `POST` and `PUT` request to get better performances at the cost of transactional guarantees. +- Server: The `/store` endpoints now has a `lenient` HTTP option for `POST` and `PUT` request to ignore syntax errors (requires the `no_transaction` option). +- Server: allows path that are not valid UTF-8 in file path CLI arguments. +- Rust: `From` to `oxigraph::Query` (thanks to @hobofan). + +## Changed +- SPARQL: `NOW()` function properly returns the current time and not 1970-01-01 +- SPARQL: fixes serialization of SPARQL queries (property path and STRSTARTS function). +- SPARQL: slightly optimize aggregates by avoiding an unneeded projection. +- SPARQL: the parser now cleanly fails if invalid `VALUES` clauses are provided. +- SPARQL: In DELETE/INSERT UPDATEs the currently written values can't be read anymore ("Halloween problem"). +- `oxrdf`: makes Clippy run without warnings when `rdf-star` is disable. +- Python: makes type annotations compatible with Python 3.7. +- Python: makes sure the parameter default value is always included in the type annotation. + + ## [0.3.8] - 2022-10-22 ### Changed diff --git a/Cargo.lock b/Cargo.lock index 70013887..1d4bedc1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21,9 +21,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "0.7.19" +version = "0.7.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" dependencies = [ "memchr", ] @@ -40,11 +40,20 @@ version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6" +[[package]] +name = "arbitrary" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a7924531f38b1970ff630f03eb20a2fde69db5c590c93b0f3482e95dcc5fd60" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "assert_cmd" -version = "2.0.6" +version = "2.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba45b8163c49ab5f972e59a8a5a03b6d2972619d486e19ec9fe744f7c2753d3c" +checksum = "fa3d466004a8b4cb1bc34044240a2fd29d17607e2e3bd613eb44fd48e8100da3" dependencies = [ "bstr 1.0.1", "doc-comment", @@ -56,9 +65,9 @@ dependencies = [ [[package]] name = "assert_fs" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1429b32ede0cb31afd9f6cb1e8f06f1e32a4c75ed9290f9f4d3cda0c5981e061" +checksum = "d94b2a3f3786ff2996a98afbd6b4e5b7e890d685ccf67577f508ee2342c71cc9" dependencies = [ "doc-comment", "globwalk", @@ -74,7 +83,7 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ - "hermit-abi", + "hermit-abi 0.1.19", "libc", "winapi 0.3.9", ] @@ -163,9 +172,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.0.73" +version = "1.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" +checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4" dependencies = [ "jobserver", ] @@ -225,9 +234,9 @@ dependencies = [ [[package]] name = "clap" -version = "3.2.22" +version = "3.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86447ad904c7fb335a790c9d7fe3d0d971dc523b8ccd1561a520de9a85302750" +checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5" dependencies = [ "bitflags", "clap_lex 0.2.4", @@ -237,14 +246,14 @@ dependencies = [ [[package]] name = "clap" -version = "4.0.18" +version = "4.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335867764ed2de42325fafe6d18b8af74ba97ee0c590fa016f157535b42ab04b" +checksum = "4d63b9e9c07271b9957ad22c173bae2a4d9a81127680962039296abcd2f8251d" dependencies = [ - "atty", "bitflags", "clap_derive", "clap_lex 0.3.0", + "is-terminal", "once_cell", "strsim", "termcolor", @@ -252,9 +261,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.0.18" +version = "4.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16a1b0f6422af32d5da0c58e2703320f379216ee70198241c84173a8c5ac28f3" +checksum = "0177313f9f02afc995627906bbd8967e2be069f5261954222dac78290c2b9014" dependencies = [ "heck", "proc-macro-error", @@ -335,7 +344,7 @@ dependencies = [ "atty", "cast", "ciborium", - "clap 3.2.22", + "clap 3.2.23", "criterion-plot", "itertools", "lazy_static", @@ -384,22 +393,22 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.11" +version = "0.9.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f916dfc5d356b0ed9dae65f1db9fc9770aa2851d2662b988ccf4fe3516e86348" +checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a" dependencies = [ "autocfg", "cfg-if", "crossbeam-utils", - "memoffset", + "memoffset 0.7.1", "scopeguard", ] [[package]] name = "crossbeam-utils" -version = "0.8.12" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edbafec5fa1f196ca66527c1b12c2ec4745ca14b50f1ad8f9f6f720b55d11fac" +checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" dependencies = [ "cfg-if", ] @@ -414,6 +423,17 @@ dependencies = [ "typenum", ] +[[package]] +name = "derive_arbitrary" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9a577516173adb681466d517d39bd468293bc2c2a16439375ef0f35bba45f3d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "difflib" version = "0.4.0" @@ -422,9 +442,9 @@ checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" [[package]] name = "digest" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c" +checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" dependencies = [ "block-buffer", "crypto-common", @@ -442,6 +462,27 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" +[[package]] +name = "errno" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "escargot" version = "0.5.7" @@ -465,9 +506,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" +checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" dependencies = [ "crc32fast", "miniz_oxide", @@ -595,6 +636,15 @@ dependencies = [ "libc", ] +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + [[package]] name = "hex" version = "0.4.3" @@ -637,9 +687,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.9.1" +version = "1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" +checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" dependencies = [ "autocfg", "hashbrown 0.12.3", @@ -660,6 +710,28 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "io-lifetimes" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46112a93252b123d31a119a8d1a1ac19deac4fac6e0e8b0df58f0d4e5870e63c" +dependencies = [ + "libc", + "windows-sys 0.42.0", +] + +[[package]] +name = "is-terminal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "927609f78c2913a6f6ac3c27a4fe87f43e2a35367c0c4b0f8265e8f49a104330" +dependencies = [ + "hermit-abi 0.2.6", + "io-lifetimes", + "rustix", + "windows-sys 0.42.0", +] + [[package]] name = "itertools" version = "0.10.5" @@ -732,20 +804,26 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.135" +version = "0.2.138" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68783febc7782c6c5cb401fbda4de5a9898be1762314da0bb2c10ced61f18b0c" +checksum = "db6d7e329c562c5dfab7a46a2afabc8b987ab9a4834c9d1ca04dc54c1546cef8" [[package]] name = "libloading" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efbc0f03f9a775e9f6aed295c6a1ba2253c5757a9e03d55c6caa46a681abcddd" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" dependencies = [ "cfg-if", "winapi 0.3.9", ] +[[package]] +name = "linux-raw-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f9f08d8963a6c613f4b1a78f4f4a4dbfadf8e6545b2d72861731e4858b8b47f" + [[package]] name = "lock_api" version = "0.4.9" @@ -789,6 +867,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -797,9 +884,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.5.4" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34" +checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" dependencies = [ "adler", ] @@ -840,28 +927,19 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" -dependencies = [ - "hermit-abi", - "libc", -] - -[[package]] -name = "num_threads" -version = "0.1.6" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5" dependencies = [ + "hermit-abi 0.1.19", "libc", ] [[package]] name = "once_cell" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" +checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" [[package]] name = "oorandom" @@ -877,9 +955,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "os_str_bytes" -version = "6.3.0" +version = "6.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" +checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" [[package]] name = "oxhttp" @@ -898,7 +976,7 @@ dependencies = [ [[package]] name = "oxigraph" -version = "0.3.8" +version = "0.3.9" dependencies = [ "criterion", "digest", @@ -931,7 +1009,7 @@ dependencies = [ [[package]] name = "oxigraph_js" -version = "0.3.8" +version = "0.3.9" dependencies = [ "console_error_panic_hook", "js-sys", @@ -942,12 +1020,12 @@ dependencies = [ [[package]] name = "oxigraph_server" -version = "0.3.8" +version = "0.3.9" dependencies = [ "anyhow", "assert_cmd", "assert_fs", - "clap 4.0.18", + "clap 4.0.29", "escargot", "flate2", "oxhttp", @@ -962,10 +1040,10 @@ dependencies = [ [[package]] name = "oxigraph_testsuite" -version = "0.3.8" +version = "0.3.9" dependencies = [ "anyhow", - "clap 4.0.18", + "clap 4.0.29", "criterion", "oxigraph", "text-diff", @@ -986,7 +1064,7 @@ checksum = "bb175ec8981211357b7b379869c2f8d555881c55ea62311428ec0de46d89bd5c" [[package]] name = "oxrdf" -version = "0.1.0" +version = "0.1.1" dependencies = [ "lasso", "oxilangtag", @@ -1015,9 +1093,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dc9e0dc2adc1c69d09143aff38d3d30c5c3f0df0dad82e6d25547af174ebec0" +checksum = "7ff9f3fef3968a3ec5945535ed654cb38ff72d7495a25619e2247fb15a2ed9ba" dependencies = [ "cfg-if", "libc", @@ -1095,15 +1173,15 @@ dependencies = [ [[package]] name = "ppv-lite86" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "predicates" -version = "2.1.3" +version = "2.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed6bd09a7f7e68f3f0bf710fb7ab9c4615a488b58b5f653382a687701e458c92" +checksum = "f54fc5dc63ed3bbf19494623db4f3af16842c0d975818e469022d09e53f0aa05" dependencies = [ "difflib", "float-cmp", @@ -1164,14 +1242,14 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.17.2" +version = "0.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "201b6887e5576bf2f945fe65172c1fcbf3fcf285b23e4d71eb171d9736e38d32" +checksum = "268be0c73583c183f2b14052337465768c07726936a260f480f0857cb95ba543" dependencies = [ "cfg-if", "indoc", "libc", - "memoffset", + "memoffset 0.6.5", "parking_lot", "pyo3-build-config", "pyo3-ffi", @@ -1181,9 +1259,9 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.17.2" +version = "0.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf0708c9ed01692635cbf056e286008e5a2927ab1a5e48cdd3aeb1ba5a6fef47" +checksum = "28fcd1e73f06ec85bf3280c48c67e731d8290ad3d730f8be9dc07946923005c8" dependencies = [ "once_cell", "target-lexicon", @@ -1191,9 +1269,9 @@ dependencies = [ [[package]] name = "pyo3-ffi" -version = "0.17.2" +version = "0.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90352dea4f486932b72ddf776264d293f85b79a1d214de1d023927b41461132d" +checksum = "0f6cb136e222e49115b3c51c32792886defbfb0adead26a688142b346a0b9ffc" dependencies = [ "libc", "pyo3-build-config", @@ -1201,9 +1279,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.17.2" +version = "0.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb24b804a2d9e88bfcc480a5a6dd76f006c1e3edaf064e8250423336e2cd79d" +checksum = "94144a1266e236b1c932682136dc35a9dee8d3589728f68130c7c3861ef96b28" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -1213,9 +1291,9 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.17.2" +version = "0.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f22bb49f6a7348c253d7ac67a6875f2dc65f36c2ae64a82c381d528972bea6d6" +checksum = "c8df9be978a2d2f0cdebabb03206ed73b11314701a5bfe71b0d753b81997777f" dependencies = [ "proc-macro2", "quote", @@ -1224,7 +1302,7 @@ dependencies = [ [[package]] name = "pyoxigraph" -version = "0.3.8" +version = "0.3.9" dependencies = [ "oxigraph", "pyo3", @@ -1289,11 +1367,10 @@ dependencies = [ [[package]] name = "rayon" -version = "1.5.3" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd99e5772ead8baa5215278c9b15bf92087709e9c1b2d1f97cdb5a183c933a7d" +checksum = "1e060280438193c554f654141c9ea9417886713b7acd75974c85b18a69a88e0b" dependencies = [ - "autocfg", "crossbeam-deque", "either", "rayon-core", @@ -1301,9 +1378,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.9.3" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "258bcdb5ac6dad48491bb2992db6b7cf74878b0384908af124823d118c99683f" +checksum = "cac410af5d00ab6884528b4ab69d1e8e146e8d471201800fa1b4524126de6ad3" dependencies = [ "crossbeam-channel", "crossbeam-deque", @@ -1322,9 +1399,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" +checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a" dependencies = [ "aho-corasick", "memchr", @@ -1339,9 +1416,9 @@ checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" [[package]] name = "regex-syntax" -version = "0.6.27" +version = "0.6.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" [[package]] name = "remove_dir_all" @@ -1402,6 +1479,20 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustix" +version = "0.36.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3807b5d10909833d3e9acd1eb5fb988f79376ff10fce42937de71a449c4c588" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys 0.42.0", +] + [[package]] name = "rustls" version = "0.20.7" @@ -1462,9 +1553,9 @@ dependencies = [ [[package]] name = "scoped-tls" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" [[package]] name = "scopeguard" @@ -1507,18 +1598,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.147" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965" +checksum = "256b9932320c590e707b94576e3cc1f7c9024d0ee6612dfbcf1cb106cbe8e055" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.147" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852" +checksum = "b4eae9b04cbffdfd550eb462ed33bc6a1b68c935127d008b27444d08380f94e4" dependencies = [ "proc-macro2", "quote", @@ -1527,9 +1618,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.87" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce777b7b150d76b9cf60d28b55f5847135a003f7d7350c6be7a773508ce7d45" +checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db" dependencies = [ "itoa", "ryu", @@ -1538,9 +1629,9 @@ dependencies = [ [[package]] name = "sha-1" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" +checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" dependencies = [ "cfg-if", "cpufeatures", @@ -1578,7 +1669,7 @@ checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" [[package]] name = "sparesults" -version = "0.1.1" +version = "0.1.2" dependencies = [ "json-event-parser", "oxrdf", @@ -1587,7 +1678,7 @@ dependencies = [ [[package]] name = "spargebra" -version = "0.2.2" +version = "0.2.3" dependencies = [ "oxilangtag", "oxiri", @@ -1596,6 +1687,13 @@ dependencies = [ "rand", ] +[[package]] +name = "sparql-smith" +version = "0.1.0-alpha.1" +dependencies = [ + "arbitrary", +] + [[package]] name = "spin" version = "0.5.2" @@ -1610,9 +1708,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" -version = "1.0.103" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d" +checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908" dependencies = [ "proc-macro2", "quote", @@ -1621,9 +1719,9 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.26.5" +version = "0.26.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade661fa5e048ada64ad7901713301c21d2dbc5b65ee7967de8826c111452960" +checksum = "29ddf41e393a9133c81d5f0974195366bd57082deac6e0eb02ed39b8341c2bb6" dependencies = [ "cfg-if", "core-foundation-sys", @@ -1636,9 +1734,9 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c02424087780c9b71cc96799eaeddff35af2bc513278cda5c99fc1f5d026d3c1" +checksum = "9410d0f6853b1d94f0e519fb95df60f29d2c1eff2d921ffdf01a4c8a3b54f12d" [[package]] name = "tempfile" @@ -1691,9 +1789,9 @@ dependencies = [ [[package]] name = "textwrap" -version = "0.15.1" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "949517c0cf1bf4ee812e2e07e08ab448e3ae0d23472aee8a06c985f0c8815b16" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" [[package]] name = "thread_local" @@ -1706,13 +1804,29 @@ dependencies = [ [[package]] name = "time" -version = "0.3.15" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d634a985c4d4238ec39cacaed2e7ae552fbd3c476b552c1deac3021b7d7eaf0c" +checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376" dependencies = [ "itoa", - "libc", - "num_threads", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" + +[[package]] +name = "time-macros" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d967f99f534ca7e495c575c62638eebc2898a8c84c119b89e250477bc4ba16b2" +dependencies = [ + "time-core", ] [[package]] @@ -1742,9 +1856,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "typenum" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" [[package]] name = "unicode-bidi" @@ -2094,18 +2208,18 @@ checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" [[package]] name = "zstd" -version = "0.11.2+zstd.1.5.2" +version = "0.12.1+zstd.1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" +checksum = "5c947d2adc84ff9a59f2e3c03b81aa4128acf28d6ad7d56273f7e8af14e47bea" dependencies = [ "zstd-safe", ] [[package]] name = "zstd-safe" -version = "5.0.2+zstd.1.5.2" +version = "6.0.2+zstd.1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" +checksum = "a6cf39f730b440bab43da8fb5faf5f254574462f73f260f85f7987f32154ff17" dependencies = [ "libc", "zstd-sys", @@ -2113,9 +2227,9 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "2.0.1+zstd.1.5.2" +version = "2.0.4+zstd.1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fd07cbbc53846d9145dbffdf6dd09a7a0aa52be46741825f5c97bdd4f73f12b" +checksum = "4fa202f2ef00074143e219d15b62ffc317d17cc33909feac471c044087cad7b0" dependencies = [ "cc", "libc", diff --git a/Cargo.toml b/Cargo.toml index 0d4beeeb..f7e74791 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "lib/oxrdf", "lib/spargebra", "lib/sparesults", + "lib/sparql-smith", "python", "oxrocksdb-sys", "server", diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 18ca4657..2518ec4c 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -9,12 +9,19 @@ edition = "2021" cargo-fuzz = true [dependencies] +lazy_static = "1" libfuzzer-sys = "0.4" spargebra = { path = "../lib/spargebra", features = ["rdf-star"] } sparesults = { path = "../lib/sparesults", features = ["rdf-star"] } +sparql-smith = { path = "../lib/sparql-smith" } +oxigraph = { path = "../lib" } [workspace] +[[bin]] +name = "sparql_eval" +path = "fuzz_targets/sparql_eval.rs" + [[bin]] name = "sparql_query" path = "fuzz_targets/sparql_query.rs" diff --git a/fuzz/fuzz_targets/sparql_eval.rs b/fuzz/fuzz_targets/sparql_eval.rs new file mode 100644 index 00000000..40c32d15 --- /dev/null +++ b/fuzz/fuzz_targets/sparql_eval.rs @@ -0,0 +1,26 @@ +#![no_main] +use lazy_static::lazy_static; +use libfuzzer_sys::fuzz_target; +use oxigraph::io::DatasetFormat; +use oxigraph::sparql::Query; +use oxigraph::store::Store; + +lazy_static! { + static ref STORE: Store = { + let store = Store::new().unwrap(); + store + .load_dataset( + sparql_smith::DATA_TRIG.as_bytes(), + DatasetFormat::TriG, + None, + ) + .unwrap(); + store + }; +} + +fuzz_target!(|data: sparql_smith::Query| { + if let Ok(q) = Query::parse(&data.to_string(), None) { + STORE.query(q).unwrap(); + } +}); diff --git a/fuzz/fuzz_targets/sparql_query.rs b/fuzz/fuzz_targets/sparql_query.rs index b0286f6a..889d3e79 100644 --- a/fuzz/fuzz_targets/sparql_query.rs +++ b/fuzz/fuzz_targets/sparql_query.rs @@ -1,10 +1,7 @@ #![no_main] use libfuzzer_sys::fuzz_target; use spargebra::Query; -use std::str; -fuzz_target!(|data: &[u8]| { - if let Ok(data) = str::from_utf8(data) { - Query::parse(data, None); - } +fuzz_target!(|data: &str| { + Query::parse(data, None); }); diff --git a/fuzz/fuzz_targets/sparql_update.rs b/fuzz/fuzz_targets/sparql_update.rs index 252bf493..15c0a995 100644 --- a/fuzz/fuzz_targets/sparql_update.rs +++ b/fuzz/fuzz_targets/sparql_update.rs @@ -3,8 +3,6 @@ use libfuzzer_sys::fuzz_target; use spargebra::Update; use std::str; -fuzz_target!(|data: &[u8]| { - if let Ok(data) = str::from_utf8(data) { - Update::parse(data, None); - } +fuzz_target!(|data: &str| { + Update::parse(data, None); }); diff --git a/js/Cargo.toml b/js/Cargo.toml index 3c10b92e..4300445b 100644 --- a/js/Cargo.toml +++ b/js/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "oxigraph_js" -version = "0.3.8" +version = "0.3.9" authors = ["Tpt "] license = "MIT OR Apache-2.0" readme = "README.md" @@ -14,7 +14,7 @@ crate-type = ["cdylib"] name = "oxigraph" [dependencies] -oxigraph = { version = "0.3.8", path="../lib" } +oxigraph = { version = "0.3.9", path="../lib" } wasm-bindgen = "0.2" js-sys = "0.3" console_error_panic_hook = "0.1" diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 385f7aee..6c8965bd 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "oxigraph" -version = "0.3.8" +version = "0.3.9" authors = ["Tpt "] license = "MIT OR Apache-2.0" readme = "README.md" @@ -37,9 +37,9 @@ nom = "7" siphasher = "0.3" lazy_static = "1" sysinfo = "0.26" -oxrdf = { version = "0.1.0", path="oxrdf", features = ["rdf-star"] } -spargebra = { version = "0.2.2", path="spargebra", features = ["rdf-star"] } -sparesults = { version = "0.1.1", path="sparesults", features = ["rdf-star"] } +oxrdf = { version = "0.1.1", path="oxrdf", features = ["rdf-star"] } +spargebra = { version = "0.2.3", path="spargebra", features = ["rdf-star"] } +sparesults = { version = "0.1.2", path="sparesults", features = ["rdf-star"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] libc = "0.2" @@ -53,7 +53,7 @@ getrandom = {version="0.2", features=["js"]} [dev-dependencies] criterion = "0.4" oxhttp = "0.1" -zstd = "0.11" +zstd = "0.12" [target.'cfg(target_arch = "wasm32")'.dev-dependencies] wasm-bindgen-test = "0.3" diff --git a/lib/oxrdf/Cargo.toml b/lib/oxrdf/Cargo.toml index b9924f94..9154eeaa 100644 --- a/lib/oxrdf/Cargo.toml +++ b/lib/oxrdf/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "oxrdf" -version = "0.1.0" +version = "0.1.1" authors = ["Tpt "] license = "MIT OR Apache-2.0" readme = "README.md" diff --git a/lib/sparesults/Cargo.toml b/lib/sparesults/Cargo.toml index b91e6e66..6708be91 100644 --- a/lib/sparesults/Cargo.toml +++ b/lib/sparesults/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sparesults" -version = "0.1.1" +version = "0.1.2" authors = ["Tpt "] license = "MIT OR Apache-2.0" readme = "README.md" @@ -18,7 +18,7 @@ rdf-star = ["oxrdf/rdf-star"] [dependencies] json-event-parser = "0.1" -oxrdf = { version = "0.1.0", path="../oxrdf" } +oxrdf = { version = "0.1.1", path="../oxrdf" } quick-xml = "0.26" [package.metadata.docs.rs] diff --git a/lib/spargebra/Cargo.toml b/lib/spargebra/Cargo.toml index 2ae069c7..a7e19626 100644 --- a/lib/spargebra/Cargo.toml +++ b/lib/spargebra/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "spargebra" -version = "0.2.2" +version = "0.2.3" authors = ["Tpt "] license = "MIT OR Apache-2.0" readme = "README.md" @@ -21,7 +21,7 @@ peg = "0.8" rand = "0.8" oxiri = "0.2" oxilangtag = "0.1" -oxrdf = { version = "0.1.0", path="../oxrdf" } +oxrdf = { version = "0.1.1", path="../oxrdf" } [package.metadata.docs.rs] all-features = true diff --git a/lib/sparql-smith/Cargo.toml b/lib/sparql-smith/Cargo.toml new file mode 100644 index 00000000..68acc44b --- /dev/null +++ b/lib/sparql-smith/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "sparql-smith" +version = "0.1.0-alpha.1" +authors = ["Tpt "] +license = "MIT OR Apache-2.0" +readme = "README.md" +keywords = ["SPARQL"] +repository = "https://github.com/oxigraph/oxigraph/tree/main/lib/sparql-smith" +homepage = "https://oxigraph.org/" +description = """ +A SPARQL test cases generator +""" +edition = "2021" + +[dependencies] +arbitrary = { version = "1", features = ["derive"] } diff --git a/lib/sparql-smith/README.md b/lib/sparql-smith/README.md new file mode 100644 index 00000000..a02586b4 --- /dev/null +++ b/lib/sparql-smith/README.md @@ -0,0 +1,44 @@ +SPARQL smith +============ + +[![Latest Version](https://img.shields.io/crates/v/sparql-smith.svg)](https://crates.io/crates/sparql-smith) +[![Released API docs](https://docs.rs/sparql-smith/badge.svg)](https://docs.rs/sparql-smith) +[![Crates.io downloads](https://img.shields.io/crates/d/sparql-smith)](https://crates.io/crates/sparql-smith) +[![actions status](https://github.com/oxigraph/oxigraph/workflows/build/badge.svg)](https://github.com/oxigraph/oxigraph/actions) +[![Gitter](https://badges.gitter.im/oxigraph/community.svg)](https://gitter.im/oxigraph/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) + +sparql-smith is a test case generator for the [SPARQL](https://www.w3.org/TR/sparql11-overview/) language. + +It provides a single struct, `Query` that could be serialized to a SPARQL query using `to_string()`. + +The queries generated are sadly not always valid. Variables scopes are not properly handled yet. +All SPARQL features are not supported yet. + +The `DATA_TRIG` constant is provided as an example dataset on which queries could be evaluated. + +Usage example with [libfuzzer-sys](https://docs.rs/libfuzzer-sys) and [spargebra](https://docs.rs/spargebra): + +```rust +#![no_main] +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: sparql_smith::Query| { + spargebra::Query::parse(&data.to_string(), None).unwrap() +}); +``` + +## License + +This project is licensed under either of + +* Apache License, Version 2.0, ([LICENSE-APACHE](../LICENSE-APACHE) or + ``) +* MIT license ([LICENSE-MIT](../LICENSE-MIT) or + ``) + +at your option. + + +### Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in Oxigraph by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/lib/sparql-smith/src/lib.rs b/lib/sparql-smith/src/lib.rs new file mode 100644 index 00000000..95913fd4 --- /dev/null +++ b/lib/sparql-smith/src/lib.rs @@ -0,0 +1,1522 @@ +use arbitrary::{Arbitrary, Result, Unstructured}; +use std::fmt; +use std::fmt::Debug; +use std::iter::once; +use std::ops::ControlFlow; + +pub const DATA_TRIG: &str = " +@prefix : . + +:1 :2 :3 , :4 ; + :5 true , 1 , 1.0 , 1e0 . + +:3 :2 :4 ; + :5 false , 0 , 0.0 , 0e0 . +"; + +const NUMBER_OF_NAMED_NODES: u8 = 5; +const NUMBER_OF_VARIABLES: u8 = 4; + +#[derive(Arbitrary)] +pub struct Query { + // [1] QueryUnit ::= Query + // [2] Query ::= Prologue ( SelectQuery | ConstructQuery | DescribeQuery | AskQuery ) ValuesClause + variant: QueryVariant, + values_clause: ValuesClause, +} + +#[derive(Debug, Arbitrary)] +enum QueryVariant { + Select(SelectQuery), + //TODO: Other variants! +} + +impl fmt::Display for Query { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self.variant { + QueryVariant::Select(s) => write!(f, "{}", s), + }?; + write!(f, "{}", self.values_clause) + } +} + +impl Debug for Query { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(self, f) + } +} + +#[derive(Debug, Arbitrary)] +struct SelectQuery { + // [7] SelectQuery ::= SelectClause DatasetClause* WhereClause SolutionModifier + select_clause: SelectClause, + where_clause: WhereClause, + solution_modifier: SolutionModifier, +} + +impl fmt::Display for SelectQuery { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}{}{}", + self.select_clause, self.where_clause, self.solution_modifier + ) + } +} + +#[derive(Debug, Arbitrary)] +struct SubSelect { + // [8] SubSelect ::= SelectClause WhereClause SolutionModifier ValuesClause + select_clause: SelectClause, + where_clause: WhereClause, + solution_modifier: SolutionModifier, + values_clause: ValuesClause, +} + +impl fmt::Display for SubSelect { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}{}{}{}", + self.select_clause, self.where_clause, self.solution_modifier, self.values_clause + ) + } +} + +#[derive(Debug, Arbitrary)] +struct SelectClause { + // [9] SelectClause ::= 'SELECT' ( 'DISTINCT' | 'REDUCED' )? ( ( Var | ( '(' Expression 'AS' Var ')' ) )+ | '*' ) + option: Option, + values: SelectValues, +} + +#[derive(Debug, Arbitrary)] +enum SelectOption { + Distinct, + Reduced, +} + +#[derive(Debug, Arbitrary)] +enum SelectValues { + Star, + Projection { + start: SelectProjection, + others: Vec, + }, +} + +#[derive(Debug, Arbitrary)] +enum SelectProjection { + Variable(Var), + Projection(Expression, Var), +} + +impl fmt::Display for SelectClause { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "SELECT")?; + if let Some(option) = &self.option { + match option { + SelectOption::Distinct => write!(f, " DISTINCT"), + SelectOption::Reduced => write!(f, " REDUCED"), + }?; + } + match &self.values { + SelectValues::Star => write!(f, " *"), + SelectValues::Projection { start, others } => { + for e in once(start).chain(others) { + match e { + SelectProjection::Variable(v) => write!(f, " {}", v), + SelectProjection::Projection(e, v) => write!(f, " ({} AS {})", e, v), + }?; + } + Ok(()) + } + } + } +} + +#[derive(Debug, Arbitrary)] +struct WhereClause { + // [17] WhereClause ::= 'WHERE'? GroupGraphPattern + with_where: bool, + group_graph_pattern: GroupGraphPattern, +} + +impl fmt::Display for WhereClause { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.with_where { + write!(f, " WHERE ")?; + } + write!(f, "{}", self.group_graph_pattern) + } +} + +#[derive(Debug, Arbitrary)] +struct SolutionModifier { + // [18] SolutionModifier ::= GroupClause? HavingClause? OrderClause? LimitOffsetClauses? + group: Option, + having: Option, + order: Option, + limit_offset: Option, +} + +impl fmt::Display for SolutionModifier { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(group) = &self.group { + write!(f, " {}", group)?; + } + if let Some(having) = &self.having { + write!(f, " {}", having)?; + } + if let Some(order) = &self.order { + write!(f, " {}", order)?; + } + if let Some(limit_offset) = &self.limit_offset { + write!(f, " {}", limit_offset)?; + } + Ok(()) + } +} + +#[derive(Debug, Arbitrary)] +struct GroupClause { + // [19] GroupClause ::= 'GROUP' 'BY' GroupCondition+ + start: GroupCondition, + others: Vec, +} + +impl fmt::Display for GroupClause { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "GROUP BY {}", self.start)?; + for o in &self.others { + write!(f, " {}", o)?; + } + Ok(()) + } +} + +#[derive(Debug, Arbitrary)] +enum GroupCondition { + // [20] GroupCondition ::= BuiltInCall | FunctionCall | '(' Expression ( 'AS' Var )? ')' | Var + BuiltInCall(BuiltInCall), + // TODO FunctionCall(FunctionCall) + Projection(Expression, Option), + Var(Var), +} + +impl fmt::Display for GroupCondition { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::BuiltInCall(c) => write!(f, "{}", c), + //Self::FunctionCall(c) => write!(f, "{}", c), + Self::Projection(e, v) => { + if let Some(v) = v { + write!(f, "({} AS {})", e, v) + } else { + write!(f, "({})", e) + } + } + Self::Var(v) => write!(f, "{}", v), + } + } +} + +#[derive(Debug, Arbitrary)] +struct HavingClause { + // [21] HavingClause ::= 'HAVING' HavingCondition+ + start: HavingCondition, + others: Vec, +} + +impl fmt::Display for HavingClause { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "HAVING {}", self.start)?; + for o in &self.others { + write!(f, " {}", o)?; + } + Ok(()) + } +} + +// [22] HavingCondition ::= Constraint +type HavingCondition = Constraint; + +#[derive(Debug, Arbitrary)] +struct OrderClause { + // [23] OrderClause ::= 'ORDER' 'BY' OrderCondition+ + start: OrderCondition, + others: Vec, +} + +impl fmt::Display for OrderClause { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "ORDER BY {}", self.start)?; + for other in &self.others { + write!(f, " {}", other)?; + } + Ok(()) + } +} + +#[derive(Debug, Arbitrary)] +enum OrderCondition { + // [24] OrderCondition ::= ( ( 'ASC' | 'DESC' ) BrackettedExpression ) | ( Constraint | Var ) + BrackettedExpression { + is_asc: bool, + inner: BrackettedExpression, + }, + Constraint(Constraint), + Var(Var), +} + +impl fmt::Display for OrderCondition { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::BrackettedExpression { is_asc, inner } => { + if *is_asc { + write!(f, "ASC{}", inner) + } else { + write!(f, "DESC{}", inner) + } + } + Self::Constraint(c) => write!(f, "{}", c), + Self::Var(v) => write!(f, "{}", v), + } + } +} + +#[derive(Debug, Arbitrary)] +enum LimitOffsetClauses { + // [25] LimitOffsetClauses ::= LimitClause OffsetClause? | OffsetClause LimitClause? + LimitOffset(LimitClause, Option), + OffsetLimit(OffsetClause, Option), +} + +impl fmt::Display for LimitOffsetClauses { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::LimitOffset(l, Some(o)) => write!(f, "{} {}", l, o), + Self::LimitOffset(l, None) => write!(f, "{}", l), + Self::OffsetLimit(o, Some(l)) => write!(f, "{} {}", o, l), + Self::OffsetLimit(o, None) => write!(f, "{}", o), + } + } +} + +#[derive(Debug, Arbitrary)] +struct LimitClause { + // [26] LimitClause ::= 'LIMIT' INTEGER + value: u8, +} + +impl fmt::Display for LimitClause { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "LIMIT {}", self.value) + } +} + +#[derive(Debug, Arbitrary)] +struct OffsetClause { + // [27] OffsetClause ::= 'OFFSET' INTEGER + value: u8, +} + +impl fmt::Display for OffsetClause { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "OFFSET {}", self.value) + } +} + +#[derive(Debug, Arbitrary)] +struct ValuesClause { + // [28] ValuesClause ::= ( 'VALUES' DataBlock )? + value: Option, +} + +impl fmt::Display for ValuesClause { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(value) = &self.value { + write!(f, " VALUES {}", value) + } else { + Ok(()) + } + } +} + +#[derive(Debug, Arbitrary)] +enum GroupGraphPattern { + // [53] GroupGraphPattern ::= '{' ( SubSelect | GroupGraphPatternSub ) '}' + GroupGraphPatternSub(GroupGraphPatternSub), + SubSelect(Box), +} + +impl fmt::Display for GroupGraphPattern { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, " {{ ")?; + match self { + Self::GroupGraphPatternSub(p) => write!(f, "{}", p), + Self::SubSelect(s) => write!(f, "{}", s), + }?; + write!(f, " }} ") + } +} + +#[derive(Debug, Arbitrary)] +struct GroupGraphPatternSub { + // [54] GroupGraphPatternSub ::= TriplesBlock? ( GraphPatternNotTriples '.'? TriplesBlock? )* + start: Option, + others: Vec, +} + +#[derive(Debug, Arbitrary)] +struct GroupGraphPatternSubOtherBlock { + start: GraphPatternNotTriples, + with_dot: bool, + end: Option, +} + +impl fmt::Display for GroupGraphPatternSub { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(start) = &self.start { + write!(f, "{}", start)?; + } + for other in &self.others { + write!(f, "{}", other.start)?; + if other.with_dot { + write!(f, " . ")?; + } + if let Some(end) = &other.end { + write!(f, "{}", end)?; + } + } + Ok(()) + } +} + +#[derive(Debug, Arbitrary)] +struct TriplesBlock { + // [55] TriplesBlock ::= TriplesSameSubjectPath ( '.' TriplesBlock? )? + start: TriplesSameSubjectPath, + end: Option>>, +} + +impl fmt::Display for TriplesBlock { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.start)?; + if let Some(end) = &self.end { + write!(f, " . ")?; + if let Some(end) = end { + write!(f, "{}", end)?; + } + } + Ok(()) + } +} + +#[derive(Debug, Arbitrary)] +enum GraphPatternNotTriples { + // [56] GraphPatternNotTriples ::= GroupOrUnionGraphPattern | OptionalGraphPattern | MinusGraphPattern | GraphGraphPattern | ServiceGraphPattern | Filter | Bind | InlineData + GroupOrUnion(GroupOrUnionGraphPattern), + Optional(OptionalGraphPattern), + Minus(MinusGraphPattern), + Graph(GraphGraphPattern), + Filter(Filter), + Bind(Bind), + InlineData(InlineData), // TODO: ServiceGraphPattern +} + +impl fmt::Display for GraphPatternNotTriples { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::GroupOrUnion(p) => write!(f, "{}", p), + Self::Optional(p) => write!(f, "{}", p), + Self::Minus(p) => write!(f, "{}", p), + Self::Graph(p) => write!(f, "{}", p), + Self::Filter(p) => write!(f, "{}", p), + Self::Bind(p) => write!(f, "{}", p), + Self::InlineData(p) => write!(f, "{}", p), + } + } +} + +#[derive(Debug, Arbitrary)] +struct OptionalGraphPattern { + // [57] OptionalGraphPattern ::= 'OPTIONAL' GroupGraphPattern + inner: GroupGraphPattern, +} + +impl fmt::Display for OptionalGraphPattern { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, " OPTIONAL {}", self.inner) + } +} + +#[derive(Debug, Arbitrary)] +struct GraphGraphPattern { + // [58] GraphGraphPattern ::= 'GRAPH' VarOrIri GroupGraphPattern + graph: VarOrIri, + inner: GroupGraphPattern, +} + +impl fmt::Display for GraphGraphPattern { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, " GRAPH {} {}", self.graph, self.inner) + } +} + +#[derive(Debug, Arbitrary)] +struct Bind { + // [60] Bind ::= 'BIND' '(' Expression 'AS' Var ')' + expression: Expression, + var: Var, +} + +impl fmt::Display for Bind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, " BIND({} AS {})", self.expression, self.var) + } +} + +#[derive(Debug, Arbitrary)] +struct InlineData { + // [61] InlineData ::= 'VALUES' DataBlock + inner: DataBlock, +} + +impl fmt::Display for InlineData { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "VALUES {}", &self.inner) + } +} + +#[derive(Debug, Arbitrary)] +enum DataBlock { + // [62] DataBlock ::= InlineDataOneVar | InlineDataFull + OneVar(InlineDataOneVar), + Full(InlineDataFull), +} + +impl fmt::Display for DataBlock { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::OneVar(e) => write!(f, "{}", e), + Self::Full(c) => write!(f, "{}", c), + } + } +} + +#[derive(Debug, Arbitrary)] +struct InlineDataOneVar { + // [63] InlineDataOneVar ::= Var '{' DataBlockValue* '}' + var: Var, + values: Vec, +} + +impl fmt::Display for InlineDataOneVar { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} {{", self.var)?; + for v in &self.values { + write!(f, " {}", v)?; + } + write!(f, " }}") + } +} + +#[derive(Debug)] +struct InlineDataFull { + // [64] InlineDataFull ::= ( NIL | '(' Var* ')' ) '{' ( '(' DataBlockValue* ')' | NIL )* '}' + vars: Vec, + values: Vec>, +} + +impl<'a> Arbitrary<'a> for InlineDataFull { + fn arbitrary(u: &mut Unstructured<'a>) -> Result { + let vars = u.arbitrary_iter()?.collect::>>()?; + + let mut values = Vec::new(); + u.arbitrary_loop(Some(0), Some(3), |u| { + let mut row = Vec::with_capacity(vars.len()); + u.arbitrary_loop( + Some(vars.len().try_into().unwrap()), + Some(vars.len().try_into().unwrap()), + |u| { + row.push(u.arbitrary()?); + Ok(ControlFlow::Continue(())) + }, + )?; + values.push(row); + Ok(ControlFlow::Continue(())) + })?; + + Ok(Self { vars, values }) + } +} + +impl fmt::Display for InlineDataFull { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "( ")?; + for v in &self.vars { + write!(f, " {}", v)?; + } + write!(f, " ) {{")?; + for vs in &self.values { + write!(f, " (")?; + for v in vs { + write!(f, " {}", v)?; + } + write!(f, " )")?; + } + write!(f, " }}") + } +} + +#[derive(Debug, Arbitrary)] +enum DataBlockValue { + // [65] DataBlockValue ::= iri | RDFLiteral | NumericLiteral | BooleanLiteral | 'UNDEF' + Iri(Iri), + Literal(Literal), + Undef, +} + +impl fmt::Display for DataBlockValue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Iri(i) => write!(f, "{}", i), + Self::Literal(l) => write!(f, "{}", l), + Self::Undef => write!(f, "UNDEF"), + } + } +} + +#[derive(Debug, Arbitrary)] +struct MinusGraphPattern { + // [66] MinusGraphPattern ::= 'MINUS' GroupGraphPattern + inner: GroupGraphPattern, +} + +impl fmt::Display for MinusGraphPattern { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, " MINUS {}", self.inner) + } +} + +#[derive(Debug, Arbitrary)] +struct GroupOrUnionGraphPattern { + // [67] GroupOrUnionGraphPattern ::= GroupGraphPattern ( 'UNION' GroupGraphPattern )* + start: GroupGraphPattern, + others: Vec, +} + +impl fmt::Display for GroupOrUnionGraphPattern { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.start)?; + for other in &self.others { + write!(f, " UNION {}", other)?; + } + Ok(()) + } +} + +#[derive(Debug, Arbitrary)] +struct Filter { + // [68] Filter ::= 'FILTER' Constraint + constraint: Constraint, +} + +impl fmt::Display for Filter { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "FILTER {}", self.constraint) + } +} + +#[derive(Debug, Arbitrary)] +enum Constraint { + // [69] Constraint ::= BrackettedExpression | BuiltInCall | FunctionCall + BrackettedExpression(BrackettedExpression), + BuiltInCall(BuiltInCall), + // TODO FunctionCall(FunctionCall), +} + +impl fmt::Display for Constraint { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::BrackettedExpression(e) => write!(f, "{}", e), + Self::BuiltInCall(c) => write!(f, "{}", c), + //Self::FunctionCall(c) => write!(f, "{}", c), + } + } +} + +#[derive(Debug, Arbitrary)] +struct FunctionCall { + // [70] FunctionCall ::= iri ArgList + iri: Iri, + args: ArgList, +} + +impl fmt::Display for FunctionCall { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}{}", self.iri, self.args) + } +} + +#[derive(Debug, Arbitrary)] +enum ArgList { + // [71] ArgList ::= NIL | '(' 'DISTINCT'? Expression ( ',' Expression )* ')' + Nil, + NotNil { + // TODO: DISTINCT + start: Box, + others: Vec, + }, +} + +impl fmt::Display for ArgList { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "(")?; + if let Self::NotNil { start, others } = self { + write!(f, "{}", start)?; + for e in others { + write!(f, ", {}", e)?; + } + } + write!(f, ")") + } +} + +#[derive(Debug, Arbitrary)] +struct ExpressionList { + // [72] ExpressionList ::= NIL | '(' Expression ( ',' Expression )* ')' + inner: Vec, +} + +impl fmt::Display for ExpressionList { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "(")?; + for (i, e) in self.inner.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{}", e)?; + } + write!(f, ")") + } +} + +#[derive(Debug, Arbitrary)] +struct PropertyListNotEmpty { + // [77] PropertyListNotEmpty ::= Verb ObjectList ( ';' ( Verb ObjectList )? )* + start_predicate: Verb, + start_object: Box, + others: Vec>, +} + +#[derive(Debug, Arbitrary)] +struct PropertyListElement { + predicate: Verb, + object: ObjectList, +} + +impl fmt::Display for PropertyListNotEmpty { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} {}", self.start_predicate, self.start_object)?; + for other in &self.others { + write!(f, " ; ")?; + if let Some(e) = other { + write!(f, "{} {}", e.predicate, e.object)?; + } + } + Ok(()) + } +} + +#[derive(Debug, Arbitrary)] +enum Verb { + // [78] Verb ::= VarOrIri | 'a' + VarOrIri(VarOrIri), + A, +} + +impl fmt::Display for Verb { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::VarOrIri(iri) => write!(f, "{}", iri), + Self::A => write!(f, " a "), + } + } +} + +#[derive(Debug, Arbitrary)] +struct ObjectList { + // [79] ObjectList ::= Object ( ',' Object )* + start: Object, + others: Vec, +} + +impl fmt::Display for ObjectList { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.start)?; + for other in &self.others { + write!(f, " , ")?; + write!(f, "{}", other)?; + } + Ok(()) + } +} + +// [80] Object ::= GraphNode +type Object = GraphNode; + +#[derive(Debug, Arbitrary)] +enum TriplesSameSubjectPath { + // [81] TriplesSameSubjectPath ::= VarOrTerm PropertyListPathNotEmpty | TriplesNodePath PropertyListPath + Atomic { + subject: VarOrTerm, + predicate_object: PropertyListPathNotEmpty, + }, + Other { + subject: TriplesNodePath, + predicate_object: PropertyListPath, + }, +} + +impl fmt::Display for TriplesSameSubjectPath { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Atomic { + subject, + predicate_object, + } => { + write!(f, "{}{}", subject, predicate_object) + } + Self::Other { + subject, + predicate_object, + } => { + write!(f, "{} {}", subject, predicate_object) + } + } + } +} + +#[derive(Debug, Arbitrary)] +struct PropertyListPath { + // [82] PropertyListPath ::= PropertyListPathNotEmpty? + inner: Option, +} + +impl fmt::Display for PropertyListPath { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(p) = &self.inner { + write!(f, "{}", p) + } else { + Ok(()) + } + } +} + +#[derive(Debug, Arbitrary)] +struct PropertyListPathNotEmpty { + // [83] PropertyListPathNotEmpty ::= ( VerbPath | VerbSimple ) ObjectListPath ( ';' ( ( VerbPath | VerbSimple ) ObjectList )? )* + start_predicate: PropertyListPathNotEmptyVerb, + start_object: Box, + others: Vec>, +} + +#[derive(Debug, Arbitrary)] +enum PropertyListPathNotEmptyVerb { + VerbPath(VerbPath), + VerbSimple(VerbSimple), +} + +#[derive(Debug, Arbitrary)] +struct PropertyListPathElement { + predicate: PropertyListPathNotEmptyVerb, + object: ObjectList, +} + +impl fmt::Display for PropertyListPathNotEmpty { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self.start_predicate { + PropertyListPathNotEmptyVerb::VerbPath(p) => write!(f, "{}", p), + PropertyListPathNotEmptyVerb::VerbSimple(s) => write!(f, "{}", s), + }?; + write!(f, "{}", self.start_object)?; + for other in &self.others { + write!(f, " ; ")?; + if let Some(e) = other { + match &e.predicate { + PropertyListPathNotEmptyVerb::VerbPath(p) => write!(f, "{}", p), + PropertyListPathNotEmptyVerb::VerbSimple(s) => write!(f, "{}", s), + }?; + write!(f, "{}", e.object)?; + } + } + Ok(()) + } +} + +// [84] VerbPath ::= Path +type VerbPath = Path; + +// [85] VerbSimple ::= Var +type VerbSimple = Var; + +#[derive(Debug, Arbitrary)] +struct ObjectListPath { + // [86] ObjectListPath ::= ObjectPath ( ',' ObjectPath )* + start: ObjectPath, + others: Vec, +} + +impl fmt::Display for ObjectListPath { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.start)?; + for other in &self.others { + write!(f, " , {}", other)?; + } + Ok(()) + } +} + +// [87] ObjectPath ::= GraphNodePath +type ObjectPath = GraphNodePath; + +// [88] Path ::= PathAlternative +type Path = PathAlternative; + +#[derive(Debug, Arbitrary)] +struct PathAlternative { + // [89] PathAlternative ::= PathSequence ( '|' PathSequence )* + start: PathSequence, + others: Vec, +} + +impl fmt::Display for PathAlternative { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.start)?; + for other in &self.others { + write!(f, " | {}", other)?; + } + Ok(()) + } +} + +#[derive(Debug, Arbitrary)] +struct PathSequence { + // [90] PathSequence ::= PathEltOrInverse ( '/' PathEltOrInverse )* + start: PathEltOrInverse, + others: Vec, +} + +impl fmt::Display for PathSequence { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.start)?; + for other in &self.others { + write!(f, " / {}", other)?; + } + Ok(()) + } +} + +#[derive(Debug, Arbitrary)] +struct PathElt { + // [91] PathElt ::= PathPrimary PathMod? + path: PathPrimary, + mode: Option, +} + +impl fmt::Display for PathElt { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.path)?; + if let Some(mode) = &self.mode { + write!(f, "{}", mode)?; + } + Ok(()) + } +} + +#[derive(Debug, Arbitrary)] +enum PathEltOrInverse { + // [92] PathEltOrInverse ::= PathElt | '^' PathElt + PathElt(PathElt), + Inverse(PathElt), +} + +impl fmt::Display for PathEltOrInverse { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::PathElt(e) => write!(f, "{}", e), + Self::Inverse(e) => write!(f, " ^{}", e), + } + } +} + +#[derive(Debug, Arbitrary)] +enum PathMod { + // [93] PathMod ::= '?' | '*' | '+' + ZeroOrOne, + ZeroOrMore, + OneOrMore, +} + +impl fmt::Display for PathMod { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::ZeroOrOne => write!(f, " ? "), + Self::ZeroOrMore => write!(f, " * "), + Self::OneOrMore => write!(f, " + "), + } + } +} + +#[derive(Debug, Arbitrary)] +enum PathPrimary { + // [94] PathPrimary ::= iri | 'a' | '!' PathNegatedPropertySet | '(' Path ')' + Iri(Iri), + A, + Negated(PathNegatedPropertySet), + Child(Box), +} + +impl fmt::Display for PathPrimary { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Iri(iri) => write!(f, "{}", iri), + Self::A => write!(f, " a "), + Self::Negated(n) => write!(f, "!{}", n), + Self::Child(c) => write!(f, "({})", c), + } + } +} + +#[derive(Debug, Arbitrary)] +enum PathNegatedPropertySet { + // [95] PathNegatedPropertySet ::= PathOneInPropertySet | '(' ( PathOneInPropertySet ( '|' PathOneInPropertySet )* )? ')' + Single(PathOneInPropertySet), + Multiple { + start: PathOneInPropertySet, + others: Vec, + }, +} + +impl fmt::Display for PathNegatedPropertySet { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Single(p) => write!(f, "{}", p), + Self::Multiple { start, others } => { + write!(f, " ( {}", start)?; + for other in others { + write!(f, " | {}", other)?; + } + write!(f, " ) ") + } + } + } +} + +#[derive(Debug, Arbitrary)] +enum PathOneInPropertySet { + // [96] PathOneInPropertySet ::= iri | 'a' | '^' ( iri | 'a' ) + Iri(Iri), + A, + NegatedIri(Iri), + NegatedA, +} + +impl fmt::Display for PathOneInPropertySet { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Iri(iri) => write!(f, "{}", iri), + Self::A => write!(f, " a "), + Self::NegatedIri(iri) => write!(f, "^{}", iri), + Self::NegatedA => write!(f, " ^a "), + } + } +} + +#[derive(Debug, Arbitrary)] +enum TriplesNode { + // [98] TriplesNode ::= Collection | BlankNodePropertyList + Collection(Collection), + BlankNodePropertyList(BlankNodePropertyList), +} + +impl fmt::Display for TriplesNode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Collection(p) => write!(f, "{}", p), + Self::BlankNodePropertyList(p) => write!(f, "{}", p), + } + } +} + +#[derive(Debug, Arbitrary)] +struct BlankNodePropertyList { + // [99] BlankNodePropertyList ::= '[' PropertyListNotEmpty ']' + inner: PropertyListNotEmpty, +} + +impl fmt::Display for BlankNodePropertyList { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "[ {} ]", self.inner) + } +} + +#[derive(Debug, Arbitrary)] +enum TriplesNodePath { + // [100] TriplesNodePath ::= CollectionPath | BlankNodePropertyListPath + CollectionPath(CollectionPath), + BlankNodePropertyListPath(BlankNodePropertyListPath), +} + +impl fmt::Display for TriplesNodePath { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::CollectionPath(p) => write!(f, "{}", p), + Self::BlankNodePropertyListPath(p) => write!(f, "{}", p), + } + } +} + +#[derive(Debug, Arbitrary)] +struct BlankNodePropertyListPath { + // [101] BlankNodePropertyListPath ::= '[' PropertyListPathNotEmpty ']' + inner: PropertyListPathNotEmpty, +} + +impl fmt::Display for BlankNodePropertyListPath { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "[ {} ]", self.inner) + } +} + +#[derive(Debug, Arbitrary)] +struct Collection { + // [102] Collection ::= '(' GraphNode+ ')' + start: Box, + others: Vec, +} + +impl fmt::Display for Collection { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "( {}", self.start)?; + for e in &self.others { + write!(f, " {}", e)?; + } + write!(f, " )") + } +} + +#[derive(Debug, Arbitrary)] +struct CollectionPath { + // [103] CollectionPath ::= '(' GraphNodePath+ ')' + start: Box, + others: Vec, +} + +impl fmt::Display for CollectionPath { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "( {}", self.start)?; + for e in &self.others { + write!(f, " {}", e)?; + } + write!(f, " )") + } +} + +#[derive(Debug, Arbitrary)] +enum GraphNode { + // [104] GraphNode ::= VarOrTerm | TriplesNode + VarOrTerm(VarOrTerm), + TriplesNode(TriplesNode), +} + +impl fmt::Display for GraphNode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::VarOrTerm(t) => write!(f, "{}", t), + Self::TriplesNode(t) => write!(f, "{}", t), + } + } +} + +#[derive(Debug, Arbitrary)] +enum GraphNodePath { + // [105] GraphNodePath ::= VarOrTerm | TriplesNodePath + VarOrTerm(VarOrTerm), + TriplesNodePath(TriplesNodePath), +} + +impl fmt::Display for GraphNodePath { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::VarOrTerm(t) => write!(f, "{}", t), + Self::TriplesNodePath(p) => write!(f, "{}", p), + } + } +} + +#[derive(Debug, Arbitrary)] +enum VarOrTerm { + // [106] VarOrTerm ::= Var | GraphTerm + Var(Var), + GraphTerm(GraphTerm), +} + +impl fmt::Display for VarOrTerm { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Var(v) => write!(f, "{}", v), + Self::GraphTerm(t) => write!(f, "{}", t), + } + } +} + +#[derive(Debug, Arbitrary)] +enum VarOrIri { + // [107] VarOrIri ::= Var | iri + Var(Var), + Iri(Iri), +} + +impl fmt::Display for VarOrIri { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Var(v) => write!(f, "{}", v), + Self::Iri(t) => write!(f, "{}", t), + } + } +} + +#[derive(Debug)] +struct Var { + // [108] Var ::= VAR1 | VAR2 + value: u8, +} + +impl Arbitrary<'_> for Var { + fn arbitrary(u: &mut Unstructured<'_>) -> Result { + Ok(Self { + value: u.int_in_range(1..=NUMBER_OF_VARIABLES)?, + }) + } + + fn size_hint(depth: usize) -> (usize, Option) { + ::size_hint(depth) + } +} + +impl fmt::Display for Var { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, " ?{} ", self.value) + } +} + +#[derive(Debug, Arbitrary)] +enum GraphTerm { + // [109] GraphTerm ::= iri | RDFLiteral | NumericLiteral | BooleanLiteral | BlankNode | NIL + Iri(Iri), + Literal(Literal), + Nil, + // TODO: BlankNode +} + +impl fmt::Display for GraphTerm { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Iri(iri) => write!(f, "{}", iri), + Self::Literal(l) => write!(f, "{}", l), + Self::Nil => write!(f, " () "), + } + } +} + +// [110] Expression ::= ConditionalOrExpression +type Expression = ConditionalOrExpression; + +#[derive(Debug, Arbitrary)] +struct ConditionalOrExpression { + // [111] ConditionalOrExpression ::= ConditionalAndExpression ( '||' ConditionalAndExpression )* + start: ConditionalAndExpression, + others: Vec, +} + +impl fmt::Display for ConditionalOrExpression { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.start)?; + for e in &self.others { + write!(f, " || {}", e)?; + } + Ok(()) + } +} + +#[derive(Debug, Arbitrary)] +struct ConditionalAndExpression { + // [112] ConditionalAndExpression ::= ValueLogical ( '&&' ValueLogical )* + start: ValueLogical, + others: Vec, +} + +impl fmt::Display for ConditionalAndExpression { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.start)?; + for e in &self.others { + write!(f, " && {}", e)?; + } + Ok(()) + } +} + +// [113] ValueLogical ::= RelationalExpression +type ValueLogical = RelationalExpression; + +#[derive(Debug, Arbitrary)] +enum RelationalExpression { + // [114] RelationalExpression ::= NumericExpression ( '=' NumericExpression | '!=' NumericExpression | '<' NumericExpression | '>' NumericExpression | '<=' NumericExpression | '>=' NumericExpression | 'IN' ExpressionList | 'NOT' 'IN' ExpressionList )? + Base(NumericExpression), + Equal(NumericExpression, NumericExpression), + NotEqual(NumericExpression, NumericExpression), + Less(NumericExpression, NumericExpression), + LessOrEqual(NumericExpression, NumericExpression), + Greater(NumericExpression, NumericExpression), + GreaterOrEqual(NumericExpression, NumericExpression), + In(NumericExpression, ExpressionList), + NotIn(NumericExpression, ExpressionList), +} + +impl fmt::Display for RelationalExpression { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Base(e) => write!(f, "{}", e), + Self::Equal(a, b) => write!(f, "{} = {}", a, b), + Self::NotEqual(a, b) => write!(f, "{} != {}", a, b), + Self::Less(a, b) => write!(f, "{} < {}", a, b), + Self::LessOrEqual(a, b) => write!(f, "{} <= {}", a, b), + Self::Greater(a, b) => write!(f, "{} > {}", a, b), + Self::GreaterOrEqual(a, b) => write!(f, "{} >= {}", a, b), + Self::In(a, b) => write!(f, "{} IN {}", a, b), + Self::NotIn(a, b) => write!(f, "{} NOT IN {}", a, b), + } + } +} + +// [115] NumericExpression ::= AdditiveExpression +type NumericExpression = AdditiveExpression; + +#[derive(Debug, Arbitrary)] +enum AdditiveExpression { + // [116] AdditiveExpression ::= MultiplicativeExpression ( '+' MultiplicativeExpression | '-' MultiplicativeExpression | ( NumericLiteralPositive | NumericLiteralNegative ) ( ( '*' UnaryExpression ) | ( '/' UnaryExpression ) )* )* + Base(MultiplicativeExpression), + Plus(MultiplicativeExpression, MultiplicativeExpression), + Minus(MultiplicativeExpression, MultiplicativeExpression), // TODO: Prefix + and - +} + +impl fmt::Display for AdditiveExpression { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Base(e) => write!(f, "{}", e), + Self::Plus(a, b) => write!(f, "{} + {}", a, b), + Self::Minus(a, b) => write!(f, "{} - {}", a, b), + } + } +} + +#[derive(Debug, Arbitrary)] +enum MultiplicativeExpression { + // [117] MultiplicativeExpression ::= UnaryExpression ( '*' UnaryExpression | '/' UnaryExpression )* + Base(UnaryExpression), + Mul(UnaryExpression, UnaryExpression), + Div(UnaryExpression, UnaryExpression), +} + +impl fmt::Display for MultiplicativeExpression { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Base(e) => write!(f, "{}", e), + Self::Mul(a, b) => write!(f, "{} * {}", a, b), + Self::Div(a, b) => write!(f, "{} / {}", a, b), + } + } +} + +#[derive(Debug, Arbitrary)] +enum UnaryExpression { + // [118] UnaryExpression ::= '!' PrimaryExpression | '+' PrimaryExpression | '-' PrimaryExpression | PrimaryExpression + Not(PrimaryExpression), + Plus(PrimaryExpression), + Minus(PrimaryExpression), + Base(PrimaryExpression), +} + +impl fmt::Display for UnaryExpression { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Not(e) => write!(f, "!{}", e), + Self::Plus(e) => write!(f, "+{}", e), + Self::Minus(e) => write!(f, "-{}", e), + Self::Base(e) => write!(f, "{}", e), + } + } +} + +#[derive(Debug, Arbitrary)] +enum PrimaryExpression { + // [119] PrimaryExpression ::= BrackettedExpression | BuiltInCall | iriOrFunction | RDFLiteral | NumericLiteral | BooleanLiteral | Var + Bracketted(BrackettedExpression), + BuiltInCall(BuiltInCall), + IriOrFunction(IriOrFunction), + Literal(Literal), + Var(Var), +} + +impl fmt::Display for PrimaryExpression { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Bracketted(e) => write!(f, "{}", e), + Self::BuiltInCall(e) => write!(f, "{}", e), + Self::IriOrFunction(e) => write!(f, "{}", e), + Self::Literal(e) => write!(f, "{}", e), + Self::Var(e) => write!(f, "{}", e), + } + } +} + +#[derive(Debug, Arbitrary)] +struct BrackettedExpression { + // [120] BrackettedExpression ::= '(' Expression ')' + inner: Box, +} + +impl fmt::Display for BrackettedExpression { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "({})", self.inner) + } +} + +#[derive(Debug, Arbitrary)] +enum BuiltInCall { + // [121] BuiltInCall ::= Aggregate + // | 'STR' '(' Expression ')' + // | 'LANG' '(' Expression ')' + // | 'LANGMATCHES' '(' Expression ',' Expression ')' + // | 'DATATYPE' '(' Expression ')' + // | 'BOUND' '(' Var ')' + // | 'IRI' '(' Expression ')' + // | 'URI' '(' Expression ')' + // | 'BNODE' ( '(' Expression ')' | NIL ) + // | 'RAND' NIL + // | 'ABS' '(' Expression ')' + // | 'CEIL' '(' Expression ')' + // | 'FLOOR' '(' Expression ')' + // | 'ROUND' '(' Expression ')' + // | 'CONCAT' ExpressionList + // | SubstringExpression + // | 'STRLEN' '(' Expression ')' + // | StrReplaceExpression + // | 'UCASE' '(' Expression ')' + // | 'LCASE' '(' Expression ')' + // | 'ENCODE_FOR_URI' '(' Expression ')' + // | 'CONTAINS' '(' Expression ',' Expression ')' + // | 'STRSTARTS' '(' Expression ',' Expression ')' + // | 'STRENDS' '(' Expression ',' Expression ')' + // | 'STRBEFORE' '(' Expression ',' Expression ')' + // | 'STRAFTER' '(' Expression ',' Expression ')' + // | 'YEAR' '(' Expression ')' + // | 'MONTH' '(' Expression ')' + // | 'DAY' '(' Expression ')' + // | 'HOURS' '(' Expression ')' + // | 'MINUTES' '(' Expression ')' + // | 'SECONDS' '(' Expression ')' + // | 'TIMEZONE' '(' Expression ')' + // | 'TZ' '(' Expression ')' + // | 'NOW' NIL + // | 'UUID' NIL + // | 'STRUUID' NIL + // | 'MD5' '(' Expression ')' + // | 'SHA1' '(' Expression ')' + // | 'SHA256' '(' Expression ')' + // | 'SHA384' '(' Expression ')' + // | 'SHA512' '(' Expression ')' + // | 'COALESCE' ExpressionList + // | 'IF' '(' Expression ',' Expression ',' Expression ')' + // | 'STRLANG' '(' Expression ',' Expression ')' + // | 'STRDT' '(' Expression ',' Expression ')' + // | 'sameTerm' '(' Expression ',' Expression ')' + // | 'isIRI' '(' Expression ')' + // | 'isURI' '(' Expression ')' + // | 'isBLANK' '(' Expression ')' + // | 'isLITERAL' '(' Expression ')' + // | 'isNUMERIC' '(' Expression ')' + // | RegexExpression + // | ExistsFunc + // | NotExistsFunc + Bound(Var), //TODO: Other functions +} + +impl fmt::Display for BuiltInCall { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Bound(v) => write!(f, "BOUND({})", v), + } + } +} + +#[derive(Debug, Arbitrary)] +struct IriOrFunction { + // [128] iriOrFunction ::= iri ArgList? + iri: Iri, + //TODO args: Option, +} + +impl fmt::Display for IriOrFunction { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.iri)?; + /*if let Some(args) = &self.args { + write!(f, "{}", args)?; + }*/ + Ok(()) + } +} + +#[derive(Debug, Arbitrary)] +struct Literal { + // [129] RDFLiteral ::= String ( LANGTAG | ( '^^' iri ) )? + // [130] NumericLiteral ::= NumericLiteralUnsigned | NumericLiteralPositive | NumericLiteralNegative + // [131] NumericLiteralUnsigned ::= INTEGER | DECIMAL | DOUBLE + // [132] NumericLiteralPositive ::= INTEGER_POSITIVE | DECIMAL_POSITIVE | DOUBLE_POSITIVE + // [133] NumericLiteralNegative ::= INTEGER_NEGATIVE | DECIMAL_NEGATIVE | DOUBLE_NEGATIVE + // [134] BooleanLiteral ::= 'true' | 'false' + //TODO: Implement! +} + +impl fmt::Display for Literal { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, " 123 ") + } +} + +#[derive(Debug)] +struct Iri { + // [136] iri ::= IRIREF | PrefixedName + value: u8, +} + +impl Arbitrary<'_> for Iri { + fn arbitrary(u: &mut Unstructured<'_>) -> Result { + Ok(Self { + value: u.int_in_range(1..=NUMBER_OF_NAMED_NODES)?, + }) + } + + fn size_hint(depth: usize) -> (usize, Option) { + ::size_hint(depth) + } +} + +impl fmt::Display for Iri { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, " ", self.value) + } +} diff --git a/lib/src/sparql/update.rs b/lib/src/sparql/update.rs index 38a23129..6bd1e9e5 100644 --- a/lib/src/sparql/update.rs +++ b/lib/src/sparql/update.rs @@ -131,8 +131,9 @@ impl<'a, 'b: 'a> SimpleUpdateEvaluator<'a, 'b> { Rc::new(self.options.query_options.custom_functions.clone()), ); let mut bnodes = HashMap::new(); - for tuple in evaluator.plan_evaluator(&plan)(EncodedTuple::with_capacity(variables.len())) { - let tuple = tuple?; + let tuples = evaluator.plan_evaluator(&plan)(EncodedTuple::with_capacity(variables.len())) + .collect::, _>>()?; //TODO: would be much better to stream + for tuple in tuples { for quad in delete { if let Some(quad) = Self::convert_ground_quad_pattern(quad, &variables, &tuple, &dataset)? diff --git a/python/Cargo.toml b/python/Cargo.toml index 20762193..95be846b 100644 --- a/python/Cargo.toml +++ b/python/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyoxigraph" -version = "0.3.8" +version = "0.3.9" authors = ["Tpt"] license = "MIT OR Apache-2.0" readme = "README.md" @@ -16,5 +16,5 @@ name = "pyoxigraph" doctest = false [dependencies] -oxigraph = { version = "0.3.8", path="../lib", features = ["http_client"] } +oxigraph = { version = "0.3.9", path="../lib", features = ["http_client"] } pyo3 = { version = "0.17", features = ["extension-module", "abi3-py37"] } diff --git a/python/generate_stubs.py b/python/generate_stubs.py index 0be189f5..9bb80ecf 100644 --- a/python/generate_stubs.py +++ b/python/generate_stubs.py @@ -5,48 +5,84 @@ import inspect import logging import re import subprocess -from functools import reduce -from typing import Set, List, Mapping, Any +from typing import Set, List, Mapping, Any, Tuple, Union, Optional, Dict + + +def _path_to_type(*elements: str) -> ast.AST: + base: ast.AST = ast.Name(id=elements[0], ctx=AST_LOAD) + for e in elements[1:]: + base = ast.Attribute(value=base, attr=e, ctx=AST_LOAD) + return base + AST_LOAD = ast.Load() AST_ELLIPSIS = ast.Ellipsis() AST_STORE = ast.Store() -AST_TYPING_ANY = ast.Attribute( - value=ast.Name(id="typing", ctx=AST_LOAD), attr="Any", ctx=AST_LOAD -) +AST_TYPING_ANY = _path_to_type("typing", "Any") GENERICS = { - "iter": ast.Attribute( - value=ast.Name(id="typing", ctx=AST_LOAD), attr="Iterator", ctx=AST_LOAD - ), - "list": ast.Attribute( - value=ast.Name(id="typing", ctx=AST_LOAD), attr="List", ctx=AST_LOAD - ), + "iterable": _path_to_type("typing", "Iterable"), + "iterator": _path_to_type("typing", "Iterator"), + "list": _path_to_type("typing", "List"), } OBJECT_MEMBERS = dict(inspect.getmembers(object)) -ATTRIBUTES_BLACKLIST = { - "__class__", - "__dir__", - "__doc__", - "__init_subclass__", - "__module__", - "__new__", - "__subclasshook__", +BUILTINS: Dict[str, Union[None, Tuple[List[ast.AST], ast.AST]]] = { + "__annotations__": None, + "__bool__": ([], _path_to_type("bool")), + "__bytes__": ([], _path_to_type("bytes")), + "__class__": None, + "__contains__": ([AST_TYPING_ANY], _path_to_type("bool")), + "__del__": None, + "__delattr__": ([_path_to_type("str")], _path_to_type("None")), + "__delitem__": ([AST_TYPING_ANY], AST_TYPING_ANY), + "__dict__": None, + "__dir__": None, + "__doc__": None, + "__eq__": ([AST_TYPING_ANY], _path_to_type("bool")), + "__format__": ([_path_to_type("str")], _path_to_type("str")), + "__ge__": ([AST_TYPING_ANY], _path_to_type("bool")), + "__getattribute__": ([_path_to_type("str")], AST_TYPING_ANY), + "__getitem__": ([AST_TYPING_ANY], AST_TYPING_ANY), + "__gt__": ([AST_TYPING_ANY], _path_to_type("bool")), + "__hash__": ([], _path_to_type("int")), + "__init__": ([], _path_to_type("None")), + "__init_subclass__": None, + "__iter__": ([], AST_TYPING_ANY), + "__le__": ([AST_TYPING_ANY], _path_to_type("bool")), + "__len__": ([], _path_to_type("int")), + "__lt__": ([AST_TYPING_ANY], _path_to_type("bool")), + "__module__": None, + "__ne__": ([AST_TYPING_ANY], _path_to_type("bool")), + "__new__": None, + "__next__": ([], AST_TYPING_ANY), + "__reduce__": None, + "__reduce_ex__": None, + "__repr__": ([], _path_to_type("str")), + "__setattr__": ([_path_to_type("str"), AST_TYPING_ANY], _path_to_type("None")), + "__setitem__": ([AST_TYPING_ANY, AST_TYPING_ANY], AST_TYPING_ANY), + "__sizeof__": None, + "__str__": ([], _path_to_type("str")), + "__subclasshook__": None, } -def module_stubs(module) -> ast.Module: +def module_stubs(module: Any) -> ast.Module: types_to_import = {"typing"} classes = [] functions = [] for (member_name, member_value) in inspect.getmembers(module): + element_path = [module.__name__, member_name] if member_name.startswith("__"): pass elif inspect.isclass(member_value): - classes.append(class_stubs(member_name, member_value, types_to_import)) + classes.append( + class_stubs(member_name, member_value, element_path, types_to_import) + ) elif inspect.isbuiltin(member_value): - functions.append(function_stub(member_name, member_value, types_to_import)) + functions.append( + function_stub(member_name, member_value, element_path, types_to_import) + ) else: logging.warning(f"Unsupported root construction {member_name}") return ast.Module( @@ -57,36 +93,48 @@ def module_stubs(module) -> ast.Module: ) -def class_stubs(cls_name: str, cls_def, types_to_import: Set[str]) -> ast.ClassDef: +def class_stubs( + cls_name: str, cls_def: Any, element_path: List[str], types_to_import: Set[str] +) -> ast.ClassDef: attributes: List[ast.AST] = [] methods: List[ast.AST] = [] magic_methods: List[ast.AST] = [] for (member_name, member_value) in inspect.getmembers(cls_def): + current_element_path = element_path + [member_name] if member_name == "__init__": try: inspect.signature(cls_def) # we check it actually exists methods = [ - function_stub(member_name, cls_def, types_to_import) + function_stub( + member_name, cls_def, current_element_path, types_to_import + ) ] + methods except ValueError as e: if "no signature found" not in str(e): raise ValueError( f"Error while parsing signature of {cls_name}.__init__: {e}" ) - elif member_name in ATTRIBUTES_BLACKLIST or member_value == OBJECT_MEMBERS.get( - member_name + elif ( + member_value == OBJECT_MEMBERS.get(member_name) + or BUILTINS.get(member_name, ()) is None ): pass elif inspect.isdatadescriptor(member_value): attributes.extend( - data_descriptor_stub(member_name, member_value, types_to_import) + data_descriptor_stub( + member_name, member_value, current_element_path, types_to_import + ) ) elif inspect.isroutine(member_value): (magic_methods if member_name.startswith("__") else methods).append( - function_stub(member_name, member_value, types_to_import) + function_stub( + member_name, member_value, current_element_path, types_to_import + ) ) else: - logging.warning(f"Unsupported member {member_name} of class {cls_name}") + logging.warning( + f"Unsupported member {member_name} of class {'.'.join(element_path)}" + ) doc = inspect.getdoc(cls_def) return ast.ClassDef( @@ -100,28 +148,29 @@ def class_stubs(cls_name: str, cls_def, types_to_import: Set[str]) -> ast.ClassD + magic_methods ) or [AST_ELLIPSIS], - decorator_list=[ - ast.Attribute( - value=ast.Name(id="typing", ctx=AST_LOAD), attr="final", ctx=AST_LOAD - ) - ], + decorator_list=[_path_to_type("typing", "final")], ) def data_descriptor_stub( - data_desc_name: str, data_desc_def, types_to_import: Set[str] -) -> tuple: + data_desc_name: str, + data_desc_def: Any, + element_path: List[str], + types_to_import: Set[str], +) -> Union[Tuple[ast.AnnAssign, ast.Expr], Tuple[ast.AnnAssign]]: annotation = None doc_comment = None doc = inspect.getdoc(data_desc_def) if doc is not None: - annotation = returns_stub(doc, types_to_import) - m = re.findall(r":return: *(.*) *\n", doc) + annotation = returns_stub(data_desc_name, doc, element_path, types_to_import) + m = re.findall(r"^ *:return: *(.*) *$", doc, re.MULTILINE) if len(m) == 1: doc_comment = m[0] elif len(m) > 1: - raise ValueError("Multiple return annotations found with :return:") + raise ValueError( + f"Multiple return annotations found with :return: in {'.'.join(element_path)} documentation" + ) assign = ast.AnnAssign( target=ast.Name(id=data_desc_name, ctx=AST_STORE), @@ -131,23 +180,33 @@ def data_descriptor_stub( return (assign, build_doc_comment(doc_comment)) if doc_comment else (assign,) -def function_stub(fn_name: str, fn_def, types_to_import: Set[str]) -> ast.FunctionDef: - body = [] +def function_stub( + fn_name: str, fn_def: Any, element_path: List[str], types_to_import: Set[str] +) -> ast.FunctionDef: + body: List[ast.AST] = [] doc = inspect.getdoc(fn_def) - if doc is not None and not fn_name.startswith("__"): + if doc is not None: body.append(build_doc_comment(doc)) return ast.FunctionDef( fn_name, - arguments_stub(fn_name, fn_def, doc or "", types_to_import), + arguments_stub(fn_name, fn_def, doc or "", element_path, types_to_import), body or [AST_ELLIPSIS], decorator_list=[], - returns=returns_stub(doc, types_to_import) if doc else None, + returns=returns_stub(fn_name, doc, element_path, types_to_import) + if doc + else None, lineno=0, ) -def arguments_stub(callable_name, callable_def, doc: str, types_to_import: Set[str]): +def arguments_stub( + callable_name: str, + callable_def: Any, + doc: str, + element_path: List[str], + types_to_import: Set[str], +) -> ast.arguments: real_parameters: Mapping[str, inspect.Parameter] = inspect.signature( callable_def ).parameters @@ -159,16 +218,29 @@ def arguments_stub(callable_name, callable_def, doc: str, types_to_import: Set[s parsed_param_types = {} optional_params = set() - for match in re.findall(r"\n *:type *([a-z_]+): ([^\n]*) *\n", doc): + + # Types for magic functions types + builtin = BUILTINS.get(callable_name) + if isinstance(builtin, tuple): + param_names = list(real_parameters.keys()) + if param_names and param_names[0] == "self": + del param_names[0] + for name, t in zip(param_names, builtin[0]): + parsed_param_types[name] = t + + # Types from comment + for match in re.findall(r"^ *:type *([a-z_]+): ([^\n]*) *$", doc, re.MULTILINE): if match[0] not in real_parameters: raise ValueError( - f"The parameter {match[0]} is defined in the documentation but not in the function signature" + f"The parameter {match[0]} of {'.'.join(element_path)} is defined in the documentation but not in the function signature" ) type = match[1] if type.endswith(", optional"): optional_params.add(match[0]) type = type[:-10] - parsed_param_types[match[0]] = convert_type_from_doc(type, types_to_import) + parsed_param_types[match[0]] = convert_type_from_doc( + type, element_path, types_to_import + ) # we parse the parameters posonlyargs = [] @@ -179,13 +251,9 @@ def arguments_stub(callable_name, callable_def, doc: str, types_to_import: Set[s kwarg = None defaults = [] for param in real_parameters.values(): - if ( - param.name != "self" - and param.name not in parsed_param_types - and (callable_name == "__init__" or not callable_name.startswith("__")) - ): + if param.name != "self" and param.name not in parsed_param_types: raise ValueError( - f"The parameter {param.name} of {callable_name} has no type definition in the function documentation" + f"The parameter {param.name} of {'.'.join(element_path)} has no type definition in the function documentation" ) param_ast = ast.arg( arg=param.name, annotation=parsed_param_types.get(param.name) @@ -196,11 +264,11 @@ def arguments_stub(callable_name, callable_def, doc: str, types_to_import: Set[s default_ast = ast.Constant(param.default) if param.name not in optional_params: raise ValueError( - f"Parameter {param.name} is optional according to the type but not flagged as such in the doc" + f"Parameter {param.name} of {'.'.join(element_path)} is optional according to the type but not flagged as such in the doc" ) elif param.name in optional_params: raise ValueError( - f"Parameter {param.name} is optional according to the documentation but has no default value" + f"Parameter {param.name} of {'.'.join(element_path)} is optional according to the documentation but has no default value" ) if param.kind == param.POSITIONAL_ONLY: @@ -228,22 +296,35 @@ def arguments_stub(callable_name, callable_def, doc: str, types_to_import: Set[s ) -def returns_stub(doc: str, types_to_import: Set[str]): - m = re.findall(r"\n *:rtype: *([^\n]*) *\n", doc) +def returns_stub( + callable_name: str, doc: str, element_path: List[str], types_to_import: Set[str] +) -> Optional[ast.AST]: + m = re.findall(r"^ *:rtype: *([^\n]*) *$", doc, re.MULTILINE) if len(m) == 0: - return None + builtin = BUILTINS.get(callable_name) + if isinstance(builtin, tuple) and builtin[1] is not None: + return builtin[1] + raise ValueError( + f"The return type of {'.'.join(element_path)} has no type definition using :rtype: in the function documentation" + ) elif len(m) == 1: - return convert_type_from_doc(m[0], types_to_import) + return convert_type_from_doc(m[0], element_path, types_to_import) else: - raise ValueError("Multiple return type annotations found with :rtype:") + raise ValueError( + f"Multiple return type annotations found with :rtype: for {'.'.join(element_path)}" + ) -def convert_type_from_doc(type_str: str, types_to_import: Set[str]): +def convert_type_from_doc( + type_str: str, element_path: List[str], types_to_import: Set[str] +) -> ast.AST: type_str = type_str.strip() - return parse_type_to_ast(type_str, types_to_import) + return parse_type_to_ast(type_str, element_path, types_to_import) -def parse_type_to_ast(type_str: str, types_to_import: Set[str]): +def parse_type_to_ast( + type_str: str, element_path: List[str], types_to_import: Set[str] +) -> ast.AST: # let's tokenize tokens = [] current_token = "" @@ -272,32 +353,30 @@ def parse_type_to_ast(type_str: str, types_to_import: Set[str]): stack[-1].append(token) # then it's easy - def parse_sequence(sequence): + def parse_sequence(sequence: List[Any]) -> ast.AST: # we split based on "or" - or_groups = [[]] + or_groups: List[List[str]] = [[]] for e in sequence: if e == "or": or_groups.append([]) else: or_groups[-1].append(e) if any(not g for g in or_groups): - raise ValueError(f'Not able to parse type "{type_str}"') + raise ValueError( + f"Not able to parse type '{type_str}' used by {'.'.join(element_path)}" + ) - new_elements = [] + new_elements: List[ast.AST] = [] for group in or_groups: if len(group) == 1 and isinstance(group[0], str): parts = group[0].split(".") if any(not p for p in parts): - raise ValueError(f'Not able to parse type "{type_str}"') + raise ValueError( + f"Not able to parse type '{type_str}' used by {'.'.join(element_path)}" + ) if len(parts) > 1: types_to_import.add(parts[0]) - new_elements.append( - reduce( - lambda acc, n: ast.Attribute(value=acc, attr=n, ctx=AST_LOAD), - parts[1:], - ast.Name(id=parts[0], ctx=AST_LOAD), - ) - ) + new_elements.append(_path_to_type(*parts)) elif ( len(group) == 2 and isinstance(group[0], str) @@ -305,7 +384,7 @@ def parse_type_to_ast(type_str: str, types_to_import: Set[str]): ): if group[0] not in GENERICS: raise ValueError( - f'Constructor {group[0]} is not supported in type "{type_str}"' + f"Constructor {group[0]} is not supported in type '{type_str}' used by {'.'.join(element_path)}" ) new_elements.append( ast.Subscript( @@ -315,14 +394,12 @@ def parse_type_to_ast(type_str: str, types_to_import: Set[str]): ) ) else: - raise ValueError(f'Not able to parse type "{type_str}"') + raise ValueError( + f"Not able to parse type '{type_str}' used by {'.'.join(element_path)}" + ) return ( ast.Subscript( - value=ast.Attribute( - value=ast.Name(id="typing", ctx=AST_LOAD), - attr="Union", - ctx=AST_LOAD, - ), + value=_path_to_type("typing", "Union"), slice=ast.Tuple(elts=new_elements, ctx=AST_LOAD), ctx=AST_LOAD, ) @@ -333,7 +410,7 @@ def parse_type_to_ast(type_str: str, types_to_import: Set[str]): return parse_sequence(stack[0]) -def build_doc_comment(doc: str): +def build_doc_comment(doc: str) -> ast.Expr: lines = [l.strip() for l in doc.split("\n")] clean_lines = [] for l in lines: @@ -371,6 +448,9 @@ if __name__ == "__main__": ) args = parser.parse_args() stub_content = ast.unparse(module_stubs(importlib.import_module(args.module_name))) + stub_content = stub_content.replace( + ", /", "" + ) # TODO: remove when targeting Python 3.8+ if args.black: stub_content = format_with_black(stub_content) args.out.write(stub_content) diff --git a/python/src/io.rs b/python/src/io.rs index 3d066bdd..568330b8 100644 --- a/python/src/io.rs +++ b/python/src/io.rs @@ -38,7 +38,7 @@ pub fn add_to_module(module: &PyModule) -> PyResult<()> { /// :param base_iri: the base IRI used to resolve the relative IRIs in the file or :py:const:`None` if relative IRI resolution should not be done. /// :type base_iri: str or None, optional /// :return: an iterator of RDF triples or quads depending on the format. -/// :rtype: iter(Triple) or iter(Quad) +/// :rtype: iterator(Triple) or iterator(Quad) /// :raises ValueError: if the MIME type is not supported. /// :raises SyntaxError: if the provided data is invalid. /// @@ -46,7 +46,7 @@ pub fn add_to_module(module: &PyModule) -> PyResult<()> { /// >>> list(parse(input, "text/turtle", base_iri="http://example.com/")) /// [ predicate= object=>>] #[pyfunction] -#[pyo3(text_signature = "(input, /, mime_type, *, base_iri = None)")] +#[pyo3(text_signature = "(input, mime_type, *, base_iri = None)")] pub fn parse( input: PyObject, mime_type: &str, @@ -104,11 +104,12 @@ pub fn parse( /// and ``application/xml`` for `RDF/XML `_. /// /// :param input: the RDF triples and quads to serialize. -/// :type input: iter(Triple) or iter(Quad) +/// :type input: iterable(Triple) or iterable(Quad) /// :param output: The binary I/O object or file path to write to. For example, it could be a file path as a string or a file writer opened in binary mode with ``open('my_file.ttl', 'wb')``. /// :type output: io.RawIOBase or io.BufferedIOBase or str /// :param mime_type: the MIME type of the RDF serialization. /// :type mime_type: str +/// :rtype: None /// :raises ValueError: if the MIME type is not supported. /// :raises TypeError: if a triple is given during a quad format serialization or reverse. /// @@ -117,7 +118,7 @@ pub fn parse( /// >>> output.getvalue() /// b' "1" .\n' #[pyfunction] -#[pyo3(text_signature = "(input, output, /, mime_type)")] +#[pyo3(text_signature = "(input, output, mime_type)")] pub fn serialize(input: &PyAny, output: PyObject, mime_type: &str, py: Python<'_>) -> PyResult<()> { let output = if let Ok(path) = output.extract::<&str>(py) { PyWritable::from_file(path, py) diff --git a/python/src/model.rs b/python/src/model.rs index a57c6286..d050516d 100644 --- a/python/src/model.rs +++ b/python/src/model.rs @@ -371,6 +371,8 @@ impl PyDefaultGraph { Self {} } + /// :return: the empty string. + /// :rtype: str #[getter] fn value(&self) -> &str { "" diff --git a/python/src/store.rs b/python/src/store.rs index 062fd1e4..a9fabf15 100644 --- a/python/src/store.rs +++ b/python/src/store.rs @@ -59,6 +59,7 @@ impl PyStore { /// /// :param quad: the quad to add. /// :type quad: Quad + /// :rtype: None /// :raises IOError: if an I/O error happens during the quad insertion. /// /// >>> store = Store() @@ -77,6 +78,7 @@ impl PyStore { /// /// :param quad: the quad to remove. /// :type quad: Quad + /// :rtype: None /// :raises IOError: if an I/O error happens during the quad removal. /// /// >>> store = Store() @@ -104,7 +106,7 @@ impl PyStore { /// :param graph_name: the quad graph name. To match only the default graph, use :py:class:`DefaultGraph`. To match everything use :py:const:`None`. /// :type graph_name: NamedNode or BlankNode or DefaultGraph or None, optional /// :return: an iterator of the quads matching the pattern. - /// :rtype: iter(Quad) + /// :rtype: iterator(Quad) /// :raises IOError: if an I/O error happens during the quads lookup. /// /// >>> store = Store() @@ -208,6 +210,7 @@ impl PyStore { /// :type update: str /// :param base_iri: the base IRI used to resolve the relative IRIs in the SPARQL update or :py:const:`None` if relative IRI resolution should not be done. /// :type base_iri: str or None, optional + /// :rtype: None /// :raises SyntaxError: if the provided update is invalid. /// :raises IOError: if an I/O error happens while reading the store. /// @@ -270,6 +273,7 @@ impl PyStore { /// :type base_iri: str or None, optional /// :param to_graph: if it is a file composed of triples, the graph in which the triples should be stored. By default, the default graph is used. /// :type to_graph: NamedNode or BlankNode or DefaultGraph or None, optional + /// :rtype: None /// :raises ValueError: if the MIME type is not supported or the `to_graph` parameter is given with a quad file. /// :raises SyntaxError: if the provided data is invalid. /// :raises IOError: if an I/O error happens during a quad insertion. @@ -278,7 +282,7 @@ impl PyStore { /// >>> store.load(io.BytesIO(b'

"1" .'), "text/turtle", base_iri="http://example.com/", to_graph=NamedNode("http://example.com/g")) /// >>> list(store) /// [ predicate= object=> graph_name=>] - #[pyo3(text_signature = "($self, input, /, mime_type, *, base_iri = None, to_graph = None)")] + #[pyo3(text_signature = "($self, input, mime_type, *, base_iri = None, to_graph = None)")] #[args(input, mime_type, "*", base_iri = "None", to_graph = "None")] fn load( &self, @@ -354,6 +358,7 @@ impl PyStore { /// :type base_iri: str or None, optional /// :param to_graph: if it is a file composed of triples, the graph in which the triples should be stored. By default, the default graph is used. /// :type to_graph: NamedNode or BlankNode or DefaultGraph or None, optional + /// :rtype: None /// :raises ValueError: if the MIME type is not supported or the `to_graph` parameter is given with a quad file. /// :raises SyntaxError: if the provided data is invalid. /// :raises IOError: if an I/O error happens during a quad insertion. @@ -362,7 +367,7 @@ impl PyStore { /// >>> store.bulk_load(io.BytesIO(b'

"1" .'), "text/turtle", base_iri="http://example.com/", to_graph=NamedNode("http://example.com/g")) /// >>> list(store) /// [ predicate= object=> graph_name=>] - #[pyo3(text_signature = "($self, input, /, mime_type, *, base_iri = None, to_graph = None)")] + #[pyo3(text_signature = "($self, input, mime_type, *, base_iri = None, to_graph = None)")] #[args(input, mime_type, "*", base_iri = "None", to_graph = "None")] fn bulk_load( &self, @@ -433,6 +438,7 @@ impl PyStore { /// :type mime_type: str /// :param from_graph: if a triple based format is requested, the store graph from which dump the triples. By default, the default graph is used. /// :type from_graph: NamedNode or BlankNode or DefaultGraph or None, optional + /// :rtype: None /// :raises ValueError: if the MIME type is not supported or the `from_graph` parameter is given with a quad syntax. /// :raises IOError: if an I/O error happens during a quad lookup /// @@ -442,7 +448,7 @@ impl PyStore { /// >>> store.dump(output, "text/turtle", from_graph=NamedNode("http://example.com/g")) /// >>> output.getvalue() /// b' "1" .\n' - #[pyo3(text_signature = "($self, output, /, mime_type, *, from_graph = None)")] + #[pyo3(text_signature = "($self, output, mime_type, *, from_graph = None)")] #[args(output, mime_type, "*", from_graph = "None")] fn dump( &self, @@ -492,7 +498,7 @@ impl PyStore { /// Returns an iterator over all the store named graphs. /// /// :return: an iterator of the store graph names. - /// :rtype: iter(NamedNode or BlankNode) + /// :rtype: iterator(NamedNode or BlankNode) /// :raises IOError: if an I/O error happens during the named graphs lookup. /// /// >>> store = Store() @@ -510,6 +516,7 @@ impl PyStore { /// /// :param graph_name: the name of the name graph to add. /// :type graph_name: NamedNode or BlankNode + /// :rtype: None /// :raises IOError: if an I/O error happens during the named graph insertion. /// /// >>> store = Store() @@ -537,6 +544,7 @@ impl PyStore { /// /// :param graph_name: the name of the name graph to clear. /// :type graph_name: NamedNode or BlankNode or DefaultGraph + /// :rtype: None /// :raises IOError: if an I/O error happens during the operation. /// /// >>> store = Store() @@ -562,6 +570,7 @@ impl PyStore { /// /// :param graph_name: the name of the name graph to remove. /// :type graph_name: NamedNode or BlankNode or DefaultGraph + /// :rtype: None /// :raises IOError: if an I/O error happens during the named graph removal. /// /// >>> store = Store() @@ -588,6 +597,7 @@ impl PyStore { /// Clears the store by removing all its contents. /// + /// :rtype: None /// :raises IOError: if an I/O error happens during the operation. /// /// >>> store = Store() @@ -606,6 +616,7 @@ impl PyStore { /// /// Flushes are automatically done using background threads but might lag a little bit. /// + /// :rtype: None /// :raises IOError: if an I/O error happens during the flush. #[pyo3(text_signature = "($self)")] fn flush(&self, py: Python<'_>) -> PyResult<()> { @@ -616,6 +627,7 @@ impl PyStore { /// /// Useful to call after a batch upload or another similar operation. /// + /// :rtype: None /// :raises IOError: if an I/O error happens during the optimization. #[pyo3(text_signature = "($self)")] fn optimize(&self, py: Python<'_>) -> PyResult<()> { @@ -641,6 +653,7 @@ impl PyStore { /// /// :param target_directory: the directory name to save the database to. /// :type target_directory: str + /// :rtype: None /// :raises IOError: if an I/O error happens during the backup. #[pyo3(text_signature = "($self, target_directory)")] fn backup(&self, target_directory: &str, py: Python<'_>) -> PyResult<()> { diff --git a/python/tests/test_io.py b/python/tests/test_io.py index 62894b31..93204942 100644 --- a/python/tests/test_io.py +++ b/python/tests/test_io.py @@ -11,7 +11,7 @@ EXAMPLE_TRIPLE = Triple( class TestParse(unittest.TestCase): - def test_parse_file(self): + def test_parse_file(self) -> None: with NamedTemporaryFile() as fp: fp.write(b'

"1" .') fp.flush() @@ -20,11 +20,11 @@ class TestParse(unittest.TestCase): [EXAMPLE_TRIPLE], ) - def test_parse_not_existing_file(self): + def test_parse_not_existing_file(self) -> None: with self.assertRaises(IOError) as _: parse("/tmp/not-existing-oxigraph-file.ttl", "text/turtle") - def test_parse_str_io(self): + def test_parse_str_io(self) -> None: self.assertEqual( list( parse( @@ -36,7 +36,7 @@ class TestParse(unittest.TestCase): [EXAMPLE_TRIPLE], ) - def test_parse_bytes_io(self): + def test_parse_bytes_io(self) -> None: self.assertEqual( list( parse( @@ -48,7 +48,7 @@ class TestParse(unittest.TestCase): [EXAMPLE_TRIPLE], ) - def test_parse_io_error(self): + def test_parse_io_error(self) -> None: class BadIO(RawIOBase): pass @@ -57,7 +57,7 @@ class TestParse(unittest.TestCase): class TestSerialize(unittest.TestCase): - def test_serialize_to_bytes_io(self): + def test_serialize_to_bytes_io(self) -> None: output = BytesIO() serialize([EXAMPLE_TRIPLE], output, "text/turtle") self.assertEqual( @@ -65,7 +65,7 @@ class TestSerialize(unittest.TestCase): b' "1" .\n', ) - def test_serialize_to_file(self): + def test_serialize_to_file(self) -> None: with NamedTemporaryFile() as fp: serialize([EXAMPLE_TRIPLE], fp.name, "text/turtle") self.assertEqual( diff --git a/python/tests/test_model.py b/python/tests/test_model.py index d663d7a1..e84322a4 100644 --- a/python/tests/test_model.py +++ b/python/tests/test_model.py @@ -7,26 +7,26 @@ RDF_LANG_STRING = NamedNode("http://www.w3.org/1999/02/22-rdf-syntax-ns#langStri class TestNamedNode(unittest.TestCase): - def test_constructor(self): + def test_constructor(self) -> None: self.assertEqual(NamedNode("http://foo").value, "http://foo") - def test_string(self): + def test_string(self) -> None: self.assertEqual(str(NamedNode("http://foo")), "") - def test_equal(self): + def test_equal(self) -> None: self.assertEqual(NamedNode("http://foo"), NamedNode("http://foo")) self.assertNotEqual(NamedNode("http://foo"), NamedNode("http://bar")) class TestBlankNode(unittest.TestCase): - def test_constructor(self): + def test_constructor(self) -> None: self.assertEqual(BlankNode("foo").value, "foo") self.assertNotEqual(BlankNode(), BlankNode()) - def test_string(self): + def test_string(self) -> None: self.assertEqual(str(BlankNode("foo")), "_:foo") - def test_equal(self): + def test_equal(self) -> None: self.assertEqual(BlankNode("foo"), BlankNode("foo")) self.assertNotEqual(BlankNode("foo"), BlankNode("bar")) self.assertNotEqual(BlankNode("foo"), NamedNode("http://foo")) @@ -34,7 +34,7 @@ class TestBlankNode(unittest.TestCase): class TestLiteral(unittest.TestCase): - def test_constructor(self): + def test_constructor(self) -> None: self.assertEqual(Literal("foo").value, "foo") self.assertEqual(Literal("foo").datatype, XSD_STRING) @@ -45,7 +45,7 @@ class TestLiteral(unittest.TestCase): self.assertEqual(Literal("foo", datatype=XSD_INTEGER).value, "foo") self.assertEqual(Literal("foo", datatype=XSD_INTEGER).datatype, XSD_INTEGER) - def test_string(self): + def test_string(self) -> None: self.assertEqual(str(Literal("foo")), '"foo"') self.assertEqual(str(Literal("foo", language="en")), '"foo"@en') self.assertEqual( @@ -53,7 +53,7 @@ class TestLiteral(unittest.TestCase): '"foo"^^', ) - def test_equals(self): + def test_equals(self) -> None: self.assertEqual(Literal("foo", datatype=XSD_STRING), Literal("foo")) self.assertEqual( Literal("foo", language="en", datatype=RDF_LANG_STRING), @@ -66,7 +66,7 @@ class TestLiteral(unittest.TestCase): class TestTriple(unittest.TestCase): - def test_constructor(self): + def test_constructor(self) -> None: t = Triple( NamedNode("http://example.com/s"), NamedNode("http://example.com/p"), @@ -76,7 +76,7 @@ class TestTriple(unittest.TestCase): self.assertEqual(t.predicate, NamedNode("http://example.com/p")) self.assertEqual(t.object, NamedNode("http://example.com/o")) - def test_rdf_star_constructor(self): + def test_rdf_star_constructor(self) -> None: t = Triple( Triple( NamedNode("http://example.com/ss"), @@ -108,7 +108,7 @@ class TestTriple(unittest.TestCase): ), ) - def test_mapping(self): + def test_mapping(self) -> None: t = Triple( NamedNode("http://example.com/s"), NamedNode("http://example.com/p"), @@ -118,7 +118,7 @@ class TestTriple(unittest.TestCase): self.assertEqual(t[1], NamedNode("http://example.com/p")) self.assertEqual(t[2], NamedNode("http://example.com/o")) - def test_destruct(self): + def test_destruct(self) -> None: (s, p, o) = Triple( NamedNode("http://example.com/s"), NamedNode("http://example.com/p"), @@ -128,7 +128,7 @@ class TestTriple(unittest.TestCase): self.assertEqual(p, NamedNode("http://example.com/p")) self.assertEqual(o, NamedNode("http://example.com/o")) - def test_string(self): + def test_string(self) -> None: self.assertEqual( str( Triple( @@ -142,7 +142,7 @@ class TestTriple(unittest.TestCase): class TestQuad(unittest.TestCase): - def test_constructor(self): + def test_constructor(self) -> None: t = Quad( NamedNode("http://example.com/s"), NamedNode("http://example.com/p"), @@ -175,7 +175,7 @@ class TestQuad(unittest.TestCase): ), ) - def test_mapping(self): + def test_mapping(self) -> None: t = Quad( NamedNode("http://example.com/s"), NamedNode("http://example.com/p"), @@ -187,7 +187,7 @@ class TestQuad(unittest.TestCase): self.assertEqual(t[2], NamedNode("http://example.com/o")) self.assertEqual(t[3], NamedNode("http://example.com/g")) - def test_destruct(self): + def test_destruct(self) -> None: (s, p, o, g) = Quad( NamedNode("http://example.com/s"), NamedNode("http://example.com/p"), @@ -199,7 +199,7 @@ class TestQuad(unittest.TestCase): self.assertEqual(o, NamedNode("http://example.com/o")) self.assertEqual(g, NamedNode("http://example.com/g")) - def test_string(self): + def test_string(self) -> None: self.assertEqual( str( Triple( @@ -213,13 +213,13 @@ class TestQuad(unittest.TestCase): class TestVariable(unittest.TestCase): - def test_constructor(self): + def test_constructor(self) -> None: self.assertEqual(Variable("foo").value, "foo") - def test_string(self): + def test_string(self) -> None: self.assertEqual(str(Variable("foo")), "?foo") - def test_equal(self): + def test_equal(self) -> None: self.assertEqual(Variable("foo"), Variable("foo")) self.assertNotEqual(Variable("foo"), Variable("bar")) diff --git a/python/tests/test_store.py b/python/tests/test_store.py index 88152f7d..a8ac1e0b 100644 --- a/python/tests/test_store.py +++ b/python/tests/test_store.py @@ -1,6 +1,7 @@ import os import unittest from io import BytesIO, RawIOBase +from typing import Any from pyoxigraph import * from tempfile import NamedTemporaryFile @@ -13,7 +14,7 @@ graph = NamedNode("http://graph") class TestStore(unittest.TestCase): - def test_add(self): + def test_add(self) -> None: store = Store() store.add(Quad(foo, bar, baz)) store.add(Quad(foo, bar, baz, DefaultGraph())) @@ -22,7 +23,7 @@ class TestStore(unittest.TestCase): store.add(Quad(foo, bar, triple)) self.assertEqual(len(store), 4) - def test_remove(self): + def test_remove(self) -> None: store = Store() store.add(Quad(foo, bar, baz)) store.add(Quad(foo, bar, baz, DefaultGraph())) @@ -30,13 +31,13 @@ class TestStore(unittest.TestCase): store.remove(Quad(foo, bar, baz)) self.assertEqual(len(store), 1) - def test_len(self): + def test_len(self) -> None: store = Store() store.add(Quad(foo, bar, baz)) store.add(Quad(foo, bar, baz, graph)) self.assertEqual(len(store), 2) - def test_in(self): + def test_in(self) -> None: store = Store() store.add(Quad(foo, bar, baz)) store.add(Quad(foo, bar, baz, DefaultGraph())) @@ -46,7 +47,7 @@ class TestStore(unittest.TestCase): self.assertIn(Quad(foo, bar, baz, graph), store) self.assertNotIn(Quad(foo, bar, baz, foo), store) - def test_iter(self): + def test_iter(self) -> None: store = Store() store.add(Quad(foo, bar, baz, DefaultGraph())) store.add(Quad(foo, bar, baz, graph)) @@ -55,7 +56,7 @@ class TestStore(unittest.TestCase): {Quad(foo, bar, baz, DefaultGraph()), Quad(foo, bar, baz, graph)}, ) - def test_quads_for_pattern(self): + def test_quads_for_pattern(self) -> None: store = Store() store.add(Quad(foo, bar, baz, DefaultGraph())) store.add(Quad(foo, bar, baz, graph)) @@ -76,26 +77,26 @@ class TestStore(unittest.TestCase): {Quad(foo, bar, baz, DefaultGraph())}, ) - def test_ask_query(self): + def test_ask_query(self) -> None: store = Store() store.add(Quad(foo, foo, foo)) self.assertTrue(store.query("ASK { ?s ?s ?s }")) self.assertFalse(store.query("ASK { FILTER(false) }")) - def test_construct_query(self): + def test_construct_query(self) -> None: store = Store() store.add(Quad(foo, bar, baz)) - results = store.query("CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }") + results: Any = store.query("CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }") self.assertIsInstance(results, QueryTriples) self.assertEqual( set(results), {Triple(foo, bar, baz)}, ) - def test_select_query(self): + def test_select_query(self) -> None: store = Store() store.add(Quad(foo, bar, baz)) - solutions = store.query("SELECT ?s ?o WHERE { ?s ?p ?o }") + solutions: Any = store.query("SELECT ?s ?o WHERE { ?s ?p ?o }") self.assertIsInstance(solutions, QuerySolutions) self.assertEqual(solutions.variables, [Variable("s"), Variable("o")]) solution = next(solutions) @@ -110,10 +111,11 @@ class TestStore(unittest.TestCase): self.assertEqual(s, foo) self.assertEqual(o, baz) - def test_select_query_union_default_graph(self): + def test_select_query_union_default_graph(self) -> None: store = Store() store.add(Quad(foo, bar, baz, graph)) - self.assertEqual(len(list(store.query("SELECT ?s WHERE { ?s ?p ?o }"))), 0) + results: Any = store.query("SELECT ?s WHERE { ?s ?p ?o }") + self.assertEqual(len(list(results)), 0) results = store.query( "SELECT ?s WHERE { ?s ?p ?o }", use_default_graph_as_union=True ) @@ -125,13 +127,14 @@ class TestStore(unittest.TestCase): ) self.assertEqual(len(list(results)), 1) - def test_select_query_with_default_graph(self): + def test_select_query_with_default_graph(self) -> None: store = Store() graph_bnode = BlankNode("g") store.add(Quad(foo, bar, baz, graph)) store.add(Quad(foo, bar, foo)) store.add(Quad(foo, bar, bar, graph_bnode)) - self.assertEqual(len(list(store.query("SELECT ?s WHERE { ?s ?p ?o }"))), 1) + results: Any = store.query("SELECT ?s WHERE { ?s ?p ?o }") + self.assertEqual(len(list(results)), 1) results = store.query("SELECT ?s WHERE { ?s ?p ?o }", default_graph=graph) self.assertEqual(len(list(results)), 1) results = store.query( @@ -140,52 +143,52 @@ class TestStore(unittest.TestCase): ) self.assertEqual(len(list(results)), 3) - def test_select_query_with_named_graph(self): + def test_select_query_with_named_graph(self) -> None: store = Store() graph_bnode = BlankNode("g") store.add(Quad(foo, bar, baz, graph)) store.add(Quad(foo, bar, foo)) store.add(Quad(foo, bar, bar, graph_bnode)) store.add(Quad(foo, bar, bar, foo)) - results = store.query( + results: Any = store.query( "SELECT ?s WHERE { GRAPH ?g { ?s ?p ?o } }", named_graphs=[graph, graph_bnode], ) self.assertEqual(len(list(results)), 2) - def test_update_insert_data(self): + def test_update_insert_data(self) -> None: store = Store() store.update("INSERT DATA { }") self.assertEqual(len(store), 1) - def test_update_delete_data(self): + def test_update_delete_data(self) -> None: store = Store() store.add(Quad(foo, foo, foo)) store.update("DELETE DATA { }") self.assertEqual(len(store), 0) - def test_update_delete_where(self): + def test_update_delete_where(self) -> None: store = Store() store.add(Quad(foo, foo, foo)) store.update("DELETE WHERE { ?v ?v ?v }") self.assertEqual(len(store), 0) - def test_update_load(self): + def test_update_load(self) -> None: store = Store() store.update("LOAD ") self.assertGreater(len(store), 100) - def test_update_star(self): + def test_update_star(self) -> None: store = Store() store.update( "PREFIX : INSERT DATA { :alice :claims << :bob :age 23 >> }" ) - results = store.query( + results: Any = store.query( "PREFIX : SELECT ?p ?a WHERE { ?p :claims << :bob :age ?a >> }" ) self.assertEqual(len(list(results)), 1) - def test_load_ntriples_to_default_graph(self): + def test_load_ntriples_to_default_graph(self) -> None: store = Store() store.load( BytesIO(b" ."), @@ -193,7 +196,7 @@ class TestStore(unittest.TestCase): ) self.assertEqual(set(store), {Quad(foo, bar, baz, DefaultGraph())}) - def test_load_ntriples_to_named_graph(self): + def test_load_ntriples_to_named_graph(self) -> None: store = Store() store.load( BytesIO(b" ."), @@ -202,7 +205,7 @@ class TestStore(unittest.TestCase): ) self.assertEqual(set(store), {Quad(foo, bar, baz, graph)}) - def test_load_turtle_with_base_iri(self): + def test_load_turtle_with_base_iri(self) -> None: store = Store() store.load( BytesIO(b" <> ."), @@ -211,7 +214,7 @@ class TestStore(unittest.TestCase): ) self.assertEqual(set(store), {Quad(foo, bar, baz, DefaultGraph())}) - def test_load_nquads(self): + def test_load_nquads(self) -> None: store = Store() store.load( BytesIO(b" ."), @@ -219,7 +222,7 @@ class TestStore(unittest.TestCase): ) self.assertEqual(set(store), {Quad(foo, bar, baz, graph)}) - def test_load_trig_with_base_iri(self): + def test_load_trig_with_base_iri(self) -> None: store = Store() store.load( BytesIO(b" { <> . }"), @@ -228,7 +231,7 @@ class TestStore(unittest.TestCase): ) self.assertEqual(set(store), {Quad(foo, bar, baz, graph)}) - def test_load_file(self): + def test_load_file(self) -> None: with NamedTemporaryFile(delete=False) as fp: file_name = fp.name fp.write(b" .") @@ -237,14 +240,14 @@ class TestStore(unittest.TestCase): os.remove(file_name) self.assertEqual(set(store), {Quad(foo, bar, baz, graph)}) - def test_load_with_io_error(self): + def test_load_with_io_error(self) -> None: class BadIO(RawIOBase): pass with self.assertRaises(NotImplementedError) as _: Store().load(BadIO(), mime_type="application/n-triples") - def test_dump_ntriples(self): + def test_dump_ntriples(self) -> None: store = Store() store.add(Quad(foo, bar, baz, graph)) output = BytesIO() @@ -254,7 +257,7 @@ class TestStore(unittest.TestCase): b" .\n", ) - def test_dump_nquads(self): + def test_dump_nquads(self) -> None: store = Store() store.add(Quad(foo, bar, baz, graph)) output = BytesIO() @@ -264,7 +267,7 @@ class TestStore(unittest.TestCase): b" .\n", ) - def test_dump_file(self): + def test_dump_file(self) -> None: with NamedTemporaryFile(delete=False) as fp: file_name = fp.name store = Store() @@ -277,14 +280,14 @@ class TestStore(unittest.TestCase): " .\n", ) - def test_dump_with_io_error(self): + def test_dump_with_io_error(self) -> None: class BadIO(RawIOBase): pass with self.assertRaises(OSError) as _: Store().dump(BadIO(), mime_type="application/rdf+xml") - def test_write_in_read(self): + def test_write_in_read(self) -> None: store = Store() store.add(Quad(foo, bar, bar)) store.add(Quad(foo, bar, baz)) @@ -292,12 +295,12 @@ class TestStore(unittest.TestCase): store.add(Quad(triple.object, triple.predicate, triple.subject)) self.assertEqual(len(store), 4) - def test_add_graph(self): + def test_add_graph(self) -> None: store = Store() store.add_graph(graph) self.assertEqual(list(store.named_graphs()), [graph]) - def test_remove_graph(self): + def test_remove_graph(self) -> None: store = Store() store.add(Quad(foo, bar, baz, graph)) store.add_graph(NamedNode("http://graph2")) diff --git a/server/Cargo.toml b/server/Cargo.toml index bfcd54ba..0183d9c4 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "oxigraph_server" -version = "0.3.8" +version = "0.3.9" authors = ["Tpt "] license = "MIT OR Apache-2.0" readme = "README.md" @@ -15,8 +15,8 @@ edition = "2021" anyhow = "1" oxhttp = { version = "0.1", features = ["rayon"] } clap = { version = "4", features = ["derive"] } -oxigraph = { version = "0.3.8", path = "../lib", features = ["http_client"] } -sparesults = { version = "0.1.1", path = "../lib/sparesults", features = ["rdf-star"] } +oxigraph = { version = "0.3.9", path = "../lib", features = ["http_client"] } +sparesults = { version = "0.1.2", path = "../lib/sparesults", features = ["rdf-star"] } rand = "0.8" url = "2" oxiri = "0.2" diff --git a/testsuite/Cargo.toml b/testsuite/Cargo.toml index ba90c607..87bc7282 100644 --- a/testsuite/Cargo.toml +++ b/testsuite/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "oxigraph_testsuite" -version = "0.3.8" +version = "0.3.9" authors = ["Tpt "] license = "MIT OR Apache-2.0" readme = "../README.md" @@ -15,7 +15,7 @@ publish = false anyhow = "1" clap = { version = "4", features = ["derive"] } time = { version = "0.3", features = ["formatting"] } -oxigraph = { version = "0.3.8", path="../lib" } +oxigraph = { version = "0.3.9", path="../lib" } text-diff = "0.4" [dev-dependencies] diff --git a/testsuite/oxigraph-tests/sparql/halloween_problem.ru b/testsuite/oxigraph-tests/sparql/halloween_problem.ru index cff7eaf4..d62147d2 100644 --- a/testsuite/oxigraph-tests/sparql/halloween_problem.ru +++ b/testsuite/oxigraph-tests/sparql/halloween_problem.ru @@ -1,3 +1,3 @@ PREFIX ex: -INSERT DATA { ex:s ex:salary 1200 . ex:s2 ex:salary 1250 . ex:boss ex:salary 1600 . }; +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) } diff --git a/testsuite/oxigraph-tests/sparql/halloween_problem_result.ttl b/testsuite/oxigraph-tests/sparql/halloween_problem_result.ttl index c25c63a0..5e394780 100644 --- a/testsuite/oxigraph-tests/sparql/halloween_problem_result.ttl +++ b/testsuite/oxigraph-tests/sparql/halloween_problem_result.ttl @@ -2,4 +2,5 @@ ex:s ex:salary 1300 . ex:s2 ex:salary 1350 . +ex:s3 ex:salary 1380 . ex:boss ex:salary 1600 .