extralite-bundle 2.4 → 2.6

Sign up to get free protection for your applications and to get access to all the features.
data/TODO.md CHANGED
@@ -1,7 +1,7 @@
1
- - Improve tracing
2
1
  - Transactions and savepoints:
3
2
 
4
- - `DB#transaction {}` - does a `BEGIN..COMMIT` - non-reentrant!
3
+ - https://www.sqlite.org/lang_savepoint.html
4
+
5
5
  - `DB#savepoint(name)` - creates a savepoint
6
6
  - `DB#release(name)` - releases a savepoint
7
7
  - `DB#rollback` - raises `Extralite::Rollback`, which is rescued by `DB#transaction`
@@ -10,7 +10,6 @@
10
10
  - More database methods:
11
11
 
12
12
  - `Database#quote`
13
- - `Database#busy_timeout=` https://sqlite.org/c3ref/busy_timeout.html
14
13
  - `Database#cache_flush` https://sqlite.org/c3ref/db_cacheflush.html
15
14
  - `Database#release_memory` https://sqlite.org/c3ref/db_release_memory.html
16
15
 
@@ -0,0 +1,463 @@
1
+ #ifdef EXTRALITE_ENABLE_CHANGESET
2
+ #include <stdio.h>
3
+ #include "extralite.h"
4
+
5
+ /*
6
+ * Document-class: Extralite::Changeset
7
+ *
8
+ * This class implements a Changeset for tracking changes to the database.
9
+ */
10
+
11
+ VALUE cChangeset;
12
+
13
+ VALUE SYM_delete;
14
+ VALUE SYM_insert;
15
+ VALUE SYM_update;
16
+
17
+ static size_t Changeset_size(const void *ptr) {
18
+ return sizeof(Changeset_t);
19
+ }
20
+
21
+ static void Changeset_free(void *ptr) {
22
+ Changeset_t *changeset = ptr;
23
+ if (changeset->changeset_ptr)
24
+ sqlite3_free(changeset->changeset_ptr);
25
+ free(ptr);
26
+ }
27
+
28
+ static const rb_data_type_t Changeset_type = {
29
+ "Changeset",
30
+ {0, Changeset_free, Changeset_size,},
31
+ 0, 0, RUBY_TYPED_FREE_IMMEDIATELY
32
+ };
33
+
34
+ static VALUE Changeset_allocate(VALUE klass) {
35
+ Changeset_t *changeset = ALLOC(Changeset_t);
36
+ changeset->changeset_len = 0;
37
+ changeset->changeset_ptr = NULL;
38
+ return TypedData_Wrap_Struct(klass, &Changeset_type, changeset);
39
+ }
40
+
41
+ static inline Changeset_t *self_to_changeset(VALUE obj) {
42
+ Changeset_t *changeset;
43
+ TypedData_Get_Struct((obj), Changeset_t, &Changeset_type, (changeset));
44
+ return changeset;
45
+ }
46
+
47
+ /* Initializes an empty changeset.
48
+ *
49
+ * @return [void]
50
+ */
51
+ VALUE Changeset_initialize(VALUE self) {
52
+ Changeset_t *changeset = self_to_changeset(self);
53
+ changeset->changeset_len = 0;
54
+ changeset->changeset_ptr = NULL;
55
+ return Qnil;
56
+ }
57
+
58
+ static inline VALUE tbl_str(VALUE tbl) {
59
+ switch (TYPE(tbl)) {
60
+ case T_NIL:
61
+ case T_STRING:
62
+ return tbl;
63
+ default:
64
+ return rb_funcall(tbl, ID_to_s, 0);
65
+ }
66
+ }
67
+
68
+ static inline void Changeset_track_attach(sqlite3 *db, struct sqlite3_session *session, VALUE tables) {
69
+ long len = RARRAY_LEN(tables);
70
+ VALUE name = Qnil;
71
+ for (long i = 0; i < len; i++) {
72
+ name = tbl_str(RARRAY_AREF(tables, i));
73
+ int rc = sqlite3session_attach(session, NIL_P(name) ? NULL : StringValueCStr(name));
74
+ if (rc != SQLITE_OK)
75
+ rb_raise(cError, "Error while attaching session tables: %s", sqlite3_errstr(rc));
76
+ }
77
+ RB_GC_GUARD(name);
78
+ }
79
+
80
+ struct track_ctx {
81
+ Changeset_t *changeset;
82
+ sqlite3 *sqlite3_db;
83
+ sqlite3_session *session;
84
+ VALUE db;
85
+ VALUE tables;
86
+ };
87
+
88
+ VALUE safe_track(struct track_ctx *ctx) {
89
+ int rc;
90
+
91
+ if (!NIL_P(ctx->tables))
92
+ Changeset_track_attach(ctx->sqlite3_db, ctx->session, ctx->tables);
93
+ else {
94
+ rc = sqlite3session_attach(ctx->session, NULL);
95
+ if (rc != SQLITE_OK)
96
+ rb_raise(cError, "Error while attaching all tables: %s", sqlite3_errstr(rc));
97
+ }
98
+
99
+ rb_yield(ctx->db);
100
+
101
+ rc = sqlite3session_changeset(
102
+ ctx->session,
103
+ &ctx->changeset->changeset_len,
104
+ &ctx->changeset->changeset_ptr
105
+ );
106
+ if (rc != SQLITE_OK)
107
+ rb_raise(cError, "Error while collecting changeset from session: %s", sqlite3_errstr(rc));
108
+
109
+ return Qnil;
110
+ }
111
+
112
+ VALUE cleanup_track(struct track_ctx *ctx) {
113
+ sqlite3session_delete(ctx->session);
114
+ return Qnil;
115
+ }
116
+
117
+ /* Tracks changes in the given block and collects them into the changeset.
118
+ * Changes are tracked only for the given tables. If nil is supplied as the
119
+ * given tables, changes are tracked for all tables.
120
+ *
121
+ * # track changes for the foo and bar tables
122
+ * changeset.track(db, [:foo, :bar]) do
123
+ * run_some_queries
124
+ * end
125
+ * store_changes(changeset.to_blob)
126
+ *
127
+ * @param db [Extralite::Database] database to track
128
+ * @param tables [Array<String, Symbol>, nil] tables to track (or nil for all tables)
129
+ * @return [Extralite::Changeset] changeset
130
+ */
131
+ VALUE Changeset_track(VALUE self, VALUE db, VALUE tables) {
132
+ Changeset_t *changeset = self_to_changeset(self);
133
+ Database_t *db_struct = self_to_database(db);
134
+ sqlite3 *sqlite3_db = db_struct->sqlite3_db;
135
+
136
+ if (changeset->changeset_ptr) {
137
+ sqlite3_free(changeset->changeset_ptr);
138
+ changeset->changeset_len = 0;
139
+ changeset->changeset_ptr = NULL;
140
+ }
141
+
142
+ struct track_ctx ctx = {
143
+ .changeset = changeset,
144
+ .sqlite3_db = sqlite3_db,
145
+ .session = NULL,
146
+ .db = db,
147
+ .tables = tables
148
+ };
149
+ int rc = sqlite3session_create(sqlite3_db, "main", &ctx.session);
150
+ if (rc != SQLITE_OK)
151
+ rb_raise(cError, "Error while creating session: %s", sqlite3_errstr(rc));
152
+
153
+ rb_ensure(SAFE(safe_track), (VALUE)&ctx, SAFE(cleanup_track), (VALUE)&ctx);
154
+
155
+ return self;
156
+ }
157
+
158
+ struct each_ctx {
159
+ sqlite3_changeset_iter *iter;
160
+ };
161
+
162
+ static inline VALUE op_symbol(int op) {
163
+ switch (op) {
164
+ case SQLITE_DELETE:
165
+ return SYM_delete;
166
+ case SQLITE_INSERT:
167
+ return SYM_insert;
168
+ case SQLITE_UPDATE:
169
+ return SYM_update;
170
+ default:
171
+ rb_raise(cError, "Invalid changeset op code %d", op);
172
+ }
173
+ }
174
+
175
+ static inline VALUE convert_value(sqlite3_value *value) {
176
+ if (!value) return Qnil;
177
+
178
+ int type = sqlite3_value_type(value);
179
+ switch (type) {
180
+ case SQLITE_INTEGER:
181
+ return LL2NUM(sqlite3_value_int64(value));
182
+ case SQLITE_FLOAT:
183
+ return DBL2NUM(sqlite3_value_double(value));
184
+ case SQLITE_NULL:
185
+ return Qnil;
186
+ case SQLITE_BLOB:
187
+ {
188
+ int len = sqlite3_value_bytes(value);
189
+ void *blob = sqlite3_value_blob(value);
190
+ return rb_str_new(blob, len);
191
+ }
192
+ case SQLITE_TEXT:
193
+ {
194
+ int len = sqlite3_value_bytes(value);
195
+ void *text = sqlite3_value_text(value);
196
+ return rb_enc_str_new(text, len, UTF8_ENCODING);
197
+ }
198
+ default:
199
+ rb_raise(cError, "Invalid value type: %d\n", type);
200
+ }
201
+ }
202
+
203
+ VALUE changeset_iter_info(sqlite3_changeset_iter *iter) {
204
+ VALUE op = Qnil;
205
+ VALUE tbl = Qnil;
206
+ VALUE old_values = Qnil;
207
+ VALUE new_values = Qnil;
208
+ VALUE converted = Qnil;
209
+ VALUE row = rb_ary_new2(4);
210
+
211
+ const char *tbl_name;
212
+ int column_count;
213
+ int op_int;
214
+
215
+ int rc = sqlite3changeset_op(iter, &tbl_name, &column_count, &op_int, NULL);
216
+ if (rc != SQLITE_OK)
217
+ rb_raise(cError, "Error while iterating (sqlite3changeset_op): %s", sqlite3_errstr(rc));
218
+
219
+ op = op_symbol(op_int);
220
+ tbl = rb_str_new_cstr(tbl_name);
221
+
222
+ if (op_int == SQLITE_UPDATE || op_int == SQLITE_DELETE) {
223
+ sqlite3_value *value = NULL;
224
+ old_values = rb_ary_new2(column_count);
225
+ for (int i = 0; i < column_count; i++) {
226
+ rc = sqlite3changeset_old(iter, i, &value);
227
+ if (rc != SQLITE_OK)
228
+ rb_raise(cError, "Error while iterating (sqlite3changeset_old): %s", sqlite3_errstr(rc));
229
+ converted = convert_value(value);
230
+ rb_ary_push(old_values, converted);
231
+ }
232
+ }
233
+
234
+ if (op_int == SQLITE_UPDATE || op_int == SQLITE_INSERT) {
235
+ sqlite3_value *value = NULL;
236
+ new_values = rb_ary_new2(column_count);
237
+ for (int i = 0; i < column_count; i++) {
238
+ rc = sqlite3changeset_new(iter, i, &value);
239
+ if (rc != SQLITE_OK)
240
+ rb_raise(cError, "Error while iterating (sqlite3changeset_new): %s", sqlite3_errstr(rc));
241
+ converted = convert_value(value);
242
+ rb_ary_push(new_values, converted);
243
+ }
244
+ }
245
+
246
+ rb_ary_push(row, op);
247
+ rb_ary_push(row, tbl);
248
+ rb_ary_push(row, old_values);
249
+ rb_ary_push(row, new_values);
250
+
251
+ RB_GC_GUARD(op);
252
+ RB_GC_GUARD(tbl);
253
+ RB_GC_GUARD(old_values);
254
+ RB_GC_GUARD(new_values);
255
+ RB_GC_GUARD(converted);
256
+
257
+ return row;
258
+ }
259
+
260
+ VALUE safe_each(struct each_ctx *ctx) {
261
+ VALUE row = Qnil;
262
+ while (sqlite3changeset_next(ctx->iter) == SQLITE_ROW) {
263
+ row = changeset_iter_info(ctx->iter);
264
+ rb_yield_splat(row);
265
+ }
266
+
267
+ RB_GC_GUARD(row);
268
+ return Qnil;
269
+ }
270
+
271
+ VALUE safe_to_a(struct each_ctx *ctx) {
272
+ VALUE row = Qnil;
273
+ VALUE array = rb_ary_new();
274
+ while (sqlite3changeset_next(ctx->iter) == SQLITE_ROW) {
275
+ row = changeset_iter_info(ctx->iter);
276
+ rb_ary_push(array, row);
277
+ }
278
+
279
+ RB_GC_GUARD(row);
280
+ RB_GC_GUARD(array);
281
+ return array;
282
+ }
283
+
284
+ VALUE cleanup_iter(struct each_ctx *ctx) {
285
+ int rc = sqlite3changeset_finalize(ctx->iter);
286
+ if (rc != SQLITE_OK)
287
+ rb_raise(cError, "Error while finalizing changeset iterator: %s", sqlite3_errstr(rc));
288
+
289
+ return Qnil;
290
+ }
291
+
292
+ inline void verify_changeset(Changeset_t *changeset) {
293
+ if (!changeset->changeset_ptr)
294
+ rb_raise(cError, "Changeset not available");
295
+ }
296
+
297
+ /* Iterates through the changeset, providing each change to the given block.
298
+ * Each change entry is an array containing the operation (:insert / :update /
299
+ * :delete), the table name, an array containing the old values, and an array
300
+ * containing the new values.
301
+ *
302
+ * changeset.each do |(op, table, old_values, new_values)|
303
+ * ...
304
+ * end
305
+ *
306
+ * @return [Extralite::Changeset] changeset
307
+ */
308
+ VALUE Changeset_each(VALUE self) {
309
+ Changeset_t *changeset = self_to_changeset(self);
310
+ verify_changeset(changeset);
311
+
312
+ struct each_ctx ctx = { .iter = NULL };
313
+ int rc = sqlite3changeset_start(&ctx.iter, changeset->changeset_len, changeset->changeset_ptr);
314
+ if (rc!=SQLITE_OK)
315
+ rb_raise(cError, "Error while starting iterator: %s", sqlite3_errstr(rc));
316
+
317
+ rb_ensure(SAFE(safe_each), (VALUE)&ctx, SAFE(cleanup_iter), (VALUE)&ctx);
318
+ return self;
319
+ }
320
+
321
+ /* Returns an array containing all changes in the changeset. Each change entry
322
+ * is an array containing the operation (:insert / :update / :delete), the table
323
+ * name, an array containing the old values, and an array containing the new
324
+ * values.
325
+ *
326
+ * @return [Array<Array>] changes in the changeset
327
+ */
328
+ VALUE Changeset_to_a(VALUE self) {
329
+ Changeset_t *changeset = self_to_changeset(self);
330
+ verify_changeset(changeset);
331
+
332
+ struct each_ctx ctx = { .iter = NULL };
333
+ int rc = sqlite3changeset_start(&ctx.iter, changeset->changeset_len, changeset->changeset_ptr);
334
+ if (rc!=SQLITE_OK)
335
+ rb_raise(cError, "Error while starting iterator: %s", sqlite3_errstr(rc));
336
+
337
+ return rb_ensure(SAFE(safe_to_a), (VALUE)&ctx, SAFE(cleanup_iter), (VALUE)&ctx);
338
+ }
339
+
340
+ // copied from: https://sqlite.org/sessionintro.html
341
+ static int xConflict(void *pCtx, int eConflict, sqlite3_changeset_iter *pIter){
342
+ int ret = (long)pCtx;
343
+ return ret;
344
+ }
345
+
346
+ /* Applies the changeset to the given database.
347
+ *
348
+ * @param db [Extralite::Database] database to apply changes to
349
+ * @return [Extralite::Changeset] changeset
350
+ */
351
+ VALUE Changeset_apply(VALUE self, VALUE db) {
352
+ Changeset_t *changeset = self_to_changeset(self);
353
+ verify_changeset(changeset);
354
+
355
+ Database_t *db_struct = self_to_database(db);
356
+ sqlite3 *sqlite3_db = db_struct->sqlite3_db;
357
+
358
+ int rc = sqlite3changeset_apply(
359
+ sqlite3_db,
360
+ changeset->changeset_len,
361
+ changeset->changeset_ptr,
362
+ NULL,
363
+ xConflict,
364
+ (void*)1
365
+ );
366
+ if (rc != SQLITE_OK)
367
+ rb_raise(cError, "Error while applying changeset: %s", sqlite3_errstr(rc));
368
+
369
+ return self;
370
+ }
371
+
372
+ /* Returns an inverted changeset. The inverted changeset can be used to undo the
373
+ * changes in the original changeset.
374
+ *
375
+ * # undo changes
376
+ * changeset.invert.apply(db)
377
+ *
378
+ * @return [Extralite::Changeset] inverted changeset
379
+ */
380
+ VALUE Changeset_invert(VALUE self) {
381
+ Changeset_t *changeset = self_to_changeset(self);
382
+ verify_changeset(changeset);
383
+
384
+ VALUE inverted = rb_funcall(cChangeset, ID_new, 0);
385
+ Changeset_t *inverted_changeset = self_to_changeset(inverted);
386
+
387
+ int rc = sqlite3changeset_invert(
388
+ changeset->changeset_len, changeset->changeset_ptr,
389
+ &inverted_changeset->changeset_len, &inverted_changeset->changeset_ptr
390
+ );
391
+ if (rc != SQLITE_OK)
392
+ rb_raise(cError, "Error while inverting changeset: %s", sqlite3_errstr(rc));
393
+
394
+ RB_GC_GUARD(inverted);
395
+ return inverted;
396
+ }
397
+
398
+ /* Returns a string BLOB containing the changeset in serialized form. The
399
+ * changeset BLOB can be stored to file for later retrieval.
400
+ *
401
+ * File.open('my.changes', 'w+') { |f| f << changeset.to_blob }
402
+ *
403
+ * @return [String] changeset BLOB
404
+ */
405
+ VALUE Changeset_to_blob(VALUE self) {
406
+ Changeset_t *changeset = self_to_changeset(self);
407
+
408
+ if (changeset->changeset_ptr)
409
+ return rb_str_new(changeset->changeset_ptr, changeset->changeset_len);
410
+ else
411
+ return rb_str_new("", 0);
412
+ }
413
+
414
+ /* Loads a changeset from the given string. This method can be used to load a
415
+ * changeset from a file in order to apply it to a database.
416
+ *
417
+ * changeset = Extralite::Changeset.new
418
+ * changeset.load(IO.read('my.changes'))
419
+ * changeset.apply(db)
420
+ *
421
+ * @param blob [String] changeset BLOB
422
+ * @return [Extralite::Changeset] changeset
423
+ */
424
+ VALUE Changeset_load(VALUE self, VALUE blob) {
425
+ Changeset_t *changeset = self_to_changeset(self);
426
+ if (changeset->changeset_ptr) {
427
+ sqlite3_free(changeset->changeset_ptr);
428
+ changeset->changeset_ptr = NULL;
429
+ changeset->changeset_len = 0;
430
+ }
431
+
432
+ changeset->changeset_len = RSTRING_LEN(blob);
433
+ changeset->changeset_ptr = sqlite3_malloc(changeset->changeset_len);
434
+ memcpy(changeset->changeset_ptr, RSTRING_PTR(blob), changeset->changeset_len);
435
+
436
+ return self;
437
+ }
438
+
439
+ void Init_ExtraliteChangeset(void) {
440
+ VALUE mExtralite = rb_define_module("Extralite");
441
+
442
+ cChangeset = rb_define_class_under(mExtralite, "Changeset", rb_cObject);
443
+ rb_define_alloc_func(cChangeset, Changeset_allocate);
444
+
445
+ rb_define_method(cChangeset, "initialize", Changeset_initialize, 0);
446
+
447
+ rb_define_method(cChangeset, "apply", Changeset_apply, 1);
448
+ rb_define_method(cChangeset, "each", Changeset_each, 0);
449
+ rb_define_method(cChangeset, "invert", Changeset_invert, 0);
450
+ rb_define_method(cChangeset, "load", Changeset_load, 1);
451
+ rb_define_method(cChangeset, "to_a", Changeset_to_a, 0);
452
+ rb_define_method(cChangeset, "to_blob", Changeset_to_blob, 0);
453
+ rb_define_method(cChangeset, "track", Changeset_track, 2);
454
+
455
+ SYM_delete = ID2SYM(rb_intern("delete"));
456
+ SYM_insert = ID2SYM(rb_intern("insert"));
457
+ SYM_update = ID2SYM(rb_intern("update"));
458
+
459
+ rb_gc_register_mark_object(SYM_delete);
460
+ rb_gc_register_mark_object(SYM_insert);
461
+ rb_gc_register_mark_object(SYM_update);
462
+ }
463
+ #endif