Appearance
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 dataFrontend Details
- Trigger: "Add Column" button in the properties panel or column header menu
- Effect:
ColumnCrudEffectsdispatchesPOST /api/metadata/columnswith 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 toViewColumnService.createByViewUuid()- The service creates both a
ColumnMetadatarecord (schema-level) and aViewColumnrecord (view-level presentation) advancedOptions.defaultValueis validated against the column type (e.g., boolean only acceptstrue/false, strings must be single-quoted). Invalid defaults return 400.- If the request includes a
constraintsarray, each constraint is persisted sequentially viaConstraintService.create()(auto-assigns priority, validates compatibility) - The outbox message is written in the same transaction as the metadata records
- The
MetadataUpdateProducerpublishes to themetadata-updatesexchange with routing key for column operations
Processor Details
EntityMetadataConsumer.processColumnMetadata()receives the messageHibernateSchemaTools.generateMigrationDDL()producesALTER TABLE {table} ADD COLUMN {name} {type}DDLFlywayMigrationService.applyMigration()executes the DDL and records aWorkspaceSchemaMigrationentry- 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.createdis broadcast to all connected clients in the workspace - Frontend
SSERoutingEffectsroutes the event to thecolumnAddedFromSSEaction - 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 + dataThe Sync/Async Split
The SchemaChangeDetector inspects the update payload and classifies each field change:
| Change Type | Example Fields | Processing |
|---|---|---|
| Presentation (sync) | displayName, hidden, width, position, description | Applied immediately to metadata DB |
| Schema (async) | columnType, nullable, defaultValue, unique | Queued 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:
- SSE event
metadata.column.schema.changingis broadcast with the column UUID - Frontend adds the column UUID to
columnLocksstate - The column header shows a lock indicator
- Cell editing is disabled for the locked column
- On completion,
metadata.column.schema.changedfires - Frontend removes the column from
columnLocks
This prevents users from editing data in a column while its type is being changed.
Frontend Details
- Effect:
ColumnSchemaEffectshandles updates that may involve schema changes - Detects the response code:
200means done,202means async in progress - On
202: dispatchescolumnSchemaChangeStartedwhich adds the column tocolumnLocks - On SSE completion: dispatches
columnSchemaChangeCompletedwhich 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.failednotifies 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 updateFrontend 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):
PUTto the API, sync response - Schema changes: routed to
ColumnSchemaEffectsinstead (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 dataCascade Delete Order
When the processor confirms a successful DROP COLUMN, the ColumnOperationHandler.handleDelete() cascade deletes in this order:
ViewColumnPath— any path references to this columnViewColumn— the view-level column record (may exist in multiple views)ColumnMetadata— the schema-level column definition
Optimistic Delete
The frontend uses optimistic deletion:
- Column is immediately hidden from the UI on delete action
- If the backend returns success (or SSE confirms), the column is permanently removed from state
- 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
ViewColumnandColumnMetadatarecords - Does not send a message to the processor
- Does not drop any column from the customer database
- Returns
200 OK(synchronous)
Response Code Summary
| Operation | Sync/Async | Success Code | Notes |
|---|---|---|---|
| Column CREATE | Async | 202 Accepted | Always requires DDL |
| Column UPDATE (schema) | Async | 202 Accepted | Schema changes queued |
| Column UPDATE (presentation) | Sync | 200 OK | No DDL needed |
| Column UPDATE (mixed) | Both | 202 Accepted | Sync applied first, async queued |
| Column DELETE (regular) | Async | 202 Accepted | Requires DROP COLUMN |
| Column DELETE (relationship) | Sync | 200 OK | Virtual only, no DDL |
Cross-References
- Processor — DDL generation and migration tracking
- System Overview — messaging architecture and SSE
- Frontend Overview — NgRx state management and SSE implementation