sheets

Append row edits to a Sheet's WAL.

Appends a batch of row edits (upserts and tombstones) onto the sheet's write-ahead log (ADR-0031 D4 / DVT-214). Upsert payloads are sealed via the engine broker before storage — dvt's metadata DB stores only ciphertext (ADR-0012). Requires sheet:write and the sheet must have a writeback connectionId set — auto-bound at create time in the Snowflake Native App (ADR-0077 r2, DVT-2905), declared explicitly elsewhere. At most 1000 edits per request.

Each upsert value is checked against its column's declared type before the edit is accepted (DVT-2476). A value the target warehouse could not store is rejected here with 400 rather than written to the WAL, where it would otherwise block every subsequent flush of the sheet.

Values are checked only against the columns declared on the sheet AT THE TIME OF THIS REQUEST. A value whose key matches no declared column is not checked here; if that column is still undeclared at flush time the value is dropped, but if it is declared in the meantime (via PATCH /v1/sheets/{sheetId}) the value is flushed and checked then — and a value that fails at flush blocks every subsequent flush of the sheet. This endpoint cannot re-check edits already in the write-ahead log: their payloads are sealed (ADR-0012) and only the engine can read them. Declare a sheet's columns before appending rows to it.

Changed 2026-08-09: values that this endpoint previously accepted and that then failed later at flush are now rejected with 400 at append time. All of the following were accepted here before and are not any more — existing integrations sending any of them will start receiving 400:

  • a minute-precision time carrying any zone (2024-01-15T10:30Z, 2024-01-15 10:30+05:30, 2024-01-15T10:30-05) — a zone requires SECONDS precision;
  • a named zone after a T-separated time (2024-01-15T10:30:00 UTC) — UTC/GMT are accepted only after a space-separated time;
  • named zones other than UTC/GMT in any position (… PST, … CEST);
  • slash-form (01/15/2024) and month-name (Jan 15 2024) dates;
  • an edit whose values is null ({"op":"upsert","values":null}). This was accepted with 200 and sealed an empty edit into the write-ahead log. values has always been declared as an object and never as nullable, so this is a conformance fix rather than a narrowing of the contract — but it is still an observable 200→400 for anything relying on it. An OMITTED values is likewise rejected; an EMPTY object ({}) remains valid and writes a row whose declared columns are all null.

The enumeration below is the authoritative accepted set; this list names the previously-working forms that fall outside it.

Changed 2026-08-10: an append to a sheet whose declared columns are not unique when matched case-insensitively is rejected with 400. Such a definition has no defined meaning, and it could previously be created, so an existing sheet may be in this state. POST /v1/sheets describes what these sheets do at flush time, per warehouse, per declared-type combination, and per the target table's current columns; it ranges from the flush failing on every attempt, through the submitted value being silently written into both colliding columns, to a sheet that flushes until a later value only one of them accepts jams it — with the rows written before that point already in the warehouse — to the two silently holding values that differ by the warehouse's own coercion. The rejection here applies to any append, including a tombstone-only batch, because the sheet's definition is what is ambiguous. PATCH /v1/sheets/{sheetId} to keep only one of the colliding columns, then retry.

A date/timestamp value must be one of the following spellings exactly (this is the complete list, not a grammar — combinations not listed here, such as a minute-precision time with a zone, are rejected). The enumeration is verified against Postgres and Redshift. Snowflake's AUTO input format is documented to accept a superset, but that has NOT been verified against a live Snowflake — treat this list as the forms dvt validates, not as a guarantee about Snowflake's own parsing:

  • 2024-01-15 — date only
  • 20240115 — date only, ISO 8601 basic
  • 2024-1-5 — date only, unpadded
  • 2024-01-15T10:30 / 2024-01-15 10:30 — minute precision, no zone
  • 2024-01-15T10:30:00 / 2024-01-15 10:30:00 — seconds, no zone
  • 2024-01-15T10:30:00Z / 2024-01-15 10:30:00Z — Z (UTC) zone, after either separator; and any RFC 3339 value, with or without fractional seconds
  • 2024-01-15 10:30:00+05:30 — space-separated, ±HH:MM zone
  • 2024-01-15T10:30:00+05 / 2024-01-15 10:30:00+05 — ±HH zone
  • 2024-01-15T10:30:00+0530 / 2024-01-15 10:30:00+0530 — ±HHMM zone
  • 2024-01-15 10:30:00 UTC — seconds, named zone (UTC or GMT only)

