Skip to content

Column Operations

End-to-end flows for column CREATE, UPDATE, and DELETE. Column operations are the most nuanced in the system because they can be synchronous (presentation-only changes), asynchronous (schema changes via the processor), or a mix of both in a single request.

Column CREATE

Creates a new column in the view — always asynchronous (requires ALTER TABLE ADD COLUMN on the customer database).

Response: 202 Accepted

Frontend (Spread)                  Quarkus REST                         RabbitMQ              Processor (Spring Boot)
─────────────────                  ────────────                         ────────              ───────────────────────
User clicks "Add Column"


NgRx action: createColumn


ColumnCrudEffects
    │ POST /api/metadata/columns

ColumnResource.createColumn()


ViewColumnService
  .createByViewUuid()

    ├── Creates ColumnMetadata
    ├── Creates ViewColumn
    ├── Creates Constraints (if provided)
    └── Queues outbox message

                                                              MetadataUpdateProducer
                                                            .onColumnChange(CREATE)


                                                                  metadata-updates
                                                                     exchange


                                                              EntityMetadataConsumer

                                                              processColumnMetadata()


                                                              ALTER TABLE ADD COLUMN
                                                              (via FlywayMigrationService)


                                                              TaskCompletionProducer
                                                                .sendSuccess()


                                                                  task-completion
                                                                     exchange
    ┌─────────────────────────────────────────────────────────────────────┘

Quarkus TaskCompletionConsumer


ColumnOperationHandler
  .handleCreate()


SSE: metadata.view.column.created


Frontend receives SSE


NgRx action: columnAddedFromSSE


Reload view data

Frontend Details

  • Trigger: "Add Column" button in the properties panel or column header menu
  • Effect: ColumnCrudEffects dispatches POST /api/metadata/columns with column metadata (name, type, constraints)
  • Optimistic update: Column is added to the UI immediately with a pending state. On SSE confirmation, the pending state is cleared. On failure, the column is removed and a notification is shown.

Backend Details

  • ColumnResource.createColumn() validates the request and delegates to ViewColumnService.createByViewUuid()
  • The service creates both a ColumnMetadata record (schema-level) and a ViewColumn record (view-level presentation)
  • advancedOptions.defaultValue is validated against the column type (e.g., boolean only accepts true/false, strings must be single-quoted). Invalid defaults return 400.
  • If the request includes a constraints array, each constraint is persisted sequentially via ConstraintService.create() (auto-assigns priority, validates compatibility)
  • The outbox message is written in the same transaction as the metadata records
  • The MetadataUpdateProducer publishes to the metadata-updates exchange with routing key for column operations

