rdf-oxigraph 0.0.1-x86_64-linux

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3998d114420e71667cd34d012257eb3f9bd0eda248df52cc5f1beedebc88e193
4
+ data.tar.gz: be902d50e35dc817568b3413de8482023cdb7cb8e6d14d629e01d2b5597345a2
5
+ SHA512:
6
+ metadata.gz: 5925144c48c78f26bae8c63fa78e0861dd93e8ff2dea49a356db5e1b1fba859366f57d127931a7edd64217a0153529584f39f0c90d849053e69f05af6e038e1a
7
+ data.tar.gz: 2313fa31f1f553e966cd4864a5b90c3743edfc1ad7bcde1a2fab93f82cf064861b192087b4737c495633f2d5181897af0fddc395c69302ef21dd6ffdd997311f
data/CHANGELOG.md ADDED
@@ -0,0 +1,31 @@
1
+ # Changelog
2
+
3
+ ## [Unreleased] (0.0.1)
4
+
5
+ Initial development release. A drop-in RDF.rb-compatible storage/SPARQL backend
6
+ powered by the [oxigraph](https://github.com/oxigraph/oxigraph) Rust engine via a
7
+ magnus + rb-sys native extension.
8
+
9
+ ### Added
10
+ - Built on **oxigraph 0.5** (in-memory store; `default-features = false`, no RocksDB).
11
+ - Native `RDF::Oxigraph::Store` over `oxigraph::store::Store`
12
+ (insert/delete/size/load/dump/each_quad/query/update) with full term
13
+ marshalling (IRIs, blank nodes, simple/typed/language literals, default and
14
+ named graphs).
15
+ - `RDF::Oxigraph::Repository < RDF::Repository` — passes 319/338 of the official
16
+ `rdf-spec` `RDF::Repository` shared examples (the remainder is oxigraph's
17
+ typed-literal canonicalization; see README).
18
+ - `RDF::Reader`/`RDF::Writer`/`RDF::Format` for Turtle, TriG, N-Triples, N-Quads,
19
+ RDF/XML and JSON-LD (native oxigraph parse/serialize), plus fast
20
+ `Repository#load`/`#dump`.
21
+ - SPARQL query + update: `Repository#sparql` (→ `RDF::Query::Solutions` /
22
+ `RDF::Graph` / boolean), `Repository#sparql_update`, and a
23
+ `RDF::Oxigraph::SPARQL::Client` string-based wrapper. Full FILTER/REGEX/PREFIX.
24
+ - In-process **SHACL validation** via the rudof `shacl` crate (0.3.x):
25
+ `Repository#shacl_validate(shapes)` and `RDF::Oxigraph::SHACL.validate(data:, shapes:)`,
26
+ returning a report with `#conforms?`, `#violations`, `#results`, `#to_s`. No external
27
+ service; no RocksDB/C++ pulled in.
28
+ - Precompiled, platform-specific gems (install with no Rust toolchain required)
29
+ for Linux x86_64/arm64 (glibc + musl) and macOS arm64/x86_64. oxigraph is built
30
+ with `default-features = false` (in-memory store only, no RocksDB/C++), keeping
31
+ Linux cross-compilation reliable.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mehmet Celik
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,366 @@
1
+ # rdf-oxigraph
2
+
3
+ A Ruby gem providing a **drop-in [RDF.rb](https://github.com/ruby-rdf/rdf)-compatible**
4
+ storage + SPARQL backend powered by the
5
+ [oxigraph](https://github.com/oxigraph/oxigraph) Rust engine, via a
6
+ [magnus](https://github.com/matsadler/magnus) + [rb-sys](https://github.com/oxidize-rb/rb-sys)
7
+ native extension.
8
+
9
+ ## Why
10
+
11
+ `RDF::Repository` ships with rdf.rb as a pure-Ruby, in-memory store, and SPARQL on top of
12
+ it (the `sparql` gem) is a pure-Ruby query engine. That's portable but slow, and SPARQL 1.1
13
+ coverage is uneven.
14
+
15
+ **rdf-oxigraph is a drop-in replacement for that backend.** It swaps the pure-Ruby store and
16
+ query engine for oxigraph — a fast, embedded, native **SPARQL 1.1** engine written in Rust —
17
+ while keeping the exact RDF.rb surface your code already uses. Existing rdf.rb /
18
+ `SPARQL::Client` consumers get the native engine **without changing their
19
+ code**: the only edit is which `Repository` class you instantiate.
20
+
21
+ What you get over the stock backend:
22
+
23
+ - Native SPARQL 1.1 **query + update** (SELECT/ASK/CONSTRUCT/DESCRIBE, full FILTER/REGEX/PREFIX).
24
+ - Native, fast **parse/serialize** for Turtle, TriG, N-Triples, N-Quads, RDF/XML, JSON-LD.
25
+ - In-process **SHACL validation** (oxigraph has none; provided via the rudof `shacl` crate).
26
+ - **Precompiled gems** — no Rust toolchain needed to install on the common platforms.
27
+
28
+ ## Installation
29
+
30
+ Add to your `Gemfile`:
31
+
32
+ ```ruby
33
+ gem "rdf-oxigraph"
34
+ ```
35
+
36
+ or:
37
+
38
+ ```bash
39
+ gem install rdf-oxigraph
40
+ ```
41
+
42
+ Precompiled, platform-specific gems are published, so **no Rust toolchain is required to
43
+ install** on:
44
+
45
+ | Platform | Targets |
46
+ |----------|---------|
47
+ | Linux (glibc) | `x86_64-linux`, `aarch64-linux` |
48
+ | Linux (musl, Alpine, …) | `x86_64-linux-musl`, `aarch64-linux-musl` |
49
+ | macOS | `arm64-darwin` (Apple Silicon), `x86_64-darwin` (Intel) |
50
+
51
+ If no precompiled gem matches your platform, the gem falls back to **compiling from source**,
52
+ which requires a Rust toolchain (rustup recommended), Ruby headers, and clang/libclang.
53
+
54
+ > The extension uses oxigraph's **in-memory** store only (oxigraph built with
55
+ > `default-features = false`, no RocksDB / no C++). This keeps the gem small (~2.5 MB) and the
56
+ > Linux glibc/musl cross-builds reliable. On-disk persistence (`Store::open`) is intentionally
57
+ > not enabled — re-enable oxigraph's `rocksdb` feature in `ext/rdf_oxigraph/Cargo.toml` if you
58
+ > need it.
59
+
60
+ ## Architecture — how it fits into rdf.rb
61
+
62
+ rdf.rb defines the *interface* (the `RDF::Enumerable` / `RDF::Queryable` / `RDF::Mutable`
63
+ mixins, plus the `RDF::Reader` / `RDF::Writer` / `RDF::Format` registries). The slow parts
64
+ are the *implementations*: the pure-Ruby in-memory store and the pure-Ruby SPARQL engine.
65
+
66
+ rdf-oxigraph keeps the interfaces and replaces the implementations. Your code talks to the
67
+ same rdf.rb objects (`RDF::Statement`, `RDF::Query::Solutions`, `RDF::Graph`); underneath,
68
+ calls cross a thin tagged-tuple bridge into the native oxigraph engine.
69
+
70
+ ```mermaid
71
+ flowchart TD
72
+ App["Your app<br/>(plain RDF.rb code)"]
73
+
74
+ subgraph rdfrb["rdf.rb (interfaces — unchanged)"]
75
+ Mixins["RDF::Enumerable / Queryable / Mutable"]
76
+ Registry["RDF::Reader / Writer / Format registry"]
77
+ Terms["RDF::Statement · Query::Solutions · Graph"]
78
+ end
79
+
80
+ subgraph gem["rdf-oxigraph (this gem, Ruby)"]
81
+ Repo["RDF::Oxigraph::Repository<br/>< RDF::Repository"]
82
+ RW["Oxigraph Reader / Writer / Format<br/>(turtle, trig, n-triples, n-quads, rdf/xml, json-ld)"]
83
+ Sparql["SPARQL.results + SPARQL::Client"]
84
+ Shacl["SHACL.validate → Report"]
85
+ Conv["Conversion<br/>(RDF.rb terms ⇄ tagged tuples)"]
86
+ end
87
+
88
+ subgraph native["native extension (Rust, via magnus + rb-sys)"]
89
+ Store["RDF::Oxigraph::Store"]
90
+ Oxi["oxigraph 0.5<br/>(in-memory store + SPARQL 1.1)"]
91
+ Rudof["rudof shacl crate"]
92
+ end
93
+
94
+ App --> Mixins
95
+ App --> Registry
96
+ App --> Terms
97
+
98
+ Mixins --> Repo
99
+ Registry --> RW
100
+ Repo --> Sparql
101
+ Repo --> Shacl
102
+ Repo --> Conv
103
+ RW --> Conv
104
+
105
+ Conv <--> Store
106
+ Sparql --> Store
107
+ Shacl --> Rudof
108
+ Store --> Oxi
109
+ ```
110
+
111
+ The native boundary is deliberately narrow: terms are marshalled as small tagged tuples
112
+ (e.g. `["uri", "..."]`, `["lit", value, datatype, lang]`), and SHACL crosses the boundary
113
+ only as serialized RDF strings.
114
+
115
+ ## Usage
116
+
117
+ ### Do I have to use `RDF::Oxigraph::Repository`?
118
+
119
+ **Yes — instantiating `RDF::Oxigraph::Repository` (or the lower-level
120
+ `RDF::Oxigraph::Store`) is the one and only thing you change.** That object *is* the
121
+ oxigraph-backed store; without it, your data lives in the stock pure-Ruby store and none of the
122
+ native speedups apply. Everything else stays standard rdf.rb:
123
+
124
+ ```ruby
125
+ require "rdf/oxigraph"
126
+
127
+ repo = RDF::Oxigraph::Repository.new # <-- the only oxigraph-specific line
128
+
129
+ # ...from here on it's plain RDF.rb — identical to stock RDF::Repository:
130
+ repo << RDF::Statement.new(subject, predicate, object)
131
+ repo.query([nil, RDF.type, RDF::URI("http://example.org/Person")])
132
+ repo.count
133
+ ```
134
+
135
+ Two nuances:
136
+
137
+ - **Readers/Writers register globally.** Once `rdf/oxigraph` is required, the oxigraph-backed
138
+ Turtle/TriG/N-Triples/N-Quads/RDF-XML/JSON-LD readers/writers are registered in rdf.rb's
139
+ format registry. `RDF::Reader.for(...)` / `RDF::Writer.for(...)` and `Repository#load` /
140
+ `#dump` then use the native parsers — no extra wiring.
141
+ - **SPARQL needs the repository.** `repo.sparql(...)`, `repo.sparql_update(...)`, and the
142
+ `RDF::Oxigraph::SPARQL::Client` wrapper all run against the oxigraph store, so they require
143
+ the oxigraph repository.
144
+
145
+ ### Drop-in repository
146
+
147
+ The same rdf.rb code runs unchanged against the stock store and the oxigraph store with
148
+ identical results:
149
+
150
+ ```ruby
151
+ require "rdf/oxigraph"
152
+
153
+ EX = RDF::Vocabulary.new("http://example.org/")
154
+ repo = RDF::Oxigraph::Repository.new
155
+ repo << RDF::Statement.new(EX.alice, RDF.type, EX.Person)
156
+ repo << RDF::Statement.new(EX.alice, EX.name, RDF::Literal("Alice"))
157
+
158
+ repo.query([nil, RDF.type, EX.Person]).each { |s| puts s.subject }
159
+ ```
160
+
161
+ ### Load & serialize (native parse/serialize)
162
+
163
+ ```ruby
164
+ repo = RDF::Oxigraph::Repository.new
165
+ repo.load("data.ttl") # native Turtle parse, straight into the store
166
+ puts repo.dump(:ntriples) # native serialization
167
+ puts repo.dump(:jsonld)
168
+
169
+ RDF::Reader.for(content_type: "text/turtle") # => oxigraph-backed reader
170
+ RDF::Writer.for(:ntriples) # => oxigraph-backed writer
171
+ ```
172
+
173
+ ### SPARQL query + update
174
+
175
+ ```ruby
176
+ repo = RDF::Oxigraph::Repository.new
177
+ repo.load("data.ttl")
178
+
179
+ # SELECT -> RDF::Query::Solutions
180
+ repo.sparql("SELECT ?s WHERE { ?s a <http://example.org/Book> }").each { |sol| puts sol[:s] }
181
+
182
+ # ASK -> true/false ; CONSTRUCT/DESCRIBE -> RDF::Graph
183
+ repo.sparql("ASK { ?s ?p ?o }")
184
+ graph = repo.sparql("CONSTRUCT { ?a ?b ?c } WHERE { ?a ?b ?c }")
185
+
186
+ # SPARQL 1.1 Update
187
+ repo.sparql_update("INSERT DATA { <http://example.org/x> a <http://example.org/Book> }")
188
+
189
+ # SPARQL::Client-shaped wrapper (string-based query/update)
190
+ client = RDF::Oxigraph::SPARQL::Client.new(repo)
191
+ client.query("SELECT (COUNT(?s) AS ?n) WHERE { ?s ?p ?o }")
192
+ ```
193
+
194
+ ### SHACL validation
195
+
196
+ oxigraph itself has no SHACL; validation is provided in-process by the rudof
197
+ [`shacl`](https://crates.io/crates/shacl) crate (native, no external service):
198
+
199
+ ```ruby
200
+ report = repo.shacl_validate(shapes_turtle_string) # validate the repo's data
201
+ report.conforms? # => true / false
202
+ report.violations # => count
203
+ report.results # => [{ "focus" => ..., "severity" => ..., "message" => ..., "path" => ... }, ...]
204
+ puts report.to_s # human-readable report
205
+
206
+ # or validate two RDF strings directly, no repository needed:
207
+ RDF::Oxigraph::SHACL.validate(data: data_ttl, shapes: shapes_ttl)
208
+ ```
209
+
210
+ ### Runnable examples
211
+
212
+ The [`examples/`](examples/) directory has self-contained scripts (shared dataset in
213
+ `examples/data/books.ttl`). After `rake compile` (or installing the gem):
214
+
215
+ ```bash
216
+ ruby -Ilib examples/01_drop_in_repository.rb # drop-in: same code, stock vs oxigraph
217
+ ruby -Ilib examples/02_load_and_serialize.rb # native load + serialize, format registry
218
+ ruby -Ilib examples/03_sparql.rb # SELECT/ASK/CONSTRUCT + UPDATE + client
219
+ ruby -Ilib examples/04_shacl.rb # SHACL validation
220
+ ```
221
+
222
+ (Drop the `-Ilib` once the gem is installed.)
223
+
224
+ ## Conformance & known deviations
225
+
226
+ `RDF::Oxigraph::Repository` passes **319/338** of the official `rdf-spec` `RDF::Repository`
227
+ shared examples (`rake conformance`). The remaining ~19 failures stem from a single,
228
+ documented backend behavior:
229
+
230
+ - **oxigraph canonicalizes typed literals.** `"01"^^xsd:integer` is stored as
231
+ `"1"^^xsd:integer`, and `"1.0e0"^^xsd:double` as its canonical form. rdf-spec treats
232
+ distinct lexical forms as distinct terms, so the lexical-preservation / term-count /
233
+ value-pattern tests fail. This is semantically reasonable (both denote the same value) and
234
+ is harmless.
235
+ - `#dump` differs only in auto-assigned blank-node labels (the triples are identical).
236
+
237
+ These are inherent to oxigraph's value-based storage and are not fixable in the gem.
238
+
239
+ ## Tests
240
+
241
+ Two suites:
242
+
243
+ - **Minitest** — the main suite and the default `rake` task:
244
+
245
+ ```bash
246
+ CARGO_TARGET_DIR="$HOME/.cargo-target/rdf-oxigraph" bundle exec rake test
247
+ ```
248
+
249
+ - **RSpec** — runs only the `rdf-spec` conformance shared examples (which are RSpec-only):
250
+
251
+ ```bash
252
+ CARGO_TARGET_DIR="$HOME/.cargo-target/rdf-oxigraph" bundle exec rake conformance
253
+ ```
254
+
255
+ `rake test` is green; `rake conformance` has ~19 expected failures (see
256
+ [Conformance & known deviations](#conformance--known-deviations)). Both tasks depend on
257
+ `:compile`, so they rebuild the native extension first. Setting `CARGO_TARGET_DIR` keeps Rust
258
+ build artifacts out of Dropbox/source control (optional but recommended).
259
+
260
+ CI (`.github/workflows/ci.yml`) runs both suites across Ruby 3.1–3.4 on Linux and macOS.
261
+
262
+ ## Compile (development)
263
+
264
+ ```bash
265
+ bundle install
266
+
267
+ # Build the native extension into lib/rdf/oxigraph/:
268
+ CARGO_TARGET_DIR="$HOME/.cargo-target/rdf-oxigraph" bundle exec rake compile
269
+
270
+ # Smoke test:
271
+ ruby -Ilib -e 'require "rdf/oxigraph"; \
272
+ r = RDF::Oxigraph::Repository.new; \
273
+ r.sparql_update("INSERT DATA { <http://ex/a> <http://ex/b> <http://ex/c> }"); \
274
+ puts r.sparql("ASK { ?s ?p ?o }")'
275
+ ```
276
+
277
+ Requirements for compiling from source: Ruby >= 3.0 with headers, a Rust toolchain (rustup
278
+ recommended), and clang/libclang.
279
+
280
+ ## Building & publishing the gem (Intel + ARM)
281
+
282
+ The pure-Ruby `gem build` produces a source gem (Rust compiled at install time). To ship
283
+ **precompiled, platform-specific** gems so users don't need Rust, build one gem per platform.
284
+
285
+ ### Recommended: GitHub Actions (all platforms)
286
+
287
+ The [`precompile`](.github/workflows/precompile.yml) workflow uses
288
+ [`oxidize-rb/actions/cross-gem`](https://github.com/oxidize-rb/actions) to cross-compile gems
289
+ for every supported platform (Linux x86_64/arm64 glibc + musl, macOS arm64/x86_64) across Ruby
290
+ 3.1–3.4. It triggers on a version tag or manually:
291
+
292
+ ```bash
293
+ # Tag a release -> the workflow builds every platform gem and uploads them as artifacts:
294
+ git tag v0.0.1
295
+ git push origin v0.0.1
296
+ ```
297
+
298
+ Download the artifacts and push them:
299
+
300
+ ```bash
301
+ gem push rdf-oxigraph-0.0.1-arm64-darwin.gem
302
+ gem push rdf-oxigraph-0.0.1-x86_64-darwin.gem
303
+ gem push rdf-oxigraph-0.0.1-x86_64-linux.gem
304
+ gem push rdf-oxigraph-0.0.1-aarch64-linux.gem
305
+ gem push rdf-oxigraph-0.0.1-x86_64-linux-musl.gem
306
+ gem push rdf-oxigraph-0.0.1-aarch64-linux-musl.gem
307
+ ```
308
+
309
+ > The gemspec sets `rubygems_mfa_required`, so `gem push` will prompt for your RubyGems OTP.
310
+
311
+ ### Local cross-compilation (Intel + ARM)
312
+
313
+ Cross-compilation is Docker-based, via
314
+ [`rb-sys-dock`](https://github.com/oxidize-rb/rb-sys) — no per-platform machines needed. It
315
+ requires Docker (or Podman) running locally.
316
+
317
+ #### Easiest: `rake build` (interactive)
318
+
319
+ ```bash
320
+ bundle exec rake build
321
+ ```
322
+
323
+ This wraps `rb-sys-dock` with a menu: it lists every supported target, builds the one you
324
+ pick, and loops until you quit. It also handles the two footguns described below for you
325
+ (corrects symlinked paths, pins Ruby to `~> 3.0`). Override the Ruby pin with
326
+ `RUBY_VERSIONS=... bundle exec rake build` if you need to.
327
+
328
+ #### Manual: `rb-sys-dock` per platform
329
+
330
+ ```bash
331
+ # macOS — Apple Silicon (arm64) and Intel (x86_64):
332
+ bundle exec rb-sys-dock --platform arm64-darwin --ruby-versions "~> 3.0" --build
333
+ bundle exec rb-sys-dock --platform x86_64-darwin --ruby-versions "~> 3.0" --build
334
+
335
+ # Linux — Intel + ARM (glibc and musl):
336
+ bundle exec rb-sys-dock --platform x86_64-linux --ruby-versions "~> 3.0" --build
337
+ bundle exec rb-sys-dock --platform aarch64-linux --ruby-versions "~> 3.0" --build
338
+ bundle exec rb-sys-dock --platform x86_64-linux-musl --ruby-versions "~> 3.0" --build
339
+ bundle exec rb-sys-dock --platform aarch64-linux-musl --ruby-versions "~> 3.0" --build
340
+ ```
341
+
342
+ Each build drops a `pkg/rdf-oxigraph-<version>-<platform>.gem`; push each with `gem push` as
343
+ above.
344
+
345
+ > **Two gotchas** (both handled automatically by `rake build`):
346
+ >
347
+ > - **Pin Ruby to `~> 3.0`.** The cross-compile image also ships a Ruby 4.0 target, but
348
+ > `magnus 0.7` cannot compile against it (`no field 'typed_flag' on type RTypedData`). The
349
+ > `--ruby-versions "~> 3.0"` flag builds for Ruby 3.0–3.4 and skips 4.0. (Plain `--build`
350
+ > targets every Ruby in the image and fails on 4.0.)
351
+ > - **Don't launch from a symlinked path.** `rb-sys-dock` mounts the shell's `$(pwd)`
352
+ > (symlinks *not* resolved) but runs the container in the resolved path. If you reached the
353
+ > project through a symlink (e.g. `~/Sources -> …/Dropbox/AllSources`), the container's
354
+ > working dir ends up empty — surfacing as `No Rakefile found` or `Could not locate Gemfile`.
355
+ > `cd "$(pwd -P)"` first, or just use `rake build`.
356
+
357
+ A native gem for **your current machine only** (no Docker) is just:
358
+
359
+ ```bash
360
+ CARGO_TARGET_DIR="$HOME/.cargo-target/rdf-oxigraph" bundle exec rake native gem
361
+ # => pkg/rdf-oxigraph-<version>-<your-platform>.gem
362
+ ```
363
+
364
+ ## License
365
+
366
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,76 @@
1
+ module RDF
2
+ module Oxigraph
3
+ # Maps between RDF.rb terms and the native extension's tagged-tuple wire format.
4
+ #
5
+ # IRI: ["uri", "<iri>"]
6
+ # blank node: ["bnode", "<id>"]
7
+ # literal: ["lit", "<value>", <datatype-iri or nil>, <lang or nil>]
8
+ # nil (e.g. default graph): nil
9
+ module Conversion
10
+ module_function
11
+
12
+ # RDF.rb term -> tagged tuple (or nil).
13
+ def serialize(term)
14
+ case term
15
+ when nil
16
+ nil
17
+ when RDF::URI
18
+ ["uri", term.to_s]
19
+ when RDF::Node
20
+ ["bnode", term.id.to_s]
21
+ when RDF::Literal
22
+ if term.language? && !term.language.to_s.empty?
23
+ ["lit", term.value, nil, term.language.to_s]
24
+ else
25
+ ["lit", term.value, term.datatype.to_s, nil]
26
+ end
27
+ else
28
+ raise ArgumentError, "cannot serialize term: #{term.inspect}"
29
+ end
30
+ end
31
+
32
+ # tagged tuple (or nil) -> RDF.rb term.
33
+ def deserialize(tuple)
34
+ return nil if tuple.nil?
35
+
36
+ case tuple[0]
37
+ when "uri"
38
+ RDF::URI.new(tuple[1])
39
+ when "bnode"
40
+ RDF::Node.new(tuple[1])
41
+ when "lit"
42
+ value, datatype, lang = tuple[1], tuple[2], tuple[3]
43
+ if lang && !lang.empty?
44
+ RDF::Literal.new(value, language: lang.to_sym)
45
+ elsif datatype && !datatype.empty?
46
+ RDF::Literal.new(value, datatype: RDF::URI.new(datatype))
47
+ else
48
+ RDF::Literal.new(value)
49
+ end
50
+ else
51
+ raise ArgumentError, "cannot deserialize tuple: #{tuple.inspect}"
52
+ end
53
+ end
54
+
55
+ # RDF::Statement -> [s, p, o, g] tagged tuples.
56
+ def serialize_statement(statement)
57
+ [
58
+ serialize(statement.subject),
59
+ serialize(statement.predicate),
60
+ serialize(statement.object),
61
+ statement.graph_name ? serialize(statement.graph_name) : nil
62
+ ]
63
+ end
64
+
65
+ # [s, p, o, g] tagged tuples -> RDF::Statement.
66
+ def deserialize_statement(quad)
67
+ RDF::Statement.new(
68
+ deserialize(quad[0]),
69
+ deserialize(quad[1]),
70
+ deserialize(quad[2]),
71
+ graph_name: deserialize(quad[3])
72
+ )
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,117 @@
1
+ module RDF
2
+ module Oxigraph
3
+ # Native-format strings keyed by RDF.rb format symbol.
4
+ FORMATS = {
5
+ turtle: "turtle",
6
+ ntriples: "ntriples",
7
+ nquads: "nquads",
8
+ trig: "trig",
9
+ rdfxml: "rdfxml",
10
+ jsonld: "jsonld"
11
+ }.freeze
12
+
13
+ # Map an RDF.rb format symbol (or content type / file name) to the native
14
+ # oxigraph format string, or nil if unsupported.
15
+ def self.native_format(format)
16
+ return FORMATS[format] if format.is_a?(Symbol)
17
+
18
+ fmt = RDF::Format.for(format)
19
+ fmt && FORMATS[fmt.to_sym]
20
+ end
21
+
22
+ # Batch RDF::Reader backed by oxigraph's native parsers. Subclasses set the
23
+ # oxigraph format via `oxigraph_format`.
24
+ class Reader < RDF::Reader
25
+ def self.oxigraph_format(value = nil)
26
+ @oxigraph_format = value unless value.nil?
27
+ @oxigraph_format
28
+ end
29
+
30
+ def initialize(input = $stdin, **options, &block)
31
+ @content = input.respond_to?(:read) ? input.read : input.to_s
32
+ super
33
+ end
34
+
35
+ def each_statement
36
+ return enum_for(:each_statement) unless block_given?
37
+ RDF::Oxigraph.parse(@content, self.class.oxigraph_format).each do |quad|
38
+ yield Conversion.deserialize_statement(quad)
39
+ end
40
+ end
41
+ alias_method :each, :each_statement
42
+
43
+ def each_triple
44
+ return enum_for(:each_triple) unless block_given?
45
+ each_statement { |st| yield(st.subject, st.predicate, st.object) }
46
+ end
47
+ end
48
+
49
+ # Batch RDF::Writer backed by oxigraph's native serializers. Buffers statements
50
+ # and serializes once on epilogue. Subclasses set `oxigraph_format`.
51
+ class Writer < RDF::Writer
52
+ def self.oxigraph_format(value = nil)
53
+ @oxigraph_format = value unless value.nil?
54
+ @oxigraph_format
55
+ end
56
+
57
+ def initialize(output = $stdout, **options, &block)
58
+ @quads = []
59
+ super
60
+ end
61
+
62
+ def write_statement(statement)
63
+ @quads << Conversion.serialize_statement(statement)
64
+ statement
65
+ end
66
+
67
+ def write_triple(subject, predicate, object)
68
+ write_statement(RDF::Statement.new(subject, predicate, object))
69
+ end
70
+
71
+ def write_epilogue
72
+ @output.write(RDF::Oxigraph.serialize(@quads, self.class.oxigraph_format))
73
+ super
74
+ end
75
+ end
76
+
77
+ class TurtleReader < Reader; oxigraph_format "turtle"; end
78
+ class TurtleWriter < Writer; oxigraph_format "turtle"; end
79
+ class Turtle < RDF::Format
80
+ content_type "text/turtle", extension: :ttl
81
+ reader { TurtleReader }
82
+ writer { TurtleWriter }
83
+ end
84
+
85
+ class NTriplesReader < Reader; oxigraph_format "ntriples"; end
86
+ class NTriplesWriter < Writer; oxigraph_format "ntriples"; end
87
+ class NTriples < RDF::Format
88
+ content_type "application/n-triples", extension: :nt
89
+ reader { NTriplesReader }
90
+ writer { NTriplesWriter }
91
+ end
92
+
93
+ class NQuadsReader < Reader; oxigraph_format "nquads"; end
94
+ class NQuadsWriter < Writer; oxigraph_format "nquads"; end
95
+ class NQuads < RDF::Format
96
+ content_type "application/n-quads", extension: :nq
97
+ reader { NQuadsReader }
98
+ writer { NQuadsWriter }
99
+ end
100
+
101
+ class TriGReader < Reader; oxigraph_format "trig"; end
102
+ class TriGWriter < Writer; oxigraph_format "trig"; end
103
+ class TriG < RDF::Format
104
+ content_type "application/trig", extension: :trig
105
+ reader { TriGReader }
106
+ writer { TriGWriter }
107
+ end
108
+
109
+ class JsonLdReader < Reader; oxigraph_format "jsonld"; end
110
+ class JsonLdWriter < Writer; oxigraph_format "jsonld"; end
111
+ class JsonLd < RDF::Format
112
+ content_type "application/ld+json", extension: :jsonld
113
+ reader { JsonLdReader }
114
+ writer { JsonLdWriter }
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,97 @@
1
+ module RDF
2
+ module Oxigraph
3
+ # An RDF.rb-compatible {RDF::Repository} backed by the native oxigraph store.
4
+ #
5
+ # Implements the storage backend protocol (insert/delete/each/count) over the
6
+ # tagged-tuple bridge; the RDF::Enumerable/Queryable/Mutable mixins build the
7
+ # rest of the RDF.rb surface on top.
8
+ class Repository < RDF::Repository
9
+ def initialize(**options, &block)
10
+ @store = RDF::Oxigraph::Store.new
11
+ super(**options, &block)
12
+ end
13
+
14
+ def supports?(feature)
15
+ case feature.to_sym
16
+ when :graph_name then true # quad store
17
+ when :literal_equality then true
18
+ else false
19
+ end
20
+ end
21
+
22
+ def durable?
23
+ false # in-memory; persistent (RocksDB) variant comes later
24
+ end
25
+
26
+ def count
27
+ @store.size
28
+ end
29
+
30
+ def empty?
31
+ count.zero?
32
+ end
33
+
34
+ def each_statement(&block)
35
+ return enum_for(:each_statement) unless block_given?
36
+ @store.each_quad.each do |quad|
37
+ block.call(Conversion.deserialize_statement(quad))
38
+ end
39
+ end
40
+ alias_method :each, :each_statement
41
+
42
+ def insert_statement(statement)
43
+ raise ArgumentError, "incomplete statement: #{statement.inspect}" if statement.incomplete?
44
+ s, p, o, g = Conversion.serialize_statement(statement)
45
+ @store.insert(s, p, o, g)
46
+ end
47
+
48
+ def delete_statement(statement)
49
+ s, p, o, g = Conversion.serialize_statement(statement)
50
+ @store.delete(s, p, o, g)
51
+ end
52
+
53
+ # Fast bulk load via oxigraph's native parser (skips per-statement Ruby
54
+ # inserts). Falls back to RDF::Mutable#load for unsupported formats or when
55
+ # a graph_name override is requested.
56
+ def load(filename, **options)
57
+ format = options[:format] || RDF::Format.for(file_name: filename.to_s)&.to_sym
58
+ native = RDF::Oxigraph::FORMATS[format]
59
+ return super if native.nil? || options[:graph_name]
60
+ @store.load(File.read(filename), native)
61
+ self
62
+ end
63
+
64
+ # Fast native serialization of the whole repository for supported formats;
65
+ # falls back to RDF::Enumerable#dump otherwise.
66
+ def dump(format, **options)
67
+ native = format.is_a?(Symbol) && RDF::Oxigraph::FORMATS[format]
68
+ return super if native == false || native.nil?
69
+ @store.dump(native)
70
+ end
71
+
72
+ # Execute a SPARQL query string via oxigraph's native engine. Returns
73
+ # RDF.rb-native results: RDF::Query::Solutions (SELECT), RDF::Graph
74
+ # (CONSTRUCT/DESCRIBE), or true/false (ASK).
75
+ def sparql(query_string)
76
+ RDF::Oxigraph::SPARQL.results(@store.query(query_string))
77
+ end
78
+
79
+ # Execute a SPARQL 1.1 Update string against this repository.
80
+ def sparql_update(update_string)
81
+ @store.update(update_string)
82
+ self
83
+ end
84
+
85
+ # Validate this repository's data against SHACL `shapes` (a serialized-RDF
86
+ # string). Returns an RDF::Oxigraph::SHACL::Report.
87
+ def shacl_validate(shapes, shapes_format: :turtle)
88
+ RDF::Oxigraph::SHACL.validate(
89
+ data: @store.dump("ntriples"),
90
+ data_format: :ntriples,
91
+ shapes: shapes,
92
+ shapes_format: shapes_format
93
+ )
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,46 @@
1
+ module RDF
2
+ module Oxigraph
3
+ # In-process SHACL validation, backed by the rudof `shacl` engine in the native
4
+ # extension. Decoupled from the store via serialized strings.
5
+ module SHACL
6
+ module_function
7
+
8
+ # Validate a data graph against SHACL shapes. Both `data` and `shapes` are
9
+ # strings of serialized RDF (default Turtle), or anything that responds to
10
+ # #to_s. Returns a {Report}.
11
+ def validate(data:, shapes:, data_format: :turtle, shapes_format: :turtle)
12
+ raw = RDF::Oxigraph.shacl_validate(
13
+ data.to_s, data_format.to_s, shapes.to_s, shapes_format.to_s
14
+ )
15
+ Report.new(raw)
16
+ end
17
+
18
+ # A SHACL validation report. `#results` is an array of hashes with string keys:
19
+ # focus, path, value, source, severity, message.
20
+ class Report
21
+ attr_reader :raw
22
+
23
+ def initialize(raw)
24
+ @raw = raw
25
+ end
26
+
27
+ def conforms?
28
+ @raw["conforms"]
29
+ end
30
+
31
+ def violations
32
+ @raw["violations"]
33
+ end
34
+
35
+ def results
36
+ @raw["results"]
37
+ end
38
+
39
+ # Human-readable validation report.
40
+ def to_s
41
+ @raw["text"]
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,66 @@
1
+ module RDF
2
+ module Oxigraph
3
+ module SPARQL
4
+ module_function
5
+
6
+ # Convert a native Store#query tagged result into RDF.rb-native objects:
7
+ # ["boolean", bool] -> true / false
8
+ # ["solutions", vars, rows] -> RDF::Query::Solutions
9
+ # ["graph", triples] -> RDF::Graph
10
+ def results(tagged)
11
+ case tagged[0]
12
+ when "boolean"
13
+ tagged[1]
14
+ when "solutions"
15
+ _, vars, rows = tagged
16
+ solutions = RDF::Query::Solutions.new
17
+ rows.each do |row|
18
+ bindings = {}
19
+ vars.each_with_index do |var, i|
20
+ term = Conversion.deserialize(row[i])
21
+ bindings[var.to_sym] = term unless term.nil?
22
+ end
23
+ solutions << RDF::Query::Solution.new(bindings)
24
+ end
25
+ solutions
26
+ when "graph"
27
+ graph = RDF::Graph.new
28
+ tagged[1].each do |triple|
29
+ graph << RDF::Statement.new(
30
+ Conversion.deserialize(triple[0]),
31
+ Conversion.deserialize(triple[1]),
32
+ Conversion.deserialize(triple[2])
33
+ )
34
+ end
35
+ graph
36
+ else
37
+ raise ArgumentError, "unknown query result kind: #{tagged[0].inspect}"
38
+ end
39
+ end
40
+
41
+ # A small `SPARQL::Client`-shaped wrapper around a repository (anything that
42
+ # responds to #sparql and #sparql_update). Covers the string-based query/update
43
+ # surface; the chainable query-builder DSL of the sparql-client gem is out of scope.
44
+ class Client
45
+ attr_reader :repository
46
+
47
+ def initialize(repository)
48
+ @repository = repository
49
+ end
50
+
51
+ # Execute a SPARQL query string. `update: true` routes to #update (mirrors
52
+ # the sparql-client gem's convention used by some callers).
53
+ def query(query_string, **options)
54
+ return update(query_string) if options[:update]
55
+ @repository.sparql(query_string)
56
+ end
57
+
58
+ # Execute a SPARQL Update string.
59
+ def update(update_string, **)
60
+ @repository.sparql_update(update_string)
61
+ self
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,5 @@
1
+ module RDF
2
+ module Oxigraph
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
@@ -0,0 +1,12 @@
1
+ require "rdf"
2
+ require_relative "oxigraph/version"
3
+
4
+ # Load the compiled native extension (built into lib/rdf/oxigraph/ by rake-compiler).
5
+ # Defines RDF::Oxigraph::Store.
6
+ require "rdf/oxigraph/rdf_oxigraph"
7
+
8
+ require_relative "oxigraph/conversion"
9
+ require_relative "oxigraph/format"
10
+ require_relative "oxigraph/sparql"
11
+ require_relative "oxigraph/shacl"
12
+ require_relative "oxigraph/repository"
@@ -0,0 +1,43 @@
1
+ require_relative "lib/rdf/oxigraph/version"
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "rdf-oxigraph"
5
+ spec.version = RDF::Oxigraph::VERSION
6
+ spec.authors = ["Mehmet Celik"]
7
+ spec.email = ["mehmet@celik.be"]
8
+ spec.summary = "Drop-in RDF.rb-compatible storage/SPARQL backed by oxigraph (Rust)"
9
+ spec.description = "A Ruby RDF.rb backend (RDF::Repository + SPARQL::Client-compatible) " \
10
+ "backed by the oxigraph Rust engine via a magnus/rb-sys native extension."
11
+ spec.homepage = "https://github.com/libis/rdf-oxigraph"
12
+ spec.license = "MIT"
13
+ spec.required_ruby_version = ">= 3.0.0"
14
+ spec.required_rubygems_version = ">= 3.3.11"
15
+
16
+ spec.metadata = {
17
+ "source_code_uri" => spec.homepage,
18
+ "changelog_uri" => "#{spec.homepage}/blob/main/CHANGELOG.md",
19
+ "rubygems_mfa_required" => "true"
20
+ }
21
+
22
+ spec.files = Dir[
23
+ "lib/**/*.rb",
24
+ "ext/**/*.{rs,toml,rb,lock}",
25
+ "Cargo.toml",
26
+ "Cargo.lock",
27
+ "*.gemspec",
28
+ "README.md",
29
+ "CHANGELOG.md",
30
+ "LICENSE*"
31
+ ]
32
+ spec.require_paths = ["lib"]
33
+ spec.extensions = ["ext/rdf_oxigraph/extconf.rb"]
34
+
35
+ spec.add_dependency "rdf", "~> 3.3"
36
+ spec.add_dependency "rb_sys", "~> 0.9"
37
+
38
+ spec.add_development_dependency "rake", "~> 13.0"
39
+ spec.add_development_dependency "rake-compiler", "~> 1.2"
40
+ spec.add_development_dependency "minitest", "~> 5.16" # main test suite
41
+ spec.add_development_dependency "rspec", "~> 3.0" # rdf-spec conformance only
42
+ spec.add_development_dependency "rdf-spec", "~> 3.3" # rdf-spec conformance only
43
+ end
metadata ADDED
@@ -0,0 +1,150 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rdf-oxigraph
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: x86_64-linux
6
+ authors:
7
+ - Mehmet Celik
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-08-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rdf
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.3'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '13.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '13.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake-compiler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.2'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.2'
55
+ - !ruby/object:Gem::Dependency
56
+ name: minitest
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '5.16'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '5.16'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rdf-spec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '3.3'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '3.3'
97
+ description: A Ruby RDF.rb backend (RDF::Repository + SPARQL::Client-compatible) backed
98
+ by the oxigraph Rust engine via a magnus/rb-sys native extension.
99
+ email:
100
+ - mehmet@celik.be
101
+ executables: []
102
+ extensions: []
103
+ extra_rdoc_files: []
104
+ files:
105
+ - CHANGELOG.md
106
+ - LICENSE
107
+ - README.md
108
+ - lib/rdf/oxigraph.rb
109
+ - lib/rdf/oxigraph/3.0/rdf_oxigraph.so
110
+ - lib/rdf/oxigraph/3.1/rdf_oxigraph.so
111
+ - lib/rdf/oxigraph/3.2/rdf_oxigraph.so
112
+ - lib/rdf/oxigraph/3.3/rdf_oxigraph.so
113
+ - lib/rdf/oxigraph/3.4/rdf_oxigraph.so
114
+ - lib/rdf/oxigraph/conversion.rb
115
+ - lib/rdf/oxigraph/format.rb
116
+ - lib/rdf/oxigraph/repository.rb
117
+ - lib/rdf/oxigraph/shacl.rb
118
+ - lib/rdf/oxigraph/sparql.rb
119
+ - lib/rdf/oxigraph/version.rb
120
+ - rdf-oxigraph.gemspec
121
+ homepage: https://github.com/libis/rdf-oxigraph
122
+ licenses:
123
+ - MIT
124
+ metadata:
125
+ source_code_uri: https://github.com/libis/rdf-oxigraph
126
+ changelog_uri: https://github.com/libis/rdf-oxigraph/blob/main/CHANGELOG.md
127
+ rubygems_mfa_required: 'true'
128
+ post_install_message:
129
+ rdoc_options: []
130
+ require_paths:
131
+ - lib
132
+ required_ruby_version: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - ">="
135
+ - !ruby/object:Gem::Version
136
+ version: '3.0'
137
+ - - "<"
138
+ - !ruby/object:Gem::Version
139
+ version: 3.5.dev
140
+ required_rubygems_version: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: 3.3.11
145
+ requirements: []
146
+ rubygems_version: 3.5.23
147
+ signing_key:
148
+ specification_version: 4
149
+ summary: Drop-in RDF.rb-compatible storage/SPARQL backed by oxigraph (Rust)
150
+ test_files: []