LPG Modeler
Reference

Seven targets, and what each one refuses to promise

Every generator publishes a typed capability set. The compiler works out what your model requires, compares the two, and reports every gap — as an editor diagnostic and as a comment at the lossy site in the artifact.

A matrix of five model features against four targets, showing which are enforced natively, which are downgraded, and which are transformed.
Silent best-effort was rejected deliberately: a required constraint that quietly vanishes is a data-integrity bug that surfaces in production, long after the model stopped being read.

ladybug

LadybugDB — formerly Kuzu — is an embedded Cypher property graph database with a mandatory closed schema: CREATE NODE TABLE and CREATE REL TABLE, typed columns, a required primary key, and declared endpoint pairs. Output extension: .cypher.

How the hierarchy lands

An abstract hierarchy is flattened to one node table per concrete leaf type, with inherited columns copied down. The abstract type itself gets no table. The cost is that an edge declared on an abstract endpoint expands to a cross-product of endpoint pairs, and adding a subtype becomes a schema migration.

What it will not enforce

Measured against LadybugDB 0.19.1, the engine enforces less than the flattening suggests:

These are measured, not assumed. The Ladybug generator executes its own output against an in-process LadybugDB instance and then asserts that the declared constraints really do reject invalid data. That is how the -- versus // comment syntax was discovered: the SQL-style form is rejected by the parser, which a golden file alone would never have caught.


neo4j

Neo4j is schema-optional: there is no table DDL, only constraints and indexes. Multi-label nodes are native, so an abstract hierarchy flattens to labels rather than to separate tables — a Person node carries :Person :Party. Output extension: .cypher.

Edition awareness

Existence and node key constraints require Enterprise. Under a Community configuration they are reported as downgrades and emitted as comments rather than silently dropped — a node key becomes a plain uniqueness constraint, which does not enforce that the key is present.

// Setting: lpg.targets.neo4j.edition
// DOWNGRADE: NODE KEY requires Enterprise.
// Uniqueness only; presence unenforced.
CREATE CONSTRAINT person_key_unique IF NOT EXISTS
  FOR (n:Person) REQUIRE (n.id) IS UNIQUE;

Set the editions setting to enterprise, or pass --edition enterprise to the CLI, and the real constraints are emitted instead.


shacl

SHACL is the primary constraint artifact for RDF, because it is closed-world validation and so means what a property graph schema means. Required, unique, cardinality and datatype all translate faithfully, and a generated shape genuinely rejects invalid data. Output extension: .ttl.

The one gap: uniqueness across all instances needs a SPARQL-based constraint, which core SHACL cannot express. It is reported as a downgrade with a comment on the affected property shape.


owl

The ontology is emitted alongside SHACL, not instead of it, because a property graph schema and an OWL ontology do not mean the same thing. A schema is a closed-world constraint; OWL is open-world inference. Output extension: .ttl.

The safe assertional subset

Only classes, subClassOf, hasKey, disjointness and inverse properties are emitted. Domain, range and cardinality restrictions are deliberately omitted.

Why omitting them is the careful choice. Emitting rdfs:domain for an edge type does not constrain anything — it instructs a reasoner that anything carrying that relation belongs to the domain class, silently reclassifying unrelated individuals. Mapping constraints naively into OWL does not lose information so much as invert its meaning. The constraints live in the SHACL artifact, and every omission here is reported with a diagnostic pointing at it.

Gradual reification

An edge with no properties becomes a plain object property. An edge that carries properties becomes an n-ary relation class plus a shortcut property, and its SHACL shape targets that class.

An edge with no properties becomes a plain object property; an edge carrying properties becomes an n-ary relation class plus a shortcut property.
Reifying only what needs it keeps the output inside OWL DL, so reasoners keep working. RDF-star was rejected as the uniform representation because OWL DL reasoners do not handle quoted triples and SHACL cannot constrain them.

Standards targets

Three targets exist so a model is readable by tools this project does not own. None is a database dialect: they are the schema languages the graph world has agreed on, and emitting them is how a model authored here stays portable.

Why emit a standard rather than adopt one. No published standard covers what a model file has to do. PG-Schema and GQL graph types are textual DSLs with nowhere to hang stable element ids, import aliases, or a rename’s previous IRI, and neither has a namespace concept the RDF targets need. LinkML is the closest serialization, but it has no binary edge that can carry properties — adopting it would force the uniform reification that gradual reification exists to avoid. So the YAML stays, and the standards come out of the pipeline instead.

gql

GQL graph types, per ISO/IEC 39075 — the international standard for graph query languages. A graph type is a list of element types, each naming an identifying label, the labels it implies, and its typed properties. Output extension: .gql.