Processor Details

  • EntityMetadataConsumer.processColumnMetadata() receives the message
  • HibernateSchemaTools.generateMigrationDDL() produces ALTER TABLE {table} ADD COLUMN {name} {type} DDL
  • FlywayMigrationService.applyMigration() executes the DDL and records a WorkspaceSchemaMigration entry
  • On success: TaskCompletionProducer.sendSuccess() with the column metadata
  • On failure: TaskCompletionProducer.sendFailure() with the error — the column metadata records are not rolled back (they remain in the metadata DB but the physical column doesn't exist)

Completion

  • ColumnOperationHandler.handleCreate() on the Quarkus side processes the task completion
  • SSE event metadata.view.column.created is broadcast to all connected clients in the workspace
  • Frontend SSERoutingEffects routes the event to the columnAddedFromSSE action
  • The view data is reloaded to include the new column

Column UPDATE (Schema Change)

Updates a column with changes that require DDL modification (e.g. type change, nullable toggle). This is the most complex column operation because a single update request can contain both sync and async changes.

Response: 200 OK (sync-only) or 202 Accepted (contains schema changes)

Frontend (Spread)                  Quarkus REST                         RabbitMQ              Processor (Spring Boot)
─────────────────                  ────────────                         ────────              ───────────────────────
User edits column type
or advanced options


NgRx action: updateColumn


ColumnSchemaEffects
    │ PUT /api/metadata/columns/{uuid}

ColumnResource.updateColumn()


ViewColumnService
  .updateWithSchemaDetection()

    ├── SchemaChangeDetector
    │     .detectChanges()
    │         │
    │         ├── Sync changes → applied immediately
    │         │   (displayName, position, etc.)
    │         │
    │         └── Schema changes detected?
    │             │
    │             ├── YES → MetadataUpdateProducer
    │             │           .onColumnSchemaChange()
    │             │                    │
    │             │                    ▼                        EntityMetadataConsumer
    │             │            metadata-updates    ──────────►       │
    │             │               exchange                   processColumnMetadata()
    │             │                                                  │
    │             │                                          ALTER TABLE ALTER COLUMN
    │             │                                                  │
    │             │                                          TaskCompletionProducer
    │             │                                            .sendSuccess()
    │             │                                                  │
    │             │   ◄──────────────────────────────────────────────┘
    │             │         task-completion exchange
    │             │
    │             └── NO → return 200


Response: 200 (sync) or 202 (async)

    ── If 202 ──────────────────────────────

SSE: metadata.column.schema.changing


Frontend: column lock activated
    │ (column shows lock icon,
    │  edits disabled)

    ... processor executes DDL ...

SSE: metadata.column.schema.changed


Frontend: column lock released


Reload column metadata + data

The Sync/Async Split

The SchemaChangeDetector inspects the update payload and classifies each field change:

Change TypeExample FieldsProcessing
Presentation (sync)displayName, hidden, width, position, descriptionApplied immediately to metadata DB
Schema (async)columnType, nullable, defaultValue, uniqueQueued for processor via RabbitMQ

A single request can trigger both paths. The sync changes are committed first, then the async schema change is queued. The response is 202 if any schema change was detected, 200 if all changes were presentation-only.

Column Locking

When a schema change is in progress:

  1. SSE event metadata.column.schema.changing is broadcast with the column UUID
  2. Frontend adds the column UUID to columnLocks state
  3. The column header shows a lock indicator
  4. Cell editing is disabled for the locked column
  5. On completion, metadata.column.schema.changed fires
  6. Frontend removes the column from columnLocks

This prevents users from editing data in a column while its type is being changed.

Frontend Details

  • Effect: ColumnSchemaEffects handles updates that may involve schema changes
  • Detects the response code: 200 means done, 202 means async in progress
  • On 202: dispatches columnSchemaChangeStarted which adds the column to columnLocks
  • On SSE completion: dispatches columnSchemaChangeCompleted which removes the lock and reloads

Error Handling

  • If the processor fails the DDL (e.g. incompatible type conversion), TaskCompletionProducer.sendFailure() is sent
  • The column metadata is reverted to its pre-change state
  • SSE event metadata.column.schema.failed notifies the frontend
  • Frontend removes the lock and shows an error notification

Column UPDATE (Presentation Only)

Updates column properties that don't affect the database schema — purely synchronous, no processor involvement.

Response: 200 OK

Frontend (Spread)                  Quarkus REST
─────────────────                  ────────────
User changes column
display name, width,
visibility, or position


NgRx action: updateColumn


ColumnCrudEffects

    ├── Presentation-only props?
    │   (displayName, hidden,
    │    width, position)

    ├── YES → Skip API call
    │         Update local state only
    │         (some props like position
    │          still need API call)

    └── Full update needed →
        PUT /api/metadata/columns/{uuid}


        ViewColumnService
          .updateWithAffected()

            ├── Updates ViewColumn
            │   in metadata DB

            └── Returns affected
                column UUIDs


        SSE: metadata.view.column.updated


        Other clients receive update

Frontend Optimization

ColumnCrudEffects classifies updates before deciding whether to call the API:

  • Local-only changes (e.g. column width during drag): applied to NgRx state directly, no API call
  • Persisted presentation changes (e.g. rename, hide/show, reorder): PUT to the API, sync response
  • Schema changes: routed to ColumnSchemaEffects instead (see above)

SSE Event

The metadata.view.column.updated event includes:

  • The updated column metadata
  • affectedColumnUuids — list of columns whose position changed (for reorder operations)
  • originClientId — for echo prevention (the tab that made the change doesn't re-apply it)

Column DELETE

Deletes a column — asynchronous for regular columns (requires DROP COLUMN), synchronous for relationship/virtual columns.

Response: 200 OK (virtual) or 202 Accepted (regular)

Frontend (Spread)                  Quarkus REST                         RabbitMQ              Processor (Spring Boot)
─────────────────                  ────────────                         ────────              ───────────────────────
User deletes column
(confirm dialog)


NgRx action: deleteColumn


ColumnCrudEffects
    │ DELETE /api/metadata/
    │   columns/{uuid}

ColumnResource.deleteColumn()


ViewColumnService

    ├── Is relationship/virtual column?
    │   │
    │   ├── YES → Delete ViewColumn
    │   │         + ColumnMetadata
    │   │         from metadata DB only
    │   │         (no DROP COLUMN needed)
    │   │         → return 200
    │   │
    │   └── NO (regular column) →
    │         MetadataUpdateProducer
    │           .onColumnChange(DELETE)
    │                    │
    │                    ▼                        EntityMetadataConsumer
    │            metadata-updates    ──────────►       │
    │               exchange                   processColumnMetadata()
    │                                                  │
    │                                          ALTER TABLE DROP COLUMN
    │                                                  │
    │                                          TaskCompletionProducer
    │                                            .sendSuccess()
    │                                                  │
    │   ◄──────────────────────────────────────────────┘
    │         task-completion exchange


ColumnOperationHandler
  .handleDelete()

    ├── Delete ViewColumnPath records
    ├── Delete ViewColumn records
    └── Delete ColumnMetadata record


SSE: metadata.view.column.deleted


Frontend: columnDeletedFromSSE


Remove column from state,
reload view data

Cascade Delete Order

When the processor confirms a successful DROP COLUMN, the ColumnOperationHandler.handleDelete() cascade deletes in this order:

  1. ViewColumnPath — any path references to this column
  2. ViewColumn — the view-level column record (may exist in multiple views)
  3. ColumnMetadata — the schema-level column definition

Optimistic Delete

The frontend uses optimistic deletion:

  1. Column is immediately hidden from the UI on delete action
  2. If the backend returns success (or SSE confirms), the column is permanently removed from state
  3. If the backend returns failure, the column is restored and an error notification is shown

Relationship Column Delete

Relationship columns (foreign key references) are virtual — they don't correspond to a physical database column. Deleting them:

  • Removes only the ViewColumn and ColumnMetadata records
  • Does not send a message to the processor
  • Does not drop any column from the customer database
  • Returns 200 OK (synchronous)

Response Code Summary

OperationSync/AsyncSuccess CodeNotes
Column CREATEAsync202 AcceptedAlways requires DDL
Column UPDATE (schema)Async202 AcceptedSchema changes queued
Column UPDATE (presentation)Sync200 OKNo DDL needed
Column UPDATE (mixed)Both202 AcceptedSync applied first, async queued
Column DELETE (regular)Async202 AcceptedRequires DROP COLUMN
Column DELETE (relationship)Sync200 OKVirtual only, no DDL

Cross-References

SchemaStack Internal Developer Documentation