Compare commits
11 Commits
Author | SHA1 | Date |
---|---|---|
Tpt | ecdc106fd5 | 1 year ago |
Tpt | ea1ec3ede0 | 1 year ago |
Tpt | 0f7d116900 | 1 year ago |
Tpt | 99801ca34a | 1 year ago |
Tpt | 701328f444 | 1 year ago |
Tpt | d2d98e09e8 | 1 year ago |
Tpt | ee90363898 | 1 year ago |
Tpt | 4f3f99b382 | 1 year ago |
Tpt | 8f94baedf6 | 1 year ago |
Tpt | 1a4adfc02a | 1 year ago |
Tpt | 2bd5da4a5a | 1 year ago |
@ -0,0 +1,120 @@ |
||||
#![cfg_attr(not(feature = "rules"), allow(dead_code))] |
||||
use crate::parser::{parse_rule_set, ParseError}; |
||||
use crate::term::*; |
||||
use std::fmt; |
||||
use std::str::FromStr; |
||||
|
||||
/// A parsed if/then rule set.
|
||||
#[derive(Eq, PartialEq, Debug, Clone, Hash, Default)] |
||||
pub struct RuleSet { |
||||
pub rules: Vec<Rule>, |
||||
} |
||||
|
||||
impl RuleSet { |
||||
/// Parses a set of rules with an optional base IRI to resolve relative IRIs in the rules.
|
||||
/// Note that this base IRI will not be used during execution.
|
||||
pub fn parse(rules: &str, base_iri: Option<&str>) -> Result<Self, ParseError> { |
||||
parse_rule_set(rules, base_iri) |
||||
} |
||||
|
||||
/// Formats using the [SPARQL S-Expression syntax](https://jena.apache.org/documentation/notes/sse.html).
|
||||
pub fn to_sse(&self) -> String { |
||||
let mut buffer = String::new(); |
||||
self.fmt_sse(&mut buffer).unwrap(); |
||||
buffer |
||||
} |
||||
|
||||
/// Formats using the [SPARQL S-Expression syntax](https://jena.apache.org/documentation/notes/sse.html).
|
||||
fn fmt_sse(&self, f: &mut impl fmt::Write) -> fmt::Result { |
||||
write!(f, "(")?; |
||||
for (i, r) in self.rules.iter().enumerate() { |
||||
if i > 0 { |
||||
write!(f, " ")?; |
||||
} |
||||
r.fmt_sse(f)?; |
||||
} |
||||
write!(f, ") ") |
||||
} |
||||
} |
||||
|
||||
impl fmt::Display for RuleSet { |
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
||||
for r in &self.rules { |
||||
writeln!(f, "{r} ;")?; |
||||
} |
||||
Ok(()) |
||||
} |
||||
} |
||||
|
||||
impl FromStr for RuleSet { |
||||
type Err = ParseError; |
||||
|
||||
fn from_str(rules: &str) -> Result<Self, ParseError> { |
||||
Self::parse(rules, None) |
||||
} |
||||
} |
||||
|
||||
impl<'a> TryFrom<&'a str> for RuleSet { |
||||
type Error = ParseError; |
||||
|
||||
fn try_from(rules: &str) -> Result<Self, ParseError> { |
||||
Self::from_str(rules) |
||||
} |
||||
} |
||||
|
||||
impl<'a> TryFrom<&'a String> for RuleSet { |
||||
type Error = ParseError; |
||||
|
||||
fn try_from(rules: &String) -> Result<Self, ParseError> { |
||||
Self::from_str(rules) |
||||
} |
||||
} |
||||
|
||||
/// A parsed if/then rule.
|
||||
#[derive(Eq, PartialEq, Debug, Clone, Hash)] |
||||
pub struct Rule { |
||||
/// The construction template.
|
||||
pub head: Vec<GroundTriplePattern>, |
||||
/// The rule body graph pattern.
|
||||
pub body: Vec<TriplePattern>, |
||||
} |
||||
|
||||
impl Rule { |
||||
/// Formats using the [SPARQL S-Expression syntax](https://jena.apache.org/documentation/notes/sse.html).
|
||||
pub fn to_sse(&self) -> String { |
||||
let mut buffer = String::new(); |
||||
self.fmt_sse(&mut buffer).unwrap(); |
||||
buffer |
||||
} |
||||
|
||||
/// Formats using the [SPARQL S-Expression syntax](https://jena.apache.org/documentation/notes/sse.html).
|
||||
fn fmt_sse(&self, f: &mut impl fmt::Write) -> fmt::Result { |
||||
write!(f, "(rule (")?; |
||||
for (i, t) in self.head.iter().enumerate() { |
||||
if i > 0 { |
||||
write!(f, " ")?; |
||||
} |
||||
t.fmt_sse(f)?; |
||||
} |
||||
write!(f, ") (bgp")?; |
||||
for pattern in &self.body { |
||||
write!(f, " ")?; |
||||
pattern.fmt_sse(f)?; |
||||
} |
||||
write!(f, "))") |
||||
} |
||||
} |
||||
|
||||
impl fmt::Display for Rule { |
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
||||
write!(f, "IF {{ ")?; |
||||
for triple in &self.body { |
||||
write!(f, "{triple} . ")?; |
||||
} |
||||
write!(f, "}} THEN {{ ")?; |
||||
for triple in &self.head { |
||||
write!(f, "{triple} . ")?; |
||||
} |
||||
write!(f, "}}") |
||||
} |
||||
} |
@ -0,0 +1,30 @@ |
||||
[package] |
||||
name = "sparopt" |
||||
version = "0.1.0-alpha.1" |
||||
authors = ["Tpt <thomas@pellissier-tanon.fr>"] |
||||
license = "MIT OR Apache-2.0" |
||||
readme = "README.md" |
||||
keywords = ["SPARQL"] |
||||
repository = "https://github.com/oxigraph/oxigraph/tree/main/lib/sparopt" |
||||
homepage = "https://oxigraph.org/" |
||||
description = """ |
||||
A SPARQL optimizer |
||||
""" |
||||
edition = "2021" |
||||
rust-version = "1.60" |
||||
|
||||
[features] |
||||
default = [] |
||||
fixed-point = [] |
||||
rdf-star = ["oxrdf/rdf-star", "spargebra/rdf-star"] |
||||
sep-0002 = ["spargebra/sep-0002"] |
||||
sep-0006 = ["spargebra/sep-0006"] |
||||
rules = ["spargebra/rules", "fixed-point"] |
||||
|
||||
[dependencies] |
||||
oxrdf = { version = "0.1.5", path="../oxrdf" } |
||||
rand = "0.8" |
||||
spargebra = { version = "0.2.8-dev", path="../spargebra" } |
||||
|
||||
[package.metadata.docs.rs] |
||||
all-features = true |
@ -0,0 +1,32 @@ |
||||
sparopt |
||||
========= |
||||
|
||||
[![Latest Version](https://img.shields.io/crates/v/sparopt.svg)](https://crates.io/crates/sparopt) |
||||
[![Released API docs](https://docs.rs/sparopt/badge.svg)](https://docs.rs/sparopt) |
||||
[![Crates.io downloads](https://img.shields.io/crates/d/sparopt)](https://crates.io/crates/sparopt) |
||||
[![actions status](https://github.com/oxigraph/oxigraph/workflows/build/badge.svg)](https://github.com/oxigraph/oxigraph/actions) |
||||
[![Gitter](https://badges.gitter.im/oxigraph/community.svg)](https://gitter.im/oxigraph/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) |
||||
|
||||
sparopt is a work in progress [SPARQL Query](https://www.w3.org/TR/sparql11-query/) optimizer. |
||||
|
||||
It relies on the output of [spargebra](https://crates.io/crates/spargebra). |
||||
|
||||
Support for [SPARQL-star](https://w3c.github.io/rdf-star/cg-spec/#sparql-star) is also available behind the `rdf-star` feature. |
||||
|
||||
This crate is intended to be a building piece for SPARQL implementations in Rust like [Oxigraph](https://oxigraph.org). |
||||
|
||||
## License |
||||
|
||||
This project is licensed under either of |
||||
|
||||
* Apache License, Version 2.0, ([LICENSE-APACHE](../LICENSE-APACHE) or |
||||
`<http://www.apache.org/licenses/LICENSE-2.0>`) |
||||
* MIT license ([LICENSE-MIT](../LICENSE-MIT) or |
||||
`<http://opensource.org/licenses/MIT>`) |
||||
|
||||
at your option. |
||||
|
||||
|
||||
### Contribution |
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in Oxigraph by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,8 @@ |
||||
pub use crate::optimizer::Optimizer; |
||||
#[cfg(feature = "rules")] |
||||
pub use crate::reasoning::QueryRewriter; |
||||
|
||||
pub mod algebra; |
||||
mod optimizer; |
||||
#[cfg(feature = "rules")] |
||||
mod reasoning; |
@ -0,0 +1,195 @@ |
||||
use crate::algebra::{Expression, FixedPointGraphPattern, GraphPattern}; |
||||
|
||||
#[derive(Default)] |
||||
pub struct Optimizer {} |
||||
|
||||
impl Optimizer { |
||||
pub fn optimize(&mut self, pattern: GraphPattern) -> GraphPattern { |
||||
Self::normalize_pattern(pattern) |
||||
} |
||||
|
||||
fn normalize_pattern(pattern: GraphPattern) -> GraphPattern { |
||||
match pattern { |
||||
GraphPattern::QuadPattern { |
||||
subject, |
||||
predicate, |
||||
object, |
||||
graph_name, |
||||
} => GraphPattern::QuadPattern { |
||||
subject, |
||||
predicate, |
||||
object, |
||||
graph_name, |
||||
}, |
||||
GraphPattern::Path { |
||||
subject, |
||||
path, |
||||
object, |
||||
graph_name, |
||||
} => GraphPattern::Path { |
||||
subject, |
||||
path, |
||||
object, |
||||
graph_name, |
||||
}, |
||||
GraphPattern::Join { left, right } => GraphPattern::join( |
||||
Self::normalize_pattern(*left), |
||||
Self::normalize_pattern(*right), |
||||
), |
||||
GraphPattern::LeftJoin { |
||||
left, |
||||
right, |
||||
expression, |
||||
} => GraphPattern::left_join( |
||||
Self::normalize_pattern(*left), |
||||
Self::normalize_pattern(*right), |
||||
Self::normalize_expression(expression), |
||||
), |
||||
#[cfg(feature = "sep-0006")] |
||||
GraphPattern::Lateral { left, right } => GraphPattern::lateral( |
||||
Self::normalize_pattern(*left), |
||||
Self::normalize_pattern(*right), |
||||
), |
||||
GraphPattern::Filter { inner, expression } => GraphPattern::filter( |
||||
Self::normalize_pattern(*inner), |
||||
Self::normalize_expression(expression), |
||||
), |
||||
GraphPattern::Union { inner } => inner |
||||
.into_iter() |
||||
.map(Self::normalize_pattern) |
||||
.reduce(GraphPattern::union) |
||||
.unwrap_or_else(GraphPattern::empty), |
||||
GraphPattern::Extend { |
||||
inner, |
||||
variable, |
||||
expression, |
||||
} => GraphPattern::extend(Self::normalize_pattern(*inner), variable, expression), |
||||
GraphPattern::Minus { left, right } => GraphPattern::minus( |
||||
Self::normalize_pattern(*left), |
||||
Self::normalize_pattern(*right), |
||||
), |
||||
GraphPattern::Values { |
||||
variables, |
||||
bindings, |
||||
} => GraphPattern::values(variables, bindings), |
||||
GraphPattern::OrderBy { inner, expression } => { |
||||
GraphPattern::order_by(Self::normalize_pattern(*inner), expression) |
||||
} |
||||
GraphPattern::Project { inner, variables } => { |
||||
GraphPattern::project(Self::normalize_pattern(*inner), variables) |
||||
} |
||||
GraphPattern::Distinct { inner } => { |
||||
GraphPattern::distinct(Self::normalize_pattern(*inner)) |
||||
} |
||||
GraphPattern::Reduced { inner } => { |
||||
GraphPattern::reduced(Self::normalize_pattern(*inner)) |
||||
} |
||||
GraphPattern::Slice { |
||||
inner, |
||||
start, |
||||
length, |
||||
} => GraphPattern::slice(Self::normalize_pattern(*inner), start, length), |
||||
GraphPattern::Group { |
||||
inner, |
||||
variables, |
||||
aggregates, |
||||
} => GraphPattern::group(Self::normalize_pattern(*inner), variables, aggregates), |
||||
GraphPattern::Service { |
||||
name, |
||||
inner, |
||||
silent, |
||||
} => GraphPattern::service(Self::normalize_pattern(*inner), name, silent), |
||||
#[cfg(feature = "fixed-point")] |
||||
GraphPattern::FixedPoint { |
||||
id, |
||||
variables, |
||||
constant, |
||||
recursive, |
||||
} => { |
||||
GraphPattern::fixed_point( |
||||
id, |
||||
FixedPointGraphPattern::union(*constant, *recursive), |
||||
variables, |
||||
) |
||||
//TODO: recursive normalization
|
||||
} |
||||
} |
||||
} |
||||
|
||||
fn normalize_expression(expression: Expression) -> Expression { |
||||
match expression { |
||||
Expression::NamedNode(node) => node.into(), |
||||
Expression::Literal(literal) => literal.into(), |
||||
Expression::Variable(variable) => variable.into(), |
||||
Expression::Or(left, right) => Expression::or( |
||||
Self::normalize_expression(*left), |
||||
Self::normalize_expression(*right), |
||||
), |
||||
Expression::And(left, right) => Expression::and( |
||||
Self::normalize_expression(*left), |
||||
Self::normalize_expression(*right), |
||||
), |
||||
Expression::Equal(left, right) => Expression::equal( |
||||
Self::normalize_expression(*left), |
||||
Self::normalize_expression(*right), |
||||
), |
||||
Expression::SameTerm(left, right) => Expression::same_term( |
||||
Self::normalize_expression(*left), |
||||
Self::normalize_expression(*right), |
||||
), |
||||
Expression::Greater(left, right) => Expression::greater( |
||||
Self::normalize_expression(*left), |
||||
Self::normalize_expression(*right), |
||||
), |
||||
Expression::GreaterOrEqual(left, right) => Expression::greater_or_equal( |
||||
Self::normalize_expression(*left), |
||||
Self::normalize_expression(*right), |
||||
), |
||||
Expression::Less(left, right) => Expression::less( |
||||
Self::normalize_expression(*left), |
||||
Self::normalize_expression(*right), |
||||
), |
||||
Expression::LessOrEqual(left, right) => Expression::less_or_equal( |
||||
Self::normalize_expression(*left), |
||||
Self::normalize_expression(*right), |
||||
), |
||||
Expression::Add(left, right) => Expression::add( |
||||
Self::normalize_expression(*left), |
||||
Self::normalize_expression(*right), |
||||
), |
||||
Expression::Subtract(left, right) => Expression::subtract( |
||||
Self::normalize_expression(*left), |
||||
Self::normalize_expression(*right), |
||||
), |
||||
Expression::Multiply(left, right) => Expression::multiply( |
||||
Self::normalize_expression(*left), |
||||
Self::normalize_expression(*right), |
||||
), |
||||
Expression::Divide(left, right) => Expression::divide( |
||||
Self::normalize_expression(*left), |
||||
Self::normalize_expression(*right), |
||||
), |
||||
Expression::UnaryPlus(inner) => { |
||||
Expression::unary_plus(Self::normalize_expression(*inner)) |
||||
} |
||||
Expression::UnaryMinus(inner) => { |
||||
Expression::unary_minus(Self::normalize_expression(*inner)) |
||||
} |
||||
Expression::Not(inner) => Expression::not(Self::normalize_expression(*inner)), |
||||
Expression::Exists(inner) => Expression::exists(Self::normalize_pattern(*inner)), |
||||
Expression::Bound(variable) => Expression::Bound(variable), |
||||
Expression::If(cond, then, els) => Expression::if_cond( |
||||
Self::normalize_expression(*cond), |
||||
Self::normalize_expression(*then), |
||||
Self::normalize_expression(*els), |
||||
), |
||||
Expression::Coalesce(inners) => { |
||||
Expression::coalesce(inners.into_iter().map(Self::normalize_expression).collect()) |
||||
} |
||||
Expression::FunctionCall(name, args) => Expression::call( |
||||
name, |
||||
args.into_iter().map(Self::normalize_expression).collect(), |
||||
), |
||||
} |
||||
} |
||||
} |
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,48 @@ |
||||
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> . |
||||
@prefix : <https://github.com/oxigraph/oxigraph/tests/sparql/manifest#> . |
||||
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> . |
||||
@prefix mf: <http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#> . |
||||
@prefix qt: <http://www.w3.org/2001/sw/DataAccess/tests/test-query#> . |
||||
@prefix ox: <https://github.com/oxigraph/oxigraph/tests#> . |
||||
|
||||
<> rdf:type mf:Manifest ; |
||||
rdfs:label "Oxigraph SPARQL reasoning tests" ; |
||||
mf:entries |
||||
( |
||||
:simple_type_inheritance |
||||
:simple_fact |
||||
:with_graph_name |
||||
:simple_recursion |
||||
) . |
||||
|
||||
:simple_type_inheritance rdf:type ox:SparqlRuleEvaluationTest ; |
||||
mf:name "Simple type inheritance" ; |
||||
mf:action |
||||
[ qt:query <simple_type_inheritance.rq> ; |
||||
qt:data <simple_type_inheritance.ttl> ] ; |
||||
ox:rulesData <simple_type_inheritance.rr> ; |
||||
mf:result <simple_type_inheritance.srj> . |
||||
|
||||
:simple_fact rdf:type ox:SparqlRuleEvaluationTest ; |
||||
mf:name "Simple fact" ; |
||||
mf:action |
||||
[ qt:query <simple_fact.rq> ; |
||||
qt:data <simple_fact.ttl> ] ; |
||||
ox:rulesData <simple_fact.rr> ; |
||||
mf:result <simple_fact.srj> . |
||||
|
||||
:with_graph_name rdf:type ox:SparqlRuleEvaluationTest ; |
||||
mf:name "Simple type inheritance" ; |
||||
mf:action |
||||
[ qt:query <with_graph_name.rq> ; |
||||
qt:graphData <with_graph_name.ttl> ] ; |
||||
ox:rulesData <with_graph_name.rr> ; |
||||
mf:result <with_graph_name.srj> . |
||||
|
||||
:simple_recursion rdf:type ox:SparqlRuleEvaluationTest ; |
||||
mf:name "Simple recursion" ; |
||||
mf:action |
||||
[ qt:query <simple_recursion.rq> ; |
||||
qt:data <simple_recursion.ttl> ] ; |
||||
ox:rulesData <simple_recursion.rr> ; |
||||
mf:result <simple_recursion.srj> . |
@ -0,0 +1,3 @@ |
||||
SELECT * WHERE { |
||||
?s ?p ?o |
||||
} |
@ -0,0 +1,3 @@ |
||||
PREFIX ex: <http://example.org/> |
||||
|
||||
IF {} THEN { ex:s ex:p ex:o } |
@ -0,0 +1,23 @@ |
||||
{ |
||||
"head": { |
||||
"vars": ["s", "p", "o"] |
||||
}, |
||||
"results": { |
||||
"bindings": [ |
||||
{ |
||||
"s": { |
||||
"type": "uri", |
||||
"value": "http://example.org/s" |
||||
}, |
||||
"p": { |
||||
"type": "uri", |
||||
"value": "http://example.org/p" |
||||
}, |
||||
"o": { |
||||
"type": "uri", |
||||
"value": "http://example.org/o" |
||||
} |
||||
} |
||||
] |
||||
} |
||||
} |
@ -0,0 +1,5 @@ |
||||
PREFIX ex: <http://example.org/> |
||||
|
||||
SELECT DISTINCT * WHERE { |
||||
?s ex:includedIn ?o |
||||
} |
@ -0,0 +1,3 @@ |
||||
PREFIX ex: <http://example.org/> |
||||
|
||||
IF { ?s ex:includedIn ?m . ?m ex:includedIn ?o } THEN { ?s ex:includedIn ?o } |
@ -0,0 +1,69 @@ |
||||
{ |
||||
"head": { |
||||
"vars": ["s", "o"] |
||||
}, |
||||
"results": { |
||||
"bindings": [ |
||||
{ |
||||
"s": { |
||||
"type": "uri", |
||||
"value": "http://example.org/Bar" |
||||
}, |
||||
"o": { |
||||
"type": "uri", |
||||
"value": "http://example.org/Foo" |
||||
} |
||||
}, |
||||
{ |
||||
"s": { |
||||
"type": "uri", |
||||
"value": "http://example.org/Baz" |
||||
}, |
||||
"o": { |
||||
"type": "uri", |
||||
"value": "http://example.org/Foo" |
||||
} |
||||
}, |
||||
{ |
||||
"s": { |
||||
"type": "uri", |
||||
"value": "http://example.org/Baz" |
||||
}, |
||||
"o": { |
||||
"type": "uri", |
||||
"value": "http://example.org/Bar" |
||||
} |
||||
}, |
||||
{ |
||||
"s": { |
||||
"type": "uri", |
||||
"value": "http://example.org/Baa" |
||||
}, |
||||
"o": { |
||||
"type": "uri", |
||||
"value": "http://example.org/Foo" |
||||
} |
||||
}, |
||||
{ |
||||
"s": { |
||||
"type": "uri", |
||||
"value": "http://example.org/Baa" |
||||
}, |
||||
"o": { |
||||
"type": "uri", |
||||
"value": "http://example.org/Bar" |
||||
} |
||||
}, |
||||
{ |
||||
"s": { |
||||
"type": "uri", |
||||
"value": "http://example.org/Baa" |
||||
}, |
||||
"o": { |
||||
"type": "uri", |
||||
"value": "http://example.org/Baz" |
||||
} |
||||
} |
||||
] |
||||
} |
||||
} |
@ -0,0 +1,4 @@ |
||||
PREFIX ex: <http://example.org/> |
||||
ex:Bar ex:includedIn ex:Foo . |
||||
ex:Baz ex:includedIn ex:Bar . |
||||
ex:Baa ex:includedIn ex:Baz . |
@ -0,0 +1,5 @@ |
||||
PREFIX ex: <http://example.org/> |
||||
|
||||
SELECT * WHERE { |
||||
?s a ex:Bar |
||||
} |
@ -0,0 +1,3 @@ |
||||
PREFIX ex: <http://example.org/> |
||||
|
||||
IF { ?s a ex:Foo } THEN { ?s a ex:Bar } |
@ -0,0 +1,15 @@ |
||||
{ |
||||
"head": { |
||||
"vars": ["s"] |
||||
}, |
||||
"results": { |
||||
"bindings": [ |
||||
{ |
||||
"s": { |
||||
"type": "uri", |
||||
"value": "http://example.org/s1" |
||||
} |
||||
} |
||||
] |
||||
} |
||||
} |
@ -0,0 +1,2 @@ |
||||
PREFIX ex: <http://example.org/> |
||||
ex:s1 a ex:Foo . |
@ -0,0 +1,5 @@ |
||||
PREFIX ex: <http://example.org/> |
||||
|
||||
SELECT * WHERE { |
||||
GRAPH ?g { ?s a ex:Bar } |
||||
} |
@ -0,0 +1,3 @@ |
||||
PREFIX ex: <http://example.org/> |
||||
|
||||
IF { ?g a ex:Foo } THEN { ?g a ex:Bar } |
@ -0,0 +1,19 @@ |
||||
{ |
||||
"head": { |
||||
"vars": ["s", "g"] |
||||
}, |
||||
"results": { |
||||
"bindings": [ |
||||
{ |
||||
"s": { |
||||
"type": "uri", |
||||
"value": "http://example.org/s1" |
||||
}, |
||||
"g": { |
||||
"type": "uri", |
||||
"value": "https://github.com/oxigraph/oxigraph/tests/sparql-reasoning/with_graph_name.ttl" |
||||
} |
||||
} |
||||
] |
||||
} |
||||
} |
@ -0,0 +1,2 @@ |
||||
PREFIX ex: <http://example.org/> |
||||
ex:s1 a ex:Foo . |
Loading…
Reference in new issue