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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +487 -0
- data/lib/shikibu/activity.rb +135 -0
- data/lib/shikibu/app.rb +299 -0
- data/lib/shikibu/channels.rb +360 -0
- data/lib/shikibu/constants.rb +70 -0
- data/lib/shikibu/context.rb +208 -0
- data/lib/shikibu/errors.rb +137 -0
- data/lib/shikibu/integrations/active_job.rb +95 -0
- data/lib/shikibu/integrations/sidekiq.rb +104 -0
- data/lib/shikibu/locking.rb +110 -0
- data/lib/shikibu/middleware/rack_app.rb +197 -0
- data/lib/shikibu/notify/notify_base.rb +67 -0
- data/lib/shikibu/notify/pg_notify.rb +217 -0
- data/lib/shikibu/notify/wake_event.rb +56 -0
- data/lib/shikibu/outbox/relayer.rb +227 -0
- data/lib/shikibu/replay.rb +361 -0
- data/lib/shikibu/retry_policy.rb +81 -0
- data/lib/shikibu/storage/migrations.rb +179 -0
- data/lib/shikibu/storage/sequel_storage.rb +883 -0
- data/lib/shikibu/version.rb +5 -0
- data/lib/shikibu/worker.rb +389 -0
- data/lib/shikibu/workflow.rb +398 -0
- data/lib/shikibu.rb +152 -0
- data/schema/LICENSE +21 -0
- data/schema/README.md +57 -0
- data/schema/db/migrations/mysql/20251217000000_initial_schema.sql +284 -0
- data/schema/db/migrations/postgresql/20251217000000_initial_schema.sql +284 -0
- data/schema/db/migrations/sqlite/20251217000000_initial_schema.sql +284 -0
- data/schema/docs/column-values.md +91 -0
- metadata +231 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'sequel'
|
|
4
|
+
|
|
5
|
+
module Shikibu
|
|
6
|
+
module Storage
|
|
7
|
+
# Handles database schema migrations from shared durax-io/schema
|
|
8
|
+
# Compatible with dbmate migration format and schema_migrations tracking
|
|
9
|
+
class Migrations
|
|
10
|
+
SCHEMA_PATH = File.expand_path('../../../schema/db/migrations', __dir__)
|
|
11
|
+
|
|
12
|
+
# Advisory lock key for migration (consistent across workers)
|
|
13
|
+
MIGRATION_LOCK_KEY = 20_251_217_000_000
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
# Apply migrations to the database
|
|
17
|
+
# @param db [Sequel::Database] The database connection
|
|
18
|
+
# @return [Array<String>] List of applied migration versions
|
|
19
|
+
def apply(db)
|
|
20
|
+
db_type = detect_db_type(db)
|
|
21
|
+
migration_dir = File.join(SCHEMA_PATH, db_type)
|
|
22
|
+
|
|
23
|
+
raise Error, "Migration directory not found: #{migration_dir}" unless File.directory?(migration_dir)
|
|
24
|
+
|
|
25
|
+
# Ensure schema_migrations table exists
|
|
26
|
+
ensure_schema_migrations_table(db)
|
|
27
|
+
|
|
28
|
+
# Use advisory lock to ensure only one worker applies migrations
|
|
29
|
+
with_migration_lock(db, db_type) do |acquired|
|
|
30
|
+
unless acquired
|
|
31
|
+
# Another worker is applying migrations, wait and return
|
|
32
|
+
sleep(1)
|
|
33
|
+
return []
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
apply_pending_migrations(db, migration_dir)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def detect_db_type(db)
|
|
43
|
+
case db.database_type
|
|
44
|
+
when :postgres then 'postgresql'
|
|
45
|
+
when :mysql, :mysql2 then 'mysql'
|
|
46
|
+
else 'sqlite'
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Create schema_migrations table if it doesn't exist (dbmate compatible)
|
|
51
|
+
def ensure_schema_migrations_table(db)
|
|
52
|
+
db.run <<~SQL
|
|
53
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
54
|
+
version VARCHAR(255) PRIMARY KEY
|
|
55
|
+
)
|
|
56
|
+
SQL
|
|
57
|
+
rescue Sequel::DatabaseError => e
|
|
58
|
+
# Handle concurrent table creation
|
|
59
|
+
raise unless e.message.include?('already exists') || e.message.include?('duplicate')
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Get set of already applied migration versions
|
|
63
|
+
def get_applied_migrations(db)
|
|
64
|
+
db[:schema_migrations].select_map(:version).to_set
|
|
65
|
+
rescue Sequel::DatabaseError
|
|
66
|
+
# Table might not exist yet
|
|
67
|
+
Set.new
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Record a migration as applied
|
|
71
|
+
# @return [Boolean] true if recorded, false if already exists (race condition)
|
|
72
|
+
def record_migration(db, version)
|
|
73
|
+
db[:schema_migrations].insert(version: version)
|
|
74
|
+
true
|
|
75
|
+
rescue Sequel::UniqueConstraintViolation
|
|
76
|
+
# Another worker already recorded this migration
|
|
77
|
+
false
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Extract version from migration filename
|
|
81
|
+
# e.g., "20251217000000_initial_schema.sql" -> "20251217000000"
|
|
82
|
+
def extract_version(filename)
|
|
83
|
+
basename = File.basename(filename)
|
|
84
|
+
match = basename.match(/^(\d+)_/)
|
|
85
|
+
match ? match[1] : basename.sub('.sql', '')
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Apply pending migrations with version tracking
|
|
89
|
+
def apply_pending_migrations(db, migration_dir)
|
|
90
|
+
applied = get_applied_migrations(db)
|
|
91
|
+
migration_files = Dir.glob(File.join(migration_dir, '*.sql'))
|
|
92
|
+
applied_versions = []
|
|
93
|
+
|
|
94
|
+
migration_files.each do |file|
|
|
95
|
+
version = extract_version(file)
|
|
96
|
+
|
|
97
|
+
# Skip if already applied
|
|
98
|
+
next if applied.include?(version)
|
|
99
|
+
|
|
100
|
+
# Apply migration
|
|
101
|
+
apply_migration(db, file)
|
|
102
|
+
|
|
103
|
+
# Record as applied (handles race condition)
|
|
104
|
+
applied_versions << version if record_migration(db, version)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
applied_versions
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Acquire advisory lock for migrations (database-specific)
|
|
111
|
+
def with_migration_lock(db, db_type, &)
|
|
112
|
+
case db_type
|
|
113
|
+
when 'postgresql'
|
|
114
|
+
with_postgres_lock(db, &)
|
|
115
|
+
when 'mysql'
|
|
116
|
+
with_mysql_lock(db, &)
|
|
117
|
+
else
|
|
118
|
+
# SQLite doesn't need advisory locks (single-writer)
|
|
119
|
+
yield true
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def with_postgres_lock(db)
|
|
124
|
+
acquired = db.get(Sequel.function(:pg_try_advisory_lock, MIGRATION_LOCK_KEY))
|
|
125
|
+
begin
|
|
126
|
+
yield acquired
|
|
127
|
+
ensure
|
|
128
|
+
db.get(Sequel.function(:pg_advisory_unlock, MIGRATION_LOCK_KEY)) if acquired
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def with_mysql_lock(db)
|
|
133
|
+
acquired = db.get(Sequel.function(:GET_LOCK, 'shikibu_migration', 0)) == 1
|
|
134
|
+
begin
|
|
135
|
+
yield acquired
|
|
136
|
+
ensure
|
|
137
|
+
db.get(Sequel.function(:RELEASE_LOCK, 'shikibu_migration')) if acquired
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def apply_migration(db, file)
|
|
142
|
+
sql = File.read(file)
|
|
143
|
+
|
|
144
|
+
# Extract migrate:up section
|
|
145
|
+
up_sql = extract_up_section(sql)
|
|
146
|
+
return if up_sql.nil? || up_sql.strip.empty?
|
|
147
|
+
|
|
148
|
+
# Execute each statement
|
|
149
|
+
statements = split_statements(up_sql)
|
|
150
|
+
statements.each do |statement|
|
|
151
|
+
next if statement.strip.empty?
|
|
152
|
+
|
|
153
|
+
begin
|
|
154
|
+
db.run(statement)
|
|
155
|
+
rescue Sequel::DatabaseError => e
|
|
156
|
+
# Ignore "table already exists" errors for idempotent migrations
|
|
157
|
+
raise unless e.message.include?('already exists') ||
|
|
158
|
+
e.message.include?('duplicate')
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def extract_up_section(sql)
|
|
164
|
+
# Split by "-- migrate:down" and take the first part
|
|
165
|
+
parts = sql.split(/--\s*migrate:down/i)
|
|
166
|
+
up_section = parts.first
|
|
167
|
+
|
|
168
|
+
# Remove "-- migrate:up" marker
|
|
169
|
+
up_section&.sub(/--\s*migrate:up/i, '')
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def split_statements(sql)
|
|
173
|
+
# Split by semicolon at end of line
|
|
174
|
+
sql.split(/;\s*$/).map(&:strip).reject(&:empty?)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|