Projections

A projection turns dataset rows into ontology objects, links, and telemetry points. Reach for one when you have clean tables — say invoices and customers pulled from your ERP — and want them to become the Invoice and Customer objects your app reads through sixb.objects(...).

Syncs and pipelines prepare the rows; projections create the objects and links on top of them. defineProjection selects the right builder from its ontology target:

TargetProducesOne row becomes
ObjectTypeObjects (and FK links)One complete object root
ObjectType.l.<linkId>Many-to-many linksOne complete link root
ObjectType.p.<telemetryId>Telemetry pointsOne reading on a series

Object projection

Each row becomes one object. Map object properties to dataset columns with .properties(...). The primary property (usually id) must be mapped.

TS
import { defineProjection } from "@sixb/core"
import { erpCustomersDataset } from "../datasets/erp"
import { Customer } from "../ontology/customer"

export const customerProjection = defineProjection("customer-proj", Customer)
  .fromDataset(erpCustomersDataset)
  .properties({
    id: "customer_id",
    name: "contact_name",
    email: "contact_email",
    company: "company_name",
    industry: "industry_sector",
    tier: "service_tier",
  })
PartMeaning
defineProjection(id, ObjectType)Names the projection and its target object type
.fromDataset(dataset)Chooses the source dataset
.properties({ prop: "column" })Maps object property ids to dataset column names

The object property id reads from column customer_id, name from contact_name, and so on.

Source and managed-edit conflict resolution

By default, an Action or runtime edit to a projected property remains authoritative until application code resets that property. This editsWin policy is useful when Sixb owns the decision.

When the source system remains authoritative, use mostRecent with a non-null timestamp column that contains the source system's own update time:

TS
export const githubIssueProjection = defineProjection("github-issues", GitHubIssue)
  .fromDataset(githubIssues)
  .properties({
    id: "id",
    title: "title",
    body: "body",
    state: "state",
  })
  .resolveConflicts({
    strategy: "mostRecent",
    sourceTimestamp: "updated_at",
  })

Resolution is per property. A source value wins when its timestamp is equal to or newer than that property's Action or runtime edit time; otherwise the managed edit wins. Editing one property does not refresh any other property's edit time. Unmapped properties remain edit-only.

Use the source record's own update time—not dataset ingestion or commit time—because those times do not establish when the source value changed. The timestamp must carry a time zone, progress monotonically for each record, and have enough precision to order source updates against managed edits. Sixb canonicalizes it to UTC, then compares it with the Sixb commit clock, so the source and Sixb clocks must be reasonably synchronized.

When a row carries a foreign key, turn it into an ontology link with .withLinks(...). Each entry is keyed by the link id and uses the inline { link, sourceField, target } descriptor:

TS
import { defineProjection } from "@sixb/core"
import { erpInvoicesDataset } from "../datasets/erp"
import { Customer } from "../ontology/customer"
import { Invoice } from "../ontology/invoice"
import { Project } from "../ontology/project"

export const invoiceProjection = defineProjection("invoice-proj", Invoice)
  .fromDataset(erpInvoicesDataset)
  .properties({
    id: "id",
    number: "number",
    amount: "amount",
    currency: "currency",
    status: "status",
    issuedAt: "issuedAt",
    dueDate: "dueDate",
  })
  .withLinks({
    customer: {
      link: Invoice.l.customer,
      sourceField: "customer_id",
      target: Customer,
    },
    project: {
      link: Invoice.l.project,
      sourceField: "project_id",
      target: Project,
    },
  })

The value in customer_id equals the primary id of a Customer, so the projection creates the Invoice -> Customer link; project_id creates Invoice -> Project.

Descriptor fields

