@korajs/tauri 0.3.3 → 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.
- package/dist/index.cjs +4 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/plugin/Cargo.lock +4656 -0
- package/plugin/src/lib.rs +2 -2
- package/plugin/tests/integration.rs +555 -0
package/plugin/src/lib.rs
CHANGED
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
//!
|
|
10
10
|
//! Add the plugin to your Tauri app:
|
|
11
11
|
//!
|
|
12
|
-
//! ```rust,
|
|
12
|
+
//! ```rust,ignore
|
|
13
13
|
//! fn main() {
|
|
14
14
|
//! tauri::Builder::default()
|
|
15
|
-
//! .plugin(
|
|
15
|
+
//! .plugin(tauri_plugin_kora_sqlite::init())
|
|
16
16
|
//! .run(tauri::generate_context!())
|
|
17
17
|
//! .expect("error while running tauri application");
|
|
18
18
|
//! }
|
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
use rusqlite::Connection;
|
|
2
|
+
|
|
3
|
+
/// Helper: open an in-memory database with the same pragmas the plugin uses.
|
|
4
|
+
fn open_kora_db() -> Connection {
|
|
5
|
+
let conn = Connection::open_in_memory().unwrap();
|
|
6
|
+
conn.pragma_update(None, "journal_mode", "WAL").ok();
|
|
7
|
+
conn.pragma_update(None, "foreign_keys", "ON").unwrap();
|
|
8
|
+
conn.pragma_update(None, "synchronous", "NORMAL").unwrap();
|
|
9
|
+
conn.pragma_update(None, "busy_timeout", "5000").unwrap();
|
|
10
|
+
conn
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/// Helper: create the standard Kora metadata tables.
|
|
14
|
+
fn create_kora_meta(conn: &Connection) {
|
|
15
|
+
conn.execute_batch(
|
|
16
|
+
"
|
|
17
|
+
CREATE TABLE IF NOT EXISTS _kora_meta (
|
|
18
|
+
key TEXT PRIMARY KEY,
|
|
19
|
+
value TEXT NOT NULL
|
|
20
|
+
);
|
|
21
|
+
CREATE TABLE IF NOT EXISTS _kora_version_vector (
|
|
22
|
+
node_id TEXT PRIMARY KEY,
|
|
23
|
+
max_sequence_number INTEGER NOT NULL,
|
|
24
|
+
last_seen_at INTEGER NOT NULL
|
|
25
|
+
);
|
|
26
|
+
",
|
|
27
|
+
)
|
|
28
|
+
.unwrap();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const DDL_TODOS: &str = "
|
|
32
|
+
CREATE TABLE IF NOT EXISTS todos (
|
|
33
|
+
id TEXT PRIMARY KEY,
|
|
34
|
+
title TEXT NOT NULL,
|
|
35
|
+
completed INTEGER NOT NULL DEFAULT 0,
|
|
36
|
+
_created_at INTEGER NOT NULL,
|
|
37
|
+
_updated_at INTEGER NOT NULL
|
|
38
|
+
);
|
|
39
|
+
CREATE TABLE IF NOT EXISTS _kora_ops_todos (
|
|
40
|
+
id TEXT PRIMARY KEY,
|
|
41
|
+
node_id TEXT NOT NULL,
|
|
42
|
+
type TEXT NOT NULL,
|
|
43
|
+
collection TEXT NOT NULL,
|
|
44
|
+
record_id TEXT NOT NULL,
|
|
45
|
+
data TEXT,
|
|
46
|
+
previous_data TEXT,
|
|
47
|
+
wall_time INTEGER NOT NULL,
|
|
48
|
+
logical INTEGER NOT NULL,
|
|
49
|
+
ts_node_id TEXT NOT NULL,
|
|
50
|
+
sequence_number INTEGER NOT NULL,
|
|
51
|
+
causal_deps TEXT NOT NULL DEFAULT '[]',
|
|
52
|
+
schema_version INTEGER NOT NULL
|
|
53
|
+
);
|
|
54
|
+
CREATE INDEX IF NOT EXISTS idx_todos_completed ON todos(completed);
|
|
55
|
+
";
|
|
56
|
+
|
|
57
|
+
// ── Database initialization tests ─────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
#[test]
|
|
60
|
+
fn test_open_sets_wal_mode() {
|
|
61
|
+
let conn = open_kora_db();
|
|
62
|
+
let mode: String = conn
|
|
63
|
+
.pragma_query_value(None, "journal_mode", |row| row.get(0))
|
|
64
|
+
.unwrap();
|
|
65
|
+
// In-memory databases fall back to 'memory' when WAL is requested
|
|
66
|
+
assert!(mode == "wal" || mode == "memory", "expected wal or memory, got {}", mode);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#[test]
|
|
70
|
+
fn test_open_enables_foreign_keys() {
|
|
71
|
+
let conn = open_kora_db();
|
|
72
|
+
let fk: i64 = conn
|
|
73
|
+
.pragma_query_value(None, "foreign_keys", |row| row.get(0))
|
|
74
|
+
.unwrap();
|
|
75
|
+
assert_eq!(fk, 1, "foreign_keys should be ON");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── DDL execution tests ──────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
#[test]
|
|
81
|
+
fn test_create_tables_from_ddl() {
|
|
82
|
+
let conn = open_kora_db();
|
|
83
|
+
create_kora_meta(&conn);
|
|
84
|
+
conn.execute_batch(DDL_TODOS).unwrap();
|
|
85
|
+
|
|
86
|
+
let tables: Vec<String> = conn
|
|
87
|
+
.prepare("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
|
|
88
|
+
.unwrap()
|
|
89
|
+
.query_map([], |row| row.get(0))
|
|
90
|
+
.unwrap()
|
|
91
|
+
.collect::<Result<Vec<_>, _>>()
|
|
92
|
+
.unwrap();
|
|
93
|
+
|
|
94
|
+
assert!(tables.contains(&"_kora_meta".to_string()));
|
|
95
|
+
assert!(tables.contains(&"_kora_version_vector".to_string()));
|
|
96
|
+
assert!(tables.contains(&"todos".to_string()));
|
|
97
|
+
assert!(tables.contains(&"_kora_ops_todos".to_string()));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
#[test]
|
|
101
|
+
fn test_create_indexes() {
|
|
102
|
+
let conn = open_kora_db();
|
|
103
|
+
create_kora_meta(&conn);
|
|
104
|
+
conn.execute_batch(DDL_TODOS).unwrap();
|
|
105
|
+
|
|
106
|
+
let indexes: Vec<String> = conn
|
|
107
|
+
.prepare("SELECT name FROM sqlite_master WHERE type='index' AND name NOT LIKE 'sqlite_%' ORDER BY name")
|
|
108
|
+
.unwrap()
|
|
109
|
+
.query_map([], |row| row.get(0))
|
|
110
|
+
.unwrap()
|
|
111
|
+
.collect::<Result<Vec<_>, _>>()
|
|
112
|
+
.unwrap();
|
|
113
|
+
|
|
114
|
+
assert!(indexes.contains(&"idx_todos_completed".to_string()));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── CRUD tests ───────────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
#[test]
|
|
120
|
+
fn test_insert_and_select() {
|
|
121
|
+
let conn = open_kora_db();
|
|
122
|
+
create_kora_meta(&conn);
|
|
123
|
+
conn.execute_batch(DDL_TODOS).unwrap();
|
|
124
|
+
|
|
125
|
+
conn.execute(
|
|
126
|
+
"INSERT INTO todos (id, title, completed, _created_at, _updated_at) VALUES (?1, ?2, ?3, ?4, ?5)",
|
|
127
|
+
["rec-1", "Hello", "0", "1000", "1000"],
|
|
128
|
+
)
|
|
129
|
+
.unwrap();
|
|
130
|
+
|
|
131
|
+
let (title, completed): (String, i64) = conn
|
|
132
|
+
.query_row("SELECT title, completed FROM todos WHERE id = ?1", ["rec-1"], |row| {
|
|
133
|
+
Ok((row.get(0)?, row.get(1)?))
|
|
134
|
+
})
|
|
135
|
+
.unwrap();
|
|
136
|
+
|
|
137
|
+
assert_eq!(title, "Hello");
|
|
138
|
+
assert_eq!(completed, 0);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
#[test]
|
|
142
|
+
fn test_update_record() {
|
|
143
|
+
let conn = open_kora_db();
|
|
144
|
+
create_kora_meta(&conn);
|
|
145
|
+
conn.execute_batch(DDL_TODOS).unwrap();
|
|
146
|
+
|
|
147
|
+
conn.execute(
|
|
148
|
+
"INSERT INTO todos (id, title, completed, _created_at, _updated_at) VALUES (?1, ?2, ?3, ?4, ?5)",
|
|
149
|
+
["rec-1", "Old Title", "0", "1000", "1000"],
|
|
150
|
+
).unwrap();
|
|
151
|
+
|
|
152
|
+
conn.execute(
|
|
153
|
+
"UPDATE todos SET title = ?1, completed = ?2, _updated_at = ?3 WHERE id = ?4",
|
|
154
|
+
["Updated", "1", "1001", "rec-1"],
|
|
155
|
+
)
|
|
156
|
+
.unwrap();
|
|
157
|
+
|
|
158
|
+
let (title, completed): (String, i64) = conn
|
|
159
|
+
.query_row("SELECT title, completed FROM todos WHERE id = ?1", ["rec-1"], |row| {
|
|
160
|
+
Ok((row.get(0)?, row.get(1)?))
|
|
161
|
+
})
|
|
162
|
+
.unwrap();
|
|
163
|
+
|
|
164
|
+
assert_eq!(title, "Updated");
|
|
165
|
+
assert_eq!(completed, 1);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
#[test]
|
|
169
|
+
fn test_delete_record() {
|
|
170
|
+
let conn = open_kora_db();
|
|
171
|
+
create_kora_meta(&conn);
|
|
172
|
+
conn.execute_batch(DDL_TODOS).unwrap();
|
|
173
|
+
|
|
174
|
+
conn.execute(
|
|
175
|
+
"INSERT INTO todos (id, title, completed, _created_at, _updated_at) VALUES (?1, ?2, ?3, ?4, ?5)",
|
|
176
|
+
["rec-1", "Delete Me", "0", "1000", "1000"],
|
|
177
|
+
)
|
|
178
|
+
.unwrap();
|
|
179
|
+
|
|
180
|
+
conn.execute("DELETE FROM todos WHERE id = ?1", ["rec-1"])
|
|
181
|
+
.unwrap();
|
|
182
|
+
|
|
183
|
+
let count: i64 = conn
|
|
184
|
+
.query_row("SELECT COUNT(*) FROM todos WHERE id = ?1", ["rec-1"], |row| {
|
|
185
|
+
row.get(0)
|
|
186
|
+
})
|
|
187
|
+
.unwrap();
|
|
188
|
+
assert_eq!(count, 0);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ── Operation log tests ──────────────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
#[test]
|
|
194
|
+
fn test_insert_and_read_operation() {
|
|
195
|
+
let conn = open_kora_db();
|
|
196
|
+
create_kora_meta(&conn);
|
|
197
|
+
conn.execute_batch(DDL_TODOS).unwrap();
|
|
198
|
+
|
|
199
|
+
conn.execute(
|
|
200
|
+
"INSERT INTO _kora_ops_todos (id, node_id, type, collection, record_id, data, previous_data, wall_time, logical, ts_node_id, sequence_number, causal_deps, schema_version)
|
|
201
|
+
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)",
|
|
202
|
+
["op-1", "node-a", "insert", "todos", "rec-1", r#"{"title":"Hello"}"#, "", "1000", "0", "node-a", "1", "[]", "1"],
|
|
203
|
+
).unwrap();
|
|
204
|
+
|
|
205
|
+
let (op_id, op_type, op_record_id): (String, String, String) = conn
|
|
206
|
+
.query_row(
|
|
207
|
+
"SELECT id, type, record_id FROM _kora_ops_todos WHERE id = ?1",
|
|
208
|
+
["op-1"],
|
|
209
|
+
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
|
|
210
|
+
)
|
|
211
|
+
.unwrap();
|
|
212
|
+
|
|
213
|
+
assert_eq!(op_id, "op-1");
|
|
214
|
+
assert_eq!(op_type, "insert");
|
|
215
|
+
assert_eq!(op_record_id, "rec-1");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
#[test]
|
|
219
|
+
fn test_operation_range_query() {
|
|
220
|
+
let conn = open_kora_db();
|
|
221
|
+
create_kora_meta(&conn);
|
|
222
|
+
conn.execute_batch(DDL_TODOS).unwrap();
|
|
223
|
+
|
|
224
|
+
for i in 1..=5 {
|
|
225
|
+
conn.execute(
|
|
226
|
+
"INSERT INTO _kora_ops_todos (id, node_id, type, collection, record_id, data, previous_data, wall_time, logical, ts_node_id, sequence_number, causal_deps, schema_version)
|
|
227
|
+
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)",
|
|
228
|
+
rusqlite::params![
|
|
229
|
+
format!("op-{}", i), "node-a", "insert", "todos", format!("rec-{}", i),
|
|
230
|
+
r#"{"title":"test"}"#, "", 1000 + i, 0, "node-a", i, "[]", 1,
|
|
231
|
+
],
|
|
232
|
+
).unwrap();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
let ops: Vec<String> = conn
|
|
236
|
+
.prepare("SELECT id FROM _kora_ops_todos WHERE node_id = ?1 AND sequence_number >= ?2 AND sequence_number <= ?3 ORDER BY sequence_number")
|
|
237
|
+
.unwrap()
|
|
238
|
+
.query_map(rusqlite::params!["node-a", 2, 4], |row| row.get(0))
|
|
239
|
+
.unwrap()
|
|
240
|
+
.collect::<Result<Vec<_>, _>>()
|
|
241
|
+
.unwrap();
|
|
242
|
+
|
|
243
|
+
assert_eq!(ops, vec!["op-2", "op-3", "op-4"]);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ── Transaction tests ─────────────────────────────────────────────────────────
|
|
247
|
+
|
|
248
|
+
#[test]
|
|
249
|
+
fn test_transaction_commits() {
|
|
250
|
+
let conn = open_kora_db();
|
|
251
|
+
create_kora_meta(&conn);
|
|
252
|
+
conn.execute_batch(DDL_TODOS).unwrap();
|
|
253
|
+
|
|
254
|
+
conn.execute_batch("BEGIN").unwrap();
|
|
255
|
+
conn.execute(
|
|
256
|
+
"INSERT INTO todos (id, title, completed, _created_at, _updated_at) VALUES (?1, ?2, ?3, ?4, ?5)",
|
|
257
|
+
["rec-1", "In TX", "0", "1000", "1000"],
|
|
258
|
+
).unwrap();
|
|
259
|
+
conn.execute_batch("COMMIT").unwrap();
|
|
260
|
+
|
|
261
|
+
let count: i64 = conn
|
|
262
|
+
.query_row("SELECT COUNT(*) FROM todos", [], |row| row.get(0))
|
|
263
|
+
.unwrap();
|
|
264
|
+
assert_eq!(count, 1);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
#[test]
|
|
268
|
+
fn test_transaction_rollback_on_error() {
|
|
269
|
+
let conn = open_kora_db();
|
|
270
|
+
create_kora_meta(&conn);
|
|
271
|
+
conn.execute_batch(DDL_TODOS).unwrap();
|
|
272
|
+
|
|
273
|
+
// Start transaction, insert, then intentionally fail
|
|
274
|
+
conn.execute_batch("BEGIN").unwrap();
|
|
275
|
+
conn.execute(
|
|
276
|
+
"INSERT INTO todos (id, title, completed, _created_at, _updated_at) VALUES (?1, ?2, ?3, ?4, ?5)",
|
|
277
|
+
["rec-1", "Rollback Me", "0", "1000", "1000"],
|
|
278
|
+
).unwrap();
|
|
279
|
+
// Simulate an error by rolling back
|
|
280
|
+
conn.execute_batch("ROLLBACK").unwrap();
|
|
281
|
+
|
|
282
|
+
let count: i64 = conn
|
|
283
|
+
.query_row("SELECT COUNT(*) FROM todos", [], |row| row.get(0))
|
|
284
|
+
.unwrap();
|
|
285
|
+
assert_eq!(count, 0);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
#[test]
|
|
289
|
+
fn test_transaction_isolation() {
|
|
290
|
+
let conn = open_kora_db();
|
|
291
|
+
create_kora_meta(&conn);
|
|
292
|
+
conn.execute_batch(DDL_TODOS).unwrap();
|
|
293
|
+
|
|
294
|
+
conn.execute_batch("BEGIN").unwrap();
|
|
295
|
+
conn.execute(
|
|
296
|
+
"INSERT INTO todos (id, title, completed, _created_at, _updated_at) VALUES (?1, ?2, ?3, ?4, ?5)",
|
|
297
|
+
["rec-1", "Isolated", "0", "1000", "1000"],
|
|
298
|
+
).unwrap();
|
|
299
|
+
|
|
300
|
+
// Changes shouldn't be visible outside the transaction
|
|
301
|
+
let count: i64 = conn
|
|
302
|
+
.query_row("SELECT COUNT(*) FROM todos", [], |row| row.get(0))
|
|
303
|
+
.unwrap();
|
|
304
|
+
assert_eq!(count, 1); // Within same connection, uncommitted data IS visible (SQLite default)
|
|
305
|
+
|
|
306
|
+
conn.execute_batch("ROLLBACK").unwrap();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ── Migration tests ───────────────────────────────────────────────────────────
|
|
310
|
+
|
|
311
|
+
#[test]
|
|
312
|
+
fn test_migration_adds_column() {
|
|
313
|
+
let conn = open_kora_db();
|
|
314
|
+
create_kora_meta(&conn);
|
|
315
|
+
conn.execute_batch(DDL_TODOS).unwrap();
|
|
316
|
+
|
|
317
|
+
// Add a new column (migration)
|
|
318
|
+
conn.execute_batch("BEGIN").unwrap();
|
|
319
|
+
conn.execute_batch("ALTER TABLE todos ADD COLUMN notes TEXT").unwrap();
|
|
320
|
+
conn.execute_batch("COMMIT").unwrap();
|
|
321
|
+
|
|
322
|
+
// Verify the new column exists and can be written to
|
|
323
|
+
conn.execute(
|
|
324
|
+
"INSERT INTO todos (id, title, completed, notes, _created_at, _updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
|
325
|
+
["rec-1", "With Notes", "0", "Some notes", "1000", "1000"],
|
|
326
|
+
).unwrap();
|
|
327
|
+
|
|
328
|
+
let notes: String = conn
|
|
329
|
+
.query_row("SELECT notes FROM todos WHERE id = ?1", ["rec-1"], |row| {
|
|
330
|
+
row.get(0)
|
|
331
|
+
})
|
|
332
|
+
.unwrap();
|
|
333
|
+
assert_eq!(notes, "Some notes");
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
#[test]
|
|
337
|
+
fn test_migration_rolls_back_on_error() {
|
|
338
|
+
let conn = open_kora_db();
|
|
339
|
+
create_kora_meta(&conn);
|
|
340
|
+
conn.execute_batch(DDL_TODOS).unwrap();
|
|
341
|
+
|
|
342
|
+
// Try to apply invalid SQL within a migration
|
|
343
|
+
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
|
344
|
+
conn.execute_batch("BEGIN").unwrap();
|
|
345
|
+
conn.execute_batch("INVALID SQL STATEMENT").unwrap(); // This will panic
|
|
346
|
+
conn.execute_batch("COMMIT").unwrap();
|
|
347
|
+
}));
|
|
348
|
+
|
|
349
|
+
assert!(result.is_err(), "invalid SQL should cause a panic/error");
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
#[test]
|
|
353
|
+
fn test_safe_alter_table_ignores_duplicate_column() {
|
|
354
|
+
let conn = open_kora_db();
|
|
355
|
+
create_kora_meta(&conn);
|
|
356
|
+
conn.execute_batch(DDL_TODOS).unwrap();
|
|
357
|
+
|
|
358
|
+
// First add a column
|
|
359
|
+
conn.execute_batch("ALTER TABLE todos ADD COLUMN priority TEXT DEFAULT 'medium'").unwrap();
|
|
360
|
+
|
|
361
|
+
// Try adding the same column again — the plugin's safe-alter pattern
|
|
362
|
+
// should ignore the "duplicate column name" error
|
|
363
|
+
let sql = "ALTER TABLE todos ADD COLUMN priority TEXT DEFAULT 'medium'";
|
|
364
|
+
match conn.execute_batch(sql) {
|
|
365
|
+
Ok(_) => {} // Should not happen — column already exists
|
|
366
|
+
Err(e) => {
|
|
367
|
+
let msg = e.to_string();
|
|
368
|
+
assert!(
|
|
369
|
+
msg.contains("duplicate column name"),
|
|
370
|
+
"expected duplicate column error, got: {}",
|
|
371
|
+
msg
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ── Data type tests ───────────────────────────────────────────────────────────
|
|
378
|
+
|
|
379
|
+
#[test]
|
|
380
|
+
fn test_all_sql_types_roundtrip() {
|
|
381
|
+
let conn = open_kora_db();
|
|
382
|
+
conn.execute_batch(
|
|
383
|
+
"CREATE TABLE types (
|
|
384
|
+
id TEXT PRIMARY KEY,
|
|
385
|
+
int_v INTEGER,
|
|
386
|
+
float_v REAL,
|
|
387
|
+
text_v TEXT,
|
|
388
|
+
bool_v INTEGER
|
|
389
|
+
)",
|
|
390
|
+
)
|
|
391
|
+
.unwrap();
|
|
392
|
+
|
|
393
|
+
conn.execute(
|
|
394
|
+
"INSERT INTO types (id, int_v, float_v, text_v, bool_v) VALUES (?1, ?2, ?3, ?4, ?5)",
|
|
395
|
+
rusqlite::params!["rec-1", 42, 3.14, "hello", 1],
|
|
396
|
+
)
|
|
397
|
+
.unwrap();
|
|
398
|
+
|
|
399
|
+
let (int_v, float_v, text_v, bool_v): (i64, f64, String, i64) = conn
|
|
400
|
+
.query_row(
|
|
401
|
+
"SELECT int_v, float_v, text_v, bool_v FROM types WHERE id = ?1",
|
|
402
|
+
["rec-1"],
|
|
403
|
+
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)),
|
|
404
|
+
)
|
|
405
|
+
.unwrap();
|
|
406
|
+
|
|
407
|
+
assert_eq!(int_v, 42);
|
|
408
|
+
assert!((float_v - 3.14).abs() < 1e-10);
|
|
409
|
+
assert_eq!(text_v, "hello");
|
|
410
|
+
assert_eq!(bool_v, 1);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
#[test]
|
|
414
|
+
fn test_null_values() {
|
|
415
|
+
let conn = open_kora_db();
|
|
416
|
+
conn.execute_batch(
|
|
417
|
+
"CREATE TABLE nullable (id TEXT PRIMARY KEY, value TEXT)",
|
|
418
|
+
).unwrap();
|
|
419
|
+
|
|
420
|
+
conn.execute(
|
|
421
|
+
"INSERT INTO nullable (id, value) VALUES (?1, ?2)",
|
|
422
|
+
rusqlite::params!["rec-1", rusqlite::types::Null],
|
|
423
|
+
)
|
|
424
|
+
.unwrap();
|
|
425
|
+
|
|
426
|
+
let value: Option<String> = conn
|
|
427
|
+
.query_row("SELECT value FROM nullable WHERE id = ?1", ["rec-1"], |row| {
|
|
428
|
+
row.get(0)
|
|
429
|
+
})
|
|
430
|
+
.unwrap();
|
|
431
|
+
assert_eq!(value, None);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
#[test]
|
|
435
|
+
fn test_blob_as_hex() {
|
|
436
|
+
let conn = open_kora_db();
|
|
437
|
+
conn.execute_batch("CREATE TABLE blobs (id TEXT PRIMARY KEY, data BLOB)").unwrap();
|
|
438
|
+
|
|
439
|
+
let input: Vec<u8> = vec![0x00, 0xFF, 0xAB, 0xCD];
|
|
440
|
+
conn.execute(
|
|
441
|
+
"INSERT INTO blobs (id, data) VALUES (?1, ?2)",
|
|
442
|
+
rusqlite::params!["rec-1", input],
|
|
443
|
+
)
|
|
444
|
+
.unwrap();
|
|
445
|
+
|
|
446
|
+
let output: Vec<u8> = conn
|
|
447
|
+
.query_row("SELECT data FROM blobs WHERE id = ?1", ["rec-1"], |row| {
|
|
448
|
+
row.get(0)
|
|
449
|
+
})
|
|
450
|
+
.unwrap();
|
|
451
|
+
assert_eq!(output, input);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// ── Error case tests ──────────────────────────────────────────────────────────
|
|
455
|
+
|
|
456
|
+
#[test]
|
|
457
|
+
fn test_invalid_sql_returns_error() {
|
|
458
|
+
let conn = open_kora_db();
|
|
459
|
+
let result = conn.execute_batch("SELECT FROM nowhere");
|
|
460
|
+
assert!(result.is_err(), "invalid SQL should return an error");
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
#[test]
|
|
464
|
+
fn test_query_missing_table_returns_error() {
|
|
465
|
+
let conn = open_kora_db();
|
|
466
|
+
let result = conn.prepare("SELECT * FROM nonexistent");
|
|
467
|
+
assert!(result.is_err(), "query on missing table should error");
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// ── Version vector tests ─────────────────────────────────────────────────────
|
|
471
|
+
|
|
472
|
+
#[test]
|
|
473
|
+
fn test_version_vector_insert_and_update() {
|
|
474
|
+
let conn = open_kora_db();
|
|
475
|
+
create_kora_meta(&conn);
|
|
476
|
+
|
|
477
|
+
conn.execute(
|
|
478
|
+
"INSERT INTO _kora_version_vector (node_id, max_sequence_number, last_seen_at) VALUES (?1, ?2, ?3)",
|
|
479
|
+
["node-a", "1", "1000"],
|
|
480
|
+
).unwrap();
|
|
481
|
+
|
|
482
|
+
let (seq, seen): (i64, i64) = conn
|
|
483
|
+
.query_row(
|
|
484
|
+
"SELECT max_sequence_number, last_seen_at FROM _kora_version_vector WHERE node_id = ?1",
|
|
485
|
+
["node-a"],
|
|
486
|
+
|row| Ok((row.get(0)?, row.get(1)?)),
|
|
487
|
+
)
|
|
488
|
+
.unwrap();
|
|
489
|
+
assert_eq!(seq, 1);
|
|
490
|
+
assert_eq!(seen, 1000);
|
|
491
|
+
|
|
492
|
+
// Update
|
|
493
|
+
conn.execute(
|
|
494
|
+
"UPDATE _kora_version_vector SET max_sequence_number = ?1, last_seen_at = ?2 WHERE node_id = ?3",
|
|
495
|
+
["5", "1002", "node-a"],
|
|
496
|
+
).unwrap();
|
|
497
|
+
|
|
498
|
+
let seq: i64 = conn
|
|
499
|
+
.query_row(
|
|
500
|
+
"SELECT max_sequence_number FROM _kora_version_vector WHERE node_id = ?1",
|
|
501
|
+
["node-a"],
|
|
502
|
+
|row| row.get(0),
|
|
503
|
+
)
|
|
504
|
+
.unwrap();
|
|
505
|
+
assert_eq!(seq, 5);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
#[test]
|
|
509
|
+
fn test_version_vector_increments_monotonically() {
|
|
510
|
+
let conn = open_kora_db();
|
|
511
|
+
create_kora_meta(&conn);
|
|
512
|
+
|
|
513
|
+
for i in 1..=10 {
|
|
514
|
+
conn.execute(
|
|
515
|
+
"INSERT INTO _kora_version_vector (node_id, max_sequence_number, last_seen_at) VALUES (?1, ?2, ?3)
|
|
516
|
+
ON CONFLICT(node_id) DO UPDATE SET max_sequence_number = ?2, last_seen_at = ?3",
|
|
517
|
+
rusqlite::params!["node-a", i, 1000 + i],
|
|
518
|
+
).unwrap();
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
let seq: i64 = conn
|
|
522
|
+
.query_row(
|
|
523
|
+
"SELECT max_sequence_number FROM _kora_version_vector WHERE node_id = ?1",
|
|
524
|
+
["node-a"],
|
|
525
|
+
|row| row.get(0),
|
|
526
|
+
)
|
|
527
|
+
.unwrap();
|
|
528
|
+
assert_eq!(seq, 10);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// ── Bulk operation tests ─────────────────────────────────────────────────────
|
|
532
|
+
|
|
533
|
+
#[test]
|
|
534
|
+
fn test_bulk_insert_and_count() {
|
|
535
|
+
let conn = open_kora_db();
|
|
536
|
+
create_kora_meta(&conn);
|
|
537
|
+
conn.execute_batch(DDL_TODOS).unwrap();
|
|
538
|
+
|
|
539
|
+
for i in 0..100 {
|
|
540
|
+
conn.execute(
|
|
541
|
+
"INSERT INTO todos (id, title, completed, _created_at, _updated_at) VALUES (?1, ?2, ?3, ?4, ?5)",
|
|
542
|
+
rusqlite::params![format!("rec-{}", i), format!("Task {}", i), if i % 2 == 0 { 1 } else { 0 }, 1000 + i, 1000 + i],
|
|
543
|
+
).unwrap();
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
let total: i64 = conn
|
|
547
|
+
.query_row("SELECT COUNT(*) FROM todos", [], |row| row.get(0))
|
|
548
|
+
.unwrap();
|
|
549
|
+
assert_eq!(total, 100);
|
|
550
|
+
|
|
551
|
+
let completed: i64 = conn
|
|
552
|
+
.query_row("SELECT COUNT(*) FROM todos WHERE completed = 1", [], |row| row.get(0))
|
|
553
|
+
.unwrap();
|
|
554
|
+
assert_eq!(completed, 50);
|
|
555
|
+
}
|