1. guides
prorm
  • prorm documentation
  • Known test gaps
  • Architecture
  • Core concepts
  • Database types & coverage
  • Getting started
  • Going further
  • compliance
    • Compliance
    • The security toolkit: `prorm audit`
    • Checklists by regulation
    • Declaring personal data
    • Evidence bundles
    • Evidence
    • Frameworks: `@Security`
    • Jurisdictions
    • Platform policies
    • Protecting data
    • The regulation dataset
    • Residency and cross-border transfers
    • Retention & integrity
    • Status
    • Subject rights
  • decorators
    • prorm with decorators
    • Models with decorators
    • Querying a decorated model
    • Relations with decorators
  • diagrams
    • Diagram generators
    • Chen diagram
    • Class diagram
    • Dependency diagram
    • ER diagram
    • Flow diagram
    • From your models to a diagram
    • Gantt diagram
    • Index diagram
    • Migration diagram
    • Model diagram
    • Package diagram
    • Crow’s-foot diagram
    • Schema documentation
    • Sequence diagram
    • State diagram
    • Tree diagram
  • dialects
    • SQL dialects
    • AlloyDB Dialect
    • AlloyDB Omni Dialect
    • Amazon Athena Dialect
    • Aurora DSQL Dialect
    • Amazon Aurora MySQL Dialect
    • Amazon Aurora PostgreSQL Dialect
    • Azure Database for MySQL Dialect
    • Azure Database for PostgreSQL Dialect
    • Azure SQL Database Dialect
    • Azure SQL Managed Instance Dialect
    • Google BigQuery Dialect
    • Citus Dialect
    • ClickHouse dialect
    • Cloud Authentication (`cloud-auth`)
    • Cloud SQL for SQL Server Dialect
    • Cloud SQL for MySQL Dialect
    • Cloud SQL for PostgreSQL Dialect
    • CockroachDB Dialect
    • Azure Cosmos DB for PostgreSQL Dialect
    • CrateDB Dialect
    • Cloudflare D1 Dialect
    • Databricks dialect
    • IBM Db2 Dialect
    • Dolt Dialect
    • Apache Doris Dialect
    • Druid Dialect
    • DuckDB Dialect
    • Exasol dialect
    • Microsoft Fabric Data Warehouse Dialect
    • Firebird Dialect
    • Greenplum Dialect
    • SAP HANA Dialect
    • MySQL HeatWave Dialect
    • Azure Data Explorer (Kusto) Dialect — T-SQL
    • MariaDB Dialect
    • Materialize Dialect
    • MonetDB Dialect
    • MotherDuck Dialect
    • Microsoft SQL Server Dialect
    • MySQL Dialect
    • Neon Dialect
    • OceanBase Dialect
    • openGauss Dialect
    • Oracle Dialect
    • Oracle Autonomous Database Dialect
    • ParadeDB Dialect
    • PolarDB Dialect
    • PostgreSQL Dialect
    • Presto Dialect
    • QuestDB Dialect
    • Aurora RDS Data API Dialect (`rdsdata`)
    • Amazon RDS for Db2 Dialect
    • Amazon RDS for MariaDB Dialect
    • Amazon RDS for SQL Server Dialect
    • Amazon RDS for MySQL Dialect
    • Amazon RDS for Oracle Dialect
    • Amazon RDS for PostgreSQL Dialect
    • Amazon Redshift
    • Amazon Redshift Data API Dialect
    • RisingWave Dialect
    • rqlite Dialect
    • SingleStore Dialect
    • Snowflake Dialect
    • Google Cloud Spanner Dialect
    • Spanner (PostgreSQL interface) Dialect
    • SQLite Dialect
    • StarRocks Dialect
    • Supabase Dialect
    • Azure Synapse Analytics (dedicated SQL pool)
    • Teradata Dialect
    • TiDB Dialect
    • TimescaleDB Dialect
    • Trino Dialect
    • Turso / libSQL Dialect
    • Vertica dialect
    • Vitess Dialect
    • Yellowbrick Dialect
    • YugabyteDB Dialect
  • guides
    • Access rules and behaviour profiles
    • ArangoDB (AQL)
    • Associations
    • Audit levels and escalation
    • Audit logging
    • In the browser
    • Bulk operations
    • Caching
    • Capabilities, tiers and team roles
    • CLI
    • Cloudflare
    • Compliance & data protection
    • Connection Pooling & Performance
    • Security in the console
    • Couchbase: structured N1QL queries
    • Data types
    • Decorators
    • Eager loading
    • Enums and constants
    • Error handling & error types
    • Extensions
    • External fields
    • Foreign data wrappers
    • Graph databases
    • Hooks & lifecycle events
    • Indexes & constraints
    • InfluxDB: structured Flux pipelines
    • klock-errors
    • klock — the `.klock` file
    • Local databases (`prorm db up`)
    • Structured log search — Splunk, Graylog, Papertrail, Sumo Logic
    • Logging
    • Grafana Loki: structured LogQL queries
    • MCP server
    • Migrate — the data transfer subsystem
    • Migrations & Schema Sync
    • Models from an existing database
    • Defining models
    • Multiple databases
    • OrientDB, Neptune and TigerGraph
    • Passwords
    • Importing a Prisma schema
    • Structured PromQL and MetricsQL
    • Query firewall
    • QueryInterface
    • Query operators reference
    • Query optimization
    • Composing find options with `pipe()` and `Q`
    • Query safety: the fail-closed WHERE guard
    • Querying
    • Raw Queries & Query Building
    • Read replicas
    • Schema diffing & migration generation
    • Views, triggers & other schema objects
    • Scopes & default scopes
    • Seeders
    • SPARQL / RDF triplestores
    • SQL constants
    • SQL features
    • SQL function builders
    • SQLite advanced features
    • StoreKey — a prod-orm database as the storage
    • Streaming results
    • Terraform
    • Transactions & Isolation Levels
    • Type mapping: legacy and modern
    • TypeScript types
    • Database users, roles & privileges
    • Validation & constraints
    • Virtual & Computed Fields
    • Writing docs
  • javascript
    • prorm in JavaScript
    • Models in JavaScript
    • Querying in JavaScript
    • Relations in JavaScript
  • runbooks
    • Operational runbooks
    • Connection failures
    • Credential rotation
    • Failover
    • Incident checklist
    • Deploying klock v2
    • Migration rollback
  • running-databases
    • Running databases in Docker
    • Running prorm under Bun in Docker
    • Running Cassandra in Docker with prorm
    • Running ClickHouse in Docker with prorm
    • Running CockroachDB in Docker with prorm
    • Running prorm under Deno in Docker
    • Running Elasticsearch in Docker with prorm
    • Running MariaDB in Docker with prorm
    • Running MongoDB in Docker with prorm
  • prorm control plane
    • Introduction
    • Authentication
    • Errors
    • Getting started
      • Service
        • Is it up
        • This description
    • Account
      • Sign-up and verification
        • Create an account
        • Exchange credentials for a token
        • Verify with the emailed code
        • Verify by clicking the emailed link
        • Name the parent or guardian
        • What a consent link is about
        • Give consent for a child's account
        • Ask the parent or guardian again
        • Send a new code and link
      • Family
        • Everyone this account is responsible for
        • One child, in detail
        • Turn a limit on or off
      • Profile
        • Who am I
        • Change the account
        • Every address on the account
        • Add an address
        • Confirm an address with the code mailed to it
        • Mail a fresh code to an unconfirmed address
        • Make an address primary
        • Remove an address
        • What this caller may do
    • Security
      • Sessions
        • Everywhere this account is signed in
        • Sign out everywhere else
        • Sign one out
        • A token for the CLI on a named machine
        • A token for the customer console, from the staff one
        • Park a sign-in for the CLI to collect
        • The CLI collecting its sign-in
        • The team's members' live sessions
        • Sign a member's session out
        • Sign an account out everywhere
        • Everywhere the account is signed in
        • Sign an account's session out
      • Single sign-on
        • Begin a single sign-on
        • The provider's return
        • Link the signed-in account to a team's provider
        • Which team an address signs in to
        • The team's single sign-on, as a member sees it
        • Set the team's OpenID Connect provider up, or change it
        • Remove the provider
        • The team's domain claims and the DNS record each needs
        • Prove a claimed domain
      • MFA and passwords
        • Start enrolling a second factor
        • Prove the app has it
        • Remove the second factor
        • Ask for a reset link
        • Set a new password with the emailed token
      • Klock
        • Open a device-bound klock session
        • Check in, for the next grant
        • Step up the session asking
        • Rotate the device key
        • Freeze a session
        • Unfreeze a session
        • A guest grant
        • The revocation feed
        • The published keyset
        • What this control plane supports
      • Devices
        • Your devices, or a team's
        • Approve a new device's pairing code
        • One device
        • Rename a device
        • Revoke a device
        • Suspend a device
        • Reinstate a suspended device
        • Wipe a device
        • Ask a device to rotate its key
        • The team's members' devices
        • Revoke a member's device
        • An account's devices
        • Revoke an account's device
        • Wipe an account's device
        • The device fleet
      • Access rules and behaviour
        • The access rules that bear on you
        • Add an access rule
        • This request, as the rules see it
        • One access rule
        • Remove an access rule
        • The behaviour profiles that bear on you
        • Add or change a behaviour profile
        • The profile presets
        • Your behaviour findings
        • Acknowledge a finding
        • One behaviour profile
        • Remove a behaviour profile
        • The team's access rules
        • Add a team access rule
        • Remove a team access rule
        • The team's behaviour profiles
        • Add or change a team behaviour profile
        • Remove a team behaviour profile
        • What the team's profiles caught
        • Acknowledge a team finding
      • Recovery and lockdown
        • Ask for a recovery link
        • Recover access with the emailed code
        • Is the account locked down
        • Lock the account down
        • Release a lockdown
        • Lock a member down
        • Release a member's lockdown
      • MCP tokens
        • Make a delegated token for one MCP client
        • The account's delegated tokens
        • Revoke a delegated token
        • What a delegated token may do now
        • Run one MCP tool with a delegated token
        • Run a device-signed MCP tool call relayed by the MCP Worker
      • Kerberos
        • Give the team its own Kerberos realm
        • The team's realm
        • Remove the team's realm
    • Logging and monitoring
      • Audit log
        • What has been done to the account
        • What has happened to this account
        • The account's audit trail
      • Operations
        • Everything that has been asked of this database
        • One, with its steps
        • Stop it
        • Put a failed one back in the queue
        • Every operation, across all accounts
      • Metrics and events
        • Every measurement, for a graph
        • Who did what to it
    • Billing
      • Plans and pricing
        • What the plans allow
        • The rate card
        • What one database would cost
        • What this account is paying for, itemised
      • Subscriptions and invoices
        • What this month's invoice will say
        • What this account is on
        • What it has been charged
        • Start a Stripe Checkout session
        • Open the customer portal
        • Stripe's events
        • Buy more connection pools
      • Team billing
        • The team's billing account
        • What the team has been charged
        • What this month's invoice will say
        • The team's cards
        • Add a card, through a Stripe-hosted setup session
        • Make a card the team's default
        • Remove a card
        • Start the team paying
        • Open the Stripe portal on the team's customer
        • Change the team's plan
        • Who receives the team's bills
        • How the team pays for transfers
    • Support
      • Support chat
        • My conversations
        • Start a conversation
        • Every support conversation, open first
        • A ticket for the socket
        • One conversation, with who is in it and who is connected
        • Rename a group; open or close a support conversation
        • The lines, oldest first
        • Say something
        • Add somebody
        • Mark read
    • Administration
      • Users
        • Every account
        • One account, with everything hanging off it
        • Grant or revoke admin
        • Verify an account by hand
        • Lock or unlock sign-in
        • Allow or hold provisioning
        • Send the verification mail again
        • Set the plan by hand
      • Platform
        • Every database and store, across all accounts
        • The counts worth putting on a dashboard
      • Staff and permissions
        • What kind of admin the caller is, and which scopes they hold
        • The admin presets
        • Every admin, and the scopes each resolves to
        • Make an account an admin of a kind
        • Change an admin's kind, or a custom admin's scopes
        • Revoke admin
      • Team administration
        • Every team, with its owner and size
        • One team in full — members with their accounts, and projects
        • Hand the team to another member
        • Put an account in the team, active at once
        • Change a member's role
        • Remove a member
      • Assumed identities
        • Assume an identity — act as a customer account
        • The identities the caller holds open
        • End an assumed identity
      • Support inbox
        • The platform's addresses
        • Add a platform address
        • One address, with its counts
        • Change an address's label, routing, or whether it accepts mail
        • Remove an address that holds no mail
        • Make, adopt or repair the address's routing rule
        • Every admin's personal address, made where missing
        • File a raw message, as if it had arrived
        • The inbox
        • One message, whole
        • Mark a message
        • Hand a message to an admin, or take it back
        • Leave an internal note
        • Reply, from the address the message came to
        • What is waiting
    • Databases
      • Databases
        • Data stores
          • Every data store on the account
          • Provision a data store
          • Change a data store that is already running
          • One data store
          • Deprovision it
          • Report what it is using
          • How to reach a data store
        • Connections and credentials
          • The api keys on a database
          • Issue a key
          • Revoke a key
          • The engine users we created
          • Create a user inside the database
          • Give a user a new password
          • Drop a user
          • How the database is reached
          • Set how it is reached
          • Revoke every api key on it
          • How to connect to it
          • The ssh keys that open its bastion
          • Add a key, or have one made
          • The private half of a generated key, once
          • Revoke an ssh key
        • Schema and objects
          • The SQL objects we put in it
          • Create one, and apply it
          • What is in the database, as the engine reports it
          • One table
          • What it would run, without running it
          • Redefine an object in place
          • Drop it
          • The extensions installed in it
        • Migrations
          • A plan compiled to statements, recording nothing
          • The migrations recorded for it
          • Record one
          • Apply everything recorded and not yet run
          • Apply everything pending, oldest version first
        • Lifecycle
          • Grow or shrink it
          • Stop it accepting connections
          • Start it again
          • Move it to another region
        • Backups and replicas
          • Take a backup
          • The backups it has
          • Restore one into it
          • What has been restored into it, and from where
          • Its read copies
          • Add one
        • Policies
          • The catalog
          • One policy
          • What this database is held to
          • Switch a policy on, or change its knobs
          • Switch it off
          • Mark the policies reviewed
          • Evaluate the periodic policies now
          • Who is behind a credential
        • Passwordless
          • The database's passwordless sessions
          • Issue a passwordless session (v1)
          • Revoke a passwordless session
          • Bind a session to a device key
          • Issue a v2 session for a host to bind
          • Redeem an enrolment ticket on the host
          • Check a passwordless credential
          • Run one described query under a passwordless credential
        • Database roles
          • The database's roles
          • Define a role
          • One role
          • Change what a role permits
          • Delete a role, and revoke every key granted it
          • Grant a role, which mints a key
          • Who holds a role
          • The api keys bound to a role
        • Pools
          • A database's connection pools, and how busy each is
          • Open another pool
          • Rename a pool or change its size
          • Close a pool
        • Data plane
          • Run a query against a database
          • Several statements, in order, over one connection
          • Is this database answering
          • What the proxy itself is holding
        • Transfers
          • The strategies this account's tier may use
          • Price a transfer without creating it
          • The account's transfer jobs, newest first
          • Create a transfer job
          • One job, and whether its certificate holds
          • Cancel an unfinished job
          • Start a created job, confirming its price
          • Report a job's status (the runner, or the owner's side of a local run)
        • Every database on the account
        • Provision a database
        • Change a database that is already running
        • One database
        • Deprovision it
        • Read or write it
        • Report what it is using
        • The engines this API will provision
        • Where a managed database can be put
    • Teams
      • Teams
        • Members
          • Who is on it
          • Invite somebody
          • Accept an invitation
          • Change somebody's role
          • Remove somebody
        • Projects
          • The projects a team runs
          • Start one
          • Rename a project, or move its environment
          • Delete a project
          • One project, and what is in it
          • Put a database or store in it
          • What is in a project
          • Take one out
        • The teams this account is on
        • Start one
        • The twelve roles, and what each may do
        • Rename a team
        • Close a team
        • One team
        • Hand the team to somebody else
        • Leave a team
    • Schemas
      • Error
      • ValidationErrors
      • Token
      • PolicyCategory
      • PolicySeverity
      • PolicyAction
      • PolicyTrigger
      • PolicyParam
      • Policy
      • PolicyCatalog
      • PolicyAssignment
      • PolicyOutcome
      • PolicyFiring
      • CredentialTrace
      • Relation
      • Restrictions
      • ChildSummary
      • SignupRequest
      • SigninRequest
      • UpdateMeRequest
      • User
      • Address
      • GeoPoint
      • D1Region
      • Usage
      • UsageReport
      • Resource
      • Database
      • DataStore
      • NewDatabase
      • NewDataStore
      • QueryRequest
      • DatabasePrivilege
      • ApiKey
      • DbUser
      • Access
      • AccessRequest
      • DatabaseObject
      • NewDatabaseObject
      • NewMigration
      • SchemaOperation
      • Migration
      • Operation
      • Session
      • AdminPowers
      • Admin
      • AuditEntry
      • TeamRole
      • Conversation
      • ChatMessage
      • TeamSso
      • Team
      • TeamMember
      • SshKey
      • ConnectionInfo
      • InboxAddress
      • InboxMailbox
      • InboxMessageSummary
      • InboxMessage
      • InboxNote
      • Page
      • Quote
      • Plan
      • ApiError
      • CapabilityRefusal
      • KlockErrorCode
      • PageWindow
      • SessionView
      • KlockStepUpRequest
      • KlockOpenRequest
      • KlockOpenedSession
      • UpdateMeResult
      • CapabilitiesView
      • SessionLimits
      • Device
      • DeviceListed
      • DeviceTeamRef
      • DeviceCommand
      • DeviceAuditLine
      • DeviceActionRequest
      • StaffActionRequest
      • ManagedSession
      • SessionRevoked
      • AccessRule
      • AccessRuleRequest
      • BehaviourProfile
      • BehaviourProfileRequest
      • BehaviourProfileSaved
      • BehaviourFinding
      • BehaviourFindingAck
      • StepUpProof
      • Removed
      • MemberLockdownRequest
      • McpClient
      • McpDelegatedView
      • McpToolCall
      • McpForwardRequest
      • PasswordlessScope
      • RoleScope
      • PasswordlessSession
      • PasswordlessTlsMaterial
      • PasswordlessKerberosMaterial
      • ServiceDeviceKeys
      • ServiceSession
      • DatabaseRole
      • TransferEndpoint
      • TransferJobRequest
      • TransferQuote
      • TransferStrategy
      • TransferJob
      • TransferJobAccess
      • Pool
      • PoolAllowance
      • BillingSubscription
      • Invoice
      • BillingContact
      • PaymentMethod
      • UpcomingCharges
      • TeamBilling
      • TeamKerberosRealm
      • TeamSsoDomain
      • TeamSsoResponse
      • StoreConnection
      • BatchFailure
      • ProxyStatus
  1. guides

