extralite 2.5 → 2.6

Sign up to get free protection for your applications and to get access to all the features.
data/TODO.md CHANGED
@@ -1,5 +1,7 @@
1
1
  - Transactions and savepoints:
2
2
 
3
+ - https://www.sqlite.org/lang_savepoint.html
4
+
3
5
  - `DB#savepoint(name)` - creates a savepoint
4
6
  - `DB#release(name)` - releases a savepoint
5
7
  - `DB#rollback` - raises `Extralite::Rollback`, which is rescued by `DB#transaction`
@@ -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
@@ -205,9 +205,9 @@ statements. It will release the GVL while the statements are being prepared and
205
205
  executed. All statements excluding the last one are executed. The last statement
206
206
  is not executed, but instead handed back to the caller for looping over results.
207
207
  */
208
- void prepare_multi_stmt(sqlite3 *db, sqlite3_stmt **stmt, VALUE sql) {
208
+ void prepare_multi_stmt(enum gvl_mode mode, sqlite3 *db, sqlite3_stmt **stmt, VALUE sql) {
209
209
  prepare_stmt_ctx ctx = {db, stmt, RSTRING_PTR(sql), RSTRING_LEN(sql), 0};
210
- gvl_call(GVL_RELEASE, prepare_multi_stmt_impl, (void *)&ctx);
210
+ gvl_call(mode, prepare_multi_stmt_impl, (void *)&ctx);
211
211
  RB_GC_GUARD(sql);
212
212
 
213
213
  switch (ctx.rc) {
@@ -247,9 +247,9 @@ end:
247
247
  return NULL;
248
248
  }
249
249
 
250
- void prepare_single_stmt(sqlite3 *db, sqlite3_stmt **stmt, VALUE sql) {
250
+ void prepare_single_stmt(enum gvl_mode mode, sqlite3 *db, sqlite3_stmt **stmt, VALUE sql) {
251
251
  prepare_stmt_ctx ctx = {db, stmt, RSTRING_PTR(sql), RSTRING_LEN(sql), 0};
252
- gvl_call(GVL_RELEASE, prepare_single_stmt_impl, (void *)&ctx);
252
+ gvl_call(mode, prepare_single_stmt_impl, (void *)&ctx);
253
253
  RB_GC_GUARD(sql);
254
254
 
255
255
  switch (ctx.rc) {