activerecord-libsql 0.1.1 → 0.1.3
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/activerecord-libsql.gemspec +5 -7
- data/lib/active_record/connection_adapters/libsql_adapter.rb +32 -8
- data/lib/activerecord/libsql/version.rb +1 -1
- data/lib/turso_libsql/connection.rb +192 -0
- data/lib/turso_libsql/database.rb +153 -0
- data/lib/turso_libsql.rb +12 -0
- metadata +13 -15
- data/Cargo.lock +0 -2406
- data/Cargo.toml +0 -3
- data/ext/turso_libsql/Cargo.toml +0 -20
- data/ext/turso_libsql/extconf.rb +0 -6
- data/ext/turso_libsql/src/lib.rs +0 -299
data/Cargo.toml
DELETED
data/ext/turso_libsql/Cargo.toml
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
[package]
|
|
2
|
-
name = "turso_libsql"
|
|
3
|
-
version = "0.1.0"
|
|
4
|
-
edition = "2021"
|
|
5
|
-
|
|
6
|
-
[lib]
|
|
7
|
-
name = "turso_libsql"
|
|
8
|
-
crate-type = ["cdylib"]
|
|
9
|
-
|
|
10
|
-
[dependencies]
|
|
11
|
-
magnus = { version = "0.8", features = [] }
|
|
12
|
-
libsql = { version = "0.9.23", features = ["encryption", "sync"] }
|
|
13
|
-
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] }
|
|
14
|
-
once_cell = "1"
|
|
15
|
-
|
|
16
|
-
[profile.release]
|
|
17
|
-
opt-level = 3
|
|
18
|
-
lto = true
|
|
19
|
-
codegen-units = 1
|
|
20
|
-
panic = "abort"
|
data/ext/turso_libsql/extconf.rb
DELETED
data/ext/turso_libsql/src/lib.rs
DELETED
|
@@ -1,299 +0,0 @@
|
|
|
1
|
-
use magnus::{function, method, prelude::*, Error, Ruby};
|
|
2
|
-
use once_cell::sync::OnceCell;
|
|
3
|
-
use std::sync::Arc;
|
|
4
|
-
use std::time::Duration;
|
|
5
|
-
use tokio::runtime::Runtime;
|
|
6
|
-
|
|
7
|
-
// グローバル Tokio ランタイム(libsql は async API のため必要)
|
|
8
|
-
static RUNTIME: OnceCell<Runtime> = OnceCell::new();
|
|
9
|
-
|
|
10
|
-
fn runtime() -> &'static Runtime {
|
|
11
|
-
RUNTIME.get_or_init(|| {
|
|
12
|
-
tokio::runtime::Builder::new_multi_thread()
|
|
13
|
-
.enable_all()
|
|
14
|
-
.build()
|
|
15
|
-
.expect("Failed to create Tokio runtime")
|
|
16
|
-
})
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/// async ブロック内から Ruby の RuntimeError を生成するヘルパー
|
|
20
|
-
fn mk_err(msg: impl std::fmt::Display) -> Error {
|
|
21
|
-
let ruby = Ruby::get().expect("called outside Ruby thread");
|
|
22
|
-
Error::new(ruby.exception_runtime_error(), msg.to_string())
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// -----------------------------------------------------------------------
|
|
26
|
-
// TursoDatabase — Database を保持するラッパー(sync のために必要)
|
|
27
|
-
// -----------------------------------------------------------------------
|
|
28
|
-
|
|
29
|
-
#[magnus::wrap(class = "TursoLibsql::Database", free_immediately, size)]
|
|
30
|
-
struct TursoDatabase {
|
|
31
|
-
inner: Arc<libsql::Database>,
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
impl TursoDatabase {
|
|
35
|
-
/// リモート接続用 Database を作成(既存の remote モード)
|
|
36
|
-
fn new_remote(url: String, token: String) -> Result<Self, Error> {
|
|
37
|
-
let db = runtime().block_on(async {
|
|
38
|
-
libsql::Builder::new_remote(url, token)
|
|
39
|
-
.build()
|
|
40
|
-
.await
|
|
41
|
-
.map_err(mk_err)
|
|
42
|
-
})?;
|
|
43
|
-
Ok(Self { inner: Arc::new(db) })
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/// Embedded Replica 用 Database を作成
|
|
47
|
-
/// path: ローカル DB ファイルパス
|
|
48
|
-
/// url: Turso リモート URL (libsql://...)
|
|
49
|
-
/// token: 認証トークン
|
|
50
|
-
/// sync_interval_secs: バックグラウンド自動同期間隔(秒)。0 なら手動のみ
|
|
51
|
-
fn new_remote_replica(
|
|
52
|
-
path: String,
|
|
53
|
-
url: String,
|
|
54
|
-
token: String,
|
|
55
|
-
sync_interval_secs: u64,
|
|
56
|
-
) -> Result<Self, Error> {
|
|
57
|
-
let db = runtime().block_on(async {
|
|
58
|
-
let mut builder = libsql::Builder::new_remote_replica(path, url, token);
|
|
59
|
-
if sync_interval_secs > 0 {
|
|
60
|
-
builder = builder.sync_interval(Duration::from_secs(sync_interval_secs));
|
|
61
|
-
}
|
|
62
|
-
builder.build().await.map_err(mk_err)
|
|
63
|
-
})?;
|
|
64
|
-
Ok(Self { inner: Arc::new(db) })
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/// Offline write 用 Database を作成
|
|
68
|
-
/// write はローカルに書いてすぐ返す。sync() でまとめてリモートへ反映する。
|
|
69
|
-
/// path: ローカル DB ファイルパス
|
|
70
|
-
/// url: Turso リモート URL (libsql://...)
|
|
71
|
-
/// token: 認証トークン
|
|
72
|
-
/// sync_interval_secs: バックグラウンド自動同期間隔(秒)。0 なら手動のみ
|
|
73
|
-
fn new_synced(
|
|
74
|
-
path: String,
|
|
75
|
-
url: String,
|
|
76
|
-
token: String,
|
|
77
|
-
sync_interval_secs: u64,
|
|
78
|
-
) -> Result<Self, Error> {
|
|
79
|
-
let db = runtime().block_on(async {
|
|
80
|
-
let mut builder = libsql::Builder::new_synced_database(path, url, token);
|
|
81
|
-
if sync_interval_secs > 0 {
|
|
82
|
-
builder = builder.sync_interval(Duration::from_secs(sync_interval_secs));
|
|
83
|
-
}
|
|
84
|
-
builder.build().await.map_err(mk_err)
|
|
85
|
-
})?;
|
|
86
|
-
Ok(Self { inner: Arc::new(db) })
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/// リモートから最新フレームを手動で同期する(pull)
|
|
90
|
-
/// offline モードでは write もまとめてリモートへ push される
|
|
91
|
-
fn sync(&self) -> Result<(), Error> {
|
|
92
|
-
let db = Arc::clone(&self.inner);
|
|
93
|
-
runtime().block_on(async move {
|
|
94
|
-
db.sync().await.map_err(mk_err)
|
|
95
|
-
})?;
|
|
96
|
-
Ok(())
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/// この Database から Connection を取得して TursoConnection を返す
|
|
100
|
-
fn connect(&self) -> Result<TursoConnection, Error> {
|
|
101
|
-
let conn = self.inner.connect().map_err(mk_err)?;
|
|
102
|
-
Ok(TursoConnection {
|
|
103
|
-
inner: Arc::new(conn),
|
|
104
|
-
})
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// -----------------------------------------------------------------------
|
|
109
|
-
// TursoConnection — Ruby に公開する接続オブジェクト
|
|
110
|
-
// -----------------------------------------------------------------------
|
|
111
|
-
|
|
112
|
-
#[magnus::wrap(class = "TursoLibsql::Connection", free_immediately, size)]
|
|
113
|
-
struct TursoConnection {
|
|
114
|
-
inner: Arc<libsql::Connection>,
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
impl TursoConnection {
|
|
118
|
-
/// 新しいリモート接続を作成する(Ruby: TursoLibsql::Connection.new(url, token))
|
|
119
|
-
/// 後方互換のために残す。内部では TursoDatabase を経由する
|
|
120
|
-
fn new(url: String, token: String) -> Result<Self, Error> {
|
|
121
|
-
let db = runtime().block_on(async {
|
|
122
|
-
libsql::Builder::new_remote(url, token)
|
|
123
|
-
.build()
|
|
124
|
-
.await
|
|
125
|
-
.map_err(mk_err)
|
|
126
|
-
})?;
|
|
127
|
-
let conn = db.connect().map_err(mk_err)?;
|
|
128
|
-
Ok(Self {
|
|
129
|
-
inner: Arc::new(conn),
|
|
130
|
-
})
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/// SQL を実行し、影響を受けた行数を返す(INSERT/UPDATE/DELETE 用)
|
|
134
|
-
fn execute(&self, sql: String) -> Result<u64, Error> {
|
|
135
|
-
let conn = Arc::clone(&self.inner);
|
|
136
|
-
runtime().block_on(async move {
|
|
137
|
-
conn.execute(&sql, ()).await.map_err(mk_err)
|
|
138
|
-
})
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/// SQL を実行し、結果を Array of Hash で返す(SELECT 用)
|
|
142
|
-
fn query(&self, sql: String) -> Result<magnus::RArray, Error> {
|
|
143
|
-
let conn = Arc::clone(&self.inner);
|
|
144
|
-
|
|
145
|
-
let rows_data: Vec<Vec<(String, libsql::Value)>> =
|
|
146
|
-
runtime().block_on(async move {
|
|
147
|
-
let mut rows = conn.query(&sql, ()).await.map_err(mk_err)?;
|
|
148
|
-
let mut result: Vec<Vec<(String, libsql::Value)>> = Vec::new();
|
|
149
|
-
|
|
150
|
-
while let Some(row) = rows.next().await.map_err(mk_err)? {
|
|
151
|
-
let col_count = rows.column_count();
|
|
152
|
-
let mut record: Vec<(String, libsql::Value)> =
|
|
153
|
-
Vec::with_capacity(col_count as usize);
|
|
154
|
-
|
|
155
|
-
for i in 0..col_count {
|
|
156
|
-
let name = rows.column_name(i).unwrap_or("?").to_string();
|
|
157
|
-
let val = row.get_value(i).map_err(mk_err)?;
|
|
158
|
-
record.push((name, val));
|
|
159
|
-
}
|
|
160
|
-
result.push(record);
|
|
161
|
-
}
|
|
162
|
-
Ok::<_, Error>(result)
|
|
163
|
-
})?;
|
|
164
|
-
|
|
165
|
-
let ruby = Ruby::get().expect("called outside Ruby thread");
|
|
166
|
-
let outer = ruby.ary_new_capa(rows_data.len());
|
|
167
|
-
for record in rows_data {
|
|
168
|
-
let hash = ruby.hash_new();
|
|
169
|
-
for (col, val) in record {
|
|
170
|
-
let ruby_key = ruby.str_new(&col);
|
|
171
|
-
let ruby_val = libsql_value_to_ruby(&ruby, val)?;
|
|
172
|
-
hash.aset(ruby_key, ruby_val)?;
|
|
173
|
-
}
|
|
174
|
-
outer.push(hash)?;
|
|
175
|
-
}
|
|
176
|
-
Ok(outer)
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/// プリペアドステートメントで SQL を実行(パラメータ付き)
|
|
180
|
-
fn execute_with_params(&self, sql: String, params: Vec<String>) -> Result<u64, Error> {
|
|
181
|
-
let conn = Arc::clone(&self.inner);
|
|
182
|
-
runtime().block_on(async move {
|
|
183
|
-
let params: Vec<libsql::Value> =
|
|
184
|
-
params.into_iter().map(libsql::Value::Text).collect();
|
|
185
|
-
conn.execute(&sql, libsql::params_from_iter(params))
|
|
186
|
-
.await
|
|
187
|
-
.map_err(mk_err)
|
|
188
|
-
})
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/// トランザクションを開始する
|
|
192
|
-
fn begin_transaction(&self) -> Result<(), Error> {
|
|
193
|
-
let conn = Arc::clone(&self.inner);
|
|
194
|
-
runtime().block_on(async move {
|
|
195
|
-
conn.execute("BEGIN", ()).await.map_err(mk_err)
|
|
196
|
-
})?;
|
|
197
|
-
Ok(())
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/// トランザクションをコミットする
|
|
201
|
-
fn commit_transaction(&self) -> Result<(), Error> {
|
|
202
|
-
let conn = Arc::clone(&self.inner);
|
|
203
|
-
runtime().block_on(async move {
|
|
204
|
-
conn.execute("COMMIT", ()).await.map_err(mk_err)
|
|
205
|
-
})?;
|
|
206
|
-
Ok(())
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/// トランザクションをロールバックする
|
|
210
|
-
fn rollback_transaction(&self) -> Result<(), Error> {
|
|
211
|
-
let conn = Arc::clone(&self.inner);
|
|
212
|
-
runtime().block_on(async move {
|
|
213
|
-
conn.execute("ROLLBACK", ()).await.map_err(mk_err)
|
|
214
|
-
})?;
|
|
215
|
-
Ok(())
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/// 最後に挿入した行の rowid を返す
|
|
219
|
-
fn last_insert_rowid(&self) -> Result<i64, Error> {
|
|
220
|
-
let conn = Arc::clone(&self.inner);
|
|
221
|
-
runtime().block_on(async move {
|
|
222
|
-
let mut rows = conn
|
|
223
|
-
.query("SELECT last_insert_rowid()", ())
|
|
224
|
-
.await
|
|
225
|
-
.map_err(mk_err)?;
|
|
226
|
-
|
|
227
|
-
if let Some(row) = rows.next().await.map_err(mk_err)? {
|
|
228
|
-
row.get::<i64>(0).map_err(mk_err)
|
|
229
|
-
} else {
|
|
230
|
-
Ok(0)
|
|
231
|
-
}
|
|
232
|
-
})
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// -----------------------------------------------------------------------
|
|
237
|
-
// libsql::Value → Ruby Value 変換
|
|
238
|
-
// -----------------------------------------------------------------------
|
|
239
|
-
|
|
240
|
-
fn libsql_value_to_ruby(ruby: &Ruby, val: libsql::Value) -> Result<magnus::Value, Error> {
|
|
241
|
-
match val {
|
|
242
|
-
libsql::Value::Null => Ok(ruby.qnil().as_value()),
|
|
243
|
-
libsql::Value::Integer(i) => Ok(ruby.integer_from_i64(i).as_value()),
|
|
244
|
-
libsql::Value::Real(f) => Ok(ruby.float_from_f64(f).as_value()),
|
|
245
|
-
libsql::Value::Text(s) => Ok(ruby.str_new(&s).as_value()),
|
|
246
|
-
libsql::Value::Blob(b) => Ok(ruby.str_from_slice(&b).as_value()),
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// -----------------------------------------------------------------------
|
|
251
|
-
// Magnus init — Ruby 拡張のエントリポイント
|
|
252
|
-
// -----------------------------------------------------------------------
|
|
253
|
-
|
|
254
|
-
#[magnus::init]
|
|
255
|
-
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
256
|
-
let module = ruby.define_module("TursoLibsql")?;
|
|
257
|
-
|
|
258
|
-
// TursoLibsql::Database
|
|
259
|
-
let db_class = module.define_class("Database", ruby.class_object())?;
|
|
260
|
-
db_class.define_singleton_method("new_remote", function!(TursoDatabase::new_remote, 2))?;
|
|
261
|
-
db_class.define_singleton_method(
|
|
262
|
-
"new_remote_replica",
|
|
263
|
-
function!(TursoDatabase::new_remote_replica, 4),
|
|
264
|
-
)?;
|
|
265
|
-
db_class.define_singleton_method(
|
|
266
|
-
"new_synced",
|
|
267
|
-
function!(TursoDatabase::new_synced, 4),
|
|
268
|
-
)?;
|
|
269
|
-
db_class.define_method("sync", method!(TursoDatabase::sync, 0))?;
|
|
270
|
-
db_class.define_method("connect", method!(TursoDatabase::connect, 0))?;
|
|
271
|
-
|
|
272
|
-
// TursoLibsql::Connection
|
|
273
|
-
let conn_class = module.define_class("Connection", ruby.class_object())?;
|
|
274
|
-
conn_class.define_singleton_method("new", function!(TursoConnection::new, 2))?;
|
|
275
|
-
conn_class.define_method("execute", method!(TursoConnection::execute, 1))?;
|
|
276
|
-
conn_class.define_method("query", method!(TursoConnection::query, 1))?;
|
|
277
|
-
conn_class.define_method(
|
|
278
|
-
"execute_with_params",
|
|
279
|
-
method!(TursoConnection::execute_with_params, 2),
|
|
280
|
-
)?;
|
|
281
|
-
conn_class.define_method(
|
|
282
|
-
"begin_transaction",
|
|
283
|
-
method!(TursoConnection::begin_transaction, 0),
|
|
284
|
-
)?;
|
|
285
|
-
conn_class.define_method(
|
|
286
|
-
"commit_transaction",
|
|
287
|
-
method!(TursoConnection::commit_transaction, 0),
|
|
288
|
-
)?;
|
|
289
|
-
conn_class.define_method(
|
|
290
|
-
"rollback_transaction",
|
|
291
|
-
method!(TursoConnection::rollback_transaction, 0),
|
|
292
|
-
)?;
|
|
293
|
-
conn_class.define_method(
|
|
294
|
-
"last_insert_rowid",
|
|
295
|
-
method!(TursoConnection::last_insert_rowid, 0),
|
|
296
|
-
)?;
|
|
297
|
-
|
|
298
|
-
Ok(())
|
|
299
|
-
}
|