SPARQL / RDF triplestores

Stardog, Apache Jena Fuseki, Blazegraph, Ontotext GraphDB, OpenLink Virtuoso and
AllegroGraph are all RDF triplestores that speak the SPARQL 1.1 Protocol over
HTTP. Until now the only way to use them from this library was to hand-write
SPARQL and pass it to store.query(...).
SparqlGraphDialect (src/graph/sparql) implements the same
graph dialect contract as the Cypher, Gremlin and Dgraph
DQL dialects — the same 13 compile* methods, the same findAll-shaped spec
objects — and compiles them to SPARQL. Every one of the six stores now exposes a
graph() accessor that returns a dialect bound to it:
import { Op } from 'ts-prorm-orm';
import { FusekiStore } from 'ts-prorm-orm/nosql/fuseki';

const store = new FusekiStore({ endpoint: 'http://localhost:3030', dataset: 'kb' });
await store.connect();

const graph = store.graph();

await graph.createNode('User', { id: 42, name: 'Ada', age: 36 });

const users = await graph.findNodes({
  label: 'User',
  attributes: ['id', 'name', 'age'],
  where: { age: { [Op.gte]: 30 } },
  order: [['name', 'ASC']],
  limit: 10,
});

await graph.relate({
  fromLabel: 'User',  fromWhere: { id: 42 },
  edgeLabel: 'HAS_POSTS',
  toLabel: 'Post',    toWhere: { id: 7 },
});

