Skip to content

Data Layout

SAL separates source files from generated data. Your Git repository contains RDF source files and project code; .sal/data contains generated output.

After sal init, a project has:

.sal/
data/

After sal build, .sal/data contains the built data product. For the main triples table, SAL writes an Apache Iceberg table under the project namespace:

.sal/
config.jsonld
data/
blobs/
<sha256-of-a-vocabulary-document>
<git-project-name>/
triples/
data/
metadata/

The metadata directory contains Iceberg table metadata, manifest lists, manifests, and version-hint.text. The data directory contains the table’s data files.

The files named by a hash under .sal/data/blobs are the vocabulary documents the project pins. They are described in Pinned vocabularies below. sal serve serves them over HTTP at /blobs/{sha256}.

SAL builds RDF into rows representing triples, with the object split across one column per kind of term so that each can be compared and read natively:

  • subject
  • predicate
  • object_string: a string or any other literal without a typed column, or a blank node with its _: prefix
  • object_iri: an IRI object
  • object_geometry: a GeoSPARQL wktLiteral, stored as an Iceberg geometry
  • object_byte: an xsd:byte literal, stored as an INTEGER (Iceberg has no 8-bit type)
  • object_integer: an integer-valued XSD literal (xsd:integer, xsd:int, xsd:long, …), stored as a BIGINT
  • object_float: an xsd:float, xsd:double, or xsd:decimal literal, stored as a DOUBLE
  • object_time: an xsd:dateTime literal with an explicit timezone, stored as a TIMESTAMP normalized to UTC
  • object_type: the datatype IRI the literal object was written with; NULL for an IRI or blank node object
  • triple_hash

Exactly one of the object value columns is set on each row, chosen by the literal’s declared datatype rather than by what its text happens to parse as. A typed literal whose lexical form does not fit its column — an out-of-range xsd:byte, a zoneless or sub-microsecond xsd:dateTime — is stored in object_string, and any datatype without a typed column (booleans, dates, custom types) lands there too. object_type keeps the exact datatype in every case, so exporting the table back to RDF (sal export) reproduces the original typed literal even when the value was stored as a string. The geometry column is what makes the table an Iceberg format version 3 table.

The hash identifies a triple from its subject, predicate, object, and the object’s datatype. SAL uses this identity to avoid writing duplicate triples when the table already contains the same data.

Before writing output, SAL validates RDF source files. Source data can be Turtle or JSON-LD.

Validation checks that terms used through prefixes are defined by the provided vocabularies. For example, a misspelled term such as schema:nameee should fail validation rather than becoming a built triple.

Use sal validate to run these checks without writing a new build.

.sal/config.jsonld is a single JSON-LD document with one @graph that condenses everything a project records about itself and the vocabularies it resolves against: the project’s own ontology (what sal import writes) and the pinned vocabulary versions (what sal build writes). Both used to be separate files, .sal/ontology.jsonld and .sal/ns-prefix-versions.jsonld; they are now nodes in the same @graph, so a project has a single file describing everything it resolves and materializes against.

The first node in the graph, if the project has run sal import at least once, is the project’s own ontology:

{
"@id": ".",
"@type": "owl:Ontology",
"dc:title": "my-project",
"owl:imports": [{ "@id": "http://www.w3.org/2002/07/owl#" }],
"rdfs:comment": "Represents the ontology for the my-project project overall and all the vocabularies that are directly materialized into the graph, not just referenced."
}

dc:title defaults to the Git project name and --title overrides it; rdfs:comment is always generated. Ontologies listed with owl:imports are merged at the version pinned for them; pinning a vocabulary does not import it, so a vocabulary the project validates against but does not import stays out of the triples table.

Vocabularies are resolved per project rather than from a shared cache on the machine. sal build records the exact version of every vocabulary the project resolves against as the rest of .sal/config.jsonld’s @graph, one node per vocabulary, distinguished from the project ontology node by carrying owl:versionIRI:

