@pgflow/core 0.0.5-prealpha.2

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.
Files changed (120) hide show
  1. package/LICENSE.md +660 -0
  2. package/README.md +373 -0
  3. package/__tests__/mocks/index.ts +1 -0
  4. package/__tests__/mocks/postgres.ts +37 -0
  5. package/__tests__/types/PgflowSqlClient.test-d.ts +59 -0
  6. package/dist/LICENSE.md +660 -0
  7. package/dist/README.md +373 -0
  8. package/dist/index.js +54 -0
  9. package/docs/options_for_flow_and_steps.md +75 -0
  10. package/docs/pgflow-blob-reference-system.md +179 -0
  11. package/eslint.config.cjs +22 -0
  12. package/example-flow.mermaid +5 -0
  13. package/example-flow.svg +1 -0
  14. package/flow-lifecycle.mermaid +83 -0
  15. package/flow-lifecycle.svg +1 -0
  16. package/out-tsc/vitest/__tests__/mocks/index.d.ts +2 -0
  17. package/out-tsc/vitest/__tests__/mocks/index.d.ts.map +1 -0
  18. package/out-tsc/vitest/__tests__/mocks/postgres.d.ts +15 -0
  19. package/out-tsc/vitest/__tests__/mocks/postgres.d.ts.map +1 -0
  20. package/out-tsc/vitest/__tests__/types/PgflowSqlClient.test-d.d.ts +2 -0
  21. package/out-tsc/vitest/__tests__/types/PgflowSqlClient.test-d.d.ts.map +1 -0
  22. package/out-tsc/vitest/tsconfig.spec.tsbuildinfo +1 -0
  23. package/out-tsc/vitest/vite.config.d.ts +3 -0
  24. package/out-tsc/vitest/vite.config.d.ts.map +1 -0
  25. package/package.json +28 -0
  26. package/pkgs/core/dist/index.js +54 -0
  27. package/pkgs/core/dist/pkgs/core/LICENSE.md +660 -0
  28. package/pkgs/core/dist/pkgs/core/README.md +373 -0
  29. package/pkgs/dsl/dist/index.js +123 -0
  30. package/pkgs/dsl/dist/pkgs/dsl/README.md +11 -0
  31. package/project.json +125 -0
  32. package/prompts/architect.md +87 -0
  33. package/prompts/condition.md +33 -0
  34. package/prompts/declarative_sql.md +15 -0
  35. package/prompts/deps_in_payloads.md +20 -0
  36. package/prompts/dsl-multi-arg.ts +48 -0
  37. package/prompts/dsl-options.md +39 -0
  38. package/prompts/dsl-single-arg.ts +51 -0
  39. package/prompts/dsl-two-arg.ts +61 -0
  40. package/prompts/dsl.md +119 -0
  41. package/prompts/fanout_steps.md +1 -0
  42. package/prompts/json_schemas.md +36 -0
  43. package/prompts/one_shot.md +286 -0
  44. package/prompts/pgtap.md +229 -0
  45. package/prompts/sdk.md +59 -0
  46. package/prompts/step_types.md +62 -0
  47. package/prompts/versioning.md +16 -0
  48. package/queries/fail_permanently.sql +17 -0
  49. package/queries/fail_task.sql +21 -0
  50. package/queries/sequential.sql +47 -0
  51. package/queries/two_roots_left_right.sql +59 -0
  52. package/schema.svg +1 -0
  53. package/scripts/colorize-pgtap-output.awk +72 -0
  54. package/scripts/run-test-with-colors +5 -0
  55. package/scripts/watch-test +7 -0
  56. package/src/PgflowSqlClient.ts +85 -0
  57. package/src/database-types.ts +759 -0
  58. package/src/index.ts +3 -0
  59. package/src/types.ts +103 -0
  60. package/supabase/config.toml +32 -0
  61. package/supabase/migrations/000000_schema.sql +150 -0
  62. package/supabase/migrations/000005_create_flow.sql +29 -0
  63. package/supabase/migrations/000010_add_step.sql +48 -0
  64. package/supabase/migrations/000015_start_ready_steps.sql +45 -0
  65. package/supabase/migrations/000020_start_flow.sql +46 -0
  66. package/supabase/migrations/000030_read_with_poll_backport.sql +70 -0
  67. package/supabase/migrations/000040_poll_for_tasks.sql +100 -0
  68. package/supabase/migrations/000045_maybe_complete_run.sql +30 -0
  69. package/supabase/migrations/000050_complete_task.sql +98 -0
  70. package/supabase/migrations/000055_calculate_retry_delay.sql +11 -0
  71. package/supabase/migrations/000060_fail_task.sql +124 -0
  72. package/supabase/migrations/000_edge_worker_initial.sql +86 -0
  73. package/supabase/seed.sql +202 -0
  74. package/supabase/tests/add_step/basic_step_addition.test.sql +29 -0
  75. package/supabase/tests/add_step/circular_dependency.test.sql +21 -0
  76. package/supabase/tests/add_step/flow_isolation.test.sql +26 -0
  77. package/supabase/tests/add_step/idempotent_step_addition.test.sql +20 -0
  78. package/supabase/tests/add_step/invalid_step_slug.test.sql +16 -0
  79. package/supabase/tests/add_step/nonexistent_dependency.test.sql +16 -0
  80. package/supabase/tests/add_step/nonexistent_flow.test.sql +13 -0
  81. package/supabase/tests/add_step/options.test.sql +66 -0
  82. package/supabase/tests/add_step/step_with_dependency.test.sql +36 -0
  83. package/supabase/tests/add_step/step_with_multiple_dependencies.test.sql +46 -0
  84. package/supabase/tests/complete_task/archives_message.test.sql +67 -0
  85. package/supabase/tests/complete_task/completes_run_if_no_more_remaining_steps.test.sql +62 -0
  86. package/supabase/tests/complete_task/completes_task_and_updates_dependents.test.sql +64 -0
  87. package/supabase/tests/complete_task/decrements_remaining_steps_if_completing_step.test.sql +62 -0
  88. package/supabase/tests/complete_task/saves_output_when_completing_run.test.sql +57 -0
  89. package/supabase/tests/create_flow/flow_creation.test.sql +27 -0
  90. package/supabase/tests/create_flow/idempotency_and_duplicates.test.sql +26 -0
  91. package/supabase/tests/create_flow/invalid_slug.test.sql +13 -0
  92. package/supabase/tests/create_flow/options.test.sql +57 -0
  93. package/supabase/tests/fail_task/exponential_backoff.test.sql +70 -0
  94. package/supabase/tests/fail_task/mark_as_failed_if_no_retries_available.test.sql +49 -0
  95. package/supabase/tests/fail_task/respects_flow_retry_settings.test.sql +48 -0
  96. package/supabase/tests/fail_task/respects_step_retry_settings.test.sql +48 -0
  97. package/supabase/tests/fail_task/retry_task_if_retries_available.test.sql +39 -0
  98. package/supabase/tests/is_valid_slug.test.sql +72 -0
  99. package/supabase/tests/poll_for_tasks/builds_proper_input_from_deps_outputs.test.sql +35 -0
  100. package/supabase/tests/poll_for_tasks/hides_messages.test.sql +35 -0
  101. package/supabase/tests/poll_for_tasks/increments_attempts_count.test.sql +35 -0
  102. package/supabase/tests/poll_for_tasks/multiple_task_processing.test.sql +24 -0
  103. package/supabase/tests/poll_for_tasks/polls_only_queued_tasks.test.sql +35 -0
  104. package/supabase/tests/poll_for_tasks/reads_messages.test.sql +38 -0
  105. package/supabase/tests/poll_for_tasks/returns_no_tasks_if_no_step_task_for_message.test.sql +34 -0
  106. package/supabase/tests/poll_for_tasks/returns_no_tasks_if_queue_is_empty.test.sql +19 -0
  107. package/supabase/tests/poll_for_tasks/returns_no_tasks_when_qty_set_to_0.test.sql +22 -0
  108. package/supabase/tests/poll_for_tasks/sets_vt_delay_based_on_opt_timeout.test.sql +41 -0
  109. package/supabase/tests/poll_for_tasks/tasks_reapppear_if_not_processed_in_time.test.sql +59 -0
  110. package/supabase/tests/start_flow/creates_run.test.sql +24 -0
  111. package/supabase/tests/start_flow/creates_step_states_for_all_steps.test.sql +25 -0
  112. package/supabase/tests/start_flow/creates_step_tasks_only_for_root_steps.test.sql +54 -0
  113. package/supabase/tests/start_flow/returns_run.test.sql +24 -0
  114. package/supabase/tests/start_flow/sends_messages_on_the_queue.test.sql +50 -0
  115. package/supabase/tests/start_flow/starts_only_root_steps.test.sql +21 -0
  116. package/supabase/tests/step_dsl_is_idempotent.test.sql +34 -0
  117. package/tsconfig.json +16 -0
  118. package/tsconfig.lib.json +26 -0
  119. package/tsconfig.spec.json +35 -0
  120. package/vite.config.ts +57 -0
