activerecord-turso 0.1.0 → 0.2.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 +4 -4
- data/README.md +59 -15
- data/lib/active_record/connection_adapters/turso_adapter/connection_management.rb +11 -12
- data/lib/active_record/connection_adapters/turso_adapter/database_statements.rb +30 -60
- data/lib/active_record/connection_adapters/turso_adapter/error_translation.rb +8 -0
- data/lib/active_record/connection_adapters/turso_adapter/schema_statements.rb +10 -0
- data/lib/active_record/connection_adapters/turso_adapter/statement_pool.rb +18 -0
- data/lib/active_record/connection_adapters/turso_adapter/transaction_management.rb +36 -9
- data/lib/active_record/connection_adapters/turso_adapter.rb +7 -4
- data/lib/activerecord-turso/version.rb +5 -0
- data/lib/turso/ar/connection.rb +90 -7
- metadata +18 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 69e6be67a21fd809792bf867f81f6205bb06a6cc457aa05c2b80080cf272fd9b
|
|
4
|
+
data.tar.gz: d1a18f2e763cb6b5781521f1e8bd12f8a7159d3350c2fe5e91fe02ee00a50f1f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: defa80378418aabb5b0b619392e180102f633ce7f8f6a13d6778f34a47c47f14c2739dc5248aaf0011e8f40788002ae42fea373789e3fc8b5e6e1ea5ebac8b35
|
|
7
|
+
data.tar.gz: 573a7f4f59c98621b9c4f95af1daa2a8fe133b3e487ef83c7d73af51c8c65655cbff080ef2d3813d7e64868273b186f121d69653499bee67f1180463fc946f9a
|
data/README.md
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Activerecord-turso
|
|
2
2
|
|
|
3
|
-
ActiveRecord adapter for [Turso](https://turso.tech) (SQLite compatible).
|
|
3
|
+
ActiveRecord adapter for [Turso](https://turso.tech) (SQLite compatible db).
|
|
4
4
|
|
|
5
5
|
## Status
|
|
6
6
|
|
|
7
|
-
This adapter is under active development. It can run basic ActiveRecord operations against Turso today, but several production features are still being hardened. See [Limitations and Risks](#limitations-and-risks) before using it in production.
|
|
7
|
+
This adapter is under **active** development. It can run basic ActiveRecord operations against Turso today, but several production features are still being hardened. See [Limitations and Risks](#limitations-and-risks) before using it in production.
|
|
8
8
|
|
|
9
9
|
## Requirements
|
|
10
10
|
|
|
11
11
|
- Ruby >= 3.0
|
|
12
12
|
- ActiveRecord >= 8.0, < 8.2
|
|
13
|
-
- The `turso` Ruby gem
|
|
13
|
+
- The `turso` Ruby gem
|
|
14
14
|
|
|
15
15
|
## Installation
|
|
16
16
|
|
|
17
17
|
Add to your Gemfile:
|
|
18
18
|
|
|
19
19
|
```ruby
|
|
20
|
-
gem "activerecord-turso"
|
|
20
|
+
gem "activerecord-turso"
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
Then configure `database.yml`:
|
|
@@ -94,13 +94,24 @@ class AddFtsToPosts < ActiveRecord::Migration[8.1]
|
|
|
94
94
|
end
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
-
Query with
|
|
97
|
+
Query with the adapter helpers:
|
|
98
98
|
|
|
99
|
-
```
|
|
100
|
-
|
|
99
|
+
```ruby
|
|
100
|
+
match = ActiveRecord::Base.connection.fts_match(:posts, [:title, :body], "database")
|
|
101
|
+
Post.where(match)
|
|
102
|
+
|
|
103
|
+
score = ActiveRecord::Base.connection.fts_score(:posts, [:title, :body], "database")
|
|
104
|
+
Post.select(:id, :title, score.as("rank")).where(match).order("rank ASC")
|
|
101
105
|
```
|
|
102
106
|
|
|
103
|
-
Note: `fts5` virtual tables are not supported. Use `USING fts` indexes instead.
|
|
107
|
+
Note: `fts5` virtual tables are not supported. Use `USING fts` indexes instead. This requires the `index_method` experimental feature:
|
|
108
|
+
|
|
109
|
+
```yaml
|
|
110
|
+
production:
|
|
111
|
+
adapter: turso
|
|
112
|
+
database: db/production.sqlite3
|
|
113
|
+
experimental_features: "index_method"
|
|
114
|
+
```
|
|
104
115
|
|
|
105
116
|
### Concurrent transactions
|
|
106
117
|
|
|
@@ -112,19 +123,39 @@ development:
|
|
|
112
123
|
database: db/dev.sqlite3
|
|
113
124
|
journal_mode: mvcc
|
|
114
125
|
busy_timeout: 5000
|
|
126
|
+
|
|
127
|
+
# Enable experimental Turso features when needed (e.g., custom index methods for FTS).
|
|
128
|
+
# Pass a comma-separated string or an array of feature names.
|
|
129
|
+
experimental_features: "index_method"
|
|
115
130
|
```
|
|
116
131
|
|
|
117
132
|
Use `concurrent: true` inside the same pinned connection:
|
|
118
133
|
|
|
119
134
|
```ruby
|
|
120
135
|
ActiveRecord::Base.connection.transaction(concurrent: true) do
|
|
121
|
-
# read-modify-write
|
|
136
|
+
# read-modify-write using raw SQL or bulk operations
|
|
122
137
|
end
|
|
123
138
|
```
|
|
124
139
|
|
|
125
140
|
Caveats:
|
|
126
141
|
- The connection must stay checked out for the whole transaction; avoid work that yields the ActiveRecord connection back to the pool.
|
|
127
142
|
- `busy_timeout` is used for lock waits, not MVCC snapshot conflicts. Snapshot conflicts retry with bounded backoff.
|
|
143
|
+
- Normal ActiveRecord model persistence (`create!`, `save!`, `update!`, `touch`) is **not supported** inside a concurrent transaction because ActiveRecord opens its own internal transaction for each model change. Use raw SQL (`execute`, `exec_query`) or bulk operations (`insert_all`, `update_all`, `update_columns`) instead.
|
|
144
|
+
- FTS custom index modules are not supported in MVCC mode.
|
|
145
|
+
|
|
146
|
+
## Recommended production configuration
|
|
147
|
+
|
|
148
|
+
```yaml
|
|
149
|
+
production:
|
|
150
|
+
adapter: turso
|
|
151
|
+
database: db/production.sqlite3
|
|
152
|
+
journal_mode: wal # Use mvcc only if you need concurrent writers and understand the caveats above
|
|
153
|
+
pool: 5
|
|
154
|
+
timeout: 5000 # busy_timeout default in milliseconds
|
|
155
|
+
query_timeout: 30000 # maximum time a single query may run
|
|
156
|
+
busy_timeout: 5000 # explicit busy timeout (falls back to timeout)
|
|
157
|
+
experimental_features: "index_method"
|
|
158
|
+
```
|
|
128
159
|
|
|
129
160
|
## Limitations and Risks
|
|
130
161
|
|
|
@@ -138,19 +169,20 @@ The adapter builds column type maps from `column_decltype` metadata. This works
|
|
|
138
169
|
|
|
139
170
|
The adapter's batch execution path splits multi-statement SQL on semicolons. This means SQL containing semicolons inside string literals, triggers, or stored expressions may be split incorrectly. Avoid relying on multi-statement strings other than simple schema dumps.
|
|
140
171
|
|
|
141
|
-
### 3. MVCC requires opt-in and has
|
|
172
|
+
### 3. MVCC requires opt-in and has ActiveRecord compatibility caveats
|
|
142
173
|
|
|
143
174
|
`BEGIN CONCURRENT` is powerful but breaks ActiveRecord's default assumptions:
|
|
144
175
|
|
|
145
176
|
- ActiveRecord expects transactions to commit unless the database returns an error. With `BEGIN CONCURRENT`, the commit can fail with a snapshot conflict and must be retried.
|
|
146
177
|
- The retry loop must run on the same connection. Rails' connection pool is not MVCC-aware and may return the connection to the pool between retries.
|
|
147
178
|
- Do not combine concurrent transactions with pessimistic locking (`lock!`, `with_lock`, `lock_version`).
|
|
179
|
+
- Normal model persistence (`create!`, `save!`, `update!`, `touch`) is unsupported inside a concurrent transaction because ActiveRecord opens an internal transaction for each model change. The error is retried a few times, then raised as `ActiveRecord::StatementInvalid`.
|
|
148
180
|
|
|
149
|
-
Only use `transaction(concurrent: true)` after testing it under your app's concurrency patterns.
|
|
181
|
+
Only use `transaction(concurrent: true)` after testing it under your app's concurrency patterns, and prefer raw SQL or bulk operations inside the block.
|
|
150
182
|
|
|
151
|
-
### 4. Prepared statement cache is
|
|
183
|
+
### 4. Prepared statement cache is enabled with a bounded pool
|
|
152
184
|
|
|
153
|
-
The adapter
|
|
185
|
+
The adapter uses a bounded statement pool with a default limit inherited from Rails (typically `1000`). Statements are evicted with an LRU policy and finalized against the underlying Turso connection. Set `statement_limit` in `database.yml` to tune the pool size.
|
|
154
186
|
|
|
155
187
|
### 5. ActiveRecord 8.0 support is CI-tested, not locally tested
|
|
156
188
|
|
|
@@ -175,7 +207,19 @@ bundle install
|
|
|
175
207
|
bundle exec rake test
|
|
176
208
|
```
|
|
177
209
|
|
|
178
|
-
|
|
210
|
+
Run with MVCC mode:
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
TURSO_TEST_JOURNAL_MODE=mvcc bundle exec rake test
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Run with generated-columns experimental feature:
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
TURSO_TEST_EXPERIMENTAL_FEATURES=generated_columns bundle exec rake test
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
The CI workflow in `.github/workflows/ci.yml` runs all three configurations automatically.
|
|
179
223
|
|
|
180
224
|
## License
|
|
181
225
|
|
|
@@ -9,14 +9,16 @@ module ActiveRecord
|
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
module ClassMethods
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
def new_client(config)
|
|
13
|
+
db_config = config.symbolize_keys.merge(:timeout => config[:timeout] || 5000)
|
|
14
|
+
db_config[:experimental_features] = Array(db_config[:experimental_features])
|
|
15
|
+
::Turso::AR::Connection.new(db_config)
|
|
16
|
+
end
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
def active?
|
|
19
|
-
|
|
20
|
+
return false unless @raw_connection
|
|
21
|
+
!@raw_connection.closed?
|
|
20
22
|
rescue ::Turso::Error
|
|
21
23
|
false
|
|
22
24
|
end
|
|
@@ -57,14 +59,11 @@ module ActiveRecord
|
|
|
57
59
|
|
|
58
60
|
execute("PRAGMA foreign_keys = ON")
|
|
59
61
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
@raw_connection.busy_timeout = timeout
|
|
63
|
-
end
|
|
62
|
+
timeout = @config[:busy_timeout] || @config[:timeout] || 5000
|
|
63
|
+
@raw_connection.busy_timeout = timeout
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
end
|
|
65
|
+
query_timeout = @config[:query_timeout] || 30_000
|
|
66
|
+
@raw_connection.query_timeout = query_timeout
|
|
68
67
|
end
|
|
69
68
|
end
|
|
70
69
|
end
|
|
@@ -9,41 +9,6 @@ module ActiveRecord
|
|
|
9
9
|
raw_execute(sql, name)
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
-
def exec_query(sql, name = "SQL", binds = [], prepare: false)
|
|
13
|
-
materialize_transactions
|
|
14
|
-
|
|
15
|
-
type_casted_binds = type_casted_binds(binds)
|
|
16
|
-
log(sql, name, binds, type_casted_binds) do
|
|
17
|
-
with_raw_connection do |conn|
|
|
18
|
-
stmt = conn.prepare(sql)
|
|
19
|
-
stmt.bind_positional(type_casted_binds) unless type_casted_binds.empty?
|
|
20
|
-
result = build_result(stmt)
|
|
21
|
-
stmt.finalize
|
|
22
|
-
result
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def exec_insert(sql, name = nil, binds = [], pk = nil, sequence_name = nil, returning = nil)
|
|
28
|
-
if returning && supports_insert_returning?
|
|
29
|
-
return exec_query(sql, name, binds)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
materialize_transactions
|
|
33
|
-
|
|
34
|
-
type_casted_binds = type_casted_binds(binds)
|
|
35
|
-
log(sql, name, binds, type_casted_binds) do
|
|
36
|
-
with_raw_connection do |conn|
|
|
37
|
-
stmt = conn.prepare(sql)
|
|
38
|
-
stmt.bind_positional(type_casted_binds) unless type_casted_binds.empty?
|
|
39
|
-
stmt.execute
|
|
40
|
-
last_id = pk ? last_inserted_id(sql) : nil
|
|
41
|
-
stmt.finalize
|
|
42
|
-
last_id
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
|
|
47
12
|
def last_inserted_id(sql)
|
|
48
13
|
@raw_connection.last_insert_rowid
|
|
49
14
|
end
|
|
@@ -85,22 +50,37 @@ module ActiveRecord
|
|
|
85
50
|
return ActiveRecord::Result.empty(affected_rows: 0)
|
|
86
51
|
end
|
|
87
52
|
|
|
88
|
-
if
|
|
89
|
-
raw_connection.
|
|
90
|
-
affected_rows = raw_connection.changes
|
|
91
|
-
verified!
|
|
92
|
-
notification_payload[:affected_rows] = affected_rows
|
|
93
|
-
notification_payload[:row_count] = 0
|
|
94
|
-
ActiveRecord::Result.empty(affected_rows: affected_rows)
|
|
53
|
+
stmt = if prepare
|
|
54
|
+
@statements[sql] ||= raw_connection.prepare(sql)
|
|
95
55
|
else
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
56
|
+
raw_connection.prepare(sql)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
stmt.reset if prepare
|
|
60
|
+
stmt.bind_positional(type_casted_binds) unless type_casted_binds.empty?
|
|
61
|
+
|
|
62
|
+
begin
|
|
63
|
+
if write_query?(sql)
|
|
64
|
+
affected_rows = stmt.execute
|
|
65
|
+
verified!
|
|
66
|
+
notification_payload[:affected_rows] = affected_rows
|
|
67
|
+
notification_payload[:row_count] = 0
|
|
68
|
+
ActiveRecord::Result.empty(affected_rows: affected_rows)
|
|
69
|
+
else
|
|
70
|
+
columns = (0...stmt.column_count).map { |i| stmt.column_name(i) }
|
|
71
|
+
rows = []
|
|
72
|
+
while stmt.step == 1
|
|
73
|
+
rows << stmt.row.to_a
|
|
74
|
+
end
|
|
75
|
+
affected_rows = raw_connection.changes
|
|
76
|
+
verified!
|
|
77
|
+
notification_payload[:affected_rows] = affected_rows
|
|
78
|
+
notification_payload[:row_count] = rows.length
|
|
79
|
+
type_map = build_type_map(stmt)
|
|
80
|
+
ActiveRecord::Result.new(columns, rows, type_map, affected_rows: affected_rows)
|
|
81
|
+
end
|
|
82
|
+
ensure
|
|
83
|
+
stmt.finalize unless prepare
|
|
104
84
|
end
|
|
105
85
|
end
|
|
106
86
|
|
|
@@ -122,16 +102,6 @@ module ActiveRecord
|
|
|
122
102
|
|
|
123
103
|
private
|
|
124
104
|
|
|
125
|
-
def build_result(stmt)
|
|
126
|
-
columns = (0...stmt.column_count).map { |i| stmt.column_name(i) }
|
|
127
|
-
type_map = build_type_map(stmt)
|
|
128
|
-
rows = []
|
|
129
|
-
while stmt.step == 1
|
|
130
|
-
rows << stmt.row.to_a
|
|
131
|
-
end
|
|
132
|
-
ActiveRecord::Result.new(columns, rows, type_map)
|
|
133
|
-
end
|
|
134
|
-
|
|
135
105
|
def build_type_map(stmt)
|
|
136
106
|
type_map = {}
|
|
137
107
|
count = stmt.column_count
|
|
@@ -16,6 +16,14 @@ module ActiveRecord
|
|
|
16
16
|
when ::Turso::BusyError
|
|
17
17
|
ActiveRecord::Deadlocked.new(message, sql: sql, binds: binds)
|
|
18
18
|
when ::Turso::ReadonlyError
|
|
19
|
+
ActiveRecord::ReadOnlyRecord.new(message, sql: sql, binds: binds)
|
|
20
|
+
when ::Turso::IoError, ::Turso::CorruptError
|
|
21
|
+
ActiveRecord::StatementInvalid.new(message, sql: sql, binds: binds)
|
|
22
|
+
when ::Turso::DatabaseFullError
|
|
23
|
+
ActiveRecord::StatementInvalid.new(message, sql: sql, binds: binds)
|
|
24
|
+
when ::Turso::InterruptError
|
|
25
|
+
ActiveRecord::QueryCanceled.new(message, sql: sql, binds: binds)
|
|
26
|
+
when ::Turso::MisuseError
|
|
19
27
|
ActiveRecord::StatementInvalid.new(message, sql: sql, binds: binds)
|
|
20
28
|
else
|
|
21
29
|
super
|
|
@@ -75,6 +75,16 @@ module ActiveRecord
|
|
|
75
75
|
SQL
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
+
def fts_match(_table_name, columns, query)
|
|
79
|
+
cols = Array(columns).map { |c| quote_table_name(c) }.join(", ")
|
|
80
|
+
Arel.sql("fts_match(#{cols}, #{quote(query)})")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def fts_score(_table_name, columns, query)
|
|
84
|
+
cols = Array(columns).map { |c| quote_table_name(c) }.join(", ")
|
|
85
|
+
Arel.sql("fts_score(#{cols}, #{quote(query)})")
|
|
86
|
+
end
|
|
87
|
+
|
|
78
88
|
private
|
|
79
89
|
|
|
80
90
|
def options_to_fts_options(options)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveRecord
|
|
4
|
+
module ConnectionAdapters
|
|
5
|
+
class TursoAdapter < SQLite3Adapter
|
|
6
|
+
class StatementPool < ConnectionAdapters::StatementPool
|
|
7
|
+
alias reset clear
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def dealloc(stmt)
|
|
12
|
+
stmt.finalize
|
|
13
|
+
rescue ::Turso::Error
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -43,35 +43,62 @@ module ActiveRecord
|
|
|
43
43
|
private
|
|
44
44
|
|
|
45
45
|
def transaction_with_mvcc(options, &block)
|
|
46
|
+
connect if @raw_connection.nil?
|
|
47
|
+
|
|
46
48
|
unless @mvcc_enabled
|
|
47
49
|
raise ActiveRecord::AdapterError,
|
|
48
50
|
"transaction(concurrent: true) requires journal_mode: 'mvcc' in database.yml"
|
|
49
51
|
end
|
|
50
52
|
|
|
53
|
+
if options[:requires_new] == false
|
|
54
|
+
raise ActiveRecord::AdapterError,
|
|
55
|
+
"transaction(concurrent: true) is incompatible with nested transactions"
|
|
56
|
+
end
|
|
57
|
+
|
|
51
58
|
max_retries = @config.fetch(:concurrent_retry_limit, 50)
|
|
52
59
|
base_delay_ms = @config.fetch(:concurrent_retry_base_ms, 2)
|
|
53
60
|
retries = 0
|
|
61
|
+
pinned_connection_id = @raw_connection.object_id
|
|
54
62
|
|
|
55
63
|
loop do
|
|
56
64
|
begin
|
|
57
|
-
|
|
58
|
-
yield
|
|
59
|
-
|
|
60
|
-
return
|
|
65
|
+
@raw_connection.execute("BEGIN CONCURRENT")
|
|
66
|
+
result = yield
|
|
67
|
+
@raw_connection.execute("COMMIT")
|
|
68
|
+
return result
|
|
61
69
|
rescue ActiveRecord::StatementInvalid => e
|
|
62
|
-
|
|
70
|
+
rollback_if_active
|
|
63
71
|
raise unless concurrent_conflict?(e) && retries < max_retries
|
|
64
72
|
|
|
65
73
|
retries += 1
|
|
66
|
-
|
|
74
|
+
verified!
|
|
75
|
+
backoff(base_delay_ms, retries)
|
|
76
|
+
|
|
77
|
+
unless @raw_connection.object_id == pinned_connection_id
|
|
78
|
+
raise ActiveRecord::AdapterError,
|
|
79
|
+
"MVCC retry detected a different database connection. " \
|
|
80
|
+
"transaction(concurrent: true) must run on a pinned connection."
|
|
81
|
+
end
|
|
67
82
|
end
|
|
68
83
|
end
|
|
69
84
|
end
|
|
70
85
|
|
|
71
86
|
def concurrent_conflict?(exception)
|
|
72
|
-
exception.cause
|
|
73
|
-
|
|
74
|
-
|
|
87
|
+
cause = exception.cause
|
|
88
|
+
conflict_classes = [::Turso::BusySnapshotError, ::Turso::BusyError]
|
|
89
|
+
conflict_classes << ::Turso::WriteWriteConflict if defined?(::Turso::WriteWriteConflict)
|
|
90
|
+
|
|
91
|
+
conflict_classes.any? { |klass| cause.is_a?(klass) } ||
|
|
92
|
+
/snapshot conflict|busy snapshot|database is locked|cannot start a transaction within a transaction/i.match?(exception.message)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def rollback_if_active
|
|
96
|
+
return unless @raw_connection && !@raw_connection.closed?
|
|
97
|
+
@raw_connection.execute("ROLLBACK") rescue nil
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def backoff(base_delay_ms, retries)
|
|
101
|
+
sleep(base_delay_ms * retries / 1000.0)
|
|
75
102
|
end
|
|
76
103
|
|
|
77
104
|
def savepoint_name(name)
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
require "active_record/connection_adapters/sqlite3_adapter"
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
require_relative "turso_adapter/statement_pool"
|
|
6
|
+
|
|
7
|
+
Dir[File.expand_path("turso_adapter/*.rb", __dir__)].sort.each { |f| require f }
|
|
6
8
|
|
|
7
9
|
module ActiveRecord
|
|
8
10
|
module ConnectionAdapters
|
|
@@ -35,14 +37,14 @@ module ActiveRecord
|
|
|
35
37
|
true
|
|
36
38
|
end
|
|
37
39
|
|
|
38
|
-
def explain(arel, binds = [])
|
|
40
|
+
def explain(arel, binds = [], _options = [])
|
|
39
41
|
sql = "EXPLAIN QUERY PLAN " + to_sql(arel, binds)
|
|
40
42
|
result = exec_query(sql, "EXPLAIN", binds)
|
|
41
43
|
SQLite3::ExplainPrettyPrinter.new.pp(result)
|
|
42
44
|
end
|
|
43
45
|
|
|
44
|
-
def
|
|
45
|
-
|
|
46
|
+
def build_statement_pool
|
|
47
|
+
StatementPool.new(self.class.type_cast_config_to_integer(@config[:statement_limit]))
|
|
46
48
|
end
|
|
47
49
|
|
|
48
50
|
def database_file_exists?
|
|
@@ -69,6 +71,7 @@ module ActiveRecord
|
|
|
69
71
|
def initialize_type_map(m)
|
|
70
72
|
super
|
|
71
73
|
m.register_type(%r(boolean)i, Type::Boolean.new)
|
|
74
|
+
m.register_type(%r(json)i, Type::Json.new)
|
|
72
75
|
end
|
|
73
76
|
end
|
|
74
77
|
end
|
data/lib/turso/ar/connection.rb
CHANGED
|
@@ -9,11 +9,17 @@ module Turso
|
|
|
9
9
|
|
|
10
10
|
def_delegators :@db, :close, :closed?, :changes, :total_changes
|
|
11
11
|
|
|
12
|
+
DEFAULT_BUSY_TIMEOUT_MS = 5000
|
|
13
|
+
DEFAULT_QUERY_TIMEOUT_MS = 30_000
|
|
14
|
+
|
|
12
15
|
def initialize(config)
|
|
13
16
|
@config = config
|
|
14
|
-
|
|
15
|
-
busy_timeout: config[:busy_timeout] || config[:timeout],
|
|
16
|
-
query_timeout: config[:query_timeout]
|
|
17
|
+
db_opts = {
|
|
18
|
+
busy_timeout: config[:busy_timeout] || config[:timeout] || DEFAULT_BUSY_TIMEOUT_MS,
|
|
19
|
+
query_timeout: config[:query_timeout] || DEFAULT_QUERY_TIMEOUT_MS
|
|
20
|
+
}
|
|
21
|
+
db_opts[:experimental_features] = config[:experimental_features] if config[:experimental_features]
|
|
22
|
+
@db = ::Turso::DB.new(config[:database].to_s, **db_opts)
|
|
17
23
|
end
|
|
18
24
|
|
|
19
25
|
def last_insert_rowid
|
|
@@ -42,14 +48,13 @@ module Turso
|
|
|
42
48
|
end
|
|
43
49
|
|
|
44
50
|
def execute_batch(sql)
|
|
45
|
-
sql
|
|
46
|
-
|
|
47
|
-
@db.execute(s) unless s.empty?
|
|
51
|
+
split_batch(sql).each do |stmt|
|
|
52
|
+
@db.execute(stmt)
|
|
48
53
|
end
|
|
49
54
|
end
|
|
50
55
|
|
|
51
56
|
def prepare(sql)
|
|
52
|
-
@db.
|
|
57
|
+
@db.prepare(sql)
|
|
53
58
|
end
|
|
54
59
|
|
|
55
60
|
def busy_timeout=(ms)
|
|
@@ -66,6 +71,84 @@ module Turso
|
|
|
66
71
|
|
|
67
72
|
private
|
|
68
73
|
|
|
74
|
+
def split_batch(sql)
|
|
75
|
+
statements = []
|
|
76
|
+
current = +""
|
|
77
|
+
in_string = false
|
|
78
|
+
in_line_comment = false
|
|
79
|
+
in_block_comment = false
|
|
80
|
+
i = 0
|
|
81
|
+
while i < sql.length
|
|
82
|
+
char = sql[i]
|
|
83
|
+
next_char = sql[i + 1]
|
|
84
|
+
|
|
85
|
+
if in_line_comment
|
|
86
|
+
if char == "\n"
|
|
87
|
+
in_line_comment = false
|
|
88
|
+
end
|
|
89
|
+
i += 1
|
|
90
|
+
next
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
if in_block_comment
|
|
94
|
+
if char == "*" && next_char == "/"
|
|
95
|
+
in_block_comment = false
|
|
96
|
+
i += 2
|
|
97
|
+
else
|
|
98
|
+
i += 1
|
|
99
|
+
end
|
|
100
|
+
next
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
if in_string
|
|
104
|
+
if char == "'" && next_char == "'"
|
|
105
|
+
current << char << next_char
|
|
106
|
+
i += 2
|
|
107
|
+
next
|
|
108
|
+
elsif char == "'"
|
|
109
|
+
in_string = false
|
|
110
|
+
current << char
|
|
111
|
+
i += 1
|
|
112
|
+
next
|
|
113
|
+
end
|
|
114
|
+
current << char
|
|
115
|
+
i += 1
|
|
116
|
+
next
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
case char
|
|
120
|
+
when "'"
|
|
121
|
+
in_string = true
|
|
122
|
+
current << char
|
|
123
|
+
when "-"
|
|
124
|
+
if next_char == "-"
|
|
125
|
+
in_line_comment = true
|
|
126
|
+
i += 2
|
|
127
|
+
next
|
|
128
|
+
end
|
|
129
|
+
current << char
|
|
130
|
+
when "/"
|
|
131
|
+
if next_char == "*"
|
|
132
|
+
in_block_comment = true
|
|
133
|
+
i += 2
|
|
134
|
+
next
|
|
135
|
+
end
|
|
136
|
+
current << char
|
|
137
|
+
when ";"
|
|
138
|
+
stmt = current.strip
|
|
139
|
+
statements << stmt unless stmt.empty?
|
|
140
|
+
current = +""
|
|
141
|
+
else
|
|
142
|
+
current << char
|
|
143
|
+
end
|
|
144
|
+
i += 1
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
stmt = current.strip
|
|
148
|
+
statements << stmt unless stmt.empty?
|
|
149
|
+
statements
|
|
150
|
+
end
|
|
151
|
+
|
|
69
152
|
def normalize_binds(binds)
|
|
70
153
|
binds.map do |value|
|
|
71
154
|
case value
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: activerecord-turso
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ben D'Angelo
|
|
@@ -43,6 +43,20 @@ dependencies:
|
|
|
43
43
|
- - "~>"
|
|
44
44
|
- !ruby/object:Gem::Version
|
|
45
45
|
version: '0.1'
|
|
46
|
+
- !ruby/object:Gem::Dependency
|
|
47
|
+
name: irb
|
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
|
49
|
+
requirements:
|
|
50
|
+
- - ">="
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '0'
|
|
53
|
+
type: :development
|
|
54
|
+
prerelease: false
|
|
55
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - ">="
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '0'
|
|
46
60
|
executables: []
|
|
47
61
|
extensions: []
|
|
48
62
|
extra_rdoc_files: []
|
|
@@ -53,8 +67,10 @@ files:
|
|
|
53
67
|
- lib/active_record/connection_adapters/turso_adapter/database_statements.rb
|
|
54
68
|
- lib/active_record/connection_adapters/turso_adapter/error_translation.rb
|
|
55
69
|
- lib/active_record/connection_adapters/turso_adapter/schema_statements.rb
|
|
70
|
+
- lib/active_record/connection_adapters/turso_adapter/statement_pool.rb
|
|
56
71
|
- lib/active_record/connection_adapters/turso_adapter/transaction_management.rb
|
|
57
72
|
- lib/activerecord-turso.rb
|
|
73
|
+
- lib/activerecord-turso/version.rb
|
|
58
74
|
- lib/turso/ar/connection.rb
|
|
59
75
|
licenses:
|
|
60
76
|
- MIT
|
|
@@ -66,7 +82,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
66
82
|
requirements:
|
|
67
83
|
- - ">="
|
|
68
84
|
- !ruby/object:Gem::Version
|
|
69
|
-
version: 3.
|
|
85
|
+
version: 3.2.0
|
|
70
86
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
71
87
|
requirements:
|
|
72
88
|
- - ">="
|