const reachable = await graph.traverse({
  label: 'User', where: { id: 42 },
  edgeLabel: 'KNOWS', direction: 'both', maxDepth: 3,
});
The raw query / update / ask / add / drop methods on each store are
untouched. They are a legitimate escape hatch for the things SPARQL does that no
ORM shape covers — CONSTRUCT, federation, inference, vendor extensions. They are
simply no longer the only way in.

Side by side#

The graph() calls from the top of this page and the SPARQL each one compiles to, with the default http://prorm.dev/ns/ bases. ?_p0-style placeholders are filled by bindSparql() just before the request is sent (see Injection).
import { Op } from 'ts-prorm-orm';
import { FusekiStore } from 'ts-prorm-orm/nosql/fuseki';

const store = new FusekiStore({ endpoint: 'http://localhost:3030', dataset: 'kb' });
await store.connect();
const graph = store.graph();

// 1. flat projection: attributes are known, one row per node
const users = await graph.findNodes({
  label: 'User',
  attributes: ['id', 'name', 'age'],
  where: { age: { [Op.gte]: 30 } },
  order: [['name', 'ASC']],
  limit: 10,
});

// 2. everyone within two KNOWS hops, either direction
const circle = await graph.traverse({ label: 'User', where: { id: 42 }, edgeLabel: 'KNOWS', direction: 'both', maxDepth: 2 });

