> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bijection.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Virtual series

> Declare observations owned by a dataset or connector with the same temporal meaning as a native series

A virtual series gives observations that another owner stores (a dataset
version, a connector source or an approved warehouse capture) the same temporal
meaning as a native series. Bijection keeps no copy: the source owner retains
its versions, permissions and lifetime. The declaration is a virtual table in
your schema with a versioned temporal contract.

Use `defineSeries` when Bijection admits and stores the observations; use
`defineVirtualSeries` when they already live elsewhere.

## Declare the series

```ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { defineVirtualSeries } from "bijection/server";
import { warehouseReadings } from "./datasets";

export const temperatures = defineVirtualSeries({
  source: warehouseReadings,
  temporal: {
    key: ["tankId", "installationId"],
    time: "observedAt",
    observations: {
      kind: "revisable" as const,
      key: ["readingId"],
      withdrawal: "withdrawn",
      order: "revision",
    },
    measures: {
      temperature: { field: "temperature", unit: "°C" },
      pressure: { field: "pressureBar", unit: "bar" },
    },
  },
});
```

Add the declaration to your schema like any other table. Deployment checks the
contract against the source's row schema:

| Option | Source field |
| - | - |
| `key` | 1–3 required string, id, int64 or boolean fields naming one series |
| `time` | Required int64: signed epoch microseconds |
| `observations.key` | Required scalar fields identifying an observation within a series |
| `observations.withdrawal` | Required boolean field marking removal of the identified observation; explicit null for a source without withdrawals |
| `observations.order` | Required int64: a business order, not an arrival time |
| `measures` | int64 or float64 fields, optionally null, each with a unit |

Units are the series units, for example `"°C"`, `"K"`, `"bar"`, `"kW"` or
`"ppm"`. A warehouse capture also names its matching `executor`.

## Corrections, withdrawals and gaps

Each observation resolves to its greatest `observations.order` before any time
range applies, so a correction can move an observation to another instant. Each
instant then keeps its greatest order, the same rule a native series applies to
its accepted occurrences:

* identical copies at the winning order count once;
* a contradictory winning value refuses the read, even when a later filter or
  limit would have hidden it;
* an explicit withdrawal removes the identified observation after correction conflict checks;
* a null measure reports an unavailable value at that instant and remains a signal gap boundary;
* an instant with no observation is a gap; nothing is interpolated unless you
  ask for a signal.

Intervals are half-open: `{ from, to }` includes `from` and excludes `to`.

## Read samples

```ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
const samples = temperatures
  .for({ tankId, installationId })
  .measure("temperature")
  .between({ from, to });
```

`between` returns a typed relation of
`{ subject, time_us, source_order, value, unit }`. Use it in an analytical
question or `q.program` admitted through Compute; it is not a native
subscription.

## Summarize a population

`measure(name)` without a key selects every series. Summarize it for each member
of a population, comparing every key field:

```ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
const ranking = q
  .from(tanks)
  .summarizeSeries("temperature", {
    from: temperatures.measure("temperature"),
    interval: { from, to },
    on: (tank, sample) =>
      q.and(
        q.eq(tank.tankId, sample.subject.tankId),
        q.eq(tank.installationId, sample.subject.installationId),
      ),
    measures: (m) => ({ samples: m.count(), peak: m.max((r) => r.value) }),
  })
  .orderBy((tank) => [q.desc(tank.temperature.peak), tank.tankId])
  .limit(100);
```

A member without observations keeps a count of zero and a null maximum. A sample
whose key names another installation does not count for this member.

## Signals

Native and virtual series share one signal vocabulary:

```ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
const signal = temperatures
  .for({ tankId, installationId })
  .measure("temperature")
  .signal({ method: "linear", maxGap: "5m" });
```

A native series' signal plans directly against current native storage. A virtual
series' signal is analytical: `signal.capture(questions, { from, to }, limits)`
first accepts a complete input in one original question, then
`resolve(ctx.series)` evaluates the same expression with the shared series
kernel.

## Object properties

`q.seriesRef(temperatures, { key, measure })` binds a measure to a property of
an ontology view, exactly as for a native series. Reading the object does not
acquire external observations; an analysis captures them explicitly.

## What a virtual series does not do

It creates no native storage, admission ledger, retention owner or permission
namespace, and offers no live subscription. Freshness, versions and access come
from the source owner.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.