activerecord-beagle-turso 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/README.md +231 -0
- data/config/recurring.yml.example +22 -0
- data/lib/active_record/connection_adapters/beagle_turso/sync_job.rb +35 -0
- data/lib/active_record/connection_adapters/beagle_turso/sync_manager.rb +101 -0
- data/lib/active_record/connection_adapters/beagle_turso_adapter.rb +276 -0
- data/lib/activerecord/beagle_turso/version.rb +7 -0
- data/lib/activerecord-beagle-turso.rb +6 -0
- metadata +125 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 63f87bcbfe56cbc279209bb29456d62bd757f1d4ea32d69ffc8c922891121c4e
|
|
4
|
+
data.tar.gz: 94028b900a3aa33ac4bbfb5e975101dad3289016f601021085aa15445fe22d6a
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: a407e3b271141a740dcc6c5996165ce17dd3ab4aedb0605a769ef4586e65af19825001958e42c60a4e544e5af3c4528af013312936f214dd00d36b9c9ee5b12d
|
|
7
|
+
data.tar.gz: d22efba814f090b6a114aa72e29e3e882c3acb87d542279f72eab16bf2ec7f48a6c381c38b4cb9d3e8a0b87bd3d79c0d187cefe0de9b604c92b1c1b55dba1624
|
data/README.md
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# activerecord-beagle-turso
|
|
2
|
+
|
|
3
|
+
An ActiveRecord connection adapter for [Turso](https://turso.tech)'s libsql
|
|
4
|
+
engine, backed by the [`beagle-turso`](../beagle-turso) driver. It subclasses
|
|
5
|
+
Rails' stock `SQLite3Adapter` and reroutes only the raw-execution seam onto
|
|
6
|
+
`beagle-turso`, so SQL generation, quoting, schema introspection, and
|
|
7
|
+
transaction handling are all inherited unchanged — you get a normal
|
|
8
|
+
ActiveRecord experience, running against a local, in-memory, or
|
|
9
|
+
remote-synced Turso database.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
Add both gems to your `Gemfile` (`activerecord-beagle-turso` depends on
|
|
14
|
+
`beagle-turso`, but Bundler needs it listed explicitly if you're installing
|
|
15
|
+
from a path/git source rather than RubyGems):
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
gem "activerecord-beagle-turso"
|
|
19
|
+
gem "beagle-turso"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
or install directly:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
gem install activerecord-beagle-turso
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`beagle-turso` has a native (Rust) extension, so installing it requires a
|
|
29
|
+
Rust toolchain unless a precompiled binary gem is available for your
|
|
30
|
+
platform — see [`beagle-turso`'s README](../beagle-turso/README.md#install)
|
|
31
|
+
for details. **There is currently no precompiled/cross-compiled gem release
|
|
32
|
+
job for either gem** — installing means building the extension locally
|
|
33
|
+
(or, as in this repo, from a path dependency built once already).
|
|
34
|
+
|
|
35
|
+
## `config/database.yml`
|
|
36
|
+
|
|
37
|
+
Set `adapter: beagle_turso`. A **local** connection (no sync) needs only
|
|
38
|
+
`database:`:
|
|
39
|
+
|
|
40
|
+
```yaml
|
|
41
|
+
development:
|
|
42
|
+
adapter: beagle_turso
|
|
43
|
+
database: storage/development.sqlite3
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
A **synced** connection additionally sets `remote_url:` and `auth_token:` —
|
|
47
|
+
both are required together to enable sync (see
|
|
48
|
+
[`Beagle::Turso::Database.open`](../beagle-turso/README.md#synced-example)):
|
|
49
|
+
|
|
50
|
+
```yaml
|
|
51
|
+
production:
|
|
52
|
+
adapter: beagle_turso
|
|
53
|
+
database: storage/production.sqlite3
|
|
54
|
+
remote_url: <%= Rails.application.credentials.dig(:turso, :prod_url) %>
|
|
55
|
+
auth_token: <%= Rails.application.credentials.dig(:turso, :prod_token) %>
|
|
56
|
+
# Informational only (see "Recurring sync" below) -- not read by this
|
|
57
|
+
# adapter or by SyncManager. Keep it in sync with recurring.yml by hand.
|
|
58
|
+
sync_interval: 30 # seconds
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Never log `remote_url`/`auth_token` — Rails masks `password`-ish keys in
|
|
62
|
+
some diagnostics, but these are custom `database.yml` keys, so that masking
|
|
63
|
+
does not cover them automatically. Source them from encrypted credentials or
|
|
64
|
+
`ENV`, as above, and avoid logging/inspecting the raw connection config.
|
|
65
|
+
|
|
66
|
+
An optional `bootstrap_if_empty:` (default `true`) controls whether a brand
|
|
67
|
+
new/empty local file is populated from the remote on first open — see
|
|
68
|
+
`Beagle::Turso::Database.open`.
|
|
69
|
+
|
|
70
|
+
### Durability note: writes commit on-sync, not synchronously
|
|
71
|
+
|
|
72
|
+
Writes go to the **local** file immediately, as part of the normal
|
|
73
|
+
ActiveRecord write path (`INSERT`/`UPDATE`/`DELETE` all commit locally,
|
|
74
|
+
synchronously, like any SQLite-backed adapter). They are only propagated to
|
|
75
|
+
the remote Turso database **on sync** — i.e. whenever something calls
|
|
76
|
+
`push!` (see below) — not automatically after every write or transaction
|
|
77
|
+
commit. Until a sync succeeds, a write that is fully durable on this
|
|
78
|
+
replica's local disk is not yet visible to any other replica of the same
|
|
79
|
+
remote database. Plan your sync cadence (and any read-after-write
|
|
80
|
+
expectations across replicas) around that gap.
|
|
81
|
+
|
|
82
|
+
## Sync: `Beagle::Turso::SyncManager` + Solid Queue recurring job
|
|
83
|
+
|
|
84
|
+
`Beagle::Turso::SyncManager` drives push/pull from the ActiveRecord layer,
|
|
85
|
+
given a connection using `adapter: beagle_turso`:
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
Beagle::Turso::SyncManager.push!(ActiveRecord::Base.connection) # local writes -> remote
|
|
89
|
+
Beagle::Turso::SyncManager.pull!(ActiveRecord::Base.connection) # remote writes -> local
|
|
90
|
+
Beagle::Turso::SyncManager.sync!(ActiveRecord::Base.connection) # push!, then pull!
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
All three default their `connection` argument to `ActiveRecord::Base.connection`,
|
|
94
|
+
so `Beagle::Turso::SyncManager.sync!` alone is the usual call. Calling any of
|
|
95
|
+
them against a **local-only** connection (no `remote_url:`/`auth_token:` in
|
|
96
|
+
`database.yml`) raises `Beagle::Turso::SyncManager::NotSyncedError` — a
|
|
97
|
+
distinct, rescuable type — rather than the driver's bare `RuntimeError`.
|
|
98
|
+
|
|
99
|
+
To sync on a timer, wire
|
|
100
|
+
`ActiveRecord::ConnectionAdapters::BeagleTurso::SyncJob` (a thin delegator to
|
|
101
|
+
`SyncManager.sync!`) into [Solid Queue's recurring
|
|
102
|
+
jobs](https://github.com/rails/solid_queue?tab=readme-ov-file#recurring-tasks).
|
|
103
|
+
This gem does not generate or load `config/recurring.yml` itself — copy the
|
|
104
|
+
relevant block from
|
|
105
|
+
[`config/recurring.yml.example`](config/recurring.yml.example) into your
|
|
106
|
+
host app's own `config/recurring.yml`, for whichever environment(s) actually
|
|
107
|
+
use a synced connection:
|
|
108
|
+
|
|
109
|
+
```yaml
|
|
110
|
+
production:
|
|
111
|
+
beagle_turso_sync:
|
|
112
|
+
class: ActiveRecord::ConnectionAdapters::BeagleTurso::SyncJob
|
|
113
|
+
queue: default
|
|
114
|
+
schedule: every 30 seconds # match database.yml's sync_interval: for this env
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`SyncJob` subclasses `ActiveJob::Base` when ActiveJob is loaded (the normal
|
|
118
|
+
case in a Rails app running Solid Queue) and falls back to a plain class
|
|
119
|
+
with the same `#perform` otherwise, so requiring this gem never raises a
|
|
120
|
+
`LoadError` on its own in a non-Rails context.
|
|
121
|
+
|
|
122
|
+
A recurring `SyncJob` against a local-only connection will fail every run
|
|
123
|
+
with `NotSyncedError` — only add the recurring entry for environments whose
|
|
124
|
+
`database.yml` connection actually carries `remote_url:`/`auth_token:`.
|
|
125
|
+
|
|
126
|
+
## Test coverage
|
|
127
|
+
|
|
128
|
+
This gem's own spec suite (`spec/`, run via `bundle exec rspec`) is 35
|
|
129
|
+
examples covering, against the real `beagle-turso` driver (nothing mocked):
|
|
130
|
+
CRUD through a model, foreign-key enforcement, data-modifying CTEs,
|
|
131
|
+
transactions (commit/rollback/nested `requires_new`), DDL
|
|
132
|
+
(`add_column`/`remove_column`/`change_column`/`add_index`), every AR column
|
|
133
|
+
type round-tripping through a model, `pluck`/`where`/`update_all`/`delete_all`,
|
|
134
|
+
and (with real Turso credentials — otherwise a clean `PENDING` skip, no
|
|
135
|
+
silent pass) a genuine push/pull sync round trip between two replicas.
|
|
136
|
+
|
|
137
|
+
### Real-Rails-boot integration test
|
|
138
|
+
|
|
139
|
+
`spec/dummy_app_integration_spec.rb` boots `test/dummy` — a small but
|
|
140
|
+
**genuine** `Rails::Application` (not a mock or a stand-in) — through Rails'
|
|
141
|
+
own `config/environment.rb` → `Rails.application.initialize!` sequence, the
|
|
142
|
+
same path a real app's `bin/rails server`/`bin/rails console`/`bin/rails db:migrate`
|
|
143
|
+
takes. It proves the adapter survives that boot rather than only the
|
|
144
|
+
hand-called `ActiveRecord::Base.establish_connection(adapter: "beagle_turso", ...)`
|
|
145
|
+
the rest of the suite uses:
|
|
146
|
+
|
|
147
|
+
- `adapter: beagle_turso` is resolved from a real `config/database.yml` by
|
|
148
|
+
`ActiveRecord::Railtie`'s own `active_record.initialize_database`
|
|
149
|
+
initializer, not passed as a literal hash.
|
|
150
|
+
- A real `ActiveRecord::Migration` file is run via `ActiveRecord::MigrationContext`
|
|
151
|
+
(not `ActiveRecord::Schema.define`, which the rest of the suite uses) —
|
|
152
|
+
this exercises `schema_migrations`/`ar_internal_metadata` bookkeeping,
|
|
153
|
+
which no other spec in this gem touches.
|
|
154
|
+
- The model under test (`DummyWidget`) is never `require`d by hand — it's
|
|
155
|
+
resolved through Rails' normal Zeitwerk `app/models` autoloading.
|
|
156
|
+
|
|
157
|
+
`test/dummy` intentionally boots only the frameworks
|
|
158
|
+
`ActiveRecord::Railtie` itself pulls in (it hard-requires
|
|
159
|
+
`action_controller/railtie`, and transitively `action_view/railtie`, for
|
|
160
|
+
long-standing middleware-configuration reasons — see the comment at the top
|
|
161
|
+
of Rails' `active_record/railtie.rb`) — no Active Job, Action Mailer, Active
|
|
162
|
+
Storage, Action Cable, etc. See `test/dummy/config/application.rb` for the
|
|
163
|
+
exact boot list.
|
|
164
|
+
|
|
165
|
+
### What's deferred: ActiveRecord's own internal adapter test suite
|
|
166
|
+
|
|
167
|
+
ActiveRecord ships an extensive internal adapter conformance suite
|
|
168
|
+
(`activerecord/test/cases/*_test.rb` in the [Rails source
|
|
169
|
+
tree](https://github.com/rails/rails/tree/v8.1.3.1/activerecord/test/cases))
|
|
170
|
+
that official adapters (`mysql2`, `pg`, `trilogy`) are exercised against.
|
|
171
|
+
**This gem does not vendor or run that suite, and that is a deliberate,
|
|
172
|
+
documented decision, not a silent gap:**
|
|
173
|
+
|
|
174
|
+
- That suite is not shipped in the released `activerecord` gem — only in
|
|
175
|
+
the Rails monorepo's source tree. It is not designed for consumption by
|
|
176
|
+
external adapter gems.
|
|
177
|
+
- We confirmed this concretely: even a single, relatively adapter-agnostic
|
|
178
|
+
file from it (`test/cases/adapter_test.rb`) `require`s `cases/helper`,
|
|
179
|
+
`support/connection_helper`, and several `models/*` fixture classes,
|
|
180
|
+
which in turn depend on ActiveRecord's internal `ARTest` test harness and
|
|
181
|
+
its ~1,500-line `test/schema/schema.rb` (dozens of interlocking fixture
|
|
182
|
+
tables — authors, books, posts, comments, and more) plus a `test/config.yml`
|
|
183
|
+
whose format/location has itself shifted between Rails versions. None of
|
|
184
|
+
this is a stable, public interface; vendoring even one file means
|
|
185
|
+
vendoring most of that infrastructure and keeping it hand-synced with
|
|
186
|
+
whatever Rails version this gem targets.
|
|
187
|
+
- This is precisely why official third-party adapters handle it by
|
|
188
|
+
checking out the **entire** Rails source at a pinned commit/tag (often as
|
|
189
|
+
a CI-only step, sometimes a git submodule) and running AR's suite
|
|
190
|
+
in-tree against their adapter — a substantial, ongoing-maintenance
|
|
191
|
+
commitment, not something addable as a "focused subset" inside a single
|
|
192
|
+
gem release.
|
|
193
|
+
- What that suite covers beyond this gem's own 35 examples: exhaustive
|
|
194
|
+
per-type edge cases (encoding, precision/scale boundaries, reserved-word
|
|
195
|
+
quoting across dozens of identifiers), schema-dumping round-trips
|
|
196
|
+
(`db/schema.rb` load-and-compare), the full fixture-based association
|
|
197
|
+
graph, and adapter-independent behavior that has nothing to do with the
|
|
198
|
+
raw-execution seam this adapter actually changes. None of that is
|
|
199
|
+
exercised here.
|
|
200
|
+
|
|
201
|
+
If this adapter needs that level of assurance in the future (e.g. before a
|
|
202
|
+
1.0 release, or before proposing it for inclusion upstream), the right next
|
|
203
|
+
step is a separately-scoped effort to vendor the full Rails source at a
|
|
204
|
+
pinned tag and run its adapter suite in CI — not a partial port bolted onto
|
|
205
|
+
this gem.
|
|
206
|
+
|
|
207
|
+
## Limitations (v1)
|
|
208
|
+
|
|
209
|
+
1. **Uncast raw/computed SELECT values.** Reads go through `ActiveRecord::Result`
|
|
210
|
+
without result column types, so ordinary model attributes and `pluck(:column)`
|
|
211
|
+
cast correctly, but a raw/computed SELECT expression with no backing attribute
|
|
212
|
+
(e.g. `select_all("SELECT some_datetime_expr ...")`, `pluck(Arel.sql("..."))`)
|
|
213
|
+
may come back uncast (string / 0/1 instead of Time / boolean). Cast such values
|
|
214
|
+
yourself.
|
|
215
|
+
|
|
216
|
+
2. **Data-modifying CTEs and read-replica write-guards.** A hand-written
|
|
217
|
+
`WITH ... UPDATE/INSERT/DELETE` executes correctly, but is classified as a read
|
|
218
|
+
by ActiveRecord's write-guard layer — so under `connected_to(role: :reading)` it
|
|
219
|
+
won't trip the read-only guard, and it won't mark the transaction dirty. Use
|
|
220
|
+
plain `UPDATE`/`DELETE` (or `Model.update_all`/`delete_all`) if you rely on
|
|
221
|
+
read/write splitting.
|
|
222
|
+
|
|
223
|
+
3. **Sync holds the GVL.** `SyncManager.push!`/`pull!` block Ruby's GVL for the
|
|
224
|
+
full network round-trip. Under `SOLID_QUEUE_IN_PUMA=true` (jobs in the Puma
|
|
225
|
+
process), a recurring `SyncJob` stalls all Puma threads for the duration of
|
|
226
|
+
each sync — pick a sync cadence accordingly, or run Solid Queue in a separate
|
|
227
|
+
process.
|
|
228
|
+
|
|
229
|
+
## License
|
|
230
|
+
|
|
231
|
+
[MIT](https://opensource.org/licenses/MIT).
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Example Solid Queue recurring-job entry for ActiveRecord::ConnectionAdapters::BeagleTurso::SyncJob.
|
|
2
|
+
#
|
|
3
|
+
# This is NOT loaded by this gem -- copy the relevant environment block(s)
|
|
4
|
+
# into your host Rails app's own config/recurring.yml (Solid Queue reads
|
|
5
|
+
# that file; see the solid_queue README for the full recurring.yml format).
|
|
6
|
+
# Only add this for environments whose database.yml connection is actually
|
|
7
|
+
# synced (has both remote_url: and auth_token: -- see
|
|
8
|
+
# lib/active_record/connection_adapters/beagle_turso/sync_manager.rb). A
|
|
9
|
+
# recurring SyncJob against a local-only connection will fail every run with
|
|
10
|
+
# Beagle::Turso::SyncManager::NotSyncedError.
|
|
11
|
+
#
|
|
12
|
+
# Keep the `schedule:` cadence here in sync (by hand) with whatever
|
|
13
|
+
# `sync_interval:` you documented in database.yml for the same environment --
|
|
14
|
+
# Solid Queue does not read database.yml, so there is no single source of
|
|
15
|
+
# truth to enforce this automatically.
|
|
16
|
+
|
|
17
|
+
production:
|
|
18
|
+
beagle_turso_sync:
|
|
19
|
+
class: ActiveRecord::ConnectionAdapters::BeagleTurso::SyncJob
|
|
20
|
+
queue: default
|
|
21
|
+
# Match database.yml's production `sync_interval:` for this connection.
|
|
22
|
+
schedule: every 30 seconds
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_record/connection_adapters/beagle_turso/sync_manager"
|
|
4
|
+
|
|
5
|
+
module ActiveRecord
|
|
6
|
+
module ConnectionAdapters
|
|
7
|
+
module BeagleTurso
|
|
8
|
+
# Thin Solid Queue recurring job: on each run, it just delegates to
|
|
9
|
+
# Beagle::Turso::SyncManager.sync! (push local writes, then pull remote
|
|
10
|
+
# ones) against the app's default AR connection. No retry/backoff logic
|
|
11
|
+
# here -- Solid Queue's own job-failure handling covers that, and
|
|
12
|
+
# SyncManager itself carries no state between runs.
|
|
13
|
+
#
|
|
14
|
+
# Wire it up via config/recurring.yml (see
|
|
15
|
+
# config/recurring.yml.example alongside this file), scheduled at
|
|
16
|
+
# whatever cadence database.yml's +sync_interval:+ documents for the
|
|
17
|
+
# target environment. This gem does not read recurring.yml or generate
|
|
18
|
+
# it -- add the entry to the host app's own file.
|
|
19
|
+
#
|
|
20
|
+
# This gem does not declare a hard dependency on `activejob` (a Rails
|
|
21
|
+
# app using Solid Queue already has it loaded by the time this file is
|
|
22
|
+
# required). When ActiveJob is present, SyncJob subclasses
|
|
23
|
+
# ActiveJob::Base as Solid Queue expects; otherwise it falls back to a
|
|
24
|
+
# plain class with the same #perform, so requiring this file never
|
|
25
|
+
# raises a LoadError on its own.
|
|
26
|
+
sync_job_superclass = defined?(::ActiveJob::Base) ? ::ActiveJob::Base : Object
|
|
27
|
+
|
|
28
|
+
class SyncJob < sync_job_superclass
|
|
29
|
+
def perform
|
|
30
|
+
::Beagle::Turso::SyncManager.sync!
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Beagle
|
|
4
|
+
module Turso
|
|
5
|
+
# Drives a synced beagle-turso database's `push`/`pull` from the
|
|
6
|
+
# ActiveRecord layer, given an AR connection using +adapter: beagle_turso+
|
|
7
|
+
# (see BeagleTursoAdapter). It is a thin delegator: all the real sync work
|
|
8
|
+
# -- WAL-frame push/pull against the remote Turso database -- happens in
|
|
9
|
+
# Beagle::Turso::Database#push/#pull (native, via beagle_turso_core). This
|
|
10
|
+
# module only reaches +connection.raw_connection+ (the adapter's +Client+,
|
|
11
|
+
# see BeagleTursoAdapter::Client) and calls through.
|
|
12
|
+
#
|
|
13
|
+
# Intended as the callback a Solid Queue recurring job invokes on a timer
|
|
14
|
+
# (see ActiveRecord::ConnectionAdapters::BeagleTurso::SyncJob) -- not a
|
|
15
|
+
# sync engine in its own right. It does not schedule itself, retry, queue
|
|
16
|
+
# writes, or track sync state; scheduling is Solid Queue's job (see
|
|
17
|
+
# config/recurring.yml.example alongside this file).
|
|
18
|
+
#
|
|
19
|
+
# == database.yml keys this depends on
|
|
20
|
+
#
|
|
21
|
+
# A connection is "synced" -- i.e. what +push!+/+pull!+ operate on --
|
|
22
|
+
# only when its +database.yml+ entry carries BOTH of:
|
|
23
|
+
#
|
|
24
|
+
# remote_url: # the Turso database's libsql:// URL
|
|
25
|
+
# auth_token: # its auth token
|
|
26
|
+
#
|
|
27
|
+
# (See BeagleTursoAdapter.new_client.) A third key, +sync_interval:+, is
|
|
28
|
+
# NOT read by this adapter or by SyncManager -- it exists purely as a
|
|
29
|
+
# documented convention for how often a host app's config/recurring.yml
|
|
30
|
+
# should invoke SyncJob; keep the two in sync by hand (or via ERB) since
|
|
31
|
+
# Solid Queue's recurring.yml is not sourced from database.yml. Example:
|
|
32
|
+
#
|
|
33
|
+
# # config/database.yml
|
|
34
|
+
# production:
|
|
35
|
+
# adapter: beagle_turso
|
|
36
|
+
# database: storage/production.sqlite3
|
|
37
|
+
# remote_url: <%= Rails.application.credentials.dig(:turso, :prod_url) %>
|
|
38
|
+
# auth_token: <%= Rails.application.credentials.dig(:turso, :prod_token) %>
|
|
39
|
+
# sync_interval: 30 # seconds -- informational; wire it into recurring.yml yourself
|
|
40
|
+
#
|
|
41
|
+
# Never log +remote_url+/+auth_token+ -- Rails masks +password+-ish keys
|
|
42
|
+
# in some diagnostics, but these are custom keys, so it doesn't cover them
|
|
43
|
+
# automatically. Source them from encrypted credentials or ENV (as
|
|
44
|
+
# BeagleTursoAdapter's callers already do) and don't pass them through
|
|
45
|
+
# anything that inspects/logs config (e.g. don't log +connection_config+).
|
|
46
|
+
module SyncManager
|
|
47
|
+
# Raised by +push!+/+pull!+/+sync!+ when the given connection is
|
|
48
|
+
# local-only (its database.yml entry has no +remote_url+/+auth_token+,
|
|
49
|
+
# so BeagleTursoAdapter opened it via +Database.open_local+). The
|
|
50
|
+
# underlying Client#push/#pull already raise a bare RuntimeError for
|
|
51
|
+
# this (see Beagle::Turso::Database#push/#pull); SyncManager re-raises
|
|
52
|
+
# it as this distinct, documented type so callers can rescue it
|
|
53
|
+
# specifically instead of matching on the driver's message text. A
|
|
54
|
+
# genuine sync failure against a real remote (network error, auth
|
|
55
|
+
# failure, etc.) is NOT reclassified -- it propagates as whatever error
|
|
56
|
+
# the driver raised, unchanged.
|
|
57
|
+
class NotSyncedError < StandardError; end
|
|
58
|
+
|
|
59
|
+
# Matches only the driver's local-only wording (see
|
|
60
|
+
# beagle_turso_core's `Error::Sync` in `Database::push`/`Database::pull`)
|
|
61
|
+
# so a real sync failure -- which also surfaces as a RuntimeError, just
|
|
62
|
+
# with different text -- is never misreported as "not synced".
|
|
63
|
+
LOCAL_ONLY_MESSAGE_REGEX = /on a local-only database/i
|
|
64
|
+
private_constant :LOCAL_ONLY_MESSAGE_REGEX
|
|
65
|
+
|
|
66
|
+
module_function
|
|
67
|
+
|
|
68
|
+
# Push this connection's local writes to its remote Turso database.
|
|
69
|
+
def push!(connection = ActiveRecord::Base.connection)
|
|
70
|
+
reraise_local_only { connection.raw_connection.push }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Pull remote writes down into this connection's local replica.
|
|
74
|
+
# Returns the driver's own true/false (whether anything changed).
|
|
75
|
+
def pull!(connection = ActiveRecord::Base.connection)
|
|
76
|
+
reraise_local_only { connection.raw_connection.pull }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Push, then pull. Pushing first means this replica's own writes reach
|
|
80
|
+
# the remote before it re-syncs down, so afterwards this connection is
|
|
81
|
+
# caught up with both its own writes (now durable remotely) and
|
|
82
|
+
# whatever else landed on the remote from other replicas. Call push!
|
|
83
|
+
# or pull! directly when only one direction is wanted.
|
|
84
|
+
def sync!(connection = ActiveRecord::Base.connection)
|
|
85
|
+
push!(connection)
|
|
86
|
+
pull!(connection)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def reraise_local_only
|
|
90
|
+
yield
|
|
91
|
+
rescue RuntimeError => e
|
|
92
|
+
raise e unless LOCAL_ONLY_MESSAGE_REGEX.match?(e.message)
|
|
93
|
+
|
|
94
|
+
raise NotSyncedError,
|
|
95
|
+
"#{e.message} -- this connection is local-only; configure remote_url: " \
|
|
96
|
+
"and auth_token: in database.yml to enable sync"
|
|
97
|
+
end
|
|
98
|
+
private_class_method :reraise_local_only
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_record"
|
|
4
|
+
require "active_record/connection_adapters/sqlite3_adapter"
|
|
5
|
+
require "beagle/turso"
|
|
6
|
+
|
|
7
|
+
module ActiveRecord
|
|
8
|
+
module ConnectionAdapters
|
|
9
|
+
# = ActiveRecord Beagle Turso Adapter
|
|
10
|
+
#
|
|
11
|
+
# Subclasses the stock SQLite3Adapter and reroutes only its raw-execution
|
|
12
|
+
# seam (+perform_query+) onto a beagle-turso Connection. beagle-turso is a
|
|
13
|
+
# libsql/Turso driver: it speaks a SQLite-compatible dialect but has its own
|
|
14
|
+
# native handle rather than the +sqlite3+ gem's +::SQLite3::Database+.
|
|
15
|
+
#
|
|
16
|
+
# Everything ActiveRecord relies on above the raw connection -- SQL
|
|
17
|
+
# generation, quoting, schema introspection (PRAGMA table_xinfo,
|
|
18
|
+
# sqlite_master), type mapping, transactions -- is inherited unchanged from
|
|
19
|
+
# SQLite3Adapter. We swap out:
|
|
20
|
+
#
|
|
21
|
+
# * +new_client+ / +connect+ : open a Beagle::Turso::Database instead of a
|
|
22
|
+
# ::SQLite3::Database, wrapped in a Client that answers the handful of
|
|
23
|
+
# lifecycle/pragma messages the SQLite3Adapter sends a raw connection.
|
|
24
|
+
# * +configure_connection+ : re-assert `PRAGMA foreign_keys = ON` through
|
|
25
|
+
# the real exec path, because the stock DEFAULT_PRAGMA setter is a no-op on
|
|
26
|
+
# our Client (SQLite defaults FKs OFF per-connection).
|
|
27
|
+
# * +perform_query+ : the single method every query flows through
|
|
28
|
+
# (see AbstractAdapter#raw_execute). Reads/RETURNING -> ActiveRecord::Result
|
|
29
|
+
# with columns+rows; writes (incl. data-modifying CTEs) -> affected-row
|
|
30
|
+
# count + last_insert_rowid.
|
|
31
|
+
# * +last_inserted_id+ : fall back to last_insert_rowid when a plain
|
|
32
|
+
# INSERT (no RETURNING) was used.
|
|
33
|
+
class BeagleTursoAdapter < SQLite3Adapter
|
|
34
|
+
ADAPTER_NAME = "BeagleTurso"
|
|
35
|
+
|
|
36
|
+
# A RETURNING clause makes any write yield rows, so it routes to the read
|
|
37
|
+
# (query_result) branch.
|
|
38
|
+
RETURNING_REGEX = /\bRETURNING\b/i
|
|
39
|
+
private_constant :RETURNING_REGEX
|
|
40
|
+
|
|
41
|
+
# A data-modifying CTE: a leading WITH whose *main* statement -- the token
|
|
42
|
+
# right after the CTE definition list's closing paren -- is INSERT / UPDATE
|
|
43
|
+
# / DELETE. AR's write_query? classifies any leading WITH as a READ, so
|
|
44
|
+
# without this such writes would be routed to query_result: the change still
|
|
45
|
+
# persists, but the affected-row count comes back nil and a CTE-INSERT's
|
|
46
|
+
# rowid is lost. The `\)\s*` anchor is deliberate -- it keeps `WITH ... SELECT`
|
|
47
|
+
# reads (and DML keywords that appear inside string literals or identifiers)
|
|
48
|
+
# from being misclassified as writes.
|
|
49
|
+
#
|
|
50
|
+
# Residual limitation: this is a heuristic, not a full SQL parse. A
|
|
51
|
+
# data-modifying CTE whose main verb is not immediately preceded by the CTE
|
|
52
|
+
# list's closing `)` (extremely unusual) could be missed, and a read CTE
|
|
53
|
+
# containing the literal text `) UPDATE`/`) INSERT`/`) DELETE` could be
|
|
54
|
+
# mis-routed. The read branch also re-captures last_insert_rowid whenever the
|
|
55
|
+
# SQL mentions INSERT, so an inserted id is never silently lost even on a miss.
|
|
56
|
+
DATA_MODIFYING_CTE_REGEX = /\A\s*WITH\b[\s\S]*\)\s*(?:INSERT|UPDATE|DELETE)\b/i
|
|
57
|
+
private_constant :DATA_MODIFYING_CTE_REGEX
|
|
58
|
+
|
|
59
|
+
INSERT_REGEX = /\bINSERT\b/i
|
|
60
|
+
private_constant :INSERT_REGEX
|
|
61
|
+
|
|
62
|
+
class << self
|
|
63
|
+
# Open a beagle-turso Database (synced remote when +remote_url+ is
|
|
64
|
+
# present, otherwise local/in-memory) and wrap it so the SQLite3Adapter
|
|
65
|
+
# can drive it as a raw connection.
|
|
66
|
+
def new_client(config)
|
|
67
|
+
database =
|
|
68
|
+
if config[:remote_url]
|
|
69
|
+
Beagle::Turso::Database.open(
|
|
70
|
+
local_path: config[:database].to_s,
|
|
71
|
+
remote_url: config[:remote_url],
|
|
72
|
+
auth_token: config[:auth_token],
|
|
73
|
+
bootstrap_if_empty: config.fetch(:bootstrap_if_empty, true)
|
|
74
|
+
)
|
|
75
|
+
else
|
|
76
|
+
Beagle::Turso::Database.open_local(config[:database].to_s)
|
|
77
|
+
end
|
|
78
|
+
Client.new(database)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Adapts a Beagle::Turso::Database + its connected Connection to the subset
|
|
83
|
+
# of the +sqlite3+ gem's Database surface that SQLite3Adapter pokes at.
|
|
84
|
+
#
|
|
85
|
+
# The execution methods (+query_result+, +execute+, +execute_batch+,
|
|
86
|
+
# +last_insert_rowid+) are the real work. The rest are shims: the adapter's
|
|
87
|
+
# +configure_connection+ sets PRAGMAs via +raw_connection.foo = value+ and
|
|
88
|
+
# calls a couple of sqlite3-specific lifecycle methods that libsql neither
|
|
89
|
+
# needs nor supports; we accept and no-op those so connect/configure runs
|
|
90
|
+
# cleanly.
|
|
91
|
+
class Client
|
|
92
|
+
attr_reader :database, :connection
|
|
93
|
+
|
|
94
|
+
def initialize(database)
|
|
95
|
+
@database = database
|
|
96
|
+
@connection = database.connect
|
|
97
|
+
@closed = false
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# A bare read of exactly +defer_foreign_keys+ or +read_uncommitted+
|
|
101
|
+
# (no `=`) -- how SQLite3Adapter's DDL and transaction-isolation code
|
|
102
|
+
# reads their state back (e.g. +disable_referential_integrity+'s
|
|
103
|
+
# `query_value("PRAGMA defer_foreign_keys")`, used by every
|
|
104
|
+
# alter_table-based schema change: add_column/remove_column/
|
|
105
|
+
# change_column/etc). libsql/turso does not track queryable state for
|
|
106
|
+
# either of these two specific pragmas -- reading them back yields an
|
|
107
|
+
# empty result set instead of a value, even right after being set.
|
|
108
|
+
# An empty result makes `query_value` return nil, which the caller
|
|
109
|
+
# then interpolates into a follow-up statement
|
|
110
|
+
# (`"PRAGMA defer_foreign_keys = #{nil}"`) -> invalid SQL ("incomplete
|
|
111
|
+
# input"). Both default to OFF/0 per SQLite's docs, and this driver
|
|
112
|
+
# has no way to report a truer answer, so synthesize that default --
|
|
113
|
+
# exactly analogous to the FK-enforcement bridge in
|
|
114
|
+
# +configure_connection+.
|
|
115
|
+
#
|
|
116
|
+
# Deliberately NOT a generic `PRAGMA \w+` allowlist: `PRAGMA
|
|
117
|
+
# foreign_key_check` is also a bare, argument-less pragma read (via
|
|
118
|
+
# SQLite3Adapter#check_all_foreign_keys_valid!), but for it an EMPTY
|
|
119
|
+
# result is the *correct*, meaningful answer ("no FK violations") --
|
|
120
|
+
# synthesizing a fake row for it would make check_all_foreign_keys_valid!
|
|
121
|
+
# (called by ActiveRecord::FixtureSet when
|
|
122
|
+
# verify_foreign_keys_for_fixtures is on -- the Rails 7.1+ app-generator
|
|
123
|
+
# default) raise a false-positive "Foreign key violations found:" on
|
|
124
|
+
# perfectly clean data. Scoping the match to only the two pragmas
|
|
125
|
+
# actually root-caused here means any other bare pragma (including
|
|
126
|
+
# foreign_key_check) falls through to the real driver result instead
|
|
127
|
+
# of a synthesized one -- failing loudly on a genuine future gap
|
|
128
|
+
# rather than silently lying.
|
|
129
|
+
BARE_PRAGMA_READ_REGEX = /\A\s*PRAGMA\s+(defer_foreign_keys|read_uncommitted)\s*\z/i
|
|
130
|
+
private_constant :BARE_PRAGMA_READ_REGEX
|
|
131
|
+
|
|
132
|
+
# --- execution surface used by BeagleTursoAdapter#perform_query ---
|
|
133
|
+
def query_result(sql, params)
|
|
134
|
+
columns, rows = connection.query_result(sql, params)
|
|
135
|
+
if columns.empty? && rows.empty? && (match = BARE_PRAGMA_READ_REGEX.match(sql))
|
|
136
|
+
[[match[1]], [[0]]]
|
|
137
|
+
else
|
|
138
|
+
[columns, rows]
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def execute(sql, params) = connection.execute(sql, params)
|
|
143
|
+
def execute_batch(sql) = connection.execute_batch(sql)
|
|
144
|
+
def last_insert_rowid = connection.last_insert_rowid
|
|
145
|
+
|
|
146
|
+
# --- sync passthrough (Database-level) ---
|
|
147
|
+
def push = database.push
|
|
148
|
+
def pull = database.pull
|
|
149
|
+
|
|
150
|
+
# --- sqlite3-gem lifecycle shims the SQLite3Adapter drives ---
|
|
151
|
+
# configure_connection only touches this when config[:timeout] is set.
|
|
152
|
+
def busy_handler_timeout=(_)
|
|
153
|
+
nil
|
|
154
|
+
end
|
|
155
|
+
# connected? => !(@raw_connection.nil? || @raw_connection.closed?)
|
|
156
|
+
def closed? = @closed
|
|
157
|
+
# Release the beagle-turso session eagerly rather than waiting for GC.
|
|
158
|
+
# A *synced* Database holds a server-side sync session open; without an
|
|
159
|
+
# explicit close each ActiveRecord disconnect/reconnect (e.g.
|
|
160
|
+
# verify!/db:prepare) leaked one, accumulating on the remote until it
|
|
161
|
+
# reported "database is busy". Close the connection first, then the
|
|
162
|
+
# database (which ends the sync session). Idempotent.
|
|
163
|
+
#
|
|
164
|
+
# @closed is only flipped true AFTER both closes have run: if
|
|
165
|
+
# @connection.close raised and we had set @closed up front, the ensure'd
|
|
166
|
+
# @database.close would be skipped yet closed? would already report true
|
|
167
|
+
# -- the session would leak silently (SQLite3Adapter#disconnect! swallows
|
|
168
|
+
# the exception). The ensure guarantees the database (session) close runs
|
|
169
|
+
# even if the connection close raises, and @closed reflects reality.
|
|
170
|
+
def close
|
|
171
|
+
return if @closed
|
|
172
|
+
begin
|
|
173
|
+
@connection.close
|
|
174
|
+
ensure
|
|
175
|
+
@database.close
|
|
176
|
+
@closed = true
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
# SQLite3Adapter#reconnect calls this when reusing a live connection.
|
|
180
|
+
def rollback = execute("ROLLBACK", [])
|
|
181
|
+
|
|
182
|
+
# configure_connection applies DEFAULT_PRAGMAS via `raw_connection.foo = v`.
|
|
183
|
+
# libsql manages journalling/locking itself, so accept and ignore them.
|
|
184
|
+
def method_missing(name, *args)
|
|
185
|
+
name.to_s.end_with?("=") ? nil : super
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def respond_to_missing?(name, include_private = false)
|
|
189
|
+
name.to_s.end_with?("=") || super
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
private
|
|
194
|
+
# The single seam. AbstractAdapter#raw_execute funnels every statement
|
|
195
|
+
# here and expects an ActiveRecord::Result back, with the notification
|
|
196
|
+
# payload's affected_rows/row_count filled in. cast_result (inherited)
|
|
197
|
+
# returns the Result untouched and affected_rows (inherited) reads
|
|
198
|
+
# Result#affected_rows, so building the Result correctly is all we need.
|
|
199
|
+
def perform_query(raw_connection, sql, binds, type_casted_binds, prepare:, notification_payload:, batch: false)
|
|
200
|
+
params = type_casted_binds || []
|
|
201
|
+
|
|
202
|
+
result =
|
|
203
|
+
if batch
|
|
204
|
+
raw_connection.execute_batch(sql)
|
|
205
|
+
::ActiveRecord::Result.empty
|
|
206
|
+
elsif write_statement?(sql)
|
|
207
|
+
affected = raw_connection.execute(sql, params)
|
|
208
|
+
@last_inserted_rowid = raw_connection.last_insert_rowid
|
|
209
|
+
::ActiveRecord::Result.empty(affected_rows: affected)
|
|
210
|
+
else
|
|
211
|
+
columns, rows = raw_connection.query_result(sql, params)
|
|
212
|
+
# Belt-and-suspenders: a RETURNING insert lands here (id read from
|
|
213
|
+
# rows by last_inserted_id), and a data-modifying CTE that slips past
|
|
214
|
+
# write_statement? would too -- keep the rowid so an inserted id is
|
|
215
|
+
# never silently lost even on a heuristic miss.
|
|
216
|
+
@last_inserted_rowid = raw_connection.last_insert_rowid if INSERT_REGEX.match?(sql)
|
|
217
|
+
::ActiveRecord::Result.new(columns, rows)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
verified!
|
|
221
|
+
notification_payload[:affected_rows] = result.affected_rows
|
|
222
|
+
notification_payload[:row_count] = result.length
|
|
223
|
+
result
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Route to the WRITE branch (execute -> affected count + last_insert_rowid)
|
|
227
|
+
# rather than the read branch. A statement is a write when SQLite3's own
|
|
228
|
+
# read/write classifier says so, OR when it is a data-modifying CTE that
|
|
229
|
+
# the classifier misreads as a read (see DATA_MODIFYING_CTE_REGEX). A
|
|
230
|
+
# RETURNING clause always yields rows, so it goes to the read branch even
|
|
231
|
+
# though it mutates. write_query? is a pure predicate here, so calling it a
|
|
232
|
+
# second time is side-effect free.
|
|
233
|
+
def write_statement?(sql)
|
|
234
|
+
return false if RETURNING_REGEX.match?(sql)
|
|
235
|
+
write_query?(sql) || data_modifying_cte?(sql)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def data_modifying_cte?(sql)
|
|
239
|
+
DATA_MODIFYING_CTE_REGEX.match?(sql)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# For INSERT ... RETURNING "id" the id arrives in the result rows (super).
|
|
243
|
+
# For a plain INSERT (RETURNING unsupported) the result is empty, so fall
|
|
244
|
+
# back to the rowid captured in perform_query's write branch.
|
|
245
|
+
def last_inserted_id(result)
|
|
246
|
+
super || @last_inserted_rowid
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Open the beagle-turso connection. Mirrors SQLite3Adapter#connect but
|
|
250
|
+
# without its ::SQLite3-specific ConnectionNotEstablished rescue.
|
|
251
|
+
def connect
|
|
252
|
+
@raw_connection = self.class.new_client(@connection_parameters)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# SQLite disables foreign keys per-connection by default; stock
|
|
256
|
+
# SQLite3Adapter turns them on via `raw_connection.foreign_keys = true`
|
|
257
|
+
# (a DEFAULT_PRAGMA setter). On our Client that setter is a swallowed
|
|
258
|
+
# no-op, so FK enforcement would silently be OFF -- a regression vs the
|
|
259
|
+
# stock adapter. Re-assert it here through the real exec path.
|
|
260
|
+
#
|
|
261
|
+
# `super` still runs (check_version + the other DEFAULT_PRAGMA setters);
|
|
262
|
+
# journal_mode/synchronous/mmap_size are moot for in-memory and remote
|
|
263
|
+
# libsql, so leaving those as no-ops is fine.
|
|
264
|
+
def configure_connection
|
|
265
|
+
super
|
|
266
|
+
@raw_connection.execute("PRAGMA foreign_keys = ON", [])
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
ActiveRecord::ConnectionAdapters.register(
|
|
273
|
+
"beagle_turso",
|
|
274
|
+
"ActiveRecord::ConnectionAdapters::BeagleTursoAdapter",
|
|
275
|
+
"active_record/connection_adapters/beagle_turso_adapter"
|
|
276
|
+
)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "activerecord/beagle_turso/version"
|
|
4
|
+
require "active_record/connection_adapters/beagle_turso_adapter"
|
|
5
|
+
require "active_record/connection_adapters/beagle_turso/sync_manager"
|
|
6
|
+
require "active_record/connection_adapters/beagle_turso/sync_job"
|
metadata
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: activerecord-beagle-turso
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- BeagleSoftwareUK
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-08-08 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: activerecord
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '8.1'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '8.1'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: beagle-turso
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '0.1'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '0.1'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: sqlite3
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '2.1'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '2.1'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: rspec
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '3.13'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '3.13'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: railties
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '8.1'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '8.1'
|
|
83
|
+
description: |
|
|
84
|
+
An ActiveRecord connection adapter for Turso's libsql engine. It subclasses
|
|
85
|
+
the stock SQLite3Adapter and reroutes only the raw-execution seam onto the
|
|
86
|
+
beagle-turso native driver, so schema migrations and model CRUD run against
|
|
87
|
+
a local, in-memory, or synced remote Turso database while inheriting
|
|
88
|
+
SQLite3Adapter's SQL generation, quoting, and schema introspection.
|
|
89
|
+
email:
|
|
90
|
+
executables: []
|
|
91
|
+
extensions: []
|
|
92
|
+
extra_rdoc_files: []
|
|
93
|
+
files:
|
|
94
|
+
- README.md
|
|
95
|
+
- config/recurring.yml.example
|
|
96
|
+
- lib/active_record/connection_adapters/beagle_turso/sync_job.rb
|
|
97
|
+
- lib/active_record/connection_adapters/beagle_turso/sync_manager.rb
|
|
98
|
+
- lib/active_record/connection_adapters/beagle_turso_adapter.rb
|
|
99
|
+
- lib/activerecord-beagle-turso.rb
|
|
100
|
+
- lib/activerecord/beagle_turso/version.rb
|
|
101
|
+
homepage: https://github.com/BeagleSoftwareUK/beagle-turso
|
|
102
|
+
licenses:
|
|
103
|
+
- MIT
|
|
104
|
+
metadata:
|
|
105
|
+
source_code_uri: https://github.com/BeagleSoftwareUK/beagle-turso
|
|
106
|
+
post_install_message:
|
|
107
|
+
rdoc_options: []
|
|
108
|
+
require_paths:
|
|
109
|
+
- lib
|
|
110
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
111
|
+
requirements:
|
|
112
|
+
- - ">="
|
|
113
|
+
- !ruby/object:Gem::Version
|
|
114
|
+
version: '3.3'
|
|
115
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
116
|
+
requirements:
|
|
117
|
+
- - ">="
|
|
118
|
+
- !ruby/object:Gem::Version
|
|
119
|
+
version: '0'
|
|
120
|
+
requirements: []
|
|
121
|
+
rubygems_version: 3.5.22
|
|
122
|
+
signing_key:
|
|
123
|
+
specification_version: 4
|
|
124
|
+
summary: ActiveRecord adapter for Turso, backed by the beagle-turso driver.
|
|
125
|
+
test_files: []
|