{
"@context": {
"dc": "http://purl.org/dc/elements/1.1/",
"owl": "http://www.w3.org/2002/07/owl#",
"dcterms": "http://purl.org/dc/terms/",
"xsd": "http://www.w3.org/2001/XMLSchema#",
"rdfs": "http://www.w3.org/2000/01/rdf-schema#"
},
"@graph": [
{
"@id": ".",
"@type": "owl:Ontology",
"dc:title": "my-project",
"rdfs:comment": "Represents the ontology for the my-project project overall and all the vocabularies that are directly materialized into the graph, not just referenced."
},
{
"@id": "https://schema.org/",
"@type": "owl:Ontology",
"owl:versionIRI": { "@id": "urn:sha256:c3a1…" },
"dcterms:format": "application/ld+json",
"dcterms:modified": { "@value": "2026-08-11T17:21:12Z", "@type": "xsd:dateTime" },
"rdfs:comment": "Represents the cached version of the https://schema.org/ vocabularies as of 2026-08-11T17:21:12Z"
}
]
}

The document behind each pin is stored under .sal/data/blobs named by that same SHA-256. config.jsonld belongs in Git; the documents it names do not, since sal init adds .sal/data to .gitignore and they travel with the data product instead.

On each build, SAL:

  1. Collects every prefix the project’s source files declare, building any SAL module it references so the module’s own ontology counts as one of them.
  2. Resolves each prefix that is not already pinned, hashes the document it received, writes it under .sal/data/blobs, and records the mapping.
  3. Resolves every prefix that is already pinned from the stored document rather than from the network.

Validation therefore checks terms against the exact version of a vocabulary the project pinned, not against whatever its publisher is serving today. A prefix that is declared but never used is pinned too, so adding a term from it later does not silently change which version the project validates against. A pin whose document is missing locally — as in a fresh clone that has not pulled the data product — is fetched again rather than failing the build.

Use --no-cache on sal build to resolve every prefix from its source again and re-pin it, which is how a project moves to newer vocabulary versions.

Every pinned vocabulary node sal build writes to .sal/config.jsonld is also written into the triples table itself, as an owl:Ontology node carrying the same owl:versionIRI, dcterms:format, dcterms:modified, and rdfs:comment the config file records. This is provenance: it says which exact version of each vocabulary the data next to it was validated against, and it is queryable like any other triple, through sal query --sparql, sal serve’s /sparql endpoint, or the Ontology versions sample in the SPARQL tab of sal serve --with-ui.

/sparql translates SPARQL to DuckDB SQL and only understands basic triple patterns, FILTER comparisons, DISTINCT, and LIMIT — no OPTIONAL and no ORDER BY — so the query stays inside that subset (see SPARQL Query Engine):

PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX owl: <http://www.w3.org/2002/07/owl#>
PREFIX dcterms: <http://purl.org/dc/terms/>
SELECT ?ontology ?versionIRI ?format ?modified
WHERE {
?ontology rdf:type owl:Ontology .
?ontology owl:versionIRI ?versionIRI .
?ontology dcterms:format ?format .
?ontology dcterms:modified ?modified .
}
ontology versionIRI format modified
http://www.opengis.net/ont/geosparql# urn:sha256:6a55c78c2b38c077a31075363ef5e908fa683c61eb059a0fc9828aeb… application/rdf+xml 2026-08-11T17:21:12Z
https://schema.org/ urn:sha256:c3a1e7c9c4b6f6b1a3f5e6d7c8b9a0e1f2d3c4b5a6978869e6fa84d… application/ld+json 2026-08-11T17:21:12Z

A vocabulary the project pins but does not import, one it only checks terms against, shows up here the same as an imported one; whether an ontology’s own statements were merged into the table is a separate question from which version it validated against.

sal push preserves .sal/data as an OCI artifact, including the pinned vocabulary documents. The artifact metadata records the source repository and commit, which lets sal clone recreate the matching source tree and data directory.

sal upload deploys the same data to an object store. For object stores, SAL rewrites a staged copy of the Iceberg metadata so table paths point to the remote table root. The local .sal/data directory is not changed by upload.

For examples, see Publishing Data.

Developed byCenter for Geospatial SolutionsCenter for Geospatial Solutions