Skip to content

TimeSeriesTable reference

TimeSeriesTable manages table lifecycle (create/open/append/add_columns/optimize/vacuum) on the local filesystem.

entity_columns is an ordered identity definition. A table may contain many identities, and one Parquet segment may contain rows for several identities. Different identities may use the same index interval. One complete identity may have at most one row per interval, both within one append and across committed appends.

Registered entity column types are Arrow string, large_string, int32, int64, and uint64. Actual values must be non-null. Composite identity components follow the configured entity_columns order. Incoming integer columns may use the lossless widenings documented below, but signedness changes and unsupported domains are rejected. Persisted identities retain their registered types and are never stringified for comparison.

Python exposes one TimeSeriesTable, not child tables per identity. After registration, entity columns remain ordinary SQL columns for filtering and grouping.

Append Arrow data

TimeSeriesTable.append(source, *, compression=None, max_rows_per_row_group=None, max_bytes_per_row_group=None) accepts these sources:

Source Behavior
pyarrow.RecordBatch Appended as one batch without copying its arrays in Python
pyarrow.Table Its existing chunks are streamed without calling combine_chunks()
pyarrow.RecordBatchReader Batches are consumed lazily
Object implementing __arrow_c_stream__ One schema-bearing Arrow C Stream is requested and consumed

The method returns an immutable AppendReport describing the committed segment and its effective writer settings. Use table.append(source).committed_version when only the new table version is needed. The method does not accept file paths, pandas or NumPy objects, mappings, row iterables, or arbitrary iterables of batches. Convert those inputs to one of the supported Arrow forms explicitly.

The keyword-only settings control the physical layout of the new table-owned Parquet segment:

Setting Default Meaning
compression "zstd" "uncompressed", "snappy", or "zstd"
max_rows_per_row_group 1,048,576 Maximum rows per output row group
max_bytes_per_row_group 128 MiB Maximum estimated encoded bytes per output row group

Both row-group limits apply, and the first one reached closes the active row group. The byte limit is an estimate, not a strict process-memory ceiling, and a single oversized value may exceed it. Settings apply only to the current append and are not persisted in table metadata.

After the table has a canonical schema, append matches top-level fields by name and writes them in canonical order. Nullability must match. Types must match except for these lossless widenings: int8 to int32 or int64, int16 to int32 or int64, int32 to int64, uint8, uint16, or uint32 to uint64, and float32 to float64. Signedness changes, timestamp changes, and nested widening are rejected.

After an explicit add_columns operation, append may omit nullable payload fields, including pre-existing nullable fields. Missing keys and non-nullable fields remain errors. Baseline tables retain their strict missing-field behavior until the first addition.

For a materialized source, pass a table or record batch directly. This example assumes the target table expects the shown schema:

import pyarrow as pa

source = pa.table(
    {
        "ts": pa.array([0, 3_600_000_000], type=pa.timestamp("us")),
        "symbol": pa.array(["A", "A"]),
        "value": pa.array([1.0, 2.0]),
    }
)
report = table.append(source)
new_version = report.committed_version

For streaming ingestion, pass a RecordBatchReader:

batch = pa.record_batch(
    {
        "ts": pa.array([7_200_000_000], type=pa.timestamp("us")),
        "symbol": pa.array(["A"]),
        "value": pa.array([3.0]),
    }
)
reader = pa.RecordBatchReader.from_batches(batch.schema, [batch])
new_version = table.append(
    reader,
    compression="zstd",
    max_rows_per_row_group=4_096,
    max_bytes_per_row_group=128 * 1024 * 1024,
).committed_version

Append imports the source through Arrow C Stream and writes a table-owned Parquet segment. It does not stage or collect the complete input in Python. A RecordBatch or Table remains usable after the call; a reader or other single-use stream is consumed. Once Rust owns the stream, append runs with the Python GIL released.

Unsupported sources raise TypeError. Invalid writer settings, stream exporters, or capsules raise ValueError; writer settings are validated before the source is exported or consumed. Table failures use the library's existing exception hierarchy. Boundary and mid-stream source failures do not commit a new version.

Update row values

report = table.update_rows(source, columns=["quality"], expected_version=computed_from_version)

Both keyword arguments are required. columns accepts a sequence of strings and expected_version accepts an integer in 1..=18446744073709551615, excluding bool. The source accepts the same Arrow C Stream inputs as append. It contains exactly all entity columns, the raw ordered-index column, and the selected payload fields. The core validates schema compatibility and exact one-to-one key matches.

