rdf-oxigraph 0.0.1-aarch64-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: a4faf077f870ca9b98acfe66f0057b384e147a949362d922bb7a00b01a3b7272
4
+ data.tar.gz: 402579a24d56238605cc2a72a52fc0c3d3d3288b8ebcbc7f831c8cf2b56ff743
5
+ SHA512:
6
+ metadata.gz: 8e984a16412f8411c32520f8f7f3729c621c24247c0895e178fcb8975697112e44819ed79cf148574875be6bef5434272f866e8d982c8ae6ea29a240f05cd96c
7
+ data.tar.gz: bc899482e7f9b4d457f12719c9518fa86e9860ea7f5cdad1cac5708007a9015c212d404a103901efa7e54cae10f18a9e87daf6974f78467e0dc04f63f22e91f1
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,380 @@
1
+ # rdf-oxigraph
2
+ 🤖 Claude (Anthropic) helped writing this library.
3
+
4
+ A Ruby gem providing a **drop-in [RDF.rb](https://github.com/ruby-rdf/rdf)-compatible**
5
+ storage + SPARQL backend powered by the
6
+ [oxigraph](https://github.com/oxigraph/oxigraph) Rust engine, via a
7
+ [magnus](https://github.com/matsadler/magnus) + [rb-sys](https://github.com/oxidize-rb/rb-sys)
8
+ native extension.
9
+
10
+ ## Why
11
+
12
+ `RDF::Repository` ships with rdf.rb as a pure-Ruby, in-memory store, and SPARQL on top of
13
+ it (the `sparql` gem) is a pure-Ruby query engine. That's portable but slow, and SPARQL 1.1
14
+ coverage is uneven.
15
+
16
+ **rdf-oxigraph is a drop-in replacement for that backend.** It swaps the pure-Ruby store and
17
+ query engine for oxigraph — a fast, embedded, native **SPARQL 1.1** engine written in Rust —
18
+ while keeping the exact RDF.rb surface your code already uses. Existing rdf.rb /
19
+ `SPARQL::Client` consumers get the native engine **without changing their
20
+ code**: the only edit is which `Repository` class you instantiate.
21
+
22
+ What you get over the stock backend:
23
+
24
+ - Native SPARQL 1.1 **query + update** (SELECT/ASK/CONSTRUCT/DESCRIBE, full FILTER/REGEX/PREFIX).
25
+ - Native, fast **parse/serialize** for Turtle, TriG, N-Triples, N-Quads, RDF/XML, JSON-LD.
26
+ - In-process **SHACL validation** (oxigraph has none; provided via the rudof `shacl` crate).
27
+ - **Precompiled gems** — no Rust toolchain needed to install on the common platforms.
28
+
29
+ ## Installation
30
+
31
+ Add to your `Gemfile`:
32
+
33
+ ```ruby
34
+ gem "rdf-oxigraph"
35
+ ```
36
+
37
+ or:
38
+
39
+ ```bash
40
+ gem install rdf-oxigraph
41
+ ```
42
+
43
+ Precompiled, platform-specific gems are published, so **no Rust toolchain is required to
44
+ install** on:
45
+
46
+ | Platform | Targets |
47
+ |----------|---------|
48
+ | Linux (glibc) | `x86_64-linux`, `aarch64-linux` |
49
+ | Linux (musl, Alpine, …) | `x86_64-linux-musl`, `aarch64-linux-musl` |
50
+ | macOS | `arm64-darwin` (Apple Silicon), `x86_64-darwin` (Intel) |
51
+
52
+ If no precompiled gem matches your platform, the gem falls back to **compiling from source**,
53
+ which requires a Rust toolchain (rustup recommended), Ruby headers, and clang/libclang.
54
+
55
+ > The extension uses oxigraph's **in-memory** store only (oxigraph built with
56
+ > `default-features = false`, no RocksDB / no C++). This keeps the gem small (~2.5 MB) and the
57
+ > Linux glibc/musl cross-builds reliable. On-disk persistence (`Store::open`) is intentionally
58
+ > not enabled — re-enable oxigraph's `rocksdb` feature in `ext/rdf_oxigraph/Cargo.toml` if you
59
+ > need it.
60
+
61
+ ## Architecture — how it fits into rdf.rb
62
+
63
+ rdf.rb defines the *interface* (the `RDF::Enumerable` / `RDF::Queryable` / `RDF::Mutable`
64
+ mixins, plus the `RDF::Reader` / `RDF::Writer` / `RDF::Format` registries). The slow parts
65
+ are the *implementations*: the pure-Ruby in-memory store and the pure-Ruby SPARQL engine.
66
+
67
+ rdf-oxigraph keeps the interfaces and replaces the implementations. Your code talks to the
68
+ same rdf.rb objects (`RDF::Statement`, `RDF::Query::Solutions`, `RDF::Graph`); underneath,
69
+ calls cross a thin tagged-tuple bridge into the native oxigraph engine.
70
+
71
+ ```mermaid
72
+ flowchart TD
73
+ App["Your app<br/>(plain RDF.rb code)"]
74
+
75
+ subgraph rdfrb["rdf.rb (interfaces — unchanged)"]
76
+ Mixins["RDF::Enumerable / Queryable / Mutable"]
77
+ Registry["RDF::Reader / Writer / Format registry"]
78
+ Terms["RDF::Statement · Query::Solutions · Graph"]
79
+ end
80
+
81
+ subgraph gem["rdf-oxigraph (this gem, Ruby)"]
82
+ Repo["RDF::Oxigraph::Repository<br/>< RDF::Repository"]
83
+ RW["Oxigraph Reader / Writer / Format<br/>(turtle, trig, n-triples, n-quads, rdf/xml, json-ld)"]
84
+ Sparql["SPARQL.results + SPARQL::Client"]
85
+ Shacl["SHACL.validate → Report"]
86
+ Conv["Conversion<br/>(RDF.rb terms ⇄ tagged tuples)"]
87
+ end
88
+
89
+ subgraph native["native extension (Rust, via magnus + rb-sys)"]
90
+ Store["RDF::Oxigraph::Store"]
91
+ Oxi["oxigraph 0.5<br/>(in-memory store + SPARQL 1.1)"]
92
+ Rudof["rudof shacl crate"]
93
+ end
94
+
95
+ App --> Mixins
96
+ App --> Registry
97
+ App --> Terms
98
+
99
+ Mixins --> Repo
100
+ Registry --> RW
101
+ Repo --> Sparql
102
+ Repo --> Shacl
103
+ Repo --> Conv
104
+ RW --> Conv
105
+
106
+ Conv <--> Store
107
+ Sparql --> Store
108
+ Shacl --> Rudof
109
+ Store --> Oxi
110
+ ```
111
+
112
+ The native boundary is deliberately narrow: terms are marshalled as small tagged tuples
113
+ (e.g. `["uri", "..."]`, `["lit", value, datatype, lang]`), and SHACL crosses the boundary
114
+ only as serialized RDF strings.
115
+
116
+ ## Usage
117
+
118
+ ### Do I have to use `RDF::Oxigraph::Repository`?
119
+
120
+ **Yes — instantiating `RDF::Oxigraph::Repository` (or the lower-level
121
+ `RDF::Oxigraph::Store`) is the one and only thing you change.** That object *is* the
122
+ oxigraph-backed store; without it, your data lives in the stock pure-Ruby store and none of the
123
+ native speedups apply. Everything else stays standard rdf.rb:
124
+
125
+ ```ruby
126
+ require "rdf/oxigraph"
127
+
128
+ repo = RDF::Oxigraph::Repository.new # <-- the only oxigraph-specific line
129
+
130
+ # ...from here on it's plain RDF.rb — identical to stock RDF::Repository:
131
+ repo << RDF::Statement.new(subject, predicate, object)
132
+ repo.query([nil, RDF.type, RDF::URI("http://example.org/Person")])
133
+ repo.count
134
+ ```
135
+
136
+ Two nuances:
137
+
138
+ - **Readers/Writers register globally.** Once `rdf/oxigraph` is required, the oxigraph-backed
139
+ Turtle/TriG/N-Triples/N-Quads/RDF-XML/JSON-LD readers/writers are registered in rdf.rb's
140
+ format registry. `RDF::Reader.for(...)` / `RDF::Writer.for(...)` and `Repository#load` /
141
+ `#dump` then use the native parsers — no extra wiring.
142
+ - **SPARQL needs the repository.** `repo.sparql(...)`, `repo.sparql_update(...)`, and the
143
+ `RDF::Oxigraph::SPARQL::Client` wrapper all run against the oxigraph store, so they require
144
+ the oxigraph repository.
145
+
146
+ ### Drop-in repository
147
+
148
+ The same rdf.rb code runs unchanged against the stock store and the oxigraph store with
149
+ identical results:
150
+
151
+ ```ruby
152
+ require "rdf/oxigraph"
153
+
154
+ EX = RDF::Vocabulary.new("http://example.org/")
155
+ repo = RDF::Oxigraph::Repository.new
156
+ repo << RDF::Statement.new(EX.alice, RDF.type, EX.Person)
157
+ repo << RDF::Statement.new(EX.alice, EX.name, RDF::Literal("Alice"))
158
+
159
+ repo.query([nil, RDF.type, EX.Person]).each { |s| puts s.subject }
160
+ ```
161
+
162
+ ### Load & serialize (native parse/serialize)
163
+
164
+ ```ruby
165
+ repo = RDF::Oxigraph::Repository.new
166
+ repo.load("data.ttl") # native Turtle parse, straight into the store
167
+ puts repo.dump(:ntriples) # native serialization
168
+ puts repo.dump(:jsonld)
169
+
170
+ RDF::Reader.for(content_type: "text/turtle") # => oxigraph-backed reader
171
+ RDF::Writer.for(:ntriples) # => oxigraph-backed writer
172
+ ```
173
+
174
+ ### SPARQL query + update
175
+
176
+ ```ruby
177
+ repo = RDF::Oxigraph::Repository.new
178
+ repo.load("data.ttl")
179
+
180
+ # SELECT -> RDF::Query::Solutions
181
+ repo.sparql("SELECT ?s WHERE { ?s a <http://example.org/Book> }").each { |sol| puts sol[:s] }
182
+
183
+ # ASK -> true/false ; CONSTRUCT/DESCRIBE -> RDF::Graph
184
+ repo.sparql("ASK { ?s ?p ?o }")
185
+ graph = repo.sparql("CONSTRUCT { ?a ?b ?c } WHERE { ?a ?b ?c }")
186
+
187
+ # SPARQL 1.1 Update
188
+ repo.sparql_update("INSERT DATA { <http://example.org/x> a <http://example.org/Book> }")
189
+
190
+ # SPARQL::Client-shaped wrapper (string-based query/update)
191
+ client = RDF::Oxigraph::SPARQL::Client.new(repo)
192
+ client.query("SELECT (COUNT(?s) AS ?n) WHERE { ?s ?p ?o }")
193
+ ```
194
+
195
+ ### SHACL validation
196
+
197
+ oxigraph itself has no SHACL; validation is provided in-process by the rudof
198
+ [`shacl`](https://crates.io/crates/shacl) crate (native, no external service):
199
+
200
+ ```ruby
201
+ report = repo.shacl_validate(shapes_turtle_string) # validate the repo's data
202
+ report.conforms? # => true / false
203
+ report.violations # => count
204
+ report.results # => [{ "focus" => ..., "severity" => ..., "message" => ..., "path" => ... }, ...]
205
+ puts report.to_s # human-readable report
206
+
207
+ # or validate two RDF strings directly, no repository needed:
208
+ RDF::Oxigraph::SHACL.validate(data: data_ttl, shapes: shapes_ttl)
209
+ ```
210
+
211
+ ### Runnable examples
212
+
213
+ The [`examples/`](examples/) directory has self-contained scripts (shared dataset in
214
+ `examples/data/books.ttl`). After `rake compile` (or installing the gem):
215
+
216
+ ```bash
217
+ ruby -Ilib examples/01_drop_in_repository.rb # drop-in: same code, stock vs oxigraph
218
+ ruby -Ilib examples/02_load_and_serialize.rb # native load + serialize, format registry
219
+ ruby -Ilib examples/03_sparql.rb # SELECT/ASK/CONSTRUCT + UPDATE + client
220
+ ruby -Ilib examples/04_shacl.rb # SHACL validation
221
+ ```
222
+
223
+ (Drop the `-Ilib` once the gem is installed.)
224
+
225
+ ## Conformance & known deviations
226
+
227
+ `RDF::Oxigraph::Repository` passes **319/338** of the official `rdf-spec` `RDF::Repository`
228
+ shared examples (`rake conformance`). The other 19 stem from a single, documented backend
229
+ behavior, and are marked **pending** (see [spec/known_deviations.rb](spec/known_deviations.rb))
230
+ so the suite runs green:
231
+
232
+ - **oxigraph canonicalizes typed literals.** `"01"^^xsd:integer` is stored as
233
+ `"1"^^xsd:integer`, and `"1.0e0"^^xsd:double` as its canonical form. rdf-spec treats
234
+ distinct lexical forms as distinct terms, so the lexical-preservation / term-count /
235
+ value-pattern tests fail. This is semantically reasonable (both denote the same value) and
236
+ is harmless.
237
+ - `#dump` differs only in auto-assigned blank-node labels (the triples are identical).
238
+
239
+ These are inherent to oxigraph's value-based storage and are not fixable in the gem.
240
+
241
+ Marking them pending rather than ignoring the failures keeps the suite honest in both
242
+ directions:
243
+
244
+ - if a pending example ever **starts passing**, RSpec reports it as a failure
245
+ (`Expected pending ... to fail`) — delete the entry from `known_deviations.rb`;
246
+ - if an entry stops matching because rdf-spec renamed the example, that example is no
247
+ longer marked pending and fails normally, and `spec_helper.rb` prints a warning naming
248
+ the stale entry.
249
+
250
+ So a real regression still turns CI red — only these 19 documented deviations are excused.
251
+
252
+ ## Tests
253
+
254
+ Two suites:
255
+
256
+ - **Minitest** — the main suite and the default `rake` task:
257
+
258
+ ```bash
259
+ CARGO_TARGET_DIR="$HOME/.cargo-target/rdf-oxigraph" bundle exec rake test
260
+ ```
261
+
262
+ - **RSpec** — runs only the `rdf-spec` conformance shared examples (which are RSpec-only):
263
+
264
+ ```bash
265
+ CARGO_TARGET_DIR="$HOME/.cargo-target/rdf-oxigraph" bundle exec rake conformance
266
+ ```
267
+
268
+ Both are green: `rake conformance` reports `0 failures, 19 pending`, those 19 being the
269
+ documented deviations (see
270
+ [Conformance & known deviations](#conformance--known-deviations)). Both tasks depend on
271
+ `:compile`, so they rebuild the native extension first. Setting `CARGO_TARGET_DIR` keeps Rust
272
+ build artifacts out of Dropbox/source control (optional but recommended).
273
+
274
+ CI (`.github/workflows/ci.yml`) runs both suites across Ruby 3.1–3.4 on Linux and macOS.
275
+
276
+ ## Compile (development)
277
+
278
+ ```bash
279
+ bundle install
280
+
281
+ # Build the native extension into lib/rdf/oxigraph/:
282
+ CARGO_TARGET_DIR="$HOME/.cargo-target/rdf-oxigraph" bundle exec rake compile
283
+
284
+ # Smoke test:
285
+ ruby -Ilib -e 'require "rdf/oxigraph"; \
286
+ r = RDF::Oxigraph::Repository.new; \
287
+ r.sparql_update("INSERT DATA { <http://ex/a> <http://ex/b> <http://ex/c> }"); \
288
+ puts r.sparql("ASK { ?s ?p ?o }")'
289
+ ```
290
+
291
+ Requirements for compiling from source: Ruby >= 3.0 with headers, a Rust toolchain (rustup
292
+ recommended), and clang/libclang.
293
+
294
+ ## Building & publishing the gem (Intel + ARM)
295
+
296
+ The pure-Ruby `gem build` produces a source gem (Rust compiled at install time). To ship
297
+ **precompiled, platform-specific** gems so users don't need Rust, build one gem per platform.
298
+
299
+ ### Recommended: GitHub Actions (all platforms)
300
+
301
+ The [`precompile`](.github/workflows/precompile.yml) workflow uses
302
+ [`oxidize-rb/actions/cross-gem`](https://github.com/oxidize-rb/actions) to cross-compile gems
303
+ for every supported platform (Linux x86_64/arm64 glibc + musl, macOS arm64/x86_64) across Ruby
304
+ 3.1–3.4. It triggers on a version tag or manually:
305
+
306
+ ```bash
307
+ # Tag a release -> the workflow builds every platform gem and uploads them as artifacts:
308
+ git tag v0.0.1
309
+ git push origin v0.0.1
310
+ ```
311
+
312
+ Download the artifacts and push them:
313
+
314
+ ```bash
315
+ gem push rdf-oxigraph-0.0.1-arm64-darwin.gem
316
+ gem push rdf-oxigraph-0.0.1-x86_64-darwin.gem
317
+ gem push rdf-oxigraph-0.0.1-x86_64-linux.gem
318
+ gem push rdf-oxigraph-0.0.1-aarch64-linux.gem
319
+ gem push rdf-oxigraph-0.0.1-x86_64-linux-musl.gem
320
+ gem push rdf-oxigraph-0.0.1-aarch64-linux-musl.gem
321
+ ```
322
+
323
+ > The gemspec sets `rubygems_mfa_required`, so `gem push` will prompt for your RubyGems OTP.
324
+
325
+ ### Local cross-compilation (Intel + ARM)
326
+
327
+ Cross-compilation is Docker-based, via
328
+ [`rb-sys-dock`](https://github.com/oxidize-rb/rb-sys) — no per-platform machines needed. It
329
+ requires Docker (or Podman) running locally.
330
+
331
+ #### Easiest: `rake build` (interactive)
332
+
333
+ ```bash
334
+ bundle exec rake build
335
+ ```
336
+
337
+ This wraps `rb-sys-dock` with a menu: it lists every supported target, builds the one you
338
+ pick, and loops until you quit. It also handles the two footguns described below for you
339
+ (corrects symlinked paths, pins Ruby to `~> 3.0`). Override the Ruby pin with
340
+ `RUBY_VERSIONS=... bundle exec rake build` if you need to.
341
+
342
+ #### Manual: `rb-sys-dock` per platform
343
+
344
+ ```bash
345
+ # macOS — Apple Silicon (arm64) and Intel (x86_64):
346
+ bundle exec rb-sys-dock --platform arm64-darwin --ruby-versions "~> 3.0" --build
347
+ bundle exec rb-sys-dock --platform x86_64-darwin --ruby-versions "~> 3.0" --build
348
+
349
+ # Linux — Intel + ARM (glibc and musl):
350
+ bundle exec rb-sys-dock --platform x86_64-linux --ruby-versions "~> 3.0" --build
351
+ bundle exec rb-sys-dock --platform aarch64-linux --ruby-versions "~> 3.0" --build
352
+ bundle exec rb-sys-dock --platform x86_64-linux-musl --ruby-versions "~> 3.0" --build
353
+ bundle exec rb-sys-dock --platform aarch64-linux-musl --ruby-versions "~> 3.0" --build
354
+ ```
355
+
356
+ Each build drops a `pkg/rdf-oxigraph-<version>-<platform>.gem`; push each with `gem push` as
357
+ above.
358
+
359
+ > **Two gotchas** (both handled automatically by `rake build`):
360
+ >
361
+ > - **Pin Ruby to `~> 3.0`.** The cross-compile image also ships a Ruby 4.0 target, but
362
+ > `magnus 0.7` cannot compile against it (`no field 'typed_flag' on type RTypedData`). The
363
+ > `--ruby-versions "~> 3.0"` flag builds for Ruby 3.0–3.4 and skips 4.0. (Plain `--build`
364
+ > targets every Ruby in the image and fails on 4.0.)
365
+ > - **Don't launch from a symlinked path.** `rb-sys-dock` mounts the shell's `$(pwd)`
366
+ > (symlinks *not* resolved) but runs the container in the resolved path. If you reached the
367
+ > project through a symlink (e.g. `~/Sources -> …/Dropbox/AllSources`), the container's
368
+ > working dir ends up empty — surfacing as `No Rakefile found` or `Could not locate Gemfile`.
369
+ > `cd "$(pwd -P)"` first, or just use `rake build`.
370
+
371
+ A native gem for **your current machine only** (no Docker) is just:
372
+
373
+ ```bash
374
+ CARGO_TARGET_DIR="$HOME/.cargo-target/rdf-oxigraph" bundle exec rake native gem
375
+ # => pkg/rdf-oxigraph-<version>-<your-platform>.gem
376
+ ```
377
+
378
+ ## License
379
+
380
+ 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,118 @@
1
+ module RDF
2
+ module Oxigraph
3
+ # Native-format strings keyed by RDF.rb format symbol.
4
+ FORMATS = {
5
+ turtle: "turtle",
6
+ ttl: "turtle",
7
+ ntriples: "ntriples",
8
+ nquads: "nquads",
9
+ trig: "trig",
10
+ rdfxml: "rdfxml",
11
+ jsonld: "jsonld"
12
+ }.freeze
13
+
14
+ # Map an RDF.rb format symbol (or content type / file name) to the native
15
+ # oxigraph format string, or nil if unsupported.
16
+ def self.native_format(format)
17
+ return FORMATS[format] if format.is_a?(Symbol)
18
+
19
+ fmt = RDF::Format.for(format)
20
+ fmt && FORMATS[fmt.to_sym]
21
+ end
22
+
23
+ # Batch RDF::Reader backed by oxigraph's native parsers. Subclasses set the
24
+ # oxigraph format via `oxigraph_format`.
25
+ class Reader < RDF::Reader
26
+ def self.oxigraph_format(value = nil)
27
+ @oxigraph_format = value unless value.nil?
28
+ @oxigraph_format
29
+ end
30
+
31
+ def initialize(input = $stdin, **options, &block)
32
+ @content = input.respond_to?(:read) ? input.read : input.to_s
33
+ super
34
+ end
35
+
36
+ def each_statement
37
+ return enum_for(:each_statement) unless block_given?
38
+ RDF::Oxigraph.parse(@content, self.class.oxigraph_format).each do |quad|
39
+ yield Conversion.deserialize_statement(quad)
40
+ end
41
+ end
42
+ alias_method :each, :each_statement
43
+
44
+ def each_triple
45
+ return enum_for(:each_triple) unless block_given?
46
+ each_statement { |st| yield(st.subject, st.predicate, st.object) }
47
+ end
48
+ end
49
+
50
+ # Batch RDF::Writer backed by oxigraph's native serializers. Buffers statements
51
+ # and serializes once on epilogue. Subclasses set `oxigraph_format`.
52
+ class Writer < RDF::Writer
53
+ def self.oxigraph_format(value = nil)
54
+ @oxigraph_format = value unless value.nil?
55
+ @oxigraph_format
56
+ end
57
+
58
+ def initialize(output = $stdout, **options, &block)
59
+ @quads = []
60
+ super
61
+ end
62
+
63
+ def write_statement(statement)
64
+ @quads << Conversion.serialize_statement(statement)
65
+ statement
66
+ end
67
+
68
+ def write_triple(subject, predicate, object)
69
+ write_statement(RDF::Statement.new(subject, predicate, object))
70
+ end
71
+
72
+ def write_epilogue
73
+ @output.write(RDF::Oxigraph.serialize(@quads, self.class.oxigraph_format))
74
+ super
75
+ end
76
+ end
77
+
78
+ class TurtleReader < Reader; oxigraph_format "turtle"; end
79
+ class TurtleWriter < Writer; oxigraph_format "turtle"; end
80
+ class Turtle < RDF::Format
81
+ content_type "text/turtle", extension: :ttl
82
+ reader { TurtleReader }
83
+ writer { TurtleWriter }
84
+ end
85
+
86
+ class NTriplesReader < Reader; oxigraph_format "ntriples"; end
87
+ class NTriplesWriter < Writer; oxigraph_format "ntriples"; end
88
+ class NTriples < RDF::Format
89
+ content_type "application/n-triples", extension: :nt
90
+ reader { NTriplesReader }
91
+ writer { NTriplesWriter }
92
+ end
93
+
94
+ class NQuadsReader < Reader; oxigraph_format "nquads"; end
95
+ class NQuadsWriter < Writer; oxigraph_format "nquads"; end
96
+ class NQuads < RDF::Format
97
+ content_type "application/n-quads", extension: :nq
98
+ reader { NQuadsReader }
99
+ writer { NQuadsWriter }
100
+ end
101
+
102
+ class TriGReader < Reader; oxigraph_format "trig"; end
103
+ class TriGWriter < Writer; oxigraph_format "trig"; end
104
+ class TriG < RDF::Format
105
+ content_type "application/trig", extension: :trig
106
+ reader { TriGReader }
107
+ writer { TriGWriter }
108
+ end
109
+
110
+ class JsonLdReader < Reader; oxigraph_format "jsonld"; end
111
+ class JsonLdWriter < Writer; oxigraph_format "jsonld"; end
112
+ class JsonLd < RDF::Format
113
+ content_type "application/ld+json", extension: :jsonld
114
+ reader { JsonLdReader }
115
+ writer { JsonLdWriter }
116
+ end
117
+ end
118
+ 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: aarch64-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: []