shikibu 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,284 @@
1
+ -- migrate:up
2
+
3
+ -- Schema version tracking
4
+ CREATE TABLE IF NOT EXISTS schema_version (
5
+ version INT PRIMARY KEY,
6
+ applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
7
+ description TEXT NOT NULL
8
+ );
9
+
10
+ -- Workflow definitions (source code storage)
11
+ CREATE TABLE IF NOT EXISTS workflow_definitions (
12
+ workflow_name VARCHAR(255) NOT NULL,
13
+ source_hash VARCHAR(64) NOT NULL,
14
+ source_code LONGTEXT NOT NULL,
15
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
16
+ PRIMARY KEY (workflow_name, source_hash)
17
+ );
18
+
19
+ CREATE INDEX idx_definitions_name ON workflow_definitions(workflow_name);
20
+ CREATE INDEX idx_definitions_hash ON workflow_definitions(source_hash);
21
+
22
+ -- Workflow instances with distributed locking support
23
+ CREATE TABLE IF NOT EXISTS workflow_instances (
24
+ instance_id VARCHAR(255) PRIMARY KEY,
25
+ workflow_name VARCHAR(255) NOT NULL,
26
+ source_hash VARCHAR(64) NOT NULL,
27
+ owner_service VARCHAR(255) NOT NULL,
28
+ framework VARCHAR(50) NOT NULL DEFAULT 'python',
29
+ status VARCHAR(50) NOT NULL DEFAULT 'running',
30
+ current_activity_id VARCHAR(255),
31
+ continued_from VARCHAR(255),
32
+ started_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
33
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
34
+ input_data LONGTEXT NOT NULL,
35
+ output_data LONGTEXT,
36
+ locked_by VARCHAR(255),
37
+ locked_at TIMESTAMP NULL,
38
+ lock_timeout_seconds INT,
39
+ lock_expires_at TIMESTAMP NULL,
40
+ CONSTRAINT valid_status CHECK (
41
+ status IN ('running', 'completed', 'failed', 'waiting_for_event', 'waiting_for_timer', 'waiting_for_message', 'compensating', 'cancelled', 'recurred')
42
+ ),
43
+ FOREIGN KEY (continued_from) REFERENCES workflow_instances(instance_id),
44
+ FOREIGN KEY (workflow_name, source_hash) REFERENCES workflow_definitions(workflow_name, source_hash)
45
+ );
46
+
47
+ CREATE INDEX idx_instances_status ON workflow_instances(status);
48
+ CREATE INDEX idx_instances_workflow ON workflow_instances(workflow_name);
49
+ CREATE INDEX idx_instances_owner ON workflow_instances(owner_service);
50
+ CREATE INDEX idx_instances_framework ON workflow_instances(framework);
51
+ CREATE INDEX idx_instances_locked ON workflow_instances(locked_by, locked_at);
52
+ CREATE INDEX idx_instances_lock_expires ON workflow_instances(lock_expires_at);
53
+ CREATE INDEX idx_instances_updated ON workflow_instances(updated_at);
54
+ CREATE INDEX idx_instances_hash ON workflow_instances(source_hash);
55
+ CREATE INDEX idx_instances_continued_from ON workflow_instances(continued_from);
56
+ CREATE INDEX idx_instances_resumable ON workflow_instances(status, locked_by);
57
+
58
+ -- Workflow execution history (for deterministic replay)
59
+ CREATE TABLE IF NOT EXISTS workflow_history (
60
+ id INT AUTO_INCREMENT PRIMARY KEY,
61
+ instance_id VARCHAR(255) NOT NULL,
62
+ activity_id VARCHAR(255) NOT NULL,
63
+ event_type VARCHAR(100) NOT NULL,
64
+ data_type VARCHAR(10) NOT NULL DEFAULT 'json',
65
+ event_data LONGTEXT,
66
+ event_data_binary LONGBLOB,
67
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
68
+ FOREIGN KEY (instance_id) REFERENCES workflow_instances(instance_id) ON DELETE CASCADE,
69
+ UNIQUE KEY unique_instance_activity (instance_id, activity_id)
70
+ );
71
+
72
+ CREATE INDEX idx_history_instance ON workflow_history(instance_id, activity_id);
73
+ CREATE INDEX idx_history_created ON workflow_history(created_at);
74
+
75
+ -- Archived workflow history (for recur pattern)
76
+ CREATE TABLE IF NOT EXISTS workflow_history_archive (
77
+ id INT AUTO_INCREMENT PRIMARY KEY,
78
+ instance_id VARCHAR(255) NOT NULL,
79
+ activity_id VARCHAR(255) NOT NULL,
80
+ event_type VARCHAR(100) NOT NULL,
81
+ data_type VARCHAR(10) NOT NULL DEFAULT 'json',
82
+ event_data LONGTEXT,
83
+ event_data_binary LONGBLOB,
84
+ created_at TIMESTAMP NOT NULL,
85
+ archived_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
86
+ FOREIGN KEY (instance_id) REFERENCES workflow_instances(instance_id) ON DELETE CASCADE
87
+ );
88
+
89
+ CREATE INDEX idx_history_archive_instance ON workflow_history_archive(instance_id);
90
+ CREATE INDEX idx_history_archive_archived ON workflow_history_archive(archived_at);
91
+
92
+ -- Compensation transactions (LIFO stack for Saga pattern)
93
+ CREATE TABLE IF NOT EXISTS workflow_compensations (
94
+ id INT AUTO_INCREMENT PRIMARY KEY,
95
+ instance_id VARCHAR(255) NOT NULL,
96
+ activity_id VARCHAR(255) NOT NULL,
97
+ activity_name VARCHAR(255) NOT NULL,
98
+ args LONGTEXT NOT NULL,
99
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
100
+ FOREIGN KEY (instance_id) REFERENCES workflow_instances(instance_id) ON DELETE CASCADE
101
+ );
102
+
103
+ CREATE INDEX idx_compensations_instance ON workflow_compensations(instance_id, created_at DESC);
104
+
105
+ -- Timer subscriptions (for wait_timer)
106
+ CREATE TABLE IF NOT EXISTS workflow_timer_subscriptions (
107
+ id INT AUTO_INCREMENT PRIMARY KEY,
108
+ instance_id VARCHAR(255) NOT NULL,
109
+ timer_id VARCHAR(255) NOT NULL,
110
+ expires_at TIMESTAMP NOT NULL,
111
+ activity_id VARCHAR(255),
112
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
113
+ FOREIGN KEY (instance_id) REFERENCES workflow_instances(instance_id) ON DELETE CASCADE,
114
+ UNIQUE KEY unique_instance_timer (instance_id, timer_id)
115
+ );
116
+
117
+ CREATE INDEX idx_timer_subscriptions_expires ON workflow_timer_subscriptions(expires_at);
118
+ CREATE INDEX idx_timer_subscriptions_instance ON workflow_timer_subscriptions(instance_id);
119
+
120
+ -- Group memberships (Erlang pg style)
121
+ CREATE TABLE IF NOT EXISTS workflow_group_memberships (
122
+ id INT AUTO_INCREMENT PRIMARY KEY,
123
+ instance_id VARCHAR(255) NOT NULL,
124
+ group_name VARCHAR(255) NOT NULL,
125
+ joined_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
126
+ FOREIGN KEY (instance_id) REFERENCES workflow_instances(instance_id) ON DELETE CASCADE,
127
+ UNIQUE KEY unique_instance_group (instance_id, group_name)
128
+ );
129
+
130
+ CREATE INDEX idx_group_memberships_group ON workflow_group_memberships(group_name);
131
+ CREATE INDEX idx_group_memberships_instance ON workflow_group_memberships(instance_id);
132
+
133
+ -- Transactional outbox pattern
134
+ CREATE TABLE IF NOT EXISTS outbox_events (
135
+ event_id VARCHAR(255) PRIMARY KEY,
136
+ event_type VARCHAR(255) NOT NULL,
137
+ event_source VARCHAR(255) NOT NULL,
138
+ data_type VARCHAR(10) NOT NULL DEFAULT 'json',
139
+ event_data LONGTEXT,
140
+ event_data_binary LONGBLOB,
141
+ content_type VARCHAR(100) NOT NULL DEFAULT 'application/json',
142
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
143
+ published_at TIMESTAMP NULL,
144
+ status VARCHAR(50) NOT NULL DEFAULT 'pending',
145
+ retry_count INT DEFAULT 0,
146
+ last_error TEXT,
147
+ CONSTRAINT valid_outbox_status CHECK (status IN ('pending', 'processing', 'published', 'failed', 'invalid', 'expired'))
148
+ );
149
+
150
+ CREATE INDEX idx_outbox_status ON outbox_events(status, created_at);
151
+ CREATE INDEX idx_outbox_retry ON outbox_events(status, retry_count);
152
+ CREATE INDEX idx_outbox_published ON outbox_events(published_at);
153
+
154
+ -- Channel messages (persistent message queue)
155
+ CREATE TABLE IF NOT EXISTS channel_messages (
156
+ id INT AUTO_INCREMENT PRIMARY KEY,
157
+ channel VARCHAR(255) NOT NULL,
158
+ message_id VARCHAR(255) NOT NULL UNIQUE,
159
+ data_type VARCHAR(10) NOT NULL,
160
+ data LONGTEXT,
161
+ data_binary LONGBLOB,
162
+ metadata TEXT,
163
+ published_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
164
+ CONSTRAINT valid_data_type CHECK (data_type IN ('json', 'binary')),
165
+ CONSTRAINT data_type_consistency CHECK (
166
+ (data_type = 'json' AND data IS NOT NULL AND data_binary IS NULL) OR
167
+ (data_type = 'binary' AND data IS NULL AND data_binary IS NOT NULL)
168
+ )
169
+ );
170
+
171
+ CREATE INDEX idx_channel_messages_channel ON channel_messages(channel, published_at);
172
+ CREATE INDEX idx_channel_messages_id ON channel_messages(id);
173
+
174
+ -- Channel subscriptions
175
+ CREATE TABLE IF NOT EXISTS channel_subscriptions (
176
+ id INT AUTO_INCREMENT PRIMARY KEY,
177
+ instance_id VARCHAR(255) NOT NULL,
178
+ channel VARCHAR(255) NOT NULL,
179
+ mode VARCHAR(20) NOT NULL,
180
+ activity_id VARCHAR(255),
181
+ cursor_message_id INT,
182
+ timeout_at TIMESTAMP NULL,
183
+ subscribed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
184
+ FOREIGN KEY (instance_id) REFERENCES workflow_instances(instance_id) ON DELETE CASCADE,
185
+ CONSTRAINT valid_mode CHECK (mode IN ('broadcast', 'competing')),
186
+ UNIQUE KEY unique_instance_channel (instance_id, channel)
187
+ );
188
+
189
+ CREATE INDEX idx_channel_subscriptions_channel ON channel_subscriptions(channel);
190
+ CREATE INDEX idx_channel_subscriptions_instance ON channel_subscriptions(instance_id);
191
+ CREATE INDEX idx_channel_subscriptions_waiting ON channel_subscriptions(channel, activity_id);
192
+ CREATE INDEX idx_channel_subscriptions_timeout ON channel_subscriptions(timeout_at);
193
+
194
+ -- Channel delivery cursors (broadcast mode: track who read what)
195
+ CREATE TABLE IF NOT EXISTS channel_delivery_cursors (
196
+ id INT AUTO_INCREMENT PRIMARY KEY,
197
+ channel VARCHAR(255) NOT NULL,
198
+ instance_id VARCHAR(255) NOT NULL,
199
+ last_delivered_id INT NOT NULL,
200
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
201
+ FOREIGN KEY (instance_id) REFERENCES workflow_instances(instance_id) ON DELETE CASCADE,
202
+ UNIQUE KEY unique_channel_instance (channel, instance_id)
203
+ );
204
+
205
+ CREATE INDEX idx_channel_delivery_cursors_channel ON channel_delivery_cursors(channel);
206
+
207
+ -- Channel message claims (competing mode: who is processing what)
208
+ CREATE TABLE IF NOT EXISTS channel_message_claims (
209
+ message_id VARCHAR(255) PRIMARY KEY,
210
+ instance_id VARCHAR(255) NOT NULL,
211
+ claimed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
212
+ FOREIGN KEY (message_id) REFERENCES channel_messages(message_id) ON DELETE CASCADE,
213
+ FOREIGN KEY (instance_id) REFERENCES workflow_instances(instance_id) ON DELETE CASCADE
214
+ );
215
+
216
+ CREATE INDEX idx_channel_message_claims_instance ON channel_message_claims(instance_id);
217
+
218
+ -- System locks (for coordinating background tasks across pods)
219
+ CREATE TABLE IF NOT EXISTS system_locks (
220
+ lock_name VARCHAR(255) PRIMARY KEY,
221
+ locked_by VARCHAR(255),
222
+ locked_at TIMESTAMP NULL,
223
+ lock_expires_at TIMESTAMP NULL
224
+ );
225
+
226
+ CREATE INDEX idx_system_locks_expires ON system_locks(lock_expires_at);
227
+
228
+
229
+ -- migrate:down
230
+
231
+ DROP INDEX idx_channel_message_claims_instance ON channel_message_claims;
232
+ DROP TABLE IF EXISTS channel_message_claims;
233
+
234
+ DROP INDEX idx_channel_delivery_cursors_channel ON channel_delivery_cursors;
235
+ DROP TABLE IF EXISTS channel_delivery_cursors;
236
+
237
+ DROP INDEX idx_channel_subscriptions_waiting ON channel_subscriptions;
238
+ DROP INDEX idx_channel_subscriptions_instance ON channel_subscriptions;
239
+ DROP INDEX idx_channel_subscriptions_channel ON channel_subscriptions;
240
+ DROP TABLE IF EXISTS channel_subscriptions;
241
+
242
+ DROP INDEX idx_channel_messages_id ON channel_messages;
243
+ DROP INDEX idx_channel_messages_channel ON channel_messages;
244
+ DROP TABLE IF EXISTS channel_messages;
245
+
246
+ DROP INDEX idx_outbox_published ON outbox_events;
247
+ DROP INDEX idx_outbox_retry ON outbox_events;
248
+ DROP INDEX idx_outbox_status ON outbox_events;
249
+ DROP TABLE IF EXISTS outbox_events;
250
+
251
+ DROP INDEX idx_group_memberships_instance ON workflow_group_memberships;
252
+ DROP INDEX idx_group_memberships_group ON workflow_group_memberships;
253
+ DROP TABLE IF EXISTS workflow_group_memberships;
254
+
255
+ DROP INDEX idx_timer_subscriptions_instance ON workflow_timer_subscriptions;
256
+ DROP INDEX idx_timer_subscriptions_expires ON workflow_timer_subscriptions;
257
+ DROP TABLE IF EXISTS workflow_timer_subscriptions;
258
+
259
+ DROP INDEX idx_compensations_instance ON workflow_compensations;
260
+ DROP TABLE IF EXISTS workflow_compensations;
261
+
262
+ DROP INDEX idx_history_archive_archived ON workflow_history_archive;
263
+ DROP INDEX idx_history_archive_instance ON workflow_history_archive;
264
+ DROP TABLE IF EXISTS workflow_history_archive;
265
+
266
+ DROP INDEX idx_history_created ON workflow_history;
267
+ DROP INDEX idx_history_instance ON workflow_history;
268
+ DROP TABLE IF EXISTS workflow_history;
269
+
270
+ DROP INDEX idx_instances_continued_from ON workflow_instances;
271
+ DROP INDEX idx_instances_hash ON workflow_instances;
272
+ DROP INDEX idx_instances_updated ON workflow_instances;
273
+ DROP INDEX idx_instances_locked ON workflow_instances;
274
+ DROP INDEX idx_instances_framework ON workflow_instances;
275
+ DROP INDEX idx_instances_owner ON workflow_instances;
276
+ DROP INDEX idx_instances_workflow ON workflow_instances;
277
+ DROP INDEX idx_instances_status ON workflow_instances;
278
+ DROP TABLE IF EXISTS workflow_instances;
279
+
280
+ DROP INDEX idx_definitions_hash ON workflow_definitions;
281
+ DROP INDEX idx_definitions_name ON workflow_definitions;
282
+ DROP TABLE IF EXISTS workflow_definitions;
283
+
284
+ DROP TABLE IF EXISTS schema_version;
@@ -0,0 +1,284 @@
1
+ -- migrate:up
2
+
3
+ -- Schema version tracking
4
+ CREATE TABLE IF NOT EXISTS schema_version (
5
+ version INTEGER PRIMARY KEY,
6
+ applied_at TIMESTAMP NOT NULL DEFAULT NOW(),
7
+ description TEXT NOT NULL
8
+ );
9
+
10
+ -- Workflow definitions (source code storage)
11
+ CREATE TABLE IF NOT EXISTS workflow_definitions (
12
+ workflow_name TEXT NOT NULL,
13
+ source_hash TEXT NOT NULL,
14
+ source_code TEXT NOT NULL,
15
+ created_at TIMESTAMP NOT NULL DEFAULT NOW(),
16
+ PRIMARY KEY (workflow_name, source_hash)
17
+ );
18
+
19
+ CREATE INDEX IF NOT EXISTS idx_definitions_name ON workflow_definitions(workflow_name);
20
+ CREATE INDEX IF NOT EXISTS idx_definitions_hash ON workflow_definitions(source_hash);
21
+
22
+ -- Workflow instances with distributed locking support
23
+ CREATE TABLE IF NOT EXISTS workflow_instances (
24
+ instance_id TEXT PRIMARY KEY,
25
+ workflow_name TEXT NOT NULL,
26
+ source_hash TEXT NOT NULL,
27
+ owner_service TEXT NOT NULL,
28
+ framework TEXT NOT NULL DEFAULT 'python',
29
+ status TEXT NOT NULL DEFAULT 'running',
30
+ current_activity_id TEXT,
31
+ continued_from TEXT,
32
+ started_at TIMESTAMP NOT NULL DEFAULT NOW(),
33
+ updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
34
+ input_data TEXT NOT NULL,
35
+ output_data TEXT,
36
+ locked_by TEXT,
37
+ locked_at TIMESTAMP,
38
+ lock_timeout_seconds INTEGER,
39
+ lock_expires_at TIMESTAMP,
40
+ CONSTRAINT valid_status CHECK (
41
+ status IN ('running', 'completed', 'failed', 'waiting_for_event', 'waiting_for_timer', 'waiting_for_message', 'compensating', 'cancelled', 'recurred')
42
+ ),
43
+ FOREIGN KEY (continued_from) REFERENCES workflow_instances(instance_id),
44
+ FOREIGN KEY (workflow_name, source_hash) REFERENCES workflow_definitions(workflow_name, source_hash)
45
+ );
46
+
47
+ CREATE INDEX IF NOT EXISTS idx_instances_status ON workflow_instances(status);
48
+ CREATE INDEX IF NOT EXISTS idx_instances_workflow ON workflow_instances(workflow_name);
49
+ CREATE INDEX IF NOT EXISTS idx_instances_owner ON workflow_instances(owner_service);
50
+ CREATE INDEX IF NOT EXISTS idx_instances_framework ON workflow_instances(framework);
51
+ CREATE INDEX IF NOT EXISTS idx_instances_locked ON workflow_instances(locked_by, locked_at);
52
+ CREATE INDEX IF NOT EXISTS idx_instances_lock_expires ON workflow_instances(lock_expires_at);
53
+ CREATE INDEX IF NOT EXISTS idx_instances_updated ON workflow_instances(updated_at);
54
+ CREATE INDEX IF NOT EXISTS idx_instances_hash ON workflow_instances(source_hash);
55
+ CREATE INDEX IF NOT EXISTS idx_instances_continued_from ON workflow_instances(continued_from);
56
+ CREATE INDEX IF NOT EXISTS idx_instances_resumable ON workflow_instances(status, locked_by);
57
+
58
+ -- Workflow execution history (for deterministic replay)
59
+ CREATE TABLE IF NOT EXISTS workflow_history (
60
+ id SERIAL PRIMARY KEY,
61
+ instance_id TEXT NOT NULL,
62
+ activity_id TEXT NOT NULL,
63
+ event_type TEXT NOT NULL,
64
+ data_type TEXT NOT NULL DEFAULT 'json',
65
+ event_data TEXT,
66
+ event_data_binary BYTEA,
67
+ created_at TIMESTAMP NOT NULL DEFAULT NOW(),
68
+ FOREIGN KEY (instance_id) REFERENCES workflow_instances(instance_id) ON DELETE CASCADE,
69
+ CONSTRAINT unique_instance_activity UNIQUE (instance_id, activity_id)
70
+ );
71
+
72
+ CREATE INDEX IF NOT EXISTS idx_history_instance ON workflow_history(instance_id, activity_id);
73
+ CREATE INDEX IF NOT EXISTS idx_history_created ON workflow_history(created_at);
74
+
75
+ -- Archived workflow history (for recur pattern)
76
+ CREATE TABLE IF NOT EXISTS workflow_history_archive (
77
+ id SERIAL PRIMARY KEY,
78
+ instance_id TEXT NOT NULL,
79
+ activity_id TEXT NOT NULL,
80
+ event_type TEXT NOT NULL,
81
+ data_type TEXT NOT NULL DEFAULT 'json',
82
+ event_data TEXT,
83
+ event_data_binary BYTEA,
84
+ created_at TIMESTAMP NOT NULL,
85
+ archived_at TIMESTAMP NOT NULL DEFAULT NOW(),
86
+ FOREIGN KEY (instance_id) REFERENCES workflow_instances(instance_id) ON DELETE CASCADE
87
+ );
88
+
89
+ CREATE INDEX IF NOT EXISTS idx_history_archive_instance ON workflow_history_archive(instance_id);
90
+ CREATE INDEX IF NOT EXISTS idx_history_archive_archived ON workflow_history_archive(archived_at);
91
+
92
+ -- Compensation transactions (LIFO stack for Saga pattern)
93
+ CREATE TABLE IF NOT EXISTS workflow_compensations (
94
+ id SERIAL PRIMARY KEY,
95
+ instance_id TEXT NOT NULL,
96
+ activity_id TEXT NOT NULL,
97
+ activity_name TEXT NOT NULL,
98
+ args TEXT NOT NULL,
99
+ created_at TIMESTAMP NOT NULL DEFAULT NOW(),
100
+ FOREIGN KEY (instance_id) REFERENCES workflow_instances(instance_id) ON DELETE CASCADE
101
+ );
102
+
103
+ CREATE INDEX IF NOT EXISTS idx_compensations_instance ON workflow_compensations(instance_id, created_at DESC);
104
+
105
+ -- Timer subscriptions (for wait_timer)
106
+ CREATE TABLE IF NOT EXISTS workflow_timer_subscriptions (
107
+ id SERIAL PRIMARY KEY,
108
+ instance_id TEXT NOT NULL,
109
+ timer_id TEXT NOT NULL,
110
+ expires_at TIMESTAMP NOT NULL,
111
+ activity_id TEXT,
112
+ created_at TIMESTAMP NOT NULL DEFAULT NOW(),
113
+ FOREIGN KEY (instance_id) REFERENCES workflow_instances(instance_id) ON DELETE CASCADE,
114
+ CONSTRAINT unique_instance_timer UNIQUE (instance_id, timer_id)
115
+ );
116
+
117
+ CREATE INDEX IF NOT EXISTS idx_timer_subscriptions_expires ON workflow_timer_subscriptions(expires_at);
118
+ CREATE INDEX IF NOT EXISTS idx_timer_subscriptions_instance ON workflow_timer_subscriptions(instance_id);
119
+
120
+ -- Group memberships (Erlang pg style)
121
+ CREATE TABLE IF NOT EXISTS workflow_group_memberships (
122
+ id SERIAL PRIMARY KEY,
123
+ instance_id TEXT NOT NULL,
124
+ group_name TEXT NOT NULL,
125
+ joined_at TIMESTAMP NOT NULL DEFAULT NOW(),
126
+ FOREIGN KEY (instance_id) REFERENCES workflow_instances(instance_id) ON DELETE CASCADE,
127
+ CONSTRAINT unique_instance_group UNIQUE (instance_id, group_name)
128
+ );
129
+
130
+ CREATE INDEX IF NOT EXISTS idx_group_memberships_group ON workflow_group_memberships(group_name);
131
+ CREATE INDEX IF NOT EXISTS idx_group_memberships_instance ON workflow_group_memberships(instance_id);
132
+
133
+ -- Transactional outbox pattern
134
+ CREATE TABLE IF NOT EXISTS outbox_events (
135
+ event_id TEXT PRIMARY KEY,
136
+ event_type TEXT NOT NULL,
137
+ event_source TEXT NOT NULL,
138
+ data_type TEXT NOT NULL DEFAULT 'json',
139
+ event_data TEXT,
140
+ event_data_binary BYTEA,
141
+ content_type TEXT NOT NULL DEFAULT 'application/json',
142
+ created_at TIMESTAMP NOT NULL DEFAULT NOW(),
143
+ published_at TIMESTAMP,
144
+ status TEXT NOT NULL DEFAULT 'pending',
145
+ retry_count INTEGER DEFAULT 0,
146
+ last_error TEXT,
147
+ CONSTRAINT valid_outbox_status CHECK (status IN ('pending', 'processing', 'published', 'failed', 'invalid', 'expired'))
148
+ );
149
+
150
+ CREATE INDEX IF NOT EXISTS idx_outbox_status ON outbox_events(status, created_at);
151
+ CREATE INDEX IF NOT EXISTS idx_outbox_retry ON outbox_events(status, retry_count);
152
+ CREATE INDEX IF NOT EXISTS idx_outbox_published ON outbox_events(published_at);
153
+
154
+ -- Channel messages (persistent message queue)
155
+ CREATE TABLE IF NOT EXISTS channel_messages (
156
+ id SERIAL PRIMARY KEY,
157
+ channel TEXT NOT NULL,
158
+ message_id TEXT NOT NULL UNIQUE,
159
+ data_type TEXT NOT NULL,
160
+ data TEXT,
161
+ data_binary BYTEA,
162
+ metadata TEXT,
163
+ published_at TIMESTAMP NOT NULL DEFAULT NOW(),
164
+ CONSTRAINT valid_data_type CHECK (data_type IN ('json', 'binary')),
165
+ CONSTRAINT data_type_consistency CHECK (
166
+ (data_type = 'json' AND data IS NOT NULL AND data_binary IS NULL) OR
167
+ (data_type = 'binary' AND data IS NULL AND data_binary IS NOT NULL)
168
+ )
169
+ );
170
+
171
+ CREATE INDEX IF NOT EXISTS idx_channel_messages_channel ON channel_messages(channel, published_at);
172
+ CREATE INDEX IF NOT EXISTS idx_channel_messages_id ON channel_messages(id);
173
+
174
+ -- Channel subscriptions
175
+ CREATE TABLE IF NOT EXISTS channel_subscriptions (
176
+ id SERIAL PRIMARY KEY,
177
+ instance_id TEXT NOT NULL,
178
+ channel TEXT NOT NULL,
179
+ mode TEXT NOT NULL,
180
+ activity_id TEXT,
181
+ cursor_message_id INTEGER,
182
+ timeout_at TIMESTAMP,
183
+ subscribed_at TIMESTAMP NOT NULL DEFAULT NOW(),
184
+ FOREIGN KEY (instance_id) REFERENCES workflow_instances(instance_id) ON DELETE CASCADE,
185
+ CONSTRAINT valid_mode CHECK (mode IN ('broadcast', 'competing')),
186
+ CONSTRAINT unique_instance_channel UNIQUE (instance_id, channel)
187
+ );
188
+
189
+ CREATE INDEX IF NOT EXISTS idx_channel_subscriptions_channel ON channel_subscriptions(channel);
190
+ CREATE INDEX IF NOT EXISTS idx_channel_subscriptions_instance ON channel_subscriptions(instance_id);
191
+ CREATE INDEX IF NOT EXISTS idx_channel_subscriptions_waiting ON channel_subscriptions(channel, activity_id);
192
+ CREATE INDEX IF NOT EXISTS idx_channel_subscriptions_timeout ON channel_subscriptions(timeout_at);
193
+
194
+ -- Channel delivery cursors (broadcast mode: track who read what)
195
+ CREATE TABLE IF NOT EXISTS channel_delivery_cursors (
196
+ id SERIAL PRIMARY KEY,
197
+ channel TEXT NOT NULL,
198
+ instance_id TEXT NOT NULL,
199
+ last_delivered_id INTEGER NOT NULL,
200
+ updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
201
+ FOREIGN KEY (instance_id) REFERENCES workflow_instances(instance_id) ON DELETE CASCADE,
202
+ CONSTRAINT unique_channel_instance UNIQUE (channel, instance_id)
203
+ );
204
+
205
+ CREATE INDEX IF NOT EXISTS idx_channel_delivery_cursors_channel ON channel_delivery_cursors(channel);
206
+
207
+ -- Channel message claims (competing mode: who is processing what)
208
+ CREATE TABLE IF NOT EXISTS channel_message_claims (
209
+ message_id TEXT PRIMARY KEY,
210
+ instance_id TEXT NOT NULL,
211
+ claimed_at TIMESTAMP NOT NULL DEFAULT NOW(),
212
+ FOREIGN KEY (message_id) REFERENCES channel_messages(message_id) ON DELETE CASCADE,
213
+ FOREIGN KEY (instance_id) REFERENCES workflow_instances(instance_id) ON DELETE CASCADE
214
+ );
215
+
216
+ CREATE INDEX IF NOT EXISTS idx_channel_message_claims_instance ON channel_message_claims(instance_id);
217
+
218
+ -- System locks (for coordinating background tasks across pods)
219
+ CREATE TABLE IF NOT EXISTS system_locks (
220
+ lock_name TEXT PRIMARY KEY,
221
+ locked_by TEXT,
222
+ locked_at TIMESTAMP,
223
+ lock_expires_at TIMESTAMP
224
+ );
225
+
226
+ CREATE INDEX IF NOT EXISTS idx_system_locks_expires ON system_locks(lock_expires_at);
227
+
228
+
229
+ -- migrate:down
230
+
231
+ DROP INDEX IF EXISTS idx_channel_message_claims_instance;
232
+ DROP TABLE IF EXISTS channel_message_claims;
233
+
234
+ DROP INDEX IF EXISTS idx_channel_delivery_cursors_channel;
235
+ DROP TABLE IF EXISTS channel_delivery_cursors;
236
+
237
+ DROP INDEX IF EXISTS idx_channel_subscriptions_waiting;
238
+ DROP INDEX IF EXISTS idx_channel_subscriptions_instance;
239
+ DROP INDEX IF EXISTS idx_channel_subscriptions_channel;
240
+ DROP TABLE IF EXISTS channel_subscriptions;
241
+
242
+ DROP INDEX IF EXISTS idx_channel_messages_id;
243
+ DROP INDEX IF EXISTS idx_channel_messages_channel;
244
+ DROP TABLE IF EXISTS channel_messages;
245
+
246
+ DROP INDEX IF EXISTS idx_outbox_published;
247
+ DROP INDEX IF EXISTS idx_outbox_retry;
248
+ DROP INDEX IF EXISTS idx_outbox_status;
249
+ DROP TABLE IF EXISTS outbox_events;
250
+
251
+ DROP INDEX IF EXISTS idx_group_memberships_instance;
252
+ DROP INDEX IF EXISTS idx_group_memberships_group;
253
+ DROP TABLE IF EXISTS workflow_group_memberships;
254
+
255
+ DROP INDEX IF EXISTS idx_timer_subscriptions_instance;
256
+ DROP INDEX IF EXISTS idx_timer_subscriptions_expires;
257
+ DROP TABLE IF EXISTS workflow_timer_subscriptions;
258
+
259
+ DROP INDEX IF EXISTS idx_compensations_instance;
260
+ DROP TABLE IF EXISTS workflow_compensations;
261
+
262
+ DROP INDEX IF EXISTS idx_history_archive_archived;
263
+ DROP INDEX IF EXISTS idx_history_archive_instance;
264
+ DROP TABLE IF EXISTS workflow_history_archive;
265
+
266
+ DROP INDEX IF EXISTS idx_history_created;
267
+ DROP INDEX IF EXISTS idx_history_instance;
268
+ DROP TABLE IF EXISTS workflow_history;
269
+
270
+ DROP INDEX IF EXISTS idx_instances_continued_from;
271
+ DROP INDEX IF EXISTS idx_instances_hash;
272
+ DROP INDEX IF EXISTS idx_instances_updated;
273
+ DROP INDEX IF EXISTS idx_instances_locked;
274
+ DROP INDEX IF EXISTS idx_instances_framework;
275
+ DROP INDEX IF EXISTS idx_instances_owner;
276
+ DROP INDEX IF EXISTS idx_instances_workflow;
277
+ DROP INDEX IF EXISTS idx_instances_status;
278
+ DROP TABLE IF EXISTS workflow_instances;
279
+
280
+ DROP INDEX IF EXISTS idx_definitions_hash;
281
+ DROP INDEX IF EXISTS idx_definitions_name;
282
+ DROP TABLE IF EXISTS workflow_definitions;
283
+
284
+ DROP TABLE IF EXISTS schema_version;