|
|
|
@ -17,15 +17,14 @@ use std::borrow::Cow; |
|
|
|
|
use std::cell::RefCell; |
|
|
|
|
use std::cmp::{max, min}; |
|
|
|
|
use std::ffi::OsStr; |
|
|
|
|
use std::fmt; |
|
|
|
|
use std::fs::File; |
|
|
|
|
use std::io::{self, stdin, stdout, BufRead, BufReader, BufWriter, Read, Write}; |
|
|
|
|
use std::path::{Path, PathBuf}; |
|
|
|
|
use std::rc::Rc; |
|
|
|
|
use std::str; |
|
|
|
|
use std::str::FromStr; |
|
|
|
|
use std::thread::available_parallelism; |
|
|
|
|
use std::time::{Duration, Instant}; |
|
|
|
|
use std::{fmt, fs, str}; |
|
|
|
|
use url::form_urlencoded; |
|
|
|
|
|
|
|
|
|
const MAX_SPARQL_BODY_SIZE: u64 = 1_048_576; |
|
|
|
@ -161,6 +160,56 @@ enum Command { |
|
|
|
|
#[arg(long)] |
|
|
|
|
graph: Option<String>, |
|
|
|
|
}, |
|
|
|
|
/// Executes a SPARQL query against the store.
|
|
|
|
|
Query { |
|
|
|
|
/// Directory in which the data stored by Oxigraph are persisted.
|
|
|
|
|
#[arg(short, long)] |
|
|
|
|
location: PathBuf, |
|
|
|
|
/// The SPARQL query to execute.
|
|
|
|
|
///
|
|
|
|
|
/// If no query or query file are given, stdin is used.
|
|
|
|
|
#[arg(short, long, conflicts_with = "query_file")] |
|
|
|
|
query: Option<String>, |
|
|
|
|
/// File in which the query is stored.
|
|
|
|
|
///
|
|
|
|
|
/// If no query or query file are given, stdin is used.
|
|
|
|
|
#[arg(long, conflicts_with = "query")] |
|
|
|
|
query_file: Option<PathBuf>, |
|
|
|
|
/// Base URI of the query.
|
|
|
|
|
#[arg(long)] |
|
|
|
|
query_base: Option<String>, |
|
|
|
|
/// File in which the query results will be stored.
|
|
|
|
|
///
|
|
|
|
|
/// If no file is given, stdout is used.
|
|
|
|
|
#[arg(short, long)] |
|
|
|
|
results_file: Option<PathBuf>, |
|
|
|
|
/// The format of the results.
|
|
|
|
|
///
|
|
|
|
|
/// Can be an extension like "nt" or a MIME type like "application/n-triples".
|
|
|
|
|
///
|
|
|
|
|
/// By default the format is guessed from the results file extension.
|
|
|
|
|
#[arg(long, required_unless_present = "results_file")] |
|
|
|
|
results_format: Option<String>, |
|
|
|
|
}, |
|
|
|
|
/// Executes a SPARQL update against the store.
|
|
|
|
|
Update { |
|
|
|
|
/// Directory in which the data stored by Oxigraph are persisted.
|
|
|
|
|
#[arg(short, long)] |
|
|
|
|
location: PathBuf, |
|
|
|
|
/// The SPARQL update to execute.
|
|
|
|
|
///
|
|
|
|
|
/// If no query or query file are given, stdin is used.
|
|
|
|
|
#[arg(short, long, conflicts_with = "update_file")] |
|
|
|
|
update: Option<String>, |
|
|
|
|
/// File in which the update is stored.
|
|
|
|
|
///
|
|
|
|
|
/// If no update or update file are given, stdin is used.
|
|
|
|
|
#[arg(long, conflicts_with = "update")] |
|
|
|
|
update_file: Option<PathBuf>, |
|
|
|
|
/// Base URI of the update.
|
|
|
|
|
#[arg(long)] |
|
|
|
|
update_base: Option<String>, |
|
|
|
|
}, |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
pub fn main() -> anyhow::Result<()> { |
|
|
|
@ -367,6 +416,153 @@ pub fn main() -> anyhow::Result<()> { |
|
|
|
|
dump(&store, stdout().lock(), format, graph) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
Command::Query { |
|
|
|
|
location, |
|
|
|
|
query, |
|
|
|
|
query_file, |
|
|
|
|
query_base, |
|
|
|
|
results_file, |
|
|
|
|
results_format, |
|
|
|
|
} => { |
|
|
|
|
let query = if let Some(query) = query { |
|
|
|
|
query |
|
|
|
|
} else if let Some(query_file) = query_file { |
|
|
|
|
fs::read_to_string(&query_file).with_context(|| { |
|
|
|
|
format!("Not able to read query file {}", query_file.display()) |
|
|
|
|
})? |
|
|
|
|
} else { |
|
|
|
|
// TODO: use io::read_to_string
|
|
|
|
|
let mut query = String::new(); |
|
|
|
|
stdin().lock().read_to_string(&mut query)?; |
|
|
|
|
query |
|
|
|
|
}; |
|
|
|
|
let query = Query::parse(&query, query_base.as_deref())?; |
|
|
|
|
let store = Store::open_read_only(location)?; |
|
|
|
|
match store.query(query)? { |
|
|
|
|
QueryResults::Solutions(solutions) => { |
|
|
|
|
let format = if let Some(name) = results_format { |
|
|
|
|
if let Some(format) = QueryResultsFormat::from_extension(&name) { |
|
|
|
|
format |
|
|
|
|
} else if let Some(format) = QueryResultsFormat::from_media_type(&name) { |
|
|
|
|
format |
|
|
|
|
} else { |
|
|
|
|
bail!("The file format '{name}' is unknown") |
|
|
|
|
} |
|
|
|
|
} else if let Some(results_file) = &results_file { |
|
|
|
|
format_from_path(results_file, |ext| { |
|
|
|
|
QueryResultsFormat::from_extension(ext) |
|
|
|
|
.ok_or_else(|| anyhow!("The file extension '{ext}' is unknown")) |
|
|
|
|
})? |
|
|
|
|
} else { |
|
|
|
|
bail!("The --results-format option must be set when writing to stdout") |
|
|
|
|
}; |
|
|
|
|
if let Some(results_file) = results_file { |
|
|
|
|
let mut writer = QueryResultsSerializer::from_format(format) |
|
|
|
|
.solutions_writer( |
|
|
|
|
BufWriter::new(File::create(results_file)?), |
|
|
|
|
solutions.variables().to_vec(), |
|
|
|
|
)?; |
|
|
|
|
for solution in solutions { |
|
|
|
|
writer.write(&solution?)?; |
|
|
|
|
} |
|
|
|
|
writer.finish()?; |
|
|
|
|
} else { |
|
|
|
|
let stdout = stdout(); // Not needed in Rust 1.61
|
|
|
|
|
let mut writer = QueryResultsSerializer::from_format(format) |
|
|
|
|
.solutions_writer(stdout.lock(), solutions.variables().to_vec())?; |
|
|
|
|
for solution in solutions { |
|
|
|
|
writer.write(&solution?)?; |
|
|
|
|
} |
|
|
|
|
let _ = writer.finish()?; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
QueryResults::Boolean(result) => { |
|
|
|
|
let format = if let Some(name) = results_format { |
|
|
|
|
if let Some(format) = QueryResultsFormat::from_extension(&name) { |
|
|
|
|
format |
|
|
|
|
} else if let Some(format) = QueryResultsFormat::from_media_type(&name) { |
|
|
|
|
format |
|
|
|
|
} else { |
|
|
|
|
bail!("The file format '{name}' is unknown") |
|
|
|
|
} |
|
|
|
|
} else if let Some(results_file) = &results_file { |
|
|
|
|
format_from_path(results_file, |ext| { |
|
|
|
|
QueryResultsFormat::from_extension(ext) |
|
|
|
|
.ok_or_else(|| anyhow!("The file extension '{ext}' is unknown")) |
|
|
|
|
})? |
|
|
|
|
} else { |
|
|
|
|
bail!("The --results-format option must be set when writing to stdout") |
|
|
|
|
}; |
|
|
|
|
if let Some(results_file) = results_file { |
|
|
|
|
QueryResultsSerializer::from_format(format).write_boolean_result( |
|
|
|
|
BufWriter::new(File::create(results_file)?), |
|
|
|
|
result, |
|
|
|
|
)?; |
|
|
|
|
} else { |
|
|
|
|
let _ = QueryResultsSerializer::from_format(format) |
|
|
|
|
.write_boolean_result(stdout().lock(), result)?; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
QueryResults::Graph(triples) => { |
|
|
|
|
let format = if let Some(name) = results_format { |
|
|
|
|
if let Some(format) = GraphFormat::from_extension(&name) { |
|
|
|
|
format |
|
|
|
|
} else if let Some(format) = GraphFormat::from_media_type(&name) { |
|
|
|
|
format |
|
|
|
|
} else { |
|
|
|
|
bail!("The file format '{name}' is unknown") |
|
|
|
|
} |
|
|
|
|
} else if let Some(results_file) = &results_file { |
|
|
|
|
format_from_path(results_file, |ext| { |
|
|
|
|
GraphFormat::from_extension(ext) |
|
|
|
|
.ok_or_else(|| anyhow!("The file extension '{ext}' is unknown")) |
|
|
|
|
})? |
|
|
|
|
} else { |
|
|
|
|
bail!("The --results-format option must be set when writing to stdout") |
|
|
|
|
}; |
|
|
|
|
if let Some(results_file) = results_file { |
|
|
|
|
let mut writer = GraphSerializer::from_format(format) |
|
|
|
|
.triple_writer(BufWriter::new(File::create(results_file)?))?; |
|
|
|
|
for triple in triples { |
|
|
|
|
writer.write(triple?.as_ref())?; |
|
|
|
|
} |
|
|
|
|
writer.finish()?; |
|
|
|
|
} else { |
|
|
|
|
let stdout = stdout(); // Not needed in Rust 1.61
|
|
|
|
|
let mut writer = |
|
|
|
|
GraphSerializer::from_format(format).triple_writer(stdout.lock())?; |
|
|
|
|
for triple in triples { |
|
|
|
|
writer.write(triple?.as_ref())?; |
|
|
|
|
} |
|
|
|
|
writer.finish()?; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
Ok(()) |
|
|
|
|
} |
|
|
|
|
Command::Update { |
|
|
|
|
location, |
|
|
|
|
update, |
|
|
|
|
update_file, |
|
|
|
|
update_base, |
|
|
|
|
} => { |
|
|
|
|
let update = if let Some(update) = update { |
|
|
|
|
update |
|
|
|
|
} else if let Some(update_file) = update_file { |
|
|
|
|
fs::read_to_string(&update_file).with_context(|| { |
|
|
|
|
format!("Not able to read update file {}", update_file.display()) |
|
|
|
|
})? |
|
|
|
|
} else { |
|
|
|
|
// TODO: use io::read_to_string
|
|
|
|
|
let mut update = String::new(); |
|
|
|
|
stdin().lock().read_to_string(&mut update)?; |
|
|
|
|
update |
|
|
|
|
}; |
|
|
|
|
let update = Update::parse(&update, update_base.as_deref())?; |
|
|
|
|
let store = Store::open(location)?; |
|
|
|
|
store.update(update)?; |
|
|
|
|
Ok(()) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -424,18 +620,7 @@ enum GraphOrDatasetFormat { |
|
|
|
|
|
|
|
|
|
impl GraphOrDatasetFormat { |
|
|
|
|
fn from_path(path: &Path) -> anyhow::Result<Self> { |
|
|
|
|
if let Some(ext) = path.extension().and_then(|ext| ext.to_str()) { |
|
|
|
|
Self::from_extension(ext).map_err(|e| { |
|
|
|
|
e.context(format!( |
|
|
|
|
"Not able to guess the file format from file name extension '{ext}'" |
|
|
|
|
)) |
|
|
|
|
}) |
|
|
|
|
} else { |
|
|
|
|
bail!( |
|
|
|
|
"The path {} has no extension to guess a file format from", |
|
|
|
|
path.display() |
|
|
|
|
) |
|
|
|
|
} |
|
|
|
|
format_from_path(path, Self::from_extension) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fn from_extension(name: &str) -> anyhow::Result<Self> { |
|
|
|
@ -467,6 +652,24 @@ impl GraphOrDatasetFormat { |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fn format_from_path<T>( |
|
|
|
|
path: &Path, |
|
|
|
|
from_extension: impl FnOnce(&str) -> anyhow::Result<T>, |
|
|
|
|
) -> anyhow::Result<T> { |
|
|
|
|
if let Some(ext) = path.extension().and_then(|ext| ext.to_str()) { |
|
|
|
|
from_extension(ext).map_err(|e| { |
|
|
|
|
e.context(format!( |
|
|
|
|
"Not able to guess the file format from file name extension '{ext}'" |
|
|
|
|
)) |
|
|
|
|
}) |
|
|
|
|
} else { |
|
|
|
|
bail!( |
|
|
|
|
"The path {} has no extension to guess a file format from", |
|
|
|
|
path.display() |
|
|
|
|
) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
impl FromStr for GraphOrDatasetFormat { |
|
|
|
|
type Err = Error; |
|
|
|
|
|
|
|
|
@ -1388,6 +1591,33 @@ mod tests { |
|
|
|
|
)) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fn initialized_cli_store(data: &'static str) -> Result<TempDir> { |
|
|
|
|
let store_dir = TempDir::new()?; |
|
|
|
|
cli_command()? |
|
|
|
|
.arg("load") |
|
|
|
|
.arg("--location") |
|
|
|
|
.arg(store_dir.path()) |
|
|
|
|
.arg("--format") |
|
|
|
|
.arg("trig") |
|
|
|
|
.write_stdin(data) |
|
|
|
|
.assert() |
|
|
|
|
.success(); |
|
|
|
|
Ok(store_dir) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fn assert_cli_state(store_dir: TempDir, data: &'static str) -> Result<()> { |
|
|
|
|
cli_command()? |
|
|
|
|
.arg("dump") |
|
|
|
|
.arg("--location") |
|
|
|
|
.arg(store_dir.path()) |
|
|
|
|
.arg("--format") |
|
|
|
|
.arg("nq") |
|
|
|
|
.assert() |
|
|
|
|
.stdout(data) |
|
|
|
|
.success(); |
|
|
|
|
Ok(()) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
#[test] |
|
|
|
|
fn cli_help() -> Result<()> { |
|
|
|
|
cli_command()? |
|
|
|
@ -1583,16 +1813,9 @@ mod tests { |
|
|
|
|
|
|
|
|
|
#[test] |
|
|
|
|
fn cli_backup() -> Result<()> { |
|
|
|
|
let store_dir = TempDir::new()?; |
|
|
|
|
cli_command()? |
|
|
|
|
.arg("load") |
|
|
|
|
.arg("--location") |
|
|
|
|
.arg(store_dir.path()) |
|
|
|
|
.arg("--format") |
|
|
|
|
.arg("nq") |
|
|
|
|
.write_stdin("<http://example.com/s> <http://example.com/p> <http://example.com/o> .") |
|
|
|
|
.assert() |
|
|
|
|
.success(); |
|
|
|
|
let store_dir = initialized_cli_store( |
|
|
|
|
"<http://example.com/s> <http://example.com/p> <http://example.com/o> .", |
|
|
|
|
)?; |
|
|
|
|
|
|
|
|
|
let backup_dir = TempDir::new()?; |
|
|
|
|
remove_dir_all(backup_dir.path())?; // The directory should not exist yet
|
|
|
|
@ -1605,18 +1828,133 @@ mod tests { |
|
|
|
|
.assert() |
|
|
|
|
.success(); |
|
|
|
|
|
|
|
|
|
assert_cli_state( |
|
|
|
|
store_dir, |
|
|
|
|
"<http://example.com/s> <http://example.com/p> <http://example.com/o> .\n", |
|
|
|
|
) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
#[test] |
|
|
|
|
fn cli_ask_query_inline() -> Result<()> { |
|
|
|
|
let store_dir = initialized_cli_store( |
|
|
|
|
"<http://example.com/s> <http://example.com/p> <http://example.com/o> .", |
|
|
|
|
)?; |
|
|
|
|
cli_command()? |
|
|
|
|
.arg("dump") |
|
|
|
|
.arg("query") |
|
|
|
|
.arg("--location") |
|
|
|
|
.arg(backup_dir.path()) |
|
|
|
|
.arg("--format") |
|
|
|
|
.arg("nq") |
|
|
|
|
.arg(store_dir.path()) |
|
|
|
|
.arg("--query") |
|
|
|
|
.arg("ASK { <s> <p> <o> }") |
|
|
|
|
.arg("--query-base") |
|
|
|
|
.arg("http://example.com/") |
|
|
|
|
.arg("--results-format") |
|
|
|
|
.arg("csv") |
|
|
|
|
.assert() |
|
|
|
|
.success() |
|
|
|
|
.stdout("<http://example.com/s> <http://example.com/p> <http://example.com/o> .\n"); |
|
|
|
|
.stdout("true") |
|
|
|
|
.success(); |
|
|
|
|
Ok(()) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
#[test] |
|
|
|
|
fn cli_construct_query_stdin() -> Result<()> { |
|
|
|
|
let store_dir = initialized_cli_store( |
|
|
|
|
"<http://example.com/s> <http://example.com/p> <http://example.com/o> .", |
|
|
|
|
)?; |
|
|
|
|
cli_command()? |
|
|
|
|
.arg("query") |
|
|
|
|
.arg("--location") |
|
|
|
|
.arg(store_dir.path()) |
|
|
|
|
.arg("--query-base") |
|
|
|
|
.arg("http://example.com/") |
|
|
|
|
.arg("--results-format") |
|
|
|
|
.arg("nt") |
|
|
|
|
.write_stdin("CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }") |
|
|
|
|
.assert() |
|
|
|
|
.stdout("<http://example.com/s> <http://example.com/p> <http://example.com/o> .\n") |
|
|
|
|
.success(); |
|
|
|
|
Ok(()) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
#[test] |
|
|
|
|
fn cli_select_query_file() -> Result<()> { |
|
|
|
|
let store_dir = initialized_cli_store( |
|
|
|
|
"<http://example.com/s> <http://example.com/p> <http://example.com/o> .", |
|
|
|
|
)?; |
|
|
|
|
let input_file = NamedTempFile::new("input.rq")?; |
|
|
|
|
input_file.write_str("SELECT ?s WHERE { ?s ?p ?o }")?; |
|
|
|
|
let output_file = NamedTempFile::new("output.tsv")?; |
|
|
|
|
cli_command()? |
|
|
|
|
.arg("query") |
|
|
|
|
.arg("--location") |
|
|
|
|
.arg(store_dir.path()) |
|
|
|
|
.arg("--query-file") |
|
|
|
|
.arg(input_file.path()) |
|
|
|
|
.arg("--results-file") |
|
|
|
|
.arg(output_file.path()) |
|
|
|
|
.assert() |
|
|
|
|
.success(); |
|
|
|
|
output_file.assert("?s\n<http://example.com/s>\n"); |
|
|
|
|
Ok(()) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
#[test] |
|
|
|
|
fn cli_ask_update_inline() -> Result<()> { |
|
|
|
|
let store_dir = TempDir::new()?; |
|
|
|
|
cli_command()? |
|
|
|
|
.arg("update") |
|
|
|
|
.arg("--location") |
|
|
|
|
.arg(store_dir.path()) |
|
|
|
|
.arg("--update") |
|
|
|
|
.arg("INSERT DATA { <s> <p> <o> }") |
|
|
|
|
.arg("--update-base") |
|
|
|
|
.arg("http://example.com/") |
|
|
|
|
.assert() |
|
|
|
|
.success(); |
|
|
|
|
assert_cli_state( |
|
|
|
|
store_dir, |
|
|
|
|
"<http://example.com/s> <http://example.com/p> <http://example.com/o> .\n", |
|
|
|
|
) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
#[test] |
|
|
|
|
fn cli_construct_update_stdin() -> Result<()> { |
|
|
|
|
let store_dir = TempDir::new()?; |
|
|
|
|
cli_command()? |
|
|
|
|
.arg("update") |
|
|
|
|
.arg("--location") |
|
|
|
|
.arg(store_dir.path()) |
|
|
|
|
.arg("--update-base") |
|
|
|
|
.arg("http://example.com/") |
|
|
|
|
.write_stdin("INSERT DATA { <s> <p> <o> }") |
|
|
|
|
.assert() |
|
|
|
|
.success(); |
|
|
|
|
assert_cli_state( |
|
|
|
|
store_dir, |
|
|
|
|
"<http://example.com/s> <http://example.com/p> <http://example.com/o> .\n", |
|
|
|
|
) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
#[test] |
|
|
|
|
fn cli_update_file() -> Result<()> { |
|
|
|
|
let store_dir = TempDir::new()?; |
|
|
|
|
let input_file = NamedTempFile::new("input.rq")?; |
|
|
|
|
input_file.write_str( |
|
|
|
|
"INSERT DATA { <http://example.com/s> <http://example.com/p> <http://example.com/o> }", |
|
|
|
|
)?; |
|
|
|
|
cli_command()? |
|
|
|
|
.arg("update") |
|
|
|
|
.arg("--location") |
|
|
|
|
.arg(store_dir.path()) |
|
|
|
|
.arg("--update-file") |
|
|
|
|
.arg(input_file.path()) |
|
|
|
|
.assert() |
|
|
|
|
.success(); |
|
|
|
|
assert_cli_state( |
|
|
|
|
store_dir, |
|
|
|
|
"<http://example.com/s> <http://example.com/p> <http://example.com/o> .\n", |
|
|
|
|
) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
#[test] |
|
|
|
|
fn get_ui() -> Result<()> { |
|
|
|
|
ServerTest::new()?.test_status( |
|
|
|
|