activerecord-libsql 0.1.7 → 0.1.8
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 +4 -4
- data/CHANGELOG.md +33 -0
- data/activerecord-libsql.gemspec +1 -0
- data/lib/activerecord/libsql/railtie.rb +1 -1
- data/lib/activerecord/libsql/version.rb +1 -1
- data/lib/tasks/turso.rake +233 -0
- data/lib/turso_libsql/database.rb +10 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f3e95b3b3a0f542585ae7ede19f84e2facf3e87e4499248b71f3b5cdb80ae3c0
|
|
4
|
+
data.tar.gz: da7a0f17f5cf1e7f68d7b3fc6b8d8a777a230896893f8bf3c17e915d68ff515e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bbe4d99c6b76a98b58991af803f015db99c6aa41d2d997366d113fa9d0159cd1a55206eaab719cb9c2cc16d57f17c8ff7c8422a910b69e6f15efcc8ce656a51f
|
|
7
|
+
data.tar.gz: 8c8a30e54eea83a936dc2291d8604cbf3d67bd7b9ec8fec508b61d0b39dfaf999fa9ea0cde407dcab9048b5ca501e57d2e5f5afec531e64dcca1b1e561d9f447
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,39 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.1.8] - 2026-03-30
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **`SQLite3::BusyException: database is locked` when Solid Queue forks multiple workers** (#19)
|
|
10
|
+
- `LocalConnection` (used when `replica_path:` is configured) opened SQLite with the default
|
|
11
|
+
DELETE journal mode, which serializes all writes. When Solid Queue forks multiple worker
|
|
12
|
+
processes that all write to the same `.sqlite3` file concurrently, lock contention caused
|
|
13
|
+
an immediate `BusyException`.
|
|
14
|
+
- `LocalConnection#initialize` now sets `PRAGMA journal_mode=WAL` (allows concurrent reads
|
|
15
|
+
and writes across processes) and `PRAGMA busy_timeout=5000` (waits up to 5 seconds on
|
|
16
|
+
lock contention instead of failing immediately).
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **`rake turso:check` task** (#19)
|
|
21
|
+
- Verifies adapter health for all databases configured in `database.yml`.
|
|
22
|
+
- Checks: connection (`SELECT 1`), INSERT, SELECT, `datetime` UTC comparison,
|
|
23
|
+
and transaction (`BEGIN` / `COMMIT` / `ROLLBACK`).
|
|
24
|
+
- Useful after initial installation or when migrating an existing app to Turso.
|
|
25
|
+
- **Embedded Replica mode tests in `solid_queue_fork_spec.rb`** (#19)
|
|
26
|
+
- 4 new examples covering `LocalConnection` with Solid Queue fork patterns.
|
|
27
|
+
- Directly reproduces the `database is locked` scenario (5 concurrent forked workers
|
|
28
|
+
writing to the same SQLite file).
|
|
29
|
+
- Verifies WAL mode and `busy_timeout` are set on the internal connection.
|
|
30
|
+
|
|
31
|
+
### Fixed (infrastructure)
|
|
32
|
+
|
|
33
|
+
- `activerecord-libsql.gemspec`: added `lib/**/*.rake` to `spec.files` so that
|
|
34
|
+
`lib/tasks/turso.rake` is included in the published gem.
|
|
35
|
+
- `lib/activerecord/libsql/railtie.rb`: fixed rake file load path
|
|
36
|
+
(`../../tasks/turso.rake` instead of `../../../tasks/turso.rake`).
|
|
37
|
+
|
|
5
38
|
## [0.1.7] - 2026-03-27
|
|
6
39
|
|
|
7
40
|
### Fixed
|
data/activerecord-libsql.gemspec
CHANGED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'shellwords'
|
|
4
|
+
require 'tempfile'
|
|
5
|
+
require 'fileutils'
|
|
6
|
+
|
|
7
|
+
namespace :turso do
|
|
8
|
+
desc <<~DESC
|
|
9
|
+
Check Turso connection and adapter health for all configured databases.
|
|
10
|
+
|
|
11
|
+
Verifies:
|
|
12
|
+
1. Connection (SELECT 1)
|
|
13
|
+
2. Basic write/read (INSERT / SELECT / DELETE)
|
|
14
|
+
3. datetime UTC normalization (WHERE datetime <= ?)
|
|
15
|
+
4. Transaction (BEGIN / COMMIT / ROLLBACK)
|
|
16
|
+
|
|
17
|
+
Usage:
|
|
18
|
+
rake turso:check
|
|
19
|
+
DESC
|
|
20
|
+
task check: :environment do
|
|
21
|
+
configs = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env)
|
|
22
|
+
|
|
23
|
+
if configs.empty?
|
|
24
|
+
puts "No databases configured for environment: #{Rails.env}"
|
|
25
|
+
exit 1
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
all_ok = true
|
|
29
|
+
|
|
30
|
+
configs.each do |db_config|
|
|
31
|
+
name = db_config.name
|
|
32
|
+
print " [#{name}] "
|
|
33
|
+
|
|
34
|
+
begin
|
|
35
|
+
pool = ActiveRecord::Base.establish_connection(db_config.configuration_hash)
|
|
36
|
+
pool.with_connection do |conn|
|
|
37
|
+
# 1. 接続確認
|
|
38
|
+
conn.execute('SELECT 1')
|
|
39
|
+
print '✓ connect '
|
|
40
|
+
|
|
41
|
+
# 2. 一時テーブルで write/read を確認
|
|
42
|
+
table = "_turso_check_#{SecureRandom.hex(4)}"
|
|
43
|
+
begin
|
|
44
|
+
conn.create_table(table, force: true) do |t|
|
|
45
|
+
t.string :label, null: false
|
|
46
|
+
t.datetime :checked_at, null: false
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# INSERT
|
|
50
|
+
now = Time.now.utc
|
|
51
|
+
conn.execute(
|
|
52
|
+
"INSERT INTO #{table} (label, checked_at) VALUES ('ping', '#{now.strftime('%Y-%m-%d %H:%M:%S')}')"
|
|
53
|
+
)
|
|
54
|
+
print '✓ insert '
|
|
55
|
+
|
|
56
|
+
# SELECT
|
|
57
|
+
rows = conn.execute("SELECT label FROM #{table} WHERE label = 'ping'")
|
|
58
|
+
raise 'SELECT returned no rows' if rows.empty?
|
|
59
|
+
|
|
60
|
+
print '✓ select '
|
|
61
|
+
|
|
62
|
+
# datetime 比較(UTC 正規化の確認)
|
|
63
|
+
future = (now + 60).strftime('%Y-%m-%d %H:%M:%S')
|
|
64
|
+
rows = conn.execute("SELECT label FROM #{table} WHERE checked_at <= '#{future}'")
|
|
65
|
+
raise 'datetime comparison failed' if rows.empty?
|
|
66
|
+
|
|
67
|
+
print '✓ datetime '
|
|
68
|
+
|
|
69
|
+
# トランザクション
|
|
70
|
+
conn.transaction do
|
|
71
|
+
conn.execute("INSERT INTO #{table} (label, checked_at) VALUES ('txn', '#{now.strftime('%Y-%m-%d %H:%M:%S')}')")
|
|
72
|
+
end
|
|
73
|
+
print '✓ transaction '
|
|
74
|
+
ensure
|
|
75
|
+
conn.drop_table(table, if_exists: true)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
puts '→ OK'
|
|
80
|
+
rescue StandardError => e
|
|
81
|
+
puts "→ FAILED: #{e.message}"
|
|
82
|
+
all_ok = false
|
|
83
|
+
ensure
|
|
84
|
+
# primary に戻す
|
|
85
|
+
primary_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, name: 'primary')
|
|
86
|
+
ActiveRecord::Base.establish_connection(primary_config.configuration_hash) if primary_config
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
exit 1 unless all_ok
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
namespace :schema do
|
|
94
|
+
desc <<~DESC
|
|
95
|
+
Apply schema to Turso Cloud using sqldef (sqlite3def).
|
|
96
|
+
|
|
97
|
+
Compares the desired schema (schema.sql) against the current remote schema
|
|
98
|
+
and applies only the diff — idempotent and declarative.
|
|
99
|
+
|
|
100
|
+
Usage:
|
|
101
|
+
rake turso:schema:apply[db/schema.sql]
|
|
102
|
+
|
|
103
|
+
Prerequisites:
|
|
104
|
+
- sqlite3def must be installed (https://github.com/sqldef/sqldef)
|
|
105
|
+
- database.yml must have replica_path configured
|
|
106
|
+
|
|
107
|
+
Flow:
|
|
108
|
+
1. Pull latest frames from remote into local replica
|
|
109
|
+
2. Copy replica to a temp file for sqlite3def (avoids libsql metadata conflict)
|
|
110
|
+
3. Run sqlite3def --dry-run to compute diff SQL
|
|
111
|
+
4. If no diff, exit normally ("Already up to date")
|
|
112
|
+
5. Apply diff SQL to Turso Cloud
|
|
113
|
+
6. Pull again to confirm
|
|
114
|
+
DESC
|
|
115
|
+
task :apply, [:schema_file] => :environment do |_t, args|
|
|
116
|
+
schema_file = args[:schema_file]
|
|
117
|
+
abort 'Usage: rake turso:schema:apply[path/to/schema.sql]' unless schema_file
|
|
118
|
+
abort "Schema file not found: #{schema_file}" unless File.exist?(schema_file)
|
|
119
|
+
|
|
120
|
+
unless system('which sqlite3def > /dev/null 2>&1')
|
|
121
|
+
abort 'sqlite3def not found. Install it from https://github.com/sqldef/sqldef'
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
conn = ActiveRecord::Base.connection
|
|
125
|
+
replica_path = conn.instance_variable_get(:@config)&.dig(:replica_path)
|
|
126
|
+
abort 'replica_path is not configured in database.yml' unless replica_path
|
|
127
|
+
abort "Replica file not found: #{replica_path} (run the app first to initialize)" \
|
|
128
|
+
unless File.exist?(replica_path)
|
|
129
|
+
|
|
130
|
+
puts '==> [1/4] Pulling latest schema from remote...'
|
|
131
|
+
conn.sync
|
|
132
|
+
puts ' Done.'
|
|
133
|
+
|
|
134
|
+
# sqlite3def は SQLite ファイルを直接開くため、libsql の replica と競合しないよう
|
|
135
|
+
# 一時ファイルにコピーして使う。WAL モードの場合は -wal / -shm も一緒にコピーする。
|
|
136
|
+
tmp_db = Tempfile.new(['turso_schema_diff', '.db'])
|
|
137
|
+
tmp_db.close
|
|
138
|
+
begin
|
|
139
|
+
FileUtils.cp(replica_path, tmp_db.path)
|
|
140
|
+
%w[-wal -shm].each do |suffix|
|
|
141
|
+
src = "#{replica_path}#{suffix}"
|
|
142
|
+
FileUtils.cp(src, "#{tmp_db.path}#{suffix}") if File.exist?(src)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
puts '==> [2/4] Computing schema diff...'
|
|
146
|
+
diff_sql = `sqlite3def --dry-run --file #{Shellwords.escape(schema_file)} #{Shellwords.escape(tmp_db.path)} 2>&1`
|
|
147
|
+
exit_status = $?.exitstatus
|
|
148
|
+
|
|
149
|
+
abort "sqlite3def failed (exit #{exit_status}):\n#{diff_sql}" if exit_status != 0
|
|
150
|
+
ensure
|
|
151
|
+
tmp_db.unlink
|
|
152
|
+
FileUtils.rm_f("#{tmp_db.path}-wal")
|
|
153
|
+
FileUtils.rm_f("#{tmp_db.path}-shm")
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# BEGIN / COMMIT / コメント行(--)を除外する
|
|
157
|
+
statements = diff_sql
|
|
158
|
+
.split(';')
|
|
159
|
+
.map(&:strip)
|
|
160
|
+
.reject(&:empty?)
|
|
161
|
+
.reject { |s| s.match?(/\A(BEGIN|COMMIT|ROLLBACK)\z/i) }
|
|
162
|
+
.reject { |s| s.match?(/\A--/) }
|
|
163
|
+
|
|
164
|
+
if statements.empty?
|
|
165
|
+
puts ' Already up to date.'
|
|
166
|
+
next
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
puts " #{statements.size} statement(s) to apply:"
|
|
170
|
+
statements.each { |s| puts " #{s.lines.first&.strip}" }
|
|
171
|
+
|
|
172
|
+
puts '==> [3/4] Applying schema to Turso Cloud...'
|
|
173
|
+
# libsql は DDL トランザクションをサポートしないため、直接実行する
|
|
174
|
+
statements.each { |sql| conn.execute(sql) }
|
|
175
|
+
puts ' Done.'
|
|
176
|
+
|
|
177
|
+
puts '==> [4/4] Pulling to confirm...'
|
|
178
|
+
conn.sync
|
|
179
|
+
puts ' Done.'
|
|
180
|
+
|
|
181
|
+
puts '==> Schema applied successfully!'
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
desc 'Show schema diff between schema.sql and Turso Cloud (dry-run, no changes applied)'
|
|
185
|
+
task :diff, [:schema_file] => :environment do |_t, args|
|
|
186
|
+
schema_file = args[:schema_file]
|
|
187
|
+
abort 'Usage: rake turso:schema:diff[path/to/schema.sql]' unless schema_file
|
|
188
|
+
abort "Schema file not found: #{schema_file}" unless File.exist?(schema_file)
|
|
189
|
+
|
|
190
|
+
unless system('which sqlite3def > /dev/null 2>&1')
|
|
191
|
+
abort 'sqlite3def not found. Install it from https://github.com/sqldef/sqldef'
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
conn = ActiveRecord::Base.connection
|
|
195
|
+
replica_path = conn.instance_variable_get(:@config)&.dig(:replica_path)
|
|
196
|
+
abort 'replica_path is not configured in database.yml' unless replica_path
|
|
197
|
+
abort "Replica file not found: #{replica_path}" unless File.exist?(replica_path)
|
|
198
|
+
|
|
199
|
+
puts '==> Pulling latest schema from remote...'
|
|
200
|
+
conn.sync
|
|
201
|
+
|
|
202
|
+
tmp_db = Tempfile.new(['turso_schema_diff', '.db'])
|
|
203
|
+
tmp_db.close
|
|
204
|
+
begin
|
|
205
|
+
FileUtils.cp(replica_path, tmp_db.path)
|
|
206
|
+
%w[-wal -shm].each do |suffix|
|
|
207
|
+
src = "#{replica_path}#{suffix}"
|
|
208
|
+
FileUtils.cp(src, "#{tmp_db.path}#{suffix}") if File.exist?(src)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
puts '==> Schema diff (sqlite3def dry-run):'
|
|
212
|
+
diff_sql = `sqlite3def --dry-run --file #{Shellwords.escape(schema_file)} #{Shellwords.escape(tmp_db.path)} 2>&1`
|
|
213
|
+
|
|
214
|
+
statements = diff_sql
|
|
215
|
+
.split(';')
|
|
216
|
+
.map(&:strip)
|
|
217
|
+
.reject(&:empty?)
|
|
218
|
+
.reject { |s| s.match?(/\A(BEGIN|COMMIT|ROLLBACK)\z/i) }
|
|
219
|
+
.reject { |s| s.match?(/\A--/) }
|
|
220
|
+
|
|
221
|
+
if statements.empty?
|
|
222
|
+
puts ' No changes. Already up to date.'
|
|
223
|
+
else
|
|
224
|
+
puts diff_sql
|
|
225
|
+
end
|
|
226
|
+
ensure
|
|
227
|
+
tmp_db.unlink
|
|
228
|
+
FileUtils.rm_f("#{tmp_db.path}-wal")
|
|
229
|
+
FileUtils.rm_f("#{tmp_db.path}-shm")
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
@@ -110,6 +110,10 @@ module TursoLibsql
|
|
|
110
110
|
# AR の discard! が @raw_connection を nil にするので、子プロセスでは
|
|
111
111
|
# reconnect が走って新しい接続が確立される。
|
|
112
112
|
class LocalConnection
|
|
113
|
+
# Solid Queue など複数プロセスが同時に書き込む場合のロック待機時間(ミリ秒)。
|
|
114
|
+
# デフォルトの 0ms だと即 SQLite3::BusyException になる。
|
|
115
|
+
BUSY_TIMEOUT_MS = 5000
|
|
116
|
+
|
|
113
117
|
def initialize(path, remote_url, token, mode)
|
|
114
118
|
require 'sqlite3'
|
|
115
119
|
@path = path
|
|
@@ -118,6 +122,12 @@ module TursoLibsql
|
|
|
118
122
|
@mode = mode
|
|
119
123
|
@db = SQLite3::Database.new(path)
|
|
120
124
|
@db.results_as_hash = true
|
|
125
|
+
# WAL モード: 読み取りと書き込みを並行できる。
|
|
126
|
+
# デフォルトの DELETE ジャーナルモードは同時書き込みで database is locked になる。
|
|
127
|
+
# Solid Queue のように複数 fork が同じファイルに書く場合に必須。
|
|
128
|
+
@db.execute('PRAGMA journal_mode=WAL')
|
|
129
|
+
# ロック競合時に即エラーにならず、指定ミリ秒待ってリトライする。
|
|
130
|
+
@db.execute("PRAGMA busy_timeout=#{BUSY_TIMEOUT_MS}")
|
|
121
131
|
@last_insert_rowid = 0
|
|
122
132
|
@last_affected_rows = 0
|
|
123
133
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: activerecord-libsql
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- aileron
|
|
@@ -96,6 +96,7 @@ files:
|
|
|
96
96
|
- lib/activerecord-libsql.rb
|
|
97
97
|
- lib/activerecord/libsql/railtie.rb
|
|
98
98
|
- lib/activerecord/libsql/version.rb
|
|
99
|
+
- lib/tasks/turso.rake
|
|
99
100
|
- lib/turso_libsql.rb
|
|
100
101
|
- lib/turso_libsql/connection.rb
|
|
101
102
|
- lib/turso_libsql/database.rb
|