Slash-separated and month-name forms (01/15/2024, Jan 15, 2024) are not accepted even though the warehouse would take them: their meaning depends on the warehouse's date-order setting, which would make 01/02/2024 mean different dates on different deployments. Send ISO-8601.

Any spelling that carries seconds may add a fractional part (2024-01-15 10:30:00.5, 2024-01-15T10:30:00.123+05:30).

Further restrictions, each because the warehouse rejects the value: named zones other than UTC/GMT (send a numeric offset instead — other abbreviations resolve to different instants depending on the warehouse's abbreviation set); the comma decimal mark for fractional seconds (10:30:00,5 — use 10:30:00.5); a zone displacement beyond ±15:59, or whose minutes field is 60 or greater (e.g. +00:60, +0060); year zero; and non-ASCII whitespace padding such as a non-breaking space.

A number must fit NUMBER(38,9) (at most 29 integer digits, and at most 16383 digits after the point) and an integer must fit a signed 64-bit integer.

Precision: a value sent as a bare JSON number is carried as an IEEE-754 double and is therefore exact only to about 15 significant digits, with no error reported — on Postgres 1234567890123456.789 is stored as 1234567890123460 (the exact stored value differs by warehouse; Snowflake rounds the same input to 1234567890123456.8). Send a high-precision number as a JSON string ("1234567890123456.789"), which is carried verbatim, so all 29 integer digits survive.

Note that the column is NUMBER(38,9) either way: digits past the ninth decimal place are silently rounded on every carrier, strings included ("0.1234567895" stores as 0.123456790). The string carrier removes the ~15-significant-digit ceiling, not the scale limit.

The range limits above are applied to the value as it will actually be stored, so a JSON number near a bound may be rejected where the same digits sent as a string are accepted.

Other per-type rules enforced here:

  • uuid — at most 36 characters, counted in characters, not bytes.
  • boolean — a JSON boolean, or one of the strings true/false/t/f/ yes/no/y/n/on/off/1/0 (case-insensitive).
  • Every type — a JSON array or object is rejected; values must be scalars or null.
  • integer — a bare JSON number with a fractional part is accepted rather than rejected on every warehouse, and is ROUNDED when the sheet flushes. On Postgres the rounding is HALVES TO EVEN: 42.5 stores as 42, and 43.5 stores as 44. That is because the value is bound as an IEEE-754 double and the float8→bigint cast uses rint(). It is not the same as a numeric→bigint cast, which rounds halves away from zero (42.5 would give 43) — numeric is not the carrier. The rounding MODE on other warehouses is not specified here; only the fact that a fractional value is rounded rather than refused holds on every warehouse that flushes. (A Redshift sheet never flushes — see POST /v1/sheets — so no rounding behaviour is observable there, even though the value is accepted.) The same value sent as a string ("42.5") is rejected on every warehouse, because a string is bound as text and text conversion does not round.
  • Keys within one values object must not collide case-insensitively ("Title" and "title" together are rejected): columns are matched case-insensitively, so the intended value would be ambiguous.

Two rules depend on the sheet's warehouse, so the same request body can be accepted on one connection and rejected on another:

  • a JSON number for a boolean column is accepted only on Snowflake;
  • the string "NaN" (case-insensitive) for a number column is accepted only on Postgres and Redshift.
post/v1/sheets/{id}/rows

Request

  • Base URL: http://localhost:8080
  • URL: http://localhost:8080/v1/sheets/{id}/rows
  • Auth: one of:
    • API key in cookie dvt_session
    • HTTP bearer

Request body

versioninteger required

The current sheet definition version (optimistic concurrency). Obtain from the sheet's version field; a stale value returns 409.

Response

Edits appended. Returns the assigned seq for each edit and the updated pending stats.

Changes

No recorded changes to this operation across all 1 revision of this API.