@@ -0,0 +1,49 @@
1
+ begin;
2
+ select plan(5);
3
+ select pgflow_tests.reset_db();
4
+
5
+ -- SETUP
6
+ select pgflow.create_flow('with_retry');
7
+ select pgflow.add_step('with_retry', 'first', max_attempts => 0, base_delay => 0);
8
+ select pgflow.start_flow('with_retry', '{"test": true}'::JSONB);
9
+
10
+ -- max_attempts is 0, so failing once should mark the task as failed
11
+ select pgflow_tests.poll_and_fail('with_retry');
12
+
13
+ -- TEST: The task should be queued
14
+ select is(
15
+ (select status from pgflow.step_tasks where flow_slug = 'with_retry' and step_slug = 'first'),
16
+ 'failed',
17
+ 'The task should be failed'
18
+ );
19
+
20
+ -- TEST: The task should have null error_message
21
+ select is(
22
+ (select error_message from pgflow.step_tasks where flow_slug = 'with_retry' and step_slug = 'first'),
23
+ 'first FAILED',
24
+ 'The task should have attempts_count incremented'
25
+ );
26
+
27
+ -- TEST: The task's message should be in the queue
28
+ select is(
29
+ (select count(*)::INT from pgmq.q_with_retry),
30
+ 0,
31
+ 'There should be no messages in the queue'
32
+ );
33
+
34
+ -- TEST: The step should be marked as failed
35
+ select is(
36
+ (select status from pgflow.step_states where flow_slug = 'with_retry' and step_slug = 'first' limit 1),
37
+ 'failed',
38
+ 'The step should be marked as failed'
39
+ );
40
+
41
+ -- TEST: The run should be marked as failed
42
+ select is(
43
+ (select status from pgflow.runs where flow_slug = 'with_retry' limit 1),
44
+ 'failed',
45
+ 'The run should be marked as failed'
46
+ );
47
+
48
+ select finish();
49
+ rollback;
@@ -0,0 +1,48 @@
1
+ begin;
2
+ select plan(3);
3
+ select pgflow_tests.reset_db();
4
+
5
+ -- SETUP: Create a flow with custom retry settings
6
+ select pgflow.create_flow('custom_retry', max_attempts => 2, base_delay => 0);
7
+ select pgflow.add_step('custom_retry', 'test_step');
8
+
9
+ -- Start the flow
10
+ select pgflow.start_flow('custom_retry', '{"test": true}'::JSONB);
11
+
12
+ -- Fail the task first time
13
+ select pgflow_tests.poll_and_fail('custom_retry');
14
+
15
+ -- TEST: The task should be queued (first retry)
16
+ select is(
17
+ (select status from pgflow.step_tasks limit 1),
18
+ 'queued',
19
+ 'Task should be queued after first failure (1st attempt of 2)'
20
+ );
21
+
22
+ -- Fail the task second time
23
+ select pgflow_tests.poll_and_fail('custom_retry');
24
+
25
+ -- TEST: The task should be queued (second retry)
26
+ select is(
27
+ (
28
+ select status from pgflow.step_tasks
29
+ where
30
+ run_id = (select run_id from pgflow.runs where flow_slug = 'custom_retry')
31
+ and step_slug = 'test_step'
32
+ ),
33
+ 'failed',
34
+ 'Task should be failed after second failure (2nd attempt of 2)'
35
+ );
36
+
37
+ -- TEST: The run should be failed
38
+ select is(
39
+ (
40
+ select status from pgflow.runs
41
+ where flow_slug = 'custom_retry'
42
+ ),
43
+ 'failed',
44
+ 'Run should be failed after exceeding retry limit'
45
+ );
46
+
47
+ select finish();
48
+ rollback;
@@ -0,0 +1,48 @@
1
+ begin;
2
+ select plan(3);
3
+ select pgflow_tests.reset_db();
4
+
5
+ -- SETUP: Create a flow with custom retry settings
6
+ select pgflow.create_flow('custom_retry', max_attempts => 10, base_delay => 10);
7
+ select pgflow.add_step('custom_retry', 'test_step', max_attempts => 2, base_delay => 0);
8
+
9
+ -- Start the flow
10
+ select pgflow.start_flow('custom_retry', '{"test": true}'::JSONB);
11
+
12
+ -- Fail the task first time
13
+ select pgflow_tests.poll_and_fail('custom_retry');
14
+
15
+ -- TEST: The task should be queued (first retry)
16
+ select is(
17
+ (select status from pgflow.step_tasks limit 1),
18
+ 'queued',
19
+ 'Task should be queued after first failure (1st attempt of 2)'
20
+ );
21
+
22
+ -- Fail the task second time
23
+ select pgflow_tests.poll_and_fail('custom_retry');
24
+
25
+ -- TEST: The task should be queued (second retry)
26
+ select is(
27
+ (
28
+ select status from pgflow.step_tasks
29
+ where
30
+ run_id = (select run_id from pgflow.runs where flow_slug = 'custom_retry')
31
+ and step_slug = 'test_step'
32
+ ),
33
+ 'failed',
34
+ 'Task should be failed after second failure (2nd attempt of 2)'
35
+ );
36
+
37
+ -- TEST: The run should be failed
38
+ select is(
39
+ (
40
+ select status from pgflow.runs
41
+ where flow_slug = 'custom_retry'
42
+ ),
43
+ 'failed',
44
+ 'Run should be failed after exceeding retry limit'
45
+ );
46
+
47
+ select finish();
48
+ rollback;
@@ -0,0 +1,39 @@
1
+ begin;
2
+ select plan(4);
3
+ select pgflow_tests.reset_db();
4
+ select pgflow_tests.setup_flow('sequential');
5
+
6
+ -- SETUP
7
+ select pgflow.start_flow('sequential', '{"test": true}'::JSONB);
8
+ select pgflow_tests.poll_and_fail('sequential');
9
+
10
+ -- TEST: The task should be queued
11
+ select is(
12
+ (select status from pgflow.step_tasks where flow_slug = 'sequential' and step_slug = 'first'),
13
+ 'queued',
14
+ 'The task should be queued'
15
+ );
16
+
17
+ -- TEST: The task should have attempts_count incremented
18
+ select is(
19
+ (select attempts_count from pgflow.step_tasks where flow_slug = 'sequential' and step_slug = 'first'),
20
+ 1,
21
+ 'The task should have attempts_count incremented'
22
+ );
23
+
24
+ -- TEST: The task should have null error_message
25
+ select is(
26
+ (select error_message from pgflow.step_tasks where flow_slug = 'sequential' and step_slug = 'first'),
27
+ 'first FAILED',
28
+ 'The task should have attempts_count incremented'
29
+ );
30
+
31
+ -- TEST: The task's message should be in the queue
32
+ select is(
33
+ (select message ->> 'step_slug' from pgmq.q_sequential limit 1),
34
+ 'first',
35
+ 'The task''s message should be in the queue'
36
+ );
37
+
38
+ select finish();
39
+ rollback;
@@ -0,0 +1,72 @@
1
+ begin;
2
+ select plan(11);
3
+ select pgflow_tests.reset_db();
4
+
5
+ -- TEST: Null input
6
+ select ok(
7
+ not pgflow.is_valid_slug(null),
8
+ 'is_valid_slug returns false for NULL input'
9
+ );
10
+
11
+ -- TEST: Empty string
12
+ select ok(
13
+ not pgflow.is_valid_slug(''),
14
+ 'is_valid_slug returns false for empty string'
15
+ );
16
+
17
+ -- TEST: Too long string (128+ chars)
18
+ select ok(
19
+ not pgflow.is_valid_slug(repeat('x', 129)),
20
+ 'is_valid_slug returns false for strings longer than 128 chars'
21
+ );
22
+
23
+ -- TEST: String with dashes
24
+ select ok(
25
+ not pgflow.is_valid_slug('test-slug'),
26
+ 'is_valid_slug returns false for strings with dashes'
27
+ );
28
+
29
+ -- TEST: String with spaces
30
+ select ok(
31
+ not pgflow.is_valid_slug('test slug'),
32
+ 'is_valid_slug returns false for strings with spaces'
33
+ );
34
+
35
+ -- TEST: String starting with number
36
+ select ok(
37
+ not pgflow.is_valid_slug('123test'),
38
+ 'is_valid_slug returns false for strings starting with numbers'
39
+ );
40
+
41
+ -- TEST: Valid single word
42
+ select ok(
43
+ pgflow.is_valid_slug('valid'),
44
+ 'is_valid_slug returns true for single word'
45
+ );
46
+
47
+ -- TEST: Valid with underscore
48
+ select ok(
49
+ pgflow.is_valid_slug('valid_slug'),
50
+ 'is_valid_slug returns true for string with underscore'
51
+ );
52
+
53
+ -- TEST: Valid with numbers (not at start)
54
+ select ok(
55
+ pgflow.is_valid_slug('valid123'),
56
+ 'is_valid_slug returns true for string with numbers not at start'
57
+ );
58
+
59
+ -- TEST: Valid mixed case
60
+ select ok(
61
+ pgflow.is_valid_slug('validSlug'),
62
+ 'is_valid_slug returns true for mixed case string'
63
+ );
64
+
65
+ -- TEST:
66
+ select ok(
67
+ not pgflow.is_valid_slug('run'),
68
+ 'is_valid_slug returns false for reserved word'
69
+ );
70
+
71
+ select finish();
72
+ rollback;
@@ -0,0 +1,35 @@
1
+ begin;
2
+ select plan(1);
3
+ select pgflow_tests.reset_db();
4
+ select pgflow_tests.setup_flow('two_roots');
5
+
6
+ -- Start the flow and complete the root steps
7
+ select pgflow.start_flow('two_roots', '"root input"'::jsonb);
8
+ select pgflow.poll_for_tasks('two_roots', 1, 2);
9
+ select pgflow.complete_task(
10
+ (select run_id from pgflow.runs limit 1),
11
+ 'root_a',
12
+ 0,
13
+ '"root_a output"'::jsonb
14
+ );
15
+ select pgflow.complete_task(
16
+ (select run_id from pgflow.runs limit 1),
17
+ 'root_b',
18
+ 0,
19
+ '"root_b output"'::jsonb
20
+ );
21
+
22
+ -- TEST: Verify that the queued message have the proper input
23
+ select is(
24
+ (select input from pgflow.poll_for_tasks('two_roots', 1, 1)),
25
+ jsonb_build_object(
26
+ 'run', 'root input',
27
+ 'root_a', 'root_a output',
28
+ 'root_b', 'root_b output'
29
+ )
30
+ );
31
+
32
+ select finish();
33
+ rollback;
34
+
35
+ select '"yolox"'::jsonb @> '"yolo"'::jsonb;
@@ -0,0 +1,35 @@
1
+ begin;
2
+ select * from plan(2);
3
+ select pgflow_tests.reset_db();
4
+ select pgflow_tests.setup_flow('sequential');
5
+
6
+ -- SETUP: Start a flow run which will put a single task in the queue
7
+ select pgflow.start_flow('sequential', '"hello"'::jsonb);
8
+
9
+ -- TEST: Poll a single task with big visibility timeout (vt = 10)
10
+ select is(
11
+ (select count(*)::integer from pgflow.poll_for_tasks(
12
+ queue_name => 'sequential'::text,
13
+ vt => 5,
14
+ qty => 1,
15
+ max_poll_seconds => 1
16
+ )),
17
+ 1::integer,
18
+ 'First poll should get the available task'
19
+ );
20
+
21
+ -- TEST: Immediate second poll (simulating concurrent access) should get nothing
22
+ -- because the message is hidden with vt=5
23
+ select is(
24
+ (select count(*)::integer from pgflow.poll_for_tasks(
25
+ queue_name => 'sequential'::text,
26
+ vt => 5,
27
+ qty => 1,
28
+ max_poll_seconds => 1
29
+ )),
30
+ 0::integer,
31
+ 'Concurrent poll should not get the same task (due to visibility timeout)'
32
+ );
33
+
34
+ select * from finish();
35
+ rollback;
@@ -0,0 +1,35 @@
1
+ begin;
2
+ select plan(2);
3
+ select pgflow_tests.reset_db();
4
+
5
+ select pgflow.create_flow('simple', max_attempts => 3, base_delay => 0);
6
+ select pgflow.add_step('simple', 'first');
7
+ select pgflow.add_step('simple', 'last');
8
+ select pgflow.start_flow('simple', '"hello"'::jsonb);
9
+
10
+ -- SETUP: poll twice, first fialing them completing
11
+ select pgflow_tests.poll_and_fail('simple', 1, 1);
12
+ select pgflow_tests.poll_and_complete('simple', 1, 1);
13
+
14
+ -- TEST: polling increments, regardless of failure/completion
15
+ select is(
16
+ (select attempts_count::int from pgflow.step_tasks where step_slug = 'first'),
17
+ 2,
18
+ 'Polling a task should increment its attempts_count, regardless of status'
19
+ );
20
+
21
+ -- SETUP:
22
+ select pgflow_tests.poll_and_fail('simple', 1, 1);
23
+ select pgflow_tests.poll_and_fail('simple', 1, 1);
24
+ select pgflow_tests.poll_and_fail('simple', 1, 1);
25
+
26
+ -- TEST: polling increments, regardless of failure/completion
27
+ select is(
28
+ (select attempts_count::int from pgflow.step_tasks where step_slug = 'last'),
29
+ 3,
30
+ 'Polling a task should increment its attempts_count'
31
+ );
32
+
33
+
34
+ select finish();
35
+ rollback;
@@ -0,0 +1,24 @@
1
+ begin;
2
+ select plan(1);
3
+ select pgflow_tests.reset_db();
4
+ select pgflow_tests.setup_flow('sequential');
5
+
6
+ -- SETUP: Start multiple flow runs which will put multiple tasks in the queue
7
+ select pgflow.start_flow('sequential', '{"id": 1}'::jsonb);
8
+ select pgflow.start_flow('sequential', '{"id": 2}'::jsonb);
9
+ select pgflow.start_flow('sequential', '{"id": 3}'::jsonb);
10
+
11
+ -- TEST: Poll multiple tasks at once (qty = 3)
12
+ select is(
13
+ (select count(*)::integer from pgflow.poll_for_tasks(
14
+ queue_name => 'sequential'::text,
15
+ vt => 5,
16
+ qty => 3,
17
+ max_poll_seconds => 1
18
+ )),
19
+ 3::integer,
20
+ 'Should return 3 tasks when qty=3 and 3 tasks are available'
21
+ );
22
+
23
+ select finish();
24
+ rollback;
@@ -0,0 +1,35 @@
1
+ begin;
2
+ select plan(1);
3
+ select pgflow_tests.reset_db();
4
+ select pgflow_tests.setup_flow('sequential');
5
+
6
+ -- This is a regression test for a bug that was showing up when messages
7
+ -- were not archived properly after being completed
8
+ -- It manifested as completed tasks being updated to 'started'
9
+
10
+ -- SETUP: Start a flow, poll and complete the first task
11
+ select pgflow.start_flow('sequential', '"hello"'::jsonb);
12
+ select pgflow.poll_for_tasks('sequential'::text, 0, 1);
13
+ select pgflow.complete_task(
14
+ (
15
+ select run_id
16
+ from pgflow.runs
17
+ where flow_slug = 'sequential'
18
+ order by run_id
19
+ limit 1
20
+ ),
21
+ 'first',
22
+ 0,
23
+ '"first completed"'::jsonb
24
+ );
25
+ select pgflow.poll_for_tasks('sequential'::text, 0, 1);
26
+
27
+ -- TEST: Already completed tasks should not be changed
28
+ select is(
29
+ (select status from pgflow.step_tasks where step_slug = 'first'),
30
+ 'completed',
31
+ 'Already completed task should not be changed'
32
+ );
33
+
34
+ select finish();
35
+ rollback;
@@ -0,0 +1,38 @@
1
+ begin;
2
+ select plan(1);
3
+ select pgflow_tests.reset_db();
4
+ select pgflow_tests.setup_flow('sequential');
5
+
6
+ -- SETUP: Start a flow run
7
+ select pgflow.start_flow('sequential', '"hello"'::jsonb);
8
+
9
+ -- Prepare the actual query
10
+ PREPARE actual AS
11
+ SELECT flow_slug, run_id, step_slug, input, msg_id
12
+ FROM pgflow.poll_for_tasks (
13
+ queue_name => 'sequential',
14
+ vt => 1,
15
+ qty => 1
16
+ )
17
+ LIMIT 1 ;
18
+
19
+ -- Prepare the expected result
20
+ PREPARE expected AS
21
+ SELECT 'sequential'::text AS flow_slug,
22
+ (SELECT run_id FROM pgflow.runs WHERE flow_slug = 'sequential' LIMIT 1) AS run_id,
23
+ 'first'::text AS step_slug,
24
+ jsonb_build_object ('run', 'hello')::jsonb AS input,
25
+ (SELECT message_id FROM pgflow.step_tasks
26
+ WHERE flow_slug = 'sequential'
27
+ AND step_slug = 'first'
28
+ LIMIT 1) AS msg_id ;
29
+
30
+ -- Compare the results
31
+ SELECT results_eq (
32
+ 'actual',
33
+ 'expected',
34
+ 'poll_for_tasks() returns the expected worker task'
35
+ ) ;
36
+
37
+ SELECT finish () ;
38
+ ROLLBACK ;
@@ -0,0 +1,34 @@
1
+ begin;
2
+ select * from plan(1);
3
+ select pgflow_tests.reset_db();
4
+ select pgflow_tests.setup_flow('sequential');
5
+
6
+ -- SETUP: Start a flow run which will put a task in the queue
7
+ select * from pgflow.start_flow('sequential', '{"id": 1}'::jsonb);
8
+
9
+ -- Manually delete a step_task but keep the message in the queue
10
+ -- This simulates an inconsistent state where a message exists
11
+ -- but there's no corresponding step_task
12
+ delete from pgflow.step_tasks
13
+ where
14
+ run_id = (
15
+ select run_id from pgflow.runs
16
+ where flow_slug = 'sequential' limit 1
17
+ )
18
+ and step_slug = 'first';
19
+
20
+ -- TEST: Polling should not return tasks for missing step_tasks
21
+ -- even though messages might exist in the queue
22
+ select is(
23
+ (select count(*)::integer from pgflow.poll_for_tasks(
24
+ queue_name => 'sequential'::text,
25
+ vt => 5,
26
+ qty => 1,
27
+ max_poll_seconds => 1
28
+ )),
29
+ 0::integer,
30
+ 'Should not return tasks when step_tasks row is missing'
31
+ );
32
+
33
+ select * from finish();
34
+ rollback;
@@ -0,0 +1,19 @@
1
+ begin;
2
+ select * from plan(1);
3
+ select pgflow_tests.reset_db();
4
+ select pgflow_tests.setup_flow('sequential');
5
+
6
+ -- TEST: Polling from an empty queue returns no tasks
7
+ select is(
8
+ (select count(*)::integer from pgflow.poll_for_tasks(
9
+ queue_name => 'sequential'::text,
10
+ vt => 5,
11
+ qty => 10,
12
+ max_poll_seconds => 1
13
+ )),
14
+ 0::integer,
15
+ 'Should return no tasks when queue is empty'
16
+ );
17
+
18
+ select * from finish();
19
+ rollback;
@@ -0,0 +1,22 @@
1
+ begin;
2
+ select * from plan(1);
3
+ select pgflow_tests.reset_db();
4
+ select pgflow_tests.setup_flow('sequential');
5
+
6
+ -- SETUP: Start a flow run which will put a task in the queue
7
+ select pgflow.start_flow('sequential', '{"id": 1}'::jsonb);
8
+
9
+ -- TEST: Calling with qty = 0 should return no tasks
10
+ select is(
11
+ (select count(*)::integer from pgflow.poll_for_tasks(
12
+ queue_name => 'sequential'::text,
13
+ vt => 5,
14
+ qty => 0,
15
+ max_poll_seconds => 1
16
+ )),
17
+ 0::integer,
18
+ 'Should return no tasks when qty=0'
19
+ );
20
+
21
+ select * from finish();
22
+ rollback;
@@ -0,0 +1,41 @@
1
+ begin;
2
+ select * from plan(3);
3
+ select pgflow_tests.reset_db();
4
+
5
+ -- Create a flow with two root steps, each with different opt_timeout values
6
+ select pgflow.create_flow('timeout_test', timeout => 60);
7
+ select pgflow.add_step('timeout_test', 'step_with_flow_timeout');
8
+ select pgflow.add_step('timeout_test', 'step_30', timeout => 30);
9
+ select pgflow.add_step('timeout_test', 'step_45', timeout => 45);
10
+
11
+ -- Start the flow which will create tasks for both root steps
12
+ select pgflow.start_flow('timeout_test', '"test_input"'::jsonb);
13
+
14
+ -- Poll for both tasks (qty=2)
15
+ select * from pgflow.poll_for_tasks('timeout_test', vt => 1, qty => 999);
16
+
17
+ -- Assert that vt_delay is set to opt_timeout + 2 for each step
18
+ select pgflow_tests.assert_retry_delay(
19
+ 'timeout_test',
20
+ 'step_30',
21
+ 32, -- 30 + 2
22
+ 'step_30 should have vt_delay set to opt_timeout (30) + 2 seconds'
23
+ );
24
+
25
+ select pgflow_tests.assert_retry_delay(
26
+ 'timeout_test',
27
+ 'step_45',
28
+ 47, -- 45 + 2
29
+ 'step_45 should have vt_delay set to opt_timeout (45) + 2 seconds'
30
+ );
31
+
32
+ -- Assert that by default it will use flow opt_timeout + 2 if not overridden
33
+ select pgflow_tests.assert_retry_delay(
34
+ 'timeout_test',
35
+ 'step_with_flow_timeout',
36
+ 62, -- 60 + 2
37
+ 'step_with_flow_timeout should have vt_delay set to flow opt_timeout (60) + 2 seconds'
38
+ );
39
+
40
+ select * from finish();
41
+ rollback;
@@ -0,0 +1,59 @@
1
+ begin;
2
+ select * from plan(4);
3
+ select pgflow_tests.reset_db();
4
+ select pgflow_tests.setup_flow('sequential');
5
+
6
+ -- SETUP: Start a flow run which will put a single task in the queue
7
+ select pgflow.start_flow('sequential', '"hello"'::jsonb);
8
+
9
+ -- TEST: Poll a single task
10
+ select is(
11
+ (select count(*)::integer from pgflow.poll_for_tasks(
12
+ queue_name => 'sequential'::text,
13
+ vt => 1,
14
+ qty => 1,
15
+ max_poll_seconds => 1
16
+ )),
17
+ 1::integer,
18
+ 'First poll should get the available task'
19
+ );
20
+
21
+ -- TEST: Second poll before vt expires should not get the task
22
+ select is(
23
+ (select count(*)::integer from pgflow.poll_for_tasks(
24
+ queue_name => 'sequential'::text,
25
+ vt => 1,
26
+ qty => 1,
27
+ max_poll_seconds => 1
28
+ )),
29
+ 0::integer,
30
+ 'Second poll before vt expires should not get the task'
31
+ );
32
+
33
+ -- Wait longer than the visibility timeout
34
+ select pg_sleep(2);
35
+
36
+ -- TEST: Second poll should get the task again because visibility timeout expired
37
+ select is(
38
+ (select count(*)::integer from pgflow.poll_for_tasks(
39
+ queue_name => 'sequential'::text,
40
+ vt => 1,
41
+ qty => 1,
42
+ max_poll_seconds => 1
43
+ )),
44
+ 1::integer,
45
+ 'Second poll should get the task again after visibility timeout expired'
46
+ );
47
+
48
+ -- Verify the task was re-polled (should be the same task)
49
+ select is(
50
+ (
51
+ select status from pgflow.step_tasks
52
+ where flow_slug = 'sequential' and step_slug = 'first'
53
+ ),
54
+ 'queued',
55
+ 'The task should be queued'
56
+ );
57
+
58
+ select * from finish();
59
+ rollback;
@@ -0,0 +1,24 @@
1
+ begin;
2
+ select plan(2);
3
+ select pgflow_tests.reset_db();
4
+ select pgflow_tests.setup_flow('sequential');
5
+
6
+ -- SETUP: Start a flow run
7
+ select pgflow.start_flow('sequential', '"hello"'::jsonb);
8
+
9
+ -- TEST: Run should be created
10
+ select results_eq(
11
+ $$ SELECT flow_slug, status, input from pgflow.runs $$,
12
+ $$ VALUES ('sequential', 'started', '"hello"'::jsonb) $$,
13
+ 'Run should be created with appropriate status and input'
14
+ );
15
+
16
+ -- TEST: remaining_steps should be equal to number of steps
17
+ select is(
18
+ (select remaining_steps::int from pgflow.runs limit 1),
19
+ 3::int,
20
+ 'remaining_steps should be equal to number of steps'
21
+ );
22
+
23
+ select finish();
24
+ rollback;