Capture the version before reading and computing assignments. Both the handle and published table must still match it. The operation never refreshes or retries. It overwrites selected values, including explicit nulls in nullable fields; unselected values remain unchanged. Nested destinations replace complete top-level values. A valid empty stream is a version-checked no-op; equal-value nonempty input still commits. See the workflow guide for provenance, error handling, and resource costs.

UpdateRowsReport is read-only. Its seven fields are:

Field Meaning
starting_version Snapshot used to compute assignments
committed_version Published version, or starting version for a no-op
rows_updated Addressed rows, including equal-value assignments
segments_rewritten Affected source segments replaced
source_file_bytes Total affected source Parquet file sizes
replacement_file_bytes Total completed replacement Parquet file sizes
no_op True only for a fully validated source with zero rows

Byte fields exclude scratch, sidecars, and discovery I/O; they are not total I/O measurements. All counts are zero for empty input. The successful committed version equals table.version().

Result of assigning selected payload columns by complete row keys.

committed_version: int property

Published version, or starting_version for empty input.

no_op: bool property

True only for a fully validated source containing zero rows.

replacement_file_bytes: int property

Completed replacement Parquet file sizes, not total IO.

rows_updated: int property

Addressed rows, including equal-value assignments.

segments_rewritten: int property

Affected source segments replaced.

source_file_bytes: int property

Affected source Parquet file sizes, excluding scratch and sidecars.

starting_version: int property

Snapshot used to compute the assignments.

Add nullable columns

TimeSeriesTable.add_columns(columns: pyarrow.Schema) -> int adds the fields in one commit and updates the handle to that version. Pass only new nullable top-level fields. The table must have a canonical schema, normally established by its first successful append. Field order, exact case-sensitive names, types, and nullability are preserved. Dots are literal name characters; use SQL quoting when needed.

Schema/field metadata, including nested metadata, raises ValueError. A non-schema argument raises TypeError. Core validation failures use SchemaMismatchError, including empty schemas, duplicates, existing/key names, unsupported types, and non-nullable additions. Existing fields and key definitions cannot change. Addition fills no historical values and rewrites no data or coverage.

The operation uses the handle's selected version without refreshing or retrying. A stale handle raises ConflictError with expected and found. A create-only commit race retains StorageError and its path; it does not invent a newer observed version. Table failures preserve table_root. Reopen and reconcile before retrying a stale or ambiguous operation. TimeseriesTableError with an ambiguous-commit diagnostic does not guarantee rollback.

After adding fields, replace SQL registrations with Session.register_tstable(name, table_root). An old registration rejects newly planned scans when it detects a schema change. See Add nullable columns for a complete runnable workflow and Table protocol compatibility for unsupported-client rejection.

add_columns(columns: pyarrow.Schema) -> int

Add new nullable top-level fields and return the committed version.

columns describes only the new fields. Names, order, types, and nullable annotations are preserved; schema/field metadata, including nested metadata, is rejected. The table must already have a canonical schema, normally established by its first successful append.

Historical rows read as null without rewriting data or coverage. Later appends may omit nullable payload fields; all keys and the types/nullability of supplied fields remain required. Re-register SQL tables with Session.register_tstable after each addition.

Uses this handle's version without refreshing or retrying. Reopen and reconcile before retrying a stale or ambiguous operation. An ambiguous outcome does not guarantee rollback. The GIL is released during the Rust operation.

Raises:

Type Description
TypeError

If columns is not a pyarrow.Schema.

ValueError

If Arrow metadata is present or the schema cannot be imported.

SchemaMismatchError

If fields violate the core nullable-addition contract.

ConflictError

If the selected version is stale; includes expected and found.

TimeseriesTableError

For protocol, storage, or publication failures. Table errors include table_root; storage errors also preserve path context.

append(source: pyarrow.RecordBatch | pyarrow.Table | pyarrow.RecordBatchReader | _ArrowStreamExportable, *, compression: Literal['uncompressed', 'snappy', 'zstd'] | None = None, max_rows_per_row_group: int | None = None, max_bytes_per_row_group: int | None = None) -> AppendReport

Append Arrow data and return its commit report.

Parameters:

Name Type Description Default
source RecordBatch | Table | RecordBatchReader | _ArrowStreamExportable

A pyarrow.RecordBatch, pyarrow.Table, pyarrow.RecordBatchReader, or another object implementing __arrow_c_stream__. File paths, pandas objects, NumPy arrays, mappings, row iterables, and arbitrary batch iterables are not converted implicitly.

required
compression Literal['uncompressed', 'snappy', 'zstd'] | None

Parquet compression for this append. None uses the Zstd default.

None
max_rows_per_row_group int | None

Maximum rows per output Parquet row group. None uses 1,048,576 rows.

