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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0c8e05a11274a315a434eb615873a9402e8afd76ff296b8efc030d9bd4d212d9
4
- data.tar.gz: 0fa05a0646df999aba45e14fc31a5e2066ad471964d7501fa3994df9882cbca8
3
+ metadata.gz: 69e6be67a21fd809792bf867f81f6205bb06a6cc457aa05c2b80080cf272fd9b
4
+ data.tar.gz: d1a18f2e763cb6b5781521f1e8bd12f8a7159d3350c2fe5e91fe02ee00a50f1f
5
5
  SHA512:
6
- metadata.gz: ee153e58a81fb89d2a1578cbbd4841a5720cac7c942916c1c256c234e3c0726f6bc0a2fb500cbcd7733a357efb43b1da37e9a8774a7d2eecfd98561cf9cdda76
7
- data.tar.gz: 9d5ec6478407aa7c168c29dfbd64b6dc0661e84fbbb1167041ef8b2615fb5ffab899474787c2ceb946430f2aa92bac8684adaf6ffe2b6cf6188a953eb9ced36b
6
+ metadata.gz: defa80378418aabb5b0b619392e180102f633ce7f8f6a13d6778f34a47c47f14c2739dc5248aaf0011e8f40788002ae42fea373789e3fc8b5e6e1ea5ebac8b35
7
+ data.tar.gz: 573a7f4f59c98621b9c4f95af1daa2a8fe133b3e487ef83c7d73af51c8c65655cbff080ef2d3813d7e64868273b186f121d69653499bee67f1180463fc946f9a
data/README.md CHANGED
@@ -1,23 +1,23 @@
1
- # activerecord-turso
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 (local bindings at `~/Projects/turso/bindings/ruby` during development)
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", path: "~/Projects/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 Tantivy functions:
97
+ Query with the adapter helpers:
98
98
 
99
- ```sql
100
- SELECT * FROM posts WHERE fts_match(title, body, 'database');
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 pooling caveats
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 disabled
183
+ ### 4. Prepared statement cache is enabled with a bounded pool
152
184
 
153
- The adapter returns `false` for `default_prepared_statements` and uses a no-op statement pool. Each query is prepared and finalized individually. This is slower than the upstream SQLite3 adapter for high-volume repeated queries, but avoids correctness issues until the Turso bindings expose stable prepared-statement reuse.
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
- To run against the official Rails Active Record test suite, see the CI workflow in `.github/workflows/test.yml` and the project design doc in `docs/superpowers/specs/`.
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
- def new_client(config)
13
- db_config = config.symbolize_keys.merge(:timeout => config[:timeout] || 5000)
14
- ::Turso::AR::Connection.new(db_config)
15
- end
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
- @raw_connection && !@raw_connection.closed?
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
- if @config[:timeout] || @config[:busy_timeout]
61
- timeout = @config[:busy_timeout] || @config[:timeout]
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
- if @config[:query_timeout]
66
- @raw_connection.query_timeout = @config[:query_timeout]
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 write_query?(sql)
89
- raw_connection.execute(sql, type_casted_binds)
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
- result = raw_connection.query(sql, type_casted_binds)
97
- columns = result.column_names
98
- rows = result.map(&:values)
99
- affected_rows = raw_connection.changes
100
- verified!
101
- notification_payload[:affected_rows] = affected_rows
102
- notification_payload[:row_count] = rows.length
103
- ActiveRecord::Result.new(columns, rows, nil, affected_rows: affected_rows)
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
- raw_execute("BEGIN CONCURRENT", "TRANSACTION")
58
- yield
59
- raw_execute("COMMIT", "TRANSACTION")
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
- raw_execute("ROLLBACK", "TRANSACTION") rescue nil
70
+ rollback_if_active
63
71
  raise unless concurrent_conflict?(e) && retries < max_retries
64
72
 
65
73
  retries += 1
66
- sleep(base_delay_ms * retries / 1000.0)
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.is_a?(::Turso::BusySnapshotError) ||
73
- exception.cause.is_a?(::Turso::BusyError) ||
74
- /snapshot conflict|busy snapshot|database is locked/i.match?(exception.message)
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
- Dir[File.expand_path("turso_adapter/*.rb", __dir__)].each { |f| require f }
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 default_prepared_statements
45
- false
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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordTurso
4
+ VERSION = "0.2.0"
5
+ end
@@ -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
- @db = ::Turso::DB.new(config[:database].to_s,
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.split(";").each do |stmt|
46
- s = stmt.strip
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.instance_variable_get(:@database).connection.prepare(sql)
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.1.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.0.0
85
+ version: 3.2.0
70
86
  required_rubygems_version: !ruby/object:Gem::Requirement
71
87
  requirements:
72
88
  - - ">="