extralite 2.15 → 3.0.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/.github/workflows/test-bundle.yml +1 -1
- data/.github/workflows/test.yml +1 -1
- data/CHANGELOG.md +10 -0
- data/README.md +106 -42
- data/TODO.md +2 -16
- data/examples/transform.rb +61 -0
- data/ext/extralite/changeset.c +11 -11
- data/ext/extralite/common.c +234 -22
- data/ext/extralite/database.c +157 -100
- data/ext/extralite/extralite.h +52 -6
- data/ext/extralite/extralite_ext.c +2 -0
- data/ext/extralite/query.c +67 -41
- data/ext/extralite/transform.c +420 -0
- data/gemspec.rb +1 -1
- data/lib/extralite/version.rb +1 -1
- data/lib/extralite.rb +102 -1
- data/test/perf_array.rb +1 -1
- data/test/perf_hash.rb +1 -1
- data/test/perf_hash_prepared.rb +2 -2
- data/test/perf_splat.rb +1 -1
- data/test/perf_transform.rb +58 -0
- data/test/test_database.rb +37 -10
- data/test/test_query.rb +11 -15
- data/test/test_transform.rb +817 -0
- metadata +6 -2
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
#include <stdio.h>
|
|
2
|
+
#include <stdlib.h>
|
|
3
|
+
#include <string.h>
|
|
4
|
+
#include "ruby.h"
|
|
5
|
+
#include "extralite.h"
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* Document-class: Extralite::Transform
|
|
9
|
+
*
|
|
10
|
+
* This class represents a specification for retrieving structured data as the
|
|
11
|
+
* result of a query. When querying data from multiple tables using joins,
|
|
12
|
+
* transforms allow you to retrieve the results as an object graph containing
|
|
13
|
+
* the different entities.
|
|
14
|
+
*
|
|
15
|
+
* For example, a posts table and a tags table may be joined to represent a
|
|
16
|
+
* many-to-many relationship:
|
|
17
|
+
*
|
|
18
|
+
* select
|
|
19
|
+
* posts.id, posts.content,
|
|
20
|
+
* tags.id, tags.name
|
|
21
|
+
* from posts
|
|
22
|
+
* left outer join posts_tags on posts_tags.post_id = posts.id
|
|
23
|
+
* left outer join tags on posts_tags.tag_id = tags.id
|
|
24
|
+
*
|
|
25
|
+
* Normally, the resulting data will be represented as an array of hashes, of
|
|
26
|
+
* the form:
|
|
27
|
+
*
|
|
28
|
+
* [
|
|
29
|
+
* { posts_id: 1, posts_content: "foo", tags_id: 1, tags_name: "blah" },
|
|
30
|
+
* { posts_id: 2, posts_content: "bar", tags_id: 1, tags_name: "blah" },
|
|
31
|
+
* { posts_id: 2, posts_content: "bar", tags_id: 2, tags_name: "bleh" },
|
|
32
|
+
* { posts_id: 3, posts_content: "baz", tags_id: 2, tags_name: "blah" },
|
|
33
|
+
* ...
|
|
34
|
+
* ]
|
|
35
|
+
*
|
|
36
|
+
* Those results, while containing all the information that we requested, also
|
|
37
|
+
* contain a lot of duplication: the same post entity may appear in multiple
|
|
38
|
+
* rows, and the same tag entity may be repeated for different posts.
|
|
39
|
+
*
|
|
40
|
+
* With a properly configured transform, we can convert those flat rows with
|
|
41
|
+
* duplicate data into an object graph that represents the different entities
|
|
42
|
+
* (posts and tags), such that each post entity will also include the
|
|
43
|
+
* corresponding tags. Furthermore, we can eliminate duplication by using
|
|
44
|
+
* identity maps to create each entity only once, and reuse it if it repeats
|
|
45
|
+
* (such as in the case of tags):
|
|
46
|
+
*
|
|
47
|
+
* [
|
|
48
|
+
* { id: 1, content: "foo", tags: [{id: 1, name: "blah"}] },
|
|
49
|
+
* { id: 2, content: "bar", tags: [{id: 1, name: "blah"}, {id: 2, name: "bleh"}] },
|
|
50
|
+
* { id: 3, content: "baz", tags: [{id: 2, name: "bleh"}] }
|
|
51
|
+
* ]
|
|
52
|
+
*
|
|
53
|
+
* The transform is expressed using the transform DSL:
|
|
54
|
+
*
|
|
55
|
+
* transform = Extralite::Transform.new do
|
|
56
|
+
* {
|
|
57
|
+
* id: integer.identity,
|
|
58
|
+
* content: text,
|
|
59
|
+
* tags: [{
|
|
60
|
+
* id: integer.identity,
|
|
61
|
+
* name: text
|
|
62
|
+
* }]
|
|
63
|
+
* }
|
|
64
|
+
* end
|
|
65
|
+
*
|
|
66
|
+
* To use the transform we can feed it into Database#query:
|
|
67
|
+
*
|
|
68
|
+
* db.query(transform, sql) #=> [...]
|
|
69
|
+
*
|
|
70
|
+
* A transform may also be used with a prepared query:
|
|
71
|
+
*
|
|
72
|
+
* q = db.prepare(transform, sql)
|
|
73
|
+
* q.to_a #=> [...]
|
|
74
|
+
*
|
|
75
|
+
* Transforms can also be used for type coercion. If the type is 'auto' or not
|
|
76
|
+
* specified, the returned value will reflect the native type of the value in
|
|
77
|
+
* the database. The following types are supported:
|
|
78
|
+
*
|
|
79
|
+
* - auto: native database type
|
|
80
|
+
* - integer: 64-bit integer
|
|
81
|
+
* - float: floating point number
|
|
82
|
+
* - text: text/string value
|
|
83
|
+
* - bool: a boolean (after coercion to integer)
|
|
84
|
+
* - json: parsed JSON representation
|
|
85
|
+
* - proc: a custom proc for converting a value
|
|
86
|
+
*
|
|
87
|
+
* To use the proc type, specify the proc as the type, e.g.:
|
|
88
|
+
*
|
|
89
|
+
* Extralite::Transform.new do
|
|
90
|
+
* {
|
|
91
|
+
* stamp: ->(s) { Time.at(s) },
|
|
92
|
+
* value: float
|
|
93
|
+
* }
|
|
94
|
+
* end
|
|
95
|
+
*/
|
|
96
|
+
|
|
97
|
+
VALUE cTransform;
|
|
98
|
+
VALUE mJSON;
|
|
99
|
+
|
|
100
|
+
VALUE SYM_bool;
|
|
101
|
+
VALUE SYM_columns;
|
|
102
|
+
VALUE SYM_float;
|
|
103
|
+
VALUE SYM_identity;
|
|
104
|
+
VALUE SYM_integer;
|
|
105
|
+
VALUE SYM_json;
|
|
106
|
+
VALUE SYM_name;
|
|
107
|
+
VALUE SYM_relation;
|
|
108
|
+
VALUE SYM_text;
|
|
109
|
+
VALUE SYM_type;
|
|
110
|
+
|
|
111
|
+
static size_t Transform_size(const void *ptr) {
|
|
112
|
+
return sizeof(Transform_t);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
static inline void transform_node_mark(struct transform_node *node) {
|
|
116
|
+
rb_gc_mark_movable(node->name);
|
|
117
|
+
if (node->type == TRANSFORM_T_PROC)
|
|
118
|
+
rb_gc_mark_movable(node->conversion_proc);
|
|
119
|
+
|
|
120
|
+
struct transform_node *cur = node->subnodes_head;
|
|
121
|
+
while (cur) {
|
|
122
|
+
transform_node_mark(cur);
|
|
123
|
+
cur = cur->next;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (node->next) transform_node_mark(node->next);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
static void Transform_mark(void *ptr) {
|
|
130
|
+
Transform_t *t = ptr;
|
|
131
|
+
if (t->root) transform_node_mark(t->root);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
static inline void transform_node_compact(struct transform_node *node) {
|
|
135
|
+
if (node->flags & TRANSFORM_F_NAME)
|
|
136
|
+
node->name = rb_gc_location(node->name);
|
|
137
|
+
|
|
138
|
+
if (node->type == TRANSFORM_T_PROC)
|
|
139
|
+
node->conversion_proc = rb_gc_location(node->conversion_proc);
|
|
140
|
+
|
|
141
|
+
struct transform_node *cur = node->subnodes_head;
|
|
142
|
+
while (cur) {
|
|
143
|
+
transform_node_compact(cur);
|
|
144
|
+
cur = cur->next;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (node->next) transform_node_compact(node->next);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
static void Transform_compact(void *ptr) {
|
|
151
|
+
Transform_t *t = ptr;
|
|
152
|
+
if (t->root) transform_node_compact(t->root);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
static inline void transform_node_free(struct transform_node *node) {
|
|
156
|
+
if (node->subnodes_head)
|
|
157
|
+
transform_node_free(node->subnodes_head);
|
|
158
|
+
if (node->next)
|
|
159
|
+
transform_node_free(node->next);
|
|
160
|
+
|
|
161
|
+
free(node);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
static void Transform_free(void *ptr) {
|
|
165
|
+
Transform_t *t = ptr;
|
|
166
|
+
if (t->root) transform_node_free(t->root);
|
|
167
|
+
free(ptr);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
static const rb_data_type_t Transform_type = {
|
|
171
|
+
"Transform",
|
|
172
|
+
{Transform_mark, Transform_free, Transform_size, Transform_compact},
|
|
173
|
+
0, 0, RUBY_TYPED_FREE_IMMEDIATELY
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
static VALUE Transform_allocate(VALUE klass) {
|
|
177
|
+
Transform_t *t = ALLOC(Transform_t);
|
|
178
|
+
t->root = NULL;
|
|
179
|
+
return TypedData_Wrap_Struct(klass, &Transform_type, t);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
static inline Transform_t *self_to_transform(VALUE self) {
|
|
183
|
+
Transform_t *t;
|
|
184
|
+
TypedData_Get_Struct(self, Transform_t, &Transform_type, t);
|
|
185
|
+
return t;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
struct transform_node *get_transform_root(VALUE obj) {
|
|
189
|
+
return self_to_transform(obj)->root;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
inline struct transform_node *allocate_transform_node() {
|
|
193
|
+
struct transform_node *node = malloc(sizeof(struct transform_node));
|
|
194
|
+
memset(node, 0, sizeof(struct transform_node));
|
|
195
|
+
node->name = Qnil;
|
|
196
|
+
node->conversion_proc = Qnil;
|
|
197
|
+
return node;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
struct transform_node *compile_transform_relation(VALUE spec, int *col_counter);
|
|
201
|
+
|
|
202
|
+
enum transform_node_type get_node_type(VALUE col_hash, VALUE *proc) {
|
|
203
|
+
VALUE type = rb_hash_aref(col_hash, SYM_type);
|
|
204
|
+
if (NIL_P(type)) return TRANSFORM_T_AUTO;
|
|
205
|
+
if (type == SYM_integer) return TRANSFORM_T_INTEGER;
|
|
206
|
+
if (type == SYM_float) return TRANSFORM_T_FLOAT;
|
|
207
|
+
if (type == SYM_text) return TRANSFORM_T_TEXT;
|
|
208
|
+
if (type == SYM_bool) return TRANSFORM_T_BOOL;
|
|
209
|
+
if (type == SYM_json) return TRANSFORM_T_JSON;
|
|
210
|
+
if (type == SYM_relation) return TRANSFORM_T_RELATION;
|
|
211
|
+
|
|
212
|
+
if (TYPE(type) == T_DATA) {
|
|
213
|
+
*proc = type;
|
|
214
|
+
return TRANSFORM_T_PROC;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
rb_raise(cError, "Invalid column type");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
struct transform_node *compile_transform_column(VALUE col, int *col_counter) {
|
|
221
|
+
// In the PORO spec, a multi-row (array) relation is expressed as:
|
|
222
|
+
//
|
|
223
|
+
// spec = {
|
|
224
|
+
// columns: {
|
|
225
|
+
// **,
|
|
226
|
+
// tags: [{
|
|
227
|
+
// type: :relation,
|
|
228
|
+
// columns: {**}
|
|
229
|
+
// }]
|
|
230
|
+
// }
|
|
231
|
+
// }
|
|
232
|
+
//
|
|
233
|
+
// (Note the literal array surrounding the tags entry.) So we check for it
|
|
234
|
+
// here, it's also checked in compile_transform_relation.
|
|
235
|
+
if (TYPE(col) == T_ARRAY) goto relation_node;
|
|
236
|
+
if (TYPE(col) != T_HASH)
|
|
237
|
+
rb_raise(cError, "Each column must be a hash");
|
|
238
|
+
|
|
239
|
+
VALUE proc = Qnil;
|
|
240
|
+
enum transform_node_type node_type = get_node_type(col, &proc);
|
|
241
|
+
if (node_type == TRANSFORM_T_RELATION) goto relation_node;
|
|
242
|
+
|
|
243
|
+
struct transform_node *node = allocate_transform_node();
|
|
244
|
+
node->type = node_type;
|
|
245
|
+
node->idx = *col_counter;
|
|
246
|
+
|
|
247
|
+
VALUE identity = rb_hash_aref(col, SYM_identity);
|
|
248
|
+
if (RTEST(identity)) node->flags |= TRANSFORM_F_IDENTITY;
|
|
249
|
+
|
|
250
|
+
if (node_type == TRANSFORM_T_PROC) node->conversion_proc = proc;
|
|
251
|
+
|
|
252
|
+
(*col_counter)++;
|
|
253
|
+
RB_GC_GUARD(proc);
|
|
254
|
+
return node;
|
|
255
|
+
|
|
256
|
+
relation_node:
|
|
257
|
+
return compile_transform_relation(col, col_counter);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
struct column_iterator_ctx {
|
|
261
|
+
VALUE spec;
|
|
262
|
+
int *col_counter;
|
|
263
|
+
struct transform_node *node;
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
int column_iterator(VALUE name, VALUE col, VALUE arg) {
|
|
267
|
+
struct column_iterator_ctx *ctx = (struct column_iterator_ctx *)arg;
|
|
268
|
+
struct transform_node *node = ctx->node;
|
|
269
|
+
|
|
270
|
+
int col_idx = *(ctx->col_counter);
|
|
271
|
+
struct transform_node *col_node = compile_transform_column(col, ctx->col_counter);
|
|
272
|
+
col_node->flags |= TRANSFORM_F_NAME;
|
|
273
|
+
col_node->name = name;
|
|
274
|
+
|
|
275
|
+
if (col_node->flags & TRANSFORM_F_IDENTITY) {
|
|
276
|
+
node->identity_node = col_node;
|
|
277
|
+
node->identity_idx = col_idx;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (node->subnodes_tail) {
|
|
281
|
+
node->subnodes_tail->next = col_node;
|
|
282
|
+
node->subnodes_tail = col_node;
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
node->subnodes_head = node->subnodes_tail = col_node;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return ST_CONTINUE;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
struct transform_node *compile_transform_relation(VALUE spec, int *col_counter) {
|
|
292
|
+
struct transform_node *node = allocate_transform_node();
|
|
293
|
+
node->type = TRANSFORM_T_RELATION;
|
|
294
|
+
|
|
295
|
+
if (TYPE(spec) == T_ARRAY) {
|
|
296
|
+
node->flags |= TRANSFORM_F_ARRAY;
|
|
297
|
+
spec = rb_ary_entry(spec, 0);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
VALUE val = rb_hash_aref(spec, SYM_columns);
|
|
301
|
+
if (TYPE(val) != T_HASH)
|
|
302
|
+
rb_raise(cError, "columns member must be a hash");
|
|
303
|
+
|
|
304
|
+
struct column_iterator_ctx ctx = { spec, col_counter, node };
|
|
305
|
+
rb_hash_foreach(val, column_iterator, (VALUE)&ctx);
|
|
306
|
+
return node;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
VALUE Transform_initialize(VALUE self, VALUE spec) {
|
|
310
|
+
Transform_t *t = self_to_transform(self);
|
|
311
|
+
int col_counter = 0;
|
|
312
|
+
t->root = compile_transform_relation(spec, &col_counter);
|
|
313
|
+
return self;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
VALUE node_type_to_value(enum transform_node_type type) {
|
|
317
|
+
switch (type) {
|
|
318
|
+
case TRANSFORM_T_AUTO: return Qnil;
|
|
319
|
+
case TRANSFORM_T_INTEGER: return SYM_integer;
|
|
320
|
+
case TRANSFORM_T_FLOAT: return SYM_float;
|
|
321
|
+
case TRANSFORM_T_TEXT: return SYM_text;
|
|
322
|
+
case TRANSFORM_T_BOOL: return SYM_bool;
|
|
323
|
+
case TRANSFORM_T_JSON: return SYM_json;
|
|
324
|
+
default:
|
|
325
|
+
rb_raise(cError, "Invalid node type in node_type_to_value");
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
VALUE transform_node_to_obj(struct transform_node *node) {
|
|
330
|
+
// printf("transform_node_to_obj type: %d flags: %02x\n", node->type, node->flags);
|
|
331
|
+
// if (node->flags & TRANSFORM_F_NAME) INSPECT(" name", node->name);
|
|
332
|
+
VALUE hash = rb_hash_new();
|
|
333
|
+
if (node->type == TRANSFORM_T_RELATION) {
|
|
334
|
+
VALUE cols = rb_hash_new();
|
|
335
|
+
|
|
336
|
+
if (node->flags & TRANSFORM_F_NAME)
|
|
337
|
+
rb_hash_aset(hash, SYM_type, SYM_relation);
|
|
338
|
+
rb_hash_aset(hash, SYM_columns, cols);
|
|
339
|
+
|
|
340
|
+
struct transform_node *cur = node->subnodes_head;
|
|
341
|
+
while (cur) {
|
|
342
|
+
struct transform_node *next = cur->next;
|
|
343
|
+
VALUE val = transform_node_to_obj(cur);
|
|
344
|
+
rb_hash_aset(cols, cur->name, val);
|
|
345
|
+
RB_GC_GUARD(val);
|
|
346
|
+
|
|
347
|
+
cur = next;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (node->flags & TRANSFORM_F_ARRAY) {
|
|
351
|
+
VALUE array = rb_ary_new();
|
|
352
|
+
rb_ary_push(array, hash);
|
|
353
|
+
return array;
|
|
354
|
+
RB_GC_GUARD(array);
|
|
355
|
+
}
|
|
356
|
+
else
|
|
357
|
+
return hash;
|
|
358
|
+
RB_GC_GUARD(cols);
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
switch (node->type) {
|
|
362
|
+
case TRANSFORM_T_AUTO:
|
|
363
|
+
break;
|
|
364
|
+
case TRANSFORM_T_PROC:
|
|
365
|
+
rb_hash_aset(hash, SYM_type, node->conversion_proc);
|
|
366
|
+
break;
|
|
367
|
+
default:
|
|
368
|
+
VALUE v = node_type_to_value(node->type);
|
|
369
|
+
RB_GC_GUARD(v);
|
|
370
|
+
rb_hash_aset(hash, SYM_type, node_type_to_value(node->type));
|
|
371
|
+
}
|
|
372
|
+
if (node->flags & TRANSFORM_F_IDENTITY)
|
|
373
|
+
rb_hash_aset(hash, SYM_identity, Qtrue);
|
|
374
|
+
return hash;
|
|
375
|
+
}
|
|
376
|
+
RB_GC_GUARD(hash);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/* Returns the transform spec in literal form.
|
|
380
|
+
*
|
|
381
|
+
* @return [Hash] literal transform spec
|
|
382
|
+
*/
|
|
383
|
+
VALUE Transform_to_h(VALUE self) {
|
|
384
|
+
Transform_t *t = self_to_transform(self);
|
|
385
|
+
return transform_node_to_obj(t->root);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
void Init_ExtraliteTransform(void) {
|
|
389
|
+
VALUE mExtralite = rb_define_module("Extralite");
|
|
390
|
+
|
|
391
|
+
cTransform = rb_define_class_under(mExtralite, "Transform", rb_cObject);
|
|
392
|
+
rb_define_alloc_func(cTransform, Transform_allocate);
|
|
393
|
+
|
|
394
|
+
rb_define_method(cTransform, "initialize", Transform_initialize, 1);
|
|
395
|
+
rb_define_method(cTransform, "to_h", Transform_to_h, 0);
|
|
396
|
+
|
|
397
|
+
SYM_bool = ID2SYM(rb_intern_const("bool"));
|
|
398
|
+
SYM_columns = ID2SYM(rb_intern_const("columns"));
|
|
399
|
+
SYM_float = ID2SYM(rb_intern_const("float"));
|
|
400
|
+
SYM_identity = ID2SYM(rb_intern_const("identity"));
|
|
401
|
+
SYM_integer = ID2SYM(rb_intern_const("integer"));
|
|
402
|
+
SYM_json = ID2SYM(rb_intern_const("json"));
|
|
403
|
+
SYM_name = ID2SYM(rb_intern_const("name"));
|
|
404
|
+
SYM_relation = ID2SYM(rb_intern_const("relation"));
|
|
405
|
+
SYM_text = ID2SYM(rb_intern_const("text"));
|
|
406
|
+
SYM_type = ID2SYM(rb_intern_const("type"));
|
|
407
|
+
|
|
408
|
+
rb_gc_register_mark_object(SYM_bool);
|
|
409
|
+
rb_gc_register_mark_object(SYM_columns);
|
|
410
|
+
rb_gc_register_mark_object(SYM_float);
|
|
411
|
+
rb_gc_register_mark_object(SYM_identity);
|
|
412
|
+
rb_gc_register_mark_object(SYM_integer);
|
|
413
|
+
rb_gc_register_mark_object(SYM_json);
|
|
414
|
+
rb_gc_register_mark_object(SYM_name);
|
|
415
|
+
rb_gc_register_mark_object(SYM_relation);
|
|
416
|
+
rb_gc_register_mark_object(SYM_text);
|
|
417
|
+
rb_gc_register_mark_object(SYM_type);
|
|
418
|
+
|
|
419
|
+
mJSON = Qnil;
|
|
420
|
+
}
|
data/gemspec.rb
CHANGED
|
@@ -15,7 +15,7 @@ def common_spec(s)
|
|
|
15
15
|
s.rdoc_options = ['--title', 'Extralite', '--main', 'README.md']
|
|
16
16
|
s.extra_rdoc_files = ['README.md']
|
|
17
17
|
s.require_paths = ['lib']
|
|
18
|
-
s.required_ruby_version = '>= 3.
|
|
18
|
+
s.required_ruby_version = '>= 3.4'
|
|
19
19
|
|
|
20
20
|
s.add_development_dependency 'rake-compiler', '1.3.1'
|
|
21
21
|
s.add_development_dependency 'minitest'
|
data/lib/extralite/version.rb
CHANGED
data/lib/extralite.rb
CHANGED
|
@@ -236,7 +236,7 @@ module Extralite
|
|
|
236
236
|
# @return [Any] the given block's return value
|
|
237
237
|
def transaction(mode = :immediate)
|
|
238
238
|
abort = false
|
|
239
|
-
execute "begin #{mode} transaction"
|
|
239
|
+
execute "begin #{mode} transaction"
|
|
240
240
|
yield self
|
|
241
241
|
rescue => e
|
|
242
242
|
abort = true
|
|
@@ -296,6 +296,10 @@ module Extralite
|
|
|
296
296
|
raise Rollback
|
|
297
297
|
end
|
|
298
298
|
|
|
299
|
+
def quote(str)
|
|
300
|
+
str.gsub("'", "''")
|
|
301
|
+
end
|
|
302
|
+
|
|
299
303
|
private
|
|
300
304
|
|
|
301
305
|
def pragma_set(values)
|
|
@@ -311,4 +315,101 @@ module Extralite
|
|
|
311
315
|
class Query
|
|
312
316
|
alias_method :execute_multi, :batch_execute
|
|
313
317
|
end
|
|
318
|
+
|
|
319
|
+
class Transform
|
|
320
|
+
alias_method :orig_initialize, :initialize
|
|
321
|
+
|
|
322
|
+
# call-seq:
|
|
323
|
+
# Extralite::Transform.new(literal_spec) -> transform
|
|
324
|
+
# Extralite::Transform.new { dsl_spec } -> transform
|
|
325
|
+
#
|
|
326
|
+
# Initializes a new transform with with the given spec. The spec may be
|
|
327
|
+
# expressed as a hash containing the columns and any nested entities, or as a
|
|
328
|
+
# block with the transform DSL.
|
|
329
|
+
#
|
|
330
|
+
# # literal transform spec
|
|
331
|
+
# transform = Extralite::Transform.new(
|
|
332
|
+
# columns: {
|
|
333
|
+
# id: { type: :integer, identity: true },
|
|
334
|
+
# content: { type: :text },
|
|
335
|
+
# author: {
|
|
336
|
+
# type: :relation,
|
|
337
|
+
# columns: {
|
|
338
|
+
# id: { type: :integer, identity: true },
|
|
339
|
+
# name: { type: :text },
|
|
340
|
+
# }
|
|
341
|
+
# }
|
|
342
|
+
# }
|
|
343
|
+
# )
|
|
344
|
+
#
|
|
345
|
+
# # DSL spec
|
|
346
|
+
# transform = Extralite::Transform.new do
|
|
347
|
+
# {
|
|
348
|
+
# id: integer.identity,
|
|
349
|
+
# content: text,
|
|
350
|
+
# author: {
|
|
351
|
+
# id: integer.identity,
|
|
352
|
+
# name: text
|
|
353
|
+
# }
|
|
354
|
+
# }
|
|
355
|
+
# end
|
|
356
|
+
#
|
|
357
|
+
# @param spec [Hash, nil] literal spec
|
|
358
|
+
# @param block [Proc, nil] DSL spec
|
|
359
|
+
# @return [void]
|
|
360
|
+
def initialize(spec = nil, &block)
|
|
361
|
+
return orig_initialize(spec) if spec
|
|
362
|
+
raise "No spec given" if !block
|
|
363
|
+
|
|
364
|
+
orig_initialize(dsl_to_spec(block))
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
private
|
|
368
|
+
|
|
369
|
+
class DSLContext
|
|
370
|
+
attr_reader :hash
|
|
371
|
+
|
|
372
|
+
def initialize(hash = {})
|
|
373
|
+
@hash = hash
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def auto = mutate(type: nil)
|
|
377
|
+
def integer = mutate(type: :integer)
|
|
378
|
+
def float = mutate(type: :float)
|
|
379
|
+
def text = mutate(type: :text)
|
|
380
|
+
def bool = mutate(type: :bool)
|
|
381
|
+
def json = mutate(type: :json)
|
|
382
|
+
def identity = mutate(identity: true)
|
|
383
|
+
|
|
384
|
+
private
|
|
385
|
+
def mutate(opts)
|
|
386
|
+
DSLContext.new(@hash.merge(opts))
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def dsl_to_spec(block)
|
|
391
|
+
intermediate = DSLContext.new.instance_eval(&block)
|
|
392
|
+
dsl_intermediate_to_spec(intermediate)
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def dsl_intermediate_to_spec(intermediate)
|
|
396
|
+
columns = intermediate.each_with_object({}) do |(k, v), h|
|
|
397
|
+
h[k] = dsl_translate_intermediate_value(v)
|
|
398
|
+
end
|
|
399
|
+
{ type: :relation, columns: }
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def dsl_translate_intermediate_value(v)
|
|
403
|
+
case v
|
|
404
|
+
when DSLContext
|
|
405
|
+
v.hash
|
|
406
|
+
when Hash
|
|
407
|
+
dsl_intermediate_to_spec(v)
|
|
408
|
+
when Array
|
|
409
|
+
[dsl_intermediate_to_spec(v[0])]
|
|
410
|
+
else
|
|
411
|
+
raise Extralite::Error, 'Invalid transform spec'
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
end
|
|
314
415
|
end
|
data/test/perf_array.rb
CHANGED
data/test/perf_hash.rb
CHANGED
data/test/perf_hash_prepared.rb
CHANGED
|
@@ -5,7 +5,7 @@ require 'bundler/inline'
|
|
|
5
5
|
gemfile do
|
|
6
6
|
source 'https://rubygems.org'
|
|
7
7
|
gem 'extralite', path: '..'
|
|
8
|
-
gem 'sqlite3', '2.
|
|
8
|
+
gem 'sqlite3', '2.9.5'
|
|
9
9
|
gem 'benchmark-ips'
|
|
10
10
|
end
|
|
11
11
|
|
|
@@ -18,7 +18,7 @@ puts "DB_PATH = #{DB_PATH.inspect}"
|
|
|
18
18
|
def prepare_database(count)
|
|
19
19
|
$sqlite3_db = SQLite3::Database.new(DB_PATH, results_as_hash: true)
|
|
20
20
|
$extralite_db = Extralite::Database.new(DB_PATH, gvl_release_threshold: -1)
|
|
21
|
-
|
|
21
|
+
|
|
22
22
|
$extralite_db.query('create table if not exists foo ( a integer primary key, b text )')
|
|
23
23
|
$extralite_db.query('delete from foo')
|
|
24
24
|
$extralite_db.query('begin')
|
data/test/perf_splat.rb
CHANGED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Run on Ruby 3.3 with YJIT enabled
|
|
4
|
+
|
|
5
|
+
require 'bundler/inline'
|
|
6
|
+
|
|
7
|
+
gemfile do
|
|
8
|
+
source 'https://rubygems.org'
|
|
9
|
+
gem 'extralite', path: '..'
|
|
10
|
+
gem 'benchmark-ips'
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
require 'benchmark/ips'
|
|
14
|
+
require 'fileutils'
|
|
15
|
+
|
|
16
|
+
DB_PATH = "/tmp/extralite_sqlite3_perf-#{Time.now.to_i}-#{rand(10000)}.db"
|
|
17
|
+
puts "DB_PATH = #{DB_PATH.inspect}"
|
|
18
|
+
|
|
19
|
+
$extralite_db = Extralite::Database.new(DB_PATH, gvl_release_threshold: -1)
|
|
20
|
+
|
|
21
|
+
def prepare_database(count)
|
|
22
|
+
$extralite_db.query('create table if not exists foo ( a integer primary key, b text )')
|
|
23
|
+
$extralite_db.query('delete from foo')
|
|
24
|
+
$extralite_db.query('begin')
|
|
25
|
+
count.times { $extralite_db.query('insert into foo (b) values (?)', "hello#{rand(1000)}" )}
|
|
26
|
+
$extralite_db.query('commit')
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def run_normal(count)
|
|
30
|
+
results = $extralite_db.query('select * from foo')
|
|
31
|
+
raise unless results.size == count
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
@transform = Extralite::Transform.new {
|
|
35
|
+
{ a: integer, b: text }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
def run_transform(count)
|
|
39
|
+
results = $extralite_db.query(@transform, 'select * from foo')
|
|
40
|
+
raise unless results.size == count
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
[10, 1000, 100000].each do |c|
|
|
44
|
+
puts "Record count: #{c}"
|
|
45
|
+
prepare_database(c)
|
|
46
|
+
|
|
47
|
+
bm = Benchmark.ips do |x|
|
|
48
|
+
x.config(:time => 5, :warmup => 2)
|
|
49
|
+
|
|
50
|
+
x.report("normal") { run_normal(c) }
|
|
51
|
+
x.report("transform") { run_transform(c) }
|
|
52
|
+
|
|
53
|
+
x.compare!
|
|
54
|
+
end
|
|
55
|
+
puts;
|
|
56
|
+
bm.entries.each { |e| puts "#{e.label}: #{(e.ips * c).round.to_i} rows/s" }
|
|
57
|
+
puts;
|
|
58
|
+
end
|