diff --git a/Cargo.lock b/Cargo.lock index 5a6e8882..989fd8db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2313,6 +2313,26 @@ dependencies = [ "virtualization", ] +[[package]] +name = "maplibr" +version = "0.1.0" +dependencies = [ + "maplib", + "oxrdf", + "oxrdfio", + "polars", + "polars-arrow", + "pyo3", + "representation", + "savvy", + "serde", + "serde_json", + "sparesults", + "spargebra", + "templates", + "triplestore", +] + [[package]] name = "matchers" version = "0.2.0" @@ -4385,6 +4405,47 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "savvy" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1c95d0e631c3a259af8c2ec2492ad34c30af6399231130d86446691c69ee1f4" +dependencies = [ + "cc", + "rustversion", + "savvy-ffi", + "savvy-macro", +] + +[[package]] +name = "savvy-bindgen" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6e909d6698c296c73909e8c46b4124781e9e82f2bda72e0471b7cf8e4481c31" +dependencies = [ + "proc-macro2", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "savvy-ffi" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f1b0aeb4e546325c4c207d736d95952f8c589b85d2bf80dfc429a1975a17c74" + +[[package]] +name = "savvy-macro" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8884eebdd646f450ba554e96f32e8904d7d13f573be8d34254374934ab7b619" +dependencies = [ + "proc-macro2", + "quote 1.0.46", + "savvy-bindgen", + "syn 2.0.118", +] + [[package]] name = "schannel" version = "0.1.29" diff --git a/Cargo.toml b/Cargo.toml index a95d5aad..3109bb8d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,96 +1,81 @@ -[workspace] -resolver = "2" -members = [ - "lib/in_memory", - "lib/disk", - "lib/utils", - "lib/representation", - "lib/maplib", - "lib/triplestore", - "lib/file_io", - "lib/query_processing", - "lib/spargebra", - "lib/shacl", - "lib/fts", - "lib/datalog", - "lib/cimxml_export", - "lib/cimxml_import", - "lib/chrontext", - "lib/virtualized_query", - "lib/virtualization", - "lib/opc_ua", - "lib/external_sparql", - "py_maplib", -] - -[workspace.package] -rust-version = "1.88.0" -edition = "2021" - -[workspace.dependencies] -pyo3 = { version = "0.29.0", features = ["py-clone", "rust_decimal"] } -#Moment is there to work around a feature toggle bug -polars = { version = "0.54.4", features = [ - "simd", - "nightly", - "performant", - "cse", - "fmt", - "parquet", - "moment", - "timezones", - "lazy", -] } -polars-arrow = { version = "0.54.4" } -polars-core = { version = "0.54.4" } -rayon = "1.10.0" -regex = "1.11.1" -oxrdf = { version = "0.3.3" } -oxrdfio = { version = "0.2.5" } -oxttl = { version = "0.2.3" } -oxjsonld = { version = "0.2.5" } -oxiri = "0.2.11" -sparesults = "0.3.3" -oxsdatatypes = "0.2.2" -chrono = "0.4.41" -chrono-tz = "0.10.3" -uuid = { version = "1.16.0", features = [ - "v4", # Lets you generate random UUIDs - "v5", - "fast-rng", # Use a faster (but still sufficiently random) RNG -] } -thiserror = "2.0.12" -peg = "0.8" -rand = "0.10.1" -oxilangtag = "0.1.5" -fundu = "2.0.1" -hdt = { version = "0.7.2", default-features = false, features = ["nt"] } -memmap2 = "0.9.5" -sprs = "0.11.3" -walkdir = "2.5.0" -aho-corasick = "1.1.4" -simd-json = { version = "0.17.0", features = ["serde_json"] } -serde_json = { version = "1.0.149" } -serde = {version = "1.0.228"} -urlencoding = "2.1.3" -md-5 = "0.11.0" -sha1 = "0.11.0" -hex = "0.4.3" -memchr = "2.8.0" -quick-xml = { version = "0.41.0", features = ["arbitrary"] } -tracing = { version = "0.1", features = ["log"] } -tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } -tracing-log = "0.2" -itoa = "1.0.15" -ryu = "1.0.20" -#dev-dependencies -nohash-hasher = "0.2.0" -arrow = "58" -ordered-float = "5.1.0" -rust_decimal = "1.42.1" -range-set-blaze = "0.5.0" -reqwest = { version = "0.13.4", features = ["query", "json", "rustls-no-provider"] } - -[patch.crates-io] -#polars = { git = 'https://github.com/pola-rs/polars', rev="0df0c25d4db895ec8cad773bedc4e98400e3135e" } -#polars-core = { git = 'https://github.com/pola-rs/polars', rev="0df0c25d4db895ec8cad773bedc4e98400e3135e" } +[workspace] +resolver = "2" +members = ["lib/chrontext", "lib/cimxml_export", "lib/cimxml_import", "lib/datalog", "lib/disk", "lib/external_sparql", "lib/file_io", "lib/fts", "lib/in_memory", "lib/maplib", "lib/opc_ua", "lib/query_processing", "lib/representation", "lib/shacl", "lib/spargebra", "lib/triplestore", "lib/utils", "lib/virtualization", "lib/virtualized_query", "py_maplib", "maplibr/src/rust"] + +[workspace.package] +edition = "2021" +rust-version = "1.88.0" + +[workspace.dependencies] +aho-corasick = "1.1.4" +arrow = "58" +chrono = "0.4.41" +chrono-tz = "0.10.3" +fundu = "2.0.1" +hdt = { version = "0.7.2", default-features = false, features = ["nt"] } +hex = "0.4.3" +itoa = "1.0.15" +md-5 = "0.11.0" +memchr = "2.8.0" +memmap2 = "0.9.5" +# dev-dependencies +nohash-hasher = "0.2.0" +ordered-float = "5.1.0" +oxilangtag = "0.1.5" +oxiri = "0.2.11" +oxjsonld = { version = "0.2.5" } +oxrdf = { version = "0.3.3" } +oxrdfio = { version = "0.2.5" } +oxsdatatypes = "0.2.2" +oxttl = { version = "0.2.3" } +peg = "0.8" +# Moment is there to work around a feature toggle bug +polars = { + version = "0.54.4", + features = [ + "cse", + "fmt", + "lazy", + "moment", + "nightly", + "parquet", + "performant", + "simd", + "timezones", + ] +} +polars-arrow = { version = "0.54.4" } +polars-core = { version = "0.54.4" } +pyo3 = { version = "0.29.0", features = ["py-clone", "rust_decimal"] } +quick-xml = { version = "0.41.0", features = ["arbitrary"] } +rand = "0.10.1" +range-set-blaze = "0.5.0" +rayon = "1.10.0" +regex = "1.11.1" +reqwest = { version = "0.13.4", features = ["json", "query", "rustls-no-provider"] } +rust_decimal = "1.42.1" +ryu = "1.0.20" +serde = { version = "1.0.228" } +serde_json = { version = "1.0.149" } +sha1 = "0.11.0" +simd-json = { version = "0.17.0", features = ["serde_json"] } +sparesults = "0.3.3" +sprs = "0.11.3" +thiserror = "2.0.12" +tracing = { version = "0.1", features = ["log"] } +tracing-log = "0.2" +tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } +urlencoding = "2.1.3" +uuid = { + version = "1.16.0", + features = [ + "fast-rng", # Use a faster (but still sufficiently random) RNG + "v4", # Lets you generate random UUIDs + "v5", + ] +} +walkdir = "2.5.0" + +[patch.crates-io] +# polars = { git = 'https://github.com/pola-rs/polars', rev="0df0c25d4db895ec8cad773bedc4e98400e3135e" } +# polars-core = { git = 'https://github.com/pola-rs/polars', rev="0df0c25d4db895ec8cad773bedc4e98400e3135e" } diff --git a/maplibr/.Rbuildignore b/maplibr/.Rbuildignore new file mode 100644 index 00000000..5bf4fc92 --- /dev/null +++ b/maplibr/.Rbuildignore @@ -0,0 +1,4 @@ +^src/rust/\.cargo$ +^src/rust/target$ +^src/Makevars$ +^src/Makevars\.win$ diff --git a/maplibr/DESCRIPTION b/maplibr/DESCRIPTION new file mode 100644 index 00000000..e2d4002a --- /dev/null +++ b/maplibr/DESCRIPTION @@ -0,0 +1,41 @@ +Package: maplibr +Title: R Interface to the 'maplib' Knowledge Graph Engine +Version: 0.0.0.9000 +Authors@R: c( + person("Novica", "Nakov", , "nnovica@gmail.com", role = c("aut", "cre", "cph"), + comment = c(ORCID = "0009-0005-7773-7718"))) +Description: Build, query, and reason over RDF knowledge graphs backed by the + 'maplib' Rust engine. Graphs are constructed from tabular data using OTTR + mapping templates, queried with SPARQL, and returned as ordinary R + data frames via the Arrow C Stream Interface. Also supports RDFS + inference and reading/writing common RDF serializations (Turtle, + N-Triples, RDF/XML). +License: Apache License (>= 2.0) +Encoding: UTF-8 +Depends: + R (>= 4.1) +Imports: + R6, + S7, + nanoarrow, + jsonlite +Roxygen: list(markdown = TRUE) +RoxygenNote: 8.0.0 +SystemRequirements: Cargo (Rust's package manager), rustc +Collate: + '000-wrappers.R' + 'errors.R' + 'terms.R' + 'vocab.R' + 'rdf_type.R' + 'templates.R' + 'model.R' + 'model-verbs.R' + 'zzz.R' +Suggests: + testthat (>= 3.0.0), + knitr, + rmarkdown, + igraph +Config/testthat/edition: 3 +VignetteBuilder: knitr diff --git a/maplibr/NAMESPACE b/maplibr/NAMESPACE new file mode 100644 index 00000000..4283d402 --- /dev/null +++ b/maplibr/NAMESPACE @@ -0,0 +1,76 @@ +# Generated by roxygen2: do not edit by hand + +S3method("$<-",savvy_maplibr__sealed) +S3method("[[<-",savvy_maplibr__sealed) +S3method(print,"maplibr::RArgument__bundle") +S3method(print,"maplibr::RConstantTerm__bundle") +S3method(print,"maplibr::RGroundTerm__bundle") +S3method(print,"maplibr::RInstance__bundle") +S3method(print,"maplibr::RModel__bundle") +S3method(print,"maplibr::RParameter__bundle") +S3method(print,"maplibr::RTemplate__bundle") +export(Argument) +export(BlankNode) +export(IRI) +export(Instance) +export(Literal) +export(Model) +export(Parameter) +export(Prefix) +export(RArgument) +export(RConstantTerm) +export(RDFType) +export(RGroundTerm) +export(RInstance) +export(RModel) +export(RParameter) +export(RTemplate) +export(Template) +export(Triple) +export(Variable) +export(export_test_series) +export(export_test_solution_mappings) +export(import_test_series) +export(import_test_solution_mappings) +export(instance_template_iri) +export(instantiate) +export(make_triple) +export(model_add_graph) +export(model_add_prefixes) +export(model_add_template) +export(model_compact) +export(model_create_index) +export(model_deserialize) +export(model_detach_graph) +export(model_infer_rdfs) +export(model_insert) +export(model_map) +export(model_map_df) +export(model_map_json) +export(model_map_json_string) +export(model_map_triples) +export(model_map_xml) +export(model_map_xml_string) +export(model_query) +export(model_read) +export(model_reads) +export(model_serialize) +export(model_size) +export(model_truncate_graph) +export(model_update) +export(model_write) +export(model_writes) +export(owl) +export(rdf) +export(rdf_type_blank_node) +export(rdf_type_iri) +export(rdf_type_literal) +export(rdf_type_multi) +export(rdf_type_unknown) +export(rdfs) +export(suf) +export(validate_blank_node_id) +export(validate_iri) +export(validate_variable_name) +export(xsd) +useDynLib(maplibr, .registration = TRUE) diff --git a/maplibr/R/000-wrappers.R b/maplibr/R/000-wrappers.R new file mode 100644 index 00000000..5ac3ac01 --- /dev/null +++ b/maplibr/R/000-wrappers.R @@ -0,0 +1,598 @@ +# Generated by savvy: do not edit by hand +# +# Note: +# This wrapper file is named as `000-wrappers.R` so that this file is loaded +# first, which allows users to override the functions defined here (e.g., a +# print() method for an enum). + +#' @useDynLib maplibr, .registration = TRUE +#' @keywords internal +NULL + +# Check class and extract the external pointer embedded in the environment +.savvy_extract_ptr <- function(e, class) { + if(is.null(e)) { + return(NULL) + } + + if(inherits(e, class)) { + e$.ptr + } else { + msg <- paste0("Expected ", class, ", got ", class(e)[1]) + stop(msg, call. = FALSE) + } +} + +# Prohibit modifying environments + +#' @export +`$<-.savvy_maplibr__sealed` <- function(x, name, value) { + class <- gsub("__bundle$", "", class(x)[1]) + stop(class, " cannot be modified", call. = FALSE) +} + +#' @export +`[[<-.savvy_maplibr__sealed` <- function(x, i, value) { + class <- gsub("__bundle$", "", class(x)[1]) + stop(class, " cannot be modified", call. = FALSE) +} + +#' Build a small hardcoded i32 Series in Rust and stream it out to R +#' through the Arrow C Stream Interface. +#' +#' @param stream_ptr An external pointer from `nanoarrow::nanoarrow_allocate_array_stream()`. +#' @export +`export_test_series` <- function(`stream_ptr`) { + invisible(.Call(savvy_export_test_series__impl, `stream_ptr`)) +} + +#' Build the `build_test_solution_mappings()` fixture and stream it out via +#' `export_solution_mappings` -- proving the DataFrame case reuses the +#' Series-level mechanism unchanged, against a fixture with a genuinely +#' multi-typed column (see `build_test_solution_mappings`'s doc comment). +#' +#' @param stream_ptr An external pointer from `nanoarrow::nanoarrow_allocate_array_stream()`. +#' @returns The `rdf_node_types` side-channel, as a JSON string. +#' @export +`export_test_solution_mappings` <- function(`stream_ptr`) { + .Call(savvy_export_test_solution_mappings__impl, `stream_ptr`) +} + +#' Read an Arrow C stream from R back into a Rust Series, then hand back +#' its length and first value as a sanity check (i32 only, for this prototype). +#' +#' @param stream_ptr An external pointer holding a filled ArrowArrayStream. +#' @returns An integer vector: `c(length, first_value)`. +#' @export +`import_test_series` <- function(`stream_ptr`) { + .Call(savvy_import_test_series__impl, `stream_ptr`) +} + +#' Read an Arrow C stream from R back into a Struct-typed Series, unnest it +#' back into a DataFrame (`StructChunked::unnest`, the reverse of +#' `into_struct`), and return a small human-readable summary combining the +#' recovered DataFrame's shape with the `rdf_node_types` JSON side-channel +#' handed back in -- a sanity check that both halves survive the round trip +#' together, matching `import_test_series`'s role for the Series-only case. +#' +#' @param stream_ptr An external pointer holding a filled ArrowArrayStream. +#' @param rdf_node_types_json The JSON string returned by +#' `export_test_solution_mappings`. +#' @returns A character vector: one line per column, `": []"`. +#' @export +`import_test_solution_mappings` <- function(`stream_ptr`, `rdf_node_types_json`) { + .Call(savvy_import_test_solution_mappings__impl, `stream_ptr`, `rdf_node_types_json`) +} + +#' Build an rdf:type-style Triple instance (mirrors py_maplib's `Triple()` +#' helper, lib/templates/src/python.rs:404-417). +#' +#' @export +`make_triple` <- function(`subject`, `predicate`, `object`, `list_expander` = NULL) { + `subject` <- .savvy_extract_ptr(`subject`, "maplibr::RArgument") + `predicate` <- .savvy_extract_ptr(`predicate`, "maplibr::RArgument") + `object` <- .savvy_extract_ptr(`object`, "maplibr::RArgument") + .savvy_wrap_RInstance(.Call(savvy_make_triple__impl, `subject`, `predicate`, `object`, `list_expander`)) +} + +#' Validate a blank node identifier's syntax (mirrors oxrdf::BlankNode::new, +#' used by py_maplib's PyBlankNode::new, lib/representation/src/python.rs:531-535). +#' Raises an R error on invalid syntax; returns nothing on success. +#' +#' @param id Blank node identifier, without the leading `_:`. +#' @export +`validate_blank_node_id` <- function(`id`) { + invisible(.Call(savvy_validate_blank_node_id__impl, `id`)) +} + +#' Validate an IRI's syntax (mirrors oxrdf::NamedNode::new, the same check +#' py_maplib's PyIRI::new uses, lib/representation/src/python.rs:279-282). +#' Raises an R error on invalid syntax; returns nothing on success. +#' +#' @param iri IRI string to validate. +#' @export +`validate_iri` <- function(`iri`) { + invisible(.Call(savvy_validate_iri__impl, `iri`)) +} + +#' Validate a SPARQL variable name's syntax (mirrors oxrdf::Variable::new, +#' used by py_maplib's PyVariable::new, lib/representation/src/python.rs:340-343). +#' Raises an R error on invalid syntax; returns nothing on success. +#' +#' @param name Variable name, without the leading `?`/`$`. +#' @export +`validate_variable_name` <- function(`name`) { + invisible(.Call(savvy_validate_variable_name__impl, `name`)) +} + +### wrapper functions for RArgument + + +`.savvy_wrap_RArgument` <- function(ptr) { + e <- new.env(parent = emptyenv()) + e$.ptr <- ptr + + + class(e) <- c("maplibr::RArgument", "RArgument", "savvy_maplibr__sealed") + e +} + + +#' An OTTR argument: a Variable, or a constant term (IRI/BlankNode/Literal/none). +#' Mirrors py_maplib's `Argument` (lib/templates/src/python.rs:203-236). +#' +#' @export +`RArgument` <- new.env(parent = emptyenv()) + +### associated functions for RArgument + +`RArgument`$`from_constant_term` <- function(`term`, `list_expand`) { + `term` <- .savvy_extract_ptr(`term`, "maplibr::RConstantTerm") + .savvy_wrap_RArgument(.Call(savvy_RArgument_from_constant_term__impl, `term`, `list_expand`)) +} + +`RArgument`$`from_variable` <- function(`name`, `list_expand`) { + .savvy_wrap_RArgument(.Call(savvy_RArgument_from_variable__impl, `name`, `list_expand`)) +} + + +class(`RArgument`) <- c("maplibr::RArgument__bundle", "savvy_maplibr__sealed") + +#' @export +`print.maplibr::RArgument__bundle` <- function(x, ...) { + cat('maplibr::RArgument\n') +} + +### wrapper functions for RConstantTerm + + +`.savvy_wrap_RConstantTerm` <- function(ptr) { + e <- new.env(parent = emptyenv()) + e$.ptr <- ptr + + + class(e) <- c("maplibr::RConstantTerm", "RConstantTerm", "savvy_maplibr__sealed") + e +} + + +#' An OTTR constant term: an IRI, blank node, literal, or explicit "none". +#' Mirrors py_maplib's `extract_constant_term` (lib/templates/src/python.rs:434-446). +#' +#' @export +`RConstantTerm` <- new.env(parent = emptyenv()) + +### associated functions for RConstantTerm + +`RConstantTerm`$`blank_node` <- function(`id`) { + .savvy_wrap_RConstantTerm(.Call(savvy_RConstantTerm_blank_node__impl, `id`)) +} + +`RConstantTerm`$`iri` <- function(`iri`) { + .savvy_wrap_RConstantTerm(.Call(savvy_RConstantTerm_iri__impl, `iri`)) +} + +`RConstantTerm`$`literal` <- function(`value`, `datatype_iri`, `language` = NULL) { + .savvy_wrap_RConstantTerm(.Call(savvy_RConstantTerm_literal__impl, `value`, `datatype_iri`, `language`)) +} + +`RConstantTerm`$`none` <- function() { + .savvy_wrap_RConstantTerm(.Call(savvy_RConstantTerm_none__impl)) +} + + +class(`RConstantTerm`) <- c("maplibr::RConstantTerm__bundle", "savvy_maplibr__sealed") + +#' @export +`print.maplibr::RConstantTerm__bundle` <- function(x, ...) { + cat('maplibr::RConstantTerm\n') +} + +### wrapper functions for RGroundTerm + + +`.savvy_wrap_RGroundTerm` <- function(ptr) { + e <- new.env(parent = emptyenv()) + e$.ptr <- ptr + + + class(e) <- c("maplibr::RGroundTerm", "RGroundTerm", "savvy_maplibr__sealed") + e +} + + +#' A ground SPARQL binding value: an IRI or a Literal (no blank nodes, +#' unlike `RConstantTerm` -- blank nodes can't be bound to a query variable, +#' so there's no variant for one to construct in the first place, unlike +#' py_maplib's equivalent, which has to reject a blank node at runtime since +#' it accepts a plain Python value that could be anything, see +#' `maybe_parse_bindings`, py_maplib/src/lib.rs:479-528). Built from an R +#' `IRI`/`Literal` (terms.R) by `.to_ground_term()`, and consumed by +#' `RModel::query`'s `bindings` argument. +#' +#' @export +`RGroundTerm` <- new.env(parent = emptyenv()) + +### associated functions for RGroundTerm + +`RGroundTerm`$`iri` <- function(`iri`) { + .savvy_wrap_RGroundTerm(.Call(savvy_RGroundTerm_iri__impl, `iri`)) +} + +`RGroundTerm`$`literal` <- function(`value`, `datatype_iri`, `language` = NULL) { + .savvy_wrap_RGroundTerm(.Call(savvy_RGroundTerm_literal__impl, `value`, `datatype_iri`, `language`)) +} + + +class(`RGroundTerm`) <- c("maplibr::RGroundTerm__bundle", "savvy_maplibr__sealed") + +#' @export +`print.maplibr::RGroundTerm__bundle` <- function(x, ...) { + cat('maplibr::RGroundTerm\n') +} + +### wrapper functions for RInstance + +`RInstance_iri` <- function(self) { + function() { + .Call(savvy_RInstance_iri__impl, `self`) + } +} + +`.savvy_wrap_RInstance` <- function(ptr) { + e <- new.env(parent = emptyenv()) + e$.ptr <- ptr + e$`iri` <- `RInstance_iri`(ptr) + + class(e) <- c("maplibr::RInstance", "RInstance", "savvy_maplibr__sealed") + e +} + + +#' An OTTR template instance: a call to a template with concrete arguments. +#' Mirrors py_maplib's `Instance` (lib/templates/src/python.rs:245-288). +#' +#' @export +`RInstance` <- new.env(parent = emptyenv()) + +### associated functions for RInstance + +`RInstance`$`new` <- function(`template_iri`, `arguments`, `list_expander` = NULL) { + .savvy_wrap_RInstance(.Call(savvy_RInstance_new__impl, `template_iri`, `arguments`, `list_expander`)) +} + + +class(`RInstance`) <- c("maplibr::RInstance__bundle", "savvy_maplibr__sealed") + +#' @export +`print.maplibr::RInstance__bundle` <- function(x, ...) { + cat('maplibr::RInstance\n') +} + +### wrapper functions for RModel + +`RModel_add_graph` <- function(self) { + function(`other`, `source_graph` = NULL, `target_graph` = NULL) { + `other` <- .savvy_extract_ptr(`other`, "maplibr::RModel") + invisible(.Call(savvy_RModel_add_graph__impl, `self`, `other`, `source_graph`, `target_graph`)) + } +} + +`RModel_add_prefixes` <- function(self) { + function(`prefixes`) { + invisible(.Call(savvy_RModel_add_prefixes__impl, `self`, `prefixes`)) + } +} + +`RModel_add_template` <- function(self) { + function(`template`) { + `template` <- .savvy_extract_ptr(`template`, "maplibr::RTemplate") + invisible(.Call(savvy_RModel_add_template__impl, `self`, `template`)) + } +} + +`RModel_add_template_string` <- function(self) { + function(`doc`) { + .Call(savvy_RModel_add_template_string__impl, `self`, `doc`) + } +} + +`RModel_compact` <- function(self) { + function() { + invisible(.Call(savvy_RModel_compact__impl, `self`)) + } +} + +`RModel_create_index` <- function(self) { + function() { + invisible(.Call(savvy_RModel_create_index__impl, `self`)) + } +} + +`RModel_detach_graph` <- function(self) { + function(`preserve_name`, `graph` = NULL) { + .savvy_wrap_RModel(.Call(savvy_RModel_detach_graph__impl, `self`, `preserve_name`, `graph`)) + } +} + +`RModel_infer_rdfs` <- function(self) { + function(`graph` = NULL) { + .Call(savvy_RModel_infer_rdfs__impl, `self`, `graph`) + } +} + +`RModel_insert` <- function(self) { + function(`query`, `include_transient`, `transient`, `source_graph` = NULL, `target_graph` = NULL) { + invisible(.Call(savvy_RModel_insert__impl, `self`, `query`, `include_transient`, `transient`, `source_graph`, `target_graph`)) + } +} + +`RModel_map` <- function(self) { + function(`template_iri`, `stream_ptr`, `graph` = NULL, `validate_iris` = NULL) { + invisible(.Call(savvy_RModel_map__impl, `self`, `template_iri`, `stream_ptr`, `graph`, `validate_iris`)) + } +} + +`RModel_map_df` <- function(self) { + function(`stream_ptr`, `graph` = NULL, `uuid_namespace` = NULL) { + invisible(.Call(savvy_RModel_map_df__impl, `self`, `stream_ptr`, `graph`, `uuid_namespace`)) + } +} + +`RModel_map_json` <- function(self) { + function(`path`, `transient`, `graph` = NULL, `uuid_namespace` = NULL) { + invisible(.Call(savvy_RModel_map_json__impl, `self`, `path`, `transient`, `graph`, `uuid_namespace`)) + } +} + +`RModel_map_json_string` <- function(self) { + function(`json`, `transient`, `graph` = NULL, `uuid_namespace` = NULL) { + invisible(.Call(savvy_RModel_map_json_string__impl, `self`, `json`, `transient`, `graph`, `uuid_namespace`)) + } +} + +`RModel_map_no_data` <- function(self) { + function(`template_iri`, `graph` = NULL, `validate_iris` = NULL) { + invisible(.Call(savvy_RModel_map_no_data__impl, `self`, `template_iri`, `graph`, `validate_iris`)) + } +} + +`RModel_map_triples` <- function(self) { + function(`stream_ptr`, `predicate` = NULL, `graph` = NULL, `validate_iris` = NULL) { + invisible(.Call(savvy_RModel_map_triples__impl, `self`, `stream_ptr`, `predicate`, `graph`, `validate_iris`)) + } +} + +`RModel_map_xml` <- function(self) { + function(`path`, `transient`, `graph` = NULL, `uuid_namespace` = NULL) { + invisible(.Call(savvy_RModel_map_xml__impl, `self`, `path`, `transient`, `graph`, `uuid_namespace`)) + } +} + +`RModel_map_xml_string` <- function(self) { + function(`xml`, `transient`, `graph` = NULL, `uuid_namespace` = NULL) { + invisible(.Call(savvy_RModel_map_xml_string__impl, `self`, `xml`, `transient`, `graph`, `uuid_namespace`)) + } +} + +`RModel_query` <- function(self) { + function(`sparql`, `stream_ptr`, `include_transient`, `graph` = NULL, `bindings` = NULL) { + .Call(savvy_RModel_query__impl, `self`, `sparql`, `stream_ptr`, `include_transient`, `graph`, `bindings`) + } +} + +`RModel_read` <- function(self) { + function(`path`, `format` = NULL, `graph` = NULL) { + invisible(.Call(savvy_RModel_read__impl, `self`, `path`, `format`, `graph`)) + } +} + +`RModel_reads` <- function(self) { + function(`s`, `format`, `graph` = NULL) { + invisible(.Call(savvy_RModel_reads__impl, `self`, `s`, `format`, `graph`)) + } +} + +`RModel_serialize` <- function(self) { + function(`path` = NULL) { + invisible(.Call(savvy_RModel_serialize__impl, `self`, `path`)) + } +} + +`RModel_size` <- function(self) { + function(`graph` = NULL) { + .Call(savvy_RModel_size__impl, `self`, `graph`) + } +} + +`RModel_truncate_graph` <- function(self) { + function(`graph` = NULL) { + invisible(.Call(savvy_RModel_truncate_graph__impl, `self`, `graph`)) + } +} + +`RModel_update` <- function(self) { + function(`update`, `include_transient`, `graph` = NULL) { + invisible(.Call(savvy_RModel_update__impl, `self`, `update`, `include_transient`, `graph`)) + } +} + +`RModel_write` <- function(self) { + function(`path`, `format` = NULL, `graph` = NULL) { + invisible(.Call(savvy_RModel_write__impl, `self`, `path`, `format`, `graph`)) + } +} + +`RModel_writes` <- function(self) { + function(`format` = NULL, `graph` = NULL) { + .Call(savvy_RModel_writes__impl, `self`, `format`, `graph`) + } +} + +`.savvy_wrap_RModel` <- function(ptr) { + e <- new.env(parent = emptyenv()) + e$.ptr <- ptr + e$`add_graph` <- `RModel_add_graph`(ptr) + e$`add_prefixes` <- `RModel_add_prefixes`(ptr) + e$`add_template` <- `RModel_add_template`(ptr) + e$`add_template_string` <- `RModel_add_template_string`(ptr) + e$`compact` <- `RModel_compact`(ptr) + e$`create_index` <- `RModel_create_index`(ptr) + e$`detach_graph` <- `RModel_detach_graph`(ptr) + e$`infer_rdfs` <- `RModel_infer_rdfs`(ptr) + e$`insert` <- `RModel_insert`(ptr) + e$`map` <- `RModel_map`(ptr) + e$`map_df` <- `RModel_map_df`(ptr) + e$`map_json` <- `RModel_map_json`(ptr) + e$`map_json_string` <- `RModel_map_json_string`(ptr) + e$`map_no_data` <- `RModel_map_no_data`(ptr) + e$`map_triples` <- `RModel_map_triples`(ptr) + e$`map_xml` <- `RModel_map_xml`(ptr) + e$`map_xml_string` <- `RModel_map_xml_string`(ptr) + e$`query` <- `RModel_query`(ptr) + e$`read` <- `RModel_read`(ptr) + e$`reads` <- `RModel_reads`(ptr) + e$`serialize` <- `RModel_serialize`(ptr) + e$`size` <- `RModel_size`(ptr) + e$`truncate_graph` <- `RModel_truncate_graph`(ptr) + e$`update` <- `RModel_update`(ptr) + e$`write` <- `RModel_write`(ptr) + e$`writes` <- `RModel_writes`(ptr) + + class(e) <- c("maplibr::RModel", "RModel", "savvy_maplibr__sealed") + e +} + + +#' A maplib knowledge graph model. +#' +#' @export +`RModel` <- new.env(parent = emptyenv()) + +### associated functions for RModel + +`RModel`$`deserialize` <- function(`path` = NULL, `storage_folder` = NULL) { + .savvy_wrap_RModel(.Call(savvy_RModel_deserialize__impl, `path`, `storage_folder`)) +} + +`RModel`$`new` <- function() { + .savvy_wrap_RModel(.Call(savvy_RModel_new__impl)) +} + + +class(`RModel`) <- c("maplibr::RModel__bundle", "savvy_maplibr__sealed") + +#' @export +`print.maplibr::RModel__bundle` <- function(x, ...) { + cat('maplibr::RModel\n') +} + +### wrapper functions for RParameter + + +`.savvy_wrap_RParameter` <- function(ptr) { + e <- new.env(parent = emptyenv()) + e$.ptr <- ptr + + + class(e) <- c("maplibr::RParameter", "RParameter", "savvy_maplibr__sealed") + e +} + + +#' An OTTR template parameter. Mirrors py_maplib's `Parameter` +#' (lib/templates/src/python.rs:21-167) -- `rdf_type` here only supports a +#' basic (non-nested, non-list) RDF type, given as a datatype/class IRI +#' string; PType::Lub/List/NEList and RDFType.Nested()/Multi() are not yet +#' ported (they need the RDFType port, maplib-snz's remaining scope). +#' +#' @export +`RParameter` <- new.env(parent = emptyenv()) + +### associated functions for RParameter + +`RParameter`$`new` <- function(`variable_name`, `optional`, `allow_blank`, `rdf_type_iri` = NULL, `default_value` = NULL) { + `default_value` <- .savvy_extract_ptr(`default_value`, "maplibr::RConstantTerm") + .savvy_wrap_RParameter(.Call(savvy_RParameter_new__impl, `variable_name`, `optional`, `allow_blank`, `rdf_type_iri`, `default_value`)) +} + + +class(`RParameter`) <- c("maplibr::RParameter__bundle", "savvy_maplibr__sealed") + +#' @export +`print.maplibr::RParameter__bundle` <- function(x, ...) { + cat('maplibr::RParameter\n') +} + +### wrapper functions for RTemplate + +`RTemplate_instance` <- function(self) { + function(`arguments`, `list_expander` = NULL) { + .savvy_wrap_RInstance(.Call(savvy_RTemplate_instance__impl, `self`, `arguments`, `list_expander`)) + } +} + +`RTemplate_iri` <- function(self) { + function() { + .Call(savvy_RTemplate_iri__impl, `self`) + } +} + +`RTemplate_print_string` <- function(self) { + function() { + .Call(savvy_RTemplate_print_string__impl, `self`) + } +} + +`.savvy_wrap_RTemplate` <- function(ptr) { + e <- new.env(parent = emptyenv()) + e$.ptr <- ptr + e$`instance` <- `RTemplate_instance`(ptr) + e$`iri` <- `RTemplate_iri`(ptr) + e$`print_string` <- `RTemplate_print_string`(ptr) + + class(e) <- c("maplibr::RTemplate", "RTemplate", "savvy_maplibr__sealed") + e +} + + +#' An OTTR template: a named, parameterized set of instance patterns. +#' Mirrors py_maplib's `Template` (lib/templates/src/python.rs:291-402). +#' +#' @export +`RTemplate` <- new.env(parent = emptyenv()) + +### associated functions for RTemplate + +`RTemplate`$`new` <- function(`iri`, `parameters`, `instances`) { + .savvy_wrap_RTemplate(.Call(savvy_RTemplate_new__impl, `iri`, `parameters`, `instances`)) +} + + +class(`RTemplate`) <- c("maplibr::RTemplate__bundle", "savvy_maplibr__sealed") + +#' @export +`print.maplibr::RTemplate__bundle` <- function(x, ...) { + cat('maplibr::RTemplate\n') +} + diff --git a/maplibr/R/errors.R b/maplibr/R/errors.R new file mode 100644 index 00000000..3f74489c --- /dev/null +++ b/maplibr/R/errors.R @@ -0,0 +1,51 @@ +# R-side half of maplib-dma: mirrors the intent of py_maplib's typed +# PyMaplibError/MaplibException/FunctionArgumentException hierarchy +# (py_maplib/src/error.rs), adapted to savvy's error boundary, which can only +# carry a plain message string across (unlike pyo3, which lets Rust raise an +# actual typed Python exception object). maplibr/src/rust/src/errors.rs +# tags every error it raises with a `[maplibr_xxx_error] ` prefix; +# `.rethrow()` parses that prefix back out here and re-raises as a proper R +# condition carrying a matching class, so R code can catch by class the same +# way Python code catches by exception type: +# +# tryCatch(m$reads(...), maplibr_argument_error = function(e) ...) +# +# Every Model method that calls into the underlying RModel savvy object goes +# through `.rethrow()` (see model.R) -- S7 term/template constructors +# (terms.R/templates.R) still surface the tagged message as plain text via +# their validators (S7 wraps validator failures in its own condition class +# already), not reclassified the same way; that's lower-value there since +# those are all just simple argument-validation failures to begin with, not +# the richer MaplibError/RuntimeError distinction this exists for. + +.maplibr_error_classes <- c( + "maplibr_maplib_error", + "maplibr_argument_error", + "maplibr_runtime_error" +) + +.reclass_maplibr_error <- function(e) { + msg <- conditionMessage(e) + m <- regmatches(msg, regexec("^\\[(maplibr_[a-z_]+)\\] (.*)$", msg))[[1]] + if (length(m) == 3 && m[2] %in% .maplibr_error_classes) { + cls <- c(m[2], "maplibr_error", "error", "condition") + msg <- m[3] + } else { + cls <- c("maplibr_error", "error", "condition") + } + stop(structure(class = cls, list(message = msg, call = NULL))) +} + +#' Run `expr`, reclassifying any maplibr-tagged error into a proper R +#' condition catchable by class. +#' +#' @param expr An expression to evaluate. +#' @noRd +.rethrow <- function(expr) { + tryCatch(expr, error = function(e) { + if (inherits(e, "maplibr_error")) { + stop(e) + } + .reclass_maplibr_error(e) + }) +} diff --git a/maplibr/R/model-verbs.R b/maplibr/R/model-verbs.R new file mode 100644 index 00000000..948e74c3 --- /dev/null +++ b/maplibr/R/model-verbs.R @@ -0,0 +1,393 @@ +# Pipe-friendly functional wrappers around Model's R6 methods (`model_*()`), +# so `m |> model_query(...)` works as an alternative to `m$query(...)` -- +# both stay available side by side, same convention `arrow` (Table/Dataset +# R6 objects plus dplyr-verb S3 methods) and `torch` (`x$add(y)` alongside +# free-function `torch_add(x, y)`) already use. Named with a `model_` +# prefix rather than bare verbs (`read()`/`write()`/`map()`/`update()`) +# because those would mask `base::write`, `purrr::map`, and the +# `stats::update` S3 generic respectively if this package were attached +# alongside a normal tidyverse session. +# +# Every function here is a one-line passthrough: call the matching `Model` +# method and return exactly what it returns. Mutating `Model` methods +# already return `invisible(self)` (see model.R), so no extra work is +# needed here to make chaining/piping work -- these wrappers don't add +# behavior, only an alternative calling convention. See `?Model` for full +# parameter documentation; the descriptions below are intentionally terse. + +#' Number of triples in a graph +#' +#' Pipe-friendly wrapper for `Model$size()`. +#' @param m A `Model`. +#' @param graph Optional named graph IRI to count (default graph if NULL). +#' @return An integer triple count. +#' @seealso [Model] +#' @export +model_size <- function(m, graph = NULL) { + m$size(graph) +} + +#' Parse RDF triples from a string into a Model +#' +#' Pipe-friendly wrapper for `Model$reads()`. +#' @param m A `Model`. +#' @param s RDF data as a string. +#' @param format One of "ntriples", "turtle", "xml" (rdf/xml). +#' @param graph Optional named graph IRI to read into (default graph if NULL). +#' @return `m`, invisibly. +#' @seealso [Model] +#' @export +model_reads <- function(m, s, format, graph = NULL) { + m$reads(s, format, graph) +} + +#' Parse RDF triples from a file into a Model +#' +#' Pipe-friendly wrapper for `Model$read()`. +#' @param m A `Model`. +#' @param path Path to the RDF file to read. +#' @param format One of "ntriples", "turtle", "xml" (rdf/xml). Guessed from +#' the file extension if NULL. +#' @param graph Optional named graph IRI to read into (default graph if NULL). +#' @return `m`, invisibly. +#' @seealso [Model] +#' @export +model_read <- function(m, path, format = NULL, graph = NULL) { + m$read(path, format, graph) +} + +#' Serialize a Model's triples to a string +#' +#' Pipe-friendly wrapper for `Model$writes()`. +#' @param m A `Model`. +#' @param format One of "ntriples", "turtle", "xml" (rdf/xml). Defaults to "ntriples". +#' @param graph Optional named graph IRI to write (default graph if NULL). +#' @return A string. +#' @seealso [Model] +#' @export +model_writes <- function(m, format = NULL, graph = NULL) { + m$writes(format, graph) +} + +#' Serialize a Model's triples to a file +#' +#' Pipe-friendly wrapper for `Model$write()`. +#' @param m A `Model`. +#' @param path Path to write the RDF file to (overwritten if it exists). +#' @param format One of "ntriples", "turtle", "xml" (rdf/xml). Defaults to "ntriples". +#' @param graph Optional named graph IRI to write (default graph if NULL). +#' @return `m`, invisibly. +#' @seealso [Model] +#' @export +model_write <- function(m, path, format = NULL, graph = NULL) { + m$write(path, format, graph) +} + +#' Build a Model's default indexes +#' +#' Pipe-friendly wrapper for `Model$create_index()`. +#' @param m A `Model`. +#' @return `m`, invisibly. +#' @seealso [Model] +#' @export +model_create_index <- function(m) { + m$create_index() +} + +#' Remove all triples from a graph +#' +#' Pipe-friendly wrapper for `Model$truncate_graph()`. +#' @param m A `Model`. +#' @param graph Optional named graph IRI to truncate (default graph if NULL). +#' @return `m`, invisibly. +#' @seealso [Model] +#' @export +model_truncate_graph <- function(m, graph = NULL) { + m$truncate_graph(graph) +} + +#' Add prefix -> IRI mappings to a Model +#' +#' Pipe-friendly wrapper for `Model$add_prefixes()`. +#' @param m A `Model`. +#' @param prefixes A named R list of single strings: names are prefixes, +#' values are IRIs, e.g. `list(ex = "http://example.org/")`. +#' @return `m`, invisibly. +#' @seealso [Model] +#' @export +model_add_prefixes <- function(m, prefixes) { + m$add_prefixes(prefixes) +} + +#' Copy a graph from another Model into this one +#' +#' Pipe-friendly wrapper for `Model$add_graph()`. +#' @param m A `Model`. +#' @param other Another Model to copy a graph from. +#' @param source_graph Optional named graph IRI in `other` (default graph if NULL). +#' @param target_graph Optional named graph IRI in `m` to copy into (default graph if NULL). +#' @return `m`, invisibly. +#' @seealso [Model] +#' @export +model_add_graph <- function(m, other, source_graph = NULL, target_graph = NULL) { + m$add_graph(other, source_graph, target_graph) +} + +#' Split a graph out of a Model into a new, standalone Model +#' +#' Pipe-friendly wrapper for `Model$detach_graph()`. +#' @param m A `Model`. +#' @param preserve_name Keep the graph's own name in the new Model rather +#' than moving it to the default graph there. +#' @param graph Optional named graph IRI to detach (default graph if NULL). +#' @return A new `Model` containing only the detached graph. +#' @seealso [Model] +#' @export +model_detach_graph <- function(m, preserve_name = FALSE, graph = NULL) { + m$detach_graph(preserve_name, graph) +} + +#' Run a SPARQL SELECT or CONSTRUCT query +#' +#' Pipe-friendly wrapper for `Model$query()`. +#' @param m A `Model`. +#' @param sparql The SPARQL query string. +#' @param graph Optional named graph IRI to restrict the query to (searches +#' across the whole store if NULL). +#' @param include_transient Whether to include transient (e.g. inferred) +#' triples in the query. +#' @param bindings Optional named list of `IRI`/`Literal` objects, pre-binding +#' SPARQL variables to fixed values before the query runs. +#' @return A data.frame. +#' @seealso [Model] +#' @export +model_query <- function(m, sparql, graph = NULL, include_transient = FALSE, bindings = NULL) { + m$query(sparql, graph, include_transient, bindings) +} + +#' Run a SPARQL UPDATE against a Model +#' +#' Pipe-friendly wrapper for `Model$update()`. +#' @param m A `Model`. +#' @param update The SPARQL UPDATE string. +#' @param graph Optional named graph IRI to restrict the update to (no +#' restriction, i.e. the whole store, if NULL). +#' @param include_transient Whether the WHERE clause may match transient +#' (e.g. inferred) triples. +#' @return `m`, invisibly. +#' @seealso [Model] +#' @export +model_update <- function(m, update, graph = NULL, include_transient = FALSE) { + m$update(update, graph, include_transient) +} + +#' Insert a SPARQL CONSTRUCT query's results as new triples +#' +#' Pipe-friendly wrapper for `Model$insert()`. +#' @param m A `Model`. +#' @param query The SPARQL CONSTRUCT query string to source new triples from. +#' @param source_graph Optional named graph IRI to query from (default +#' graph if NULL). +#' @param target_graph Optional named graph IRI to insert into (default +#' graph if NULL). +#' @param include_transient Whether the query may match transient (e.g. +#' inferred) triples in the source graph. +#' @param transient Whether the newly-inserted triples themselves should be +#' marked transient rather than permanent. +#' @return `m`, invisibly. +#' @seealso [Model] +#' @export +model_insert <- function(m, query, source_graph = NULL, target_graph = NULL, + include_transient = FALSE, transient = FALSE) { + m$insert(query, source_graph, target_graph, include_transient, transient) +} + +#' Register a Template on a Model +#' +#' Pipe-friendly wrapper for `Model$add_template()`. +#' @param m A `Model`. +#' @param template A `Template` object, or an stOTTR document string. +#' @return The IRI of the registered template, invisibly. +#' @seealso [Model] +#' @export +model_add_template <- function(m, template) { + m$add_template(template) +} + +#' Expand a template against a data.frame +#' +#' Pipe-friendly wrapper for `Model$map()`. +#' @param m A `Model`. +#' @param template A `Template` object, the IRI of an already-registered +#' template, or an stOTTR document string. +#' @param data Optional data.frame, one row per template instantiation. +#' @param graph Optional named graph IRI to add the resulting triples to +#' (default graph if NULL). +#' @param validate_iris Whether to validate that IRI-typed columns contain +#' valid IRIs. Defaults to TRUE. +#' @return `m`, invisibly. +#' @seealso [Model] +#' @export +model_map <- function(m, template, data = NULL, graph = NULL, validate_iris = NULL) { + m$map(template, data, graph, validate_iris) +} + +#' Map a JSON file straight to triples via a fixed convention +#' +#' Pipe-friendly wrapper for `Model$map_json()`. +#' @param m A `Model`. +#' @param path Path to the JSON file to map. +#' @param graph Optional named graph IRI to add the resulting triples to +#' (default graph if NULL). +#' @param transient Whether the resulting triples should be transient +#' rather than permanent. +#' @param uuid_namespace Optional namespace string for the UUIDv5 blank +#' node IRIs minted for JSON objects/arrays. +#' @return `m`, invisibly. +#' @seealso [Model] +#' @export +model_map_json <- function(m, path, graph = NULL, transient = FALSE, uuid_namespace = NULL) { + m$map_json(path, graph, transient, uuid_namespace) +} + +#' Map a JSON string straight to triples via a fixed convention +#' +#' Pipe-friendly wrapper for `Model$map_json_string()`. +#' @param m A `Model`. +#' @param json The JSON document, as a string. +#' @param graph Optional named graph IRI to add the resulting triples to +#' (default graph if NULL). +#' @param transient Whether the resulting triples should be transient +#' rather than permanent. +#' @param uuid_namespace Optional namespace string for the UUIDv5 blank +#' node IRIs minted for JSON objects/arrays. +#' @return `m`, invisibly. +#' @seealso [Model] +#' @export +model_map_json_string <- function(m, json, graph = NULL, transient = FALSE, uuid_namespace = NULL) { + m$map_json_string(json, graph, transient, uuid_namespace) +} + +#' Map an XML file straight to triples via a fixed convention +#' +#' Pipe-friendly wrapper for `Model$map_xml()`. +#' @param m A `Model`. +#' @param path Path to the XML file to map. +#' @param graph Optional named graph IRI to add the resulting triples to +#' (default graph if NULL). +#' @param transient Whether the resulting triples should be transient +#' rather than permanent. +#' @param uuid_namespace Optional namespace string for the UUIDv5 blank +#' node IRIs minted for XML elements. +#' @return `m`, invisibly. +#' @seealso [Model] +#' @export +model_map_xml <- function(m, path, graph = NULL, transient = FALSE, uuid_namespace = NULL) { + m$map_xml(path, graph, transient, uuid_namespace) +} + +#' Map an XML string straight to triples via a fixed convention +#' +#' Pipe-friendly wrapper for `Model$map_xml_string()`. +#' @param m A `Model`. +#' @param xml The XML document, as a string. +#' @param graph Optional named graph IRI to add the resulting triples to +#' (default graph if NULL). +#' @param transient Whether the resulting triples should be transient +#' rather than permanent. +#' @param uuid_namespace Optional namespace string for the UUIDv5 blank +#' node IRIs minted for XML elements. +#' @return `m`, invisibly. +#' @seealso [Model] +#' @export +model_map_xml_string <- function(m, xml, graph = NULL, transient = FALSE, uuid_namespace = NULL) { + m$map_xml_string(xml, graph, transient, uuid_namespace) +} + +#' Map a data.frame's columns directly to triples, one predicate per column +#' +#' Pipe-friendly wrapper for `Model$map_df()`. +#' @param m A `Model`. +#' @param data A data.frame, one row per subject; every column becomes a +#' predicate with that row's value as the object. +#' @param graph Optional named graph IRI to add the resulting triples to +#' (default graph if NULL). +#' @param uuid_namespace Optional namespace string for the UUIDv5 subject +#' IRIs minted per row. +#' @return `m`, invisibly. +#' @seealso [Model] +#' @export +model_map_df <- function(m, data, graph = NULL, uuid_namespace = NULL) { + m$map_df(data, graph, uuid_namespace) +} + +#' Map a data.frame's subject/predicate/object columns directly to triples +#' +#' Pipe-friendly wrapper for `Model$map_triples()`. +#' @param m A `Model`. +#' @param data A data.frame with subject/predicate/object columns (or just +#' subject/object if `predicate` is given). +#' @param predicate Optional constant predicate IRI to use for every row, +#' instead of a `predicate` column in `data`. +#' @param graph Optional named graph IRI to add the resulting triples to +#' (default graph if NULL). +#' @param validate_iris Whether to validate that IRI-typed columns contain +#' valid IRIs. Defaults to TRUE. +#' @return `m`, invisibly. +#' @seealso [Model] +#' @export +model_map_triples <- function(m, data, predicate = NULL, graph = NULL, validate_iris = NULL) { + m$map_triples(data, predicate, graph, validate_iris) +} + +#' Run RDFS inference over a graph in place +#' +#' Pipe-friendly wrapper for `Model$infer_rdfs()`. +#' @param m A `Model`. +#' @param graph Optional named graph IRI to infer over (default graph if NULL). +#' @return The number of new triples inferred. +#' @seealso [Model] +#' @export +model_infer_rdfs <- function(m, graph = NULL) { + m$infer_rdfs(graph) +} + +#' Compact a Model's on-disk storage +#' +#' Pipe-friendly wrapper for `Model$compact()`. +#' @param m A `Model`. +#' @return `m`, invisibly. +#' @seealso [Model] +#' @export +model_compact <- function(m) { + m$compact() +} + +#' Serialize a Model's triples to maplib's own compact on-disk format +#' +#' Pipe-friendly wrapper for `Model$serialize()`. +#' @param m A `Model`. +#' @param path Directory to serialize into. Defaults to "./serialized_triples". +#' @return `m`, invisibly. +#' @seealso [Model] +#' @export +model_serialize <- function(m, path = NULL) { + m$serialize(path) +} + +#' Load a Model previously written by `model_serialize()`/`Model$serialize()` +#' +#' Pipe-friendly wrapper for `Model$deserialize()`. Unlike the other +#' `model_*()` functions, this one has no `m` to take as a first argument -- +#' it creates one -- so it's a pipe *source*, not something to pipe an +#' existing Model into: `model_deserialize(path) |> model_query(...)`. +#' @param path Directory previously written by serialize(). Defaults to +#' "./serialized_triples". +#' @param storage_folder Optional folder for on-disk (rather than +#' in-memory) triplestore storage. +#' @return A new `Model`. +#' @seealso [Model] +#' @export +model_deserialize <- function(path = NULL, storage_folder = NULL) { + Model$deserialize(path, storage_folder) +} diff --git a/maplibr/R/model.R b/maplibr/R/model.R new file mode 100644 index 00000000..5774b6fe --- /dev/null +++ b/maplibr/R/model.R @@ -0,0 +1,471 @@ +#' A maplib knowledge graph model +#' +#' `Model` is the user-facing wrapper around the underlying `RModel` savvy +#' object. It mirrors py_maplib's `Model` class: methods mutate the +#' underlying graph in place and are chainable (each mutating method +#' returns `self` invisibly). +#' +#' @export +Model <- R6::R6Class( + "Model", + # R6's default clone() is a shallow copy: it would copy the R-level + # reference to `private$rmodel` (a savvy external pointer wrapping a Rust + # `Mutex`), not the underlying Rust state itself -- + # so `m2 <- m$clone()` would look like an independent copy while actually + # sharing the same mutex-guarded triplestore, and mutating one would + # silently mutate the other with no error or warning. There's no cheap + # way to deep-copy the underlying Rust Model (that would need a real + # serialize/deserialize round trip, which itself isn't available for an + # in-memory Model -- see `$serialize()`). Disabling clone() entirely is + # safer than a misleading shallow one; `$detach_graph()` is the supported + # way to get an independent Model. + cloneable = FALSE, + public = list( + #' @description + #' Create a new, empty Model. Not used to wrap an existing `RModel` + #' (see `Model$deserialize()` and `$detach_graph()` for that) -- `rmodel` + #' is for internal use only. + #' @param rmodel Internal use only. + initialize = function(rmodel = NULL) { + if (is.null(rmodel)) { + rmodel <- .rethrow(RModel$new()) + } else if (!inherits(rmodel, "RModel")) { + stop("`rmodel` must be an RModel object", call. = FALSE) + } + private$rmodel <- rmodel + }, + + #' @description Number of triples in a graph. + #' @param graph Optional named graph IRI to count (default graph if NULL). + size = function(graph = NULL) { + .rethrow(private$rmodel$size(graph)) + }, + + #' @description Parse RDF triples from a string into this Model. + #' @param s RDF data as a string. + #' @param format One of "ntriples", "turtle", "xml" (rdf/xml). + #' @param graph Optional named graph IRI to read into (default graph if NULL). + reads = function(s, format, graph = NULL) { + .rethrow(private$rmodel$reads(s, format, graph)) + invisible(self) + }, + + #' @description Parse RDF triples from a file into this Model. + #' @param path Path to the RDF file to read. + #' @param format One of "ntriples", "turtle", "xml" (rdf/xml). Guessed + #' from the file extension if NULL (.ttl -> turtle, .nt -> ntriples, + #' .xml/.rdf -> xml) -- any other or missing extension raises a normal + #' error, so pass `format` explicitly for those. + #' @param graph Optional named graph IRI to read into (default graph if NULL). + read = function(path, format = NULL, graph = NULL) { + .rethrow(private$rmodel$read(path, format, graph)) + invisible(self) + }, + + #' @description Serialize this Model's triples to a string. + #' @param format One of "ntriples", "turtle", "xml" (rdf/xml). Defaults to "ntriples". + #' @param graph Optional named graph IRI to write (default graph if NULL). + writes = function(format = NULL, graph = NULL) { + .rethrow(private$rmodel$writes(format, graph)) + }, + + #' @description Serialize this Model's triples to a file. + #' @param path Path to write the RDF file to (overwritten if it exists). + #' @param format One of "ntriples", "turtle", "xml" (rdf/xml). Defaults to "ntriples". + #' @param graph Optional named graph IRI to write (default graph if NULL). + write = function(path, format = NULL, graph = NULL) { + .rethrow(private$rmodel$write(path, format, graph)) + invisible(self) + }, + + #' @description Build the default (non-FTS) indexes. + create_index = function() { + .rethrow(private$rmodel$create_index()) + invisible(self) + }, + + #' @description Remove all triples from a graph. + #' @param graph Optional named graph IRI to truncate (default graph if NULL). + truncate_graph = function(graph = NULL) { + .rethrow(private$rmodel$truncate_graph(graph)) + invisible(self) + }, + + #' @description Add prefix -> IRI mappings, used when serializing. + #' @param prefixes A named R list of single strings: names are prefixes, + #' values are IRIs, e.g. `list(ex = "http://example.org/")`. + add_prefixes = function(prefixes) { + .rethrow(private$rmodel$add_prefixes(prefixes)) + invisible(self) + }, + + #' @description Copy a graph from another Model's triplestore into this one. + #' @param other Another Model to copy a graph from. + #' @param source_graph Optional named graph IRI in `other` (default graph if NULL). + #' @param target_graph Optional named graph IRI in this Model to copy into (default graph if NULL). + add_graph = function(other, source_graph = NULL, target_graph = NULL) { + if (!inherits(other, "Model")) { + stop("`other` must be a Model object", call. = FALSE) + } + other_rmodel <- private$as_rmodel(other) + # The underlying Rust call locks `other` then `self` in fixed order, + # so if both wrap the same Mutex (`other` is `self`, or an aliased + # Model sharing its underlying pointer -- clone() is disabled, but + # two Model$new() calls can still be handed the same RModel), it + # locks the same non-reentrant Mutex twice on one thread and + # deadlocks the R session permanently. Compared via the raw external + # pointer (`.ptr`), not `identical(self, other)`, so this also + # catches two distinct Model wrapper objects that happen to share + # one underlying RModel. + if (identical(private$rmodel$.ptr, other_rmodel$.ptr)) { + stop( + "`other` must not be the same underlying Model as `self` -- add_graph(m, m) would deadlock (self and other share one lock)", + call. = FALSE + ) + } + .rethrow(private$rmodel$add_graph(other_rmodel, source_graph, target_graph)) + invisible(self) + }, + + #' @description Split a graph out of this Model into a brand new, standalone Model. + #' @param preserve_name Keep the graph's own name in the new Model rather + #' than moving it to the default graph there. + #' @param graph Optional named graph IRI to detach (default graph if NULL). + #' @return A new Model containing only the detached graph. + detach_graph = function(preserve_name = FALSE, graph = NULL) { + Model$new(.rethrow(private$rmodel$detach_graph(preserve_name, graph))) + }, + + #' @description Run a SPARQL SELECT or CONSTRUCT query and return the + #' result as a data.frame. Mirrors py_maplib's Model.query() + #' (py_maplib/src/py_model.rs:344-387). For CONSTRUCT, every matched + #' triple pattern's results are concatenated into one combined + #' subject/predicate/object data.frame (py_maplib instead returns one + #' data.frame per pattern, as a Python list -- not done here since a + #' single query() call always returns exactly one data.frame). Subject + #' and object values are always returned as plain strings for CONSTRUCT + #' results (unlike SELECT, which preserves each column's native type + #' where a single type applies) -- different patterns can produce + #' different column types, which a single concatenated data.frame can't + #' represent without collapsing to a common type regardless. + #' @param sparql The SPARQL query string. + #' @param graph Optional named graph IRI to restrict the query to (searches + #' across the whole store if NULL). + #' @param include_transient Whether to include transient (e.g. inferred) + #' triples in the query. + #' @param bindings Optional named list of `IRI`/`Literal` objects + #' (see `terms.R`) -- pre-binds each named SPARQL variable to a fixed + #' value before the query runs, e.g. `list(b = IRI("http://ex/a"))` + #' binds `?b`. A bound variable can only be used inside the query body + #' (e.g. a `FILTER`), not listed directly in `SELECT` -- binding a + #' variable that's also a `SELECT`-ed output column is a normal error + #' (`SELECT *` still works, since it doesn't name variables itself). + #' @return A data.frame. A column whose values are always the same RDF + #' node type (the common case) comes back as a plain vector of that + #' type's underlying value (an IRI/blank node id/literal value as a + #' plain string, exactly as py_maplib returns by default). A column + #' whose RDF node type can vary by row instead comes back from Arrow as + #' one sub-column per possible type (only one of which is non-NA on any + #' given row) -- collapsed here into a single vector by taking + #' whichever sub-column is non-NA per row. The precise per-column RDF + #' node type information (which py_maplib exposes via + #' SolutionMappings.rdf_types) is attached as the data.frame's + #' `"rdf_node_types"` attribute instead of a richer typed object, parsed + #' from the underlying JSON side-channel (maplibr/src/rust/src/ + #' arrow_bridge.rs) -- a full RDFType-based port of that accessor is + #' left for later. + query = function(sparql, graph = NULL, include_transient = FALSE, bindings = NULL) { + stream <- nanoarrow::nanoarrow_allocate_array_stream() + raw_bindings <- if (!is.null(bindings)) { + lapply(bindings, function(x) .savvy_extract_ptr(.to_ground_term(x), "maplibr::RGroundTerm")) + } else { + NULL + } + rdf_node_types_json <- .rethrow( + private$rmodel$query(sparql, stream, include_transient, graph, raw_bindings) + ) + df <- as.data.frame(stream) + df <- .collapse_multitype_columns(df) + attr(df, "rdf_node_types") <- jsonlite::fromJSON(rdf_node_types_json, simplifyVector = FALSE) + df + }, + + #' @description Run a SPARQL UPDATE (modify-style INSERT/DELETE ... WHERE) + #' against this Model in place. + #' @param update The SPARQL UPDATE string. Must be a DELETE/INSERT/WHERE + #' form (`INSERT { ... } WHERE { ... }`, optionally with a DELETE + #' clause too, and an empty `WHERE {}` when there's nothing to match + #' against) -- the bare `INSERT DATA { ... }`/`DELETE DATA { ... }` + #' forms are unimplemented in the core engine and panic if used (not + #' fixable in this package -- see maplib-cai upstream issue). + #' @param graph Optional named graph IRI to restrict the update to + #' (no restriction, i.e. the whole store, if NULL -- same "missing + #' means unrestricted" semantics as `$query()`'s `graph` argument, not + #' `$reads()`/`$writes()`'s "missing means the default graph"). + #' @param include_transient Whether the WHERE clause may match transient + #' (e.g. inferred) triples. + update = function(update, graph = NULL, include_transient = FALSE) { + .rethrow(private$rmodel$update(update, include_transient, graph)) + invisible(self) + }, + + #' @description Run a SPARQL CONSTRUCT query and insert its results as + #' new triples into a (possibly different) graph. + #' @param query The SPARQL CONSTRUCT query string to source new triples + #' from (a SELECT query errors -- INSERT needs a triple-shaped result). + #' @param source_graph Optional named graph IRI to query from (default + #' graph if NULL). + #' @param target_graph Optional named graph IRI to insert into (default + #' graph if NULL). + #' @param include_transient Whether the query may match transient (e.g. + #' inferred) triples in the source graph. + #' @param transient Whether the newly-inserted triples themselves should + #' be marked transient rather than permanent. + insert = function(query, source_graph = NULL, target_graph = NULL, + include_transient = FALSE, transient = FALSE) { + .rethrow(private$rmodel$insert(query, include_transient, transient, source_graph, target_graph)) + invisible(self) + }, + + #' @description Register a Template so it can be referenced by IRI from + #' `$map()`. + #' @param template A `Template` (see `templates.R`), or an stOTTR document + #' string defining one or more templates. + #' @return The IRI of the registered template, invisibly (for a `Template` + #' object, this is just `template@iri`; for a document string, it's the + #' IRI of the first template the document defines). + add_template = function(template) { + invisible(private$.resolve_template_iri(template)) + }, + + #' @description Expand a template against a data.frame, adding the + #' resulting triples to this Model. Mirrors py_maplib's `Model.map()` + #' (py_maplib/src/py_model.rs:118-142). + #' @param template A `Template` object, the IRI of an already-registered + #' template (`$add_template()`), or an stOTTR document string (defining + #' exactly the one template to expand -- registered as a side effect). + #' @param data Optional data.frame, one row per template instantiation. If + #' NULL (or zero rows), the template is expanded once against no + #' variables -- only valid for templates whose instances are entirely + #' constant terms. + #' @param graph Optional named graph IRI to add the resulting triples to + #' (default graph if NULL). + #' @param validate_iris Whether to validate that IRI-typed columns contain + #' valid IRIs. Defaults to TRUE. + map = function(template, data = NULL, graph = NULL, validate_iris = NULL) { + template_iri <- private$.resolve_template_iri(template) + if (is.null(data)) { + .rethrow(private$rmodel$map_no_data(template_iri, graph, validate_iris)) + } else { + # A zero-row data.frame is a silent no-op (RModel$map(), mirroring + # py_maplib's map_mutex) -- distinct from `data = NULL`, which + # expands the template once against no variables at all. + stream <- nanoarrow::as_nanoarrow_array_stream(data) + .rethrow(private$rmodel$map(template_iri, stream, graph, validate_iris)) + } + invisible(self) + }, + + #' @description Map a JSON file straight to triples via a fixed + #' convention (Facade-X; no template involved). + #' @param path Path to the JSON file to map. + #' @param graph Optional named graph IRI to add the resulting triples to + #' (default graph if NULL). + #' @param transient Whether the resulting triples should be transient + #' rather than permanent. + #' @param uuid_namespace Optional namespace string for the UUIDv5 blank + #' node IRIs minted for JSON objects/arrays. Defaults to the path. + map_json = function(path, graph = NULL, transient = FALSE, uuid_namespace = NULL) { + .rethrow(private$rmodel$map_json(path, transient, graph, uuid_namespace)) + invisible(self) + }, + + #' @description Map a JSON string straight to triples via a fixed + #' convention. See `$map_json()` for the file-based equivalent. + #' @param json The JSON document, as a string. + #' @param graph Optional named graph IRI to add the resulting triples to + #' (default graph if NULL). + #' @param transient Whether the resulting triples should be transient + #' rather than permanent. + #' @param uuid_namespace Optional namespace string for the UUIDv5 blank + #' node IRIs minted for JSON objects/arrays. Defaults to a random UUID + #' if not given. + map_json_string = function(json, graph = NULL, transient = FALSE, uuid_namespace = NULL) { + .rethrow(private$rmodel$map_json_string(json, transient, graph, uuid_namespace)) + invisible(self) + }, + + #' @description Map an XML file straight to triples via a fixed + #' convention. + #' @param path Path to the XML file to map. + #' @param graph Optional named graph IRI to add the resulting triples to + #' (default graph if NULL). + #' @param transient Whether the resulting triples should be transient + #' rather than permanent. + #' @param uuid_namespace Optional namespace string for the UUIDv5 blank + #' node IRIs minted for XML elements. Defaults to the path. + map_xml = function(path, graph = NULL, transient = FALSE, uuid_namespace = NULL) { + .rethrow(private$rmodel$map_xml(path, transient, graph, uuid_namespace)) + invisible(self) + }, + + #' @description Map an XML string straight to triples via a fixed + #' convention. See `$map_xml()` for the file-based equivalent. + #' @param xml The XML document, as a string. + #' @param graph Optional named graph IRI to add the resulting triples to + #' (default graph if NULL). + #' @param transient Whether the resulting triples should be transient + #' rather than permanent. + #' @param uuid_namespace Optional namespace string for the UUIDv5 blank + #' node IRIs minted for XML elements. Defaults to a random UUID if not + #' given. + map_xml_string = function(xml, graph = NULL, transient = FALSE, uuid_namespace = NULL) { + .rethrow(private$rmodel$map_xml_string(xml, transient, graph, uuid_namespace)) + invisible(self) + }, + + #' @description Map a data.frame's columns directly to + #' subject/predicate/object triples -- one column per predicate, a fresh + #' IRI subject per row, no OTTR template involved. + #' @param data A data.frame, one row per subject; every column becomes a + #' predicate (named by the column name) with that row's value as the + #' object. A zero-row data.frame is NOT a no-op here (unlike `$map()`): + #' a single root `rdf:type` triple is still added, matching the + #' underlying engine call. + #' @param graph Optional named graph IRI to add the resulting triples to + #' (default graph if NULL). + #' @param uuid_namespace Optional namespace string for the UUIDv5 subject + #' IRIs minted per row. Defaults to a random UUID if not given. + map_df = function(data, graph = NULL, uuid_namespace = NULL) { + stream <- nanoarrow::as_nanoarrow_array_stream(data) + .rethrow(private$rmodel$map_df(stream, graph, uuid_namespace)) + invisible(self) + }, + + #' @description Map a data.frame's columns directly to + #' subject/predicate/object triples using the built-in `ottr:Triple` + #' template, optionally fixing every row's predicate to a single + #' constant IRI instead of reading it from a `predicate` column. + #' @param data A data.frame with subject/predicate/object columns (or + #' just subject/object if `predicate` is given). A zero-row data.frame + #' is NOT a no-op: missing/misnamed columns still raise a normal error + #' regardless of row count. + #' @param predicate Optional constant predicate IRI to use for every + #' row, instead of a `predicate` column in `data`. + #' @param graph Optional named graph IRI to add the resulting triples to + #' (default graph if NULL). + #' @param validate_iris Whether to validate that IRI-typed columns + #' contain valid IRIs. Defaults to TRUE. + map_triples = function(data, predicate = NULL, graph = NULL, validate_iris = NULL) { + stream <- nanoarrow::as_nanoarrow_array_stream(data) + .rethrow(private$rmodel$map_triples(stream, predicate, graph, validate_iris)) + invisible(self) + }, + + #' @description Run RDFS inference over a graph in place. + #' @param graph Optional named graph IRI to infer over (default graph if NULL). + #' @return The number of new triples inferred (a triple count, not a rule + #' count -- Triplestore::interesting_rdfs_rules sums per-rule + #' inserted-triple counts). + infer_rdfs = function(graph = NULL) { + .rethrow(private$rmodel$infer_rdfs(graph)) + }, + + #' @description Compact on-disk storage. + compact = function() { + .rethrow(private$rmodel$compact()) + invisible(self) + }, + + #' @description Serialize this Model's triples to a directory in + #' maplib's own compact on-disk format (not an RDF interchange format -- + #' use `$writes()` for that). + #' @param path Directory to serialize into. Defaults to + #' "./serialized_triples". + serialize = function(path = NULL) { + .rethrow(private$rmodel$serialize(path)) + invisible(self) + }, + + #' @description Print a short summary of this Model. + #' @param ... Unused. + print = function(...) { + cat("", private$rmodel$size(NULL), "triples (default graph)\n") + invisible(self) + } + ), + private = list( + rmodel = NULL, + as_rmodel = function(model) { + model$.__enclos_env__$private$rmodel + }, + # Resolves `template` (a Template object, a template IRI, or an stOTTR + # document string) to a template IRI, registering it first if needed. + # A Template object or document string is always (re-)registered on + # every call -- mirrors py_maplib's own map_mutex, which re-registers a + # PyTemplate/doc-string `template` argument on every map() call too + # (py_maplib/src/mutexes.rs:120-153), rather than requiring a separate + # add_template() call first. + .resolve_template_iri = function(template) { + if (S7::S7_inherits(template, Template)) { + .rethrow(private$rmodel$add_template(template@raw)) + template@iri@iri + } else if (is.character(template) && length(template) == 1) { + # Discriminates a doc string from a bare IRI by the presence of a + # "{" instance-block brace: every stOTTR document has one (its + # `[params] :: { ... }` body), and "{" is never a legal IRI + # character (RFC 3987), so a real template IRI can never contain + # one -- unlike an earlier "::" substring check (roborev job 22), + # which would misclassify a bare IRI containing "::" (e.g. some URN + # forms) as a doc string. + if (grepl("{", template, fixed = TRUE)) { + .rethrow(private$rmodel$add_template_string(template)) + } else { + template + } + } else { + stop( + "`template` must be a Template, a template IRI, or an stOTTR document string", + call. = FALSE + ) + } + } + ) +) + +# A column whose RDF node type varies by row comes back from +# as.data.frame(stream) as a nested data.frame -- one sub-column per +# possible type (e.g. "I" for IRI, "B" for blank node, the datatype IRI for +# a literal; see MULTI_IRI_DT/MULTI_BLANK_DT, lib/representation/src/ +# multitype.rs:11-13), with exactly one non-NA per row. Collapses each such +# column into a single vector by coalescing across its sub-columns via +# ifelse(), which is safe today only because every sub-column is plain +# character (IRI/blank-node id/literal all surface as strings via +# format_native_columns) -- ifelse() silently coerces on a type mismatch, so +# if a future multi-typed column ever carried a non-character sub-column +# (e.g. a natively-typed numeric literal), values could be silently +# mangled. Revisit with an explicit type check (or vctrs::vec_coalesce) if +# native-typed literal columns are ever added. +.collapse_multitype_columns <- function(df) { + for (col in names(df)) { + if (is.data.frame(df[[col]])) { + sub <- df[[col]] + collapsed <- sub[[1]] + if (ncol(sub) > 1) { + for (j in 2:ncol(sub)) { + collapsed <- ifelse(is.na(collapsed), sub[[j]], collapsed) + } + } + df[[col]] <- collapsed + } + } + df +} + +# Load a Model previously written by `$serialize()`. Attached directly to +# the (already @export'd) Model generator, so users call it as +# `Model$deserialize(path)` -- mirrors RModel's own associated-function +# pattern in 000-wrappers.R. +Model$deserialize <- function(path = NULL, storage_folder = NULL) { + Model$new(.rethrow(RModel$deserialize(path, storage_folder))) +} diff --git a/maplibr/R/rdf_type.R b/maplibr/R/rdf_type.R new file mode 100644 index 00000000..70ba5897 --- /dev/null +++ b/maplibr/R/rdf_type.R @@ -0,0 +1,105 @@ +# RDFType (maplib-snz, continued). Mirrors py_maplib's `RDFType` +# (lib/representation/src/python.rs:101-268), used as a Parameter's +# `rdf_type` -- but only its "flat" cases: IRI/BlankNode/Literal/Unknown/ +# Multi. All of these ultimately collapse to a single basic-type IRI once +# they reach the Rust side (templates::ast::PType::from(&RDFNodeState), +# lib/templates/src/ast.rs:211-227): IRI/BlankNode/Literal keep their own +# type IRI, Unknown has no type constraint, and Multi (a parameter that can +# hold more than one RDF node type) collapses all the way down to +# rdfs:Resource, the OTTR top type -- there is no richer "one-of these +# specific types" representation to preserve on the Rust side. Hence +# `RDFType` here is intentionally just a wrapper around that one resulting +# IRI (or NULL for Unknown), not a structured value. +# +# Built via plain functions (rdf_type_iri()/rdf_type_blank_node()/...), not +# `RDFType$IRI()`-style "static methods" the way Model$deserialize works -- +# S7 class generators refuse `$<-` assignment (`Can't set S7 properties with +# $`), unlike R6 generators or savvy's plain-environment classes, so that +# pattern isn't available here. +# +# `RDFType.Nested(...)` is NOT ported -- it represents a parameter whose +# column itself holds nested/list values (templates::MappingColumnType:: +# Nested / PType::List), which needs Parameter's Rust constructor to accept +# a richer type than a single basic-type IRI string. No current consumer +# needs it yet (Model$map() itself isn't ported), so it's left for +# follow-up work alongside the rest of the RDFType port. + +#' An RDF node type, for use as a Parameter's `rdf_type`. +#' +#' Mirrors py_maplib's `RDFType` (lib/representation/src/python.rs:101-268). +#' Only represents a single basic type -- see the note in +#' `maplibr/R/rdf_type.R` for why `rdf_type_multi()` collapses multiple types +#' down to one IRI (`rdfs:Resource`) rather than preserving the set, and why +#' `RDFType.Nested()` isn't ported. Build one with `rdf_type_iri()`, +#' `rdf_type_blank_node()`, `rdf_type_literal()`, `rdf_type_unknown()`, or +#' `rdf_type_multi()`, not by calling `RDFType()` directly. +#' +#' @param basic_type_iri The resolved basic-type IRI, or NULL for +#' `rdf_type_unknown()` (no type constraint). +#' @export +RDFType <- S7::new_class( + "RDFType", + package = "maplibr", + properties = list(basic_type_iri = S7::new_union(IRI, NULL)) +) + +#' @export +S7::method(format, RDFType) <- function(x, ...) { + if (is.null(x@basic_type_iri)) { + "RDFType.Unknown()" + } else { + paste0("RDFType(", x@basic_type_iri@iri, ")") + } +} + +#' @export +S7::method(print, RDFType) <- function(x, ...) { + cat(format(x), "\n", sep = "") + invisible(x) +} + +#' @rdname RDFType +#' @export +rdf_type_iri <- function() { + RDFType(basic_type_iri = IRI("http://ns.ottr.xyz/0.4/IRI")) +} + +#' @rdname RDFType +#' @export +rdf_type_blank_node <- function() { + RDFType(basic_type_iri = IRI("http://ns.ottr.xyz/0.4/BlankNode")) +} + +#' @param iri_or_datatype An IRI (the literal's datatype), or a plain IRI string. +#' @rdname RDFType +#' @export +rdf_type_literal <- function(iri_or_datatype) { + iri <- if (S7::S7_inherits(iri_or_datatype, IRI)) iri_or_datatype else IRI(iri_or_datatype) + RDFType(basic_type_iri = iri) +} + +#' @rdname RDFType +#' @export +rdf_type_unknown <- function() { + RDFType(basic_type_iri = NULL) +} + +#' @param rdf_types A list of RDFTypes. +#' @rdname RDFType +#' @export +rdf_type_multi <- function(rdf_types) { + stopifnot(all(vapply(rdf_types, function(t) S7::S7_inherits(t, RDFType), logical(1)))) + RDFType(basic_type_iri = rdfs$Resource) +} + +.as_rdf_type_iri <- function(x) { + if (is.null(x)) { + NULL + } else if (S7::S7_inherits(x, IRI)) { + x@iri + } else if (S7::S7_inherits(x, RDFType)) { + if (is.null(x@basic_type_iri)) NULL else x@basic_type_iri@iri + } else { + stop("`rdf_type` must be an IRI, an RDFType, or NULL", call. = FALSE) + } +} diff --git a/maplibr/R/templates.R b/maplibr/R/templates.R new file mode 100644 index 00000000..d4e0ca5f --- /dev/null +++ b/maplibr/R/templates.R @@ -0,0 +1,205 @@ +# OTTR template/instance/parameter/argument value classes (maplib-snz, +# continued). Unlike the pure-R term classes in terms.R, these need real +# Rust-side construction -- they become part of a templates::ast::Template +# eventually passed to Model$map() -- so, like Model/RModel, there is a thin +# S7 wrapper here over a raw savvy object (RConstantTerm/RArgument/ +# RParameter/RInstance/RTemplate, maplibr/src/rust/src/templates.rs). Still +# S7, not R6: built once, never mutated in place. + +.as_raw_constant_term <- function(x) { + if (is.null(x)) { + RConstantTerm$none() + } else if (S7::S7_inherits(x, IRI)) { + RConstantTerm$iri(x@iri) + } else if (S7::S7_inherits(x, BlankNode)) { + RConstantTerm$blank_node(x@name) + } else if (S7::S7_inherits(x, Literal)) { + RConstantTerm$literal(x@value, x@datatype@iri, x@language) + } else { + stop("expected an IRI, BlankNode, Literal, or NULL", call. = FALSE) + } +} + +#' An OTTR template argument: a Variable, or a constant term. +#' +#' Mirrors py_maplib's `Argument` (lib/templates/src/python.rs:203-236). +#' Passing an existing Argument as `term` returns it unchanged (`list_expand` +#' is ignored in that case), matching the Python constructor's behavior. +#' +#' @param term A Variable, IRI, BlankNode, Literal, NULL, or an existing Argument. +#' @param list_expand Whether this argument should be OTTR list-expanded. +#' @export +Argument <- S7::new_class( + "Argument", + package = "maplibr", + properties = list(raw = S7::class_any), + constructor = function(term, list_expand = FALSE) { + if (S7::S7_inherits(term, Argument)) { + return(term) + } + raw <- if (S7::S7_inherits(term, Variable)) { + RArgument$from_variable(term@name, list_expand) + } else { + RArgument$from_constant_term(.as_raw_constant_term(term), list_expand) + } + S7::new_object(S7::S7_object(), raw = raw) + } +) + +.as_argument_ptr <- function(x) { + .savvy_extract_ptr(Argument(x)@raw, "maplibr::RArgument") +} + +#' An OTTR template parameter. +#' +#' Mirrors py_maplib's `Parameter` (lib/templates/src/python.rs:21-167). +#' `rdf_type` only supports a basic (non-nested, non-list) RDF type -- +#' `RDFType$Nested()` is not ported (see `maplibr/R/rdf_type.R`). +#' +#' @param variable A Variable. +#' @param optional Whether this parameter is optional. +#' @param allow_blank Whether a blank node is an acceptable value (default TRUE). +#' @param rdf_type Optional: an IRI (a datatype/class IRI directly) or an +#' RDFType, giving the parameter's expected RDF type. +#' @param default_value Optional default value: an IRI, BlankNode, or Literal. +#' @export +Parameter <- S7::new_class( + "Parameter", + package = "maplibr", + properties = list(raw = S7::class_any, variable = Variable), + constructor = function(variable, + optional = FALSE, + allow_blank = TRUE, + rdf_type = NULL, + default_value = NULL) { + stopifnot(S7::S7_inherits(variable, Variable)) + rdf_type_iri <- .as_rdf_type_iri(rdf_type) + default_raw <- if (is.null(default_value)) { + NULL + } else { + .as_raw_constant_term(default_value) + } + raw <- RParameter$new(variable@name, optional, allow_blank, rdf_type_iri, default_raw) + S7::new_object(S7::S7_object(), raw = raw, variable = variable) + } +) + +.as_parameter_ptr <- function(x) { + p <- if (S7::S7_inherits(x, Parameter)) x else Parameter(x) + .savvy_extract_ptr(p@raw, "maplibr::RParameter") +} + +#' An OTTR template instance: a call to a template with concrete arguments. +#' +#' Mirrors py_maplib's `Instance` (lib/templates/src/python.rs:245-288). +#' +#' @param template_iri An IRI: the template being instantiated. +#' @param arguments A list of Arguments (or bare Variable/IRI/BlankNode/ +#' Literal/NULL terms, auto-wrapped as non-list-expanded Arguments). +#' @param list_expander Optional: one of "cross", "zipMin", "zipMax". +#' @export +Instance <- S7::new_class( + "Instance", + package = "maplibr", + properties = list(raw = S7::class_any), + constructor = function(template_iri, arguments = NULL, list_expander = NULL) { + stopifnot(S7::S7_inherits(template_iri, IRI)) + arg_ptrs <- lapply(arguments, .as_argument_ptr) + raw <- RInstance$new(template_iri@iri, arg_ptrs, list_expander) + S7::new_object(S7::S7_object(), raw = raw) + } +) + +# Wraps an already-built RInstance (from Template$instance()/make_triple(), +# which validate their own arguments) as an Instance, without exposing a +# validation-bypassing raw-pointer parameter on Instance()'s own public +# constructor. S7's new_object() can only be called from within the literal +# function registered as some class's constructor -- not from an arbitrary +# unexported helper, even one Instance()'s own constructor delegates to -- +# so this is a genuine subclass with its own constructor, not just a thin +# wrapper. Its instances still satisfy S7_inherits(x, Instance) (and base +# inherits()), since S7 subclass instances carry their full ancestor class +# vector, so every existing Instance check downstream is unaffected. +.RawInstance <- S7::new_class( + "RawInstance", + package = "maplibr", + parent = Instance, + constructor = function(raw) S7::new_object(S7::S7_object(), raw = raw) +) + +#' The IRI of the template an Instance calls. +#' +#' @param x An Instance. +#' @export +instance_template_iri <- S7::new_generic("instance_template_iri", "x") + +#' @export +S7::method(instance_template_iri, Instance) <- function(x) IRI(x@raw$iri()) + +#' An OTTR template: a named, parameterized set of instance patterns. +#' +#' Mirrors py_maplib's `Template` (lib/templates/src/python.rs:291-402). +#' +#' @param iri An IRI. +#' @param parameters A list of Parameters (or bare Variables, auto-wrapped as +#' required, blank-allowed Parameters with no type constraint). +#' @param instances A list of Instances. +#' @export +Template <- S7::new_class( + "Template", + package = "maplibr", + properties = list(raw = S7::class_any, iri = IRI), + constructor = function(iri, parameters, instances) { + stopifnot(S7::S7_inherits(iri, IRI)) + param_ptrs <- lapply(parameters, .as_parameter_ptr) + stopifnot(all(vapply(instances, function(i) S7::S7_inherits(i, Instance), logical(1)))) + instance_ptrs <- lapply(instances, function(i) .savvy_extract_ptr(i@raw, "maplibr::RInstance")) + raw <- RTemplate$new(iri@iri, param_ptrs, instance_ptrs) + S7::new_object(S7::S7_object(), raw = raw, iri = iri) + } +) + +#' @export +S7::method(format, Template) <- function(x, ...) x@raw$print_string() + +#' @export +S7::method(print, Template) <- function(x, ...) { + cat(format(x), "\n", sep = "") + invisible(x) +} + +#' Build a new Instance calling a Template with concrete arguments. +#' +#' Mirrors py_maplib's `Template.instance()` (lib/templates/src/python.rs:335-346). +#' +#' @param template A Template. +#' @param arguments A list of Arguments (or bare terms, auto-wrapped). +#' @param list_expander Optional: one of "cross", "zipMin", "zipMax". +#' @return A new Instance. +#' @export +instantiate <- S7::new_generic("instantiate", "template") + +#' @export +S7::method(instantiate, Template) <- function(template, arguments, list_expander = NULL) { + arg_ptrs <- lapply(arguments, .as_argument_ptr) + raw <- template@raw$instance(arg_ptrs, list_expander) + .RawInstance(raw) +} + +#' Build an rdf:type-style Triple instance. +#' +#' Mirrors py_maplib's `Triple()` helper (lib/templates/src/python.rs:404-417). +#' +#' @param subject Argument-compatible term. +#' @param predicate Argument-compatible term. +#' @param object Argument-compatible term. +#' @param list_expander Optional: one of "cross", "zipMin", "zipMax". +#' @return A new Instance. +#' @export +Triple <- function(subject, predicate, object, list_expander = NULL) { + s <- Argument(subject) + p <- Argument(predicate) + o <- Argument(object) + raw <- make_triple(s@raw, p@raw, o@raw, list_expander) + .RawInstance(raw) +} diff --git a/maplibr/R/terms.R b/maplibr/R/terms.R new file mode 100644 index 00000000..5bd899b8 --- /dev/null +++ b/maplibr/R/terms.R @@ -0,0 +1,231 @@ +# RDF term value classes (maplib-snz). S7, not R6: these are built once and +# passed into Model$map()/reads() etc., never mutated in place -- see +# CLAUDE.md's "R-side OOP split" note. Mirrors py_maplib's PyIRI/PyPrefix/ +# PyVariable/PyLiteral/PyBlankNode (lib/representation/src/python.rs). +# +# Syntax validation (IRI/variable-name/blank-node-id grammar) is delegated to +# the same oxrdf parsers py_maplib uses, via validate_iri()/ +# validate_variable_name()/validate_blank_node_id() (maplibr/src/rust/src/terms.rs) +# -- not reimplemented as R regexes, to avoid drifting from oxrdf's grammar. + +#' An RDF IRI (Internationalized Resource Identifier) +#' +#' @param iri The IRI string. +#' @export +IRI <- S7::new_class( + "IRI", + package = "maplibr", + properties = list(iri = S7::class_character), + validator = function(self) { + if (length(self@iri) != 1 || is.na(self@iri)) { + return("@iri must be a single, non-NA string") + } + tryCatch( + { + validate_iri(self@iri) + NULL + }, + error = function(e) conditionMessage(e) + ) + } +) + +#' @export +S7::method(format, IRI) <- function(x, ...) paste0("IRI(", x@iri, ")") + +#' @export +S7::method(print, IRI) <- function(x, ...) { + cat(format(x), "\n", sep = "") + invisible(x) +} + +#' An RDF namespace prefix: a short name bound to an IRI. +#' +#' @param iri The IRI the prefix expands to. +#' @param prefix_name Optional short name for the prefix (e.g. "ex"). +#' @export +Prefix <- S7::new_class( + "Prefix", + package = "maplibr", + properties = list( + iri = S7::class_character, + prefix_name = S7::new_union(S7::class_character, NULL) + ), + validator = function(self) { + if (length(self@iri) != 1 || is.na(self@iri)) { + return("@iri must be a single, non-NA string") + } + tryCatch( + { + validate_iri(self@iri) + NULL + }, + error = function(e) conditionMessage(e) + ) + } +) + +#' Suffix a Prefix's IRI, returning a new IRI. +#' +#' Mirrors py_maplib's `Prefix.suf()` (lib/representation/src/python.rs:326-328). +#' +#' @param prefix A Prefix. +#' @param suffix String to append to the prefix's IRI. +#' @return A new IRI. +#' @export +suf <- S7::new_generic("suf", "prefix") + +#' @export +S7::method(suf, Prefix) <- function(prefix, suffix) { + IRI(paste0(prefix@iri, suffix)) +} + +#' @export +S7::method(format, Prefix) <- function(x, ...) { + if (is.null(x@prefix_name)) { + paste0("Prefix(", x@iri, ")") + } else { + paste0("Prefix(", x@prefix_name, ": ", x@iri, ")") + } +} + +#' @export +S7::method(print, Prefix) <- function(x, ...) { + cat(format(x), "\n", sep = "") + invisible(x) +} + +#' A SPARQL/OTTR variable. +#' +#' @param name The variable's name, without the leading `?`/`$`. +#' @export +Variable <- S7::new_class( + "Variable", + package = "maplibr", + properties = list(name = S7::class_character), + validator = function(self) { + if (length(self@name) != 1 || is.na(self@name)) { + return("@name must be a single, non-NA string") + } + tryCatch( + { + validate_variable_name(self@name) + NULL + }, + error = function(e) conditionMessage(e) + ) + } +) + +#' @export +S7::method(format, Variable) <- function(x, ...) paste0("?", x@name) + +#' @export +S7::method(print, Variable) <- function(x, ...) { + cat(format(x), "\n", sep = "") + invisible(x) +} + +#' An RDF blank node. +#' +#' @param name The blank node's identifier, without the leading `_:`. +#' @export +BlankNode <- S7::new_class( + "BlankNode", + package = "maplibr", + properties = list(name = S7::class_character), + validator = function(self) { + if (length(self@name) != 1 || is.na(self@name)) { + return("@name must be a single, non-NA string") + } + tryCatch( + { + validate_blank_node_id(self@name) + NULL + }, + error = function(e) conditionMessage(e) + ) + } +) + +#' @export +S7::method(format, BlankNode) <- function(x, ...) paste0("_:", x@name) + +#' @export +S7::method(print, BlankNode) <- function(x, ...) { + cat(format(x), "\n", sep = "") + invisible(x) +} + +.XSD_STRING <- "http://www.w3.org/2001/XMLSchema#string" +.RDF_LANG_STRING <- "http://www.w3.org/1999/02/22-rdf-syntax-ns#langString" + +#' An RDF literal: a value, with either a datatype IRI or a language tag. +#' +#' Mirrors py_maplib's `Literal` (lib/representation/src/python.rs:359-397). +#' Providing `language` takes priority over `datatype` (matching the Python +#' constructor's precedence); providing neither defaults to `xsd:string`. +#' +#' @param value The literal's lexical value, as a string. +#' @param datatype Optional datatype IRI (an IRI object). Ignored if `language` is given. +#' @param language Optional BCP 47 language tag (not syntax-checked, matching +#' oxrdf::Literal::new_language_tagged_literal_unchecked -- py_maplib does +#' the same). +#' @export +Literal <- S7::new_class( + "Literal", + package = "maplibr", + properties = list( + value = S7::class_character, + datatype = IRI, + language = S7::new_union(S7::class_character, NULL) + ), + validator = function(self) { + if (length(self@value) != 1 || is.na(self@value)) { + return("@value must be a single, non-NA string") + } + if (!is.null(self@language) && self@datatype@iri != .RDF_LANG_STRING) { + return("a language-tagged Literal must have datatype rdf:langString") + } + NULL + }, + constructor = function(value, datatype = NULL, language = NULL) { + if (!is.null(language)) { + datatype <- IRI(.RDF_LANG_STRING) + } else if (is.null(datatype)) { + datatype <- IRI(.XSD_STRING) + } + S7::new_object(S7::S7_object(), value = value, datatype = datatype, language = language) + } +) + +#' @export +S7::method(format, Literal) <- function(x, ...) { + if (!is.null(x@language)) { + paste0("\"", x@value, "\"@", x@language) + } else if (x@datatype@iri == .XSD_STRING) { + paste0("\"", x@value, "\"") + } else { + paste0("\"", x@value, "\"^^", format(x@datatype)) + } +} + +#' @export +S7::method(print, Literal) <- function(x, ...) { + cat(format(x), "\n", sep = "") + invisible(x) +} + +# Converts an IRI/Literal into an RGroundTerm (maplibr/src/rust/src/terms.rs) +# for Model$query()'s bindings= argument. Mirrors templates.R's +# .as_raw_constant_term() -- no BlankNode/"none" cases here, since a query +# variable can only ever be bound to an IRI or a literal value. +.to_ground_term <- function(x) { + if (S7::S7_inherits(x, IRI)) { + RGroundTerm$iri(x@iri) + } else if (S7::S7_inherits(x, Literal)) { + RGroundTerm$literal(x@value, x@datatype@iri, x@language) + } else { + stop("bindings values must be IRI or Literal objects, got: ", class(x)[1], call. = FALSE) + } +} diff --git a/maplibr/R/vocab.R b/maplibr/R/vocab.R new file mode 100644 index 00000000..0d971ec6 --- /dev/null +++ b/maplibr/R/vocab.R @@ -0,0 +1,206 @@ +# RDF/XSD vocabulary IRI constants (maplib-snz, continued). Mirrors +# py_maplib's xsd/rdf/rdfs/owl vocab helpers (lib/templates/src/python/ +# {xsd,rdf,rdfs,owl}.rs). Names match py_maplib's exactly (e.g. +# `xsd$dateTime`, `rdf$type`). +# +# Built lazily in zzz.R's .onLoad, not at top-level source time: each entry +# is an IRI(), which validates via a compiled Rust call (validate_iri(), +# terms.rs) -- and devtools::load_all()'s R-sourcing pass does not guarantee +# the package's native library is already registered at the point vocab.R +# itself is sourced (confirmed: calling IRI() here at top level fails with +# "object 'savvy_validate_iri__impl' not found" on a fresh load_all()). +# .onLoad always runs after useDynLib() registration completes, so building +# these there is the standard, safe place for a package constant that needs +# compiled code to construct. + +#' The xsd vocabulary: a named list of IRIs. +#' +#' @format A named list of IRI objects. +#' @export +xsd <- NULL + +.build_xsd_vocab <- function() { + list( + `boolean` = IRI("http://www.w3.org/2001/XMLSchema#boolean"), + `byte` = IRI("http://www.w3.org/2001/XMLSchema#byte"), + `date` = IRI("http://www.w3.org/2001/XMLSchema#date"), + `dateTime` = IRI("http://www.w3.org/2001/XMLSchema#dateTime"), + `dateTimeStamp` = IRI("http://www.w3.org/2001/XMLSchema#dateTimeStamp"), + `decimal` = IRI("http://www.w3.org/2001/XMLSchema#decimal"), + `double` = IRI("http://www.w3.org/2001/XMLSchema#double"), + `duration` = IRI("http://www.w3.org/2001/XMLSchema#duration"), + `float` = IRI("http://www.w3.org/2001/XMLSchema#float"), + `int_` = IRI("http://www.w3.org/2001/XMLSchema#int"), + `integer` = IRI("http://www.w3.org/2001/XMLSchema#integer"), + `language` = IRI("http://www.w3.org/2001/XMLSchema#language"), + `long` = IRI("http://www.w3.org/2001/XMLSchema#long"), + `short` = IRI("http://www.w3.org/2001/XMLSchema#short"), + `string` = IRI("http://www.w3.org/2001/XMLSchema#string"), + `anyURI` = IRI("http://www.w3.org/2001/XMLSchema#anyURI"), + `dayTimeDuration` = IRI("http://www.w3.org/2001/XMLSchema#dayTimeDuration"), + `base64Binary` = IRI("http://www.w3.org/2001/XMLSchema#base64Binary"), + `gDay` = IRI("http://www.w3.org/2001/XMLSchema#gDay"), + `gMonthDay` = IRI("http://www.w3.org/2001/XMLSchema#gMonthDay"), + `gMonth` = IRI("http://www.w3.org/2001/XMLSchema#gMonth"), + `gYear` = IRI("http://www.w3.org/2001/XMLSchema#gYear"), + `gYearMonth` = IRI("http://www.w3.org/2001/XMLSchema#gYearMonth"), + `hexBinary` = IRI("http://www.w3.org/2001/XMLSchema#hexBinary"), + `Name` = IRI("http://www.w3.org/2001/XMLSchema#Name"), + `NCName` = IRI("http://www.w3.org/2001/XMLSchema#NCName"), + `NMTOKEN` = IRI("http://www.w3.org/2001/XMLSchema#NMTOKEN"), + `negativeInteger` = IRI("http://www.w3.org/2001/XMLSchema#negativeInteger"), + `nonNegativeInteger` = IRI("http://www.w3.org/2001/XMLSchema#nonNegativeInteger"), + `nonPositiveInteger` = IRI("http://www.w3.org/2001/XMLSchema#nonPositiveInteger"), + `normalizedString` = IRI("http://www.w3.org/2001/XMLSchema#normalizedString"), + `token` = IRI("http://www.w3.org/2001/XMLSchema#token"), + `unsignedByte` = IRI("http://www.w3.org/2001/XMLSchema#unsignedByte"), + `unsignedInt` = IRI("http://www.w3.org/2001/XMLSchema#unsignedInt"), + `unsignedLong` = IRI("http://www.w3.org/2001/XMLSchema#unsignedLong"), + `unsignedShort` = IRI("http://www.w3.org/2001/XMLSchema#unsignedShort"), + `yearMonthDuration` = IRI("http://www.w3.org/2001/XMLSchema#yearMonthDuration") + ) +} + +#' The rdf vocabulary: a named list of IRIs. +#' +#' @format A named list of IRI objects. +#' @export +rdf <- NULL + +.build_rdf_vocab <- function() { + list( + `type` = IRI("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"), + `Alt` = IRI("http://www.w3.org/1999/02/22-rdf-syntax-ns#Alt"), + `Bag` = IRI("http://www.w3.org/1999/02/22-rdf-syntax-ns#Bag"), + `first` = IRI("http://www.w3.org/1999/02/22-rdf-syntax-ns#first"), + `HTML` = IRI("http://www.w3.org/1999/02/22-rdf-syntax-ns#HTML"), + `langString` = IRI("http://www.w3.org/1999/02/22-rdf-syntax-ns#langString"), + `List` = IRI("http://www.w3.org/1999/02/22-rdf-syntax-ns#List"), + `nil` = IRI("http://www.w3.org/1999/02/22-rdf-syntax-ns#nil"), + `object` = IRI("http://www.w3.org/1999/02/22-rdf-syntax-ns#object"), + `predicate` = IRI("http://www.w3.org/1999/02/22-rdf-syntax-ns#predicate"), + `Property` = IRI("http://www.w3.org/1999/02/22-rdf-syntax-ns#Property"), + `rest` = IRI("http://www.w3.org/1999/02/22-rdf-syntax-ns#rest"), + `Seq` = IRI("http://www.w3.org/1999/02/22-rdf-syntax-ns#Seq"), + `Statement` = IRI("http://www.w3.org/1999/02/22-rdf-syntax-ns#Statement"), + `subject` = IRI("http://www.w3.org/1999/02/22-rdf-syntax-ns#subject"), + `value` = IRI("http://www.w3.org/1999/02/22-rdf-syntax-ns#value"), + `XMLLiteral` = IRI("http://www.w3.org/1999/02/22-rdf-syntax-ns#XMLLiteral") + ) +} + +#' The rdfs vocabulary: a named list of IRIs. +#' +#' @format A named list of IRI objects. +#' @export +rdfs <- NULL + +.build_rdfs_vocab <- function() { + list( + `Class` = IRI("http://www.w3.org/2000/01/rdf-schema#Class"), + `comment` = IRI("http://www.w3.org/2000/01/rdf-schema#comment"), + `Container` = IRI("http://www.w3.org/2000/01/rdf-schema#Container"), + `Datatype` = IRI("http://www.w3.org/2000/01/rdf-schema#Datatype"), + `domain` = IRI("http://www.w3.org/2000/01/rdf-schema#domain"), + `ContainerMembershipProperty` = IRI("http://www.w3.org/2000/01/rdf-schema#ContainerMembershipProperty"), + `isDefinedBy` = IRI("http://www.w3.org/2000/01/rdf-schema#isDefinedBy"), + `label` = IRI("http://www.w3.org/2000/01/rdf-schema#label"), + `Literal` = IRI("http://www.w3.org/2000/01/rdf-schema#Literal"), + `member` = IRI("http://www.w3.org/2000/01/rdf-schema#member"), + `range` = IRI("http://www.w3.org/2000/01/rdf-schema#range"), + `seeAlso` = IRI("http://www.w3.org/2000/01/rdf-schema#seeAlso"), + `subClassOf` = IRI("http://www.w3.org/2000/01/rdf-schema#subClassOf"), + `subPropertyOf` = IRI("http://www.w3.org/2000/01/rdf-schema#subPropertyOf"), + `Resource` = IRI("http://www.w3.org/2000/01/rdf-schema#Resource") + ) +} + +#' The owl vocabulary: a named list of IRIs. +#' +#' @format A named list of IRI objects. +#' @export +owl <- NULL + +.build_owl_vocab <- function() { + list( + `allValuesFrom` = IRI("http://www.w3.org/2002/07/owl#allValuesFrom"), + `annotatedProperty` = IRI("http://www.w3.org/2002/07/owl#annotatedProperty"), + `annotatedSource` = IRI("http://www.w3.org/2002/07/owl#annotatedSource"), + `annotatedTarget` = IRI("http://www.w3.org/2002/07/owl#annotatedTarget"), + `assertionProperty` = IRI("http://www.w3.org/2002/07/owl#assertionProperty"), + `cardinality` = IRI("http://www.w3.org/2002/07/owl#cardinality"), + `complementOf` = IRI("http://www.w3.org/2002/07/owl#complementOf"), + `datatypeComplementOf` = IRI("http://www.w3.org/2002/07/owl#datatypeComplementOf"), + `differentFrom` = IRI("http://www.w3.org/2002/07/owl#differentFrom"), + `disjointUnionOf` = IRI("http://www.w3.org/2002/07/owl#disjointUnionOf"), + `disjointWith` = IRI("http://www.w3.org/2002/07/owl#disjointWith"), + `distinctMembers` = IRI("http://www.w3.org/2002/07/owl#distinctMembers"), + `equivalentClass` = IRI("http://www.w3.org/2002/07/owl#equivalentClass"), + `equivalentProperty` = IRI("http://www.w3.org/2002/07/owl#equivalentProperty"), + `hasKey` = IRI("http://www.w3.org/2002/07/owl#hasKey"), + `hasSelf` = IRI("http://www.w3.org/2002/07/owl#hasSelf"), + `hasValue` = IRI("http://www.w3.org/2002/07/owl#hasValue"), + `intersectionOf` = IRI("http://www.w3.org/2002/07/owl#intersectionOf"), + `inverseOf` = IRI("http://www.w3.org/2002/07/owl#inverseOf"), + `maxCardinality` = IRI("http://www.w3.org/2002/07/owl#maxCardinality"), + `maxQualifiedCardinality` = IRI("http://www.w3.org/2002/07/owl#maxQualifiedCardinality"), + `members` = IRI("http://www.w3.org/2002/07/owl#members"), + `minCardinality` = IRI("http://www.w3.org/2002/07/owl#minCardinality"), + `minQualifiedCardinality` = IRI("http://www.w3.org/2002/07/owl#minQualifiedCardinality"), + `onClass` = IRI("http://www.w3.org/2002/07/owl#onClass"), + `onDataRange` = IRI("http://www.w3.org/2002/07/owl#onDataRange"), + `onDatatype` = IRI("http://www.w3.org/2002/07/owl#onDatatype"), + `onProperties` = IRI("http://www.w3.org/2002/07/owl#onProperties"), + `onProperty` = IRI("http://www.w3.org/2002/07/owl#onProperty"), + `oneOf` = IRI("http://www.w3.org/2002/07/owl#oneOf"), + `propertyChainAxiom` = IRI("http://www.w3.org/2002/07/owl#propertyChainAxiom"), + `propertyDisjointWith` = IRI("http://www.w3.org/2002/07/owl#propertyDisjointWith"), + `qualifiedCardinality` = IRI("http://www.w3.org/2002/07/owl#qualifiedCardinality"), + `sameAs` = IRI("http://www.w3.org/2002/07/owl#sameAs"), + `someValuesFrom` = IRI("http://www.w3.org/2002/07/owl#someValuesFrom"), + `sourceIndividual` = IRI("http://www.w3.org/2002/07/owl#sourceIndividual"), + `targetIndividual` = IRI("http://www.w3.org/2002/07/owl#targetIndividual"), + `targetValue` = IRI("http://www.w3.org/2002/07/owl#targetValue"), + `unionOf` = IRI("http://www.w3.org/2002/07/owl#unionOf"), + `withRestrictions` = IRI("http://www.w3.org/2002/07/owl#withRestrictions"), + `AllDifferent` = IRI("http://www.w3.org/2002/07/owl#AllDifferent"), + `AllDisjointClasses` = IRI("http://www.w3.org/2002/07/owl#AllDisjointClasses"), + `AllDisjointProperties` = IRI("http://www.w3.org/2002/07/owl#AllDisjointProperties"), + `Annotation` = IRI("http://www.w3.org/2002/07/owl#Annotation"), + `AnnotationProperty` = IRI("http://www.w3.org/2002/07/owl#AnnotationProperty"), + `Axiom` = IRI("http://www.w3.org/2002/07/owl#Axiom"), + `Class` = IRI("http://www.w3.org/2002/07/owl#Class"), + `DataRange` = IRI("http://www.w3.org/2002/07/owl#DataRange"), + `DatatypeProperty` = IRI("http://www.w3.org/2002/07/owl#DatatypeProperty"), + `DeprecatedClass` = IRI("http://www.w3.org/2002/07/owl#DeprecatedClass"), + `DeprecatedProperty` = IRI("http://www.w3.org/2002/07/owl#DeprecatedProperty"), + `FunctionalProperty` = IRI("http://www.w3.org/2002/07/owl#FunctionalProperty"), + `InverseFunctionalProperty` = IRI("http://www.w3.org/2002/07/owl#InverseFunctionalProperty"), + `IrreflexiveProperty` = IRI("http://www.w3.org/2002/07/owl#IrreflexiveProperty"), + `NamedIndividual` = IRI("http://www.w3.org/2002/07/owl#NamedIndividual"), + `NegativePropertyAssertion` = IRI("http://www.w3.org/2002/07/owl#NegativePropertyAssertion"), + `ObjectProperty` = IRI("http://www.w3.org/2002/07/owl#ObjectProperty"), + `Ontology` = IRI("http://www.w3.org/2002/07/owl#Ontology"), + `OntologyProperty` = IRI("http://www.w3.org/2002/07/owl#OntologyProperty"), + `ReflexiveProperty` = IRI("http://www.w3.org/2002/07/owl#ReflexiveProperty"), + `Restriction` = IRI("http://www.w3.org/2002/07/owl#Restriction"), + `SymmetricProperty` = IRI("http://www.w3.org/2002/07/owl#SymmetricProperty"), + `TransitiveProperty` = IRI("http://www.w3.org/2002/07/owl#TransitiveProperty"), + `backwardCompatibleWith` = IRI("http://www.w3.org/2002/07/owl#backwardCompatibleWith"), + `deprecated` = IRI("http://www.w3.org/2002/07/owl#deprecated"), + `incompatibleWith` = IRI("http://www.w3.org/2002/07/owl#incompatibleWith"), + `priorVersion` = IRI("http://www.w3.org/2002/07/owl#priorVersion"), + `versionInfo` = IRI("http://www.w3.org/2002/07/owl#versionInfo"), + `Nothing` = IRI("http://www.w3.org/2002/07/owl#Nothing"), + `Thing` = IRI("http://www.w3.org/2002/07/owl#Thing"), + `bottomDataProperty` = IRI("http://www.w3.org/2002/07/owl#bottomDataProperty"), + `topDataProperty` = IRI("http://www.w3.org/2002/07/owl#topDataProperty"), + `bottomObjectProperty` = IRI("http://www.w3.org/2002/07/owl#bottomObjectProperty"), + `topObjectProperty` = IRI("http://www.w3.org/2002/07/owl#topObjectProperty"), + `imports` = IRI("http://www.w3.org/2002/07/owl#imports"), + `versionIRI` = IRI("http://www.w3.org/2002/07/owl#versionIRI"), + `rational` = IRI("http://www.w3.org/2002/07/owl#rational"), + `real` = IRI("http://www.w3.org/2002/07/owl#real") + ) +} + diff --git a/maplibr/R/zzz.R b/maplibr/R/zzz.R new file mode 100644 index 00000000..907e6a3a --- /dev/null +++ b/maplibr/R/zzz.R @@ -0,0 +1,12 @@ +.onLoad <- function(libname, pkgname) { + S7::methods_register() + + # Vocab constants (vocab.R) need a compiled Rust call to build (each is an + # IRI(), validated via validate_iri()) -- built here, not at vocab.R's own + # top level, since .onLoad is guaranteed to run after useDynLib() + # registration completes and top-level R/*.R sourcing is not. + xsd <<- .build_xsd_vocab() + rdf <<- .build_rdf_vocab() + rdfs <<- .build_rdfs_vocab() + owl <<- .build_owl_vocab() +} diff --git a/maplibr/README.md b/maplibr/README.md new file mode 100644 index 00000000..b87fea4b --- /dev/null +++ b/maplibr/README.md @@ -0,0 +1,43 @@ +# maplibr + +An R interface to the [maplib](https://github.com/DataTreehouse/maplib) knowledge +graph engine: build, query, and reason over RDF graphs from tabular data using +OTTR mapping templates, SPARQL, and the Arrow C Stream Interface for zero-copy +data frame results. + +## Installation + +```r +# from source, requires Cargo/rustc (see SystemRequirements in DESCRIPTION) +devtools::install() +``` + +## Known issue: `undefined symbol: _Py_Dealloc` when loading the package + +If `devtools::load_all()`, `devtools::check()`, or `library(maplibr)` fails +with something like: + +``` +Error in dyn.load(...) : + unable to load shared object '.../maplibr.so': + .../maplibr.so: undefined symbol: _Py_Dealloc +``` + +this is a known upstream issue in `maplib`, not a bug in this package. +`maplib`'s public API has an unconditional dependency on `pyo3` +(Python's Rust FFI crate), so `maplibr` is forced to compile against it +even though it never uses Python. On Linux, `pyo3-ffi` +never links `libpython` at build time — it expects the *loading process* to +supply those symbols, which works for Python but not for R's `dyn.load()`. + +**Workaround:** preload `libpython` into R's process before it loads the +package: + +```bash +LD_PRELOAD=$(python3-config --prefix)/lib/libpython3.$(python3 -c 'import sys;print(sys.version_info[1])').so.1.0 \ + Rscript -e 'devtools::load_all("maplibr")' +``` + +If that path doesn't resolve on your system, find the right one with +`ldconfig -p | grep libpython`. Needed for `load_all()`, `document()`, +`check()`, and any R session that actually calls into `Model`. diff --git a/maplibr/cleanup b/maplibr/cleanup new file mode 100644 index 00000000..e346d71d --- /dev/null +++ b/maplibr/cleanup @@ -0,0 +1 @@ +rm -f src/Makevars diff --git a/maplibr/cleanup.win b/maplibr/cleanup.win new file mode 100644 index 00000000..a1821746 --- /dev/null +++ b/maplibr/cleanup.win @@ -0,0 +1 @@ +rm -f src/Makevars.win diff --git a/maplibr/configure b/maplibr/configure new file mode 100644 index 00000000..58793d29 --- /dev/null +++ b/maplibr/configure @@ -0,0 +1,73 @@ +# Even when `cargo` is on `PATH`, `rustc` might not in some cases. This adds +# ~/.cargo/bin to PATH to address such cases. Note that is not always available +# (e.g. or on Ubuntu with Rust installed via APT). +if [ -d "${HOME}/.cargo/bin" ]; then + export PATH="${PATH}:${HOME}/.cargo/bin" +fi + +CARGO_VERSION="$(cargo --version)" + +if [ $? -ne 0 ]; then + echo "-------------- ERROR: CONFIGURATION FAILED --------------------" + echo "" + echo "The cargo command is not available. To install Rust, please refer" + echo "to the official instruction:" + echo "" + echo "https://www.rust-lang.org/tools/install" + echo "" + echo "---------------------------------------------------------------" + + exit 1 +fi + +# There's a little chance that rustc is not available on PATH while cargo is. +# So, just ignore the error case. +RUSTC_VERSION="$(rustc --version || true)" + +# Report the version of Rustc to comply with the CRAN policy +echo "using Rust package manager: '${CARGO_VERSION}'" +echo "using Rust compiler: '${RUSTC_VERSION}'" + +if [ "$(uname)" = "Emscripten" ]; then + TARGET="wasm32-unknown-emscripten" +fi + +# allow overriding profile externally (e.g. on CI) +if [ -n "${SAVVY_PROFILE}" ]; then + PROFILE="${SAVVY_PROFILE}" +# catch DEBUG envvar, which is passed from pkgbuild::compile_dll() +elif [ "${DEBUG}" = "true" ]; then + PROFILE=dev +else + PROFILE=release +fi + +# e.g. SAVVY_FEATURES="a b" --> "--features 'a b'" +if [ -n "${SAVVY_FEATURES}" ]; then + FEATURE_FLAGS="--features '${SAVVY_FEATURES}'" +fi + +# Set macOS deployment target for the Rust cc crate. +# +# The cc crate (used by savvy's build.rs to compile unwind_protect_wrapper.c) +# does not derive the deployment target from CC/CFLAGS. Instead, it looks up +# MACOSX_DEPLOYMENT_TARGET, falling back to `xcrun --show-sdk-version`, and +# passes the result as -mmacosx-version-min=. When the env var is unset, +# the fallback SDK version (e.g. 26.2) can be newer than the version R's +# compiler targets, producing an ld warning about version mismatch. +# +# We default to 11.0, the minimum deployment target for arm64 macOS. This +# should be safe because unwind_protect_wrapper.c is a fairly simple C code +# and uses no version-specific APIs. +if [ "$(uname)" = "Darwin" ]; then + MACOSX_DEPLOYMENT_TARGET="${MACOSX_DEPLOYMENT_TARGET:-11.0}" + echo "using macOS deployment target: ${MACOSX_DEPLOYMENT_TARGET}" + BEFORE_CARGO_BUILD="${BEFORE_CARGO_BUILD}"' export MACOSX_DEPLOYMENT_TARGET="'"${MACOSX_DEPLOYMENT_TARGET}"'" \&\&' +fi + +sed \ + -e "s/@TARGET@/${TARGET}/" \ + -e "s/@PROFILE@/${PROFILE}/" \ + -e "s/@FEATURE_FLAGS@/${FEATURE_FLAGS}/" \ + -e "s|@BEFORE_CARGO_BUILD@|${BEFORE_CARGO_BUILD}|" \ + src/Makevars.in > src/Makevars diff --git a/maplibr/configure.win b/maplibr/configure.win new file mode 100644 index 00000000..dfff18b2 --- /dev/null +++ b/maplibr/configure.win @@ -0,0 +1,55 @@ +CARGO_VERSION="$(cargo --version)" + +if [ $? -ne 0 ]; then + echo "-------------- ERROR: CONFIGURATION FAILED --------------------" + echo "" + echo "The cargo command is not available. To install Rust, please refer" + echo "to the official instruction:" + echo "" + echo "https://www.rust-lang.org/tools/install" + echo "" + echo "---------------------------------------------------------------" + + exit 1 +fi + +# There's a little chance that rustc is not available on PATH while cargo is. +# So, just ignore the error case. +RUSTC_VERSION="$(rustc --version || true)" + +# Report the version of Rustc to comply with the CRAN policy +echo "using Rust package manager: '${CARGO_VERSION}'" +echo "using Rust compiler: '${RUSTC_VERSION}'" + +# allow overriding profile externally (e.g. on CI) +if [ -n "${SAVVY_PROFILE}" ]; then + PROFILE="${SAVVY_PROFILE}" +# catch DEBUG envvar, which is passed from pkgbuild::compile_dll() +elif [ "${DEBUG}" = "true" ]; then + PROFILE=dev +else + PROFILE=release +fi + +# e.g. SAVVY_FEATURES="a b" --> "--features 'a b'" +if [ -n "${SAVVY_FEATURES}" ]; then + FEATURE_FLAGS="--features '${SAVVY_FEATURES}'" +fi + +# cf. https://github.com/apache/sedona-db/pull/1137 +CC="$("${R_HOME}/bin/R" CMD config CC)" +case "$($CC -dumpmachine)" in + aarch64*|arm64*) + TARGET="aarch64-pc-windows-gnullvm" + ;; + *) + TARGET="x86_64-pc-windows-gnu" + ;; +esac +echo "using Rust target: '${TARGET}'" + +sed \ + -e "s/@TARGET@/${TARGET}/" \ + -e "s/@PROFILE@/${PROFILE}/" \ + -e "s/@FEATURE_FLAGS@/${FEATURE_FLAGS}/" \ + src/Makevars.win.in > src/Makevars.win diff --git a/maplibr/man/Argument.Rd b/maplibr/man/Argument.Rd new file mode 100644 index 00000000..d14ced3b --- /dev/null +++ b/maplibr/man/Argument.Rd @@ -0,0 +1,18 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/templates.R +\name{Argument} +\alias{Argument} +\title{An OTTR template argument: a Variable, or a constant term.} +\usage{ +Argument(term, list_expand = FALSE) +} +\arguments{ +\item{term}{A Variable, IRI, BlankNode, Literal, NULL, or an existing Argument.} + +\item{list_expand}{Whether this argument should be OTTR list-expanded.} +} +\description{ +Mirrors py_maplib's \code{Argument} (lib/templates/src/python.rs:203-236). +Passing an existing Argument as \code{term} returns it unchanged (\code{list_expand} +is ignored in that case), matching the Python constructor's behavior. +} diff --git a/maplibr/man/BlankNode.Rd b/maplibr/man/BlankNode.Rd new file mode 100644 index 00000000..f8726f84 --- /dev/null +++ b/maplibr/man/BlankNode.Rd @@ -0,0 +1,14 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/terms.R +\name{BlankNode} +\alias{BlankNode} +\title{An RDF blank node.} +\usage{ +BlankNode(name = character(0)) +} +\arguments{ +\item{name}{The blank node's identifier, without the leading \verb{_:}.} +} +\description{ +An RDF blank node. +} diff --git a/maplibr/man/IRI.Rd b/maplibr/man/IRI.Rd new file mode 100644 index 00000000..7deda5bd --- /dev/null +++ b/maplibr/man/IRI.Rd @@ -0,0 +1,14 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/terms.R +\name{IRI} +\alias{IRI} +\title{An RDF IRI (Internationalized Resource Identifier)} +\usage{ +IRI(iri = character(0)) +} +\arguments{ +\item{iri}{The IRI string.} +} +\description{ +An RDF IRI (Internationalized Resource Identifier) +} diff --git a/maplibr/man/Instance.Rd b/maplibr/man/Instance.Rd new file mode 100644 index 00000000..12d3fd6a --- /dev/null +++ b/maplibr/man/Instance.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/templates.R +\name{Instance} +\alias{Instance} +\title{An OTTR template instance: a call to a template with concrete arguments.} +\usage{ +Instance(template_iri, arguments = NULL, list_expander = NULL) +} +\arguments{ +\item{template_iri}{An IRI: the template being instantiated.} + +\item{arguments}{A list of Arguments (or bare Variable/IRI/BlankNode/ +Literal/NULL terms, auto-wrapped as non-list-expanded Arguments).} + +\item{list_expander}{Optional: one of "cross", "zipMin", "zipMax".} +} +\description{ +Mirrors py_maplib's \code{Instance} (lib/templates/src/python.rs:245-288). +} diff --git a/maplibr/man/Literal.Rd b/maplibr/man/Literal.Rd new file mode 100644 index 00000000..f457cafc --- /dev/null +++ b/maplibr/man/Literal.Rd @@ -0,0 +1,22 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/terms.R +\name{Literal} +\alias{Literal} +\title{An RDF literal: a value, with either a datatype IRI or a language tag.} +\usage{ +Literal(value, datatype = NULL, language = NULL) +} +\arguments{ +\item{value}{The literal's lexical value, as a string.} + +\item{datatype}{Optional datatype IRI (an IRI object). Ignored if \code{language} is given.} + +\item{language}{Optional BCP 47 language tag (not syntax-checked, matching +oxrdf::Literal::new_language_tagged_literal_unchecked -- py_maplib does +the same).} +} +\description{ +Mirrors py_maplib's \code{Literal} (lib/representation/src/python.rs:359-397). +Providing \code{language} takes priority over \code{datatype} (matching the Python +constructor's precedence); providing neither defaults to \code{xsd:string}. +} diff --git a/maplibr/man/Model.Rd b/maplibr/man/Model.Rd new file mode 100644 index 00000000..5c16aa0b --- /dev/null +++ b/maplibr/man/Model.Rd @@ -0,0 +1,698 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model.R +\name{Model} +\alias{Model} +\title{A maplib knowledge graph model} +\description{ +\code{Model} is the user-facing wrapper around the underlying \code{RModel} savvy +object. It mirrors py_maplib's \code{Model} class: methods mutate the +underlying graph in place and are chainable (each mutating method +returns \code{self} invisibly). +} +\section{Methods}{ +\subsection{Public methods}{ + \itemize{ + \item \href{#method-Model-initialize}{\code{Model$new()}} + \item \href{#method-Model-size}{\code{Model$size()}} + \item \href{#method-Model-reads}{\code{Model$reads()}} + \item \href{#method-Model-read}{\code{Model$read()}} + \item \href{#method-Model-writes}{\code{Model$writes()}} + \item \href{#method-Model-write}{\code{Model$write()}} + \item \href{#method-Model-create_index}{\code{Model$create_index()}} + \item \href{#method-Model-truncate_graph}{\code{Model$truncate_graph()}} + \item \href{#method-Model-add_prefixes}{\code{Model$add_prefixes()}} + \item \href{#method-Model-add_graph}{\code{Model$add_graph()}} + \item \href{#method-Model-detach_graph}{\code{Model$detach_graph()}} + \item \href{#method-Model-query}{\code{Model$query()}} + \item \href{#method-Model-update}{\code{Model$update()}} + \item \href{#method-Model-insert}{\code{Model$insert()}} + \item \href{#method-Model-add_template}{\code{Model$add_template()}} + \item \href{#method-Model-map}{\code{Model$map()}} + \item \href{#method-Model-map_json}{\code{Model$map_json()}} + \item \href{#method-Model-map_json_string}{\code{Model$map_json_string()}} + \item \href{#method-Model-map_xml}{\code{Model$map_xml()}} + \item \href{#method-Model-map_xml_string}{\code{Model$map_xml_string()}} + \item \href{#method-Model-map_df}{\code{Model$map_df()}} + \item \href{#method-Model-map_triples}{\code{Model$map_triples()}} + \item \href{#method-Model-infer_rdfs}{\code{Model$infer_rdfs()}} + \item \href{#method-Model-compact}{\code{Model$compact()}} + \item \href{#method-Model-serialize}{\code{Model$serialize()}} + \item \href{#method-Model-print}{\code{Model$print()}} + } +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-Model-initialize}{}}} +\subsection{\code{Model$new()}}{ + Create a new, empty Model. Not used to wrap an existing \code{RModel} +(see \code{Model$deserialize()} and \verb{$detach_graph()} for that) -- \code{rmodel} +is for internal use only. + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{Model$new(rmodel = NULL)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{rmodel}}{Internal use only.} + } + \if{html}{\out{
}} + } +} + +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-Model-size}{}}} +\subsection{\code{Model$size()}}{ + Number of triples in a graph. + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{Model$size(graph = NULL)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{graph}}{Optional named graph IRI to count (default graph if NULL).} + } + \if{html}{\out{
}} + } +} + +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-Model-reads}{}}} +\subsection{\code{Model$reads()}}{ + Parse RDF triples from a string into this Model. + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{Model$reads(s, format, graph = NULL)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{s}}{RDF data as a string.} + \item{\code{format}}{One of "ntriples", "turtle", "xml" (rdf/xml).} + \item{\code{graph}}{Optional named graph IRI to read into (default graph if NULL).} + } + \if{html}{\out{
}} + } +} + +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-Model-read}{}}} +\subsection{\code{Model$read()}}{ + Parse RDF triples from a file into this Model. + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{Model$read(path, format = NULL, graph = NULL)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{path}}{Path to the RDF file to read.} + \item{\code{format}}{One of "ntriples", "turtle", "xml" (rdf/xml). Guessed +from the file extension if NULL (.ttl -> turtle, .nt -> ntriples, +.xml/.rdf -> xml) -- any other or missing extension raises a normal +error, so pass \code{format} explicitly for those.} + \item{\code{graph}}{Optional named graph IRI to read into (default graph if NULL).} + } + \if{html}{\out{
}} + } +} + +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-Model-writes}{}}} +\subsection{\code{Model$writes()}}{ + Serialize this Model's triples to a string. + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{Model$writes(format = NULL, graph = NULL)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{format}}{One of "ntriples", "turtle", "xml" (rdf/xml). Defaults to "ntriples".} + \item{\code{graph}}{Optional named graph IRI to write (default graph if NULL).} + } + \if{html}{\out{
}} + } +} + +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-Model-write}{}}} +\subsection{\code{Model$write()}}{ + Serialize this Model's triples to a file. + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{Model$write(path, format = NULL, graph = NULL)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{path}}{Path to write the RDF file to (overwritten if it exists).} + \item{\code{format}}{One of "ntriples", "turtle", "xml" (rdf/xml). Defaults to "ntriples".} + \item{\code{graph}}{Optional named graph IRI to write (default graph if NULL).} + } + \if{html}{\out{
}} + } +} + +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-Model-create_index}{}}} +\subsection{\code{Model$create_index()}}{ + Build the default (non-FTS) indexes. + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{Model$create_index()} + \if{html}{\out{
}} + } +} + +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-Model-truncate_graph}{}}} +\subsection{\code{Model$truncate_graph()}}{ + Remove all triples from a graph. + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{Model$truncate_graph(graph = NULL)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{graph}}{Optional named graph IRI to truncate (default graph if NULL).} + } + \if{html}{\out{
}} + } +} + +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-Model-add_prefixes}{}}} +\subsection{\code{Model$add_prefixes()}}{ + Add prefix -> IRI mappings, used when serializing. + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{Model$add_prefixes(prefixes)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{prefixes}}{A named R list of single strings: names are prefixes, +values are IRIs, e.g. \code{list(ex = "http://example.org/")}.} + } + \if{html}{\out{
}} + } +} + +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-Model-add_graph}{}}} +\subsection{\code{Model$add_graph()}}{ + Copy a graph from another Model's triplestore into this one. + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{Model$add_graph(other, source_graph = NULL, target_graph = NULL)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{other}}{Another Model to copy a graph from.} + \item{\code{source_graph}}{Optional named graph IRI in \code{other} (default graph if NULL).} + \item{\code{target_graph}}{Optional named graph IRI in this Model to copy into (default graph if NULL).} + } + \if{html}{\out{
}} + } +} + +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-Model-detach_graph}{}}} +\subsection{\code{Model$detach_graph()}}{ + Split a graph out of this Model into a brand new, standalone Model. + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{Model$detach_graph(preserve_name = FALSE, graph = NULL)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{preserve_name}}{Keep the graph's own name in the new Model rather +than moving it to the default graph there.} + \item{\code{graph}}{Optional named graph IRI to detach (default graph if NULL).} + } + \if{html}{\out{
}} + } + \subsection{Returns}{ + A new Model containing only the detached graph. + } +} + +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-Model-query}{}}} +\subsection{\code{Model$query()}}{ + Run a SPARQL SELECT or CONSTRUCT query and return the +result as a data.frame. Mirrors py_maplib's Model.query() +(py_maplib/src/py_model.rs:344-387). For CONSTRUCT, every matched +triple pattern's results are concatenated into one combined +subject/predicate/object data.frame (py_maplib instead returns one +data.frame per pattern, as a Python list -- not done here since a +single query() call always returns exactly one data.frame). Subject +and object values are always returned as plain strings for CONSTRUCT +results (unlike SELECT, which preserves each column's native type +where a single type applies) -- different patterns can produce +different column types, which a single concatenated data.frame can't +represent without collapsing to a common type regardless. + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{Model$query(sparql, graph = NULL, include_transient = FALSE, bindings = NULL)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{sparql}}{The SPARQL query string.} + \item{\code{graph}}{Optional named graph IRI to restrict the query to (searches +across the whole store if NULL).} + \item{\code{include_transient}}{Whether to include transient (e.g. inferred) +triples in the query.} + \item{\code{bindings}}{Optional named list of \code{IRI}/\code{Literal} objects +(see \code{terms.R}) -- pre-binds each named SPARQL variable to a fixed +value before the query runs, e.g. \code{list(b = IRI("http://ex/a"))} +binds \code{?b}. A bound variable can only be used inside the query body +(e.g. a \code{FILTER}), not listed directly in \code{SELECT} -- binding a +variable that's also a \code{SELECT}-ed output column is a normal error +(\verb{SELECT *} still works, since it doesn't name variables itself).} + } + \if{html}{\out{
}} + } + \subsection{Returns}{ + A data.frame. A column whose values are always the same RDF +node type (the common case) comes back as a plain vector of that +type's underlying value (an IRI/blank node id/literal value as a +plain string, exactly as py_maplib returns by default). A column +whose RDF node type can vary by row instead comes back from Arrow as +one sub-column per possible type (only one of which is non-NA on any +given row) -- collapsed here into a single vector by taking +whichever sub-column is non-NA per row. The precise per-column RDF +node type information (which py_maplib exposes via +SolutionMappings.rdf_types) is attached as the data.frame's +\code{"rdf_node_types"} attribute instead of a richer typed object, parsed +from the underlying JSON side-channel (maplibr/src/rust/src/ +arrow_bridge.rs) -- a full RDFType-based port of that accessor is +left for later. + } +} + +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-Model-update}{}}} +\subsection{\code{Model$update()}}{ + Run a SPARQL UPDATE (modify-style INSERT/DELETE ... WHERE) +against this Model in place. + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{Model$update(update, graph = NULL, include_transient = FALSE)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{update}}{The SPARQL UPDATE string. Must be a DELETE/INSERT/WHERE +form (\verb{INSERT \{ ... \} WHERE \{ ... \}}, optionally with a DELETE +clause too, and an empty \verb{WHERE \{\}} when there's nothing to match +against) -- the bare \verb{INSERT DATA \{ ... \}}/\verb{DELETE DATA \{ ... \}} +forms are unimplemented in the core engine and panic if used (not +fixable in this package -- see maplib-cai upstream issue).} + \item{\code{graph}}{Optional named graph IRI to restrict the update to +(no restriction, i.e. the whole store, if NULL -- same "missing +means unrestricted" semantics as \verb{$query()}'s \code{graph} argument, not +\verb{$reads()}/\verb{$writes()}'s "missing means the default graph").} + \item{\code{include_transient}}{Whether the WHERE clause may match transient +(e.g. inferred) triples.} + } + \if{html}{\out{
}} + } +} + +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-Model-insert}{}}} +\subsection{\code{Model$insert()}}{ + Run a SPARQL CONSTRUCT query and insert its results as +new triples into a (possibly different) graph. + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{Model$insert( + query, + source_graph = NULL, + target_graph = NULL, + include_transient = FALSE, + transient = FALSE +)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{query}}{The SPARQL CONSTRUCT query string to source new triples +from (a SELECT query errors -- INSERT needs a triple-shaped result).} + \item{\code{source_graph}}{Optional named graph IRI to query from (default +graph if NULL).} + \item{\code{target_graph}}{Optional named graph IRI to insert into (default +graph if NULL).} + \item{\code{include_transient}}{Whether the query may match transient (e.g. +inferred) triples in the source graph.} + \item{\code{transient}}{Whether the newly-inserted triples themselves should +be marked transient rather than permanent.} + } + \if{html}{\out{
}} + } +} + +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-Model-add_template}{}}} +\subsection{\code{Model$add_template()}}{ + Register a Template so it can be referenced by IRI from +\verb{$map()}. + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{Model$add_template(template)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{template}}{A \code{Template} (see \code{templates.R}), or an stOTTR document +string defining one or more templates.} + } + \if{html}{\out{
}} + } + \subsection{Returns}{ + The IRI of the registered template, invisibly (for a \code{Template} +object, this is just \code{template@iri}; for a document string, it's the +IRI of the first template the document defines). + } +} + +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-Model-map}{}}} +\subsection{\code{Model$map()}}{ + Expand a template against a data.frame, adding the +resulting triples to this Model. Mirrors py_maplib's \code{Model.map()} +(py_maplib/src/py_model.rs:118-142). + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{Model$map(template, data = NULL, graph = NULL, validate_iris = NULL)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{template}}{A \code{Template} object, the IRI of an already-registered +template (\verb{$add_template()}), or an stOTTR document string (defining +exactly the one template to expand -- registered as a side effect).} + \item{\code{data}}{Optional data.frame, one row per template instantiation. If +NULL (or zero rows), the template is expanded once against no +variables -- only valid for templates whose instances are entirely +constant terms.} + \item{\code{graph}}{Optional named graph IRI to add the resulting triples to +(default graph if NULL).} + \item{\code{validate_iris}}{Whether to validate that IRI-typed columns contain +valid IRIs. Defaults to TRUE.} + } + \if{html}{\out{
}} + } +} + +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-Model-map_json}{}}} +\subsection{\code{Model$map_json()}}{ + Map a JSON file straight to triples via a fixed +convention (Facade-X; no template involved). + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{Model$map_json(path, graph = NULL, transient = FALSE, uuid_namespace = NULL)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{path}}{Path to the JSON file to map.} + \item{\code{graph}}{Optional named graph IRI to add the resulting triples to +(default graph if NULL).} + \item{\code{transient}}{Whether the resulting triples should be transient +rather than permanent.} + \item{\code{uuid_namespace}}{Optional namespace string for the UUIDv5 blank +node IRIs minted for JSON objects/arrays. Defaults to the path.} + } + \if{html}{\out{
}} + } +} + +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-Model-map_json_string}{}}} +\subsection{\code{Model$map_json_string()}}{ + Map a JSON string straight to triples via a fixed +convention. See \verb{$map_json()} for the file-based equivalent. + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{Model$map_json_string( + json, + graph = NULL, + transient = FALSE, + uuid_namespace = NULL +)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{json}}{The JSON document, as a string.} + \item{\code{graph}}{Optional named graph IRI to add the resulting triples to +(default graph if NULL).} + \item{\code{transient}}{Whether the resulting triples should be transient +rather than permanent.} + \item{\code{uuid_namespace}}{Optional namespace string for the UUIDv5 blank +node IRIs minted for JSON objects/arrays. Defaults to a random UUID +if not given.} + } + \if{html}{\out{
}} + } +} + +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-Model-map_xml}{}}} +\subsection{\code{Model$map_xml()}}{ + Map an XML file straight to triples via a fixed +convention. + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{Model$map_xml(path, graph = NULL, transient = FALSE, uuid_namespace = NULL)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{path}}{Path to the XML file to map.} + \item{\code{graph}}{Optional named graph IRI to add the resulting triples to +(default graph if NULL).} + \item{\code{transient}}{Whether the resulting triples should be transient +rather than permanent.} + \item{\code{uuid_namespace}}{Optional namespace string for the UUIDv5 blank +node IRIs minted for XML elements. Defaults to the path.} + } + \if{html}{\out{
}} + } +} + +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-Model-map_xml_string}{}}} +\subsection{\code{Model$map_xml_string()}}{ + Map an XML string straight to triples via a fixed +convention. See \verb{$map_xml()} for the file-based equivalent. + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{Model$map_xml_string( + xml, + graph = NULL, + transient = FALSE, + uuid_namespace = NULL +)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{xml}}{The XML document, as a string.} + \item{\code{graph}}{Optional named graph IRI to add the resulting triples to +(default graph if NULL).} + \item{\code{transient}}{Whether the resulting triples should be transient +rather than permanent.} + \item{\code{uuid_namespace}}{Optional namespace string for the UUIDv5 blank +node IRIs minted for XML elements. Defaults to a random UUID if not +given.} + } + \if{html}{\out{
}} + } +} + +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-Model-map_df}{}}} +\subsection{\code{Model$map_df()}}{ + Map a data.frame's columns directly to +subject/predicate/object triples -- one column per predicate, a fresh +IRI subject per row, no OTTR template involved. + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{Model$map_df(data, graph = NULL, uuid_namespace = NULL)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{data}}{A data.frame, one row per subject; every column becomes a +predicate (named by the column name) with that row's value as the +object. A zero-row data.frame is NOT a no-op here (unlike \verb{$map()}): +a single root \code{rdf:type} triple is still added, matching the +underlying engine call.} + \item{\code{graph}}{Optional named graph IRI to add the resulting triples to +(default graph if NULL).} + \item{\code{uuid_namespace}}{Optional namespace string for the UUIDv5 subject +IRIs minted per row. Defaults to a random UUID if not given.} + } + \if{html}{\out{
}} + } +} + +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-Model-map_triples}{}}} +\subsection{\code{Model$map_triples()}}{ + Map a data.frame's columns directly to +subject/predicate/object triples using the built-in \code{ottr:Triple} +template, optionally fixing every row's predicate to a single +constant IRI instead of reading it from a \code{predicate} column. + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{Model$map_triples(data, predicate = NULL, graph = NULL, validate_iris = NULL)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{data}}{A data.frame with subject/predicate/object columns (or +just subject/object if \code{predicate} is given). A zero-row data.frame +is NOT a no-op: missing/misnamed columns still raise a normal error +regardless of row count.} + \item{\code{predicate}}{Optional constant predicate IRI to use for every +row, instead of a \code{predicate} column in \code{data}.} + \item{\code{graph}}{Optional named graph IRI to add the resulting triples to +(default graph if NULL).} + \item{\code{validate_iris}}{Whether to validate that IRI-typed columns +contain valid IRIs. Defaults to TRUE.} + } + \if{html}{\out{
}} + } +} + +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-Model-infer_rdfs}{}}} +\subsection{\code{Model$infer_rdfs()}}{ + Run RDFS inference over a graph in place. + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{Model$infer_rdfs(graph = NULL)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{graph}}{Optional named graph IRI to infer over (default graph if NULL).} + } + \if{html}{\out{
}} + } + \subsection{Returns}{ + The number of new triples inferred (a triple count, not a rule +count -- Triplestore::interesting_rdfs_rules sums per-rule +inserted-triple counts). + } +} + +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-Model-compact}{}}} +\subsection{\code{Model$compact()}}{ + Compact on-disk storage. + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{Model$compact()} + \if{html}{\out{
}} + } +} + +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-Model-serialize}{}}} +\subsection{\code{Model$serialize()}}{ + Serialize this Model's triples to a directory in +maplib's own compact on-disk format (not an RDF interchange format -- +use \verb{$writes()} for that). + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{Model$serialize(path = NULL)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{path}}{Directory to serialize into. Defaults to +"./serialized_triples".} + } + \if{html}{\out{
}} + } +} + +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-Model-print}{}}} +\subsection{\code{Model$print()}}{ + Print a short summary of this Model. + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{Model$print(...)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{...}}{Unused.} + } + \if{html}{\out{
}} + } +} + +} diff --git a/maplibr/man/Parameter.Rd b/maplibr/man/Parameter.Rd new file mode 100644 index 00000000..5a52d9e7 --- /dev/null +++ b/maplibr/man/Parameter.Rd @@ -0,0 +1,31 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/templates.R +\name{Parameter} +\alias{Parameter} +\title{An OTTR template parameter.} +\usage{ +Parameter( + variable, + optional = FALSE, + allow_blank = TRUE, + rdf_type = NULL, + default_value = NULL +) +} +\arguments{ +\item{variable}{A Variable.} + +\item{optional}{Whether this parameter is optional.} + +\item{allow_blank}{Whether a blank node is an acceptable value (default TRUE).} + +\item{rdf_type}{Optional: an IRI (a datatype/class IRI directly) or an +RDFType, giving the parameter's expected RDF type.} + +\item{default_value}{Optional default value: an IRI, BlankNode, or Literal.} +} +\description{ +Mirrors py_maplib's \code{Parameter} (lib/templates/src/python.rs:21-167). +\code{rdf_type} only supports a basic (non-nested, non-list) RDF type -- +\code{RDFType$Nested()} is not ported (see \code{maplibr/R/rdf_type.R}). +} diff --git a/maplibr/man/Prefix.Rd b/maplibr/man/Prefix.Rd new file mode 100644 index 00000000..20197ba4 --- /dev/null +++ b/maplibr/man/Prefix.Rd @@ -0,0 +1,16 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/terms.R +\name{Prefix} +\alias{Prefix} +\title{An RDF namespace prefix: a short name bound to an IRI.} +\usage{ +Prefix(iri = character(0), prefix_name = character(0)) +} +\arguments{ +\item{iri}{The IRI the prefix expands to.} + +\item{prefix_name}{Optional short name for the prefix (e.g. "ex").} +} +\description{ +An RDF namespace prefix: a short name bound to an IRI. +} diff --git a/maplibr/man/RArgument.Rd b/maplibr/man/RArgument.Rd new file mode 100644 index 00000000..a5549189 --- /dev/null +++ b/maplibr/man/RArgument.Rd @@ -0,0 +1,13 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/000-wrappers.R +\name{RArgument} +\alias{RArgument} +\title{An OTTR argument: a Variable, or a constant term (IRI/BlankNode/Literal/none). +Mirrors py_maplib's \code{Argument} (lib/templates/src/python.rs:203-236).} +\usage{ +RArgument +} +\description{ +An OTTR argument: a Variable, or a constant term (IRI/BlankNode/Literal/none). +Mirrors py_maplib's \code{Argument} (lib/templates/src/python.rs:203-236). +} diff --git a/maplibr/man/RConstantTerm.Rd b/maplibr/man/RConstantTerm.Rd new file mode 100644 index 00000000..922ce361 --- /dev/null +++ b/maplibr/man/RConstantTerm.Rd @@ -0,0 +1,13 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/000-wrappers.R +\name{RConstantTerm} +\alias{RConstantTerm} +\title{An OTTR constant term: an IRI, blank node, literal, or explicit "none". +Mirrors py_maplib's \code{extract_constant_term} (lib/templates/src/python.rs:434-446).} +\usage{ +RConstantTerm +} +\description{ +An OTTR constant term: an IRI, blank node, literal, or explicit "none". +Mirrors py_maplib's \code{extract_constant_term} (lib/templates/src/python.rs:434-446). +} diff --git a/maplibr/man/RDFType.Rd b/maplibr/man/RDFType.Rd new file mode 100644 index 00000000..ac328ee8 --- /dev/null +++ b/maplibr/man/RDFType.Rd @@ -0,0 +1,40 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/rdf_type.R +\name{RDFType} +\alias{RDFType} +\alias{rdf_type_iri} +\alias{rdf_type_blank_node} +\alias{rdf_type_literal} +\alias{rdf_type_unknown} +\alias{rdf_type_multi} +\title{An RDF node type, for use as a Parameter's \code{rdf_type}.} +\usage{ +RDFType(basic_type_iri = IRI()) + +rdf_type_iri() + +rdf_type_blank_node() + +rdf_type_literal(iri_or_datatype) + +rdf_type_unknown() + +rdf_type_multi(rdf_types) +} +\arguments{ +\item{basic_type_iri}{The resolved basic-type IRI, or NULL for +\code{rdf_type_unknown()} (no type constraint).} + +\item{iri_or_datatype}{An IRI (the literal's datatype), or a plain IRI string.} + +\item{rdf_types}{A list of RDFTypes.} +} +\description{ +Mirrors py_maplib's \code{RDFType} (lib/representation/src/python.rs:101-268). +Only represents a single basic type -- see the note in +\code{maplibr/R/rdf_type.R} for why \code{rdf_type_multi()} collapses multiple types +down to one IRI (\code{rdfs:Resource}) rather than preserving the set, and why +\code{RDFType.Nested()} isn't ported. Build one with \code{rdf_type_iri()}, +\code{rdf_type_blank_node()}, \code{rdf_type_literal()}, \code{rdf_type_unknown()}, or +\code{rdf_type_multi()}, not by calling \code{RDFType()} directly. +} diff --git a/maplibr/man/RGroundTerm.Rd b/maplibr/man/RGroundTerm.Rd new file mode 100644 index 00000000..f8a324ee --- /dev/null +++ b/maplibr/man/RGroundTerm.Rd @@ -0,0 +1,25 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/000-wrappers.R +\name{RGroundTerm} +\alias{RGroundTerm} +\title{A ground SPARQL binding value: an IRI or a Literal (no blank nodes, +unlike \code{RConstantTerm} -- blank nodes can't be bound to a query variable, +so there's no variant for one to construct in the first place, unlike +py_maplib's equivalent, which has to reject a blank node at runtime since +it accepts a plain Python value that could be anything, see +\code{maybe_parse_bindings}, py_maplib/src/lib.rs:479-528). Built from an R +\code{IRI}/\code{Literal} (terms.R) by \code{.to_ground_term()}, and consumed by +\code{RModel::query}'s \code{bindings} argument.} +\usage{ +RGroundTerm +} +\description{ +A ground SPARQL binding value: an IRI or a Literal (no blank nodes, +unlike \code{RConstantTerm} -- blank nodes can't be bound to a query variable, +so there's no variant for one to construct in the first place, unlike +py_maplib's equivalent, which has to reject a blank node at runtime since +it accepts a plain Python value that could be anything, see +\code{maybe_parse_bindings}, py_maplib/src/lib.rs:479-528). Built from an R +\code{IRI}/\code{Literal} (terms.R) by \code{.to_ground_term()}, and consumed by +\code{RModel::query}'s \code{bindings} argument. +} diff --git a/maplibr/man/RInstance.Rd b/maplibr/man/RInstance.Rd new file mode 100644 index 00000000..b9783bff --- /dev/null +++ b/maplibr/man/RInstance.Rd @@ -0,0 +1,13 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/000-wrappers.R +\name{RInstance} +\alias{RInstance} +\title{An OTTR template instance: a call to a template with concrete arguments. +Mirrors py_maplib's \code{Instance} (lib/templates/src/python.rs:245-288).} +\usage{ +RInstance +} +\description{ +An OTTR template instance: a call to a template with concrete arguments. +Mirrors py_maplib's \code{Instance} (lib/templates/src/python.rs:245-288). +} diff --git a/maplibr/man/RModel.Rd b/maplibr/man/RModel.Rd new file mode 100644 index 00000000..d6609dbf --- /dev/null +++ b/maplibr/man/RModel.Rd @@ -0,0 +1,11 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/000-wrappers.R +\name{RModel} +\alias{RModel} +\title{A maplib knowledge graph model.} +\usage{ +RModel +} +\description{ +A maplib knowledge graph model. +} diff --git a/maplibr/man/RParameter.Rd b/maplibr/man/RParameter.Rd new file mode 100644 index 00000000..de4fbbc3 --- /dev/null +++ b/maplibr/man/RParameter.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/000-wrappers.R +\name{RParameter} +\alias{RParameter} +\title{An OTTR template parameter. Mirrors py_maplib's \code{Parameter} +(lib/templates/src/python.rs:21-167) -- \code{rdf_type} here only supports a +basic (non-nested, non-list) RDF type, given as a datatype/class IRI +string; PType::Lub/List/NEList and RDFType.Nested()/Multi() are not yet +ported (they need the RDFType port, maplib-snz's remaining scope).} +\usage{ +RParameter +} +\description{ +An OTTR template parameter. Mirrors py_maplib's \code{Parameter} +(lib/templates/src/python.rs:21-167) -- \code{rdf_type} here only supports a +basic (non-nested, non-list) RDF type, given as a datatype/class IRI +string; PType::Lub/List/NEList and RDFType.Nested()/Multi() are not yet +ported (they need the RDFType port, maplib-snz's remaining scope). +} diff --git a/maplibr/man/RTemplate.Rd b/maplibr/man/RTemplate.Rd new file mode 100644 index 00000000..859781fa --- /dev/null +++ b/maplibr/man/RTemplate.Rd @@ -0,0 +1,13 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/000-wrappers.R +\name{RTemplate} +\alias{RTemplate} +\title{An OTTR template: a named, parameterized set of instance patterns. +Mirrors py_maplib's \code{Template} (lib/templates/src/python.rs:291-402).} +\usage{ +RTemplate +} +\description{ +An OTTR template: a named, parameterized set of instance patterns. +Mirrors py_maplib's \code{Template} (lib/templates/src/python.rs:291-402). +} diff --git a/maplibr/man/Template.Rd b/maplibr/man/Template.Rd new file mode 100644 index 00000000..f5644a37 --- /dev/null +++ b/maplibr/man/Template.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/templates.R +\name{Template} +\alias{Template} +\title{An OTTR template: a named, parameterized set of instance patterns.} +\usage{ +Template(iri, parameters, instances) +} +\arguments{ +\item{iri}{An IRI.} + +\item{parameters}{A list of Parameters (or bare Variables, auto-wrapped as +required, blank-allowed Parameters with no type constraint).} + +\item{instances}{A list of Instances.} +} +\description{ +Mirrors py_maplib's \code{Template} (lib/templates/src/python.rs:291-402). +} diff --git a/maplibr/man/Triple.Rd b/maplibr/man/Triple.Rd new file mode 100644 index 00000000..1eaa1999 --- /dev/null +++ b/maplibr/man/Triple.Rd @@ -0,0 +1,23 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/templates.R +\name{Triple} +\alias{Triple} +\title{Build an rdf:type-style Triple instance.} +\usage{ +Triple(subject, predicate, object, list_expander = NULL) +} +\arguments{ +\item{subject}{Argument-compatible term.} + +\item{predicate}{Argument-compatible term.} + +\item{object}{Argument-compatible term.} + +\item{list_expander}{Optional: one of "cross", "zipMin", "zipMax".} +} +\value{ +A new Instance. +} +\description{ +Mirrors py_maplib's \code{Triple()} helper (lib/templates/src/python.rs:404-417). +} diff --git a/maplibr/man/Variable.Rd b/maplibr/man/Variable.Rd new file mode 100644 index 00000000..ba585cf2 --- /dev/null +++ b/maplibr/man/Variable.Rd @@ -0,0 +1,14 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/terms.R +\name{Variable} +\alias{Variable} +\title{A SPARQL/OTTR variable.} +\usage{ +Variable(name = character(0)) +} +\arguments{ +\item{name}{The variable's name, without the leading \verb{?}/\code{$}.} +} +\description{ +A SPARQL/OTTR variable. +} diff --git a/maplibr/man/export_test_series.Rd b/maplibr/man/export_test_series.Rd new file mode 100644 index 00000000..2106e717 --- /dev/null +++ b/maplibr/man/export_test_series.Rd @@ -0,0 +1,16 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/000-wrappers.R +\name{export_test_series} +\alias{export_test_series} +\title{Build a small hardcoded i32 Series in Rust and stream it out to R +through the Arrow C Stream Interface.} +\usage{ +export_test_series(stream_ptr) +} +\arguments{ +\item{stream_ptr}{An external pointer from \code{nanoarrow::nanoarrow_allocate_array_stream()}.} +} +\description{ +Build a small hardcoded i32 Series in Rust and stream it out to R +through the Arrow C Stream Interface. +} diff --git a/maplibr/man/export_test_solution_mappings.Rd b/maplibr/man/export_test_solution_mappings.Rd new file mode 100644 index 00000000..fbfe5e2f --- /dev/null +++ b/maplibr/man/export_test_solution_mappings.Rd @@ -0,0 +1,23 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/000-wrappers.R +\name{export_test_solution_mappings} +\alias{export_test_solution_mappings} +\title{Build the \code{build_test_solution_mappings()} fixture and stream it out via +\code{export_solution_mappings} -- proving the DataFrame case reuses the +Series-level mechanism unchanged, against a fixture with a genuinely +multi-typed column (see \code{build_test_solution_mappings}'s doc comment).} +\usage{ +export_test_solution_mappings(stream_ptr) +} +\arguments{ +\item{stream_ptr}{An external pointer from \code{nanoarrow::nanoarrow_allocate_array_stream()}.} +} +\value{ +The \code{rdf_node_types} side-channel, as a JSON string. +} +\description{ +Build the \code{build_test_solution_mappings()} fixture and stream it out via +\code{export_solution_mappings} -- proving the DataFrame case reuses the +Series-level mechanism unchanged, against a fixture with a genuinely +multi-typed column (see \code{build_test_solution_mappings}'s doc comment). +} diff --git a/maplibr/man/import_test_series.Rd b/maplibr/man/import_test_series.Rd new file mode 100644 index 00000000..4c3d1af8 --- /dev/null +++ b/maplibr/man/import_test_series.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/000-wrappers.R +\name{import_test_series} +\alias{import_test_series} +\title{Read an Arrow C stream from R back into a Rust Series, then hand back +its length and first value as a sanity check (i32 only, for this prototype).} +\usage{ +import_test_series(stream_ptr) +} +\arguments{ +\item{stream_ptr}{An external pointer holding a filled ArrowArrayStream.} +} +\value{ +An integer vector: \code{c(length, first_value)}. +} +\description{ +Read an Arrow C stream from R back into a Rust Series, then hand back +its length and first value as a sanity check (i32 only, for this prototype). +} diff --git a/maplibr/man/import_test_solution_mappings.Rd b/maplibr/man/import_test_solution_mappings.Rd new file mode 100644 index 00000000..bee72b63 --- /dev/null +++ b/maplibr/man/import_test_solution_mappings.Rd @@ -0,0 +1,30 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/000-wrappers.R +\name{import_test_solution_mappings} +\alias{import_test_solution_mappings} +\title{Read an Arrow C stream from R back into a Struct-typed Series, unnest it +back into a DataFrame (\code{StructChunked::unnest}, the reverse of +\code{into_struct}), and return a small human-readable summary combining the +recovered DataFrame's shape with the \code{rdf_node_types} JSON side-channel +handed back in -- a sanity check that both halves survive the round trip +together, matching \code{import_test_series}'s role for the Series-only case.} +\usage{ +import_test_solution_mappings(stream_ptr, rdf_node_types_json) +} +\arguments{ +\item{stream_ptr}{An external pointer holding a filled ArrowArrayStream.} + +\item{rdf_node_types_json}{The JSON string returned by +\code{export_test_solution_mappings}.} +} +\value{ +A character vector: one line per column, \code{": []"}. +} +\description{ +Read an Arrow C stream from R back into a Struct-typed Series, unnest it +back into a DataFrame (\code{StructChunked::unnest}, the reverse of +\code{into_struct}), and return a small human-readable summary combining the +recovered DataFrame's shape with the \code{rdf_node_types} JSON side-channel +handed back in -- a sanity check that both halves survive the round trip +together, matching \code{import_test_series}'s role for the Series-only case. +} diff --git a/maplibr/man/instance_template_iri.Rd b/maplibr/man/instance_template_iri.Rd new file mode 100644 index 00000000..be3c49d3 --- /dev/null +++ b/maplibr/man/instance_template_iri.Rd @@ -0,0 +1,14 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/templates.R +\name{instance_template_iri} +\alias{instance_template_iri} +\title{The IRI of the template an Instance calls.} +\usage{ +instance_template_iri(x, ...) +} +\arguments{ +\item{x}{An Instance.} +} +\description{ +The IRI of the template an Instance calls. +} diff --git a/maplibr/man/instantiate.Rd b/maplibr/man/instantiate.Rd new file mode 100644 index 00000000..a0775362 --- /dev/null +++ b/maplibr/man/instantiate.Rd @@ -0,0 +1,21 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/templates.R +\name{instantiate} +\alias{instantiate} +\title{Build a new Instance calling a Template with concrete arguments.} +\usage{ +instantiate(template, ...) +} +\arguments{ +\item{template}{A Template.} + +\item{arguments}{A list of Arguments (or bare terms, auto-wrapped).} + +\item{list_expander}{Optional: one of "cross", "zipMin", "zipMax".} +} +\value{ +A new Instance. +} +\description{ +Mirrors py_maplib's \code{Template.instance()} (lib/templates/src/python.rs:335-346). +} diff --git a/maplibr/man/make_triple.Rd b/maplibr/man/make_triple.Rd new file mode 100644 index 00000000..1f2521a1 --- /dev/null +++ b/maplibr/man/make_triple.Rd @@ -0,0 +1,13 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/000-wrappers.R +\name{make_triple} +\alias{make_triple} +\title{Build an rdf:type-style Triple instance (mirrors py_maplib's \code{Triple()} +helper, lib/templates/src/python.rs:404-417).} +\usage{ +make_triple(subject, predicate, object, list_expander = NULL) +} +\description{ +Build an rdf:type-style Triple instance (mirrors py_maplib's \code{Triple()} +helper, lib/templates/src/python.rs:404-417). +} diff --git a/maplibr/man/model_add_graph.Rd b/maplibr/man/model_add_graph.Rd new file mode 100644 index 00000000..8be0fed6 --- /dev/null +++ b/maplibr/man/model_add_graph.Rd @@ -0,0 +1,26 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-verbs.R +\name{model_add_graph} +\alias{model_add_graph} +\title{Copy a graph from another Model into this one} +\usage{ +model_add_graph(m, other, source_graph = NULL, target_graph = NULL) +} +\arguments{ +\item{m}{A \code{Model}.} + +\item{other}{Another Model to copy a graph from.} + +\item{source_graph}{Optional named graph IRI in \code{other} (default graph if NULL).} + +\item{target_graph}{Optional named graph IRI in \code{m} to copy into (default graph if NULL).} +} +\value{ +\code{m}, invisibly. +} +\description{ +Pipe-friendly wrapper for \code{Model$add_graph()}. +} +\seealso{ +\link{Model} +} diff --git a/maplibr/man/model_add_prefixes.Rd b/maplibr/man/model_add_prefixes.Rd new file mode 100644 index 00000000..fcdfaa22 --- /dev/null +++ b/maplibr/man/model_add_prefixes.Rd @@ -0,0 +1,23 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-verbs.R +\name{model_add_prefixes} +\alias{model_add_prefixes} +\title{Add prefix -> IRI mappings to a Model} +\usage{ +model_add_prefixes(m, prefixes) +} +\arguments{ +\item{m}{A \code{Model}.} + +\item{prefixes}{A named R list of single strings: names are prefixes, +values are IRIs, e.g. \code{list(ex = "http://example.org/")}.} +} +\value{ +\code{m}, invisibly. +} +\description{ +Pipe-friendly wrapper for \code{Model$add_prefixes()}. +} +\seealso{ +\link{Model} +} diff --git a/maplibr/man/model_add_template.Rd b/maplibr/man/model_add_template.Rd new file mode 100644 index 00000000..aef1b459 --- /dev/null +++ b/maplibr/man/model_add_template.Rd @@ -0,0 +1,22 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-verbs.R +\name{model_add_template} +\alias{model_add_template} +\title{Register a Template on a Model} +\usage{ +model_add_template(m, template) +} +\arguments{ +\item{m}{A \code{Model}.} + +\item{template}{A \code{Template} object, or an stOTTR document string.} +} +\value{ +The IRI of the registered template, invisibly. +} +\description{ +Pipe-friendly wrapper for \code{Model$add_template()}. +} +\seealso{ +\link{Model} +} diff --git a/maplibr/man/model_compact.Rd b/maplibr/man/model_compact.Rd new file mode 100644 index 00000000..1238e12c --- /dev/null +++ b/maplibr/man/model_compact.Rd @@ -0,0 +1,20 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-verbs.R +\name{model_compact} +\alias{model_compact} +\title{Compact a Model's on-disk storage} +\usage{ +model_compact(m) +} +\arguments{ +\item{m}{A \code{Model}.} +} +\value{ +\code{m}, invisibly. +} +\description{ +Pipe-friendly wrapper for \code{Model$compact()}. +} +\seealso{ +\link{Model} +} diff --git a/maplibr/man/model_create_index.Rd b/maplibr/man/model_create_index.Rd new file mode 100644 index 00000000..d66bdc98 --- /dev/null +++ b/maplibr/man/model_create_index.Rd @@ -0,0 +1,20 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-verbs.R +\name{model_create_index} +\alias{model_create_index} +\title{Build a Model's default indexes} +\usage{ +model_create_index(m) +} +\arguments{ +\item{m}{A \code{Model}.} +} +\value{ +\code{m}, invisibly. +} +\description{ +Pipe-friendly wrapper for \code{Model$create_index()}. +} +\seealso{ +\link{Model} +} diff --git a/maplibr/man/model_deserialize.Rd b/maplibr/man/model_deserialize.Rd new file mode 100644 index 00000000..3c0e981c --- /dev/null +++ b/maplibr/man/model_deserialize.Rd @@ -0,0 +1,27 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-verbs.R +\name{model_deserialize} +\alias{model_deserialize} +\title{Load a Model previously written by \code{model_serialize()}/\code{Model$serialize()}} +\usage{ +model_deserialize(path = NULL, storage_folder = NULL) +} +\arguments{ +\item{path}{Directory previously written by serialize(). Defaults to +"./serialized_triples".} + +\item{storage_folder}{Optional folder for on-disk (rather than +in-memory) triplestore storage.} +} +\value{ +A new \code{Model}. +} +\description{ +Pipe-friendly wrapper for \code{Model$deserialize()}. Unlike the other +\verb{model_*()} functions, this one has no \code{m} to take as a first argument -- +it creates one -- so it's a pipe \emph{source}, not something to pipe an +existing Model into: \code{model_deserialize(path) |> model_query(...)}. +} +\seealso{ +\link{Model} +} diff --git a/maplibr/man/model_detach_graph.Rd b/maplibr/man/model_detach_graph.Rd new file mode 100644 index 00000000..b6b82e80 --- /dev/null +++ b/maplibr/man/model_detach_graph.Rd @@ -0,0 +1,25 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-verbs.R +\name{model_detach_graph} +\alias{model_detach_graph} +\title{Split a graph out of a Model into a new, standalone Model} +\usage{ +model_detach_graph(m, preserve_name = FALSE, graph = NULL) +} +\arguments{ +\item{m}{A \code{Model}.} + +\item{preserve_name}{Keep the graph's own name in the new Model rather +than moving it to the default graph there.} + +\item{graph}{Optional named graph IRI to detach (default graph if NULL).} +} +\value{ +A new \code{Model} containing only the detached graph. +} +\description{ +Pipe-friendly wrapper for \code{Model$detach_graph()}. +} +\seealso{ +\link{Model} +} diff --git a/maplibr/man/model_infer_rdfs.Rd b/maplibr/man/model_infer_rdfs.Rd new file mode 100644 index 00000000..33dab7ee --- /dev/null +++ b/maplibr/man/model_infer_rdfs.Rd @@ -0,0 +1,22 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-verbs.R +\name{model_infer_rdfs} +\alias{model_infer_rdfs} +\title{Run RDFS inference over a graph in place} +\usage{ +model_infer_rdfs(m, graph = NULL) +} +\arguments{ +\item{m}{A \code{Model}.} + +\item{graph}{Optional named graph IRI to infer over (default graph if NULL).} +} +\value{ +The number of new triples inferred. +} +\description{ +Pipe-friendly wrapper for \code{Model$infer_rdfs()}. +} +\seealso{ +\link{Model} +} diff --git a/maplibr/man/model_insert.Rd b/maplibr/man/model_insert.Rd new file mode 100644 index 00000000..1007a295 --- /dev/null +++ b/maplibr/man/model_insert.Rd @@ -0,0 +1,41 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-verbs.R +\name{model_insert} +\alias{model_insert} +\title{Insert a SPARQL CONSTRUCT query's results as new triples} +\usage{ +model_insert( + m, + query, + source_graph = NULL, + target_graph = NULL, + include_transient = FALSE, + transient = FALSE +) +} +\arguments{ +\item{m}{A \code{Model}.} + +\item{query}{The SPARQL CONSTRUCT query string to source new triples from.} + +\item{source_graph}{Optional named graph IRI to query from (default +graph if NULL).} + +\item{target_graph}{Optional named graph IRI to insert into (default +graph if NULL).} + +\item{include_transient}{Whether the query may match transient (e.g. +inferred) triples in the source graph.} + +\item{transient}{Whether the newly-inserted triples themselves should be +marked transient rather than permanent.} +} +\value{ +\code{m}, invisibly. +} +\description{ +Pipe-friendly wrapper for \code{Model$insert()}. +} +\seealso{ +\link{Model} +} diff --git a/maplibr/man/model_map.Rd b/maplibr/man/model_map.Rd new file mode 100644 index 00000000..731ef4f1 --- /dev/null +++ b/maplibr/man/model_map.Rd @@ -0,0 +1,31 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-verbs.R +\name{model_map} +\alias{model_map} +\title{Expand a template against a data.frame} +\usage{ +model_map(m, template, data = NULL, graph = NULL, validate_iris = NULL) +} +\arguments{ +\item{m}{A \code{Model}.} + +\item{template}{A \code{Template} object, the IRI of an already-registered +template, or an stOTTR document string.} + +\item{data}{Optional data.frame, one row per template instantiation.} + +\item{graph}{Optional named graph IRI to add the resulting triples to +(default graph if NULL).} + +\item{validate_iris}{Whether to validate that IRI-typed columns contain +valid IRIs. Defaults to TRUE.} +} +\value{ +\code{m}, invisibly. +} +\description{ +Pipe-friendly wrapper for \code{Model$map()}. +} +\seealso{ +\link{Model} +} diff --git a/maplibr/man/model_map_df.Rd b/maplibr/man/model_map_df.Rd new file mode 100644 index 00000000..7c386f71 --- /dev/null +++ b/maplibr/man/model_map_df.Rd @@ -0,0 +1,29 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-verbs.R +\name{model_map_df} +\alias{model_map_df} +\title{Map a data.frame's columns directly to triples, one predicate per column} +\usage{ +model_map_df(m, data, graph = NULL, uuid_namespace = NULL) +} +\arguments{ +\item{m}{A \code{Model}.} + +\item{data}{A data.frame, one row per subject; every column becomes a +predicate with that row's value as the object.} + +\item{graph}{Optional named graph IRI to add the resulting triples to +(default graph if NULL).} + +\item{uuid_namespace}{Optional namespace string for the UUIDv5 subject +IRIs minted per row.} +} +\value{ +\code{m}, invisibly. +} +\description{ +Pipe-friendly wrapper for \code{Model$map_df()}. +} +\seealso{ +\link{Model} +} diff --git a/maplibr/man/model_map_json.Rd b/maplibr/man/model_map_json.Rd new file mode 100644 index 00000000..9739c3c4 --- /dev/null +++ b/maplibr/man/model_map_json.Rd @@ -0,0 +1,31 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-verbs.R +\name{model_map_json} +\alias{model_map_json} +\title{Map a JSON file straight to triples via a fixed convention} +\usage{ +model_map_json(m, path, graph = NULL, transient = FALSE, uuid_namespace = NULL) +} +\arguments{ +\item{m}{A \code{Model}.} + +\item{path}{Path to the JSON file to map.} + +\item{graph}{Optional named graph IRI to add the resulting triples to +(default graph if NULL).} + +\item{transient}{Whether the resulting triples should be transient +rather than permanent.} + +\item{uuid_namespace}{Optional namespace string for the UUIDv5 blank +node IRIs minted for JSON objects/arrays.} +} +\value{ +\code{m}, invisibly. +} +\description{ +Pipe-friendly wrapper for \code{Model$map_json()}. +} +\seealso{ +\link{Model} +} diff --git a/maplibr/man/model_map_json_string.Rd b/maplibr/man/model_map_json_string.Rd new file mode 100644 index 00000000..6c1bc463 --- /dev/null +++ b/maplibr/man/model_map_json_string.Rd @@ -0,0 +1,37 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-verbs.R +\name{model_map_json_string} +\alias{model_map_json_string} +\title{Map a JSON string straight to triples via a fixed convention} +\usage{ +model_map_json_string( + m, + json, + graph = NULL, + transient = FALSE, + uuid_namespace = NULL +) +} +\arguments{ +\item{m}{A \code{Model}.} + +\item{json}{The JSON document, as a string.} + +\item{graph}{Optional named graph IRI to add the resulting triples to +(default graph if NULL).} + +\item{transient}{Whether the resulting triples should be transient +rather than permanent.} + +\item{uuid_namespace}{Optional namespace string for the UUIDv5 blank +node IRIs minted for JSON objects/arrays.} +} +\value{ +\code{m}, invisibly. +} +\description{ +Pipe-friendly wrapper for \code{Model$map_json_string()}. +} +\seealso{ +\link{Model} +} diff --git a/maplibr/man/model_map_triples.Rd b/maplibr/man/model_map_triples.Rd new file mode 100644 index 00000000..9bb423c6 --- /dev/null +++ b/maplibr/man/model_map_triples.Rd @@ -0,0 +1,38 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-verbs.R +\name{model_map_triples} +\alias{model_map_triples} +\title{Map a data.frame's subject/predicate/object columns directly to triples} +\usage{ +model_map_triples( + m, + data, + predicate = NULL, + graph = NULL, + validate_iris = NULL +) +} +\arguments{ +\item{m}{A \code{Model}.} + +\item{data}{A data.frame with subject/predicate/object columns (or just +subject/object if \code{predicate} is given).} + +\item{predicate}{Optional constant predicate IRI to use for every row, +instead of a \code{predicate} column in \code{data}.} + +\item{graph}{Optional named graph IRI to add the resulting triples to +(default graph if NULL).} + +\item{validate_iris}{Whether to validate that IRI-typed columns contain +valid IRIs. Defaults to TRUE.} +} +\value{ +\code{m}, invisibly. +} +\description{ +Pipe-friendly wrapper for \code{Model$map_triples()}. +} +\seealso{ +\link{Model} +} diff --git a/maplibr/man/model_map_xml.Rd b/maplibr/man/model_map_xml.Rd new file mode 100644 index 00000000..16bff214 --- /dev/null +++ b/maplibr/man/model_map_xml.Rd @@ -0,0 +1,31 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-verbs.R +\name{model_map_xml} +\alias{model_map_xml} +\title{Map an XML file straight to triples via a fixed convention} +\usage{ +model_map_xml(m, path, graph = NULL, transient = FALSE, uuid_namespace = NULL) +} +\arguments{ +\item{m}{A \code{Model}.} + +\item{path}{Path to the XML file to map.} + +\item{graph}{Optional named graph IRI to add the resulting triples to +(default graph if NULL).} + +\item{transient}{Whether the resulting triples should be transient +rather than permanent.} + +\item{uuid_namespace}{Optional namespace string for the UUIDv5 blank +node IRIs minted for XML elements.} +} +\value{ +\code{m}, invisibly. +} +\description{ +Pipe-friendly wrapper for \code{Model$map_xml()}. +} +\seealso{ +\link{Model} +} diff --git a/maplibr/man/model_map_xml_string.Rd b/maplibr/man/model_map_xml_string.Rd new file mode 100644 index 00000000..3c46cf12 --- /dev/null +++ b/maplibr/man/model_map_xml_string.Rd @@ -0,0 +1,37 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-verbs.R +\name{model_map_xml_string} +\alias{model_map_xml_string} +\title{Map an XML string straight to triples via a fixed convention} +\usage{ +model_map_xml_string( + m, + xml, + graph = NULL, + transient = FALSE, + uuid_namespace = NULL +) +} +\arguments{ +\item{m}{A \code{Model}.} + +\item{xml}{The XML document, as a string.} + +\item{graph}{Optional named graph IRI to add the resulting triples to +(default graph if NULL).} + +\item{transient}{Whether the resulting triples should be transient +rather than permanent.} + +\item{uuid_namespace}{Optional namespace string for the UUIDv5 blank +node IRIs minted for XML elements.} +} +\value{ +\code{m}, invisibly. +} +\description{ +Pipe-friendly wrapper for \code{Model$map_xml_string()}. +} +\seealso{ +\link{Model} +} diff --git a/maplibr/man/model_query.Rd b/maplibr/man/model_query.Rd new file mode 100644 index 00000000..20e28a1f --- /dev/null +++ b/maplibr/man/model_query.Rd @@ -0,0 +1,37 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-verbs.R +\name{model_query} +\alias{model_query} +\title{Run a SPARQL SELECT or CONSTRUCT query} +\usage{ +model_query( + m, + sparql, + graph = NULL, + include_transient = FALSE, + bindings = NULL +) +} +\arguments{ +\item{m}{A \code{Model}.} + +\item{sparql}{The SPARQL query string.} + +\item{graph}{Optional named graph IRI to restrict the query to (searches +across the whole store if NULL).} + +\item{include_transient}{Whether to include transient (e.g. inferred) +triples in the query.} + +\item{bindings}{Optional named list of \code{IRI}/\code{Literal} objects, pre-binding +SPARQL variables to fixed values before the query runs.} +} +\value{ +A data.frame. +} +\description{ +Pipe-friendly wrapper for \code{Model$query()}. +} +\seealso{ +\link{Model} +} diff --git a/maplibr/man/model_read.Rd b/maplibr/man/model_read.Rd new file mode 100644 index 00000000..67e5d05e --- /dev/null +++ b/maplibr/man/model_read.Rd @@ -0,0 +1,27 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-verbs.R +\name{model_read} +\alias{model_read} +\title{Parse RDF triples from a file into a Model} +\usage{ +model_read(m, path, format = NULL, graph = NULL) +} +\arguments{ +\item{m}{A \code{Model}.} + +\item{path}{Path to the RDF file to read.} + +\item{format}{One of "ntriples", "turtle", "xml" (rdf/xml). Guessed from +the file extension if NULL.} + +\item{graph}{Optional named graph IRI to read into (default graph if NULL).} +} +\value{ +\code{m}, invisibly. +} +\description{ +Pipe-friendly wrapper for \code{Model$read()}. +} +\seealso{ +\link{Model} +} diff --git a/maplibr/man/model_reads.Rd b/maplibr/man/model_reads.Rd new file mode 100644 index 00000000..23b52b2d --- /dev/null +++ b/maplibr/man/model_reads.Rd @@ -0,0 +1,26 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-verbs.R +\name{model_reads} +\alias{model_reads} +\title{Parse RDF triples from a string into a Model} +\usage{ +model_reads(m, s, format, graph = NULL) +} +\arguments{ +\item{m}{A \code{Model}.} + +\item{s}{RDF data as a string.} + +\item{format}{One of "ntriples", "turtle", "xml" (rdf/xml).} + +\item{graph}{Optional named graph IRI to read into (default graph if NULL).} +} +\value{ +\code{m}, invisibly. +} +\description{ +Pipe-friendly wrapper for \code{Model$reads()}. +} +\seealso{ +\link{Model} +} diff --git a/maplibr/man/model_serialize.Rd b/maplibr/man/model_serialize.Rd new file mode 100644 index 00000000..9d7b6316 --- /dev/null +++ b/maplibr/man/model_serialize.Rd @@ -0,0 +1,22 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-verbs.R +\name{model_serialize} +\alias{model_serialize} +\title{Serialize a Model's triples to maplib's own compact on-disk format} +\usage{ +model_serialize(m, path = NULL) +} +\arguments{ +\item{m}{A \code{Model}.} + +\item{path}{Directory to serialize into. Defaults to "./serialized_triples".} +} +\value{ +\code{m}, invisibly. +} +\description{ +Pipe-friendly wrapper for \code{Model$serialize()}. +} +\seealso{ +\link{Model} +} diff --git a/maplibr/man/model_size.Rd b/maplibr/man/model_size.Rd new file mode 100644 index 00000000..7641652a --- /dev/null +++ b/maplibr/man/model_size.Rd @@ -0,0 +1,22 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-verbs.R +\name{model_size} +\alias{model_size} +\title{Number of triples in a graph} +\usage{ +model_size(m, graph = NULL) +} +\arguments{ +\item{m}{A \code{Model}.} + +\item{graph}{Optional named graph IRI to count (default graph if NULL).} +} +\value{ +An integer triple count. +} +\description{ +Pipe-friendly wrapper for \code{Model$size()}. +} +\seealso{ +\link{Model} +} diff --git a/maplibr/man/model_truncate_graph.Rd b/maplibr/man/model_truncate_graph.Rd new file mode 100644 index 00000000..5ebc783d --- /dev/null +++ b/maplibr/man/model_truncate_graph.Rd @@ -0,0 +1,22 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-verbs.R +\name{model_truncate_graph} +\alias{model_truncate_graph} +\title{Remove all triples from a graph} +\usage{ +model_truncate_graph(m, graph = NULL) +} +\arguments{ +\item{m}{A \code{Model}.} + +\item{graph}{Optional named graph IRI to truncate (default graph if NULL).} +} +\value{ +\code{m}, invisibly. +} +\description{ +Pipe-friendly wrapper for \code{Model$truncate_graph()}. +} +\seealso{ +\link{Model} +} diff --git a/maplibr/man/model_update.Rd b/maplibr/man/model_update.Rd new file mode 100644 index 00000000..a136e9d6 --- /dev/null +++ b/maplibr/man/model_update.Rd @@ -0,0 +1,28 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-verbs.R +\name{model_update} +\alias{model_update} +\title{Run a SPARQL UPDATE against a Model} +\usage{ +model_update(m, update, graph = NULL, include_transient = FALSE) +} +\arguments{ +\item{m}{A \code{Model}.} + +\item{update}{The SPARQL UPDATE string.} + +\item{graph}{Optional named graph IRI to restrict the update to (no +restriction, i.e. the whole store, if NULL).} + +\item{include_transient}{Whether the WHERE clause may match transient +(e.g. inferred) triples.} +} +\value{ +\code{m}, invisibly. +} +\description{ +Pipe-friendly wrapper for \code{Model$update()}. +} +\seealso{ +\link{Model} +} diff --git a/maplibr/man/model_write.Rd b/maplibr/man/model_write.Rd new file mode 100644 index 00000000..3570c63e --- /dev/null +++ b/maplibr/man/model_write.Rd @@ -0,0 +1,26 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-verbs.R +\name{model_write} +\alias{model_write} +\title{Serialize a Model's triples to a file} +\usage{ +model_write(m, path, format = NULL, graph = NULL) +} +\arguments{ +\item{m}{A \code{Model}.} + +\item{path}{Path to write the RDF file to (overwritten if it exists).} + +\item{format}{One of "ntriples", "turtle", "xml" (rdf/xml). Defaults to "ntriples".} + +\item{graph}{Optional named graph IRI to write (default graph if NULL).} +} +\value{ +\code{m}, invisibly. +} +\description{ +Pipe-friendly wrapper for \code{Model$write()}. +} +\seealso{ +\link{Model} +} diff --git a/maplibr/man/model_writes.Rd b/maplibr/man/model_writes.Rd new file mode 100644 index 00000000..d63fb22f --- /dev/null +++ b/maplibr/man/model_writes.Rd @@ -0,0 +1,24 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-verbs.R +\name{model_writes} +\alias{model_writes} +\title{Serialize a Model's triples to a string} +\usage{ +model_writes(m, format = NULL, graph = NULL) +} +\arguments{ +\item{m}{A \code{Model}.} + +\item{format}{One of "ntriples", "turtle", "xml" (rdf/xml). Defaults to "ntriples".} + +\item{graph}{Optional named graph IRI to write (default graph if NULL).} +} +\value{ +A string. +} +\description{ +Pipe-friendly wrapper for \code{Model$writes()}. +} +\seealso{ +\link{Model} +} diff --git a/maplibr/man/owl.Rd b/maplibr/man/owl.Rd new file mode 100644 index 00000000..c5f9cfd9 --- /dev/null +++ b/maplibr/man/owl.Rd @@ -0,0 +1,14 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/vocab.R +\name{owl} +\alias{owl} +\title{The owl vocabulary: a named list of IRIs.} +\format{ +A named list of IRI objects. +} +\usage{ +owl +} +\description{ +The owl vocabulary: a named list of IRIs. +} diff --git a/maplibr/man/rdf.Rd b/maplibr/man/rdf.Rd new file mode 100644 index 00000000..74b083fe --- /dev/null +++ b/maplibr/man/rdf.Rd @@ -0,0 +1,14 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/vocab.R +\name{rdf} +\alias{rdf} +\title{The rdf vocabulary: a named list of IRIs.} +\format{ +A named list of IRI objects. +} +\usage{ +rdf +} +\description{ +The rdf vocabulary: a named list of IRIs. +} diff --git a/maplibr/man/rdfs.Rd b/maplibr/man/rdfs.Rd new file mode 100644 index 00000000..84234187 --- /dev/null +++ b/maplibr/man/rdfs.Rd @@ -0,0 +1,14 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/vocab.R +\name{rdfs} +\alias{rdfs} +\title{The rdfs vocabulary: a named list of IRIs.} +\format{ +A named list of IRI objects. +} +\usage{ +rdfs +} +\description{ +The rdfs vocabulary: a named list of IRIs. +} diff --git a/maplibr/man/suf.Rd b/maplibr/man/suf.Rd new file mode 100644 index 00000000..4858fda4 --- /dev/null +++ b/maplibr/man/suf.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/terms.R +\name{suf} +\alias{suf} +\title{Suffix a Prefix's IRI, returning a new IRI.} +\usage{ +suf(prefix, ...) +} +\arguments{ +\item{prefix}{A Prefix.} + +\item{suffix}{String to append to the prefix's IRI.} +} +\value{ +A new IRI. +} +\description{ +Mirrors py_maplib's \code{Prefix.suf()} (lib/representation/src/python.rs:326-328). +} diff --git a/maplibr/man/validate_blank_node_id.Rd b/maplibr/man/validate_blank_node_id.Rd new file mode 100644 index 00000000..f61ff031 --- /dev/null +++ b/maplibr/man/validate_blank_node_id.Rd @@ -0,0 +1,18 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/000-wrappers.R +\name{validate_blank_node_id} +\alias{validate_blank_node_id} +\title{Validate a blank node identifier's syntax (mirrors oxrdf::BlankNode::new, +used by py_maplib's PyBlankNode::new, lib/representation/src/python.rs:531-535). +Raises an R error on invalid syntax; returns nothing on success.} +\usage{ +validate_blank_node_id(id) +} +\arguments{ +\item{id}{Blank node identifier, without the leading \verb{_:}.} +} +\description{ +Validate a blank node identifier's syntax (mirrors oxrdf::BlankNode::new, +used by py_maplib's PyBlankNode::new, lib/representation/src/python.rs:531-535). +Raises an R error on invalid syntax; returns nothing on success. +} diff --git a/maplibr/man/validate_iri.Rd b/maplibr/man/validate_iri.Rd new file mode 100644 index 00000000..2cb00c49 --- /dev/null +++ b/maplibr/man/validate_iri.Rd @@ -0,0 +1,18 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/000-wrappers.R +\name{validate_iri} +\alias{validate_iri} +\title{Validate an IRI's syntax (mirrors oxrdf::NamedNode::new, the same check +py_maplib's PyIRI::new uses, lib/representation/src/python.rs:279-282). +Raises an R error on invalid syntax; returns nothing on success.} +\usage{ +validate_iri(iri) +} +\arguments{ +\item{iri}{IRI string to validate.} +} +\description{ +Validate an IRI's syntax (mirrors oxrdf::NamedNode::new, the same check +py_maplib's PyIRI::new uses, lib/representation/src/python.rs:279-282). +Raises an R error on invalid syntax; returns nothing on success. +} diff --git a/maplibr/man/validate_variable_name.Rd b/maplibr/man/validate_variable_name.Rd new file mode 100644 index 00000000..3cd1ea8a --- /dev/null +++ b/maplibr/man/validate_variable_name.Rd @@ -0,0 +1,18 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/000-wrappers.R +\name{validate_variable_name} +\alias{validate_variable_name} +\title{Validate a SPARQL variable name's syntax (mirrors oxrdf::Variable::new, +used by py_maplib's PyVariable::new, lib/representation/src/python.rs:340-343). +Raises an R error on invalid syntax; returns nothing on success.} +\usage{ +validate_variable_name(name) +} +\arguments{ +\item{name}{Variable name, without the leading \verb{?}/\code{$}.} +} +\description{ +Validate a SPARQL variable name's syntax (mirrors oxrdf::Variable::new, +used by py_maplib's PyVariable::new, lib/representation/src/python.rs:340-343). +Raises an R error on invalid syntax; returns nothing on success. +} diff --git a/maplibr/man/xsd.Rd b/maplibr/man/xsd.Rd new file mode 100644 index 00000000..e02c57e4 --- /dev/null +++ b/maplibr/man/xsd.Rd @@ -0,0 +1,14 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/vocab.R +\name{xsd} +\alias{xsd} +\title{The xsd vocabulary: a named list of IRIs.} +\format{ +A named list of IRI objects. +} +\usage{ +xsd +} +\description{ +The xsd vocabulary: a named list of IRIs. +} diff --git a/maplibr/src/.gitignore b/maplibr/src/.gitignore new file mode 100644 index 00000000..f1265a23 --- /dev/null +++ b/maplibr/src/.gitignore @@ -0,0 +1,7 @@ +*.o +*.so +*.dll +target + +Makevars +Makevars.win diff --git a/maplibr/src/Makevars.in b/maplibr/src/Makevars.in new file mode 100644 index 00000000..75f6df64 --- /dev/null +++ b/maplibr/src/Makevars.in @@ -0,0 +1,44 @@ +TARGET = @TARGET@ + +PROFILE = @PROFILE@ +FEATURE_FLAGS = @FEATURE_FLAGS@ + +# Add flags if necessary +RUSTFLAGS = + +TARGET_DIR = $(CURDIR)/rust/target +LIBDIR = $(TARGET_DIR)/$(TARGET)/$(subst dev,debug,$(PROFILE)) +STATLIB = $(LIBDIR)/libmaplibr.a +PKG_LIBS = -L$(LIBDIR) -lmaplibr + +CARGO_BUILD_ARGS = --lib --profile $(PROFILE) $(FEATURE_FLAGS) --manifest-path=./rust/Cargo.toml --target-dir $(TARGET_DIR) + +all: $(SHLIB) clean_intermediate + +$(SHLIB): $(STATLIB) + +$(STATLIB): + # In some environments, ~/.cargo/bin might not be included in PATH, so we need + # to set it here to ensure cargo can be invoked. It is appended to PATH and + # therefore is only used if cargo is absent from the user's PATH. + export PATH="$(PATH):$(HOME)/.cargo/bin" && \ + export CC="$(CC)" && \ + export CFLAGS="$(CFLAGS)" && \ + export RUSTFLAGS="$(RUSTFLAGS)" && \ + @BEFORE_CARGO_BUILD@ \ + if [ "$(TARGET)" != "wasm32-unknown-emscripten" ]; then \ + cargo build $(CARGO_BUILD_ARGS); \ + else \ + export CARGO_PROFILE_DEV_PANIC="abort" && \ + export CARGO_PROFILE_RELEASE_PANIC="abort" && \ + export RUSTFLAGS="$(RUSTFLAGS) -Zdefault-visibility=hidden" && \ + cargo +nightly build $(CARGO_BUILD_ARGS) --target $(TARGET) -Zbuild-std=panic_abort,std; \ + fi + +clean_intermediate: $(SHLIB) + rm -f $(STATLIB) + +clean: + rm -Rf $(SHLIB) $(OBJECTS) $(STATLIB) ./rust/target + +.PHONY: all clean_intermediate clean diff --git a/maplibr/src/Makevars.win.in b/maplibr/src/Makevars.win.in new file mode 100644 index 00000000..f17aaa8b --- /dev/null +++ b/maplibr/src/Makevars.win.in @@ -0,0 +1,41 @@ +TARGET = @TARGET@ + +PROFILE = @PROFILE@ +FEATURE_FLAGS = @FEATURE_FLAGS@ + +# Add flags if necessary +RUSTFLAGS = + +TARGET_DIR = $(CURDIR)/rust/target +LIBDIR = $(TARGET_DIR)/$(TARGET)/$(subst dev,debug,$(PROFILE)) +STATLIB = $(LIBDIR)/libmaplibr.a +PKG_LIBS = -L$(LIBDIR) -lmaplibr -lws2_32 -ladvapi32 -luserenv -lbcrypt -lntdll + +# Rtools doesn't have the linker in the location that cargo expects, so we need +# to overwrite it via configuration. +CARGO_LINKER = x86_64-w64-mingw32.static.posix-gcc.exe + +all: $(SHLIB) clean_intermediate + +$(SHLIB): $(STATLIB) + +$(STATLIB): + # When the GNU toolchain is used (i.e. on CRAN), -lgcc_eh is specified for + # building proc-macro2, but Rtools doesn't contain libgcc_eh. This isn't used + # in actual, but we need this tweak to please the compiler. + mkdir -p $(LIBDIR)/libgcc_mock && touch $(LIBDIR)/libgcc_mock/libgcc_eh.a + + export CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER="$(CARGO_LINKER)" && \ + export LIBRARY_PATH="$${LIBRARY_PATH};$(LIBDIR)/libgcc_mock" && \ + export CC="$(CC)" && \ + export CFLAGS="$(CFLAGS)" && \ + export RUSTFLAGS="$(RUSTFLAGS)" && \ + cargo build --target $(TARGET) --lib --profile $(PROFILE) $(FEATURE_FLAGS) --manifest-path ./rust/Cargo.toml --target-dir $(TARGET_DIR) + +clean_intermediate: $(SHLIB) + rm -f $(STATLIB) + +clean: + rm -Rf $(SHLIB) $(OBJECTS) $(STATLIB) ./rust/target + +.PHONY: all clean_intermediate clean diff --git a/maplibr/src/init.c b/maplibr/src/init.c new file mode 100644 index 00000000..8fa53843 --- /dev/null +++ b/maplibr/src/init.c @@ -0,0 +1,359 @@ + +// clang-format sorts includes unless SortIncludes: Never. However, the ordering +// does matter here. So, we need to disable clang-format for safety. + +// clang-format off +#include +#include +#include +// clang-format on + +#include "rust/api.h" + +static uintptr_t TAGGED_POINTER_MASK = (uintptr_t)1; + +SEXP handle_result(SEXP res_) { + uintptr_t res = (uintptr_t)res_; + + // An error is indicated by tag. + if ((res & TAGGED_POINTER_MASK) == 1) { + // Remove tag + SEXP res_aligned = (SEXP)(res & ~TAGGED_POINTER_MASK); + + // Currently, there are two types of error cases: + // + // 1. Error from Rust code + // 2. Error from R's C API, which is caught by R_UnwindProtect() + // + if (TYPEOF(res_aligned) == CHARSXP) { + // In case 1, the result is an error message that can be passed to + // Rf_errorcall() directly. + Rf_errorcall(R_NilValue, "%s", CHAR(res_aligned)); + } else { + // In case 2, the result is the token to restart the + // cleanup process on R's side. + R_ContinueUnwind(res_aligned); + } + } + + return (SEXP)res; +} + +SEXP savvy_export_test_series__impl(SEXP c_arg__stream_ptr) { + SEXP res = savvy_export_test_series__ffi(c_arg__stream_ptr); + return handle_result(res); +} + +SEXP savvy_export_test_solution_mappings__impl(SEXP c_arg__stream_ptr) { + SEXP res = savvy_export_test_solution_mappings__ffi(c_arg__stream_ptr); + return handle_result(res); +} + +SEXP savvy_import_test_series__impl(SEXP c_arg__stream_ptr) { + SEXP res = savvy_import_test_series__ffi(c_arg__stream_ptr); + return handle_result(res); +} + +SEXP savvy_import_test_solution_mappings__impl(SEXP c_arg__stream_ptr, SEXP c_arg__rdf_node_types_json) { + SEXP res = savvy_import_test_solution_mappings__ffi(c_arg__stream_ptr, c_arg__rdf_node_types_json); + return handle_result(res); +} + +SEXP savvy_make_triple__impl(SEXP c_arg__subject, SEXP c_arg__predicate, SEXP c_arg__object, SEXP c_arg__list_expander) { + SEXP res = savvy_make_triple__ffi(c_arg__subject, c_arg__predicate, c_arg__object, c_arg__list_expander); + return handle_result(res); +} + +SEXP savvy_validate_blank_node_id__impl(SEXP c_arg__id) { + SEXP res = savvy_validate_blank_node_id__ffi(c_arg__id); + return handle_result(res); +} + +SEXP savvy_validate_iri__impl(SEXP c_arg__iri) { + SEXP res = savvy_validate_iri__ffi(c_arg__iri); + return handle_result(res); +} + +SEXP savvy_validate_variable_name__impl(SEXP c_arg__name) { + SEXP res = savvy_validate_variable_name__ffi(c_arg__name); + return handle_result(res); +} + +SEXP savvy_RArgument_from_constant_term__impl(SEXP c_arg__term, SEXP c_arg__list_expand) { + SEXP res = savvy_RArgument_from_constant_term__ffi(c_arg__term, c_arg__list_expand); + return handle_result(res); +} + +SEXP savvy_RArgument_from_variable__impl(SEXP c_arg__name, SEXP c_arg__list_expand) { + SEXP res = savvy_RArgument_from_variable__ffi(c_arg__name, c_arg__list_expand); + return handle_result(res); +} + +SEXP savvy_RConstantTerm_blank_node__impl(SEXP c_arg__id) { + SEXP res = savvy_RConstantTerm_blank_node__ffi(c_arg__id); + return handle_result(res); +} + +SEXP savvy_RConstantTerm_iri__impl(SEXP c_arg__iri) { + SEXP res = savvy_RConstantTerm_iri__ffi(c_arg__iri); + return handle_result(res); +} + +SEXP savvy_RConstantTerm_literal__impl(SEXP c_arg__value, SEXP c_arg__datatype_iri, SEXP c_arg__language) { + SEXP res = savvy_RConstantTerm_literal__ffi(c_arg__value, c_arg__datatype_iri, c_arg__language); + return handle_result(res); +} + +SEXP savvy_RConstantTerm_none__impl(void) { + SEXP res = savvy_RConstantTerm_none__ffi(); + return handle_result(res); +} + +SEXP savvy_RGroundTerm_iri__impl(SEXP c_arg__iri) { + SEXP res = savvy_RGroundTerm_iri__ffi(c_arg__iri); + return handle_result(res); +} + +SEXP savvy_RGroundTerm_literal__impl(SEXP c_arg__value, SEXP c_arg__datatype_iri, SEXP c_arg__language) { + SEXP res = savvy_RGroundTerm_literal__ffi(c_arg__value, c_arg__datatype_iri, c_arg__language); + return handle_result(res); +} + +SEXP savvy_RInstance_iri__impl(SEXP self__) { + SEXP res = savvy_RInstance_iri__ffi(self__); + return handle_result(res); +} + +SEXP savvy_RInstance_new__impl(SEXP c_arg__template_iri, SEXP c_arg__arguments, SEXP c_arg__list_expander) { + SEXP res = savvy_RInstance_new__ffi(c_arg__template_iri, c_arg__arguments, c_arg__list_expander); + return handle_result(res); +} + +SEXP savvy_RModel_add_graph__impl(SEXP self__, SEXP c_arg__other, SEXP c_arg__source_graph, SEXP c_arg__target_graph) { + SEXP res = savvy_RModel_add_graph__ffi(self__, c_arg__other, c_arg__source_graph, c_arg__target_graph); + return handle_result(res); +} + +SEXP savvy_RModel_add_prefixes__impl(SEXP self__, SEXP c_arg__prefixes) { + SEXP res = savvy_RModel_add_prefixes__ffi(self__, c_arg__prefixes); + return handle_result(res); +} + +SEXP savvy_RModel_add_template__impl(SEXP self__, SEXP c_arg__template) { + SEXP res = savvy_RModel_add_template__ffi(self__, c_arg__template); + return handle_result(res); +} + +SEXP savvy_RModel_add_template_string__impl(SEXP self__, SEXP c_arg__doc) { + SEXP res = savvy_RModel_add_template_string__ffi(self__, c_arg__doc); + return handle_result(res); +} + +SEXP savvy_RModel_compact__impl(SEXP self__) { + SEXP res = savvy_RModel_compact__ffi(self__); + return handle_result(res); +} + +SEXP savvy_RModel_create_index__impl(SEXP self__) { + SEXP res = savvy_RModel_create_index__ffi(self__); + return handle_result(res); +} + +SEXP savvy_RModel_deserialize__impl(SEXP c_arg__path, SEXP c_arg__storage_folder) { + SEXP res = savvy_RModel_deserialize__ffi(c_arg__path, c_arg__storage_folder); + return handle_result(res); +} + +SEXP savvy_RModel_detach_graph__impl(SEXP self__, SEXP c_arg__preserve_name, SEXP c_arg__graph) { + SEXP res = savvy_RModel_detach_graph__ffi(self__, c_arg__preserve_name, c_arg__graph); + return handle_result(res); +} + +SEXP savvy_RModel_infer_rdfs__impl(SEXP self__, SEXP c_arg__graph) { + SEXP res = savvy_RModel_infer_rdfs__ffi(self__, c_arg__graph); + return handle_result(res); +} + +SEXP savvy_RModel_insert__impl(SEXP self__, SEXP c_arg__query, SEXP c_arg__include_transient, SEXP c_arg__transient, SEXP c_arg__source_graph, SEXP c_arg__target_graph) { + SEXP res = savvy_RModel_insert__ffi(self__, c_arg__query, c_arg__include_transient, c_arg__transient, c_arg__source_graph, c_arg__target_graph); + return handle_result(res); +} + +SEXP savvy_RModel_map__impl(SEXP self__, SEXP c_arg__template_iri, SEXP c_arg__stream_ptr, SEXP c_arg__graph, SEXP c_arg__validate_iris) { + SEXP res = savvy_RModel_map__ffi(self__, c_arg__template_iri, c_arg__stream_ptr, c_arg__graph, c_arg__validate_iris); + return handle_result(res); +} + +SEXP savvy_RModel_map_df__impl(SEXP self__, SEXP c_arg__stream_ptr, SEXP c_arg__graph, SEXP c_arg__uuid_namespace) { + SEXP res = savvy_RModel_map_df__ffi(self__, c_arg__stream_ptr, c_arg__graph, c_arg__uuid_namespace); + return handle_result(res); +} + +SEXP savvy_RModel_map_json__impl(SEXP self__, SEXP c_arg__path, SEXP c_arg__transient, SEXP c_arg__graph, SEXP c_arg__uuid_namespace) { + SEXP res = savvy_RModel_map_json__ffi(self__, c_arg__path, c_arg__transient, c_arg__graph, c_arg__uuid_namespace); + return handle_result(res); +} + +SEXP savvy_RModel_map_json_string__impl(SEXP self__, SEXP c_arg__json, SEXP c_arg__transient, SEXP c_arg__graph, SEXP c_arg__uuid_namespace) { + SEXP res = savvy_RModel_map_json_string__ffi(self__, c_arg__json, c_arg__transient, c_arg__graph, c_arg__uuid_namespace); + return handle_result(res); +} + +SEXP savvy_RModel_map_no_data__impl(SEXP self__, SEXP c_arg__template_iri, SEXP c_arg__graph, SEXP c_arg__validate_iris) { + SEXP res = savvy_RModel_map_no_data__ffi(self__, c_arg__template_iri, c_arg__graph, c_arg__validate_iris); + return handle_result(res); +} + +SEXP savvy_RModel_map_triples__impl(SEXP self__, SEXP c_arg__stream_ptr, SEXP c_arg__predicate, SEXP c_arg__graph, SEXP c_arg__validate_iris) { + SEXP res = savvy_RModel_map_triples__ffi(self__, c_arg__stream_ptr, c_arg__predicate, c_arg__graph, c_arg__validate_iris); + return handle_result(res); +} + +SEXP savvy_RModel_map_xml__impl(SEXP self__, SEXP c_arg__path, SEXP c_arg__transient, SEXP c_arg__graph, SEXP c_arg__uuid_namespace) { + SEXP res = savvy_RModel_map_xml__ffi(self__, c_arg__path, c_arg__transient, c_arg__graph, c_arg__uuid_namespace); + return handle_result(res); +} + +SEXP savvy_RModel_map_xml_string__impl(SEXP self__, SEXP c_arg__xml, SEXP c_arg__transient, SEXP c_arg__graph, SEXP c_arg__uuid_namespace) { + SEXP res = savvy_RModel_map_xml_string__ffi(self__, c_arg__xml, c_arg__transient, c_arg__graph, c_arg__uuid_namespace); + return handle_result(res); +} + +SEXP savvy_RModel_new__impl(void) { + SEXP res = savvy_RModel_new__ffi(); + return handle_result(res); +} + +SEXP savvy_RModel_query__impl(SEXP self__, SEXP c_arg__sparql, SEXP c_arg__stream_ptr, SEXP c_arg__include_transient, SEXP c_arg__graph, SEXP c_arg__bindings) { + SEXP res = savvy_RModel_query__ffi(self__, c_arg__sparql, c_arg__stream_ptr, c_arg__include_transient, c_arg__graph, c_arg__bindings); + return handle_result(res); +} + +SEXP savvy_RModel_read__impl(SEXP self__, SEXP c_arg__path, SEXP c_arg__format, SEXP c_arg__graph) { + SEXP res = savvy_RModel_read__ffi(self__, c_arg__path, c_arg__format, c_arg__graph); + return handle_result(res); +} + +SEXP savvy_RModel_reads__impl(SEXP self__, SEXP c_arg__s, SEXP c_arg__format, SEXP c_arg__graph) { + SEXP res = savvy_RModel_reads__ffi(self__, c_arg__s, c_arg__format, c_arg__graph); + return handle_result(res); +} + +SEXP savvy_RModel_serialize__impl(SEXP self__, SEXP c_arg__path) { + SEXP res = savvy_RModel_serialize__ffi(self__, c_arg__path); + return handle_result(res); +} + +SEXP savvy_RModel_size__impl(SEXP self__, SEXP c_arg__graph) { + SEXP res = savvy_RModel_size__ffi(self__, c_arg__graph); + return handle_result(res); +} + +SEXP savvy_RModel_truncate_graph__impl(SEXP self__, SEXP c_arg__graph) { + SEXP res = savvy_RModel_truncate_graph__ffi(self__, c_arg__graph); + return handle_result(res); +} + +SEXP savvy_RModel_update__impl(SEXP self__, SEXP c_arg__update, SEXP c_arg__include_transient, SEXP c_arg__graph) { + SEXP res = savvy_RModel_update__ffi(self__, c_arg__update, c_arg__include_transient, c_arg__graph); + return handle_result(res); +} + +SEXP savvy_RModel_write__impl(SEXP self__, SEXP c_arg__path, SEXP c_arg__format, SEXP c_arg__graph) { + SEXP res = savvy_RModel_write__ffi(self__, c_arg__path, c_arg__format, c_arg__graph); + return handle_result(res); +} + +SEXP savvy_RModel_writes__impl(SEXP self__, SEXP c_arg__format, SEXP c_arg__graph) { + SEXP res = savvy_RModel_writes__ffi(self__, c_arg__format, c_arg__graph); + return handle_result(res); +} + +SEXP savvy_RParameter_new__impl(SEXP c_arg__variable_name, SEXP c_arg__optional, SEXP c_arg__allow_blank, SEXP c_arg__rdf_type_iri, SEXP c_arg__default_value) { + SEXP res = savvy_RParameter_new__ffi(c_arg__variable_name, c_arg__optional, c_arg__allow_blank, c_arg__rdf_type_iri, c_arg__default_value); + return handle_result(res); +} + +SEXP savvy_RTemplate_instance__impl(SEXP self__, SEXP c_arg__arguments, SEXP c_arg__list_expander) { + SEXP res = savvy_RTemplate_instance__ffi(self__, c_arg__arguments, c_arg__list_expander); + return handle_result(res); +} + +SEXP savvy_RTemplate_iri__impl(SEXP self__) { + SEXP res = savvy_RTemplate_iri__ffi(self__); + return handle_result(res); +} + +SEXP savvy_RTemplate_new__impl(SEXP c_arg__iri, SEXP c_arg__parameters, SEXP c_arg__instances) { + SEXP res = savvy_RTemplate_new__ffi(c_arg__iri, c_arg__parameters, c_arg__instances); + return handle_result(res); +} + +SEXP savvy_RTemplate_print_string__impl(SEXP self__) { + SEXP res = savvy_RTemplate_print_string__ffi(self__); + return handle_result(res); +} + + +static const R_CallMethodDef CallEntries[] = { + {"savvy_export_test_series__impl", (DL_FUNC) &savvy_export_test_series__impl, 1}, + {"savvy_export_test_solution_mappings__impl", (DL_FUNC) &savvy_export_test_solution_mappings__impl, 1}, + {"savvy_import_test_series__impl", (DL_FUNC) &savvy_import_test_series__impl, 1}, + {"savvy_import_test_solution_mappings__impl", (DL_FUNC) &savvy_import_test_solution_mappings__impl, 2}, + {"savvy_make_triple__impl", (DL_FUNC) &savvy_make_triple__impl, 4}, + {"savvy_validate_blank_node_id__impl", (DL_FUNC) &savvy_validate_blank_node_id__impl, 1}, + {"savvy_validate_iri__impl", (DL_FUNC) &savvy_validate_iri__impl, 1}, + {"savvy_validate_variable_name__impl", (DL_FUNC) &savvy_validate_variable_name__impl, 1}, + {"savvy_RArgument_from_constant_term__impl", (DL_FUNC) &savvy_RArgument_from_constant_term__impl, 2}, + {"savvy_RArgument_from_variable__impl", (DL_FUNC) &savvy_RArgument_from_variable__impl, 2}, + {"savvy_RConstantTerm_blank_node__impl", (DL_FUNC) &savvy_RConstantTerm_blank_node__impl, 1}, + {"savvy_RConstantTerm_iri__impl", (DL_FUNC) &savvy_RConstantTerm_iri__impl, 1}, + {"savvy_RConstantTerm_literal__impl", (DL_FUNC) &savvy_RConstantTerm_literal__impl, 3}, + {"savvy_RConstantTerm_none__impl", (DL_FUNC) &savvy_RConstantTerm_none__impl, 0}, + {"savvy_RGroundTerm_iri__impl", (DL_FUNC) &savvy_RGroundTerm_iri__impl, 1}, + {"savvy_RGroundTerm_literal__impl", (DL_FUNC) &savvy_RGroundTerm_literal__impl, 3}, + {"savvy_RInstance_iri__impl", (DL_FUNC) &savvy_RInstance_iri__impl, 1}, + {"savvy_RInstance_new__impl", (DL_FUNC) &savvy_RInstance_new__impl, 3}, + {"savvy_RModel_add_graph__impl", (DL_FUNC) &savvy_RModel_add_graph__impl, 4}, + {"savvy_RModel_add_prefixes__impl", (DL_FUNC) &savvy_RModel_add_prefixes__impl, 2}, + {"savvy_RModel_add_template__impl", (DL_FUNC) &savvy_RModel_add_template__impl, 2}, + {"savvy_RModel_add_template_string__impl", (DL_FUNC) &savvy_RModel_add_template_string__impl, 2}, + {"savvy_RModel_compact__impl", (DL_FUNC) &savvy_RModel_compact__impl, 1}, + {"savvy_RModel_create_index__impl", (DL_FUNC) &savvy_RModel_create_index__impl, 1}, + {"savvy_RModel_deserialize__impl", (DL_FUNC) &savvy_RModel_deserialize__impl, 2}, + {"savvy_RModel_detach_graph__impl", (DL_FUNC) &savvy_RModel_detach_graph__impl, 3}, + {"savvy_RModel_infer_rdfs__impl", (DL_FUNC) &savvy_RModel_infer_rdfs__impl, 2}, + {"savvy_RModel_insert__impl", (DL_FUNC) &savvy_RModel_insert__impl, 6}, + {"savvy_RModel_map__impl", (DL_FUNC) &savvy_RModel_map__impl, 5}, + {"savvy_RModel_map_df__impl", (DL_FUNC) &savvy_RModel_map_df__impl, 4}, + {"savvy_RModel_map_json__impl", (DL_FUNC) &savvy_RModel_map_json__impl, 5}, + {"savvy_RModel_map_json_string__impl", (DL_FUNC) &savvy_RModel_map_json_string__impl, 5}, + {"savvy_RModel_map_no_data__impl", (DL_FUNC) &savvy_RModel_map_no_data__impl, 4}, + {"savvy_RModel_map_triples__impl", (DL_FUNC) &savvy_RModel_map_triples__impl, 5}, + {"savvy_RModel_map_xml__impl", (DL_FUNC) &savvy_RModel_map_xml__impl, 5}, + {"savvy_RModel_map_xml_string__impl", (DL_FUNC) &savvy_RModel_map_xml_string__impl, 5}, + {"savvy_RModel_new__impl", (DL_FUNC) &savvy_RModel_new__impl, 0}, + {"savvy_RModel_query__impl", (DL_FUNC) &savvy_RModel_query__impl, 6}, + {"savvy_RModel_read__impl", (DL_FUNC) &savvy_RModel_read__impl, 4}, + {"savvy_RModel_reads__impl", (DL_FUNC) &savvy_RModel_reads__impl, 4}, + {"savvy_RModel_serialize__impl", (DL_FUNC) &savvy_RModel_serialize__impl, 2}, + {"savvy_RModel_size__impl", (DL_FUNC) &savvy_RModel_size__impl, 2}, + {"savvy_RModel_truncate_graph__impl", (DL_FUNC) &savvy_RModel_truncate_graph__impl, 2}, + {"savvy_RModel_update__impl", (DL_FUNC) &savvy_RModel_update__impl, 4}, + {"savvy_RModel_write__impl", (DL_FUNC) &savvy_RModel_write__impl, 4}, + {"savvy_RModel_writes__impl", (DL_FUNC) &savvy_RModel_writes__impl, 3}, + {"savvy_RParameter_new__impl", (DL_FUNC) &savvy_RParameter_new__impl, 5}, + {"savvy_RTemplate_instance__impl", (DL_FUNC) &savvy_RTemplate_instance__impl, 3}, + {"savvy_RTemplate_iri__impl", (DL_FUNC) &savvy_RTemplate_iri__impl, 1}, + {"savvy_RTemplate_new__impl", (DL_FUNC) &savvy_RTemplate_new__impl, 3}, + {"savvy_RTemplate_print_string__impl", (DL_FUNC) &savvy_RTemplate_print_string__impl, 1}, + {NULL, NULL, 0} +}; + +void R_init_maplibr(DllInfo *dll) { + R_registerRoutines(dll, NULL, CallEntries, NULL, NULL); + R_useDynamicSymbols(dll, FALSE); + + // Functions for initialization, if any. + +} diff --git a/maplibr/src/maplibr-win.def b/maplibr/src/maplibr-win.def new file mode 100644 index 00000000..9bfea364 --- /dev/null +++ b/maplibr/src/maplibr-win.def @@ -0,0 +1,2 @@ +EXPORTS +R_init_maplibr diff --git a/maplibr/src/rust-toolchain.toml b/maplibr/src/rust-toolchain.toml new file mode 100644 index 00000000..002e23e7 --- /dev/null +++ b/maplibr/src/rust-toolchain.toml @@ -0,0 +1,9 @@ +[toolchain] +# The workspace root's polars dependency requests polars' "nightly" cargo +# feature (Cargo.toml's workspace.dependencies.polars), which some of its +# own dependencies (foldhash, argminmax) only compile under a nightly rustc. +# This mirrors flake.nix's `fenix.packages.${system}.complete` (a nightly +# channel), pinned here to whatever nightly toolchain is already installed +# locally. Scoped to this directory (where R's Makevars invokes `cargo +# build` from) so it doesn't force nightly on the rest of the repo. +channel = "nightly-2026-04-28" diff --git a/maplibr/src/rust/.cargo/config.toml b/maplibr/src/rust/.cargo/config.toml new file mode 100644 index 00000000..3d8bdda2 --- /dev/null +++ b/maplibr/src/rust/.cargo/config.toml @@ -0,0 +1,10 @@ +# On Windows, link.exe fails when the artifact contains unresolved symbols +# (i.e., R's API, which cannot be used without a real R session). This option +# makes the linker ignore these problems. +# +# This setting is needed only when you run `cargo test`, not when `R CMD check` +# etc. The `.cargo` directory need to be excluded on building the package (i.e. +# add `^src/rust/\.cargo$` to `.Rbuildignore`) because otherwise you'll get the +# "hidden files and directories" NOTE. +[target.x86_64-pc-windows-msvc] +rustflags = ["-C", "link-arg=/FORCE:UNRESOLVED"] diff --git a/maplibr/src/rust/Cargo.toml b/maplibr/src/rust/Cargo.toml new file mode 100644 index 00000000..6a828940 --- /dev/null +++ b/maplibr/src/rust/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "maplibr" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["lib", "staticlib"] + +[dependencies] +maplib = { path = "../../../lib/maplib", features = ["pyo3"] } +representation = { path = "../../../lib/representation" } +spargebra = { path = "../../../lib/spargebra" } +templates = { path = "../../../lib/templates" } +triplestore = { path = "../../../lib/triplestore" } +oxrdf = { workspace = true } +oxrdfio = { workspace = true } +sparesults = { workspace = true } +pyo3 = { workspace = true } +# Pinned explicitly rather than `workspace = true`: the root workspace's +# `polars` entry doesn't itself set `default-features`, and Cargo silently +# ignores a member's `default-features` override in that case (warns +# "could become a hard error in the future") -- switching this one to +# workspace = true would silently turn default-features back on here. +polars = { version = "0.54.4", default-features = false } +polars-arrow = { workspace = true } +savvy = "0.8" +serde = { workspace = true } +serde_json = { workspace = true } + +[profile.release] +# By default, on release build, savvy terminates the R session when a panic +# occurs. This is the right behavior in that a panic means such a fatal event +# where we can have no hope of recovery. +# +# cf. https://doc.rust-lang.org/book/ch09-03-to-panic-or-not-to-panic.html +# +# However, it's possible that the panic is thrown by some of the dependency +# crate and there's little you can do. In such cases, you can change the +# following line to `panic = "unwind"` to always catch a panic. +panic = "abort" diff --git a/maplibr/src/rust/api.h b/maplibr/src/rust/api.h new file mode 100644 index 00000000..d78115a5 --- /dev/null +++ b/maplibr/src/rust/api.h @@ -0,0 +1,65 @@ +SEXP savvy_export_test_series__ffi(SEXP c_arg__stream_ptr); +SEXP savvy_export_test_solution_mappings__ffi(SEXP c_arg__stream_ptr); +SEXP savvy_import_test_series__ffi(SEXP c_arg__stream_ptr); +SEXP savvy_import_test_solution_mappings__ffi(SEXP c_arg__stream_ptr, SEXP c_arg__rdf_node_types_json); +SEXP savvy_make_triple__ffi(SEXP c_arg__subject, SEXP c_arg__predicate, SEXP c_arg__object, SEXP c_arg__list_expander); +SEXP savvy_validate_blank_node_id__ffi(SEXP c_arg__id); +SEXP savvy_validate_iri__ffi(SEXP c_arg__iri); +SEXP savvy_validate_variable_name__ffi(SEXP c_arg__name); + +// methods and associated functions for RArgument +SEXP savvy_RArgument_from_constant_term__ffi(SEXP c_arg__term, SEXP c_arg__list_expand); +SEXP savvy_RArgument_from_variable__ffi(SEXP c_arg__name, SEXP c_arg__list_expand); + +// methods and associated functions for RConstantTerm +SEXP savvy_RConstantTerm_blank_node__ffi(SEXP c_arg__id); +SEXP savvy_RConstantTerm_iri__ffi(SEXP c_arg__iri); +SEXP savvy_RConstantTerm_literal__ffi(SEXP c_arg__value, SEXP c_arg__datatype_iri, SEXP c_arg__language); +SEXP savvy_RConstantTerm_none__ffi(void); + +// methods and associated functions for RGroundTerm +SEXP savvy_RGroundTerm_iri__ffi(SEXP c_arg__iri); +SEXP savvy_RGroundTerm_literal__ffi(SEXP c_arg__value, SEXP c_arg__datatype_iri, SEXP c_arg__language); + +// methods and associated functions for RInstance +SEXP savvy_RInstance_iri__ffi(SEXP self__); +SEXP savvy_RInstance_new__ffi(SEXP c_arg__template_iri, SEXP c_arg__arguments, SEXP c_arg__list_expander); + +// methods and associated functions for RModel +SEXP savvy_RModel_add_graph__ffi(SEXP self__, SEXP c_arg__other, SEXP c_arg__source_graph, SEXP c_arg__target_graph); +SEXP savvy_RModel_add_prefixes__ffi(SEXP self__, SEXP c_arg__prefixes); +SEXP savvy_RModel_add_template__ffi(SEXP self__, SEXP c_arg__template); +SEXP savvy_RModel_add_template_string__ffi(SEXP self__, SEXP c_arg__doc); +SEXP savvy_RModel_compact__ffi(SEXP self__); +SEXP savvy_RModel_create_index__ffi(SEXP self__); +SEXP savvy_RModel_deserialize__ffi(SEXP c_arg__path, SEXP c_arg__storage_folder); +SEXP savvy_RModel_detach_graph__ffi(SEXP self__, SEXP c_arg__preserve_name, SEXP c_arg__graph); +SEXP savvy_RModel_infer_rdfs__ffi(SEXP self__, SEXP c_arg__graph); +SEXP savvy_RModel_insert__ffi(SEXP self__, SEXP c_arg__query, SEXP c_arg__include_transient, SEXP c_arg__transient, SEXP c_arg__source_graph, SEXP c_arg__target_graph); +SEXP savvy_RModel_map__ffi(SEXP self__, SEXP c_arg__template_iri, SEXP c_arg__stream_ptr, SEXP c_arg__graph, SEXP c_arg__validate_iris); +SEXP savvy_RModel_map_df__ffi(SEXP self__, SEXP c_arg__stream_ptr, SEXP c_arg__graph, SEXP c_arg__uuid_namespace); +SEXP savvy_RModel_map_json__ffi(SEXP self__, SEXP c_arg__path, SEXP c_arg__transient, SEXP c_arg__graph, SEXP c_arg__uuid_namespace); +SEXP savvy_RModel_map_json_string__ffi(SEXP self__, SEXP c_arg__json, SEXP c_arg__transient, SEXP c_arg__graph, SEXP c_arg__uuid_namespace); +SEXP savvy_RModel_map_no_data__ffi(SEXP self__, SEXP c_arg__template_iri, SEXP c_arg__graph, SEXP c_arg__validate_iris); +SEXP savvy_RModel_map_triples__ffi(SEXP self__, SEXP c_arg__stream_ptr, SEXP c_arg__predicate, SEXP c_arg__graph, SEXP c_arg__validate_iris); +SEXP savvy_RModel_map_xml__ffi(SEXP self__, SEXP c_arg__path, SEXP c_arg__transient, SEXP c_arg__graph, SEXP c_arg__uuid_namespace); +SEXP savvy_RModel_map_xml_string__ffi(SEXP self__, SEXP c_arg__xml, SEXP c_arg__transient, SEXP c_arg__graph, SEXP c_arg__uuid_namespace); +SEXP savvy_RModel_new__ffi(void); +SEXP savvy_RModel_query__ffi(SEXP self__, SEXP c_arg__sparql, SEXP c_arg__stream_ptr, SEXP c_arg__include_transient, SEXP c_arg__graph, SEXP c_arg__bindings); +SEXP savvy_RModel_read__ffi(SEXP self__, SEXP c_arg__path, SEXP c_arg__format, SEXP c_arg__graph); +SEXP savvy_RModel_reads__ffi(SEXP self__, SEXP c_arg__s, SEXP c_arg__format, SEXP c_arg__graph); +SEXP savvy_RModel_serialize__ffi(SEXP self__, SEXP c_arg__path); +SEXP savvy_RModel_size__ffi(SEXP self__, SEXP c_arg__graph); +SEXP savvy_RModel_truncate_graph__ffi(SEXP self__, SEXP c_arg__graph); +SEXP savvy_RModel_update__ffi(SEXP self__, SEXP c_arg__update, SEXP c_arg__include_transient, SEXP c_arg__graph); +SEXP savvy_RModel_write__ffi(SEXP self__, SEXP c_arg__path, SEXP c_arg__format, SEXP c_arg__graph); +SEXP savvy_RModel_writes__ffi(SEXP self__, SEXP c_arg__format, SEXP c_arg__graph); + +// methods and associated functions for RParameter +SEXP savvy_RParameter_new__ffi(SEXP c_arg__variable_name, SEXP c_arg__optional, SEXP c_arg__allow_blank, SEXP c_arg__rdf_type_iri, SEXP c_arg__default_value); + +// methods and associated functions for RTemplate +SEXP savvy_RTemplate_instance__ffi(SEXP self__, SEXP c_arg__arguments, SEXP c_arg__list_expander); +SEXP savvy_RTemplate_iri__ffi(SEXP self__); +SEXP savvy_RTemplate_new__ffi(SEXP c_arg__iri, SEXP c_arg__parameters, SEXP c_arg__instances); +SEXP savvy_RTemplate_print_string__ffi(SEXP self__); diff --git a/maplibr/src/rust/src/arrow_bridge.rs b/maplibr/src/rust/src/arrow_bridge.rs new file mode 100644 index 00000000..d9f81225 --- /dev/null +++ b/maplibr/src/rust/src/arrow_bridge.rs @@ -0,0 +1,369 @@ +use oxrdf::{BlankNode, Literal as OxLiteral, NamedNode, Term, Variable}; +use polars::prelude::*; +use polars_arrow::ffi::{export_iterator, ArrowArrayStream, ArrowArrayStreamReader}; +use representation::solution_mapping::EagerSolutionMappings; +use representation::{BaseRDFNodeType, RDFNodeState}; +use savvy::{savvy, ExternalPointerSexp, Sexp}; +use serde::Serialize; +use sparesults::QuerySolution; +use std::collections::HashMap; + +/// Build a small hardcoded i32 Series in Rust and stream it out to R +/// through the Arrow C Stream Interface. +/// +/// @param stream_ptr An external pointer from `nanoarrow::nanoarrow_allocate_array_stream()`. +/// @export +#[savvy] +fn export_test_series(stream_ptr: Sexp) -> savvy::Result<()> { + let series = Series::new("x".into(), &[1i32, 2, 3, 4, 5]); + + let field = series.field().to_arrow(CompatLevel::newest()); + let chunks = series.chunks().clone(); + let iter = Box::new(chunks.into_iter().map(Ok)); + let stream = export_iterator(iter, field); + + let stream_ptr = unsafe { + ExternalPointerSexp::try_from(stream_ptr)?.cast_mut_unchecked::() + }; + unsafe { std::ptr::replace(stream_ptr, stream) }; + + Ok(()) +} + +/// Read an Arrow C stream from R back into a Rust Series, then hand back +/// its length and first value as a sanity check (i32 only, for this prototype). +/// +/// @param stream_ptr An external pointer holding a filled ArrowArrayStream. +/// @returns An integer vector: `c(length, first_value)`. +/// @export +#[savvy] +fn import_test_series(stream_ptr: Sexp) -> savvy::Result { + let mut stream = unsafe { + let boxed = Box::new(std::ptr::replace( + ExternalPointerSexp::try_from(stream_ptr)?.cast_mut_unchecked::(), + ArrowArrayStream::empty(), + )); + ArrowArrayStreamReader::try_new(boxed).map_err(|e| savvy::Error::new(&e.to_string()))? + }; + + let mut arrays = Vec::new(); + while let Some(arr) = unsafe { stream.next() } { + arrays.push(arr.map_err(|e| savvy::Error::new(&e.to_string()))?); + } + + let series = Series::from_arrow_chunks("x".into(), arrays) + .map_err(|e| savvy::Error::new(&e.to_string()))?; + + let mut out = savvy::OwnedIntegerSexp::new(2)?; + out[0] = series.len() as i32; + out[1] = series + .i32() + .map_err(|e| savvy::Error::new(&e.to_string()))? + .get(0) + .unwrap_or(0); + Ok(out.into()) +} + +// --- DataFrame<->R bridge (maplib-fat) --- +// +// Extends the Series-level mechanism above (export_test_series/ +// import_test_series, maplib-qi9) to a full `EagerSolutionMappings +// { mappings: DataFrame, rdf_node_types: HashMap }` +// (lib/representation/src/solution_mapping.rs:43), the shape RModel's future +// query/insert/update methods will need to move across the R/Rust boundary. +// +// Two independent problems, per docs/r-wrapper-plan.md Phase 2: +// +// 1. DataFrame -> single Series -> one Arrow C stream. Mirrors r-polars' +// `as_polars_series(df)` trick: `DataFrame::into_struct(name)` collapses +// every column into one Struct-typed Series, then the already-proven +// Series-level export_iterator/ArrowArrayStreamReader mechanism carries it +// across unchanged (`unnest()` reverses the collapse on the way back). No +// new FFI mechanism needed -- same stream, one more level of Struct +// nesting. This also has to survive genuinely nested Struct-in-Struct data, +// since a "multi" RDFNodeState column (RDFNodeState::polars_data_type, +// lib/representation/src/rdf_state.rs:46-67) is *already* Struct-typed +// before the whole-DataFrame collapse adds another layer -- exercised +// below via the "o" test column. +// +// 2. rdf_node_types has no r-polars equivalent (needed to reconstruct +// IRI/literal/blank-node distinctions from what the DataFrame otherwise +// carries as plain string/list/struct columns). Design chosen here: a +// single JSON string, one flat side-channel value returned alongside the +// Arrow stream, rather than a second stream or an extra struct field +// smuggled into the DataFrame itself -- keeps the Arrow payload exactly +// what Model methods already produce, no need for a schema-aware "does +// this column carry an extra sidecar" convention on the Arrow side. Only +// the semantic type per column travels (`RColumnType`, one entry per +// BaseRDFNodeType a column can hold) -- BaseCatState (RDFNodeState's other +// HashMap value, an internal categorical-encoding cache) does NOT travel: +// it's a Rust-side storage optimization, not something R needs, and the +// physical Arrow encoding (dictionary-typed array vs plain) already +// reflects it structurally. + +/// One RDF node type a solution-mappings column can hold. Mirrors +/// `BaseRDFNodeType` (lib/representation/src/base_rdf_type.rs) but is its own +/// type rather than reusing that enum directly, so the JSON side-channel's +/// shape doesn't change every time `representation`'s internals do. +#[derive(Serialize)] +#[serde(tag = "kind", rename_all = "snake_case")] +enum RColumnType { + Iri, + BlankNode, + Literal { datatype: String }, + None, +} + +impl From<&BaseRDFNodeType> for RColumnType { + fn from(t: &BaseRDFNodeType) -> Self { + match t { + BaseRDFNodeType::IRI => RColumnType::Iri, + BaseRDFNodeType::BlankNode => RColumnType::BlankNode, + BaseRDFNodeType::Literal(dt) => RColumnType::Literal { + datatype: dt.as_str().to_string(), + }, + BaseRDFNodeType::None => RColumnType::None, + } + } +} + +/// Serialize `rdf_node_types` (per-column possible RDF node types) to the +/// JSON side-channel string. One entry per column name; each column maps to +/// a list of 1 (single-typed) or more (a "multi" column, RDFNodeState::is_multi) +/// `RColumnType`s. +fn rdf_node_types_to_json(rdf_node_types: &HashMap) -> savvy::Result { + let summary: HashMap<&str, Vec> = rdf_node_types + .iter() + .map(|(col, state)| { + let types = state.get_sorted_types().into_iter().map(RColumnType::from).collect(); + (col.as_str(), types) + }) + .collect(); + serde_json::to_string(&summary).map_err(|e| savvy::Error::new(&e.to_string())) +} + +/// A small, genuinely mixed-type `EagerSolutionMappings` fixture, built the +/// same way real query results are (`EagerSolutionMappings::from_query_solutions`, +/// lib/representation/src/solution_mapping.rs:56-62) rather than hand-rolled, +/// so the DataFrame's column layout matches what a real `Model::query()` will +/// eventually produce. "s" is single-typed (IRI only); "o" is deliberately +/// multi-typed (IRI, then a plain literal, then a blank node across its three +/// rows) to force RDFNodeState::is_multi -- i.e. a Struct-typed column even +/// before the whole-DataFrame collapse. +fn build_test_solution_mappings() -> EagerSolutionMappings { + let vars: std::sync::Arc<[Variable]> = + vec![Variable::new("s").unwrap(), Variable::new("o").unwrap()].into(); + + let sols = vec![ + QuerySolution::from(( + vars.clone(), + vec![ + Some(Term::NamedNode(NamedNode::new("http://example.org/a").unwrap())), + Some(Term::NamedNode(NamedNode::new("http://example.org/b").unwrap())), + ], + )), + QuerySolution::from(( + vars.clone(), + vec![ + Some(Term::NamedNode(NamedNode::new("http://example.org/c").unwrap())), + Some(Term::Literal(OxLiteral::new_simple_literal("hello"))), + ], + )), + QuerySolution::from(( + vars, + vec![ + Some(Term::NamedNode(NamedNode::new("http://example.org/d").unwrap())), + Some(Term::BlankNode(BlankNode::new("x1").unwrap())), + ], + )), + ]; + + EagerSolutionMappings::from_query_solutions(&sols).unwrap() +} + +/// Recursively cast any Categorical/Enum-typed Series to plain String +/// (recursing into Struct fields too, since a "multi" RDFNodeState column is +/// itself Struct-typed). Real query results commonly store repeated IRI/ +/// literal values as `BaseCatState::CategoricalNative` (a Rust-side storage +/// optimization, lib/representation/src/solution_mapping.rs:13-30) -- Arrow's +/// dictionary encoding for a categorical column round-trips fine at the +/// top level, but confirmed live that nanoarrow's struct-to-data.frame +/// conversion does NOT resolve a dictionary-encoded field nested inside a +/// struct column back to its string values, instead surfacing the raw +/// integer codes. Decategorizing before the whole-DataFrame struct-collapse +/// sidesteps that -- the memory-efficiency benefit of categorical encoding +/// only matters Rust-side anyway, not for the R-bound wire format. +fn decategorize(s: Series) -> savvy::Result { + match s.dtype() { + DataType::Categorical(_, _) | DataType::Enum(_, _) => s + .cast(&DataType::String) + .map_err(|e| crate::errors::runtime_error(e.to_string())), + DataType::Struct(_) => { + let name = s.name().clone(); + let len = s.len(); + let ca = s + .struct_() + .map_err(|e| crate::errors::runtime_error(e.to_string()))?; + let fields: Vec = ca + .fields_as_series() + .into_iter() + .map(decategorize) + .collect::>()?; + StructChunked::from_series(name, len, fields.iter()) + .map(|sc| sc.into_series()) + .map_err(|e| crate::errors::runtime_error(e.to_string())) + } + _ => Ok(s), + } +} + +/// Collapse an `EagerSolutionMappings`' DataFrame to a Struct-typed Series +/// (`DataFrame::into_struct`) and stream it out to R through the Arrow C +/// Stream Interface, exactly like `export_test_series` does for a bare +/// Series -- the mechanism `RModel::query()` uses for a real query result, +/// proven first here against a hand-built fixture +/// (`export_test_solution_mappings`). +/// +/// @param stream_ptr An external pointer from `nanoarrow::nanoarrow_allocate_array_stream()`. +/// @returns The `rdf_node_types` side-channel, as a JSON string. +pub(crate) fn export_solution_mappings( + sm: EagerSolutionMappings, + stream_ptr: Sexp, +) -> savvy::Result { + let EagerSolutionMappings { + mappings, + rdf_node_types, + } = sm; + + let height = mappings.height(); + let columns: Vec = mappings + .columns() + .iter() + .map(|c| decategorize(c.as_materialized_series().clone()).map(Column::from)) + .collect::>()?; + let mappings = DataFrame::new(height, columns) + .map_err(|e| crate::errors::runtime_error(e.to_string()))?; + + let struct_series = mappings.into_struct("solution_mappings".into()).into_series(); + let field = struct_series.field().to_arrow(CompatLevel::newest()); + let chunks = struct_series.chunks().clone(); + let iter = Box::new(chunks.into_iter().map(Ok)); + let stream = export_iterator(iter, field); + + let stream_ptr = unsafe { + ExternalPointerSexp::try_from(stream_ptr)?.cast_mut_unchecked::() + }; + unsafe { std::ptr::replace(stream_ptr, stream) }; + + rdf_node_types_to_json(&rdf_node_types)?.try_into() +} + +/// Build the `build_test_solution_mappings()` fixture and stream it out via +/// `export_solution_mappings` -- proving the DataFrame case reuses the +/// Series-level mechanism unchanged, against a fixture with a genuinely +/// multi-typed column (see `build_test_solution_mappings`'s doc comment). +/// +/// @param stream_ptr An external pointer from `nanoarrow::nanoarrow_allocate_array_stream()`. +/// @returns The `rdf_node_types` side-channel, as a JSON string. +/// @export +#[savvy] +fn export_test_solution_mappings(stream_ptr: Sexp) -> savvy::Result { + export_solution_mappings(build_test_solution_mappings(), stream_ptr) +} + +/// Read an Arrow C stream from R into a plain `DataFrame` -- the mapping-side +/// counterpart of `export_solution_mappings`. Used by `RModel::map`/ +/// `RModel::add_template` (maplib-cgd) to bring a plain R data.frame (via +/// `nanoarrow::as_nanoarrow_array_stream(df)` on the R side) across as the +/// `data` argument to `Model::expand`. Unlike the export path, there is no +/// `rdf_node_types` side-channel here -- an input mapping data.frame is +/// plain tabular data, its RDF typing comes from the target Template's +/// signature, not from the data itself. Columns are decategorized the same +/// way `export_solution_mappings` decategorizes on the way out: an R factor +/// column arrives as a dictionary-encoded Arrow array, and `expand()`'s +/// column validation (model/expansion/validation.rs) expects plain String +/// columns, not Categorical/Enum. +pub(crate) fn import_dataframe(stream_ptr: Sexp) -> savvy::Result { + let mut stream = unsafe { + let boxed = Box::new(std::ptr::replace( + ExternalPointerSexp::try_from(stream_ptr)?.cast_mut_unchecked::(), + ArrowArrayStream::empty(), + )); + ArrowArrayStreamReader::try_new(boxed).map_err(|e| savvy::Error::new(&e.to_string()))? + }; + + let mut arrays = Vec::new(); + while let Some(arr) = unsafe { stream.next() } { + arrays.push(arr.map_err(|e| savvy::Error::new(&e.to_string()))?); + } + + let struct_series = Series::from_arrow_chunks("data".into(), arrays) + .map_err(|e| savvy::Error::new(&e.to_string()))?; + let struct_series = decategorize(struct_series)?; + struct_series + .struct_() + .map_err(|e| crate::errors::runtime_error(e.to_string())) + .map(|ca| ca.clone().unnest()) +} + +/// Read an Arrow C stream from R back into a Struct-typed Series, unnest it +/// back into a DataFrame (`StructChunked::unnest`, the reverse of +/// `into_struct`), and return a small human-readable summary combining the +/// recovered DataFrame's shape with the `rdf_node_types` JSON side-channel +/// handed back in -- a sanity check that both halves survive the round trip +/// together, matching `import_test_series`'s role for the Series-only case. +/// +/// @param stream_ptr An external pointer holding a filled ArrowArrayStream. +/// @param rdf_node_types_json The JSON string returned by +/// `export_test_solution_mappings`. +/// @returns A character vector: one line per column, `": []"`. +/// @export +#[savvy] +fn import_test_solution_mappings( + stream_ptr: Sexp, + rdf_node_types_json: &str, +) -> savvy::Result { + let mut stream = unsafe { + let boxed = Box::new(std::ptr::replace( + ExternalPointerSexp::try_from(stream_ptr)?.cast_mut_unchecked::(), + ArrowArrayStream::empty(), + )); + ArrowArrayStreamReader::try_new(boxed).map_err(|e| savvy::Error::new(&e.to_string()))? + }; + + let mut arrays = Vec::new(); + while let Some(arr) = unsafe { stream.next() } { + arrays.push(arr.map_err(|e| savvy::Error::new(&e.to_string()))?); + } + + let struct_series = Series::from_arrow_chunks("solution_mappings".into(), arrays) + .map_err(|e| savvy::Error::new(&e.to_string()))?; + let df = struct_series + .struct_() + .map_err(|e| savvy::Error::new(&e.to_string()))? + .clone() + .unnest(); + + let rdf_node_types: HashMap = + serde_json::from_str(rdf_node_types_json).map_err(|e| savvy::Error::new(&e.to_string()))?; + + let mut lines: Vec = df + .get_column_names() + .into_iter() + .zip(df.dtypes()) + .map(|(name, dtype)| { + let types = rdf_node_types + .get(name.as_str()) + .map(|v| v.to_string()) + .unwrap_or_else(|| "?".to_string()); + format!("{}: {} {}", name, dtype, types) + }) + .collect(); + lines.push(format!("{} rows", df.height())); + + let mut out = savvy::OwnedStringSexp::new(lines.len())?; + for (i, line) in lines.iter().enumerate() { + out.set_elt(i, line)?; + } + Ok(out.into()) +} diff --git a/maplibr/src/rust/src/errors.rs b/maplibr/src/rust/src/errors.rs new file mode 100644 index 00000000..97ae590c --- /dev/null +++ b/maplibr/src/rust/src/errors.rs @@ -0,0 +1,76 @@ +use std::fmt::Display; + +/// Mirrors py_maplib's `PyMaplibError` (py_maplib/src/error.rs) -- but savvy's +/// FFI boundary can only carry a plain error message string across (unlike +/// pyo3, which lets Rust raise an actual typed Python exception object), so +/// the "typed exception" intent travels as a machine-parseable `[tag] +/// message` prefix instead. `maplibr/R/errors.R`'s `.rethrow()` parses that +/// tag back out and re-raises as a proper R condition with a matching class, +/// so R code can `tryCatch(..., maplibr_argument_error = function(e) ...)` +/// the same way Python code can `except FunctionArgumentException`. +pub enum RMaplibError { + /// Wraps `maplib::errors::MaplibError` -- mirrors + /// `PyMaplibError::MaplibError`, the transparent `#[from]` variant + /// covering all of `MaplibError`'s many inner error types uniformly. + /// Matches py_maplib's own granularity: one exception class for the + /// whole enum, not one per inner variant. + Maplib(maplib::errors::MaplibError), + /// A bad argument from R: invalid IRI/variable-name/blank-node-id + /// syntax, an unrecognized format/list_expander string, etc. Mirrors + /// `PyMaplibError::FunctionArgumentError`. + Argument(String), + /// Something that should never happen (e.g. maplib producing triple + /// output that isn't valid UTF-8). Mirrors `PyMaplibError::RuntimeError`. + Runtime(String), +} + +impl RMaplibError { + fn tag(&self) -> &'static str { + match self { + RMaplibError::Maplib(_) => "maplibr_maplib_error", + RMaplibError::Argument(_) => "maplibr_argument_error", + RMaplibError::Runtime(_) => "maplibr_runtime_error", + } + } +} + +impl Display for RMaplibError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + RMaplibError::Maplib(e) => write!(f, "{e}"), + RMaplibError::Argument(s) => write!(f, "{s}"), + RMaplibError::Runtime(s) => write!(f, "{s}"), + } + } +} + +impl From for RMaplibError { + fn from(e: maplib::errors::MaplibError) -> Self { + RMaplibError::Maplib(e) + } +} + +impl From for savvy::Error { + fn from(e: RMaplibError) -> Self { + savvy::Error::new(&format!("[{}] {}", e.tag(), e)) + } +} + +/// Wrap a bad-argument error (an `IriParseError`, `VariableNameParseError`, +/// `BlankNodeIdParseError`, or a hand-written message) as a `savvy::Error` +/// tagged for R-side reclassification via `.rethrow()`. +pub fn argument_error(msg: impl Display) -> savvy::Error { + RMaplibError::Argument(msg.to_string()).into() +} + +/// Wrap an "this should never happen" error as a `savvy::Error` tagged for +/// R-side reclassification via `.rethrow()`. +pub fn runtime_error(msg: impl Display) -> savvy::Error { + RMaplibError::Runtime(msg.to_string()).into() +} + +/// Wrap a `maplib::errors::MaplibError` as a `savvy::Error` tagged for +/// R-side reclassification via `.rethrow()`. +pub fn maplib_error(e: maplib::errors::MaplibError) -> savvy::Error { + RMaplibError::from(e).into() +} diff --git a/maplibr/src/rust/src/lib.rs b/maplibr/src/rust/src/lib.rs new file mode 100644 index 00000000..71966492 --- /dev/null +++ b/maplibr/src/rust/src/lib.rs @@ -0,0 +1,7 @@ +mod arrow_bridge; +mod errors; +mod model; +mod templates; +mod terms; + +pub use model::RModel; diff --git a/maplibr/src/rust/src/model.rs b/maplibr/src/rust/src/model.rs new file mode 100644 index 00000000..0daeddaf --- /dev/null +++ b/maplibr/src/rust/src/model.rs @@ -0,0 +1,1060 @@ +use crate::errors::argument_error; +use crate::errors::maplib_error; +use crate::templates::RTemplate; +use crate::terms::RGroundTerm; +use maplib::model::MapOptions; +use oxrdf::NamedNode; +use oxrdfio::RdfFormat; +use polars::prelude::{ + coalesce, col, concat, Column, DataFrame, DataType, Expr, IntoLazy, Series, UnionArgs, +}; +use representation::cats::LockedCats; +use representation::dataset::NamedGraph; +use representation::rdf_to_polars::rdf_named_node_to_polars_literal_value; +use representation::solution_mapping::EagerSolutionMappings; +use representation::{ + BaseRDFNodeType, RDFNodeState, LANG_STRING_VALUE_FIELD, OBJECT_COL_NAME, PREDICATE_COL_NAME, + SUBJECT_COL_NAME, +}; +use savvy::{savvy, ListSexp, Sexp}; +use std::collections::HashMap; +use std::fs::File; +use std::path::Path; +use std::sync::Mutex; +use triplestore::triples_read::ExtendedRdfFormat; +use triplestore::IndexingOptions; + +const DEFAULT_TRIPLES_BATCH_SIZE: usize = 10_000_000; + +/// Mirrors py_maplib's `resolve_normal_format` (py_maplib/src/lib.rs:303-316). +fn resolve_normal_format(format: &str) -> savvy::Result { + match format.to_lowercase().as_str() { + "ntriples" => Ok(RdfFormat::NTriples), + "turtle" => Ok(RdfFormat::Turtle), + "rdf/xml" | "xml" | "rdfxml" => Ok(RdfFormat::RdfXml), + _ => Err(argument_error(format!("Unknown format: {}", format))), + } +} + +/// Mirrors py_maplib's `resolve_format` (py_maplib/src/lib.rs:318-327). +/// CIM XML and HDT deliberately unsupported here (out of scope for v1, see plan). +fn resolve_format(format: &str) -> savvy::Result { + resolve_normal_format(format).map(ExtendedRdfFormat::Normal) +} + +/// Guesses a file's RDF format from its extension, for `read()`'s +/// `format = NULL` case -- deliberately its own, narrower copy of +/// `Triplestore::read_triples_from_path`'s own extension-guessing +/// (lib/triplestore/src/triples_read.rs:62-82), NOT a passthrough of `None` +/// into that function. That function's guesser falls into a bare `todo!()` +/// for any extension it doesn't recognize (also true of JSON-LD/HDT, which +/// it *does* recognize but this package deliberately doesn't support yet, +/// see `resolve_format`) -- a `todo!()` panics, and this crate's release +/// profile sets `panic = "abort"` (src/rust/Cargo.toml), so reaching that +/// code path with an unsupported or missing extension would abort the whole +/// R session, not raise a catchable error. Guessing here instead means an +/// unrecognized extension hits this function's own `Err` arm, which +/// `read()` turns into a normal `maplibr_argument_error` well before ever +/// calling into `read_triples`. +fn guess_format_from_extension(path: &str) -> savvy::Result { + let ext = Path::new(path) + .extension() + .and_then(|e| e.to_str()) + .map(|e| e.to_lowercase()); + match ext.as_deref() { + Some("ttl") => Ok(ExtendedRdfFormat::Normal(RdfFormat::Turtle)), + Some("nt") => Ok(ExtendedRdfFormat::Normal(RdfFormat::NTriples)), + Some("xml") | Some("rdf") => Ok(ExtendedRdfFormat::Normal(RdfFormat::RdfXml)), + _ => Err(argument_error(format!( + "Could not guess an RDF format from the extension of '{}' -- pass `format` explicitly \ + (one of \"ntriples\", \"turtle\", \"xml\")", + path + ))), + } +} + +fn parse_optional_named_graph(graph: Option<&str>) -> savvy::Result { + let nn = graph + .map(|g| NamedNode::new(g).map_err(argument_error)) + .transpose()?; + Ok(NamedGraph::from_maybe_named_node(nn.as_ref())) +} + +/// Parses `query()`'s `graph` argument. Unlike `reads`/`writes`, where a +/// missing graph means "the default graph" (`parse_optional_named_graph`), +/// a missing graph here means "no restriction to one named graph" -- mirrors +/// py_maplib's own `query`, which passes `graph: Option<&NamedGraph>` +/// through to `Model::query` unchanged rather than defaulting it (py_model.rs:359-360). +fn parse_query_graph(graph: Option<&str>) -> savvy::Result> { + graph + .map(|g| { + let nn = NamedNode::new(g).map_err(argument_error)?; + Ok(NamedGraph::from_maybe_named_node(Some(&nn))) + }) + .transpose() +} + +/// Flattens a CONSTRUCT query's per-pattern results (each pattern's own +/// solution mappings, plus a constant predicate when the pattern used one +/// literally rather than binding it, e.g. `CONSTRUCT { ?s a ?type }`) into a +/// single subject/predicate/object `EagerSolutionMappings`, matching +/// `RModel::query()`'s single-DataFrame-per-call design. Mirrors py_maplib's +/// own per-pattern handling (`query_to_result`, py_maplib/src/lib.rs:235-263) +/// for adding the constant predicate column, but concatenates every +/// pattern's rows into one combined DataFrame instead of returning a +/// separate DataFrame per pattern -- py_maplib can return a heterogeneous +/// Python list; a single R data.frame per query() call cannot. +/// +/// Deliberately simplified relative to `query()`'s SELECT path: every +/// pattern's subject/predicate/object columns are cast to plain strings +/// before concatenating, rather than preserved as natively-typed (e.g. +/// integer/date) or per-row multi-typed columns. Different CONSTRUCT +/// patterns can produce genuinely different column dtypes (one pattern's +/// object might be an integer literal, another's a string), which a single +/// concatenated DataFrame can't represent without collapsing to one common +/// type anyway -- so the `rdf_node_types` side-channel reports predicate as +/// IRI (always true per the CONSTRUCT grammar) and subject/object as +/// untyped, rather than claiming a precision this can't actually preserve. +fn construct_result_to_solution_mappings( + groups: Vec<(EagerSolutionMappings, Option)>, + global_cats: LockedCats, +) -> savvy::Result { + let mut lfs = Vec::with_capacity(groups.len()); + for (sm, predicate) in groups { + let EagerSolutionMappings { + mappings, + mut rdf_node_types, + } = sm; + // A pattern's own subject/object binds against a WHERE clause + // variable, which is very commonly multi-typed (its RDF node type + // can vary by row, e.g. one row's ?o is a string, another's an + // integer) -- format_native_columns represents that as a Struct + // column (one field per possible type, at most one non-null per + // row), the same shape query()'s SELECT path sends to R for + // R-side collapsing (.collapse_multitype_columns). That collapsing + // has to happen here instead, before casting to String and + // concatenating: a Struct column can't be cast to String directly + // (confirmed live -- it silently casts every row to NA rather than + // erroring), and each pattern's own R-side round trip is skipped + // for CONSTRUCT (only the final combined frame goes to R). + let subject_state = rdf_node_types.get(SUBJECT_COL_NAME).cloned(); + let object_state = rdf_node_types.get(OBJECT_COL_NAME).cloned(); + // A variable-bound predicate (the `None` case below) is, in + // practice, always singly IRI-typed -- RDF's predicate position is + // always an IRI -- but the same variable name could in principle + // also appear in a non-predicate position elsewhere in the WHERE + // clause (e.g. across a UNION), making its RDFNodeState multi just + // like subject/object can be. Flattened the same way, rather than + // assumed safe to cast directly. + let predicate_state = rdf_node_types.get(PREDICATE_COL_NAME).cloned(); + let mut lf = representation::formatting::format_native_columns( + mappings.lazy(), + &mut rdf_node_types, + global_cats.clone(), + ); + if let Some(state) = subject_state { + lf = lf.with_column(flatten_to_string(SUBJECT_COL_NAME, &state)); + } + if let Some(state) = object_state { + lf = lf.with_column(flatten_to_string(OBJECT_COL_NAME, &state)); + } + if let Some(predicate) = &predicate { + lf = lf.with_column( + polars::prelude::lit(rdf_named_node_to_polars_literal_value(predicate)) + .alias(PREDICATE_COL_NAME), + ); + } else if let Some(state) = predicate_state { + lf = lf.with_column(flatten_to_string(PREDICATE_COL_NAME, &state)); + } + lf = lf.select([ + col(SUBJECT_COL_NAME), + col(PREDICATE_COL_NAME), + col(OBJECT_COL_NAME), + ]); + lfs.push(lf); + } + let mappings = if lfs.is_empty() { + // A CONSTRUCT template with literally zero triple patterns (valid + // but pointless SPARQL) -- build an empty, correctly-shaped + // DataFrame rather than concat()ing an empty list of LazyFrames + // (which would produce a schema-less frame, not the three empty + // String columns export_solution_mappings expects). + let empty_col = + |name: &str| Column::from(Series::new_empty(name.into(), &DataType::String)); + DataFrame::new( + 0, + vec![ + empty_col(SUBJECT_COL_NAME), + empty_col(PREDICATE_COL_NAME), + empty_col(OBJECT_COL_NAME), + ], + ) + .map_err(|e| crate::errors::runtime_error(e.to_string()))? + } else { + concat( + lfs, + UnionArgs { + parallel: true, + rechunk: true, + ..Default::default() + }, + ) + .map_err(|e| crate::errors::runtime_error(e.to_string()))? + .collect() + .map_err(|e| crate::errors::runtime_error(e.to_string()))? + }; + + let rdf_node_types = HashMap::from([ + ( + SUBJECT_COL_NAME.to_string(), + BaseRDFNodeType::None.into_default_input_rdf_node_state(), + ), + ( + PREDICATE_COL_NAME.to_string(), + BaseRDFNodeType::IRI.into_default_input_rdf_node_state(), + ), + ( + OBJECT_COL_NAME.to_string(), + BaseRDFNodeType::None.into_default_input_rdf_node_state(), + ), + ]); + Ok(EagerSolutionMappings { + mappings, + rdf_node_types, + }) +} + +/// Flattens `col(name)` -- already run through `format_native_columns`, so +/// either a single decoded column or (if `state.is_multi()`) a Struct with +/// one field per possible type -- down to one plain String column. For a +/// multi-typed column, coalesces across the Struct's fields (named per +/// `BaseRDFNodeType::field_col_name()`, matching exactly what +/// `expression_to_native`'s multi branch built them as; the lang-string +/// type is the one exception, itself a 2-field value/lang Struct rather +/// than a single field, hence the `LANG_STRING_VALUE_FIELD` special case -- +/// its language tag is dropped here, same simplification as +/// `.collapse_multitype_columns` on the R side), each field individually +/// cast to String first so mismatched underlying dtypes (e.g. an integer +/// field alongside a string field) don't need a common supertype. +fn flatten_to_string(name: &str, state: &RDFNodeState) -> Expr { + if !state.is_multi() { + // A single-typed rdf:langString column is *also* Struct-shaped + // (value + language tag fields, not one flat column) -- + // expression_to_native's non-multi branch itself falls back to + // as_struct(exprs) whenever the base type needs more than one + // expression, which is exactly the lang-string case. + return if state.is_literal() && state.get_base_type().unwrap().is_lang_string() { + col(name) + .struct_() + .field_by_name(LANG_STRING_VALUE_FIELD) + .cast(DataType::String) + .alias(name) + } else { + col(name).cast(DataType::String).alias(name) + }; + } + let candidates: Vec = state + .get_sorted_types() + .into_iter() + .map(|t| { + let field = if t.is_lang_string() { + LANG_STRING_VALUE_FIELD.to_string() + } else { + t.field_col_name() + }; + col(name) + .struct_() + .field_by_name(&field) + .cast(DataType::String) + }) + .collect(); + coalesce(&candidates).alias(name) +} + +/// A maplib knowledge graph model. +/// +/// @export +#[savvy] +pub struct RModel { + inner: Mutex, +} + +impl RModel { + /// Lock the inner Model, recovering from poison rather than panicking. + /// + /// std's Mutex poisons itself if a panic happens while it's held (e.g. + /// serialize()/deserialize() hitting the lib/disk stub's unimplemented!()) + /// -- left as the default `.lock().unwrap()`, every later call on the + /// SAME RModel would then panic forever with PoisonError, even for + /// operations unrelated to whatever originally panicked. Python's GIL + /// mutex has no poisoning concept, so `into_inner()` (ignore the poison, + /// keep using the data) is the closest match to py_maplib's behavior -- + /// the data itself is still perfectly valid, since maplib's own methods + /// don't panic mid-mutation in a way that would leave it inconsistent. + fn lock(&self) -> std::sync::MutexGuard<'_, maplib::model::Model> { + self.inner.lock().unwrap_or_else(|e| e.into_inner()) + } +} + +#[savvy] +impl RModel { + /// Create a new, empty Model. + /// + /// @export + fn new() -> savvy::Result { + // Required: see CLAUDE.md's pyo3-leak note. lib/utils/src/polars.rs's + // pl_interruptable_collect (used by query/insert/update/map/ + // infer_rdfs -- anything that materializes a LazyFrame) calls + // pyo3::Python::attach unconditionally when built with the pyo3 + // feature, which panics unless some Python interpreter has been + // initialized in this process. Python::initialize() is pyo3's own + // documented mechanism for embedding in a non-Python host; safe to + // call on every RModel::new() (verified idempotent -- a second call + // in the same session doesn't panic or error). + pyo3::Python::initialize(); + + let model = maplib::model::Model::new(None, None, None, None) + .map_err(maplib_error)?; + Ok(Self { + inner: Mutex::new(model), + }) + } + + /// Number of triples in a graph (mirrors PyModel::size, + /// py_maplib/src/py_model.rs:144-151 / size_mutex, py_maplib/src/ + /// mutexes.rs:66-73). + /// + /// @param graph Optional named graph IRI to count (default graph if NULL). + /// @export + fn size(&self, graph: Option<&str>) -> savvy::Result { + let named_graph = parse_optional_named_graph(graph)?; + let inner = self.lock(); + (inner.graph_size(&named_graph) as i32).try_into() + } + + /// Run a SPARQL SELECT or CONSTRUCT query, streaming the result out + /// through the Arrow C Stream Interface (mirrors PyModel::query, + /// py_maplib/src/py_model.rs:344-387; CONSTRUCT results are flattened + /// into one combined subject/predicate/object DataFrame, see + /// construct_result_to_solution_mappings). + /// + /// @param sparql The SPARQL query string. + /// @param stream_ptr An external pointer from `nanoarrow::nanoarrow_allocate_array_stream()`. + /// @param include_transient Whether to include transient (e.g. inferred) + /// triples in the query. Defaults to FALSE. + /// @param graph Optional named graph IRI to restrict the query to (searches + /// across the whole store if NULL, matching py_maplib's default). + /// @param bindings Optional named list of `RGroundTerm` (see terms.R's + /// `.to_ground_term()`) -- pre-binds each named SPARQL variable to a + /// fixed IRI/Literal value before the query runs, mirroring py_maplib's + /// `query(bindings=)`. A bound variable can't also be a `SELECT`-ed + /// output column (`Triplestore::query`'s underlying `maybe_replace_bindings` + /// rejects that combination with a normal error, not a panic -- + /// `SELECT *` is fine, since it names no variables itself). + /// @returns The `rdf_node_types` side-channel, as a JSON string. + /// @export + fn query( + &self, + sparql: &str, + stream_ptr: savvy::Sexp, + include_transient: bool, + graph: Option<&str>, + bindings: Option, + ) -> savvy::Result { + let named_graph = parse_query_graph(graph)?; + let bindings = bindings + .map(|b| -> savvy::Result> { + let mut map = HashMap::new(); + for (name, value) in b.iter() { + let term = <&RGroundTerm>::try_from(value)?; + map.insert(name.to_string(), term.inner.clone()); + } + Ok(map) + }) + .transpose()?; + let mut inner = self.lock(); + let global_cats = inner.triplestore.global_cats.clone(); + let result = inner + .query( + sparql, + None, + named_graph.as_ref(), + false, + include_transient, + None, + false, + bindings.as_ref(), + ) + .map_err(maplib_error)?; + match result.kind { + representation::result::QueryResultKind::Select(sm) => { + // Query results store repeated IRI/literal values as category + // codes into a Model-wide dictionary (Triplestore::global_cats, + // LockedCats) rather than plain polars Categorical-typed + // columns -- format_native_columns (mirrors py_maplib's + // native_dataframe=True path, py_model.rs query_to_result via + // fix_cats_and_multicolumns) is what actually resolves those + // codes back to real values (via maybe_decode_expr) and + // collapses "multi" RDF-typed columns into one Struct field + // per possible type, matching the shape export_solution_mappings + // expects. A plain Series-level Categorical->String cast + // (arrow_bridge::decategorize) does NOT do this -- confirmed + // live, a query result round-tripped to R without this step + // came back as raw integer category codes, not strings. + let representation::solution_mapping::EagerSolutionMappings { + mappings, + mut rdf_node_types, + } = sm; + let lf = representation::formatting::format_native_columns( + mappings.lazy(), + &mut rdf_node_types, + global_cats, + ); + let mappings = lf + .collect() + .map_err(|e| crate::errors::runtime_error(e.to_string()))?; + let sm = representation::solution_mapping::EagerSolutionMappings { + mappings, + rdf_node_types, + }; + crate::arrow_bridge::export_solution_mappings(sm, stream_ptr) + } + representation::result::QueryResultKind::Construct(groups) => { + construct_result_to_solution_mappings(groups, global_cats) + .and_then(|sm| crate::arrow_bridge::export_solution_mappings(sm, stream_ptr)) + } + } + } + + /// Run a SPARQL UPDATE (INSERT DATA/DELETE DATA/DELETE-INSERT-WHERE) + /// against this Model in place (mirrors PyModel::update, + /// py_maplib/src/py_model.rs:415-469 / update_mutex, py_maplib/src/ + /// mutexes.rs:375-397). + /// + /// @param update The SPARQL UPDATE string. Must be a DELETE/INSERT/WHERE + /// form (`INSERT { ... } WHERE { ... }`, optionally with a DELETE + /// clause too, and an empty `WHERE {}` when there's nothing to match + /// against) -- the bare `INSERT DATA { ... }`/`DELETE DATA { ... }` + /// forms are unimplemented in the core engine (`Triplestore:: + /// update_parsed`, lib/triplestore/src/sparql.rs:302-307, a bare + /// `todo!()`) and panic if used. Not fixable in this package -- see + /// maplib-cai upstream issue. + /// @param include_transient Whether the WHERE clause may match transient + /// (e.g. inferred) triples. Defaults to FALSE. + /// @param graph Optional named graph IRI to restrict the update to + /// (no restriction, i.e. the whole store, if NULL -- same "missing + /// means unrestricted" semantics as query()'s `graph` argument, not + /// reads()/writes()'s "missing means the default graph"). + /// @export + fn update(&self, update: &str, include_transient: bool, graph: Option<&str>) -> savvy::Result<()> { + let named_graph = parse_query_graph(graph)?; + let mut inner = self.lock(); + inner + .update(update, None, named_graph.as_ref(), false, include_transient, None, false) + .map_err(maplib_error)?; + Ok(()) + } + + /// Run a SPARQL SELECT/CONSTRUCT query and insert its results as new + /// triples into a (possibly different) graph (mirrors PyModel::insert, + /// py_maplib/src/py_model.rs:540-600 / insert_mutex, py_maplib/src/ + /// mutexes.rs:498-519). New triples' solution mappings (py_maplib's + /// optional return dict) aren't surfaced here -- callers who need the + /// new triples back can query() the target graph again. + /// + /// @param query The SPARQL CONSTRUCT query string to source new triples + /// from (a SELECT query errors -- INSERT needs a triple-shaped result, + /// matching `Triplestore::insert_parsed`'s own constraint, + /// lib/triplestore/src/sparql.rs:240-264). + /// @param include_transient Whether the query may match transient (e.g. + /// inferred) triples in the source graph. Defaults to FALSE. + /// @param transient Whether the newly-inserted triples themselves should + /// be marked transient rather than permanent. Defaults to FALSE. + /// @param source_graph Optional named graph IRI to query from (default + /// graph if NULL). + /// @param target_graph Optional named graph IRI to insert into (default + /// graph if NULL). + /// @export + fn insert( + &self, + query: &str, + include_transient: bool, + transient: bool, + source_graph: Option<&str>, + target_graph: Option<&str>, + ) -> savvy::Result<()> { + let source_graph = parse_optional_named_graph(source_graph)?; + let target_graph = parse_optional_named_graph(target_graph)?; + let mut inner = self.lock(); + inner + .insert( + query, + None, + &source_graph, + include_transient, + &target_graph, + transient, + false, + None, + false, + ) + .map_err(maplib_error)?; + Ok(()) + } + + /// Parse RDF triples from a string into this Model. + /// + /// @param s RDF data as a string. + /// @param format One of "ntriples", "turtle", "xml" (rdf/xml). + /// @param graph Optional named graph IRI to read into (default graph if NULL). + /// @export + fn reads(&self, s: &str, format: &str, graph: Option<&str>) -> savvy::Result<()> { + let format = resolve_format(format)?; + let named_graph = parse_optional_named_graph(graph)?; + let mut inner = self.lock(); + inner + .reads( + s, + format, + None, + false, + None, + true, + &named_graph, + false, + DEFAULT_TRIPLES_BATCH_SIZE, + HashMap::new(), + ) + .map_err(maplib_error) + } + + /// Parse RDF triples from a file into this Model (mirrors PyModel::read, + /// py_maplib/src/py_model.rs:616-647 / read_mutex, py_maplib/src/ + /// mutexes.rs:527-563 -- CIM XML and HDT deliberately unsupported here, + /// same as reads()/resolve_format). Never passes `format = NULL` through + /// to the core engine's own extension-guessing (see + /// guess_format_from_extension's doc comment for why: its guesser's + /// unrecognized-extension fallback is a process-aborting panic, not a + /// catchable error). + /// + /// @param path Path to the RDF file to read. + /// @param format One of "ntriples", "turtle", "xml" (rdf/xml). Guessed + /// from the file extension if NULL (.ttl -> turtle, .nt -> ntriples, + /// .xml/.rdf -> xml) -- any other or missing extension is a normal + /// error, pass `format` explicitly instead. + /// @param graph Optional named graph IRI to read into (default graph if NULL). + /// @export + fn read(&self, path: &str, format: Option<&str>, graph: Option<&str>) -> savvy::Result<()> { + let format = match format { + Some(f) => resolve_format(f)?, + None => guess_format_from_extension(path)?, + }; + let named_graph = parse_optional_named_graph(graph)?; + let mut inner = self.lock(); + inner + .read_triples( + Path::new(path), + Some(format), + None, + false, + None, + true, + &named_graph, + false, + DEFAULT_TRIPLES_BATCH_SIZE, + HashMap::new(), + ) + .map_err(maplib_error) + } + + /// Serialize this Model's triples to a string. + /// + /// @param format One of "ntriples", "turtle", "xml" (rdf/xml). Defaults to "ntriples". + /// @param graph Optional named graph IRI to write (default graph if NULL). + /// @export + fn writes(&self, format: Option<&str>, graph: Option<&str>) -> savvy::Result { + let format = format + .map(resolve_normal_format) + .transpose()? + .unwrap_or(RdfFormat::NTriples); + let named_graph = parse_optional_named_graph(graph)?; + let mut inner = self.lock(); + let mut out = Vec::new(); + inner + .write_triples(&mut out, &named_graph, format, None) + .map_err(maplib_error)?; + String::from_utf8(out) + .map_err(crate::errors::runtime_error)? + .try_into() + } + + /// Serialize this Model's triples to a file (mirrors PyModel::write, + /// py_maplib/src/py_model.rs:737-752 / write_triples_mutex, py_maplib/src/ + /// mutexes.rs:613-650 -- CIM XML and HDT deliberately unsupported here, + /// same as writes()/resolve_normal_format; there's no dedicated + /// path-taking write method on `Model` itself, py_maplib's own mutex + /// opens the `File` at this same layer). + /// + /// @param path Path to write the RDF file to (overwritten if it exists). + /// @param format One of "ntriples", "turtle", "xml" (rdf/xml). Defaults to "ntriples". + /// @param graph Optional named graph IRI to write (default graph if NULL). + /// @export + fn write(&self, path: &str, format: Option<&str>, graph: Option<&str>) -> savvy::Result<()> { + let format = format + .map(resolve_normal_format) + .transpose()? + .unwrap_or(RdfFormat::NTriples); + let named_graph = parse_optional_named_graph(graph)?; + let mut file = File::create(path).map_err(crate::errors::runtime_error)?; + let mut inner = self.lock(); + inner + .write_triples(&mut file, &named_graph, format, None) + .map_err(maplib_error) + } + + /// Build the default (non-FTS) indexes, matching py_maplib's create_index() + /// with no options (create_index_mutex, py_maplib/src/mutexes.rs:402-413). + /// + /// @export + fn create_index(&self) -> savvy::Result<()> { + let mut inner = self.lock(); + inner + .create_index(IndexingOptions::default()) + .map_err(maplib_error) + } + + /// Remove all triples from a graph (mirrors truncate_graph_mutex, + /// py_maplib/src/mutexes.rs:98-104 -- delegates straight to the + /// triplestore, Model has no dedicated wrapper method). + /// + /// @param graph Optional named graph IRI to truncate (default graph if NULL). + /// @export + fn truncate_graph(&self, graph: Option<&str>) -> savvy::Result<()> { + let named_graph = parse_optional_named_graph(graph)?; + let mut inner = self.lock(); + inner.triplestore.truncate(&named_graph); + Ok(()) + } + + /// Add prefix -> IRI mappings, used when serializing (mirrors + /// add_prefixes_mutex, py_maplib/src/mutexes.rs:90-96 -- direct field + /// extend, no dedicated Model method). + /// + /// @param prefixes A named R list of single strings: names are prefixes, + /// values are IRIs, e.g. `list(ex = "http://example.org/")`. + /// @export + fn add_prefixes(&self, prefixes: ListSexp) -> savvy::Result<()> { + let mut parsed = HashMap::new(); + for (name, value) in prefixes.iter() { + let iri = <&str>::try_from(value)?; + let nn = NamedNode::new(iri).map_err(argument_error)?; + parsed.insert(name.to_string(), nn); + } + let mut inner = self.lock(); + inner.prefixes.extend(parsed); + Ok(()) + } + + /// Copy a graph from another Model's triplestore into this one (mirrors + /// PyModel::add_graph, py_maplib/src/py_model.rs:997-1015). + /// + /// @param other Another Model to copy a graph from. + /// @param source_graph Optional named graph IRI in `other` (default graph if NULL). + /// @param target_graph Optional named graph IRI in this Model to copy into (default graph if NULL). + /// @export + fn add_graph( + &self, + other: &RModel, + source_graph: Option<&str>, + target_graph: Option<&str>, + ) -> savvy::Result<()> { + let source_graph = parse_optional_named_graph(source_graph)?; + let target_graph = parse_optional_named_graph(target_graph)?; + let other_inner = other.lock(); + let mut inner = self.lock(); + inner + .add_graph(&other_inner.triplestore, source_graph, target_graph) + .map_err(maplib_error) + } + + /// Split a graph out of this Model into a brand new, standalone Model + /// (mirrors detach_graph_mutex, py_maplib/src/mutexes.rs:106-118). + /// + /// @param preserve_name Keep the graph's own name in the new Model rather + /// than moving it to the default graph there. + /// @param graph Optional named graph IRI to detach (default graph if NULL). + /// @returns A new Model containing only the detached graph. + /// @export + fn detach_graph(&self, preserve_name: bool, graph: Option<&str>) -> savvy::Result { + let named_graph = parse_optional_named_graph(graph)?; + let mut inner = self.lock(); + let sprout = inner + .detach_graph(&named_graph, preserve_name) + .map_err(maplib_error)?; + Ok(RModel { + inner: Mutex::new(sprout), + }) + } + + /// Run RDFS inference over a graph in place, returning the number of new + /// triples inferred -- a triple count, not a rule count + /// (Triplestore::interesting_rdfs_rules, lib/triplestore/src/ + /// rdfs_inferencing.rs, sums per-rule inserted-triple counts) -- mirrors + /// PyModel::infer_rdfs (py_maplib/src/py_model.rs:903-916). + /// + /// @param graph Optional named graph IRI to infer over (default graph if NULL). + /// @export + fn infer_rdfs(&self, graph: Option<&str>) -> savvy::Result { + let named_graph = parse_optional_named_graph(graph)?; + let mut inner = self.lock(); + let n = inner + .infer_rdfs(&named_graph) + .map_err(maplib_error)?; + (n as i32).try_into() + } + + /// Compact on-disk storage (mirrors PyModel::compact, py_maplib/src/py_model.rs:919-924). + /// + /// @export + fn compact(&self) -> savvy::Result<()> { + let mut inner = self.lock(); + inner.compact().map_err(maplib_error) + } + + /// Serialize this Model's triples to a directory in maplib's own compact + /// on-disk format (not an RDF interchange format -- use write()/writes() + /// for that). Mirrors PyModel::serialize, py_maplib/src/py_model.rs:926-934. + /// + /// @param path Directory to serialize into. Defaults to + /// "./serialized_triples", matching py_maplib. + /// @export + fn serialize(&self, path: Option<&str>) -> savvy::Result<()> { + let mut inner = self.lock(); + inner + .serialize_triples(Path::new(path.unwrap_or("./serialized_triples"))) + .map_err(maplib_error) + } + + /// Register an already-built Template (mirrors `Model::add_template`, + /// lib/maplib/src/model.rs:165 -- py_maplib's own `add_template` just + /// forwards a parsed `Template` here too, py_model.rs:74-80). + /// + /// @param template An RTemplate (the raw pointer behind maplibr's S7 + /// `Template` class, `template@raw`). + /// @export + fn add_template(&self, template: &RTemplate) -> savvy::Result<()> { + let mut inner = self.lock(); + inner + .add_template(template.inner.clone()) + .map_err(maplib_error) + } + + /// Parse an stOTTR document string and register every template it + /// defines (mirrors `Model::add_templates_from_string`, + /// lib/maplib/src/model.rs:177-194). Errors if the document defines no + /// templates at all, matching py_maplib's own message for that case + /// (py_maplib/src/mutexes.rs:132-138). + /// + /// @param doc An stOTTR document, as a string. + /// @returns The IRI of the first template the document defines. + /// @export + fn add_template_string(&self, doc: &str) -> savvy::Result { + let mut inner = self.lock(); + let iri = inner + .add_templates_from_string(doc) + .map_err(maplib_error)? + .ok_or_else(|| { + argument_error("Template stOTTR document contained no templates") + })?; + iri.as_str().to_string().try_into() + } + + /// Expand a template against a data.frame, adding the resulting triples + /// to this Model (mirrors `map_mutex`'s data-driven branch, + /// py_maplib/src/mutexes.rs:120-169 -- `Model::expand`, + /// lib/maplib/src/model/expansion.rs:58). + /// + /// @param template_iri IRI of an already-registered template (see + /// `add_template`/`add_template_string`). + /// @param stream_ptr A *filled* Arrow C Stream Interface pointer (e.g. + /// from `nanoarrow::as_nanoarrow_array_stream(df)`), one row per + /// template instantiation. + /// @param graph Optional named graph IRI to add the resulting triples to + /// (default graph if NULL). + /// @param validate_iris Whether to validate that IRI-typed columns + /// contain valid IRIs. Defaults to TRUE, matching py_maplib. + /// @export + fn map( + &self, + template_iri: &str, + stream_ptr: Sexp, + graph: Option<&str>, + validate_iris: Option, + ) -> savvy::Result<()> { + let df = crate::arrow_bridge::import_dataframe(stream_ptr)?; + if df.height() == 0 { + // Matches py_maplib's own map_mutex (py_maplib/src/mutexes.rs: + // 158-162): an empty-but-present DataFrame is a silent no-op, + // not the same as no DataFrame at all -- expand(None) assumes a + // signature with no variables, which isn't true here. + return Ok(()); + } + let named_graph = parse_optional_named_graph(graph)?; + let options = MapOptions::from_args(named_graph, validate_iris); + let mut inner = self.lock(); + inner + .expand(template_iri, Some(df), None, options) + .map_err(maplib_error) + } + + /// Expand a template with no input data.frame -- for templates whose + /// instances are entirely made of constant terms (mirrors `map_mutex`'s + /// `df.is_none()` branch, py_maplib/src/mutexes.rs:163-168). + /// + /// @param template_iri IRI of an already-registered template. + /// @param graph Optional named graph IRI to add the resulting triples to + /// (default graph if NULL). + /// @param validate_iris Whether to validate that IRI-typed columns + /// contain valid IRIs. Defaults to TRUE. + /// @export + fn map_no_data( + &self, + template_iri: &str, + graph: Option<&str>, + validate_iris: Option, + ) -> savvy::Result<()> { + let named_graph = parse_optional_named_graph(graph)?; + let options = MapOptions::from_args(named_graph, validate_iris); + let mut inner = self.lock(); + inner + .expand(template_iri, None, None, options) + .map_err(maplib_error) + } + + /// Map a JSON file straight to triples via a fixed convention (Facade-X; + /// no template involved), mirroring PyModel::map_json's path branch + /// (py_maplib/src/py_model.rs:153-167 / map_json_mutex, py_maplib/src/ + /// mutexes.rs:181-228 -- split into a path- and a string-taking method + /// here rather than one auto-detecting argument, matching this + /// package's own read()/reads() split). + /// + /// @param path Path to the JSON file to map. + /// @param transient Whether the resulting triples should be transient + /// rather than permanent. Defaults to FALSE. + /// @param graph Optional named graph IRI to add the resulting triples to + /// (default graph if NULL). + /// @param uuid_namespace Optional namespace string for the UUIDv5 blank + /// node IRIs minted for JSON objects/arrays. Defaults to the path. + /// @export + fn map_json( + &self, + path: &str, + transient: bool, + graph: Option<&str>, + uuid_namespace: Option<&str>, + ) -> savvy::Result<()> { + let named_graph = parse_optional_named_graph(graph)?; + let mut inner = self.lock(); + inner + .map_json_path( + Path::new(path), + &named_graph, + transient, + uuid_namespace.map(String::from), + ) + .map_err(maplib_error) + } + + /// Map a JSON string straight to triples via a fixed convention + /// (mirrors PyModel::map_json's string branch). See map_json() for the + /// file-based equivalent. + /// + /// @param json The JSON document, as a string. + /// @param transient Whether the resulting triples should be transient + /// rather than permanent. Defaults to FALSE. + /// @param graph Optional named graph IRI to add the resulting triples to + /// (default graph if NULL). + /// @param uuid_namespace Optional namespace string for the UUIDv5 blank + /// node IRIs minted for JSON objects/arrays. Defaults to a random UUID + /// (v4) if not given, matching `Model::map_json_string`. + /// @export + fn map_json_string( + &self, + json: &str, + transient: bool, + graph: Option<&str>, + uuid_namespace: Option<&str>, + ) -> savvy::Result<()> { + let named_graph = parse_optional_named_graph(graph)?; + let mut inner = self.lock(); + inner + .map_json_string( + json.to_string(), + &named_graph, + transient, + uuid_namespace.map(String::from), + ) + .map_err(maplib_error) + } + + /// Map an XML file straight to triples via a fixed convention (mirrors + /// PyModel::map_xml's path branch, py_maplib/src/py_model.rs:169-183 / + /// map_xml_mutex, py_maplib/src/mutexes.rs:230-278 -- split into a path- + /// and a string-taking method here, same rationale as map_json()). + /// + /// @param path Path to the XML file to map. + /// @param transient Whether the resulting triples should be transient + /// rather than permanent. Defaults to FALSE. + /// @param graph Optional named graph IRI to add the resulting triples to + /// (default graph if NULL). + /// @param uuid_namespace Optional namespace string for the UUIDv5 blank + /// node IRIs minted for XML elements. Defaults to the path. + /// @export + fn map_xml( + &self, + path: &str, + transient: bool, + graph: Option<&str>, + uuid_namespace: Option<&str>, + ) -> savvy::Result<()> { + let named_graph = parse_optional_named_graph(graph)?; + let mut inner = self.lock(); + inner + .map_xml_path( + Path::new(path), + &named_graph, + transient, + uuid_namespace.map(String::from), + ) + .map_err(maplib_error) + } + + /// Map an XML string straight to triples via a fixed convention (mirrors + /// PyModel::map_xml's string branch). See map_xml() for the file-based + /// equivalent. + /// + /// @param xml The XML document, as a string. + /// @param transient Whether the resulting triples should be transient + /// rather than permanent. Defaults to FALSE. + /// @param graph Optional named graph IRI to add the resulting triples to + /// (default graph if NULL). + /// @param uuid_namespace Optional namespace string for the UUIDv5 blank + /// node IRIs minted for XML elements. Defaults to a random UUID (v4) + /// if not given, matching `Model::map_xml_string`. + /// @export + fn map_xml_string( + &self, + xml: &str, + transient: bool, + graph: Option<&str>, + uuid_namespace: Option<&str>, + ) -> savvy::Result<()> { + let named_graph = parse_optional_named_graph(graph)?; + let mut inner = self.lock(); + inner + .map_xml_string( + xml.to_string(), + &named_graph, + transient, + uuid_namespace.map(String::from), + ) + .map_err(maplib_error) + } + + /// Map a data.frame's columns directly to subject/predicate/object + /// triples -- one column per predicate, a fresh IRI subject per row, no + /// OTTR template involved (mirrors PyModel::map_df, py_maplib/src/ + /// py_model.rs:216-243 / map_df_mutex, py_maplib/src/mutexes.rs:324-355). + /// Unlike map()'s own zero-row handling, a zero-row data.frame here is + /// NOT a no-op: `Triplestore::map_df` (lib/triplestore/src/map_df.rs: + /// 115-132) unconditionally adds one ` rdf:type fx:root` triple + /// regardless of row count, and py_maplib's own map_df has no + /// zero-row early return either -- so neither does this one, to match. + /// + /// @param stream_ptr A *filled* Arrow C Stream Interface pointer (e.g. + /// from `nanoarrow::as_nanoarrow_array_stream(df)`). + /// @param graph Optional named graph IRI to add the resulting triples to + /// (default graph if NULL). + /// @param uuid_namespace Optional namespace string for the UUIDv5 + /// subject IRIs minted per row. Defaults to a random UUID (v4) if not + /// given, matching `Triplestore::map_df`. + /// @export + fn map_df( + &self, + stream_ptr: Sexp, + graph: Option<&str>, + uuid_namespace: Option<&str>, + ) -> savvy::Result<()> { + let df = crate::arrow_bridge::import_dataframe(stream_ptr)?; + let named_graph = parse_optional_named_graph(graph)?; + let mut inner = self.lock(); + inner + .map_df(&df, &named_graph, uuid_namespace.map(String::from)) + .map_err(maplib_error) + } + + /// Map a data.frame's columns directly to subject/predicate/object + /// triples using the built-in `ottr:Triple` template, optionally fixing + /// every row's predicate to a single constant IRI instead of reading it + /// from a `predicate` column (mirrors PyModel::map_triples, py_maplib/src/ + /// py_model.rs:184-214 / map_triples_mutex, py_maplib/src/mutexes.rs: + /// 280-301 -- both call `Model::expand_triples`, which is really an + /// `expand()` against the built-in triple template rather than a + /// separate `Triplestore` method). Unlike map()'s zero-row handling, + /// there is deliberately no zero-row early return here: `expand_triples` + /// runs the same subject/predicate/object column-existence validation + /// (lib/maplib/src/model/expansion/validation.rs) regardless of row + /// count, and py_maplib's own map_triples has no early return either -- + /// skipping it would let a misnamed/missing column go undetected on an + /// (incidentally) zero-row call instead of raising a normal error. + /// + /// @param stream_ptr A *filled* Arrow C Stream Interface pointer, with + /// subject/predicate/object columns (or just subject/object if + /// `predicate` is given). + /// @param predicate Optional constant predicate IRI to use for every + /// row, instead of a `predicate` column in the data. + /// @param graph Optional named graph IRI to add the resulting triples to + /// (default graph if NULL). + /// @param validate_iris Whether to validate that IRI-typed columns + /// contain valid IRIs. Defaults to TRUE. + /// @export + fn map_triples( + &self, + stream_ptr: Sexp, + predicate: Option<&str>, + graph: Option<&str>, + validate_iris: Option, + ) -> savvy::Result<()> { + let df = crate::arrow_bridge::import_dataframe(stream_ptr)?; + let predicate = predicate + .map(NamedNode::new) + .transpose() + .map_err(argument_error)?; + let named_graph = parse_optional_named_graph(graph)?; + let options = MapOptions::from_args(named_graph, validate_iris); + let mut inner = self.lock(); + inner + .expand_triples(df, None, predicate, options) + .map_err(maplib_error) + } + + /// Load a Model previously written by serialize() (mirrors + /// PyModel::deserialize, py_maplib/src/py_model.rs:936-954). This is an + /// associated function, not a method -- call as `RModel$deserialize(path)`. + /// + /// @param path Directory previously written by serialize(). Defaults to + /// "./serialized_triples", matching py_maplib. + /// @param storage_folder Optional folder for on-disk (rather than + /// in-memory) triplestore storage. + /// @export + fn deserialize(path: Option<&str>, storage_folder: Option<&str>) -> savvy::Result { + let model = maplib::model::Model::deserialize_triples( + Path::new(path.unwrap_or("./serialized_triples")), + storage_folder.map(String::from), + ) + .map_err(maplib_error)?; + Ok(RModel { + inner: Mutex::new(model), + }) + } +} diff --git a/maplibr/src/rust/src/templates.rs b/maplibr/src/rust/src/templates.rs new file mode 100644 index 00000000..f7d7c2d6 --- /dev/null +++ b/maplibr/src/rust/src/templates.rs @@ -0,0 +1,263 @@ +use oxrdf::{BlankNode, Literal as OxLiteral, NamedNode, Variable}; +use representation::constants::OTTR_TRIPLE; +use savvy::{savvy, ListSexp}; +use templates::ast::{ + Argument, ConstantTerm, ConstantTermOrList, Instance, ListExpanderType, PType, Parameter, + Signature, StottrTerm, Template, +}; + +fn parse_list_expander(s: Option<&str>) -> savvy::Result> { + match s { + None => Ok(None), + Some("cross") => Ok(Some(ListExpanderType::Cross)), + Some("zipMin") => Ok(Some(ListExpanderType::ZipMin)), + Some("zipMax") => Ok(Some(ListExpanderType::ZipMax)), + Some(other) => Err(crate::errors::argument_error(format!( + "Unknown list_expander \"{other}\", expected one of \"cross\", \"zipMin\", \"zipMax\"" + ))), + } +} + +/// An OTTR constant term: an IRI, blank node, literal, or explicit "none". +/// Mirrors py_maplib's `extract_constant_term` (lib/templates/src/python.rs:434-446). +/// +/// @export +#[savvy] +pub struct RConstantTerm { + pub inner: ConstantTerm, +} + +#[savvy] +impl RConstantTerm { + fn iri(iri: &str) -> savvy::Result { + let nn = NamedNode::new(iri).map_err(crate::errors::argument_error)?; + Ok(RConstantTerm { + inner: ConstantTerm::Iri(nn), + }) + } + + fn blank_node(id: &str) -> savvy::Result { + let bn = BlankNode::new(id).map_err(crate::errors::argument_error)?; + Ok(RConstantTerm { + inner: ConstantTerm::BlankNode(bn), + }) + } + + fn literal(value: &str, datatype_iri: &str, language: Option<&str>) -> savvy::Result { + let literal = if let Some(language) = language { + OxLiteral::new_language_tagged_literal_unchecked(value, language) + } else { + let dt = + NamedNode::new(datatype_iri).map_err(crate::errors::argument_error)?; + OxLiteral::new_typed_literal(value, dt) + }; + Ok(RConstantTerm { + inner: ConstantTerm::Literal(literal), + }) + } + + fn none() -> Self { + RConstantTerm { + inner: ConstantTerm::None, + } + } +} + +/// An OTTR argument: a Variable, or a constant term (IRI/BlankNode/Literal/none). +/// Mirrors py_maplib's `Argument` (lib/templates/src/python.rs:203-236). +/// +/// @export +#[savvy] +pub struct RArgument { + pub inner: Argument, +} + +#[savvy] +impl RArgument { + fn from_variable(name: &str, list_expand: bool) -> savvy::Result { + let v = Variable::new(name).map_err(crate::errors::argument_error)?; + Ok(RArgument { + inner: Argument { + list_expand, + term: StottrTerm::Variable(v), + }, + }) + } + + fn from_constant_term(term: &RConstantTerm, list_expand: bool) -> Self { + RArgument { + inner: Argument { + list_expand, + term: StottrTerm::ConstantTerm(ConstantTermOrList::ConstantTerm( + term.inner.clone(), + )), + }, + } + } +} + +/// An OTTR template parameter. Mirrors py_maplib's `Parameter` +/// (lib/templates/src/python.rs:21-167) -- `rdf_type` here only supports a +/// basic (non-nested, non-list) RDF type, given as a datatype/class IRI +/// string; PType::Lub/List/NEList and RDFType.Nested()/Multi() are not yet +/// ported (they need the RDFType port, maplib-snz's remaining scope). +/// +/// @export +#[savvy] +pub struct RParameter { + pub inner: Parameter, +} + +#[savvy] +impl RParameter { + fn new( + variable_name: &str, + optional: bool, + allow_blank: bool, + rdf_type_iri: Option<&str>, + default_value: Option<&RConstantTerm>, + ) -> savvy::Result { + let variable = + Variable::new(variable_name).map_err(crate::errors::argument_error)?; + let ptype = rdf_type_iri + .map(|iri| { + NamedNode::new(iri) + .map(PType::Basic) + .map_err(crate::errors::argument_error) + }) + .transpose()?; + let default_value = + default_value.map(|d| ConstantTermOrList::ConstantTerm(d.inner.clone())); + Ok(RParameter { + inner: Parameter { + optional, + non_blank: !allow_blank, + ptype, + variable, + default_value, + }, + }) + } +} + +/// An OTTR template instance: a call to a template with concrete arguments. +/// Mirrors py_maplib's `Instance` (lib/templates/src/python.rs:245-288). +/// +/// @export +#[savvy] +pub struct RInstance { + pub inner: Instance, +} + +#[savvy] +impl RInstance { + fn new( + template_iri: &str, + arguments: ListSexp, + list_expander: Option<&str>, + ) -> savvy::Result { + let template_iri = + NamedNode::new(template_iri).map_err(crate::errors::argument_error)?; + let list_expander = parse_list_expander(list_expander)?; + let mut argument_list = Vec::new(); + for (_, value) in arguments.iter() { + let a = <&RArgument>::try_from(value)?; + argument_list.push(a.inner.clone()); + } + Ok(RInstance { + inner: Instance { + list_expander, + template_iri, + argument_list, + }, + }) + } + + fn iri(&self) -> savvy::Result { + self.inner.template_iri.as_str().to_string().try_into() + } +} + +/// An OTTR template: a named, parameterized set of instance patterns. +/// Mirrors py_maplib's `Template` (lib/templates/src/python.rs:291-402). +/// +/// @export +#[savvy] +pub struct RTemplate { + pub inner: Template, +} + +#[savvy] +impl RTemplate { + fn new(iri: &str, parameters: ListSexp, instances: ListSexp) -> savvy::Result { + let iri = NamedNode::new(iri).map_err(crate::errors::argument_error)?; + let mut parameter_list = Vec::new(); + for (_, value) in parameters.iter() { + let p = <&RParameter>::try_from(value)?; + parameter_list.push(p.inner.clone()); + } + let mut pattern_list = Vec::new(); + for (_, value) in instances.iter() { + let i = <&RInstance>::try_from(value)?; + pattern_list.push(i.inner.clone()); + } + Ok(RTemplate { + inner: Template { + signature: Signature { + iri, + parameter_list, + annotation_list: None, + }, + pattern_list, + }, + }) + } + + fn iri(&self) -> savvy::Result { + self.inner.signature.iri.as_str().to_string().try_into() + } + + fn print_string(&self) -> savvy::Result { + self.inner.to_string().try_into() + } + + /// Build a new Instance calling this Template (mirrors + /// `PyTemplate::instance`, lib/templates/src/python.rs:335-346). + fn instance( + &self, + arguments: ListSexp, + list_expander: Option<&str>, + ) -> savvy::Result { + RInstance::new( + self.inner.signature.iri.as_str(), + arguments, + list_expander, + ) + } +} + +/// Build an rdf:type-style Triple instance (mirrors py_maplib's `Triple()` +/// helper, lib/templates/src/python.rs:404-417). +/// +/// @export +#[savvy] +fn make_triple( + subject: &RArgument, + predicate: &RArgument, + object: &RArgument, + list_expander: Option<&str>, +) -> savvy::Result { + let list_expander = parse_list_expander(list_expander)?; + let template_iri = NamedNode::new(OTTR_TRIPLE).unwrap(); + Ok(RInstance { + inner: Instance { + list_expander, + template_iri, + argument_list: vec![ + subject.inner.clone(), + predicate.inner.clone(), + object.inner.clone(), + ], + }, + }) +} diff --git a/maplibr/src/rust/src/terms.rs b/maplibr/src/rust/src/terms.rs new file mode 100644 index 00000000..78f19080 --- /dev/null +++ b/maplibr/src/rust/src/terms.rs @@ -0,0 +1,79 @@ +use oxrdf::{BlankNode, Literal, NamedNode, Variable}; +use savvy::savvy; +use spargebra::term::GroundTerm; + +/// Validate an IRI's syntax (mirrors oxrdf::NamedNode::new, the same check +/// py_maplib's PyIRI::new uses, lib/representation/src/python.rs:279-282). +/// Raises an R error on invalid syntax; returns nothing on success. +/// +/// @param iri IRI string to validate. +/// @export +#[savvy] +fn validate_iri(iri: &str) -> savvy::Result<()> { + NamedNode::new(iri).map_err(crate::errors::argument_error)?; + Ok(()) +} + +/// Validate a SPARQL variable name's syntax (mirrors oxrdf::Variable::new, +/// used by py_maplib's PyVariable::new, lib/representation/src/python.rs:340-343). +/// Raises an R error on invalid syntax; returns nothing on success. +/// +/// @param name Variable name, without the leading `?`/`$`. +/// @export +#[savvy] +fn validate_variable_name(name: &str) -> savvy::Result<()> { + Variable::new(name).map_err(crate::errors::argument_error)?; + Ok(()) +} + +/// Validate a blank node identifier's syntax (mirrors oxrdf::BlankNode::new, +/// used by py_maplib's PyBlankNode::new, lib/representation/src/python.rs:531-535). +/// Raises an R error on invalid syntax; returns nothing on success. +/// +/// @param id Blank node identifier, without the leading `_:`. +/// @export +#[savvy] +fn validate_blank_node_id(id: &str) -> savvy::Result<()> { + BlankNode::new(id).map_err(crate::errors::argument_error)?; + Ok(()) +} + +/// A ground SPARQL binding value: an IRI or a Literal (no blank nodes, +/// unlike `RConstantTerm` -- blank nodes can't be bound to a query variable, +/// so there's no variant for one to construct in the first place, unlike +/// py_maplib's equivalent, which has to reject a blank node at runtime since +/// it accepts a plain Python value that could be anything, see +/// `maybe_parse_bindings`, py_maplib/src/lib.rs:479-528). Built from an R +/// `IRI`/`Literal` (terms.R) by `.to_ground_term()`, and consumed by +/// `RModel::query`'s `bindings` argument. +/// +/// @export +#[savvy] +pub struct RGroundTerm { + pub inner: GroundTerm, +} + +#[savvy] +impl RGroundTerm { + fn iri(iri: &str) -> savvy::Result { + let nn = NamedNode::new(iri).map_err(crate::errors::argument_error)?; + Ok(RGroundTerm { + inner: GroundTerm::NamedNode(nn), + }) + } + + /// Mirrors `RConstantTerm::literal` (templates.rs) exactly: a language + /// tag takes precedence over -- rather than being combined with -- + /// `datatype_iri`, matching py_maplib's `PyLiteral::new`. + fn literal(value: &str, datatype_iri: &str, language: Option<&str>) -> savvy::Result { + let literal = if let Some(language) = language { + Literal::new_language_tagged_literal_unchecked(value, language) + } else { + let dt = NamedNode::new(datatype_iri).map_err(crate::errors::argument_error)?; + Literal::new_typed_literal(value, dt) + }; + Ok(RGroundTerm { + inner: GroundTerm::Literal(literal), + }) + } +} diff --git a/maplibr/tests/testthat.R b/maplibr/tests/testthat.R new file mode 100644 index 00000000..e755b65c --- /dev/null +++ b/maplibr/tests/testthat.R @@ -0,0 +1,12 @@ +# This file is part of the standard setup for testthat. +# It is recommended that you do not modify it. +# +# Where should you do additional test configuration? +# Learn more about the roles of various files in: +# * https://r-pkgs.org/testing-design.html#sec-tests-files-overview +# * https://testthat.r-lib.org/articles/special-files.html + +library(testthat) +library(maplibr) + +test_check("maplibr") diff --git a/maplibr/tests/testthat/test-errors.R b/maplibr/tests/testthat/test-errors.R new file mode 100644 index 00000000..34609f1a --- /dev/null +++ b/maplibr/tests/testthat/test-errors.R @@ -0,0 +1,50 @@ +# Direct unit tests for .reclass_maplibr_error()/.rethrow()'s regex-parsing +# and fallback logic, flagged by roborev review of commits 5a55ec5/c670e29 +# as still untested: every error test elsewhere goes through Model$reads(), +# which only ever exercises the maplibr_maplib_error/maplibr_argument_error +# tags -- never the fallback branch for a malformed/unrecognized prefix, and +# never maplibr_runtime_error at all. No compiled code needed for these. + +test_that(".rethrow() reclassifies each known tag with its message intact", { + for (tag in c("maplibr_maplib_error", "maplibr_argument_error", "maplibr_runtime_error")) { + err <- tryCatch( + .rethrow(stop(sprintf("[%s] something went wrong", tag))), + error = function(e) e + ) + expect_s3_class(err, tag) + expect_s3_class(err, "maplibr_error") + expect_equal(conditionMessage(err), "something went wrong") + } +}) + +test_that(".rethrow() falls back to a generic maplibr_error for an unrecognized tag", { + err <- tryCatch( + .rethrow(stop("[maplibr_totally_made_up_error] oops")), + error = function(e) e + ) + expect_s3_class(err, "maplibr_error") + expect_false(inherits(err, "maplibr_maplib_error")) + expect_false(inherits(err, "maplibr_argument_error")) + expect_false(inherits(err, "maplibr_runtime_error")) + # Malformed/unrecognized prefixes are not stripped from the message -- + # only a recognized tag is parsed out. + expect_equal(conditionMessage(err), "[maplibr_totally_made_up_error] oops") +}) + +test_that(".rethrow() falls back to a generic maplibr_error for a message with no tag at all", { + err <- tryCatch( + .rethrow(stop("plain, untagged error")), + error = function(e) e + ) + expect_s3_class(err, "maplibr_error") + expect_equal(conditionMessage(err), "plain, untagged error") +}) + +test_that(".rethrow() passes through an already-reclassed maplibr_error unchanged", { + original <- structure( + class = c("maplibr_argument_error", "maplibr_error", "error", "condition"), + list(message = "already reclassed", call = NULL) + ) + err <- tryCatch(.rethrow(stop(original)), error = function(e) e) + expect_identical(err, original) +}) diff --git a/maplibr/tests/testthat/test-model-basics.R b/maplibr/tests/testthat/test-model-basics.R new file mode 100644 index 00000000..54206e24 --- /dev/null +++ b/maplibr/tests/testthat/test-model-basics.R @@ -0,0 +1,84 @@ +# Ports the non-query subset of py_maplib/tests/test_basics.py. Most of +# that file's assertions go through Model.query()/Model.map(), neither of +# which exist in maplibr yet (see maplib-a2d's close notes) -- only the +# handful of assertions reachable without them are ported here. + +test_that("writes() on an empty Model returns without erroring", { + # Mirrors test_write_empty_model (py_maplib/tests/test_basics.py:20-22). + m <- Model$new() + s <- m$writes() + expect_type(s, "character") +}) + +test_that("reads()/writes() round-trip and chain", { + m <- Model$new() + result <- m$reads(' "c" .', format = "ntriples") + expect_identical(result, m) # chainable, returns self invisibly + + expect_equal(m$size(), 1) + expect_match(m$writes(format = "ntriples"), "", fixed = TRUE) +}) + +test_that("create_index() is chainable and doesn't error on a populated Model", { + m <- Model$new() + m$reads(' "c" .', format = "ntriples") + result <- m$create_index() + expect_identical(result, m) +}) + +test_that("truncate_graph() empties the default graph", { + m <- Model$new() + m$reads(' "c" .', format = "ntriples") + expect_equal(m$size(), 1) + m$truncate_graph() + expect_equal(m$size(), 0) +}) + +test_that("add_prefixes() affects writes() output", { + m <- Model$new() + m$reads(' .', format = "ntriples") + m$add_prefixes(list(ex = "http://example.org/")) + out <- m$writes(format = "turtle") + expect_match(out, "@prefix ex: ", fixed = TRUE) +}) + +test_that("serialize()/deserialize() no longer require path -- it defaults to \"./serialized_triples\"", { + # Before this fix, `path` was a required argument in the underlying Rust + # signature -- omitting it would fail immediately with R's own + # missing-argument error, before ever reaching serialize_triples()/ + # deserialize_triples(). It's optional now (defaulting on the Rust side to + # "./serialized_triples", matching py_maplib): calling with no path still + # errors -- the on-disk store is a closed-source stub in this checkout, + # panicking regardless of path -- but not from a missing argument. + is_missing_arg_error <- function(msg) grepl("argument", msg, fixed = TRUE) && grepl("missing", msg, fixed = TRUE) + + # serialize() creates real on-disk structure at its (default) path before + # panicking in the stub -- run from a scratch directory so this doesn't + # leave a "serialized_triples" directory behind in the working tree. + scratch_dir <- tempfile("maplibr-test-") + dir.create(scratch_dir) + old_wd <- setwd(scratch_dir) + on.exit({ + setwd(old_wd) + unlink(scratch_dir, recursive = TRUE) + }, add = TRUE) + + m <- Model$new() + m$reads(' "c" .', format = "ntriples") + err <- tryCatch(m$serialize(), error = function(e) conditionMessage(e)) + expect_false(is_missing_arg_error(err)) + + err2 <- tryCatch(Model$deserialize(), error = function(e) conditionMessage(e)) + expect_false(is_missing_arg_error(err2)) +}) + +test_that("clone() is disabled -- R6's default shallow clone would share the underlying Mutex", { + # R6's default clone() only copies the R-level reference to + # private$rmodel, not the underlying Rust state -- m2 <- m$clone() would + # look independent while actually sharing the same mutex-guarded + # triplestore. cloneable = FALSE removes $clone() entirely rather than + # leaving a misleading shallow one in place. + m <- Model$new() + expect_null(m$clone) + expect_error(m$clone()) +}) diff --git a/maplibr/tests/testthat/test-model-errors.R b/maplibr/tests/testthat/test-model-errors.R new file mode 100644 index 00000000..2e4f18d4 --- /dev/null +++ b/maplibr/tests/testthat/test-model-errors.R @@ -0,0 +1,34 @@ +# Ports the portable subset of py_maplib/tests/test_exceptions.py. Most of +# that file's exception cases only trigger inside query() (not implemented +# in maplibr yet); test_model_exception (invalid RDF syntax) doesn't need +# query() at all and ports directly. See maplib-dma's close notes for the +# maplibr_maplib_error/maplibr_argument_error condition classes. + +test_that("reads() with invalid RDF syntax raises a catchable maplibr_maplib_error", { + # Mirrors test_model_exception (py_maplib/tests/test_exceptions.py:7-11). + m <- Model$new() + expect_error( + m$reads("abc", format = "turtle", graph = "http://example.com/data"), + class = "maplibr_maplib_error" + ) +}) + +test_that("reads() with an unrecognized format raises a catchable maplibr_argument_error", { + m <- Model$new() + expect_error( + m$reads(' .', format = "not-a-format"), + class = "maplibr_argument_error" + ) +}) + +test_that("an unrecognized list_expander raises an error naming it", { + # Not reclassed to maplibr_argument_error like Model's methods are -- + # templates.R's plain constructors aren't wired through .rethrow() (see + # errors.R's top-of-file note) -- but the underlying [maplibr_argument_error] + # tag is still present in the raw message. + a <- IRI("http://example.org/a") + expect_error( + Triple(a, a, a, list_expander = "not-a-real-expander"), + regexp = "not-a-real-expander" + ) +}) diff --git a/maplibr/tests/testthat/test-model-graphs.R b/maplibr/tests/testthat/test-model-graphs.R new file mode 100644 index 00000000..5803b52c --- /dev/null +++ b/maplibr/tests/testthat/test-model-graphs.R @@ -0,0 +1,151 @@ +# Ports py_maplib/tests/test_named_graphs.py. query() (maplib-l2j) and +# size(graph) (maplib-0mj) are both available now, so this no longer needs to +# route around m.size(graph) via writes(graph=) the way an earlier version of +# this file did. + +gr1 <- ' "A" .' +gr2 <- ' "B" .' +gr3 <- ' + "B" . + "C" . +' + +test_that("reads() routes triples into the requested named graph", { + # Ports test_basic_named_graph (py_maplib/tests/test_named_graphs.py:17-30). + ng2 <- "urn:graph:gr2" + m <- Model$new() + m$reads(gr1, format = "ntriples") + m$reads(gr2, format = "ntriples", graph = ng2) + + df <- m$query("SELECT * WHERE { GRAPH { ?a ?b ?c } }") + expect_equal(nrow(df), 1) + expect_equal(df$c[1], "B") + expect_equal(m$size(ng2), 1) + expect_equal(m$size(), 1) +}) + +test_that("reads() with no graph routes into the default graph, queryable via maplib:DefaultGraph", { + # Ports test_basic_named_graph_named_default (py_maplib/tests/ + # test_named_graphs.py:35-51). + ng2 <- "urn:graph:gr2" + m <- Model$new() + m$reads(gr1, format = "ntriples", graph = ng2) + m$reads(gr2, format = "ntriples") + + df <- m$query("SELECT * WHERE { GRAPH maplib:DefaultGraph { ?a ?b ?c } }") + expect_equal(nrow(df), 1) + expect_equal(df$c[1], "B") + expect_equal(m$size(ng2), 1) + expect_equal(m$size(), 1) +}) + +test_that("the default graph's full IRI is also a valid graph argument", { + # Ports test_basic_named_graph_named_default_as_arg (py_maplib/tests/ + # test_named_graphs.py:53-67). + ng2 <- "urn:graph:gr2" + m <- Model$new() + m$reads(gr1, format = "ntriples", graph = ng2) + m$reads(gr2, format = "ntriples", graph = "https://datatreehouse.github.io/maplib/vocab#DefaultGraph") + + df <- m$query("SELECT * WHERE { ?a ?b ?c }") + expect_equal(nrow(df), 1) + expect_equal(df$c[1], "B") + expect_equal(m$size(ng2), 1) + expect_equal(m$size(), 1) +}) + +test_that("size(graph) counts only that graph's triples", { + # Ports test_basic_named_graph_more_triples (py_maplib/tests/ + # test_named_graphs.py:69-79). + ng2 <- "urn:graph:gr2" + ng3 <- "urn:graph:gr3" + m <- Model$new() + m$reads(gr1, format = "ntriples") + m$reads(gr3, format = "ntriples", graph = ng2) + + expect_equal(m$size(ng2), 2) + expect_equal(m$size(), 1) + expect_equal(m$size(ng3), 0) +}) + +test_that("query() can join across two named graphs in one WHERE clause", { + # Ports test_basic_named_graph_2 (py_maplib/tests/test_named_graphs.py:82-100). + ng1 <- "urn:graph:gr1" + ng2 <- "urn:graph:gr2" + m <- Model$new() + m$reads(gr1, format = "ntriples", graph = ng1) + m$reads(gr2, format = "ntriples", graph = ng2) + + df <- m$query(' + SELECT * WHERE { + GRAPH { ?a ?b1 ?c1 } + GRAPH { ?a ?b2 ?c2 } + } + ') + expect_equal(nrow(df), 1) + expect_equal(df$c2[1], "B") +}) + +test_that("add_graph() copies a named graph's triples into another Model", { + m1 <- Model$new() + m1$reads(gr1, format = "ntriples") + + m2 <- Model$new() + m2$add_graph(m1) + expect_equal(m2$size(), 1) + expect_match(m2$writes(format = "ntriples"), '"A"', fixed = TRUE) +}) + +test_that("add_graph() refuses to add a Model to itself", { + # add_graph()'s underlying Rust call locks other then self in fixed + # order; other = self would lock the same non-reentrant Mutex twice on + # one thread and hang the R session. Must error instead. + m <- Model$new() + m$reads(gr1, format = "ntriples") + expect_error(m$add_graph(m), "same underlying Model") +}) + +test_that("add_graph() refuses two distinct Model wrappers sharing one underlying RModel", { + # The pointer comparison (not identical(self, other)) exists specifically + # to also catch this case: two separate R6 objects wrapping the same + # underlying Rust Mutex, as a shallow clone would produce. + m <- Model$new() + m$reads(gr1, format = "ntriples") + aliased <- Model$new(m$.__enclos_env__$private$rmodel) + expect_error(m$add_graph(aliased), "same underlying Model") +}) + +test_that("detach_graph() splits a named graph into a new, standalone Model", { + # Ports test_detach_graph (py_maplib/tests/test_named_graphs.py:102-146). + ng1 <- "urn:graph:gr1" + ng2 <- "urn:graph:gr2" + m <- Model$new() + m$reads(gr1, format = "ntriples", graph = ng1) + m$reads(gr2, format = "ntriples", graph = ng2) + m2 <- m$detach_graph(preserve_name = TRUE, graph = ng2) + + expect_equal(nrow(m$query("SELECT * WHERE { GRAPH { ?a ?b2 ?c2 } }")), 0) + expect_equal(nrow(m$query("SELECT * WHERE { GRAPH { ?a ?b1 ?c1 } }")), 1) + + df2 <- m2$query("SELECT * WHERE { GRAPH { ?a ?b2 ?c2 } }") + expect_equal(nrow(df2), 1) + expect_equal(df2$c2[1], "B") + expect_equal(nrow(m2$query("SELECT * WHERE { GRAPH { ?a ?b1 ?c1 } }")), 0) +}) + +test_that("detach_graph() with no graph splits off the default graph", { + # Ports test_detach_default_graph (py_maplib/tests/test_named_graphs.py:148-188). + ng1 <- "urn:graph:gr1" + m <- Model$new() + m$reads(gr1, format = "ntriples", graph = ng1) + m$reads(gr2, format = "ntriples") + m2 <- m$detach_graph() + + expect_equal(nrow(m$query("SELECT * WHERE { ?a ?b2 ?c2 }")), 0) + expect_equal(nrow(m$query("SELECT * WHERE { GRAPH { ?a ?b1 ?c1 } }")), 1) + + df2 <- m2$query("SELECT * WHERE { ?a ?b2 ?c2 }") + expect_equal(nrow(df2), 1) + expect_equal(df2$c2[1], "B") + expect_equal(nrow(m2$query("SELECT * WHERE { GRAPH { ?a ?b1 ?c1 } }")), 0) +}) diff --git a/maplibr/tests/testthat/test-model-infer-rdfs.R b/maplibr/tests/testthat/test-model-infer-rdfs.R new file mode 100644 index 00000000..2adaec9a --- /dev/null +++ b/maplibr/tests/testthat/test-model-infer-rdfs.R @@ -0,0 +1,42 @@ +# Ports test_rdfs_inference.py (py_maplib/tests/test_rdfs_inference.py) +# against the same fixture data (tests/testthat/testdata/rdfs/). An earlier +# version of this file judged this unportable without writes() gaining an +# include_transient option -- that turned out to be a misdiagnosis: the +# py_maplib test itself only ever uses query(), which already supports +# include_transient (maplib-l2j's query() port). Now genuinely portable. + +read_fixture <- function(path) paste(readLines(path, warn = FALSE), collapse = "\n") + +rdfs_folders <- file.path( + test_path("testdata", "rdfs"), + c("rdfs2", "rdfs3", "rdfs5", "rdfs6", "rdfs8", "rdfs9", "rdfs10", "rdfs11") +) + +for (dir in rdfs_folders) { + test_that(paste0("infer_rdfs() matches the expected fixture (", basename(dir), ")"), { + m_input <- Model$new() + m_input$reads(read_fixture(file.path(dir, "input.ttl")), format = "turtle") + m_input$infer_rdfs() + + m_expected <- Model$new() + m_expected$reads(read_fixture(file.path(dir, "expected.ttl")), format = "turtle") + + order_rows <- function(df) df[do.call(order, df), , drop = FALSE] + df_inferred <- order_rows(m_input$query( + "SELECT * WHERE { ?a ?b ?c }", + include_transient = TRUE + )) + df_expected <- order_rows(m_expected$query("SELECT * WHERE { ?a ?b ?c }")) + + rownames(df_inferred) <- NULL + rownames(df_expected) <- NULL + attr(df_inferred, "rdf_node_types") <- NULL + attr(df_expected, "rdf_node_types") <- NULL + expect_equal(df_inferred, df_expected) + }) +} + +# NOTE: writes() still can't serialize transient/inferred triples at all -- +# Triplestore::write_triples (core engine, not this package) has no +# include_transient option, unlike the query path. That's a core-engine gap, +# not maplibr's to fix; flagged for upstream instead of worked around here. diff --git a/maplibr/tests/testthat/test-model-insert-update.R b/maplibr/tests/testthat/test-model-insert-update.R new file mode 100644 index 00000000..762d74e8 --- /dev/null +++ b/maplibr/tests/testthat/test-model-insert-update.R @@ -0,0 +1,80 @@ +# Tests for Model$update() and Model$insert() (maplib-zlr). + +test_that("update() runs a DELETE/INSERT/WHERE modify against existing data", { + m <- Model$new() + m$reads(' .', format = "ntriples") + + m$update("INSERT { } WHERE { }") + expect_equal(m$size(), 2) + + m$update("DELETE { } WHERE { }") + expect_equal(m$size(), 1) +}) + +test_that("update() with an empty WHERE {} inserts/deletes constant triples", { + # INSERT DATA/DELETE DATA panic in the core engine (todo!() in + # Triplestore::update_parsed) -- WHERE {} is the documented workaround, see + # Model$update()'s roxygen docs and maplib-cai (upstream issue). + m <- Model$new() + m$update("INSERT { } WHERE {}") + expect_equal(m$size(), 1) + + m$update("DELETE { } WHERE {}") + expect_equal(m$size(), 0) +}) + +test_that("update()'s graph argument restricts which graph's data is matched/modified", { + m <- Model$new() + m$reads(' .', format = "ntriples", graph = "http://ex/g1") + + m$update( + "INSERT { ?s ?o } WHERE { ?s ?o }", + graph = "http://ex/g2" + ) + expect_equal(m$size(graph = "http://ex/g1"), 1) + expect_equal(m$size(graph = "http://ex/g2"), 0) +}) + +test_that("update() raises a catchable error for invalid SPARQL syntax", { + m <- Model$new() + expect_error( + m$update("this is not valid SPARQL UPDATE !!!"), + class = "maplibr_maplib_error" + ) +}) + +test_that("insert() adds a CONSTRUCT query's results into the target graph", { + m <- Model$new() + m$reads(' .', format = "ntriples") + + m$insert( + "CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }", + target_graph = "http://ex/g2" + ) + expect_equal(m$size(), 1) + expect_equal(m$size(graph = "http://ex/g2"), 1) + + df <- m$query("SELECT ?s ?p ?o WHERE { ?s ?p ?o }", graph = "http://ex/g2") + expect_equal(df$s, "http://ex/a") +}) + +test_that("insert()'s source_graph restricts which graph is queried for new triples", { + m <- Model$new() + m$reads(' .', format = "ntriples", graph = "http://ex/g1") + + m$insert( + "CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }", + source_graph = "http://ex/nonexistent", + target_graph = "http://ex/g2" + ) + expect_equal(m$size(graph = "http://ex/g2"), 0) +}) + +test_that("insert() rejects a SELECT query (INSERT needs a triple-shaped CONSTRUCT result)", { + m <- Model$new() + m$reads(' .', format = "ntriples") + expect_error( + m$insert("SELECT ?s ?p ?o WHERE { ?s ?p ?o }"), + class = "maplibr_maplib_error" + ) +}) diff --git a/maplibr/tests/testthat/test-model-map-json-xml-df-triples.R b/maplibr/tests/testthat/test-model-map-json-xml-df-triples.R new file mode 100644 index 00000000..fae554af --- /dev/null +++ b/maplibr/tests/testthat/test-model-map-json-xml-df-triples.R @@ -0,0 +1,139 @@ +# Tests for Model$map_json()/map_json_string()/map_xml()/map_xml_string()/ +# map_df()/map_triples() (maplib-b98) -- direct-to-triples mapping entry +# points that don't go through an OTTR template, unlike $map()/$map_no_data(). + +test_that("map_json_string() maps a JSON string via the Facade-X convention", { + # `1` is mapped to an xsd:long-typed literal, not a string + # (lib/triplestore/src/map_json.rs) -- `df$o == "1"` only passes because + # the resulting multi-typed ?o column gets coalesced to character by + # .collapse_multitype_columns(). If that coercion's behavior for + # non-character sub-columns ever changes, this assertion is the one that + # would need updating, not a sign the mapping itself broke. + m <- Model$new() + m$map_json_string('{"a": 1, "b": "x"}') + df <- m$query("SELECT ?p ?o WHERE { ?s ?p ?o }") + expect_true(any(df$p == "http://sparql.xyz/facade-x/data/a" & df$o == "1")) + expect_true(any(df$p == "http://sparql.xyz/facade-x/data/b" & df$o == "x")) +}) + +test_that("map_json() maps a JSON file the same way as map_json_string()", { + m <- Model$new() + path <- tempfile(fileext = ".json") + on.exit(unlink(path)) + writeLines('{"a": 1, "b": "x"}', path) + m$map_json(path) + df <- m$query("SELECT ?p ?o WHERE { ?s ?p ?o }") + expect_true(any(df$p == "http://sparql.xyz/facade-x/data/a" & df$o == "1")) +}) + +test_that("map_json()'s graph argument scopes the mapped triples to one named graph", { + m <- Model$new() + path <- tempfile(fileext = ".json") + on.exit(unlink(path)) + writeLines('{"a": 1}', path) + m$map_json(path, graph = "http://ex/g1") + expect_gt(m$size(graph = "http://ex/g1"), 0) + expect_equal(m$size(), 0) +}) + +test_that("map_xml_string() maps an XML string via the Facade-X convention", { + # map_xml()'s convention represents each element's tag name as an rdf:type + # value (e.g. xyz:data/a), and its text content as an fx:child leaf value + # -- not "predicate named after the tag", unlike map_df(). + m <- Model$new() + m$map_xml_string("1x") + df <- m$query("SELECT ?p ?o WHERE { ?s ?p ?o }") + types <- df$o[df$p == "http://www.w3.org/1999/02/22-rdf-syntax-ns#type"] + expect_true("http://sparql.xyz/facade-x/data/a" %in% types) + expect_true("http://sparql.xyz/facade-x/data/b" %in% types) + leaves <- df$o[df$p == "http://sparql.xyz/facade-x/ns/child"] + expect_true("1" %in% leaves) + expect_true("x" %in% leaves) +}) + +test_that("map_xml() maps an XML file the same way as map_xml_string()", { + m <- Model$new() + path <- tempfile(fileext = ".xml") + on.exit(unlink(path)) + writeLines("1", path) + m$map_xml(path) + df <- m$query("SELECT ?p ?o WHERE { ?s ?p ?o }") + types <- df$o[df$p == "http://www.w3.org/1999/02/22-rdf-syntax-ns#type"] + expect_true("http://sparql.xyz/facade-x/data/a" %in% types) +}) + +test_that("map_df() maps a data.frame's columns directly, one predicate per column", { + # map_df() wraps the whole data.frame as a Facade-X-style document: a + # fresh root subject, one fx:child + fx:childNumber pair per row (for + # ordering), plus each row's own columns as direct data predicates -- + # confirmed against Triplestore::map_df (lib/triplestore/src/map_df.rs), + # not the flatter "N rows x N columns = N*M triples" shape one might + # assume from the name alone. + m <- Model$new() + d <- data.frame(name = c("Alice", "Bob"), age = c(30L, 25L), stringsAsFactors = FALSE) + m$map_df(d) + df <- m$query("SELECT ?p ?o WHERE { ?s ?p ?o }") + expect_equal(nrow(df), 9) + name_rows <- df[df$p == "http://sparql.xyz/facade-x/data/name", ] + expect_setequal(name_rows$o, c("Alice", "Bob")) +}) + +test_that("map_df() with a zero-row data.frame still adds the root triple", { + # NOT a no-op, unlike $map(): Triplestore::map_df unconditionally adds one + # rdf:type fx:root triple regardless of row count, and py_maplib's + # own map_df has no zero-row early return either -- matched here rather + # than diverging from it. + m <- Model$new() + d <- data.frame(name = character(0), stringsAsFactors = FALSE) + m$map_df(d) + expect_equal(m$size(), 1) + df <- m$query("SELECT ?p ?o WHERE { ?s ?p ?o }") + expect_equal(df$p, "http://www.w3.org/1999/02/22-rdf-syntax-ns#type") + expect_equal(df$o, "http://sparql.xyz/facade-x/ns/root") +}) + +test_that("map_triples() maps subject/predicate/object columns directly", { + m <- Model$new() + d <- data.frame( + subject = "http://ex/a", predicate = "http://ex/p", object = "http://ex/o", + stringsAsFactors = FALSE + ) + m$map_triples(d) + expect_equal(m$size(), 1) + df <- m$query("SELECT ?s ?p ?o WHERE { ?s ?p ?o }") + expect_equal(df$s, "http://ex/a") + expect_equal(df$p, "http://ex/p") + expect_equal(df$o, "http://ex/o") +}) + +test_that("map_triples()'s predicate argument fixes a constant predicate for every row", { + m <- Model$new() + d <- data.frame( + subject = c("http://ex/a", "http://ex/b"), + object = c("http://ex/o1", "http://ex/o2"), + stringsAsFactors = FALSE + ) + m$map_triples(d, predicate = "http://ex/const") + expect_equal(m$size(), 2) + df <- m$query("SELECT ?p WHERE { ?s ?p ?o }") + expect_true(all(df$p == "http://ex/const")) +}) + +test_that("map_triples() with a zero-row data.frame and correct columns is a no-op", { + m <- Model$new() + d <- data.frame(subject = character(0), predicate = character(0), object = character(0)) + m$map_triples(d) + expect_equal(m$size(), 0) +}) + +test_that("map_triples() still validates columns on a zero-row data.frame", { + # Regression case: an earlier version skipped column validation entirely + # for a zero-row input, so a misnamed/missing column (e.g. a data.frame + # upstream-filtered down to zero rows) would silently succeed instead of + # raising a normal error -- expand_triples's own column-existence check + # (lib/maplib/src/model/expansion/validation.rs) runs regardless of row + # count, and this now does too. + m <- Model$new() + d <- data.frame(subj = character(0), pred = character(0), obj = character(0)) + expect_error(m$map_triples(d), class = "maplibr_maplib_error") +}) diff --git a/maplibr/tests/testthat/test-model-map.R b/maplibr/tests/testthat/test-model-map.R new file mode 100644 index 00000000..34db70b8 --- /dev/null +++ b/maplibr/tests/testthat/test-model-map.R @@ -0,0 +1,107 @@ +# Tests for Model$map()/Model$add_template() (maplib-cgd). Ports the subset +# of py_maplib/tests/test_basics.py that only needs map()/add_template() plus +# query() (already available, maplib-l2j) -- OTTR features query() itself +# doesn't cover yet (CONSTRUCT, streaming=) are out of scope here. + +test_that("add_template(doc) registers a template usable by IRI in map()", { + # Ports test_add_template_instead_of_constructor_df (py_maplib/tests/ + # test_basics.py:43-54). + doc <- ' + @prefix ex:. + ex:ExampleTemplate [?MyValue] :: { + ottr:Triple(ex:myObject, ex:hasValue, ?MyValue) + } . + ' + m <- Model$new() + iri <- m$add_template(doc) + expect_equal(iri, "http://example.net/ns#ExampleTemplate") + + m$map("http://example.net/ns#ExampleTemplate", data.frame(MyValue = "A")) + df <- m$query("SELECT ?o WHERE { ?o }") + expect_equal(df$o, "A") +}) + +test_that("map() accepts a Template object directly, with an optional missing parameter", { + # Ports test_create_programmatic_model_with_optional_value_missing_df + # (py_maplib/tests/test_basics.py:101-145), sans the streaming= parametrization + # (query() has no streaming argument to port). + ex <- Prefix("http://example.net/ns#", "ex") + my_value <- Variable("MyValue") + my_other_value <- Variable("MyOtherValue") + my_object <- suf(ex, "MyObject") + template <- Template( + iri = suf(ex, "ExampleTemplate"), + parameters = list(Parameter(my_value), Parameter(my_other_value, optional = TRUE)), + instances = list( + Triple(my_object, suf(ex, "hasValue"), my_value), + Triple(my_object, suf(ex, "hasOtherValue"), my_other_value) + ) + ) + m <- Model$new() + m$map(template, data.frame(MyValue = "A")) + + df <- m$query("PREFIX ex: SELECT ?A WHERE { ?obj1 ex:hasValue ?A }") + expect_equal(df$A, "A") + + df2 <- m$query("PREFIX ex: SELECT ?A WHERE { ?obj1 ex:hasOtherValue ?A }") + expect_equal(nrow(df2), 0) +}) + +test_that("map() with no data.frame expands a constant-only template once", { + doc <- ' + @prefix ex:. + ex:StaticTemplate [] :: { + ottr:Triple(ex:a, ex:b, ex:c) + } . + ' + m <- Model$new() + m$add_template(doc) + m$map("http://example.net/ns#StaticTemplate") + expect_equal(m$size(), 1) +}) + +test_that("map() with a zero-row data.frame is a no-op", { + # Ports test_create_model_from_empty_polars_df (py_maplib/tests/ + # test_basics.py:24-35). + doc <- ' + @prefix ex:. + ex:ExampleTemplate [?MyValue] :: { + ottr:Triple(ex:myObject, ex:hasValue, ?MyValue) + } . + ' + m <- Model$new() + m$add_template(doc) + m$map("http://example.net/ns#ExampleTemplate", data.frame(MyValue = character(0))) + expect_equal(m$size(), 0) +}) + +test_that("map() treats a template IRI containing '::' as a plain reference, not a doc string", { + # Regression test for roborev job 22/24: .resolve_template_iri used to + # discriminate a doc string from a bare IRI by checking for a literal + # "::" substring, which would misclassify a real IRI containing "::" + # (e.g. some URN forms) as a doc string and send it to + # add_template_string(), raising a confusing stOTTR parse error instead + # of using it as a plain reference. + ex <- Prefix("http://example.net/ns#", "ex") + iri_with_colons <- "http://example.net/ns#Example::Template" + my_value <- Variable("MyValue") + template <- Template( + iri = IRI(iri_with_colons), + parameters = list(Parameter(my_value)), + instances = list(Triple(suf(ex, "myObject"), suf(ex, "hasValue"), my_value)) + ) + m <- Model$new() + m$add_template(template) + + # Passed as a plain character string (not the Template object) so it goes + # through the doc-string/IRI discriminator in .resolve_template_iri. + m$map(iri_with_colons, data.frame(MyValue = "A")) + df <- m$query("SELECT ?o WHERE { ?o }") + expect_equal(df$o, "A") +}) + +test_that("add_template()/map() reject an unrecognized `template` argument", { + m <- Model$new() + expect_error(m$add_template(42), "Template") + expect_error(m$map(42), "Template") +}) diff --git a/maplibr/tests/testthat/test-model-query-bindings.R b/maplibr/tests/testthat/test-model-query-bindings.R new file mode 100644 index 00000000..1f1fe174 --- /dev/null +++ b/maplibr/tests/testthat/test-model-query-bindings.R @@ -0,0 +1,53 @@ +# Tests for Model$query(bindings=) (maplib-aae) -- pre-binding SPARQL +# variables to R values before a query runs. Usage pattern (SELECT * plus a +# FILTER comparing against the bound variable, rather than SELECT-ing the +# bound variable directly) mirrors py_maplib's own test_replace_bindings.py. + +test_that("bindings pre-binds a variable to a Literal, usable in a FILTER", { + m <- Model$new() + df <- m$query( + 'SELECT * WHERE { VALUES ?a { "a" "b" } FILTER(?a = ?b) }', + bindings = list(b = Literal("b")) + ) + expect_equal(nrow(df), 1) + expect_equal(df$a, "b") +}) + +test_that("bindings pre-binds a variable to an IRI, usable in a FILTER", { + m <- Model$new() + df <- m$query( + "SELECT * WHERE { VALUES ?a { \"a\" } FILTER(?a = ?b) }", + bindings = list(b = IRI("urn:abc")) + ) + expect_equal(nrow(df), 1) + expect_equal(df$a, "urn:abc") +}) + +test_that("a query with no bindings behaves exactly as before", { + m <- Model$new() + df <- m$query('SELECT * WHERE { VALUES ?a { "a" "b" } }') + expect_equal(nrow(df), 2) +}) + +test_that("bindings values must be IRI or Literal objects", { + m <- Model$new() + expect_error( + m$query('SELECT * WHERE { VALUES ?a { "a" } }', bindings = list(a = "not-a-term-object")), + "IRI or Literal" + ) +}) + +test_that("binding a variable that's also SELECT-ed raises a catchable error, not a crash", { + # Core-engine restriction (Triplestore::query's underlying + # maybe_replace_bindings/replace_bindings_graph_pattern, lib/query_processing/ + # src/bindings.rs's Project match arm): a bound variable can't be listed + # directly in SELECT, only used inside the query body (e.g. a FILTER). + m <- Model$new() + expect_error( + m$query( + 'SELECT ?b WHERE { VALUES ?a { "a" } FILTER(?a = ?b) }', + bindings = list(b = Literal("a")) + ), + class = "maplibr_maplib_error" + ) +}) diff --git a/maplibr/tests/testthat/test-model-query.R b/maplibr/tests/testthat/test-model-query.R new file mode 100644 index 00000000..87c68928 --- /dev/null +++ b/maplibr/tests/testthat/test-model-query.R @@ -0,0 +1,179 @@ +# Tests for Model$query() (maplib-l2j). Real query() support removes the +# blocker that limited earlier test files (test-model-basics.R, +# test-model-infer-rdfs.R, etc.) to non-query assertions -- this file covers +# query() itself; expanding the older files to use query() where py_maplib's +# own tests did is left for follow-up (maplib-a2d). + +test_that("a simple SELECT returns real, decoded values (not category codes)", { + # Confirmed live, worth guarding against regressing: query results store + # repeated IRI/literal values as category codes into a Model-wide + # dictionary, not plain polars Categorical columns -- an earlier attempt + # at decoding (a Series-level Categorical->String cast) silently did + # nothing, and values came back as raw integer codes (e.g. "0", "1") + # instead of the real strings. Fixed via representation::formatting:: + # format_native_columns (see model.rs's query()). + m <- Model$new() + m$reads(paste( + ' "Alice" .', + ' "Bob" .', + sep = "\n" + ), format = "ntriples") + + df <- m$query("SELECT ?s ?o WHERE { ?s ?o }") + expect_setequal(df$s, c("http://ex/a", "http://ex/b")) + expect_setequal(df$o, c("Alice", "Bob")) +}) + +test_that("a column whose RDF node type varies by row is collapsed to one vector", { + m <- Model$new() + m$reads(paste( + ' "Alice" .', + " .", + sep = "\n" + ), format = "ntriples") + + df <- m$query(' + SELECT ?x WHERE { + { ?x } + UNION + { ?x } + } + ') + expect_setequal(df$x, c("Alice", "http://ex/b")) + expect_false(is.data.frame(df$x)) + + types <- attr(df, "rdf_node_types")$x + kinds <- vapply(types, function(t) t$kind, character(1)) + expect_setequal(kinds, c("iri", "literal")) +}) + +test_that("include_transient controls whether inferred triples are visible to query()", { + m <- Model$new() + m$reads(paste( + " .", + " .", + sep = "\n" + ), format = "turtle") + + n <- m$infer_rdfs() + expect_gt(n, 0) + + with_transient <- m$query("SELECT * WHERE { ?s ?p ?o }", include_transient = TRUE) + without_transient <- m$query("SELECT * WHERE { ?s ?p ?o }", include_transient = FALSE) + # n is a count of newly-inserted triples (Triplestore::interesting_rdfs_rules, + # lib/triplestore/src/rdfs_inferencing.rs:170, sums per-rule inserted-triple + # counts), not a count of rules applied, despite Model$infer_rdfs's own doc + # comment saying "the number of interesting inference rules applied" -- so + # this equality is guaranteed, not coincidental. + expect_equal(nrow(with_transient), nrow(without_transient) + n) +}) + +test_that("query() restricted to a graph with no matching triples returns zero rows", { + m <- Model$new() + m$reads(' .', format = "ntriples") + df <- m$query("SELECT * WHERE { ?s ?p ?o }", graph = "http://nonexistent/graph") + expect_equal(nrow(df), 0) +}) + +test_that("query() raises a catchable error for invalid SPARQL syntax", { + m <- Model$new() + expect_error( + m$query("this is not valid SPARQL !!!"), + class = "maplibr_maplib_error" + ) +}) + +test_that("CONSTRUCT with a literal predicate returns a subject/predicate/object data.frame", { + m <- Model$new() + m$reads(paste( + ' "Alice" .', + ' "Bob" .', + sep = "\n" + ), format = "ntriples") + + df <- m$query("CONSTRUCT { ?s ?o } WHERE { ?s ?o }") + expect_setequal(names(df), c("subject", "predicate", "object")) + expect_equal(nrow(df), 2) + expect_true(all(df$predicate == "http://ex/hasName")) + expect_setequal(df$object, c("Alice", "Bob")) +}) + +test_that("CONSTRUCT with a bound ?p and a row-varying ?o flattens multi-typed values correctly", { + # Regression case: ?o here is bound to both a string and an integer + # literal across different rows of the SAME pattern -- format_native_ + # columns represents that as a Struct (one field per possible type), which + # must be coalesced down to one string column before concatenating across + # patterns, not cast directly (a direct Struct->String cast silently + # produces NA for every row instead of erroring). + m <- Model$new() + m$reads(paste( + ' "Alice" .', + " 30 .", + sep = "\n" + ), format = "turtle") + + df <- m$query("CONSTRUCT { ?s ?o . ?s ?p ?o } WHERE { ?s ?p ?o }") + expect_equal(nrow(df), 4) + expect_false(any(is.na(df$object))) + copy_rows <- df[df$predicate == "http://ex/copy", ] + expect_setequal(copy_rows$object, c("Alice", "30")) +}) + +test_that("CONSTRUCT flattens a multi-typed *predicate* variable too, not just subject/object", { + # Regression case (roborev review of the CONSTRUCT-support commit): the + # variable-bound-predicate branch of construct_result_to_solution_mappings + # used to cast the predicate column directly, on the assumption that a + # predicate variable is always singly IRI-typed -- true of RDF's predicate + # *position*, but not guaranteed of a *variable*, which can also appear in + # a non-predicate role elsewhere in the WHERE clause (here, via UNION), + # making its RDFNodeState multi just like subject/object can be. + m <- Model$new() + m$reads(' "Alice" .', format = "ntriples") + + df <- m$query(' + CONSTRUCT { ?a ?p ?b } + WHERE { + { ?a ?p ?b } + UNION + { BIND("not-an-iri" AS ?p) BIND( AS ?a) BIND( AS ?b) } + } + ') + expect_equal(nrow(df), 2) + expect_false(any(is.na(df$predicate))) + expect_setequal(df$predicate, c("http://ex/name", "not-an-iri")) +}) + +test_that("CONSTRUCT preserves a language-tagged literal's value (dropping the language tag)", { + m <- Model$new() + m$reads(' "hello"@en .', format = "turtle") + df <- m$query("CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }") + expect_equal(df$object, "hello") +}) + +test_that("CONSTRUCT with multiple template patterns concatenates all of their rows", { + m <- Model$new() + m$reads(paste( + ' "Alice" .', + " .", + sep = "\n" + ), format = "ntriples") + + df <- m$query(" + CONSTRUCT { + ?s ?name . + ?s ?friend . + } WHERE { + ?s ?name . + ?s ?friend . + } + ") + expect_equal(nrow(df), 2) + expect_setequal(df$predicate, c("http://ex/hasName", "http://ex/hasFriend")) +}) + +test_that("a CONSTRUCT query matching nothing returns zero rows, not an error", { + m <- Model$new() + df <- m$query("CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }") + expect_equal(nrow(df), 0) + expect_setequal(names(df), c("subject", "predicate", "object")) +}) diff --git a/maplibr/tests/testthat/test-model-read-write.R b/maplibr/tests/testthat/test-model-read-write.R new file mode 100644 index 00000000..2e76bedd --- /dev/null +++ b/maplibr/tests/testthat/test-model-read-write.R @@ -0,0 +1,74 @@ +# Tests for Model$read()/Model$write() (maplib-5us) -- file-based +# counterparts to $reads()/$writes(). + +test_that("write() then read() round-trips triples through a file, explicit format", { + m <- Model$new() + m$reads(' .', format = "ntriples") + + path <- tempfile(fileext = ".nt") + on.exit(unlink(path)) + m$write(path, format = "ntriples") + + m2 <- Model$new() + m2$read(path, format = "ntriples") + expect_equal(m2$size(), 1) + df <- m2$query("SELECT ?s ?p ?o WHERE { ?s ?p ?o }") + expect_equal(df$s, "http://ex/a") +}) + +test_that("write() defaults to N-Triples format", { + m <- Model$new() + m$reads(' .', format = "ntriples") + + path <- tempfile(fileext = ".nt") + on.exit(unlink(path)) + m$write(path) + expect_match(readLines(path), "http://ex/a", fixed = TRUE, all = FALSE) +}) + +test_that("read() guesses the format from the file extension when format is NULL", { + m <- Model$new() + m$reads(' .', format = "ntriples") + path <- tempfile(fileext = ".nt") + on.exit(unlink(path)) + m$write(path, format = "ntriples") + + m2 <- Model$new() + m2$read(path) + expect_equal(m2$size(), 1) +}) + +test_that("read()/write()'s graph argument scopes to a single named graph", { + m <- Model$new() + m$reads(' .', format = "ntriples", graph = "http://ex/g1") + + path <- tempfile(fileext = ".nt") + on.exit(unlink(path)) + m$write(path, graph = "http://ex/g1") + + m2 <- Model$new() + m2$read(path, graph = "http://ex/g2") + expect_equal(m2$size(graph = "http://ex/g2"), 1) + expect_equal(m2$size(), 0) +}) + +test_that("read() raises a catchable error for a nonexistent file", { + m <- Model$new() + expect_error( + m$read(tempfile(fileext = ".nt")), + class = "maplibr_maplib_error" + ) +}) + +test_that("read() raises a catchable error for an unsupported or missing extension, instead of crashing", { + # Regression case: read()'s format-guessing used to pass format = NULL + # straight through to the core engine's own extension guesser, whose + # unrecognized-extension fallback is a bare todo!() -- a panic, and with + # this crate's release profile set to panic = "abort", that would abort + # the whole R session rather than raise a catchable error. Guessing the + # format in the R wrapper itself (guess_format_from_extension) means an + # unrecognized extension is a normal error well before reaching that code. + m <- Model$new() + expect_error(m$read(tempfile()), class = "maplibr_argument_error") + expect_error(m$read(tempfile(fileext = ".csv")), class = "maplibr_argument_error") +}) diff --git a/maplibr/tests/testthat/test-model-verbs.R b/maplibr/tests/testthat/test-model-verbs.R new file mode 100644 index 00000000..88fa6847 --- /dev/null +++ b/maplibr/tests/testthat/test-model-verbs.R @@ -0,0 +1,120 @@ +# Tests for the model_*() pipe-friendly functional wrappers (R/model-verbs.R). +# Each one is a thin passthrough to the matching Model R6 method, so these +# tests check the passthrough is correct (right args, right return value), +# not the underlying behavior itself (already covered by the R6-method +# tests in the other test-model-*.R files). + +test_that("mutating model_*() verbs return the model invisibly, and it stays chainable via |>", { + m <- Model$new() |> + model_reads(' .', format = "ntriples") |> + model_update("INSERT { ?s \"seen\" } WHERE { ?s ?p ?o }") |> + model_add_prefixes(list(ex = "http://ex/")) |> + model_create_index() + + expect_s3_class(m, "Model") + expect_equal(model_size(m), 2) +}) + +test_that("model_query() returns a data.frame, usable as the end of a pipe", { + m <- Model$new() |> model_reads(' .', format = "ntriples") + df <- m |> model_query("SELECT ?s ?p ?o WHERE { ?s ?p ?o }") + expect_s3_class(df, "data.frame") + expect_equal(df$s, "http://ex/a") +}) + +test_that("model_query()'s bindings argument passes through correctly", { + m <- Model$new() + df <- m |> model_query( + 'SELECT * WHERE { VALUES ?a { "a" "b" } FILTER(?a = ?b) }', + bindings = list(b = Literal("b")) + ) + expect_equal(nrow(df), 1) +}) + +test_that("model_insert() adds a CONSTRUCT query's results", { + m <- Model$new() |> model_reads(' .', format = "ntriples") + m |> model_insert("CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }", target_graph = "http://ex/g2") + expect_equal(model_size(m, graph = "http://ex/g2"), 1) +}) + +test_that("model_read()/model_write() round-trip through a file", { + m <- Model$new() |> model_reads(' .', format = "ntriples") + path <- tempfile(fileext = ".nt") + on.exit(unlink(path)) + m |> model_write(path) + + m2 <- Model$new() |> model_read(path) + expect_equal(model_size(m2), 1) +}) + +test_that("model_add_template() returns the template IRI (not chainable), model_map() expands it", { + ex <- Prefix("http://ex/", "ex") + var <- Variable("V") + template <- Template( + iri = suf(ex, "T"), + parameters = list(Parameter(var)), + instances = list(Triple(suf(ex, "a"), suf(ex, "b"), var)) + ) + m <- Model$new() + iri <- m |> model_add_template(template) + expect_equal(iri, "http://ex/T") + + m |> model_map(template, data.frame(V = "c")) + expect_equal(model_size(m), 1) +}) + +test_that("model_map_df()/model_map_triples()/model_map_json_string()/model_map_xml_string() all delegate correctly", { + m1 <- Model$new() + m1 |> model_map_df(data.frame(name = "Alice")) + expect_gt(model_size(m1), 0) + + m2 <- Model$new() + m2 |> model_map_triples( + data.frame(subject = "http://ex/a", object = "http://ex/b"), + predicate = "http://ex/knows" + ) + expect_equal(model_size(m2), 1) + + m3 <- Model$new() + m3 |> model_map_json_string('{"a": 1}') + expect_gt(model_size(m3), 0) + + m4 <- Model$new() + m4 |> model_map_xml_string("1") + expect_gt(model_size(m4), 0) +}) + +test_that("model_detach_graph() returns a new Model, and model_add_graph() copies into one", { + m <- Model$new() |> model_reads( + ' .', format = "ntriples", graph = "http://ex/g1" + ) + sprout <- m |> model_detach_graph(graph = "http://ex/g1") + expect_s3_class(sprout, "Model") + expect_equal(model_size(sprout), 1) + + m2 <- Model$new() |> model_add_graph(sprout) + expect_equal(model_size(m2), 1) +}) + +test_that("model_infer_rdfs() returns the inferred-triple count", { + m <- Model$new() |> model_reads(paste( + " .", + " .", + sep = "\n" + ), format = "turtle") + n <- m |> model_infer_rdfs() + expect_gt(n, 0) +}) + +test_that("model_writes() returns a string, and model_truncate_graph() empties a graph", { + m <- Model$new() |> model_reads(' .', format = "ntriples") + expect_type(model_writes(m), "character") + m |> model_truncate_graph() + expect_equal(model_size(m), 0) +}) + +test_that("model_deserialize() is a pipe source, not a target -- creates a fresh Model from a path", { + # No `m` argument at all -- unlike every other model_*() verb, it can only + # start a pipe, not receive one: model_deserialize(path) |> model_query(...). + expect_equal(names(formals(model_deserialize)), c("path", "storage_folder")) +}) diff --git a/maplibr/tests/testthat/test-templates.R b/maplibr/tests/testthat/test-templates.R new file mode 100644 index 00000000..da076d9d --- /dev/null +++ b/maplibr/tests/testthat/test-templates.R @@ -0,0 +1,83 @@ +# Tests for templates.R's OTTR classes (maplib-snz continued). Includes one +# direct port from py_maplib/tests/test_basics.py -- everything else there +# needs Model.map()/query(), not implemented in maplibr yet. + +test_that("Template prints identical OTTR syntax to py_maplib", { + # Direct port of test_programmatic_model_to_string + # (py_maplib/tests/test_basics.py:538-558). + ex <- Prefix("http://example.net/ns#", prefix_name = "ex") + example_template <- Template( + iri = suf(ex, "ExampleTemplate"), + parameters = list(), + instances = list( + Triple( + suf(ex, "myObject"), + suf(ex, "hasObj"), + suf(ex, "myOtherObject") + ) + ) + ) + expect_identical( + trimws(format(example_template)), + trimws( + "@prefix ottr: . +@prefix p0: . + +p0:ExampleTemplate [ ] :: { + ottr:Triple(p0:myObject, p0:hasObj, p0:myOtherObject) +} . " + ) + ) +}) + +test_that("Parameter accepts an IRI or an RDFType for rdf_type", { + p1 <- Parameter(Variable("x"), rdf_type = xsd$integer) + expect_true(inherits(p1, "maplibr::Parameter")) + + p2 <- Parameter(Variable("y"), rdf_type = rdf_type_iri()) + expect_true(inherits(p2, "maplibr::Parameter")) + + p3 <- Parameter(Variable("z"), rdf_type = rdf_type_unknown()) + expect_true(inherits(p3, "maplibr::Parameter")) +}) + +test_that("rdf_type_multi() collapses to rdfs:Resource", { + multi <- rdf_type_multi(list(rdf_type_iri(), rdf_type_literal(xsd$string))) + expect_equal(multi@basic_type_iri@iri, "http://www.w3.org/2000/01/rdf-schema#Resource") +}) + +test_that("Template$instantiate() builds an Instance calling the template", { + ex <- Prefix("http://example.org/", "ex") + t <- Template( + iri = suf(ex, "T"), + parameters = list(Parameter(Variable("x"))), + instances = list(Instance(IRI("http://ns.ottr.xyz/0.4/Triple"), list(Variable("x"), suf(ex, "p"), suf(ex, "o")))) + ) + inst <- instantiate(t, list(suf(ex, "a"))) + expect_true(inherits(inst, "maplibr::Instance")) + expect_equal(instance_template_iri(inst)@iri, "http://example.org/T") +}) + +test_that("Argument passthrough: passing an existing Argument returns it unchanged", { + a <- Argument(Variable("x"), list_expand = TRUE) + a2 <- Argument(a) + expect_identical(a, a2) +}) + +test_that("Instance() no longer accepts a validation-bypassing .raw argument", { + # Regression test: .raw used to be a real, exported, documented parameter + # of Instance()'s own public constructor -- a validation-bypassing escape + # hatch, needed internally by instantiate()/Triple() to wrap an + # already-built RInstance, but with no business being publicly callable. + # It's now a private, unexported subclass (.RawInstance) instead. + expect_error(Instance(.raw = "anything")) + expect_false(".raw" %in% names(formals(Instance))) +}) + +test_that("Triple() (via the private RawInstance subclass) builds a valid, checkable Instance", { + ex <- Prefix("http://example.org/", "ex") + inst <- Triple(suf(ex, "s"), suf(ex, "p"), suf(ex, "o")) + expect_true(S7::S7_inherits(inst, Instance)) + expect_true(inherits(inst, "maplibr::Instance")) # base inherits() too, not just S7_inherits() + expect_equal(instance_template_iri(inst)@iri, "http://ns.ottr.xyz/0.4/Triple") +}) diff --git a/maplibr/tests/testthat/test-terms.R b/maplibr/tests/testthat/test-terms.R new file mode 100644 index 00000000..b3fef6f3 --- /dev/null +++ b/maplibr/tests/testthat/test-terms.R @@ -0,0 +1,51 @@ +# Tests for terms.R's S7 value classes (maplib-snz). No pytest equivalent +# to port directly against -- py_maplib's IRI/Prefix/Variable/BlankNode/ +# Literal tests are folded into larger query()/map()-based test files that +# aren't portable yet -- so these are written fresh, exercising the same +# constructors and validation py_maplib's own classes provide. + +test_that("IRI validates syntax and formats", { + i <- IRI("http://example.org/a") + expect_equal(i@iri, "http://example.org/a") + expect_match(format(i), "^IRI\\(") + expect_error(IRI("not an iri")) +}) + +test_that("Prefix builds suffixed IRIs via suf()", { + ex <- Prefix("http://example.org/", "ex") + a <- suf(ex, "a") + expect_true(inherits(a, "maplibr::IRI")) + expect_equal(a@iri, "http://example.org/a") +}) + +test_that("Variable validates syntax and formats", { + v <- Variable("x") + expect_equal(v@name, "x") + expect_equal(format(v), "?x") + expect_error(Variable("bad variable name")) +}) + +test_that("BlankNode validates syntax and formats", { + b <- BlankNode("b1") + expect_equal(b@name, "b1") + expect_equal(format(b), "_:b1") + expect_error(BlankNode("")) +}) + +test_that("Literal defaults to xsd:string, and language beats datatype", { + plain <- Literal("hello") + expect_equal(plain@datatype@iri, "http://www.w3.org/2001/XMLSchema#string") + expect_equal(format(plain), '"hello"') + + typed <- Literal("42", datatype = IRI("http://www.w3.org/2001/XMLSchema#integer")) + expect_equal(format(typed), '"42"^^IRI(http://www.w3.org/2001/XMLSchema#integer)') + + tagged <- Literal("bonjour", language = "fr") + expect_equal(tagged@datatype@iri, "http://www.w3.org/1999/02/22-rdf-syntax-ns#langString") + expect_equal(format(tagged), '"bonjour"@fr') + + # language takes priority over an explicit datatype, matching py_maplib's + # constructor precedence (lib/representation/src/python.rs:368-382) + both <- Literal("x", datatype = IRI("http://example.org/dt"), language = "en") + expect_equal(both@datatype@iri, "http://www.w3.org/1999/02/22-rdf-syntax-ns#langString") +}) diff --git a/maplibr/tests/testthat/test-vocab.R b/maplibr/tests/testthat/test-vocab.R new file mode 100644 index 00000000..4314ccce --- /dev/null +++ b/maplibr/tests/testthat/test-vocab.R @@ -0,0 +1,30 @@ +# Spot-checks for vocab.R's xsd/rdf/rdfs/owl constants (maplib-snz +# continued), cross-checked against oxrdf's actual vocab.rs values when +# they were generated -- this just guards against regressions. + +test_that("vocab lists have the expected size and are built at load time", { + expect_length(xsd, 37) + expect_length(rdf, 17) + expect_length(rdfs, 15) + expect_length(owl, 78) +}) + +test_that("xsd/rdf/rdfs/owl constants resolve to the correct IRIs", { + expect_equal(xsd$dateTime@iri, "http://www.w3.org/2001/XMLSchema#dateTime") + expect_equal(xsd$anyURI@iri, "http://www.w3.org/2001/XMLSchema#anyURI") + expect_equal(xsd$int_@iri, "http://www.w3.org/2001/XMLSchema#int") + expect_equal(rdf$type@iri, "http://www.w3.org/1999/02/22-rdf-syntax-ns#type") + expect_equal(rdf$langString@iri, "http://www.w3.org/1999/02/22-rdf-syntax-ns#langString") + expect_equal(rdfs$subClassOf@iri, "http://www.w3.org/2000/01/rdf-schema#subClassOf") + expect_equal(rdfs$Class@iri, "http://www.w3.org/2000/01/rdf-schema#Class") + expect_equal(owl$Class@iri, "http://www.w3.org/2002/07/owl#Class") + expect_equal(owl$sameAs@iri, "http://www.w3.org/2002/07/owl#sameAs") +}) + +test_that("vocab IRIs interoperate with Literal/Parameter", { + lit <- Literal("42", datatype = xsd$integer) + expect_equal(format(lit), '"42"^^IRI(http://www.w3.org/2001/XMLSchema#integer)') + + p <- Parameter(Variable("x"), rdf_type = xsd$string) + expect_true(inherits(p, "maplibr::Parameter")) +}) diff --git a/maplibr/tests/testthat/testdata/rdfs/rdfs10/expected.ttl b/maplibr/tests/testthat/testdata/rdfs/rdfs10/expected.ttl new file mode 100644 index 00000000..2646e956 --- /dev/null +++ b/maplibr/tests/testthat/testdata/rdfs/rdfs10/expected.ttl @@ -0,0 +1,6 @@ +@prefix : . +@prefix rdfs: . + +:AstronomicalBody a rdfs:Class ; + rdfs:subClassOf :AstronomicalBody , rdfs:Resource . + diff --git a/maplibr/tests/testthat/testdata/rdfs/rdfs10/input.ttl b/maplibr/tests/testthat/testdata/rdfs/rdfs10/input.ttl new file mode 100644 index 00000000..f15fe34c --- /dev/null +++ b/maplibr/tests/testthat/testdata/rdfs/rdfs10/input.ttl @@ -0,0 +1,5 @@ +@prefix : . +@prefix rdf: . +@prefix rdfs: . + +:AstronomicalBody a rdfs:Class . diff --git a/maplibr/tests/testthat/testdata/rdfs/rdfs11/expected.ttl b/maplibr/tests/testthat/testdata/rdfs/rdfs11/expected.ttl new file mode 100644 index 00000000..1992748d --- /dev/null +++ b/maplibr/tests/testthat/testdata/rdfs/rdfs11/expected.ttl @@ -0,0 +1,7 @@ +@prefix : . +@prefix rdfs: . + +:Planet rdfs:subClassOf :AstronomicalBody, + :PlanetaryBody. + +:PlanetaryBody rdfs:subClassOf :AstronomicalBody . \ No newline at end of file diff --git a/maplibr/tests/testthat/testdata/rdfs/rdfs11/input.ttl b/maplibr/tests/testthat/testdata/rdfs/rdfs11/input.ttl new file mode 100644 index 00000000..6ece9e82 --- /dev/null +++ b/maplibr/tests/testthat/testdata/rdfs/rdfs11/input.ttl @@ -0,0 +1,7 @@ +@prefix : . +@prefix rdf: . +@prefix rdfs: . + +:PlanetaryBody rdfs:subClassOf :AstronomicalBody . +:Planet rdfs:subClassOf :PlanetaryBody . + diff --git a/maplibr/tests/testthat/testdata/rdfs/rdfs2/expected.ttl b/maplibr/tests/testthat/testdata/rdfs/rdfs2/expected.ttl new file mode 100644 index 00000000..6bf706fc --- /dev/null +++ b/maplibr/tests/testthat/testdata/rdfs/rdfs2/expected.ttl @@ -0,0 +1,8 @@ +@prefix : . +@prefix rdf: . +@prefix rdfs: . + +:moon rdfs:domain :Planet . +:Mars :moon :Phobos . + +:Mars rdf:type :Planet . diff --git a/maplibr/tests/testthat/testdata/rdfs/rdfs2/input.ttl b/maplibr/tests/testthat/testdata/rdfs/rdfs2/input.ttl new file mode 100644 index 00000000..4311d4f9 --- /dev/null +++ b/maplibr/tests/testthat/testdata/rdfs/rdfs2/input.ttl @@ -0,0 +1,6 @@ +@prefix : . +@prefix rdf: . +@prefix rdfs: . + +:moon rdfs:domain :Planet . +:Mars :moon :Phobos . diff --git a/maplibr/tests/testthat/testdata/rdfs/rdfs3/expected.ttl b/maplibr/tests/testthat/testdata/rdfs/rdfs3/expected.ttl new file mode 100644 index 00000000..1067ec9c --- /dev/null +++ b/maplibr/tests/testthat/testdata/rdfs/rdfs3/expected.ttl @@ -0,0 +1,8 @@ +@prefix : . +@prefix rdf: . +@prefix rdfs: . + +:moon rdfs:range :Satellite . +:Mars :moon :Phobos . + +:Phobos rdf:type :Satellite . \ No newline at end of file diff --git a/maplibr/tests/testthat/testdata/rdfs/rdfs3/input.ttl b/maplibr/tests/testthat/testdata/rdfs/rdfs3/input.ttl new file mode 100644 index 00000000..f0eaf817 --- /dev/null +++ b/maplibr/tests/testthat/testdata/rdfs/rdfs3/input.ttl @@ -0,0 +1,6 @@ +@prefix : . +@prefix rdf: . +@prefix rdfs: . + +:moon rdfs:range :Satellite . +:Mars :moon :Phobos . diff --git a/maplibr/tests/testthat/testdata/rdfs/rdfs5/expected.ttl b/maplibr/tests/testthat/testdata/rdfs/rdfs5/expected.ttl new file mode 100644 index 00000000..5916245e --- /dev/null +++ b/maplibr/tests/testthat/testdata/rdfs/rdfs5/expected.ttl @@ -0,0 +1,16 @@ +@prefix : . +@prefix rdf: . +@prefix rdfs: . + +:siderealOrbitalPeriod a rdf:Property ; + rdfs:label "sidereal orbital period"@en ; + rdfs:subPropertyOf :orbitalPeriod, + :orbitalProperty, :siderealOrbitalPeriod . + +:orbitalPeriod a rdf:Property ; + rdfs:label "orbital period"@en ; + rdfs:subPropertyOf :orbitalProperty, :orbitalPeriod . + +:orbitalProperty a rdf:Property ; + rdfs:label "orbital property"@en ; + rdfs:subPropertyOf :orbitalProperty . \ No newline at end of file diff --git a/maplibr/tests/testthat/testdata/rdfs/rdfs5/input.ttl b/maplibr/tests/testthat/testdata/rdfs/rdfs5/input.ttl new file mode 100644 index 00000000..6a4348d3 --- /dev/null +++ b/maplibr/tests/testthat/testdata/rdfs/rdfs5/input.ttl @@ -0,0 +1,14 @@ +@prefix : . +@prefix rdf: . +@prefix rdfs: . + +:orbitalProperty a rdf:Property ; + rdfs:label "orbital property"@en . + +:orbitalPeriod a rdf:Property ; + rdfs:label "orbital period"@en ; + rdfs:subPropertyOf :orbitalProperty . + +:siderealOrbitalPeriod a rdf:Property ; + rdfs:label "sidereal orbital period"@en ; + rdfs:subPropertyOf :orbitalPeriod . diff --git a/maplibr/tests/testthat/testdata/rdfs/rdfs6/expected.ttl b/maplibr/tests/testthat/testdata/rdfs/rdfs6/expected.ttl new file mode 100644 index 00000000..8afdb899 --- /dev/null +++ b/maplibr/tests/testthat/testdata/rdfs/rdfs6/expected.ttl @@ -0,0 +1,7 @@ +@prefix : . +@prefix rdf: . +@prefix rdfs: . + +:orbitalProperty a rdf:Property ; + rdfs:label "orbital property"@en ; + rdfs:subPropertyOf :orbitalProperty . \ No newline at end of file diff --git a/maplibr/tests/testthat/testdata/rdfs/rdfs6/input.ttl b/maplibr/tests/testthat/testdata/rdfs/rdfs6/input.ttl new file mode 100644 index 00000000..813a27c3 --- /dev/null +++ b/maplibr/tests/testthat/testdata/rdfs/rdfs6/input.ttl @@ -0,0 +1,6 @@ +@prefix : . +@prefix rdf: . +@prefix rdfs: . + +:orbitalProperty a rdf:Property ; + rdfs:label "orbital property"@en . \ No newline at end of file diff --git a/maplibr/tests/testthat/testdata/rdfs/rdfs8/expected.ttl b/maplibr/tests/testthat/testdata/rdfs/rdfs8/expected.ttl new file mode 100644 index 00000000..a43cf120 --- /dev/null +++ b/maplibr/tests/testthat/testdata/rdfs/rdfs8/expected.ttl @@ -0,0 +1,5 @@ +@prefix : . +@prefix rdfs: . + +:Planet a rdfs:Class ; + rdfs:subClassOf :Planet, rdfs:Resource . \ No newline at end of file diff --git a/maplibr/tests/testthat/testdata/rdfs/rdfs8/input.ttl b/maplibr/tests/testthat/testdata/rdfs/rdfs8/input.ttl new file mode 100644 index 00000000..72cdcd0b --- /dev/null +++ b/maplibr/tests/testthat/testdata/rdfs/rdfs8/input.ttl @@ -0,0 +1,5 @@ +@prefix : . +@prefix rdf: . +@prefix rdfs: . + +:Planet a rdfs:Class . \ No newline at end of file diff --git a/maplibr/tests/testthat/testdata/rdfs/rdfs9/expected.ttl b/maplibr/tests/testthat/testdata/rdfs/rdfs9/expected.ttl new file mode 100644 index 00000000..d5c9c1d4 --- /dev/null +++ b/maplibr/tests/testthat/testdata/rdfs/rdfs9/expected.ttl @@ -0,0 +1,12 @@ +@prefix : . +@prefix rdfs: . + +:AstronomicalBody a rdfs:Class ; + rdfs:subClassOf :AstronomicalBody, rdfs:Resource . + +:Mars a :AstronomicalBody, + :Planet, + rdfs:Resource . + +:Planet rdfs:subClassOf :AstronomicalBody , rdfs:Resource . + diff --git a/maplibr/tests/testthat/testdata/rdfs/rdfs9/input.ttl b/maplibr/tests/testthat/testdata/rdfs/rdfs9/input.ttl new file mode 100644 index 00000000..42cbea2e --- /dev/null +++ b/maplibr/tests/testthat/testdata/rdfs/rdfs9/input.ttl @@ -0,0 +1,7 @@ +@prefix : . +@prefix rdf: . +@prefix rdfs: . + +:AstronomicalBody a rdfs:Class . +:Planet rdfs:subClassOf :AstronomicalBody . +:Mars a :Planet . diff --git a/maplibr/vignettes/maplibr.Rmd b/maplibr/vignettes/maplibr.Rmd new file mode 100644 index 00000000..5e9329d3 --- /dev/null +++ b/maplibr/vignettes/maplibr.Rmd @@ -0,0 +1,279 @@ +--- +title: "Getting Started with maplibr" +output: rmarkdown::html_vignette +vignette: > + %\VignetteIndexEntry{Getting Started with maplibr} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +--- + +```{r, include = FALSE} +knitr::opts_chunk$set(collapse = TRUE, comment = "#>") +``` + +`maplibr` is an R interface to `maplib`, a Rust RDF knowledge-graph engine +(the same engine the Python package `py_maplib` wraps). This vignette walks +through what's implemented today: building a graph, querying and updating it +with SPARQL, mapping data into RDF (both with OTTR templates and directly), +RDFS inference, and managing named graphs. + +`Model` is an R6 class, so every method is also available as `m$method(...)` +-- but this vignette uses the pipe-friendly `model_*()` functions throughout +instead (`m |> model_query(...)` rather than `m$query(...)`), since both are +fully equivalent and interchangeable, and the pipe form reads better across +a multi-step pipeline. `model_*()` uses that prefix rather than bare +`read()`/`write()`/`map()`/`update()` deliberately: those names would mask +`base::write`, `purrr::map`, and the `stats::update` S3 generic if this +package were attached alongside a normal tidyverse session. + +```{r setup} +library(maplibr) +``` + +## Creating a Model and reading triples + +`Model` is the main entry point -- an in-memory RDF knowledge graph. +`model_reads()` mutates it in place and returns it invisibly, so it chains +straight off `Model$new()`: + +```{r create-and-read} +m <- Model$new() |> + model_reads(' + . + . + "Alice" . + ', format = "ntriples") + +model_size(m) +``` + +## Serializing back out + +```{r writes} +m |> model_writes() |> cat() +``` + +Turtle works too: + +```{r writes-turtle} +m |> model_writes(format = "turtle") |> cat() +``` + +`model_read()`/`model_write()` are the file-based counterparts to +`model_reads()`/`model_writes()` -- `model_read()` guesses the format from +the file extension (`.ttl`/`.nt`/`.xml`/`.rdf`) when `format` isn't given: + +```{r read-write-file} +path <- tempfile(fileext = ".ttl") +m |> model_write(path, format = "turtle") + +m_from_file <- Model$new() |> model_read(path) +model_size(m_from_file) +unlink(path) +``` + +## Querying with SPARQL + +`model_query()` runs a SPARQL query and returns the result as a data.frame. + +```{r query-select} +m |> model_query("SELECT ?s ?o WHERE { ?s ?o }") +``` + +CONSTRUCT queries are supported too, returned as a combined +subject/predicate/object data.frame: + +```{r query-construct} +m |> model_query(" + CONSTRUCT { ?s ?type } + WHERE { ?s a ?type } +") +``` + +`bindings` pre-binds SPARQL variables to fixed `IRI`/`Literal` values before +the query runs -- useful inside a `FILTER`, though not as a `SELECT`-ed +column itself: + +```{r query-bindings} +m |> model_query( + 'SELECT * WHERE { VALUES ?name { "Alice" "Bob" } FILTER(?name = ?who) }', + bindings = list(who = Literal("Bob")) +) +``` + +## Updating data with SPARQL UPDATE and INSERT + +`model_update()` runs a SPARQL UPDATE (a `DELETE`/`INSERT` ... `WHERE` +modify statement) against the Model in place. The bare `INSERT DATA { ... }` +form isn't supported yet by the underlying engine -- use an empty `WHERE {}` +instead: + +```{r update} +m |> model_update(' + PREFIX ex: + INSERT { ex:alice ex:greeting "Hello" } WHERE {} +') +m |> model_query("SELECT ?o WHERE { ?o }") +``` + +`model_insert()` runs a SPARQL `CONSTRUCT` query and adds its results as new +triples, optionally into a different graph than it queried: + +```{r insert} +m |> model_insert( + "CONSTRUCT { ?s ?type } WHERE { ?s a ?type }", + target_graph = "urn:graph:types" +) +model_size(m, "urn:graph:types") +``` + +## Mapping tabular data into RDF with OTTR templates + +`model_map()` expands an OTTR template against a data.frame, adding the +resulting triples to the Model. Templates can be built programmatically... + +```{r map-programmatic} +ex <- Prefix("http://example.org/", "ex") +my_value <- Variable("MyValue") +template <- Template( + iri = suf(ex, "HasNickname"), + parameters = list(Parameter(my_value)), + instances = list(Triple(suf(ex, "alice"), suf(ex, "nickname"), my_value)) +) + +m |> model_map(template, data.frame(MyValue = c("Ally", "Al"))) +m |> model_query("SELECT ?o WHERE { ?o }") +``` + +...or from an stOTTR document string, matching `py_maplib`'s own syntax (see +the root project's README for the OTTR template language itself). Since +`model_add_template()` returns the registered template's IRI rather than +`m`, it's not itself chained onward -- pipe into it, but capture its result +separately from the `m` you keep mutating: + +```{r map-doc-string} +doc <- ' +@prefix ex:. +ex:HasFriend [?Friend] :: { + ottr:Triple(ex:alice, ex:knows, ?Friend) +} . +' +m |> model_add_template(doc) +m |> model_map("http://example.org/HasFriend", data.frame(Friend = "http://example.org/bob")) +m |> model_query("SELECT ?o WHERE { ?o }") +``` + +## Mapping data directly to triples, without a template + +For data that doesn't need OTTR's templating power, several `model_map_*()` +functions add triples directly instead. `model_map_df()` turns a +data.frame's columns into predicates, one row per subject: + +```{r map-df} +readings <- data.frame(city = c("Oslo", "Bergen"), temp_c = c(14, 16)) +m_direct <- Model$new() |> model_map_df(readings) +m_direct |> model_query("SELECT ?p ?o WHERE { ?s ?p ?o }") +``` + +`model_map_triples()` maps subject/predicate/object columns directly (or +just subject/object, with a constant `predicate`): + +```{r map-triples} +m_triples <- Model$new() |> model_map_triples( + data.frame(subject = "http://example.org/alice", object = "http://example.org/bob"), + predicate = "http://example.org/knows" +) +m_triples |> model_query("SELECT ?s ?p ?o WHERE { ?s ?p ?o }") +``` + +`model_map_json_string()`/`model_map_xml_string()` (and their file-based +`model_map_json()`/`model_map_xml()` counterparts) apply a fixed convention +(Facade-X) to a whole JSON or XML document instead: + +```{r map-json} +m_json <- Model$new() |> model_map_json_string('{"name": "Alice", "age": 30}') +m_json |> model_query("SELECT ?p ?o WHERE { ?s ?p ?o }") +``` + +## RDFS inference + +`alice` is a `Person`, and `Person` is a subclass of `Agent` -- RDFS +inference should let us derive that `alice` is also an `Agent`. +`model_infer_rdfs()` returns the number of newly-inferred triples, not `m`, +so this one's called directly rather than piped further: + +```{r infer} +n <- m |> model_infer_rdfs() +n +``` + +Inferred triples are *transient* -- kept separately from the main graph, and +only visible to `model_query()` when asked for explicitly (`model_writes()` +doesn't currently support this at all, a core-engine limitation, not an +R-wrapper one -- see the "Upstream issues" section of the package's +development notes): + +```{r infer-transient} +without <- m |> model_query("SELECT * WHERE { ?s ?p ?o }") +with_transient <- m |> model_query("SELECT * WHERE { ?s ?p ?o }", include_transient = TRUE) +nrow(without) +nrow(with_transient) +``` + +## Named graphs + +Triples can be routed into named graphs, copied between Models, and split +back out. + +```{r graphs} +m |> model_reads( + ' "Bob" .', + format = "ntriples", + graph = "urn:graph:extra" +) +model_size(m, "urn:graph:extra") +model_size(m) # default graph, unaffected + +m2 <- Model$new() |> model_add_graph(m) +model_size(m2) + +sprout <- m |> model_detach_graph(graph = "urn:graph:extra") +model_size(sprout) +model_size(m, "urn:graph:extra") # emptied by the detach +``` + +## Prefixes and indexing + +Both mutate in place, so a two-step setup pipeline chains naturally: + +```{r prefixes-index} +m |> + model_add_prefixes(list(ex = "http://example.org/")) |> + model_create_index() +``` + +## Visualizing a graph + +`maplibr` has no graph-visualization function of its own, but a query result +is a plain data.frame, so any R graph package works directly on it. + +```{r graph-plot, fig.width=6, fig.height=4.5} +if (requireNamespace("igraph", quietly = TRUE)) { + triples <- m |> model_query("SELECT ?s ?p ?o WHERE { ?s ?p ?o }") + short_label <- function(iri) sub(".*[/#]", "", iri) + + edges <- data.frame(from = short_label(triples$s), to = short_label(triples$o)) + g <- igraph::graph_from_data_frame(edges, directed = TRUE) + igraph::E(g)$label <- short_label(triples$p) + + plot( + g, + edge.label = igraph::E(g)$label, + vertex.size = 32, + vertex.color = "lightblue", + vertex.label.cex = 0.9, + edge.arrow.size = 0.5, + layout = igraph::layout_with_fr(g) + ) +} +```