Skip to content

Data Operations

End-to-end flows for cell editing, row insertion, row deletion, and batch cell editing. All data operations are synchronous — they execute SQL directly on the workspace database with no RabbitMQ or processor involvement.

All data operations are synchronous

Unlike column and view operations (which go through the processor for DDL), data operations execute directly against the customer database. The Quarkus REST API runs the SQL and broadcasts the result via SSE.

Cell Edit

Edits a single cell value — direct SQL UPDATE on the workspace database.

Response: 200 OK

Frontend (Spread)                  Quarkus REST                         SSE
─────────────────                  ────────────                         ───
User edits cell value
(inline edit, blur/enter)


NgRx action: editCell


CellEditEffects
    │ PATCH /api/data/cell/{viewUuid}
    │ Body: { rowId, columnUuid, value }
    │ Header: X-Client-Id: {clientId}

ViewDataResource
  .updateCell()


ViewDataService
  .updateCell()

    ├── Resolve column metadata
    │   (type, constraints)

    ├── Validate value
    │   (type checking,
    │    constraint validation)

    ├── SQL UPDATE on
    │   workspace DB

    └── Broadcast SSE event


        SSE: data.cell.edited
        {
          viewUuid, rowId,
          columnUuid, value,
          originClientId
        }

            ├── Same tab (originClientId matches)
            │   → Event ignored (echo prevention)

            └── Other tabs / users
                → Update cell in viewData state

No Optimistic Update

Cell edits do not use optimistic updates. The flow is:

  1. User types new value and commits (blur or Enter)
  2. API request is sent
  3. Cell shows a subtle loading state
  4. On 200: cell is updated with the confirmed value
  5. On 400: cell reverts to previous value, validation error shown

This prevents flickering from rejected edits (e.g. type mismatches, constraint violations).

Validation

The backend validates cell values before writing:

  • Type checking — value must be compatible with the column type (e.g. a string for a VARCHAR column, a number for INTEGER)
  • Constraint validation — checks column constraints (NOT_BLANK, MIN_LENGTH, MAX_LENGTH, PATTERN, EMAIL, URL, MIN, MAX, etc.)
  • Nullable — null values rejected for non-nullable columns

On validation failure, the response is 400 Bad Request with field-level error details.

Echo Prevention

Every browser tab has a unique clientId generated via crypto.randomUUID() (from ClientIdService). This is sent as X-Client-Id on every request and echoed back as originClientId in the SSE event.

When an SSE event arrives, ClientIdService.isOwnEvent(originClientId) returns true if the event originated from this tab — the event is then ignored to prevent double-applying the edit.


Row Add

Inserts a new row — direct SQL INSERT on the workspace database.

Response: 201 Created

Frontend (Spread)                  Quarkus REST                         SSE
─────────────────                  ────────────                         ───
User opens "Add Row" dialog
fills in fields, submits


NgRx action: addRow


RowCrudEffects
    │ POST /api/data/add/{viewUuid}
    │ Body: { field: value, ... }

ViewDataResource
  .addRow()


ViewDataService
  .addRow()

    ├── Validate all fields

    ├── SQL INSERT on
    │   workspace DB

    └── Broadcast SSE event


        SSE: data.row.inserted
        {
          viewUuid, row,
          originClientId
        }

            ├── Same tab
            │   → Ignored (already added
            │     from API response)

            └── Other tabs / users
                → Insert row into viewData
                  (or queue if batching)

Add Row Dialog

The "Add Row" dialog is dynamically generated from the view's column metadata:

  • Each column becomes a form field with the appropriate input type
  • Constraints are applied as form validators
  • Required fields (non-nullable, no default) are marked
  • Relationship fields show a picker component

Adaptive Row Batching (Remote Inserts)

When remote users insert rows at high frequency, the spread app switches to batched insertion to prevent UI jank:

typescript
const BATCH_CONFIG = {
  WINDOW_MS: 1000,           // Rolling time window
  AUTO_INSERT_THRESHOLD: 3,  // Max inserts per window before batch mode
  SHOW_PILL_THRESHOLD: 1,    // Queued rows before showing pill
};
  • If more than 3 remote row inserts arrive within 1 second, batch mode activates
  • New rows are queued into PendingRowsState.rows instead of being inserted into the grid
  • A "N new rows" pill notification appears above the grid
  • User clicks the pill to flush all pending rows at once
  • Batch mode deactivates when the insert frequency drops below the threshold

Row Delete

Deletes a single row — direct SQL DELETE on the workspace database.

Response: 200 OK

Frontend (Spread)                  Quarkus REST                         SSE
─────────────────                  ────────────                         ───
User deletes row
(context menu or keyboard)


NgRx action: deleteRow


RowCrudEffects
    │ DELETE /api/data/item/
    │   {viewUuid}/{rowId}

ViewDataResource
  .deleteRow()


ViewDataService
  .deleteRow()

    ├── SQL DELETE on
    │   workspace DB

    └── Broadcast SSE event


        SSE: data.row.deleted
        {
          viewUuid, rowId,
          originClientId
        }

            ├── Same tab
            │   → Ignored

            └── Other tabs / users
                → Remove row from viewData

Batch Cell Edit

Edits multiple cells in a single column at once — direct SQL UPDATEs on the workspace database.

Response: 200 OK

Frontend (Spread)                  Quarkus REST                         SSE
─────────────────                  ────────────                         ───
User applies bulk value
to selected cells
(e.g. "fill column" action)


NgRx action: batchCellEdit


CellEditEffects
    │ PATCH /api/data/
    │   batch-cell-edit/{viewUuid}
    │ Body: {
    │   columnUuid,
    │   cells: [
    │     { rowId, value },
    │     ...
    │   ]
    │ }

ViewDataResource
  .batchCellEdit()


ViewDataService
  .batchCellEdit()

    ├── Validate each cell value

    ├── Multiple SQL UPDATEs
    │   on workspace DB

    └── Broadcast SSE event


        SSE: data.column.filled
        {
          viewUuid, columnUuid,
          cells: [...],
          originClientId
        }

            └── Other tabs / users
                → Update all affected
                  cells in viewData

Batch vs Bulk

These are distinct concepts:

Batch Cell EditBulk Operations
ScopeMultiple cells in one columnMultiple complete rows
ProcessingSynchronous (direct SQL)Asynchronous (RabbitMQ job)
Response200 OK202 Accepted
ProgressInstantTracked with progress events
PageThis pageBulk Operations

Response Code Summary

OperationMethodSuccess CodeNotes
Cell EditPATCH200 OKSingle cell update
Row AddPOST201 CreatedReturns created row
Row DeleteDELETE200 OKSingle row
Batch Cell EditPATCH200 OKMultiple cells, one column

Cross-References

SchemaStack Internal Developer Documentation