shacl2cypher
Guide

From a shapes file to a validation report

Install the CLI, write three constraints, compile them, and run them against a database. This takes about ten minutes.

  1. Install

    Prebuilt binaries with both database backends are attached to every GitHub release, for Linux x86_64, Linux arm64 and macOS arm64. Each tarball has a SHA-256 checksum next to it. With cargo:

    cargo install --locked shacl2cypher                              # compile only
    cargo install --locked shacl2cypher --features neo4j,ladybug     # with database backends

    The ladybug feature builds C++ and needs cmake. --locked installs the dependency versions the release was tested with; Rust 1.87 or newer is required.

    To work from Python or Node.js instead, the packages bundle both backends:

    pip install shacl2cypher      # CPython 3.9+
    npm install shacl2cypher      # Node.js 18+
  2. Write a shapes file

    Save this as shapes.ttl. It says that every :Person needs a name, that a name should start with a capital letter (a warning, not an error), and that a person works for at most one company.

    @prefix sh:  <http://www.w3.org/ns/shacl#> .
    @prefix ex:  <http://example.org/> .
    
    ex:PersonShape sh:targetClass ex:Person ;
        sh:property [ sh:path ex:name ; sh:minCount 1 ; sh:message "Person needs a name" ] ,
                    [ sh:path ex:name ; sh:pattern "^[A-Z]" ; sh:severity sh:Warning ] ,
                    [ sh:path ex:worksFor ; sh:maxCount 1 ] .

    By convention ex:Person becomes the label :Person and ex:name becomes the property name. ex:worksFor becomes the relationship type WORKS_FOR when a schema snapshot declares that type and no worksFor property. The SHACL guide covers the full rules.

  3. Compile

    shacl2cypher compile shapes.ttl --dialect neo4j --node-key id -o out/

    This writes two files:

    • out/manifest.json: the primary artifact. It records provenance (compiler version, options, the SHA-256 of each input) and one entry per rule, with its id, severity, status, cost class, source line and queries.
    • out/queries.cypher: the same queries for people. Each is headed by // name: <name> (summaries by #summary), so you can paste them into any Cypher client. They take two parameters, $limit and $sampleSize.

    --node-key id names the property that identifies nodes in result rows. A shape can override it with s2c:key.

  4. Validate a database

    Neo4j. Every query runs in a transaction that is always rolled back.

    NEO4J_PASSWORD=secret shacl2cypher validate shapes.ttl \
        --connect bolt://localhost:7687 --node-key id

    LadybugDB. The file is opened read-only, and the schema is read from it before compiling.

    shacl2cypher validate shapes.ttl --ladybug ./graph.lbug --node-key id

    You get the table shown on the overview: one line per rule, up to ten violations each, and a totals line. The exit code is 0 when nothing at or above --fail-on (default violation) failed, 1 when something did, 2 for setup errors and 3 when a query timed out and the result is incomplete.

  5. Pick a report format

    shacl2cypher validate shapes.ttl --ladybug graph.lbug --node-key id --format json  -o report.json
    shacl2cypher validate shapes.ttl --ladybug graph.lbug --node-key id --format junit -o junit.xml
    shacl2cypher validate shapes.ttl --ladybug graph.lbug --node-key id --format sarif -o report.sarif

    Useful options: --timeout <seconds> per query, --limit for violations listed per rule (0 lists all), --fail-on violation|warning|info, and --verbose to include every focus property in rows.

  6. Compile once, validate many times

    For LadybugDB, compiling ahead of time needs a schema snapshot, because its tables are typed:

    shacl2cypher schema dump --ladybug ./graph.lbug -o schema.json
    shacl2cypher compile shapes.ttl --dialect ladybug --schema schema.json --node-key id -o out/

    Commit the manifest, review query changes in pull requests, and run it later:

    shacl2cypher validate --manifest out/manifest.json --ladybug ./graph.lbug --format junit -o junit.xml

    A manifest compiled for one dialect is rejected when run against another database.

From Python

import shacl2cypher

compilation = shacl2cypher.compile(["shapes.ttl"], dialect="neo4j", node_key="id")
compilation.write("out")                        # manifest.json and queries.cypher

with shacl2cypher.Ladybug("graph.lbug") as db:  # or shacl2cypher.Neo4j("bolt://localhost:7687", password="...")
    report = db.validate(["shapes.ttl"], node_key="id", timeout=30)
    print(report.render("table"))               # also "json", "junit", "sarif"
    raise SystemExit(report.exit_code("violation"))

Shapes can be paths or in-memory shacl2cypher.Source(name, text) documents. Errors derive from Shacl2CypherError. Calls release the GIL.

From TypeScript

import { compile, Ladybug, renderReport, exitCode } from 'shacl2cypher';

const compilation = await compile({ shapes: ['shapes.ttl'], dialect: 'neo4j', nodeKey: 'id' });
compilation.write('out');

const db = await Ladybug.open('graph.lbug');   // or await Neo4j.connect({ uri: 'bolt://localhost:7687', password: '...' })
try {
  const report = await db.validate({ shapes: ['shapes.ttl'], nodeKey: 'id', timeoutMs: 30_000 });
  console.log(renderReport(report, 'table'));
  process.exitCode = exitCode(report, 'violation');
} finally {
  await db.close();
}

Calls return Promises and never block the event loop. Unknown option keys throw TypeError. Types ship in index.d.ts.

Where to go next