Knowledge from ordinary R tables
Turn related tables into knowledge you can update without losing history.
Start with data frames and a data-dict contract. graft creates a new local store, checks related records before it writes, records where each accepted change came from, and keeps earlier versions available.
Install graft
graft is currently installed from GitHub:
pak::pak("JamesHWade/graft")Using a resolved data-dict contract and an existing .graft.json contract is R-only. The data-dict CLI is needed only to resolve authored YAML, and Python with linkml-runtime is needed only to compile LinkML source.
Start with the data you already have
Suppose an R workflow produces three tables:
| Table | What one row represents | Important fields |
|---|---|---|
organization |
An organization |
id, name
|
person |
A person |
id, full_name, job_title
|
employment |
A person’s employment |
person_id, organization_id
|
Those tables can answer who works where today. They do not, by themselves, tell you whether an employment row points to a known organization, who supplied a correction, what a reviewer accepted, or what the previous job title was.
graft adds stable record identity, relationship checks, source provenance, a review step, and revision history. These are the foundations of useful knowledge: facts that remain connected and explainable as they change.
Describe the tables with data-dict
data-dict gives the tables and their relationships a readable contract. The package includes this example as team-directory.data-dict.yaml; an abridged excerpt of its relationships is:
tables:
- name: person
- name: organization
- name: employment
relationships:
- join: employment.person_id = person.id
- join: employment.organization_id = organization.idThe data-dict CLI resolves the authoring YAML with export-spec. A resolved JSON export can then be compiled by graft using R alone:
library(graft)
resolved_json <- system.file(
"extdata",
"team-directory.data-dict.json",
package = "graft",
mustWork = TRUE
)
schema <- graft_schema(resolved_json)
schema@name
names(schema@classes)The @ operator reads a public property from graft’s immutable S7 contract and plan objects; candidate records themselves remain ordinary data frames.
The full data-dict guide shows how to author, resolve, and inspect the contract. The compiler reference documents the exact supported profiles and build requirements.
Create a new, empty store
You do not need an existing database. The path below does not exist when graft_open() is called; graft creates and initializes it under the compiled contract.
store_path <- tempfile(fileext = ".duckdb")
file.exists(store_path)
#> [1] FALSE
store <- graft_open(schema, store_path, okf = "disabled")Use ":memory:" instead of a file path for a disposable in-memory store.
Catch a broken relationship before writing
Candidate records are a named list of data frames. This batch includes a valid person and organization, but its employment row points to an organization that does not exist.
records <- list(
organization = data.frame(
id = "org:daily-planet",
name = "Daily Planet"
),
person = data.frame(
id = "person:lois-lane",
full_name = "Lois Lane",
job_title = "Reporter"
),
employment = data.frame(
id = "employment:lois-lane:daily-planet",
person_id = "person:lois-lane",
organization_id = "org:missing"
)
)
origin <- graft_provenance(
producer = "directory-import",
idempotency_key = "directory-2026-08-09"
)
plan <- graft_plan(store, records, origin)
plan@valid
#> [1] FALSE
plan@issues[, c("class", "record_id", "field", "message")]The issue reports that org:missing is not a known target. Planning has not written any records or provenance, so the caller can correct the batch and review it again.
Correct, review, and commit the batch
records$employment$organization_id <- "org:daily-planet"
plan <- graft_plan(store, records, origin)
plan@valid
plan@changes[, c("class", "record_id", "action", "changed_fields")]
if (plan@valid) {
graft_commit(store, plan)
}The plan now shows three inserts. graft_commit() rechecks the reviewed plan and accepts all three records together. If a precondition fails, none of the batch is accepted.
Keep the old version when a fact changes
A later review changes Lois’s role. The update plan identifies the field that would change before anything is written.
updated_person <- list(person = data.frame(
id = "person:lois-lane",
full_name = "Lois Lane",
job_title = "Investigative editor"
))
update_origin <- graft_provenance(
producer = "hr-review",
idempotency_key = "hr-review-2026-08-10"
)
update_plan <- graft_plan(store, updated_person, update_origin)
update_plan@changes[, c("record_id", "action", "changed_fields")]
graft_commit(store, update_plan)
graft_get(store, "person:lois-lane")$record
graft_history(store, "person:lois-lane")[
, c("revision_number", "committed_at", "producer", "changed_fields")
]
graft_close(store)
unlink(store_path)Current retrieval returns the new title. History retains both accepted versions and the producer attached to each change.
Add LinkML when the relationships need graph meaning
data-dict is the simpler starting point when the domain is naturally tabular. Its foreign keys let graft reject missing targets, but they are not graph traversal edges. Move to LinkML when you need relationships that support graph traversal, inheritance, ontology identifiers, or polymorphic references.
Both providers compile to the same Graft contract and use the same plan, commit, retrieval, and history functions. The provider changes how the domain is described; it does not create another way to write accepted knowledge. The evaluated LinkML guide accepts a semantic measurement and retrieves the typed materials:testedWith edge between two materials.
Continue learning
- Get started works through the complete data-dict example with its results.
- Use a data-dict contract covers table-first authoring and compilation.
- Review knowledge changes explains plans, commit preconditions, and retries.
- Retrieve current records and history maps each read function to its job.
- Add graph semantics with LinkML continues from governed tables to typed traversal relationships.
- Understand the architecture explains storage, projections, and selective use of S7.