None
max_bytes_per_row_group int | None

Maximum estimated encoded bytes per output Parquet row group. None uses 128 MiB.

None

Returns:

Type Description
AppendReport

Metadata for the segment and table version committed by this call.

Notes

Arrow streams are consumed lazily without staging or collecting the complete input in Python. RecordBatch and Table sources remain usable after append; readers and other single-use streams are consumed. After importing the stream, append releases the GIL. The byte limit is not a strict process-memory ceiling, and a single oversized value may exceed it. Settings apply only to this append and are not persisted in table metadata.

Raises:

Type Description
TypeError

If source is not one of the supported Arrow forms.

ValueError

If a writer setting, Arrow C Stream exporter, or capsule is invalid.

IndexIntervalOverlapError

If incoming coverage overlaps committed coverage for the same entity.

DuplicateIndexIntervalError

If two incoming rows for one entity occupy the same index interval.

SchemaMismatchError

If the Arrow schema does not match the table's established schema.

TimeseriesTableError

For other table, storage, transaction, or stream failures. The exception includes a table_root attribute.

create(*, table_root: str, index_column: str, index_type: Literal['timestamp', 'int64', 'uint64'], index_granularity: str | int, entity_columns: list[str] | None = None, timezone: str | None = None) -> TimeSeriesTable classmethod

Create a new time-series table at table_root.

Parameters:

Name Type Description Default
table_root str

Filesystem directory where the table will be created.

required
index_column str

Name of the ascending ordered-index column.

required
index_type Literal['timestamp', 'int64', 'uint64']

One of "timestamp", "int64", or "uint64".

required
index_granularity str | int

Timestamp interval string such as "1h", or a positive integer for "int64" and "uint64" indexes.

required
entity_columns list[str] | None

Ordered column names that define independent identities within the table. One Parquet segment may contain multiple identities.

None
timezone str | None

Optional timestamp timezone; rejected for integer indexes.

None
Notes

The table's canonical schema is typically adopted on the first successful append.

index_spec() -> dict[str, object]

Return exactly one variant-specific ordered-index specification.

Timestamp:

{
    "index_column": str,
    "entity_columns": list[str],
    "index_type": "timestamp",
    "index_granularity": str,
    "timezone": str | None,
}

Int64:

{
    "index_column": str,
    "entity_columns": list[str],
    "index_type": "int64",
    "index_granularity": int,
}

UInt64:

{
    "index_column": str,
    "entity_columns": list[str],
    "index_type": "uint64",
    "index_granularity": int,
}

open(table_root: str) -> TimeSeriesTable classmethod

Open an existing time-series table at table_root.

optimize() -> OptimizeReport

Rewrite every mixed-entity segment into single-entity segments.

Returns:

Type Description
OptimizeReport

Complete counts and versions for the operation. A successful no-op returns a report with no_op=True and equal starting and committed versions.

Raises:

Type Description
TimeseriesTableError

If optimization is not applicable or rewriting, validation, commit, or cleanup fails. The exception includes a table_root attribute.

root() -> str

Return the table root path.

update_rows(source: pyarrow.RecordBatch | pyarrow.Table | pyarrow.RecordBatchReader | _ArrowStreamExportable, *, columns: Sequence[str], expected_version: int) -> UpdateRowsReport

Atomically assign existing payload columns using complete entity/index keys.

Capture expected_version before reading and computing assignments. Any intervening commit conflicts; the operation never refreshes or retries. Source fields contain exactly all configured keys and selected columns, with compatible types/nullability. Explicit null clears a nullable destination; unselected fields remain unchanged. Nested destinations are assigned as whole values. The source is consumed once with the GIL released, using the same Arrow C Stream boundary as append.

Empty valid input is a version-checked no-op. Nonempty equal-value assignments still commit. Errors leave this handle unchanged. Reopen and reconcile ambiguous outcomes before retrying; they do not guarantee rollback. Newly planned SQL queries see updated values without re-registration. Retained history protects original files.

vacuum(older_than: datetime, *, apply: bool = False) -> VacuumReport

Inspect or delete expired files unreachable from retained table history.

older_than must be timezone-aware, must not be in the future, and should be older than the longest expected writer duration. The default is a non-mutating dry-run. Vacuum does not expire snapshots, rewrite history, or delete transaction-log files.

A deletion failure raises VacuumApplyError; its partial_report records deletions completed before the failure.

version() -> int

Return the current table version.

AppendReport

TimeSeriesTable.append(...) returns this immutable report after a successful commit. Its segment path is table-relative. The row count and file size match the committed segment metadata, while the row-group count comes from the completed Parquet footer.

