# Datasets: schemas and identity A dataset schema defines the columns, types, nullability, and identity of a project table. The schema is fixed when the dataset is created; rows are validated against it on every push. ## Column kinds
Kind Stored as Accepted values
COLUMN_KIND_TEXT TEXT Any UTF-8 string.
COLUMN_KIND_INTEGER BIGINT 64-bit signed integer.
COLUMN_KIND_NUMBER DOUBLE Double-precision number.
COLUMN_KIND_BOOLEAN BOOLEAN true or false .
COLUMN_KIND_TIMESTAMP TIMESTAMP Timestamp with microsecond precision.
In SQL, these appear as `TEXT`, `BIGINT`, `DOUBLE`, `BOOLEAN`, and `TIMESTAMP`. See [SQL reference](/datasets/sql#result-types) for how query result types are mapped. ## Schema rules
Rule Detail
Column names ASCII letters, digits, and underscores, starting with a letter or underscore. Quote names that are not lowercase in SQL.
Uniqueness Column names are unique case-insensitively.
Reserved names __ingested_at and __deleted are reserved.
Column order Significant: ingest and result mapping use schema order.
Nullable A NULL written to a non-nullable column fails the push.
Column types Fixed after creation; the Hub dialog states this explicitly.
Dataset names Start with a letter; use letters, digits, and dashes (for example live-hosts ). Dataset tables and saved views share one namespace per project, so names must be unique case-insensitively across both.
Name length Up to 255 characters.
Description Up to 1024 characters.
Dataset names with dashes must be double-quoted in SQL: `SELECT * FROM "live-hosts"`. ## Identity and reconciliation Identity columns tell the dataset which observations describe the same logical record. With identity configured, the current state keeps only the latest observation per identity (by `__ingested_at`). Without identity columns, every pushed row is a separate record. Use the smallest stable key:
Dataset Identity
Subdomains host (optionally program if the same host appears in several programs)
Live hosts url
Findings fingerprint
DNS history usually no identity — every observation is meaningful
Identity is part of the schema, not the push: ```json { "columns": [ { "name": "host", "kind": "COLUMN_KIND_TEXT", "nullable": false, "identity": true }, { "name": "program", "kind": "COLUMN_KIND_TEXT", "nullable": false, "identity": true }, { "name": "ip", "kind": "COLUMN_KIND_TEXT", "nullable": true }, { "name": "resolved", "kind": "COLUMN_KIND_BOOLEAN", "nullable": true } ] } ``` ## Creating a dataset The Hub's **New dataset** dialog covers the common case and creates datasets **without identity columns**, which is correct for append-only history and for tables where every row is distinct. When you need reconciliation, create the dataset with the API or CLI and mark the identity columns: ```bash bh dataset create \ --project-id "$PROJECT_ID" \ subdomains \ --description "Resolved subdomains per program" \ --schema-json schema.json ``` `schema.json` uses the proto JSON shape: ```json { "columns": [ { "name": "host", "kind": "COLUMN_KIND_TEXT", "nullable": false, "identity": true }, { "name": "program", "kind": "COLUMN_KIND_TEXT", "nullable": false, "identity": true }, { "name": "ip", "kind": "COLUMN_KIND_TEXT", "nullable": true }, { "name": "resolved", "kind": "COLUMN_KIND_BOOLEAN", "nullable": true } ] } ``` The same shape is the `schema` field of `CreateDatasetRequest` — see [API and CLI](/datasets/api-cli). ## Current state and stale rows The service keeps every push as append-only observations and derives the current state per identity. The relation also exposes two reserved columns:
Column Meaning
__ingested_at When the observation was recorded.
__deleted Whether the record is stale. Current-state queries exclude stale rows.
`upsert` leaves identities that are absent from a push untouched. `sync` declares the push authoritative, so identities missing from a full snapshot are marked stale. `DeleteDatasetRows` accepts the same current-state semantics: soft deletion writes `__deleted = true` tombstones and hard deletion rewrites the stored files. See [Overview](/datasets/overview#current-state-and-history). In SQL, you can inspect the history directly: ```sql SELECT "host", "__ingested_at", "__deleted" FROM dataset ``` ## Changing a schema Column names and types cannot be changed after creation, and the update API only changes the dataset name and description. To change the shape: 1. Create a new dataset with the desired schema. 2. Backfill or re-push the rows. 3. Point workflows and views at the new dataset name. 4. Delete the old dataset when you no longer need its history. Deleting a dataset removes its stored files. Views that referenced it fail on their next execution and are marked invalid. ## Limits
Limit Value
Datasets per project 100
Columns per dataset No fixed server limit; keep schemas narrow for query speed
Column name length 255 characters in the Hub UI
Description length 1024 characters
## Next steps - [Pushing dataset rows](/datasets/workflows): push rows with `step.update_dataset`. - [SQL reference](/datasets/sql): query the schema you just designed. - [Structured filters](/datasets/filters): type-aware filtering without SQL. - [Saved views](/datasets/views): reusable queries across datasets. - [API and CLI](/datasets/api-cli): create and inspect datasets programmatically.