You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Separate physical queue identity from concrete flow identity throughout the SQL Core while preserving today's one-flow/one-queue behavior.
This issue provides the complete database foundation for private per-step queues in #651 and explicit shared queues in #652. It adds no public routing API.
Keep those fixes in separate releases. This issue consumes their corrected behavior rather than rebasing old queue assumptions over them.
Current problem
The SQL Core currently uses flow_slug as queue identity in every path:
flow creation provisions a same-named PGMQ queue;
ready tasks are sent to that queue;
task claiming treats one argument as both flow and queue;
completion, failure, retry, skip, recovery, pruning, and deletion infer the queue from the flow.
PGMQ message IDs are queue-scoped. The durable identity is:
(queue_name, message_id)
A message ID without its queue is not globally meaningful.
Data model
Add resolved queue identity to definitions and immutable queue identity to runtime tasks:
pgflow.steps
queue_name text not null references pgflow.queues(queue_name)
pgflow.step_tasks
queue_name text not null references pgflow.queues(queue_name)
message_id bigint null
Keep the task primary key:
(run_id, step_slug, task_index)
Add a partial unique index suitable for queue-message lookup:
create unique index ...
onpgflow.step_tasks (queue_name, message_id)
where message_id is not null;
message_id remains nullable. Existing cleanup tests deliberately cover tasks without a queue message; this issue must not redefine that state.
Do not add queue_name to runs or step_states.
Queue ownership registry
Persist queue ownership separately from step and task state:
pgflow.queues
queue_name text primary key
pgmq_name text not null
ownership private | shared
owner_flow_slug text null references pgflow.flows(flow_slug)
queue_name is the lowercase canonical physical identity. pgmq_name preserves the exact spelling in pgmq.meta, including mixed-case legacy names, and is used for metadata-sensitive PGMQ operations such as drop_queue().
Constraints:
queue_name = lower(queue_name)
queue_name = lower(pgmq_name)
length(queue_name) <= 47
private ownership -> owner_flow_slug is not null
shared ownership -> owner_flow_slug is null
unique lower(pgmq_name)
Index owner_flow_slug; PostgreSQL does not add an index for the foreign key automatically.
The model must distinguish:
private queue
owned by one concrete pgflow flow
may be dropped with that flow
shared queue
no exclusive flow owner
never dropped with one flow
Only private queues are created by this issue. #652 later enables explicit shared queues without changing task identity again. Do not add alias columns, route history, owner_step_slug, or multi-flow registries here.
Requirements:
reject two registry entries that address the same lowercased PGMQ tables;
backfill each existing flow-slug queue as private and owned by that concrete flow;
preserve each legacy queue's exact pgmq.meta.queue_name spelling in pgmq_name;
reject an existing PGMQ queue that has no matching pgflow ownership record instead of silently adopting it;
allow an existing matching private owner during idempotent startup;
reject a queue owned by another concrete flow.
A migration preflight must reject legacy aliases such as distinct Orders and orders metadata rows because they address the same physical tables.
Provisioning boundary
The complete compiler preflight is the primary provisioning boundary:
compile complete definition
-> resolve every required queue before mutation
-> register or verify every owner
-> validate and idempotently provision queues
-> create the flow and steps
create_flow()
creates only the flow definition
add_step()
defaults omitted queue_name to lower(flow_slug)
calls the same internal registration function
stores queue_name
start_ready_steps()
performs no queue DDL
A plain flow with zero steps still provisions its default private queue for backward compatibility. Direct internal SQL calls must use the same queue-registration function. #651 rejects withStepQueues() for an empty flow.
Task creation
start_ready_steps() must:
Read each ready step's steps.queue_name.
Group messages by queue where batching needs it.
Send each batch to its resolved queue.
Store the same queue name on every inserted task.
The task snapshot never changes after insertion.
Queue-aware operations
Every PGMQ operation must use the task queue snapshot rather than the run's flow slug. This includes:
claiming and visibility changes;
completion and late-callback archival;
retries and exhausted-task archival;
condition failures and skip cascades;
stalled-task recovery and permanent-stall handling;
maintenance pruning;
flow deletion.
Operations over several tasks must group by queue_name.
Separate the worker subscription from handler identity in claiming. The final claim boundary must carry at least:
queue_name
flow_slug
message_ids
worker_id
Preserve a compatibility wrapper for the existing plain-worker SQL signature. It resolves the flow's canonical default queue and delegates to the queue-aware boundary.
Represent PGMQ bigint message IDs as decimal strings at the JavaScript boundary. Cast them to bigint[] only in SQL calls; do not rely on unsafe JavaScript number precision.
#651 may add an exact step selector without changing queue identity again.
Deletion
delete_flow_and_data() must:
archive or remove active messages using each task's queue snapshot;
delete runtime rows and step definitions while retaining the flow identity row;
drop only private queues owned by that concrete flow;
pass the exact registry pgmq_name to metadata-sensitive PGMQ deletion;
remove each private registry row only after PGMQ deletion succeeds;
delete the concrete flow identity row last;
never drop a shared or unowned queue.
This order respects ownership foreign keys and supports fenced local destructive recompilation without stale private queues or pgmq.meta rows.
Upgrade preflight
Take the migration lock and explicit table locks before inspection so concurrent pgflow or PGMQ registration cannot invalidate the checked state.
Before mutation:
detect case-insensitive physical queue aliases in pgmq.meta;
confirm each metadata row has its expected queue, archive, and sequence objects;
reject active PGMQ messages without matching pgflow task identities;
confirm every legacy flow queue has unambiguous private ownership;
reject queues whose ownership cannot be established;
return bounded counts and sample keys with repair hints;
leave the database unchanged on failure.
Backfill queue_name = lower(flow_slug) for existing steps and tasks, including tasks whose message_id is null. Preserve the exact legacy PGMQ spelling in pgmq_name.
Add a mixed-case PGMQ 1.5.1 migration fixture that creates, migrates, archives, drops, and recreates a camelCase queue. Check metadata, queue tables, archive tables, and sequences after every operation.
Compatibility
This issue makes no routing change:
step queue = lower(concrete flow_slug)
task queue = lower(concrete flow_slug)
worker queue = the same canonical physical queue
A mixed-case worker argument still reaches the same PGMQ tables. Registry and task identity always use lowercase canonical names.
Support this rolling-upgrade matrix:
new database + old plain worker
-> supported
new database + new plain worker
-> supported
old database + new queue-aware worker
-> fail before registration or polling
Plain flow workers and starts remain source-compatible. This stage must remain operational if #651 is delayed; it is not a stable release by itself.
Summary
Separate physical queue identity from concrete flow identity throughout the SQL Core while preserving today's one-flow/one-queue behavior.
This issue provides the complete database foundation for private per-step queues in #651 and explicit shared queues in #652. It adds no public routing API.
Dependencies
start_tasks()visibility-execution fix first because this issue changes the same claim path.Keep those fixes in separate releases. This issue consumes their corrected behavior rather than rebasing old queue assumptions over them.
Current problem
The SQL Core currently uses
flow_slugas queue identity in every path:PGMQ message IDs are queue-scoped. The durable identity is:
A message ID without its queue is not globally meaningful.
Data model
Add resolved queue identity to definitions and immutable queue identity to runtime tasks:
Keep the task primary key:
Add a partial unique index suitable for queue-message lookup:
message_idremains nullable. Existing cleanup tests deliberately cover tasks without a queue message; this issue must not redefine that state.Do not add
queue_nametorunsorstep_states.Queue ownership registry
Persist queue ownership separately from step and task state:
queue_nameis the lowercase canonical physical identity.pgmq_namepreserves the exact spelling inpgmq.meta, including mixed-case legacy names, and is used for metadata-sensitive PGMQ operations such asdrop_queue().Constraints:
Index
owner_flow_slug; PostgreSQL does not add an index for the foreign key automatically.The model must distinguish:
Only private queues are created by this issue. #652 later enables explicit shared queues without changing task identity again. Do not add alias columns, route history,
owner_step_slug, or multi-flow registries here.Requirements:
pgmq.meta.queue_namespelling inpgmq_name;A migration preflight must reject legacy aliases such as distinct
Ordersandordersmetadata rows because they address the same physical tables.Provisioning boundary
The complete compiler preflight is the primary provisioning boundary:
A plain flow with zero steps still provisions its default private queue for backward compatibility. Direct internal SQL calls must use the same queue-registration function. #651 rejects
withStepQueues()for an empty flow.Task creation
start_ready_steps()must:steps.queue_name.The task snapshot never changes after insertion.
Queue-aware operations
Every PGMQ operation must use the task queue snapshot rather than the run's flow slug. This includes:
Operations over several tasks must group by
queue_name.Separate the worker subscription from handler identity in claiming. The final claim boundary must carry at least:
Preserve a compatibility wrapper for the existing plain-worker SQL signature. It resolves the flow's canonical default queue and delegates to the queue-aware boundary.
Represent PGMQ
bigintmessage IDs as decimal strings at the JavaScript boundary. Cast them tobigint[]only in SQL calls; do not rely on unsafe JavaScriptnumberprecision.#651 may add an exact step selector without changing queue identity again.
Deletion
delete_flow_and_data()must:pgmq_nameto metadata-sensitive PGMQ deletion;This order respects ownership foreign keys and supports fenced local destructive recompilation without stale private queues or
pgmq.metarows.Upgrade preflight
Take the migration lock and explicit table locks before inspection so concurrent pgflow or PGMQ registration cannot invalidate the checked state.
Before mutation:
pgmq.meta;(queue_name, message_id)identities;Backfill
queue_name = lower(flow_slug)for existing steps and tasks, including tasks whosemessage_idis null. Preserve the exact legacy PGMQ spelling inpgmq_name.Add a mixed-case PGMQ
1.5.1migration fixture that creates, migrates, archives, drops, and recreates a camelCase queue. Check metadata, queue tables, archive tables, and sequences after every operation.Compatibility
This issue makes no routing change:
A mixed-case worker argument still reaches the same PGMQ tables. Registry and task identity always use lowercase canonical names.
Support this rolling-upgrade matrix:
Plain flow workers and starts remain source-compatible. This stage must remain operational if #651 is delayed; it is not a stable release by itself.
Acceptance criteria
pgflow.queuespersists canonical identity, exact PGMQ metadata spelling, and private/shared ownership.owner_flow_slugindex enforce the ownership model.steps.queue_nameandstep_tasks.queue_nameare non-null foreign keys to the registry.step_tasks.message_idremains nullable.(queue_name, message_id)identity.create_flow()andstart_ready_steps()perform no queue DDL.add_step()resolves the default queue through the shared registration boundary.start_ready_steps()snapshots each resolved queue.Out of scope