Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/tag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ jobs:
shell: bash
run: |
# Replace with the name of your binary
binary_name="oxi_tarql"
binary_name="oxi_gen"

dirname="$binary_name-${{ env.RELEASE_VERSION }}-${{ matrix.target }}"
mkdir "$dirname"
Expand Down
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "oxi_tarql"
version = "0.3.1"
name = "oxi_gen"
version = "0.4.0"
edition = "2024"

[dependencies]
Expand Down
58 changes: 58 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# oxi-gen

A high-performance command-line tool for converting CSV/TSV files to RDF (Turtle or N-Triples) using SPARQL CONSTRUCT queries. Inspired by [Tarql](https://tarql.github.io/), oxi-gen is built in Rust on top of the [Oxigraph](https://github.com/oxigraph/oxigraph) stack and leverages multi-threaded processing to handle large datasets efficiently.

Each row of the input CSV is bound as SPARQL variable substitutions and evaluated against a CONSTRUCT query, producing RDF output. Column headers become variable names (e.g., a `name` column is available as `?name`), and the special variable `?ROWNUM` holds the current row index.

## Command-Line Options

```
oxi_gen -q <QUERY> [OPTIONS]
```

| Option | Short | Description |
|---|---|---|
| `--query <FILE>` | `-q` | SPARQL CONSTRUCT query file to apply (required) |
| `--input <FILE>` | `-i` | Input CSV file. Omit to read from STDIN |
| `--output <FILE>` | `-o` | Output file. Omit to write to STDOUT |
| `--delimiter <CHAR>` | `-d` | CSV delimiter character (default: `,`) |
| `--tab` | `-t` | Treat input as tab-separated (TSV) |
| `--no-header-row` | `-H` | Input has no header row; columns are named `a`–`z`, `A`–`Z` |
| `--normalize` | `-n` | Normalize column names to UPPERCASE |
| `--escape_char <CHAR>` | `-p` | Escape character (default: `\`) |
| `--quote_char <CHAR>` | | Quote character (default: `"`) |
| `--ntriples` | | Output N-Triples instead of Turtle |
| `--gzip` | `-g` | Gzip the output (requires `--output`) |
| `--dedup[=N]` | | Deduplicate triples within a sliding window (default window: 1000, range: 1000–5000000) |
| `--test[=N]` | | Process only the first N rows for testing (default: 5, max: 49) |
| `--split <ORIGINAL> <SPLIT> <DELIMITER>` | | Split column ORIGINAL on DELIMITER, binding each value to SPLIT. Can be repeated |
| `--bind-empty-strings` | | Bind empty CSV values as empty string literals instead of skipping them |

## Custom SPARQL Functions

oxi-gen registers two custom functions under the `tarql:` prefix (`https://semanticarts.com/tarql/`):

- **`tarql:expandPrefix(?prefix)`** — returns the IRI for a given prefix name declared in the query.
- **`tarql:expandPrefixedName(?qname)`** — expands a prefixed name (e.g., `"foaf:name"`) into a full IRI node.

## Building from Source

### Prerequisites

- [Rust](https://www.rust-lang.org/tools/install) (1.85+ required for edition 2024)

### Build a release binary

```sh
git clone git@github.com:semanticarts/oxi-gen.git
cd oxi-gen
cargo build --release
```

The optimized binary will be at `target/release/oxi_gen`. The release profile is configured with LTO, single codegen unit, and abort-on-panic for maximum performance.

### Run directly with Cargo

```sh
cargo run --release -- -q query.sparql -i data.csv -o output.ttl
```
10 changes: 5 additions & 5 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use clap::{Arg, ArgAction, ArgMatches, command, value_parser};

#[allow(dead_code)]
#[derive(Default)]
pub struct OxiTarql {
pub struct OxiGen {
pub delimiter: String,
pub tab: bool,
pub test: u32,
Expand All @@ -41,7 +41,7 @@ pub struct OxiTarql {
pub split: Vec<(String, String, String)>,
}

impl OxiTarql {
impl OxiGen {
pub fn transform(&mut self) -> Result<(), Box<dyn Error>> {
let num_workers: usize = num_cpus::get();

Expand Down Expand Up @@ -424,7 +424,7 @@ where
I: IntoIterator<Item = String>,
{
command!()
.about("Convert CSV file to RDF using SPARQL")
.about("oxi-gen: Convert CSV file to RDF using SPARQL")
.arg(
Arg::new("delimiter")
.short('d')
Expand Down Expand Up @@ -547,7 +547,7 @@ where
.get_matches_from(args)
}

pub fn configure_transform<I>(args: I) -> OxiTarql
pub fn configure_transform<I>(args: I) -> OxiGen
where
I: IntoIterator<Item = String>,
{
Expand All @@ -565,7 +565,7 @@ where
}
};

OxiTarql {
OxiGen {
delimiter: matches.get_one::<String>("delimiter").unwrap().to_string(),
tab: matches.get_flag("tab"),
test: match matches.get_one::<u32>("test") {
Expand Down
6 changes: 3 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
use oxi_tarql::configure_transform;
use oxi_gen::configure_transform;
use std::{env, time::Instant};

fn main() {
// parse the supplied arguments
let os_args: Vec<String> = env::args_os().map(|a| a.into_string().unwrap()).collect();
let mut tarql = configure_transform(os_args);
let mut transform = configure_transform(os_args);

let start = Instant::now();

tarql.transform().expect("Transformation failed");
transform.transform().expect("Transformation failed");

let duration = Instant::now().duration_since(start);
eprintln!("Processing complete in {} seconds", duration.as_secs_f32());
Expand Down
5 changes: 5 additions & 0 deletions tests/fixtures/escaped_chars.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
id,description
1,"C:\\Users\\test\\path"
2,"line1\\nline2\\ttab"
3,"say \"hello world\""
4,"mixed: C:\\path \"quoted\""
9 changes: 9 additions & 0 deletions tests/fixtures/escaped_chars.rq
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
prefix : <https://test.com/d/>
construct {
?row_iri
:description ?description ;
.
}
where {
BIND(tarql:expandPrefixedName(CONCAT(":row_", STR(?ROWNUM))) AS ?row_iri)
}
Loading
Loading