geneva_drive 0.7.0 → 0.9.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.
Files changed (89) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -0
  3. data/MANUAL.md +299 -5
  4. data/README.md +14 -1
  5. data/Rakefile +28 -1
  6. data/bin/matrix +76 -0
  7. data/bin/md2html +114 -0
  8. data/lib/generators/geneva_drive/install/templates/add_resumable_step_support.rb +5 -9
  9. data/lib/generators/geneva_drive/install/templates/add_signals_support.rb +5 -10
  10. data/lib/generators/geneva_drive/install/templates/create_workflows_migration.rb +1 -1
  11. data/lib/generators/geneva_drive/install/templates/initializer.rb.tt +33 -2
  12. data/lib/geneva_drive/dispatchers/active_job_dispatcher.rb +33 -0
  13. data/lib/geneva_drive/dispatchers/sidekiq_dispatcher.rb +112 -0
  14. data/lib/geneva_drive/engine.rb +13 -0
  15. data/lib/geneva_drive/executor.rb +23 -3
  16. data/lib/geneva_drive/job_options.rb +7 -3
  17. data/lib/geneva_drive/jobs/perform_step_job.rb +2 -35
  18. data/lib/geneva_drive/jobs/sidekiq_housekeeping_job.rb +32 -0
  19. data/lib/geneva_drive/jobs/sidekiq_perform_step_job.rb +35 -0
  20. data/lib/geneva_drive/jobs/step_execution_lookup.rb +42 -0
  21. data/lib/geneva_drive/json_column.rb +72 -0
  22. data/lib/geneva_drive/migration_helpers.rb +60 -3
  23. data/lib/geneva_drive/removed_step_definition.rb +34 -0
  24. data/lib/geneva_drive/signal.rb +4 -2
  25. data/lib/geneva_drive/step_collection.rb +31 -7
  26. data/lib/geneva_drive/step_definition.rb +9 -0
  27. data/lib/geneva_drive/step_execution.rb +3 -2
  28. data/lib/geneva_drive/version.rb +1 -1
  29. data/lib/geneva_drive/workflow.rb +200 -39
  30. data/lib/geneva_drive.rb +65 -0
  31. data/test/dummy/config/database.yml +40 -5
  32. data/test/dummy/config/initializers/geneva_drive.rb +78 -0
  33. data/test/dummy/db/mysql/schema-8.1.rb +95 -0
  34. data/test/dummy/db/postgres/schema-8.1.rb +99 -0
  35. data/test/dummy/db/sqlite/schema-7.2.rb +96 -0
  36. data/test/dummy/db/sqlite/schema-8.0.rb +96 -0
  37. data/test/dummy/db/sqlite/schema-8.1.rb +96 -0
  38. data/test/dummy/db/sqlite/test.sqlite3 +0 -0
  39. data/test/dummy/db/sqlite/test.sqlite3-0 +0 -0
  40. data/test/dummy/db/sqlite/test.sqlite3-1 +0 -0
  41. data/test/dummy/db/sqlite/test.sqlite3-2 +0 -0
  42. data/test/dummy/db/sqlite/test.sqlite3-3 +0 -0
  43. data/test/dummy/db/sqlite/test.sqlite3-4 +0 -0
  44. data/test/dummy/db/sqlite/test.sqlite3-5 +0 -0
  45. data/test/dummy/db/sqlite/test.sqlite3-6 +0 -0
  46. data/test/dummy/db/sqlite/test.sqlite3-7 +0 -0
  47. data/test/dummy/db/sqlite/test.sqlite3-8 +0 -0
  48. data/test/dummy/db/sqlite/test.sqlite3-9 +0 -0
  49. data/test/dummy/db/sqlite/test.sqlite3_0 +0 -0
  50. data/test/dummy/db/sqlite/test.sqlite3_1 +0 -0
  51. data/test/dummy/db/sqlite/test.sqlite3_2 +0 -0
  52. data/test/dummy/db/sqlite/test.sqlite3_3 +0 -0
  53. data/test/dummy/db/sqlite/test.sqlite3_4 +0 -0
  54. data/test/dummy/db/sqlite/test.sqlite3_5 +0 -0
  55. data/test/dummy/db/sqlite/test.sqlite3_6 +0 -0
  56. data/test/dummy/db/sqlite/test.sqlite3_7 +0 -0
  57. data/test/dummy/db/sqlite/test.sqlite3_8 +0 -0
  58. data/test/dummy/db/sqlite/test.sqlite3_9 +0 -0
  59. data/test/dummy/db/sqlite3/schema-7.2.rb +96 -0
  60. data/test/dummy/db/sqlite3/schema-8.1.rb +96 -0
  61. data/test/dummy/db/sqlite3/test.sqlite3 +0 -0
  62. data/test/dummy/db/sqlite3/test.sqlite3_0 +0 -0
  63. data/test/dummy/db/sqlite3/test.sqlite3_1 +0 -0
  64. data/test/dummy/db/sqlite3/test.sqlite3_2 +0 -0
  65. data/test/dummy/db/sqlite3/test.sqlite3_3 +0 -0
  66. data/test/dummy/db/sqlite3/test.sqlite3_4 +0 -0
  67. data/test/dummy/db/sqlite3/test.sqlite3_5 +0 -0
  68. data/test/dummy/db/sqlite3/test.sqlite3_6 +0 -0
  69. data/test/dummy/db/sqlite3/test.sqlite3_7 +0 -0
  70. data/test/dummy/db/sqlite3/test.sqlite3_8 +0 -0
  71. data/test/dummy/db/sqlite3/test.sqlite3_9 +0 -0
  72. data/test/dummy_install/db/migrate/20260217112949_create_geneva_drive_workflows.rb +76 -0
  73. data/test/dummy_install/db/migrate/20260217112950_create_geneva_drive_step_executions.rb +99 -0
  74. data/test/dummy_install/db/migrate/20260217112951_add_finished_at_to_geneva_drive_step_executions.rb +27 -0
  75. data/test/dummy_install/db/migrate/20260217112952_add_error_class_name_to_geneva_drive_step_executions.rb +9 -0
  76. data/test/dummy_install/db/migrate/20260217112953_add_resumable_step_support_to_geneva_drive_step_executions.rb +128 -0
  77. data/test/dummy_install/db/migrate/20260217112954_add_chained_step_executions_to_geneva_drive_step_executions.rb +136 -0
  78. data/test/dummy_install/db/schema.rb +75 -0
  79. data/test/dummy_install/log/test.log +1852 -0
  80. data/test/generators/install_generator_test.rb +56 -0
  81. data/test/geneva_drive_test.rb +10 -0
  82. data/test/migration_helpers_test.rb +62 -0
  83. data/test/sidekiq/sidekiq_dispatcher_test.rb +322 -0
  84. data/test/sidekiq/sidekiq_redis_test.rb +115 -0
  85. data/test/test_helper.rb +51 -13
  86. data/test/workflow/pause_resume_test.rb +15 -0
  87. data/test/workflow/removed_step_test.rb +294 -0
  88. data/test/workflow/resume_at_test.rb +170 -0
  89. metadata +62 -1