FieldMeaning
linkThe link token from the source object type (SourceType.l.<linkId>)
sourceFieldDataset column holding the target's primary id
sourcePropertyAlternative to sourceField: a projected property token (SourceType.p.<propId>) holding the target id
targetThe target object type (must be the link's declared target or a subtype via extends)

sourceField and sourceProperty are mutually exclusive — provide exactly one. Use sourceField when the foreign key lives only in the dataset; use sourceProperty when you also map that column to an object property and want to reuse it.

The inline descriptor is sugar over the fromForeignKey() helper. The two are equivalent — prefer the inline form; reach for fromForeignKey() only to build a descriptor separately.

When a join dataset stores relationships, target its link token. Each row becomes one link from a source object to a target object, identified by their primary ids.

TS
import { defineProjection } from "@sixb/core"
import { erpProjectMembersDataset } from "../datasets/erp"
import { Project } from "../ontology/project"

export const projectMembersProjection = defineProjection("project-members", Project.l.members)
  .fromDataset(erpProjectMembersDataset)
  .sourceField("project_id")
  .targetField("employee_id")
PartMeaning
defineProjection(id, SourceType.l.<linkId>)Names the projection and its target link
.sourceField("column")Dataset column holding the source object's primary id
.targetField("column")Dataset column holding the target object's primary id

Source and target fields must be string columns.

Telemetry projection

A telemetry projection records timestamped readings onto a telemetry-mode property. Use it when a dataset has one row per measurement: a value, the object it belongs to, and when it was recorded.

First mark the property as telemetry in the ontology (see Properties):

TS
prop("progress", "integer", { mode: "telemetry" })

Then map a dataset of readings onto it with .points(...):

TS
import { defineProjection } from "@sixb/core"
import { erpProjectProgressDataset } from "../datasets/erp"
import { Project } from "../ontology/project"

export const projectProgressProjection = defineProjection(
  "project-progress",
  Project.p.progress
)
  .fromDataset(erpProjectProgressDataset)
  .points({
    objectId: "project_id",
    at: "recorded_at",
    value: "progress_pct",
  })
Mapping keyMeaning
objectIdDataset column holding the target object's primary id
atTimestamp column for the reading
valueColumn holding the reading
unitOptional column holding the reading's unit (required only for properties that carry a unit)

Each row appends one point to the progress series of the Project named by project_id. The at column must be a string, date, or timestamp; values without a time zone (no trailing Z or numeric offset) are read as UTC.

How point identity works — and what re-projecting the same instant does — is covered in Telemetry.

Projection vs pipeline

Pipelines and projections solve different problems: pipelines make better rows, projections make objects.

NeedUse
Clean, filter, join, or reshape rowsPipeline
Create app objects from rowsProjection
Create object relationships from foreign keysProjection
Record timestamped readings on a propertyProjection
Keep data as tablesDataset or pipeline

Registration

Put projection definitions in projections/ and export them. createSixb() discovers them automatically.

TXT
your-project/
  datasets/
    erp.ts
  ontology/
    customer.ts
    invoice.ts
  projections/
    customer-projection.ts
    invoice-projection.ts
  sixb.config.ts

You can also register them explicitly:

TS
import { createSixb } from "@sixb/core"
import { erpInvoicesDataset } from "./datasets/erp"
import { Invoice } from "./ontology/invoice"
import { invoiceProjection } from "./projections/invoice-projection"

export const sixb = await createSixb({
  ontologies: [Invoice],
  datasets: [erpInvoicesDataset],
  projections: [invoiceProjection],
})

See the Runtime overview for how discovery works.

Running projections

A committed dataset version triggers projection execution. In local development, sixb dev co-hosts projection workers when projections are registered. For a separate worker process:

BASH
sixb worker projection

Behavior and validation

  • .properties(...) checks that mapped properties and columns exist and that their types are compatible. The primary property must be mapped.
  • Object and link projections are authoritative replacements for their source. A later dataset version withdraws source-owned objects or links that are no longer present; managed edits remain separate overrides.
  • Link overrides follow ontology cardinality: a many override owns one exact edge, while a one override owns the (source, linkId) slot. A projected target change therefore stays hidden until that slot is reset; it cannot create a second effective target beside the managed one.
  • Replacement snapshots are the only pre-0.1 source materialization protocol. Activation, the durable ontology_commits record, and stable outbox envelopes commit atomically. Dataset CDC or change-stream projection is deferred.
  • An object projection requires one nonblank primary identity per dataset row and one row per object root. Repeated roots fail the run instead of merging partial object state.
  • A nonblank FK contributes a link from that object row. Blank FKs contribute no link. Model cardinality-many relationships with a dedicated link projection, where each dataset row is one complete link root.
  • Link projections require string source and target fields.
  • For an FK descriptor, target must be the link's declared target type or a subtype (via extends).

Search docs

Search the documentation