CREATE GRAPH TYPE socialGraphType AS {
  (personType: Person => :Party {
    email :: STRING NOT NULL IS NODE UNIQUE,
    id :: STRING NOT NULL IS NODE KEY
  }),
  (:Party)-[ownsType: OWNS => {
    since :: DATE
  }]->(:Car)
}

Label implication carries the hierarchy, so an edge declared on an abstract endpoint stays a single element type instead of expanding to a cross-product the way the Ladybug target must. Abstract types get no element type of their own — they exist purely as implied labels.

Two things are lost, both reported. A key marker attaches to one property, so a composite key cannot be expressed; and uuid and json have no GQL value type. Engines also disagree on the statement that installs a graph type — Neo4j writes the same body after ALTER CURRENT GRAPH TYPE SET — so the generated file says so in a header comment rather than claiming portability it does not have.


pgschema

PG-Schema, the formalism from the LDBC Property Graph Schema Working Group that GQL’s graph types grew out of. This is the most faithful target of the seven: ABSTRACT, inheritance, mixins and keys all have direct counterparts, so nothing about the hierarchy is flattened. Output extension: .pgs.

CREATE GRAPH TYPE socialType STRICT {
  ABSTRACT (timestampedType {createdAt ZONED DATETIME}),
  ABSTRACT (partyType: Party {id STRING}),
  (personType: partyType & timestampedType & Person {email STRING, OPTIONAL born DATE}),
  (:partyType)-[ownsType: OWNS {OPTIONAL since DATE}]->(:carType),
  FOR (x: partyType) EXCLUSIVE MANDATORY SINGLETON x.id
}

A mixin becomes an abstract type declared without a label, which is exactly what a mixin is here. Keys become PG-Keys constraints, stated once on the type that owns them because subtypes inherit them, and a composite key needs no synthesized column. Only uuid and json are downgrades.


linkml

LinkML, the linked-data modelling language. Its classes, is_a and mixins line up almost directly with this metamodel, which is what makes the target worth having: it opens the whole LinkML generator ecosystem — Python, Pydantic, TypeScript, GraphQL, SQL DDL, SQLAlchemy — to a model authored here. Output extension: .yaml.

classes:
  Party:
    class_uri: social:Party
    abstract: true
    attributes:
      id:
        slot_uri: social:id
        range: string
        identifier: true
  Person:
    is_a: Party
    mixins:
      - Timestamped

Every class and slot carries the IRI it has in this model, so identity survives the round trip rather than degrading to a local name. A single key becomes an identifier; a composite key and every other unique property become unique_keys, the only mechanism LinkML has for them.

The one real mismatch. LinkML has no binary relation that can hold properties, so an edge that carries them is reified into a class — and that is reported as a downgrade like any other. The shortcut property the RDF targets emit is deliberately left out here: in a schema meant to be generated from, it would imply a second place the same fact is written.

Targets that are deliberately absent

Cypher compatibility is a marketing category rather than a dialect: Ladybug, Neo4j, Memgraph and Apache AGE disagree on nearly everything schema-related, so a generic Cypher generator would have no reference implementation to test against.

The rule is about reference implementations rather than running engines, which is why the standards targets ship as code despite having no instance to execute against: a published specification is a reference a generator can be held to, and a dialect nobody has specified is not.

For a dialect that cannot be tested here, the intended path is a template fed by the resolved model rather than a hand-written generator — so an additional target is a piece of configuration rather than a feature request. That, along with migrations and the lockfile diff, is not in this release.

Adding a target

Generators sit behind an internal registry: a target is one file plus one entry. The public plugin API is deliberately deferred until three real generators have shown where the seam actually falls — the capability set on this page is what that API will eventually expose.

interface Capabilities {
  target: string
  multiLabel: boolean
  inheritance: 'leaf-tables' | 'labels' | 'subclass'
  requiredConstraint: 'enforced' | 'key-only' | 'edition-dependent' | 'unsupported'
  uniqueConstraint:   'enforced' | 'key-only' | 'edition-dependent' | 'unsupported'
  compositeKey: 'native' | 'synthesized' | 'unsupported'
  edgeProps: 'native' | 'reified'
  nestedEdges: boolean
  listProps: 'native' | 'unsupported'
  enums: 'enforced' | 'documented' | 'unsupported'
  openTypes: 'native' | 'always-open' | 'unsupported'
  cardinality: 'enforced' | 'unsupported'
}

A downgrade is reported only when a model actually uses the feature. A target that cannot enforce closure declares that in its capability set, but does not raise a diagnostic on every closed type — that would be noise on every model rather than information.

Where to go next