Result of one successfully committed append operation.

committed_version: int property

Version created by the successful append commit.

compression: Literal['uncompressed', 'snappy', 'zstd'] property

Parquet compression used by this append.

file_size_bytes: int property

Completed Parquet segment size in bytes.

max_bytes_per_row_group: int property

Effective maximum estimated encoded bytes per output row group.

max_rows_per_row_group: int property

Effective maximum rows per output row group.

row_count: int property

Logical rows recorded in the committed segment metadata.

row_group_count: int property

Row groups recorded in the completed Parquet footer.

segment_path: str property

Canonical table-relative path of the committed Parquet segment.

starting_version: int property

Table version used as the optimistic commit base.

OptimizeReport

TimeSeriesTable.optimize() returns this immutable report for both rewrites and successful no-ops.

Optimization may change physical row order. It preserves logical rows, schema, and per-entity coverage, but it does not combine small files or accept a target file size. Replaced source files may remain on disk until a future vacuum operation removes unreferenced files.

Result of one entity-layout optimization operation.

candidate_source_segments: int property

Mixed source segments selected from the starting snapshot.

committed_version: int property

Committed replacement version, or starting_version for a no-op.

distinct_identities_materialized: int property

Unique complete identities represented by the replacements.

no_op: bool property

Whether no mixed live segments required rewriting.

replacement_segments_written: int property

Verified single-entity replacement segments written.

rows_read: int property

Logical rows read from selected source segments.

rows_written: int property

Logical rows written to committed replacement segments.

source_segments_replaced: int property

Selected source segments removed by the committed rewrite.

starting_version: int property

Table version used to select optimization candidates.

Vacuum expired orphan files

An interrupted append can leave an incomplete Parquet file under data/_managed/append/ without adding it to the transaction log. An interrupted entity rewrite can leave files under data/_staged/entity-rewrite/. TimeSeriesTable.vacuum(older_than, *, apply=False) finds expired files in these reserved directories that no valid retained commit references. The default dry-run does not modify the table.

Choose a timezone-aware cutoff older than the longest writer operation you expect, then inspect the plan:

from datetime import datetime, timedelta, timezone

cutoff = datetime.now(timezone.utc) - timedelta(days=7)
plan = table.vacuum(cutoff)

for artifact in plan.artifacts:
    if artifact.disposition == "removable":
        print(artifact.path, artifact.size_bytes, artifact.reason)

Apply the same retention policy after reviewing the plan:

report = table.vacuum(cutoff, apply=True)
print(report.deleted_files, report.deleted_bytes)

The cutoff is exclusive. Files modified at or after it are retained, and a future cutoff raises ValueError. Vacuum also retains files referenced anywhere in valid retained history and unrecognized files. Apply rechecks each candidate's size and modification time before deletion and retains it if either value differs from planning. This check is best effort, not atomic with deletion, so leave enough retention time for active writers to finish.

Parquet files elsewhere under data/ are not vacuum candidates. This includes append source files inside the table root.

VacuumReport.artifacts contains every regular file considered under data/ and _coverage/. Each artifact has a disposition (retained, removable, deleted, or already_absent) and a reason. already_absent means vacuum found a candidate missing before deletion; its last observed size is counted in already_absent_bytes, not deleted_bytes. The report also provides matching file and byte totals.

Apply mode can remove some files before a later deletion fails. In that case, VacuumApplyError.partial_report records the completed deletions and the remaining candidates; VacuumApplyError.path identifies the file that failed. The exception is also a StorageError, so existing storage-error handlers continue to catch it.

Vacuum is orphan-file cleanup. It does not expire snapshots, choose a transaction-log retention boundary, rewrite history, or delete transaction-log files. It scans data/ and _coverage/, but only reserved Parquet paths and recognized coverage paths can be removed.

Vacuum classification for one file below a scanned directory.

disposition: Literal['retained', 'removable', 'deleted', 'already_absent'] property

modified_at: datetime property

path: str property

reason: Literal['referenced_by_commit', 'within_retention', 'changed_since_planning', 'unrecognized_artifact', 'unreferenced', 'invalid_or_unreadable_parquet'] property

referenced_by_commit_version: int | None property

size_bytes: int property

Structured result of one vacuum invocation.

already_absent_bytes: int property

already_absent_files: int property

artifacts: list[VacuumArtifact] property

considered_bytes: int property

considered_files: int property

deleted_bytes: int property

deleted_files: int property

mode: Literal['dry_run', 'apply'] property

older_than: datetime property

removable_bytes: int property

removable_files: int property

retained_bytes: int property

retained_files: int property

table_version: int property