@@ -0,0 +1,96 @@
1
+ # This file is auto-generated from the current state of the database. Instead
2
+ # of editing this file, please use the migrations feature of Active Record to
3
+ # incrementally modify your database, and then regenerate this schema definition.
4
+ #
5
+ # This file is the source Rails uses to define your schema when running `bin/rails
6
+ # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
7
+ # be faster and is potentially less error prone than running all of your
8
+ # migrations from scratch. Old migrations may fail to apply correctly if those
9
+ # migrations use external dependencies or application code.
10
+ #
11
+ # It's strongly recommended that you check this file into your version control system.
12
+
13
+ ActiveRecord::Schema[8.1].define(version: 2026_10_25_000614) do
14
+ create_table "geneva_drive_signals", force: :cascade do |t|
15
+ t.integer "workflow_id", null: false
16
+ t.string "name", null: false
17
+ t.string "idempotency_key"
18
+ t.string "state", default: "pending", null: false
19
+ t.json "payload"
20
+ t.datetime "claimed_at"
21
+ t.datetime "consumed_at"
22
+ t.bigint "claimed", default: 0, null: false
23
+ t.bigint "consumed", default: 0, null: false
24
+ t.datetime "created_at", null: false
25
+ t.datetime "updated_at", null: false
26
+ t.index ["workflow_id", "name", "idempotency_key"], name: "index_geneva_drive_signals_dedup", unique: true
27
+ t.index ["workflow_id", "state"], name: "index_geneva_drive_signals_on_workflow_id_and_state"
28
+ t.index ["workflow_id"], name: "index_geneva_drive_signals_on_workflow_id"
29
+ end
30
+
31
+ create_table "geneva_drive_step_executions", force: :cascade do |t|
32
+ t.integer "workflow_id", null: false
33
+ t.string "step_name", null: false
34
+ t.string "state", default: "scheduled", null: false
35
+ t.string "outcome"
36
+ t.datetime "scheduled_for", null: false
37
+ t.datetime "started_at"
38
+ t.datetime "completed_at"
39
+ t.datetime "failed_at"
40
+ t.datetime "canceled_at"
41
+ t.datetime "skipped_at"
42
+ t.string "error_class_name"
43
+ t.text "error_message"
44
+ t.text "error_backtrace"
45
+ t.string "job_id"
46
+ t.text "metadata"
47
+ t.datetime "created_at", null: false
48
+ t.datetime "updated_at", null: false
49
+ t.datetime "finished_at"
50
+ t.json "cursor"
51
+ t.bigint "continues_from_id"
52
+ t.bigint "signal_id"
53
+ t.datetime "waiting_since"
54
+ t.index ["continues_from_id"], name: "index_geneva_drive_step_executions_on_continues_from_id"
55
+ t.index ["finished_at"], name: "index_geneva_drive_step_executions_on_finished_at"
56
+ t.index ["scheduled_for"], name: "index_geneva_drive_step_executions_on_scheduled_for"
57
+ t.index ["signal_id"], name: "index_geneva_drive_step_executions_on_signal_id"
58
+ t.index ["started_at"], name: "index_geneva_drive_step_executions_in_progress_started_at", where: "state = 'in_progress'"
59
+ t.index ["state", "scheduled_for"], name: "index_geneva_drive_step_executions_scheduled"
60
+ t.index ["state"], name: "index_geneva_drive_step_executions_on_state"
61
+ t.index ["workflow_id", "created_at"], name: "idx_on_workflow_id_created_at_af16a14fb2"
62
+ t.index ["workflow_id", "state"], name: "index_geneva_drive_step_executions_on_workflow_id_and_state"
63
+ t.index ["workflow_id"], name: "index_geneva_drive_step_executions_on_workflow_id"
64
+ t.index ["workflow_id"], name: "index_geneva_drive_step_executions_one_active", unique: true, where: "state IN ('scheduled', 'in_progress')"
65
+ end
66
+
67
+ create_table "geneva_drive_workflows", force: :cascade do |t|
68
+ t.string "type", null: false
69
+ t.string "hero_type"
70
+ t.integer "hero_id"
71
+ t.string "state", default: "ready", null: false
72
+ t.string "current_step_name"
73
+ t.string "next_step_name"
74
+ t.boolean "allow_multiple", default: false, null: false
75
+ t.text "metadata"
76
+ t.datetime "started_at"
77
+ t.datetime "transitioned_at"
78
+ t.datetime "created_at", null: false
79
+ t.datetime "updated_at", null: false
80
+ t.index ["hero_type", "hero_id"], name: "index_geneva_drive_workflows_on_hero_type_and_hero_id"
81
+ t.index ["state"], name: "index_geneva_drive_workflows_on_state"
82
+ t.index ["type", "hero_type", "hero_id"], name: "index_geneva_drive_workflows_unique_ongoing", unique: true, where: "state NOT IN ('finished', 'canceled') AND allow_multiple = 0"
83
+ t.index ["type"], name: "index_geneva_drive_workflows_on_type"
84
+ end
85
+
86
+ create_table "users", force: :cascade do |t|
87
+ t.string "email", null: false
88
+ t.string "name"
89
+ t.boolean "active", default: true
90
+ t.datetime "created_at", null: false
91
+ t.datetime "updated_at", null: false
92
+ end
93
+
94
+ add_foreign_key "geneva_drive_signals", "geneva_drive_workflows", column: "workflow_id", on_delete: :cascade
95
+ add_foreign_key "geneva_drive_step_executions", "geneva_drive_workflows", column: "workflow_id", on_delete: :cascade
96
+ end
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,96 @@
1
+ # This file is auto-generated from the current state of the database. Instead
2
+ # of editing this file, please use the migrations feature of Active Record to
3
+ # incrementally modify your database, and then regenerate this schema definition.
4
+ #
5
+ # This file is the source Rails uses to define your schema when running `bin/rails
6
+ # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
7
+ # be faster and is potentially less error prone than running all of your
8
+ # migrations from scratch. Old migrations may fail to apply correctly if those
9
+ # migrations use external dependencies or application code.
10
+ #
11
+ # It's strongly recommended that you check this file into your version control system.
12
+
13
+ ActiveRecord::Schema[7.2].define(version: 2026_10_25_090462) do
14
+ create_table "geneva_drive_signals", force: :cascade do |t|
15
+ t.integer "workflow_id", null: false
16
+ t.string "name", null: false
17
+ t.string "idempotency_key"
18
+ t.string "state", default: "pending", null: false
19
+ t.json "payload"
20
+ t.datetime "claimed_at"
21
+ t.datetime "consumed_at"
22
+ t.bigint "claimed", default: 0, null: false
23
+ t.bigint "consumed", default: 0, null: false
24
+ t.datetime "created_at", null: false
25
+ t.datetime "updated_at", null: false
26
+ t.index ["workflow_id", "name", "idempotency_key"], name: "index_geneva_drive_signals_dedup", unique: true
27
+ t.index ["workflow_id", "state"], name: "index_geneva_drive_signals_on_workflow_id_and_state"
28
+ t.index ["workflow_id"], name: "index_geneva_drive_signals_on_workflow_id"
29
+ end
30
+
31
+ create_table "geneva_drive_step_executions", force: :cascade do |t|
32
+ t.integer "workflow_id", null: false
33
+ t.string "step_name", null: false
34
+ t.string "state", default: "scheduled", null: false
35
+ t.string "outcome"
36
+ t.datetime "scheduled_for", null: false
37
+ t.datetime "started_at"
38
+ t.datetime "completed_at"
39
+ t.datetime "failed_at"
40
+ t.datetime "canceled_at"
41
+ t.datetime "skipped_at"
42
+ t.string "error_class_name"
43
+ t.text "error_message"
44
+ t.text "error_backtrace"
45
+ t.string "job_id"
46
+ t.text "metadata"
47
+ t.datetime "created_at", null: false
48
+ t.datetime "updated_at", null: false
49
+ t.datetime "finished_at"
50
+ t.json "cursor"
51
+ t.bigint "continues_from_id"
52
+ t.bigint "signal_id"
53
+ t.datetime "waiting_since"
54
+ t.index ["continues_from_id"], name: "index_geneva_drive_step_executions_on_continues_from_id"
55
+ t.index ["finished_at"], name: "index_geneva_drive_step_executions_on_finished_at"
56
+ t.index ["scheduled_for"], name: "index_geneva_drive_step_executions_on_scheduled_for"
57
+ t.index ["signal_id"], name: "index_geneva_drive_step_executions_on_signal_id"
58
+ t.index ["started_at"], name: "index_geneva_drive_step_executions_in_progress_started_at", where: "state = 'in_progress'"
59
+ t.index ["state", "scheduled_for"], name: "index_geneva_drive_step_executions_scheduled"
60
+ t.index ["state"], name: "index_geneva_drive_step_executions_on_state"
61
+ t.index ["workflow_id", "created_at"], name: "idx_on_workflow_id_created_at_af16a14fb2"
62
+ t.index ["workflow_id", "state"], name: "index_geneva_drive_step_executions_on_workflow_id_and_state"
63
+ t.index ["workflow_id"], name: "index_geneva_drive_step_executions_on_workflow_id"
64
+ t.index ["workflow_id"], name: "index_geneva_drive_step_executions_one_active", unique: true, where: "state IN ('scheduled', 'in_progress')"
65
+ end
66
+
67
+ create_table "geneva_drive_workflows", force: :cascade do |t|
68
+ t.string "type", null: false
69
+ t.string "hero_type"
70
+ t.integer "hero_id"
71
+ t.string "state", default: "ready", null: false
72
+ t.string "current_step_name"
73
+ t.string "next_step_name"
74
+ t.boolean "allow_multiple", default: false, null: false
75
+ t.text "metadata"
76
+ t.datetime "started_at"
77
+ t.datetime "transitioned_at"
78
+ t.datetime "created_at", null: false
79
+ t.datetime "updated_at", null: false
80
+ t.index ["hero_type", "hero_id"], name: "index_geneva_drive_workflows_on_hero_type_and_hero_id"
81
+ t.index ["state"], name: "index_geneva_drive_workflows_on_state"
82
+ t.index ["type", "hero_type", "hero_id"], name: "index_geneva_drive_workflows_unique_ongoing", unique: true, where: "state NOT IN ('finished', 'canceled') AND allow_multiple = 0"
83
+ t.index ["type"], name: "index_geneva_drive_workflows_on_type"
84
+ end
85
+
86
+ create_table "users", force: :cascade do |t|
87
+ t.string "email", null: false
88
+ t.string "name"
89
+ t.boolean "active", default: true
90
+ t.datetime "created_at", null: false
91
+ t.datetime "updated_at", null: false
92
+ end
93
+
94
+ add_foreign_key "geneva_drive_signals", "geneva_drive_workflows", column: "workflow_id", on_delete: :cascade
95
+ add_foreign_key "geneva_drive_step_executions", "geneva_drive_workflows", column: "workflow_id", on_delete: :cascade
96
+ end
@@ -0,0 +1,96 @@
1
+ # This file is auto-generated from the current state of the database. Instead
2
+ # of editing this file, please use the migrations feature of Active Record to
3
+ # incrementally modify your database, and then regenerate this schema definition.
4
+ #
5
+ # This file is the source Rails uses to define your schema when running `bin/rails
6
+ # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
7
+ # be faster and is potentially less error prone than running all of your
8
+ # migrations from scratch. Old migrations may fail to apply correctly if those
9
+ # migrations use external dependencies or application code.
10
+ #
11
+ # It's strongly recommended that you check this file into your version control system.
12
+
13
+ ActiveRecord::Schema[8.1].define(version: 2026_10_25_090462) do
14
+ create_table "geneva_drive_signals", force: :cascade do |t|
15
+ t.integer "workflow_id", null: false
16
+ t.string "name", null: false
17
+ t.string "idempotency_key"
18
+ t.string "state", default: "pending", null: false
19
+ t.json "payload"
20
+ t.datetime "claimed_at"
21
+ t.datetime "consumed_at"
22
+ t.bigint "claimed", default: 0, null: false
23
+ t.bigint "consumed", default: 0, null: false
24
+ t.datetime "created_at", null: false
25
+ t.datetime "updated_at", null: false
26
+ t.index ["workflow_id", "name", "idempotency_key"], name: "index_geneva_drive_signals_dedup", unique: true
27
+ t.index ["workflow_id", "state"], name: "index_geneva_drive_signals_on_workflow_id_and_state"
28
+ t.index ["workflow_id"], name: "index_geneva_drive_signals_on_workflow_id"
29
+ end
30
+
31
+ create_table "geneva_drive_step_executions", force: :cascade do |t|
32
+ t.integer "workflow_id", null: false
33
+ t.string "step_name", null: false
34
+ t.string "state", default: "scheduled", null: false
35
+ t.string "outcome"
36
+ t.datetime "scheduled_for", null: false
37
+ t.datetime "started_at"
38
+ t.datetime "completed_at"
39
+ t.datetime "failed_at"
40
+ t.datetime "canceled_at"
41
+ t.datetime "skipped_at"
42
+ t.string "error_class_name"
43
+ t.text "error_message"
44
+ t.text "error_backtrace"
45
+ t.string "job_id"
46
+ t.text "metadata"
47
+ t.datetime "created_at", null: false
48
+ t.datetime "updated_at", null: false
49
+ t.datetime "finished_at"
50
+ t.json "cursor"
51
+ t.bigint "continues_from_id"
52
+ t.bigint "signal_id"
53
+ t.datetime "waiting_since"
54
+ t.index ["continues_from_id"], name: "index_geneva_drive_step_executions_on_continues_from_id"
55
+ t.index ["finished_at"], name: "index_geneva_drive_step_executions_on_finished_at"
56
+ t.index ["scheduled_for"], name: "index_geneva_drive_step_executions_on_scheduled_for"
57
+ t.index ["signal_id"], name: "index_geneva_drive_step_executions_on_signal_id"
58
+ t.index ["started_at"], name: "index_geneva_drive_step_executions_in_progress_started_at", where: "state = 'in_progress'"
59
+ t.index ["state", "scheduled_for"], name: "index_geneva_drive_step_executions_scheduled"
60
+ t.index ["state"], name: "index_geneva_drive_step_executions_on_state"
61
+ t.index ["workflow_id", "created_at"], name: "idx_on_workflow_id_created_at_af16a14fb2"
62
+ t.index ["workflow_id", "state"], name: "index_geneva_drive_step_executions_on_workflow_id_and_state"
63
+ t.index ["workflow_id"], name: "index_geneva_drive_step_executions_on_workflow_id"
64
+ t.index ["workflow_id"], name: "index_geneva_drive_step_executions_one_active", unique: true, where: "state IN ('scheduled', 'in_progress')"
65
+ end
66
+
67
+ create_table "geneva_drive_workflows", force: :cascade do |t|
68
+ t.string "type", null: false
69
+ t.string "hero_type"
70
+ t.integer "hero_id"
71
+ t.string "state", default: "ready", null: false
72
+ t.string "current_step_name"
73
+ t.string "next_step_name"
74
+ t.boolean "allow_multiple", default: false, null: false
75
+ t.text "metadata"
76
+ t.datetime "started_at"
77
+ t.datetime "transitioned_at"
78
+ t.datetime "created_at", null: false
79
+ t.datetime "updated_at", null: false
80
+ t.index ["hero_type", "hero_id"], name: "index_geneva_drive_workflows_on_hero_type_and_hero_id"
81
+ t.index ["state"], name: "index_geneva_drive_workflows_on_state"
82
+ t.index ["type", "hero_type", "hero_id"], name: "index_geneva_drive_workflows_unique_ongoing", unique: true, where: "state NOT IN ('finished', 'canceled') AND allow_multiple = 0"
83
+ t.index ["type"], name: "index_geneva_drive_workflows_on_type"
84
+ end
85
+
86
+ create_table "users", force: :cascade do |t|
87
+ t.string "email", null: false
88
+ t.string "name"
89
+ t.boolean "active", default: true
90
+ t.datetime "created_at", null: false
91
+ t.datetime "updated_at", null: false
92
+ end
93
+
94
+ add_foreign_key "geneva_drive_signals", "geneva_drive_workflows", column: "workflow_id", on_delete: :cascade
95
+ add_foreign_key "geneva_drive_step_executions", "geneva_drive_workflows", column: "workflow_id", on_delete: :cascade
96
+ end
Binary file
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ # license: LGPLv3.0
4
+
5
+ class CreateGenevaDriveWorkflows < ActiveRecord::Migration[7.2]
6
+ include GenevaDrive::MigrationHelpers
7
+
8
+ def change
9
+ create_table :geneva_drive_workflows, **geneva_drive_table_options do |t|
10
+ # Core identification (STI)
11
+ t.string :type, null: false, index: true
12
+
13
+ # Polymorphic association to the hero of the workflow
14
+ t.string :hero_type, null: false
15
+ t.column :hero_id, geneva_drive_key_type, null: false
16
+
17
+ # State machine
18
+ t.string :state, null: false, default: "ready", index: true
19
+
20
+ # Current position in workflow
21
+ t.string :current_step_name
22
+ t.string :next_step_name
23
+
24
+ # Multiple workflows of same type for same hero
25
+ t.boolean :allow_multiple, default: false, null: false
26
+
27
+ # Timestamps
28
+ t.datetime :started_at
29
+ t.datetime :transitioned_at
30
+
31
+ t.timestamps
32
+ end
33
+
34
+ # Polymorphic index
35
+ add_index :geneva_drive_workflows, [:hero_type, :hero_id]
36
+
37
+ reversible do |direction|
38
+ direction.up { create_db_specific_indices }
39
+ end
40
+ end
41
+
42
+ def create_db_specific_indices
43
+ # Database-specific uniqueness constraint for ongoing workflows
44
+ # Ensures only one ongoing workflow per (type, hero) unless allow_multiple is true
45
+ adapter = connection.adapter_name.downcase
46
+ if adapter.include?("postgresql")
47
+ execute <<-SQL
48
+ CREATE UNIQUE INDEX index_geneva_drive_workflows_unique_ongoing
49
+ ON geneva_drive_workflows (type, hero_type, hero_id)
50
+ WHERE state NOT IN ('finished', 'canceled') AND allow_multiple = false;
51
+ SQL
52
+ elsif adapter.include?("mysql")
53
+ execute <<-SQL
54
+ ALTER TABLE geneva_drive_workflows
55
+ ADD COLUMN ongoing_unique_key VARCHAR(767)
56
+ AS (
57
+ CASE
58
+ WHEN state NOT IN ('finished', 'canceled') AND allow_multiple = 0
59
+ THEN CONCAT(type, '-', hero_type, '-', hero_id)
60
+ ELSE NULL
61
+ END
62
+ ) STORED;
63
+ SQL
64
+ execute <<-SQL
65
+ CREATE UNIQUE INDEX index_geneva_drive_workflows_unique_ongoing
66
+ ON geneva_drive_workflows (ongoing_unique_key);
67
+ SQL
68
+ elsif adapter.include?("sqlite")
69
+ execute <<-SQL
70
+ CREATE UNIQUE INDEX index_geneva_drive_workflows_unique_ongoing
71
+ ON geneva_drive_workflows (type, hero_type, hero_id)
72
+ WHERE state NOT IN ('finished', 'canceled') AND allow_multiple = 0;
73
+ SQL
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ # license: LGPLv3.0
4
+
5
+ class CreateGenevaDriveStepExecutions < ActiveRecord::Migration[7.2]
6
+ include GenevaDrive::MigrationHelpers
7
+
8
+ def change
9
+ key_type = geneva_drive_key_type
10
+
11
+ create_table :geneva_drive_step_executions, **geneva_drive_table_options do |t|
12
+ # Link to workflow (cascade delete when workflow is deleted)
13
+ t.references :workflow,
14
+ null: false,
15
+ foreign_key: {to_table: :geneva_drive_workflows, on_delete: :cascade},
16
+ index: true,
17
+ type: key_type
18
+
19
+ # Which step this execution represents
20
+ t.string :step_name, null: false
21
+
22
+ # Execution state machine
23
+ t.string :state, null: false, default: "scheduled", index: true
24
+
25
+ # Outcome for audit purposes
26
+ t.string :outcome
27
+
28
+ # Scheduling
29
+ t.datetime :scheduled_for, null: false, index: true
30
+
31
+ # Execution tracking (individual timestamps for audit trail)
32
+ t.datetime :started_at
33
+ t.datetime :completed_at
34
+ t.datetime :failed_at
35
+ t.datetime :canceled_at
36
+ t.datetime :skipped_at
37
+
38
+ # Error tracking
39
+ t.string :error_class_name
40
+ t.text :error_message
41
+ t.text :error_backtrace
42
+
43
+ # Job tracking (for debugging)
44
+ t.string :job_id
45
+
46
+ t.timestamps
47
+ end
48
+
49
+ # Index for finding scheduled executions
50
+ add_index :geneva_drive_step_executions,
51
+ [:state, :scheduled_for],
52
+ name: "index_geneva_drive_step_executions_scheduled"
53
+
54
+ # Index for workflow execution history
55
+ add_index :geneva_drive_step_executions, [:workflow_id, :created_at]
56
+
57
+ # Index for common query patterns
58
+ add_index :geneva_drive_step_executions, [:workflow_id, :state]
59
+
60
+ reversible do |direction|
61
+ direction.up { create_db_specific_indices }
62
+ end
63
+ end
64
+
65
+ def create_db_specific_indices
66
+ # Database-specific uniqueness constraint for active step executions
67
+ # Ensures only one active (scheduled/in_progress) step per workflow
68
+ adapter = connection.adapter_name.downcase
69
+ if adapter.include?("postgresql")
70
+ execute <<-SQL
71
+ CREATE UNIQUE INDEX index_geneva_drive_step_executions_one_active
72
+ ON geneva_drive_step_executions (workflow_id)
73
+ WHERE state IN ('scheduled', 'in_progress');
74
+ SQL
75
+ elsif adapter.include?("mysql")
76
+ execute <<-SQL
77
+ ALTER TABLE geneva_drive_step_executions
78
+ ADD COLUMN active_unique_key VARCHAR(767)
79
+ AS (
80
+ CASE
81
+ WHEN state IN ('scheduled', 'in_progress')
82
+ THEN CAST(workflow_id AS CHAR)
83
+ ELSE NULL
84
+ END
85
+ ) STORED;
86
+ SQL
87
+ execute <<-SQL
88
+ CREATE UNIQUE INDEX index_geneva_drive_step_executions_one_active
89
+ ON geneva_drive_step_executions (active_unique_key);
90
+ SQL
91
+ elsif adapter.include?("sqlite")
92
+ execute <<-SQL
93
+ CREATE UNIQUE INDEX index_geneva_drive_step_executions_one_active
94
+ ON geneva_drive_step_executions (workflow_id)
95
+ WHERE state IN ('scheduled', 'in_progress');
96
+ SQL
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ # license: LGPLv3.0
4
+
5
+ class AddFinishedAtToGenevaDriveStepExecutions < ActiveRecord::Migration[7.2]
6
+ def change
7
+ add_column :geneva_drive_step_executions, :finished_at, :datetime
8
+
9
+ add_index :geneva_drive_step_executions, :finished_at
10
+
11
+ reversible do |direction|
12
+ direction.up do
13
+ # Backfill finished_at from existing timestamp columns.
14
+ # Uses COALESCE to pick the first non-null value.
15
+ execute <<-SQL.squish
16
+ UPDATE geneva_drive_step_executions
17
+ SET finished_at = COALESCE(completed_at, failed_at, canceled_at, skipped_at)
18
+ WHERE finished_at IS NULL
19
+ AND (completed_at IS NOT NULL
20
+ OR failed_at IS NOT NULL
21
+ OR canceled_at IS NOT NULL
22
+ OR skipped_at IS NOT NULL)
23
+ SQL
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ # license: LGPLv3.0
4
+
5
+ class AddErrorClassNameToGenevaDriveStepExecutions < ActiveRecord::Migration[7.2]
6
+ def change
7
+ add_column :geneva_drive_step_executions, :error_class_name, :string, if_not_exists: true
8
+ end
9
+ end