beagle-turso 0.3.0 → 0.4.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 +44 -7
- data/ext/beagle_turso/Cargo.toml +1 -1
- data/ext/beagle_turso/src/errors.rs +3 -0
- data/ext/beagle_turso/src/lib.rs +75 -23
- data/ext/beagle_turso/src/nogvl.rs +43 -8
- data/lib/beagle/turso/version.rb +1 -1
- data/lib/beagle/turso.rb +27 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5e39443726eb8f83cd09daee0845abd245e7cde71e4ccde2517129f3340dd80d
|
|
4
|
+
data.tar.gz: bf31133bd580a883078fdb07ec997b364965c03136d573d311bb7af2e21204a2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3b1cd258ea7a8945cad36c7a18011bd3477bfc43d7b861d71f6105c23fb7f8dbde2ba1d180cb3cbdf2582ec345555117411f8efa72c6fc3acd522953ea7807e7
|
|
7
|
+
data.tar.gz: 0272a225ec262995350a0a9e5d8dff4aba8f2d49bf0ecf911363919fe4f64e7d5d1956a28c550bd32e5a339309ba30148ce533df3722b30e96c9aa21ccad76d3
|
data/README.md
CHANGED
|
@@ -125,8 +125,26 @@ still a `RuntimeError`. A replica file already held by another process raises
|
|
|
125
125
|
- One `Database` may be shared by threads. Give each thread its own
|
|
126
126
|
`Connection`; a shared `Connection` works but serializes its callers.
|
|
127
127
|
- `open`, `connect`, `push` and `pull` release the GVL, so other threads keep
|
|
128
|
-
running while they wait on the network.
|
|
129
|
-
|
|
128
|
+
running while they wait on the network.
|
|
129
|
+
- Statements (`execute`, `query`, `query_result`, `query_result_with_types`,
|
|
130
|
+
`execute_batch`) also release the GVL, so a long query or bulk import doesn't
|
|
131
|
+
stop other threads. Statements on one database file still run one at a
|
|
132
|
+
time, as they did under the GVL. That covers every connection of every
|
|
133
|
+
`Database` the process opened on that file, such as one per connection in
|
|
134
|
+
an ActiveRecord pool. A statement waits for the one in flight, never for a
|
|
135
|
+
whole transaction or import. Reads wait too: a long `SELECT` holds up every
|
|
136
|
+
other statement on that file until it finishes. The engine can't run them
|
|
137
|
+
side by side safely, so put long reports on a dedicated thread or worker.
|
|
138
|
+
- A statement never waits inside the engine for a busy write lock: it raises
|
|
139
|
+
`BusyError` at once, and `PRAGMA busy_timeout` has no effect. A wait there
|
|
140
|
+
would block the transaction holding the lock from committing. Retry
|
|
141
|
+
`BusyError` in your own code, as the Sequel adapter does.
|
|
142
|
+
- To keep statements under the GVL (the 0.3.0 behaviour), set
|
|
143
|
+
`BEAGLE_TURSO_STATEMENT_GVL=hold` (read once, when the gem loads; any case)
|
|
144
|
+
or call `Beagle::Turso.statement_gvl = :hold`; `:release` is the default.
|
|
145
|
+
Any other value of the variable is ignored, with a warning.
|
|
146
|
+
`Beagle::Turso.statement_gvl` returns the current mode. The setting is
|
|
147
|
+
process-wide.
|
|
130
148
|
- While one thread pushes, `closed?` returns immediately on other threads, and
|
|
131
149
|
queries on other connections proceed.
|
|
132
150
|
- On a synced database, `connect` called while another thread is pushing waits
|
|
@@ -136,14 +154,19 @@ still a `RuntimeError`. A replica file already held by another process raises
|
|
|
136
154
|
request.
|
|
137
155
|
- `close` during an in-flight `push` is safe: the push finishes or raises, and
|
|
138
156
|
later calls raise `ClosedError`.
|
|
139
|
-
- A
|
|
140
|
-
|
|
157
|
+
- A statement still waiting for another on the same file stops at once for
|
|
158
|
+
`Timeout`, `Thread#raise` or `Thread#kill`; it has not run. Once a statement
|
|
159
|
+
has started, it cannot be interrupted, and neither can `open`, `connect`,
|
|
160
|
+
`push` or `pull`: `Timeout` and `Thread#kill` take effect when the call
|
|
161
|
+
returns.
|
|
141
162
|
- **Falcon / Async:** on Ruby 4.0+ with `async` 2.25+ (enable its worker pool
|
|
142
163
|
with `ASYNC_SCHEDULER_WORKER_POOL=true` or
|
|
143
164
|
`Async::Scheduler.new(worker_pool: true)`), a fiber scheduler runs these
|
|
144
|
-
calls on a worker thread, so the reactor keeps serving other
|
|
145
|
-
a sync
|
|
146
|
-
|
|
165
|
+
calls and statements on a worker thread, so the reactor keeps serving other
|
|
166
|
+
requests during a sync or a long statement. A fiber's query waits (off the
|
|
167
|
+
reactor) for any statement in flight on the same file. On Ruby 3.3/3.4
|
|
168
|
+
the GVL is still released, but nothing is offloaded: the scheduler's own
|
|
169
|
+
thread blocks. Run sync and long imports from a dedicated thread there. Run one Falcon
|
|
147
170
|
process per replica (`--count 1`): the replica file is locked to one
|
|
148
171
|
process.
|
|
149
172
|
- **Fork:** handles are bound to the process that opened them. In a forked
|
|
@@ -218,6 +241,20 @@ db.close
|
|
|
218
241
|
(each worker gets its own) and call `stop` from `on_worker_shutdown` or
|
|
219
242
|
`at_exit`.
|
|
220
243
|
|
|
244
|
+
## Known engine issue: self-referencing foreign keys
|
|
245
|
+
|
|
246
|
+
Turso's engine has open bugs with foreign keys that reference their own table
|
|
247
|
+
(such as `parent_id`): a multi-row `DELETE`, `UPDATE` or `DROP` that SQLite accepts can fail with
|
|
248
|
+
`FOREIGN KEY constraint failed`, and `ON DELETE SET NULL` / `CASCADE` can leave
|
|
249
|
+
rows behind or orphaned
|
|
250
|
+
([tursodatabase/turso#8713](https://github.com/tursodatabase/turso/issues/8713),
|
|
251
|
+
[#9279](https://github.com/tursodatabase/turso/issues/9279),
|
|
252
|
+
[#9293](https://github.com/tursodatabase/turso/issues/9293),
|
|
253
|
+
[#9294](https://github.com/tursodatabase/turso/issues/9294)). Foreign keys
|
|
254
|
+
between two different tables are unaffected. Workaround: avoid `ON DELETE`
|
|
255
|
+
actions on self-referencing foreign keys, and delete children before parents,
|
|
256
|
+
one level at a time.
|
|
257
|
+
|
|
221
258
|
## Credential safety
|
|
222
259
|
|
|
223
260
|
`auth_token` is never included in `inspect` output or in error messages —
|
data/ext/beagle_turso/Cargo.toml
CHANGED
|
@@ -23,6 +23,9 @@ pub fn raise_core(ruby: &Ruby, e: CoreError) -> Error {
|
|
|
23
23
|
CoreError::Busy(_) => "BusyError",
|
|
24
24
|
CoreError::Closed => "ClosedError",
|
|
25
25
|
CoreError::Forked { .. } => "ForkedError",
|
|
26
|
+
// Never raised: run_statement retries an interrupted wait, or Ruby
|
|
27
|
+
// raises its pending interrupt instead. The base class, to be safe.
|
|
28
|
+
CoreError::Interrupted => "Error",
|
|
26
29
|
// Value conversion problems stay Ruby's TypeError, as in 0.1.x.
|
|
27
30
|
CoreError::Type(_) => return Error::new(ruby.exception_type_error(), e.to_string()),
|
|
28
31
|
};
|
data/ext/beagle_turso/src/lib.rs
CHANGED
|
@@ -14,22 +14,71 @@
|
|
|
14
14
|
// identical; the code is simpler and compiles with zero deprecation
|
|
15
15
|
// warnings.
|
|
16
16
|
|
|
17
|
-
// GVL: open_local, open, connect, push and pull
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
17
|
+
// GVL: open_local, open, connect, push and pull, and the five statement
|
|
18
|
+
// methods (execute, query, query_result, query_result_with_types,
|
|
19
|
+
// execute_batch) run via nogvl::without_gvl with the GVL released, so other
|
|
20
|
+
// threads run and a worker-pool fiber scheduler offloads them off its reactor.
|
|
21
|
+
// Statement params are converted to core values before the call and Ruby rows
|
|
22
|
+
// built after it; the closure touches no Ruby object. The core serialises
|
|
23
|
+
// statements per database file (its statement lock, shared by every Database
|
|
24
|
+
// in the process opened on that file), so releasing the GVL adds no new write
|
|
25
|
+
// overlap. `Beagle::Turso.statement_gvl = :hold` (or
|
|
26
|
+
// BEAGLE_TURSO_STATEMENT_GVL=hold at load) runs statements under the GVL
|
|
27
|
+
// instead, as in 0.3.0. last_insert_rowid, close and closed? stay under the
|
|
28
|
+
// GVL: they are cheap.
|
|
21
29
|
|
|
22
30
|
mod errors;
|
|
23
31
|
mod nogvl;
|
|
24
32
|
|
|
25
|
-
use
|
|
33
|
+
use std::sync::atomic::{AtomicBool, Ordering};
|
|
34
|
+
|
|
35
|
+
use beagle_turso_core::{Connection, Database, Error as CoreError, OpenOptions, Value};
|
|
26
36
|
use errors::raise_core;
|
|
27
|
-
use nogvl::without_gvl;
|
|
37
|
+
use nogvl::{without_gvl, without_gvl_interruptible};
|
|
28
38
|
use magnus::{
|
|
29
39
|
encoding::EncodingCapable, function, method, prelude::*, Error, Float, Integer, IntoValue,
|
|
30
40
|
RArray, RString, Ruby,
|
|
31
41
|
};
|
|
32
42
|
|
|
43
|
+
// Process-wide: true runs statements with the GVL held (`statement_gvl :hold`).
|
|
44
|
+
static HOLD_STATEMENT_GVL: AtomicBool = AtomicBool::new(false);
|
|
45
|
+
|
|
46
|
+
fn set_statement_gvl_hold(hold: bool) {
|
|
47
|
+
HOLD_STATEMENT_GVL.store(hold, Ordering::Relaxed);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
fn statement_gvl_hold() -> bool {
|
|
51
|
+
HOLD_STATEMENT_GVL.load(Ordering::Relaxed)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Never set: the interrupt flag for statements run under the GVL.
|
|
55
|
+
static NEVER_INTERRUPTED: AtomicBool = AtomicBool::new(false);
|
|
56
|
+
|
|
57
|
+
// Runs one statement: without the GVL by default, or directly under it when
|
|
58
|
+
// the switch is `:hold`. `f` must touch no Ruby object; it returns the core
|
|
59
|
+
// Result, which the caller maps to a Ruby exception once the GVL is back.
|
|
60
|
+
//
|
|
61
|
+
// Without the GVL, `f` gets the flag Ruby's unblocking function sets, so
|
|
62
|
+
// Thread#raise/kill and Timeout stop a statement still waiting for another on
|
|
63
|
+
// the same file (the core returns Interrupted; one already running finishes).
|
|
64
|
+
// Ruby then raises its pending exception from without_gvl_interruptible. If it
|
|
65
|
+
// has none to raise (a trap handler ran, say), the statement waits again.
|
|
66
|
+
fn run_statement<F, R>(mut f: F) -> Result<Result<R, CoreError>, Error>
|
|
67
|
+
where
|
|
68
|
+
F: FnMut(&AtomicBool) -> Result<R, CoreError> + Send,
|
|
69
|
+
R: Send,
|
|
70
|
+
{
|
|
71
|
+
if statement_gvl_hold() {
|
|
72
|
+
return Ok(f(&NEVER_INTERRUPTED));
|
|
73
|
+
}
|
|
74
|
+
loop {
|
|
75
|
+
match without_gvl_interruptible(|flag| f(flag))? {
|
|
76
|
+
Err(CoreError::Interrupted) => continue,
|
|
77
|
+
done => return Ok(done),
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
33
82
|
#[magnus::wrap(class = "Beagle::Turso::Database", free_immediately)]
|
|
34
83
|
struct RbDatabase {
|
|
35
84
|
inner: Database,
|
|
@@ -166,17 +215,15 @@ fn rows_to_ruby(ruby: &Ruby, rows: Vec<beagle_turso_core::Row>) -> Result<RArray
|
|
|
166
215
|
impl RbConnection {
|
|
167
216
|
fn execute(ruby: &Ruby, rb_self: &Self, sql: String, params: RArray) -> Result<u64, Error> {
|
|
168
217
|
let p = ruby_array_to_params(ruby, params)?;
|
|
169
|
-
rb_self
|
|
170
|
-
|
|
171
|
-
.execute(&sql, &p)
|
|
218
|
+
let inner = &rb_self.inner;
|
|
219
|
+
run_statement(|i| inner.execute_interruptible(&sql, &p, i))?
|
|
172
220
|
.map_err(|e| raise_core(ruby, e))
|
|
173
221
|
}
|
|
174
222
|
|
|
175
223
|
fn query(ruby: &Ruby, rb_self: &Self, sql: String, params: RArray) -> Result<RArray, Error> {
|
|
176
224
|
let p = ruby_array_to_params(ruby, params)?;
|
|
177
|
-
let
|
|
178
|
-
|
|
179
|
-
.query(&sql, &p)
|
|
225
|
+
let inner = &rb_self.inner;
|
|
226
|
+
let rows = run_statement(|i| inner.query_result_interruptible(&sql, &p, i).map(|r| r.rows))?
|
|
180
227
|
.map_err(|e| raise_core(ruby, e))?;
|
|
181
228
|
rows_to_ruby(ruby, rows)
|
|
182
229
|
}
|
|
@@ -192,10 +239,10 @@ impl RbConnection {
|
|
|
192
239
|
params: RArray,
|
|
193
240
|
) -> Result<RArray, Error> {
|
|
194
241
|
let p = ruby_array_to_params(ruby, params)?;
|
|
195
|
-
let
|
|
196
|
-
|
|
197
|
-
.
|
|
198
|
-
|
|
242
|
+
let inner = &rb_self.inner;
|
|
243
|
+
let result =
|
|
244
|
+
run_statement(|i| inner.query_result_interruptible(&sql, &p, i))?
|
|
245
|
+
.map_err(|e| raise_core(ruby, e))?;
|
|
199
246
|
let columns = ruby.ary_new();
|
|
200
247
|
for name in &result.columns {
|
|
201
248
|
columns.push(name.clone().into_value_with(ruby))?;
|
|
@@ -217,10 +264,10 @@ impl RbConnection {
|
|
|
217
264
|
params: RArray,
|
|
218
265
|
) -> Result<RArray, Error> {
|
|
219
266
|
let p = ruby_array_to_params(ruby, params)?;
|
|
220
|
-
let
|
|
221
|
-
|
|
222
|
-
.
|
|
223
|
-
|
|
267
|
+
let inner = &rb_self.inner;
|
|
268
|
+
let result =
|
|
269
|
+
run_statement(|i| inner.query_result_interruptible(&sql, &p, i))?
|
|
270
|
+
.map_err(|e| raise_core(ruby, e))?;
|
|
224
271
|
let columns = ruby.ary_new();
|
|
225
272
|
for name in result.columns {
|
|
226
273
|
columns.push(name)?;
|
|
@@ -244,9 +291,8 @@ impl RbConnection {
|
|
|
244
291
|
}
|
|
245
292
|
|
|
246
293
|
fn execute_batch(ruby: &Ruby, rb_self: &Self, sql: String) -> Result<(), Error> {
|
|
247
|
-
rb_self
|
|
248
|
-
|
|
249
|
-
.execute_batch(&sql)
|
|
294
|
+
let inner = &rb_self.inner;
|
|
295
|
+
run_statement(|i| inner.execute_batch_interruptible(&sql, i))?
|
|
250
296
|
.map_err(|e| raise_core(ruby, e))
|
|
251
297
|
}
|
|
252
298
|
|
|
@@ -265,6 +311,12 @@ impl RbConnection {
|
|
|
265
311
|
#[magnus::init]
|
|
266
312
|
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
267
313
|
let turso = ruby.define_module("Beagle")?.define_module("Turso")?;
|
|
314
|
+
// Back Beagle::Turso.statement_gvl / statement_gvl= (lib/beagle/turso.rb).
|
|
315
|
+
turso.define_module_function(
|
|
316
|
+
"_set_statement_gvl_hold",
|
|
317
|
+
function!(set_statement_gvl_hold, 1),
|
|
318
|
+
)?;
|
|
319
|
+
turso.define_module_function("_statement_gvl_hold?", function!(statement_gvl_hold, 0))?;
|
|
268
320
|
let db = turso.define_class("Database", ruby.class_object())?;
|
|
269
321
|
db.define_singleton_method("open_local", function!(RbDatabase::open_local, 1))?;
|
|
270
322
|
// Positional primitive; the public keyword-arg `Database.open` wrapper
|
|
@@ -3,16 +3,20 @@
|
|
|
3
3
|
// move the call to a worker thread (keeping its reactor live).
|
|
4
4
|
//
|
|
5
5
|
// The closure must not touch any Ruby object: convert arguments before calling
|
|
6
|
-
// and build Ruby results after it returns.
|
|
7
|
-
// so Thread#raise/kill (and Timeout) are delivered only once the
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
6
|
+
// and build Ruby results after it returns. `without_gvl` passes no unblocking
|
|
7
|
+
// function, so Thread#raise/kill (and Timeout) are delivered only once the
|
|
8
|
+
// call returns. `without_gvl_interruptible` passes one that sets a flag the
|
|
9
|
+
// closure can watch, so it can stop *waiting* early; work it has begun still
|
|
10
|
+
// runs to the end. Either way, rb_nogvl then raises the pending interrupt by
|
|
11
|
+
// longjmp. That is caught with rb_protect (magnus::rb_sys::protect), so it
|
|
12
|
+
// never unwinds through Rust frames; any value the native call produced is
|
|
13
|
+
// dropped normally (for `open`, the Database is closed) and the exception
|
|
14
|
+
// comes back as `Err`.
|
|
12
15
|
|
|
13
16
|
use std::ffi::c_void;
|
|
14
17
|
use std::panic::{self, AssertUnwindSafe};
|
|
15
18
|
use std::ptr;
|
|
19
|
+
use std::sync::atomic::{AtomicBool, Ordering};
|
|
16
20
|
|
|
17
21
|
use magnus::Error;
|
|
18
22
|
|
|
@@ -36,19 +40,50 @@ where
|
|
|
36
40
|
ptr::null_mut()
|
|
37
41
|
}
|
|
38
42
|
|
|
43
|
+
// The unblocking function: Ruby calls it, holding the GVL, from the thread that
|
|
44
|
+
// wants to interrupt this one. It only sets the flag.
|
|
45
|
+
unsafe extern "C" fn set_flag(data: *mut c_void) {
|
|
46
|
+
(*(data as *const AtomicBool)).store(true, Ordering::Release);
|
|
47
|
+
}
|
|
48
|
+
|
|
39
49
|
pub fn without_gvl<F, R>(f: F) -> Result<R, Error>
|
|
50
|
+
where
|
|
51
|
+
F: FnOnce() -> R + Send,
|
|
52
|
+
R: Send,
|
|
53
|
+
{
|
|
54
|
+
call_without_gvl(f, None)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
pub fn without_gvl_interruptible<F, R>(f: F) -> Result<R, Error>
|
|
58
|
+
where
|
|
59
|
+
F: FnOnce(&AtomicBool) -> R + Send,
|
|
60
|
+
R: Send,
|
|
61
|
+
{
|
|
62
|
+
let flag = AtomicBool::new(false);
|
|
63
|
+
let flag = &flag;
|
|
64
|
+
call_without_gvl(move || f(flag), Some(flag))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
fn call_without_gvl<F, R>(f: F, interrupt: Option<&AtomicBool>) -> Result<R, Error>
|
|
40
68
|
where
|
|
41
69
|
F: FnOnce() -> R + Send,
|
|
42
70
|
R: Send,
|
|
43
71
|
{
|
|
44
72
|
let mut call = Call { f: Some(f), out: None };
|
|
73
|
+
let (ubf, ubf_data) = match interrupt {
|
|
74
|
+
Some(flag) => (
|
|
75
|
+
Some(set_flag as unsafe extern "C" fn(*mut c_void)),
|
|
76
|
+
flag as *const AtomicBool as *mut c_void,
|
|
77
|
+
),
|
|
78
|
+
None => (None, ptr::null_mut()),
|
|
79
|
+
};
|
|
45
80
|
let protected = magnus::rb_sys::protect(|| {
|
|
46
81
|
unsafe {
|
|
47
82
|
rb_sys::rb_nogvl(
|
|
48
83
|
Some(trampoline::<F, R>),
|
|
49
84
|
&mut call as *mut Call<F, R> as *mut c_void,
|
|
50
|
-
|
|
51
|
-
|
|
85
|
+
ubf,
|
|
86
|
+
ubf_data,
|
|
52
87
|
RB_NOGVL_OFFLOAD_SAFE,
|
|
53
88
|
);
|
|
54
89
|
}
|
data/lib/beagle/turso/version.rb
CHANGED
data/lib/beagle/turso.rb
CHANGED
|
@@ -15,6 +15,33 @@ end
|
|
|
15
15
|
|
|
16
16
|
module Beagle
|
|
17
17
|
module Turso
|
|
18
|
+
# Whether statements (Connection#execute, #query, #query_result,
|
|
19
|
+
# #query_result_with_types, #execute_batch) release the GVL: :release (the
|
|
20
|
+
# default) lets other threads run and a worker-pool fiber scheduler offload
|
|
21
|
+
# them; :hold keeps them under the GVL, as in 0.3.0. Process-wide.
|
|
22
|
+
def self.statement_gvl
|
|
23
|
+
_statement_gvl_hold? ? :hold : :release
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.statement_gvl=(mode)
|
|
27
|
+
unless mode == :hold || mode == :release
|
|
28
|
+
raise ArgumentError, "statement_gvl must be :hold or :release"
|
|
29
|
+
end
|
|
30
|
+
_set_statement_gvl_hold(mode == :hold)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private_class_method :_statement_gvl_hold?, :_set_statement_gvl_hold
|
|
34
|
+
|
|
35
|
+
# Case and surrounding space don't matter. Anything else is most likely a
|
|
36
|
+
# typo in a rollback attempt, so it is reported rather than ignored.
|
|
37
|
+
case (env_gvl = ENV["BEAGLE_TURSO_STATEMENT_GVL"].to_s).strip.downcase
|
|
38
|
+
when "hold" then self.statement_gvl = :hold
|
|
39
|
+
when "", "release" then nil
|
|
40
|
+
else
|
|
41
|
+
warn "beagle-turso: ignoring BEAGLE_TURSO_STATEMENT_GVL=#{env_gvl.inspect} " \
|
|
42
|
+
"(expected hold or release); statements release the GVL"
|
|
43
|
+
end
|
|
44
|
+
|
|
18
45
|
class Database
|
|
19
46
|
# Opens a local-only database, or one synced with a remote Turso
|
|
20
47
|
# database when both `remote_url` and `auth_token` are given (see
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: beagle-turso
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- BeagleSoftwareUK
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-18 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rb_sys
|