// 3. update one property
await graph.updateNodes('User', { id: 42 }, { age: 37 });

// 4. an edge between two matched nodes
await graph.relate({ fromLabel: 'User', fromWhere: { id: 42 }, edgeLabel: 'HAS_POSTS', toLabel: 'Post', toWhere: { id: 7 } });

// 5. delete a node and every triple pointing at it
await graph.destroyNodes('User', { id: 42 });
# 1.  ?_p0 = 30
SELECT ?s ?v_id ?v_name ?v_age
WHERE {
  ?s <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://prorm.dev/ns/class/User> .
  OPTIONAL { ?s <http://prorm.dev/ns/prop/age> ?v_age . }
  OPTIONAL { ?s <http://prorm.dev/ns/prop/id> ?v_id . }
  OPTIONAL { ?s <http://prorm.dev/ns/prop/name> ?v_name . }
  FILTER(?v_age >= ?_p0)
}
ORDER BY ASC(?v_name)
LIMIT 10

# 2.  ?_p0 = 42
SELECT DISTINCT ?t
WHERE {
  ?s <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://prorm.dev/ns/class/User> .
  OPTIONAL { ?s <http://prorm.dev/ns/prop/id> ?v_id . }
  FILTER(?v_id = ?_p0)
  { ?s (<http://prorm.dev/ns/edge/KNOWS>|^<http://prorm.dev/ns/edge/KNOWS>) ?t . }
  UNION { ?s (<http://prorm.dev/ns/edge/KNOWS>|^<http://prorm.dev/ns/edge/KNOWS>)/(<http://prorm.dev/ns/edge/KNOWS>|^<http://prorm.dev/ns/edge/KNOWS>) ?t . }
}

# 3.  ?_p0 = 42, ?_p1 = 37
DELETE { ?s <http://prorm.dev/ns/prop/age> ?old_age . }
INSERT { ?s <http://prorm.dev/ns/prop/age> ?_p1 . }
WHERE {
  ?s <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://prorm.dev/ns/class/User> .
  OPTIONAL { ?s <http://prorm.dev/ns/prop/id> ?v_id . }
  FILTER(?v_id = ?_p0)
  OPTIONAL { ?s <http://prorm.dev/ns/prop/age> ?old_age . }
}

# 4.  ?_p0 = 42, ?_p1 = 7
INSERT { ?a <http://prorm.dev/ns/edge/HAS_POSTS> ?b . }
WHERE {
  ?a <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://prorm.dev/ns/class/User> .
  OPTIONAL { ?a <http://prorm.dev/ns/prop/id> ?v_id . }
  FILTER(?v_id = ?_p0)
  ?b <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://prorm.dev/ns/class/Post> .
  OPTIONAL { ?b <http://prorm.dev/ns/prop/id> ?v_id_1 . }
  FILTER(?v_id_1 = ?_p1)
}

# 5.  two operations in one request: incoming triples, then the node's own
DELETE { ?src ?inp ?s . }
WHERE {
  ?s <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://prorm.dev/ns/class/User> .
  OPTIONAL { ?s <http://prorm.dev/ns/prop/id> ?v_id . }
  FILTER(?v_id = ?_p0)
  ?src ?inp ?s .
} ;
DELETE { ?s ?p ?o . }
WHERE {
  ?s <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://prorm.dev/ns/class/User> .
  OPTIONAL { ?s <http://prorm.dev/ns/prop/id> ?v_id . }
  FILTER(?v_id = ?_p1)
  ?s ?p ?o .
}

The triple-mapping convention#

RDF has no labels, no properties and no edges — only triples. Any mapping from a
property graph onto RDF has to be invented, and the failure mode of an implicit
convention is that two systems write the same data under different IRIs and
neither can read the other's. So here it is, in full:
ORM conceptRDFDefault IRI
node label Useran rdfs:Class-style class IRI<http://prorm.dev/ns/class/User>
the label itself<subject> rdf:type <class IRI>—
node with id 42the subject IRI<http://prorm.dev/ns/data/User/42>
property namea predicate IRI<http://prorm.dev/ns/prop/name>
property value 'Ada'an object literal"Ada"
edge label HAS_POSTSa predicate IRI<http://prorm.dev/ns/edge/HAS_POSTS>
an edge<…/data/User/42> <…/edge/HAS_POSTS> <…/data/Post/7>—
The same data drawn both ways: one node with two properties and an edge, and the triples it becomes (IRIs shortened to their last segment):
So User.create({ id: 42, name: 'Ada' }) becomes:
INSERT DATA {
  <http://prorm.dev/ns/data/User/42> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://prorm.dev/ns/class/User> .
  <http://prorm.dev/ns/data/User/42> <http://prorm.dev/ns/prop/id>   "42"^^<http://www.w3.org/2001/XMLSchema#integer> .
  <http://prorm.dev/ns/data/User/42> <http://prorm.dev/ns/prop/name> "Ada" .
}

Configuring it#

Nothing above is hard-coded. Every base can be replaced, and individual names
can be pinned onto an existing vocabulary:
const graph = store.graph({
  base: 'https://example.org/kb',        // moves all four bases at once
  // …or set them individually:
  classBase:    'https://example.org/kb/class/',
  propertyBase: 'http://xmlns.com/foaf/0.1/',
  edgeBase:     'https://example.org/kb/rel/',
  dataBase:     'https://example.org/kb/id/',

  classIris:    { User: 'http://xmlns.com/foaf/0.1/Person' },
  propertyIris: { name: 'http://xmlns.com/foaf/0.1/name' },
  edgeIris:     { HAS_POSTS: 'http://schema.org/author' },

  idProperty: 'uid',        // which attribute is the node's identity
  graph: 'urn:graph:main',  // scope every pattern and write to a named graph
  defaultLanguage: 'en',    // tag bare string literals
});
Set them once at construction instead, and store.graph() picks them up:
new FusekiStore({ dataset: 'kb', graphMapping: { base: 'https://example.org/kb' } });

Why these choices#

Full IRIs, never prefixed names. Everything is emitted as an absolute IRI
inside <…>. A prefixed name's local part has to satisfy the PN_LOCAL
grammar, which plenty of real attribute names violate (first name,
2fa_enabled, price$); emitting one anyway is either a parse error at the
server or, worse, a different IRI than intended. An absolute IRI has one rule
to get right instead of two.
Identity is an IRI derived from the id, not a blank node. A blank node has
no name outside the result set it appeared in — you cannot come back and
UPDATE, DELETE or link to one. Since a node's IRI is a pure function of its
label and its id, the subject is known before a request runs, which is exactly
what makes upsert and delete expressible without a lookup. The cost: changing
an id is a delete plus a create, not an update.
Blank nodes read back fine;
they are just not something this dialect writes.
IRI segments are percent-encoded, not safeIdent-ed. The Cypher and Gremlin
dialects sanitize identifiers with safeIdent, which maps everything outside
[A-Za-z0-9_] to _. That is not injective, so labels Book/Chapter and
Book_Chapter would collapse onto one class IRI and silently share storage.
Percent-encoding round-trips. (safeIdent is still used in this dialect — for
SPARQL variable names, where the grammar really does want that character set.)
A plain JavaScript string is always a literal. To write an IRI you must say
so:
import { iri } from 'ts-prorm-orm/graph/sparql';
await graph.createNode('User', { id: 1, homepage: iri('https://ada.example') });
If "https://…" were auto-detected as an IRI, a homepage attribute holding a
URL string would silently become a link and stop matching the value it was
stored as.
RDF has no null. Setting a property to null or undefined deletes its
triples; there is no "present but empty". A where: { nickname: null } compiles
to !BOUND(?v_nickname).
Properties are multi-valued. Nothing in RDF stops two name triples on one
subject. A match then yields one row per combination — exactly as a SQL join
would — and findNodes folds repeated values into an array. Use distinct: true
and explicit attributes to keep this predictable.

Datatypes#

JavaScriptRDF term
'Ada'"Ada" (or "Ada"@en with defaultLanguage)
36"36"^^xsd:integer
1.5"1.5"^^xsd:decimal
Infinity / NaN"INF"^^xsd:double / "NaN"^^xsd:double
true"true"^^xsd:boolean
new Date(...)"…"^^xsd:dateTime
iri('http://…')<http://…>
lang('bonjour', 'fr')"bonjour"@fr
typed('P1D', xsdDuration)"P1D"^^<…#duration>
The datatypes are not cosmetic. A number written as a plain "10" literal sorts
and compares as a string, so age > 9 would exclude 10.

Injection: what is escaped, what is validated#

SPARQL has no wire-level parameter binding. A SPARQL request is one string;
there is no $p0 the server substitutes for you. (RDF4J and Jena expose
setBinding in their Java APIs, not over HTTP.) So the work is split in two,
and the split is the security boundary:
1.
Compilation never sees a value become text. Every compile* method
returns { text, params } where text contains only ?_p0-style
placeholders. This is a property you can test, and the test suite does:
expect(stmt.text).not.toContain('Ada').
2.
One function turns placeholders into text. bindSparql(text, params) in
src/graph/sparql/terms.ts is the only place a value is serialized, through
serializeTerm, which can only emit a well-formed RDF term. The dialect's
query() calls it immediately before dispatch; nothing else concatenates.
Within that:
Literals are escaped. A lexical form is emitted inside "…" with \,
", LF, CR, TAB, BS and FF replaced by their SPARQL escapes (backslash first,
so the escapes this introduces are not double-escaped). An escaped literal
cannot contain an unescaped ", so it cannot terminate its own quote — which
is the entire injection surface for a literal.
IRIs are validated and rejected, never escaped. <…> has no escape
mechanism for the characters that would break it, so there is nothing to
escape to. An IRI containing <, >, ", {, }, |, ^, `,
\, a space or an ASCII control character throws a ValidationError. That is
exactly the set IRIREF excludes in the SPARQL grammar. An IRI with no scheme
is also rejected.
Language tags are validated against SPARQL's LANGTAG production.
Datatype IRIs go through the same IRI validation.
IRI path segments are percent-encoded first, then the assembled IRI is
validated — encoding makes a hostile label harmless, and the validation
catches a malformed configured base, which is never encoded.
IN binds one placeholder per element. SPARQL's IN takes an expression
list, not a collection value, and the only way to serialize an array into an
expression list is concatenation — which is the thing being avoided.
An unbound placeholder throws. A leftover ?_p3 is a syntactically valid
SPARQL variable, so the server would run the query unconstrained and return
the wrong rows rather than an error.
Labels, property names and edge labels are interpolated, not bound — no
graph language lets you parameterise them — so every compile entry point calls
requireGraphLabel() first. A missing label throws instead of producing
<…/class/undefined>, which is a perfectly good IRI that nothing is stored
under: the query would run, match nothing, and report nothing wrong.

What compiles, and what refuses to#

compile* methodCompiles to
compileMatchSELECT with OPTIONAL property bindings + FILTER, ORDER BY, LIMIT/OFFSET, COUNT, DISTINCT, includes as OPTIONAL edge hops
compileCreateNodeINSERT DATA
compileUpdateNodesDELETE … INSERT … WHERE
compileDeleteNodestwo DELETE … WHERE operations in one request
compileUpsertNodeDELETE … WHERE then INSERT DATA, keyed on the identity property
compileIncrementDELETE … INSERT … WHERE with COALESCE(?old, 0)
compileCreateEdgeINSERT … WHERE between two matched endpoint sets
compileTraverseproperty paths — a UNION of fixed-length paths, or +/*
compileShowLabelsSELECT DISTINCT ?label over the class base
compileCreateConstraint✗ GraphCapabilityError
compileDropConstraint✗ GraphCapabilityError
compileCreateIndex✗ GraphCapabilityError
compileDropIndex✗ GraphCapabilityError

Why those four throw#

Constraints. RDF has no uniqueness or existence constraints. SHACL and OWL
describe and validate shapes; they are not DDL, they are not universally
supported, and a CREATE CONSTRAINT that silently does not constrain is worse
than an error. Use your store's own validation (Stardog ICV, GraphDB SHACL).
createTable() still works — it registers the node model and its schema hints —
it just does not try to materialize primary keys and unique columns as
constraints the way the Cypher dialect does.
Indexes. A triplestore indexes every triple by construction. Everything
beyond that is vendor-specific and outside SPARQL: Stardog search indexes,
GraphDB Lucene/vector connectors, Virtuoso's RDF_OBJ_FT_RULE_ADD.

Other refusals#

Interactive transactions. The SPARQL 1.1 Protocol has no transaction API.
startTransaction() throws. The atomic unit you actually get is one update
request, which may contain several ;-separated operations — which is how
delete and upsert are compiled here.
Upsert on a non-identity key. What makes an upsert work at all is that the
subject IRI is a pure function of the id, so no lookup is needed. On any other
key it would need one, and SPARQL cannot do "find it or mint one" atomically.
A three-operation approximation would look correct and quietly depend on the
store treating one request as one transaction.
Edge properties. A triple has three slots and none is free for
since: 2019. RDF-star quoted triples solve it, but they are not part of
SPARQL 1.1 and a store without them rejects the whole update — so they are
opt-in per store (rdfStar: true). Stardog, Fuseki and GraphDB default to on.
Bounded traversal deeper than 32 hops (configurable via
maxPathExpansion) — see below.

Traversal depth and property paths#

SPARQL 1.1 property paths have ^, /, |, *, +, ? and ! — but no
bounded repetition
. elt{n,m} was in the working draft and was dropped from
the Recommendation, so emitting <e>{1,3} is a parse error on a conforming
store. A bounded depth is therefore expanded into a UNION of fixed-length
sequence paths, which is exact and portable:
graph.compileTraverse({ label: 'User', edgeLabel: 'KNOWS', maxDepth: 3 })
{ ?s <…/edge/KNOWS> ?t . }
UNION { ?s <…/edge/KNOWS>/<…/edge/KNOWS> ?t . }
UNION { ?s <…/edge/KNOWS>/<…/edge/KNOWS>/<…/edge/KNOWS> ?t . }
maxDepth: Infinity uses + instead (or * when minDepth is 0, or a fixed
prefix followed by + when minDepth > 1). direction: 'in' inverts the path
with ^; 'both' alternates with (<e>|^<e>).

Two projection shapes#

RDF does not tell you which properties a subject has, so compileMatch picks
one of two shapes:
Flat — when the attribute list is known (from attributes, or from a node
model registered with defineNode / createTable), each attribute gets its
own projected variable. One row is one node, and ORDER BY/LIMIT mean what
they say. Prefer this.
Open — otherwise, SELECT ?s ?p ?o over a subquery that has already
applied the filter, ordering and slice to the subjects. Without the
subquery, LIMIT 10 would limit triples rather than nodes and return
three-and-a-half users. The order variables are projected out of the subquery
and the ORDER BY repeated outside, because a subquery's ordering is not
guaranteed to survive the outer join.
findNodes() folds either shape back into node objects, each carrying its
subject IRI as $iri.

Why property bindings are OPTIONAL#

Every where field compiles to OPTIONAL { ?s <prop> ?v_field } plus a
FILTER, never to a plain triple pattern. A plain pattern is a join, and that
would mean:
{ nickname: { [Op.ne]: 'x' } } drops every node with no nickname at all;
Op.isNull can never match, because the pattern binding the variable would
have to exist first;
one leg of an Op.or annihilates the whole solution instead of being false.
OPTIONAL + FILTER reproduces the ORM's three-valued semantics.

Where the six stores differ#

All six satisfy the SparqlExecutor interface (query / update / ask plus
the lifecycle), which is how graph() works without the dialect owning any HTTP
of its own. They differ in ways that matter:
StoreQuery pathUpdate pathrdfStar defaultNotes
Stardog/<db>/query/<db>/updateonICV is database-level validation, not SPARQL DDL
Fuseki/<ds>/query/<ds>/updateonJena implements RDF-star
GraphDB/repositories/<r>/repositories/<r>/statementsonfull-text/vector indexes are connectors, not CREATE INDEX
Blazegraph/blazegraph/namespace/<ns>/sparqlsameoffits <<…>> "RDR" predates and differs from RDF-star
Virtuoso/sparqlsameoffcommonly anonymous read-only — see below
AllegroGraph/repositories/<r>sameoffper-triple metadata is triple attributes, not RDF-star
Because three of the six split query and update across two paths, the dialect
classifies every compiled statement by its leading keyword (skipping comments
and PREFIX/BASE headers — without stripping # globally, since # is a
legal and common IRI character) and posts it to the right one. An update sent to
a query path is a 400, not a no-op.

Read-only endpoints#

Virtuoso's public /sparql is very often exposed with anonymous read-only
access. Declare it, and an update fails naming the reason instead of coming back
as an opaque SPARQL Request Failed / 37000 permission fault:
const store = new VirtuosoStore({
  endpoint: 'https://dbpedia.org',
  graphMapping: { readOnly: true },
});
await store.graph().createNode('User', { id: 1 });
// GraphCapabilityError: "SPARQL 1.1 Update (endpoint configured read-only)" is
// not supported by the virtuoso graph dialect
Nothing is sent to the server.

Escape hatch#

The dialect covers the ORM shape. Anything outside it still goes through the
store directly:
await store.query(`
  PREFIX foaf: <http://xmlns.com/foaf/0.1/>
  SELECT ?name (COUNT(?friend) AS ?n) WHERE {
    ?p foaf:name ?name ; foaf:knows ?friend .
  } GROUP BY ?name HAVING (COUNT(?friend) > 5)
`);
Raw results come back exactly as the endpoint sent them (SPARQL JSON bindings,
unflattened) — that is the point of an escape hatch. The dialect's own
findNodes / query flatten typed literals into JavaScript values.

See also#

Graph databases — the shared dialect contract, and the
Cypher / Gremlin / DQL implementations of it
Store pages: Stardog, Fuseki,
Blazegraph, GraphDB,
Virtuoso, AllegroGraph
Modified at 2026-10-04 00:40:16
Previous
Seeders
Next
SQL constants
Built with