ilios 1.0.6 → 1.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ae1db76d702cb217eb8145f498dd86044183e441cbd896ac6acfd0d118823d13
4
- data.tar.gz: 5e5e141d2578bdfd5c2b39b8b7e5b72e5a65de824a4e5868c3388e3308973d8c
3
+ metadata.gz: 35886070e9a98cd40ab1628296e5f2a1cf50fcd5f8429ade4d79eeb0a363cf29
4
+ data.tar.gz: e6b9d75e242db36c5fc96774fbf2dd31a135b3702ccaedc57e3efb07cd1fa22b
5
5
  SHA512:
6
- metadata.gz: 2c78135d42da8f1c532c80531d0a493fed7f4bcf96ddf2d8aabdafac649740cb819a6f038640031993fe33fdc82c245e9725868f71149751aaaadf9ca3710d0b
7
- data.tar.gz: 26771b46cf175596fdd14a19873783c151f3a1b02798ca43fa39b7828b8519a3316bbcafbc93a291d9c44cadb95d562c1e7fc6d30b19e48e2efb28592c72c674
6
+ metadata.gz: 2219b69eef45cc7977f79354bf4e03fc8e7583f59ed91dc4290653a0e1a0ea669d9b5dd1225b8cb043203824d3c6d4bf5294baee658e3f07d8ac04be87422c3c
7
+ data.tar.gz: 7e7919c71dd7d876f84331ad33f2b66a9c0831bd5b342eaf766a6e6794e018183b1ba48e798eebb1305480031464c426c11ee79ec490d2d5fec56d826239cf14
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Change Log
2
2
 
3
+ ## 1.1.0
4
+
5
+ - Support Cassandra collection types (`list`, `set` and `map`, including nested collections). `list` maps to `Array`, `set` to `Set`, `map` to `Hash` (#28)
6
+ - Accept `Symbol` values for `text`/`ascii`/`varchar` columns and collection elements (#28)
7
+ - Require Ruby 3.4 or later (#28)
8
+ - Fix `Ilios::Cassandra::Future#await` returning before the registered callback ran (PR #29)
9
+
3
10
  ## 1.0.6
4
11
 
5
12
  - Fix uninitialized memory being returned for an undecodable column value (#27)
data/Gemfile CHANGED
@@ -15,4 +15,4 @@ gem 'rubocop-minitest', '~> 0.33'
15
15
  gem 'rubocop-performance', '~> 1.19'
16
16
  gem 'rubocop-rake', '~> 0.6'
17
17
 
18
- gem 'rubocop-on-rbs', '~> 1.5' if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.2')
18
+ gem 'rubocop-on-rbs', '~> 1.5'
data/README.md CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/ilios.svg)](https://badge.fury.io/rb/ilios)
4
4
  [![CI](https://github.com/Watson1978/ilios/actions/workflows/CI.yml/badge.svg)](https://github.com/Watson1978/ilios/actions/workflows/CI.yml)
5
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/Watson1978/ilios)
5
6
 
6
7
  Ilios that Cassandra driver written by C language for Ruby using [DataStax C/C++ Driver](https://github.com/datastax/cpp-driver).
7
8
 
@@ -42,7 +43,7 @@ $ gem install ilios -- --with-libuv-dir=/path/to/libuv-installed-dir --with-cass
42
43
 
43
44
  ## Supported
44
45
 
45
- - Ruby 3.1 or later
46
+ - Ruby 3.4 or later
46
47
  - Cassandra 3.0 or later
47
48
  - Linux and macOS platform
48
49
 
@@ -117,6 +118,59 @@ while(result.next_page)
117
118
  end
118
119
  ```
119
120
 
121
+ ### Collection types
122
+
123
+ `list`, `set` and `map` columns (including nested collections such as
124
+ `list<frozen<list<int>>>`) are supported.
125
+
126
+ ```ruby
127
+ statement = session.prepare(<<~CQL)
128
+ CREATE TABLE IF NOT EXISTS ilios.collection_example (
129
+ id bigint,
130
+ tags set<text>,
131
+ scores list<int>,
132
+ attributes map<text, bigint>,
133
+ PRIMARY KEY (id)
134
+ );
135
+ CQL
136
+ session.execute(statement)
137
+
138
+ statement = session.prepare(<<~CQL)
139
+ INSERT INTO ilios.collection_example (
140
+ id,
141
+ tags,
142
+ scores,
143
+ attributes
144
+ ) VALUES (?, ?, ?, ?)
145
+ CQL
146
+ statement.bind({
147
+ id: 1,
148
+ tags: Set['ruby', 'cassandra'], # or an Array
149
+ scores: [85, 92],
150
+ attributes: { 'height' => 180 },
151
+ })
152
+ session.execute(statement)
153
+
154
+ statement = session.prepare(<<~CQL)
155
+ SELECT * FROM ilios.collection_example
156
+ CQL
157
+ session.execute(statement).each do |row|
158
+ row['tags'] # => Set["cassandra", "ruby"]
159
+ row['scores'] # => [85, 92]
160
+ row['attributes'] # => {"height" => 180}
161
+ end
162
+ ```
163
+
164
+ Notes:
165
+
166
+ - A `set` column accepts both `Set` and `Array` on bind, and is always returned as a `Set`.
167
+ - A `list` column also accepts both `Array` and `Set` on bind, but is always returned as an `Array`. Binding a `Set` to a `list` column keeps the order `Set#to_a` returns.
168
+ - Cassandra stores an empty non-frozen collection as `null`, so inserting `[]`, `Set.new` or `{}` returns `nil` on select. This is Cassandra's data model, not an Ilios limitation.
169
+ - `nil` is not allowed as a collection element (Cassandra collections cannot contain `null`).
170
+ - `Symbol` is accepted for `text` (as well as `ascii` and `varchar`) columns and collection elements, and is stored (and returned) as a `String`.
171
+ - Because a `Symbol` map key is stored as its `String` equivalent, binding a map that contains both (for example `{ k1: 1, 'k1' => 2 }`) ends up as a single key on the server; the entry bound last wins.
172
+ - Binding a `String` containing a NUL character (`\0`) to a `text` (or `ascii` / `varchar`) column raises `ArgumentError` (known limitation).
173
+
120
174
  ### Synchronous API
121
175
  `Ilios::Cassandra::Session#prepare` and `Ilios::Cassandra::Session#execute` are provided as synchronous API.
122
176
 
data/ext/ilios/future.c CHANGED
@@ -213,6 +213,7 @@ VALUE future_create(CassFuture *future, VALUE session, VALUE statement, future_k
213
213
  uv_sem_init(&cassandra_future->sem, 0);
214
214
  cassandra_future->already_waited = false;
215
215
  cassandra_future->yielded = false;
216
+ cassandra_future->queued = false;
216
217
 
217
218
  return cassandra_future_obj;
218
219
  }
@@ -232,7 +233,11 @@ static VALUE future_on_success_synchronize(VALUE future)
232
233
  RB_OBJ_WRITE(future, &cassandra_future->on_success_block, rb_block_proc());
233
234
 
234
235
  if (cass_future_ready(cassandra_future->future)) {
235
- uv_sem_post(&cassandra_future->sem);
236
+ // The pool thread posts the semaphore itself once it has yielded;
237
+ // posting here too would let #await return before that happens.
238
+ if (!cassandra_future->queued) {
239
+ uv_sem_post(&cassandra_future->sem);
240
+ }
236
241
  if (!cassandra_future->yielded &&
237
242
  cass_future_error_code(cassandra_future->future) == CASS_OK) {
238
243
  cassandra_future->yielded = true;
@@ -242,6 +247,7 @@ static VALUE future_on_success_synchronize(VALUE future)
242
247
  }
243
248
 
244
249
  if (wakeup_thread) {
250
+ cassandra_future->queued = true;
245
251
  future_queue_push(future_thread_pool_get(cassandra_future), future);
246
252
  }
247
253
 
@@ -289,7 +295,9 @@ static VALUE future_on_failure_synchronize(VALUE future)
289
295
  RB_OBJ_WRITE(future, &cassandra_future->on_failure_block, rb_block_proc());
290
296
 
291
297
  if (cass_future_ready(cassandra_future->future)) {
292
- uv_sem_post(&cassandra_future->sem);
298
+ if (!cassandra_future->queued) {
299
+ uv_sem_post(&cassandra_future->sem);
300
+ }
293
301
  if (!cassandra_future->yielded &&
294
302
  cass_future_error_code(cassandra_future->future) != CASS_OK) {
295
303
  cassandra_future->yielded = true;
@@ -299,6 +307,7 @@ static VALUE future_on_failure_synchronize(VALUE future)
299
307
  }
300
308
 
301
309
  if (wakeup_thread) {
310
+ cassandra_future->queued = true;
302
311
  future_queue_push(future_thread_pool_get(cassandra_future), future);
303
312
  }
304
313
 
data/ext/ilios/ilios.c CHANGED
@@ -12,8 +12,10 @@ VALUE eExecutionError;
12
12
  VALUE eStatementError;
13
13
 
14
14
  VALUE cSizedQueue;
15
+ VALUE cSet;
15
16
 
16
17
  VALUE id_to_time;
18
+ VALUE id_to_a;
17
19
  VALUE id_new;
18
20
  VALUE id_push;
19
21
  VALUE id_pop;
@@ -88,8 +90,12 @@ void Init_ilios(void)
88
90
  eStatementError = rb_define_class_under(mCassandra, "StatementError", rb_eStandardError);
89
91
 
90
92
  cSizedQueue = rb_const_get(rb_cThread, rb_intern("SizedQueue"));
93
+ rb_require("set");
94
+ cSet = rb_const_get(rb_cObject, rb_intern("Set"));
95
+ rb_gc_register_mark_object(cSet);
91
96
 
92
97
  id_to_time = rb_intern("to_time");
98
+ id_to_a = rb_intern("to_a");
93
99
  id_new = rb_intern("new");
94
100
  id_push = rb_intern("push");
95
101
  id_pop = rb_intern("pop");
data/ext/ilios/ilios.h CHANGED
@@ -87,6 +87,8 @@ typedef struct
87
87
  uv_sem_t sem;
88
88
  bool already_waited;
89
89
  bool yielded;
90
+ // Handed to the thread pool, which posts the semaphore once it is done.
91
+ bool queued;
90
92
  } CassandraFuture;
91
93
 
92
94
  extern const rb_data_type_t cassandra_cluster_data_type;
@@ -107,8 +109,10 @@ extern VALUE eExecutionError;
107
109
  extern VALUE eStatementError;
108
110
 
109
111
  extern VALUE cSizedQueue;
112
+ extern VALUE cSet;
110
113
 
111
114
  extern VALUE id_to_time;
115
+ extern VALUE id_to_a;
112
116
  extern VALUE id_new;
113
117
  extern VALUE id_push;
114
118
  extern VALUE id_pop;
data/ext/ilios/result.c CHANGED
@@ -87,6 +87,163 @@ static void result_check_value(CassError error_code, VALUE key)
87
87
  }
88
88
  }
89
89
 
90
+ static VALUE result_convert_value(const CassValue *value, VALUE key);
91
+
92
+ struct result_convert_collection_arg {
93
+ CassIterator *iterator;
94
+ CassValueType type;
95
+ VALUE key;
96
+ };
97
+
98
+ static VALUE result_convert_collection_body(VALUE a)
99
+ {
100
+ struct result_convert_collection_arg *args = (struct result_convert_collection_arg *)a;
101
+
102
+ if (args->type == CASS_VALUE_TYPE_MAP) {
103
+ VALUE hash = rb_hash_new();
104
+
105
+ while (cass_iterator_next(args->iterator)) {
106
+ VALUE k = result_convert_value(cass_iterator_get_map_key(args->iterator), args->key);
107
+ VALUE v = result_convert_value(cass_iterator_get_map_value(args->iterator), args->key);
108
+ rb_hash_aset(hash, k, v);
109
+ }
110
+ return hash;
111
+ } else {
112
+ VALUE array = rb_ary_new();
113
+
114
+ while (cass_iterator_next(args->iterator)) {
115
+ rb_ary_push(array, result_convert_value(cass_iterator_get_value(args->iterator), args->key));
116
+ }
117
+ if (args->type == CASS_VALUE_TYPE_SET) {
118
+ return rb_funcall(cSet, id_new, 1, array);
119
+ }
120
+ return array;
121
+ }
122
+ }
123
+
124
+ static VALUE result_convert_collection_ensure(VALUE a)
125
+ {
126
+ cass_iterator_free((CassIterator *)a);
127
+ return Qnil;
128
+ }
129
+
130
+ static VALUE result_convert_collection(const CassValue *value, CassValueType type, VALUE key)
131
+ {
132
+ struct result_convert_collection_arg args;
133
+ CassIterator *iterator;
134
+
135
+ if (type == CASS_VALUE_TYPE_MAP) {
136
+ iterator = cass_iterator_from_map(value);
137
+ } else {
138
+ iterator = cass_iterator_from_collection(value);
139
+ }
140
+ if (iterator == NULL) {
141
+ // NULL collections are filtered out by cass_value_is_null in
142
+ // result_convert_value; this is a safety net.
143
+ return Qnil;
144
+ }
145
+
146
+ args.iterator = iterator;
147
+ args.type = type;
148
+ args.key = key;
149
+ return rb_ensure(result_convert_collection_body, (VALUE)&args, result_convert_collection_ensure, (VALUE)iterator);
150
+ }
151
+
152
+ static VALUE result_convert_value(const CassValue *value, VALUE key)
153
+ {
154
+ const CassValueType type = cass_value_type(value);
155
+
156
+ if (cass_value_is_null(value)) {
157
+ return Qnil;
158
+ }
159
+
160
+ switch (type) {
161
+ case CASS_VALUE_TYPE_TINY_INT:
162
+ {
163
+ cass_int8_t output = 0;
164
+ result_check_value(cass_value_get_int8(value, &output), key);
165
+ return INT2NUM(output);
166
+ }
167
+
168
+ case CASS_VALUE_TYPE_SMALL_INT:
169
+ {
170
+ cass_int16_t output = 0;
171
+ result_check_value(cass_value_get_int16(value, &output), key);
172
+ return INT2NUM(output);
173
+ }
174
+
175
+ case CASS_VALUE_TYPE_INT:
176
+ {
177
+ cass_int32_t output = 0;
178
+ result_check_value(cass_value_get_int32(value, &output), key);
179
+ return INT2NUM(output);
180
+ }
181
+
182
+ case CASS_VALUE_TYPE_BIGINT:
183
+ {
184
+ cass_int64_t output = 0;
185
+ result_check_value(cass_value_get_int64(value, &output), key);
186
+ return LL2NUM(output);
187
+ }
188
+
189
+ case CASS_VALUE_TYPE_FLOAT:
190
+ {
191
+ cass_float_t output = 0;
192
+ result_check_value(cass_value_get_float(value, &output), key);
193
+ return DBL2NUM(output);
194
+ }
195
+
196
+ case CASS_VALUE_TYPE_DOUBLE:
197
+ {
198
+ cass_double_t output = 0;
199
+ result_check_value(cass_value_get_double(value, &output), key);
200
+ return DBL2NUM(output);
201
+ }
202
+
203
+ case CASS_VALUE_TYPE_BOOLEAN:
204
+ {
205
+ cass_bool_t output = cass_false;
206
+ result_check_value(cass_value_get_bool(value, &output), key);
207
+ return output == cass_true ? Qtrue : Qfalse;
208
+ }
209
+
210
+ case CASS_VALUE_TYPE_TEXT:
211
+ case CASS_VALUE_TYPE_ASCII:
212
+ case CASS_VALUE_TYPE_VARCHAR:
213
+ {
214
+ const char* s = NULL;
215
+ size_t s_length = 0;
216
+ result_check_value(cass_value_get_string(value, &s, &s_length), key);
217
+ return rb_str_new(s, s_length);
218
+ }
219
+
220
+ case CASS_VALUE_TYPE_TIMESTAMP:
221
+ {
222
+ cass_int64_t output = 0;
223
+ result_check_value(cass_value_get_int64(value, &output), key);
224
+ return rb_time_new(output / 1000, output % 1000 * 1000);
225
+ }
226
+
227
+ case CASS_VALUE_TYPE_UUID:
228
+ {
229
+ CassUuid output = { 0, 0 };
230
+ char uuid[40];
231
+ result_check_value(cass_value_get_uuid(value, &output), key);
232
+ cass_uuid_string(output, uuid);
233
+ return rb_str_new2(uuid);
234
+ }
235
+
236
+ case CASS_VALUE_TYPE_LIST:
237
+ case CASS_VALUE_TYPE_SET:
238
+ case CASS_VALUE_TYPE_MAP:
239
+ return result_convert_collection(value, type, key);
240
+
241
+ default:
242
+ rb_warn("Unsupported type: %d", type);
243
+ return sym_unsupported_column_type;
244
+ }
245
+ }
246
+
90
247
  static VALUE result_convert_row(const CassResult *result, const CassRow *row, size_t column_count)
91
248
  {
92
249
  VALUE key;
@@ -96,106 +253,10 @@ static VALUE result_convert_row(const CassResult *result, const CassRow *row, si
96
253
 
97
254
  for (size_t i = 0; i < column_count; i++) {
98
255
  const CassValue *value = cass_row_get_column(row, i);
99
- const CassValueType type = cass_value_type(value);
100
256
 
101
257
  cass_result_column_name(result, i, &name, &name_length);
102
258
  key = rb_enc_interned_str(name, name_length, rb_utf8_encoding());
103
-
104
- if (cass_value_is_null(value)) {
105
- rb_hash_aset(hash, key, Qnil);
106
- continue;
107
- }
108
-
109
- switch (type) {
110
- case CASS_VALUE_TYPE_TINY_INT:
111
- {
112
- cass_int8_t output = 0;
113
- result_check_value(cass_value_get_int8(value, &output), key);
114
- rb_hash_aset(hash, key, INT2NUM(output));
115
- }
116
- break;
117
-
118
- case CASS_VALUE_TYPE_SMALL_INT:
119
- {
120
- cass_int16_t output = 0;
121
- result_check_value(cass_value_get_int16(value, &output), key);
122
- rb_hash_aset(hash, key, INT2NUM(output));
123
- }
124
- break;
125
-
126
- case CASS_VALUE_TYPE_INT:
127
- {
128
- cass_int32_t output = 0;
129
- result_check_value(cass_value_get_int32(value, &output), key);
130
- rb_hash_aset(hash, key, INT2NUM(output));
131
- }
132
- break;
133
-
134
- case CASS_VALUE_TYPE_BIGINT:
135
- {
136
- cass_int64_t output = 0;
137
- result_check_value(cass_value_get_int64(value, &output), key);
138
- rb_hash_aset(hash, key, LL2NUM(output));
139
- }
140
- break;
141
-
142
- case CASS_VALUE_TYPE_FLOAT:
143
- {
144
- cass_float_t output = 0;
145
- result_check_value(cass_value_get_float(value, &output), key);
146
- rb_hash_aset(hash, key, DBL2NUM(output));
147
- }
148
- break;
149
-
150
- case CASS_VALUE_TYPE_DOUBLE:
151
- {
152
- cass_double_t output = 0;
153
- result_check_value(cass_value_get_double(value, &output), key);
154
- rb_hash_aset(hash, key, DBL2NUM(output));
155
- }
156
- break;
157
-
158
- case CASS_VALUE_TYPE_BOOLEAN:
159
- {
160
- cass_bool_t output = cass_false;
161
- result_check_value(cass_value_get_bool(value, &output), key);
162
- rb_hash_aset(hash, key, output == cass_true ? Qtrue : Qfalse);
163
- }
164
- break;
165
-
166
- case CASS_VALUE_TYPE_TEXT:
167
- case CASS_VALUE_TYPE_ASCII:
168
- case CASS_VALUE_TYPE_VARCHAR:
169
- {
170
- const char* s = NULL;
171
- size_t s_length = 0;
172
- result_check_value(cass_value_get_string(value, &s, &s_length), key);
173
- rb_hash_aset(hash, key, rb_str_new(s, s_length));
174
- }
175
- break;
176
-
177
- case CASS_VALUE_TYPE_TIMESTAMP:
178
- {
179
- cass_int64_t output = 0;
180
- result_check_value(cass_value_get_int64(value, &output), key);
181
- rb_hash_aset(hash, key, rb_time_new(output / 1000, output % 1000 * 1000));
182
- }
183
- break;
184
-
185
- case CASS_VALUE_TYPE_UUID:
186
- {
187
- CassUuid output = { 0, 0 };
188
- char uuid[40];
189
- result_check_value(cass_value_get_uuid(value, &output), key);
190
- cass_uuid_string(output, uuid);
191
- rb_hash_aset(hash, key, rb_str_new2(uuid));
192
- }
193
- break;
194
-
195
- default:
196
- rb_warn("Unsupported type: %d", type);
197
- rb_hash_aset(hash, key, sym_unsupported_column_type);
198
- }
259
+ rb_hash_aset(hash, key, result_convert_value(value, key));
199
260
  }
200
261
 
201
262
  return hash;
@@ -24,6 +24,107 @@ typedef struct
24
24
  VALUE bound_values;
25
25
  } statement_bind_context;
26
26
 
27
+ typedef enum {
28
+ bind_target_kind_statement,
29
+ bind_target_kind_collection
30
+ } statement_bind_target_kind;
31
+
32
+ // Where a value is bound to: a named parameter of a statement, or an
33
+ // element of a collection being built. Keeps the type switch in
34
+ // statement_bind_value single so that top-level columns and collection
35
+ // elements always support exactly the same scalar types.
36
+ typedef struct
37
+ {
38
+ statement_bind_target_kind kind;
39
+ union {
40
+ struct {
41
+ CassStatement *statement;
42
+ const char *name;
43
+ } statement;
44
+ CassCollection *collection;
45
+ } as;
46
+ } statement_bind_target;
47
+
48
+ static CassError bind_target_int8(statement_bind_target *target, cass_int8_t value)
49
+ {
50
+ if (target->kind == bind_target_kind_statement) {
51
+ return cass_statement_bind_int8_by_name(target->as.statement.statement, target->as.statement.name, value);
52
+ }
53
+ return cass_collection_append_int8(target->as.collection, value);
54
+ }
55
+
56
+ static CassError bind_target_int16(statement_bind_target *target, cass_int16_t value)
57
+ {
58
+ if (target->kind == bind_target_kind_statement) {
59
+ return cass_statement_bind_int16_by_name(target->as.statement.statement, target->as.statement.name, value);
60
+ }
61
+ return cass_collection_append_int16(target->as.collection, value);
62
+ }
63
+
64
+ static CassError bind_target_int32(statement_bind_target *target, cass_int32_t value)
65
+ {
66
+ if (target->kind == bind_target_kind_statement) {
67
+ return cass_statement_bind_int32_by_name(target->as.statement.statement, target->as.statement.name, value);
68
+ }
69
+ return cass_collection_append_int32(target->as.collection, value);
70
+ }
71
+
72
+ static CassError bind_target_int64(statement_bind_target *target, cass_int64_t value)
73
+ {
74
+ if (target->kind == bind_target_kind_statement) {
75
+ return cass_statement_bind_int64_by_name(target->as.statement.statement, target->as.statement.name, value);
76
+ }
77
+ return cass_collection_append_int64(target->as.collection, value);
78
+ }
79
+
80
+ static CassError bind_target_float(statement_bind_target *target, cass_float_t value)
81
+ {
82
+ if (target->kind == bind_target_kind_statement) {
83
+ return cass_statement_bind_float_by_name(target->as.statement.statement, target->as.statement.name, value);
84
+ }
85
+ return cass_collection_append_float(target->as.collection, value);
86
+ }
87
+
88
+ static CassError bind_target_double(statement_bind_target *target, cass_double_t value)
89
+ {
90
+ if (target->kind == bind_target_kind_statement) {
91
+ return cass_statement_bind_double_by_name(target->as.statement.statement, target->as.statement.name, value);
92
+ }
93
+ return cass_collection_append_double(target->as.collection, value);
94
+ }
95
+
96
+ static CassError bind_target_bool(statement_bind_target *target, cass_bool_t value)
97
+ {
98
+ if (target->kind == bind_target_kind_statement) {
99
+ return cass_statement_bind_bool_by_name(target->as.statement.statement, target->as.statement.name, value);
100
+ }
101
+ return cass_collection_append_bool(target->as.collection, value);
102
+ }
103
+
104
+ static CassError bind_target_string(statement_bind_target *target, const char *value)
105
+ {
106
+ if (target->kind == bind_target_kind_statement) {
107
+ return cass_statement_bind_string_by_name(target->as.statement.statement, target->as.statement.name, value);
108
+ }
109
+ return cass_collection_append_string(target->as.collection, value);
110
+ }
111
+
112
+ static CassError bind_target_uuid(statement_bind_target *target, CassUuid value)
113
+ {
114
+ if (target->kind == bind_target_kind_statement) {
115
+ return cass_statement_bind_uuid_by_name(target->as.statement.statement, target->as.statement.name, value);
116
+ }
117
+ return cass_collection_append_uuid(target->as.collection, value);
118
+ }
119
+
120
+ static CassError bind_target_collection(statement_bind_target *target, const CassCollection *value)
121
+ {
122
+ if (target->kind == bind_target_kind_statement) {
123
+ return cass_statement_bind_collection_by_name(target->as.statement.statement, target->as.statement.name, value);
124
+ }
125
+ return cass_collection_append_collection(target->as.collection, value);
126
+ }
127
+
27
128
  void statement_default_config(CassandraStatement *cassandra_statement)
28
129
  {
29
130
  cassandra_statement->bound_values = Qnil;
@@ -32,28 +133,32 @@ void statement_default_config(CassandraStatement *cassandra_statement)
32
133
  cass_statement_set_paging_size(cassandra_statement->statement, DEFAULT_PAGE_SIZE);
33
134
  }
34
135
 
35
- static int hash_cb(VALUE key, VALUE value, VALUE arg)
36
- {
37
- statement_bind_context *ctx = (statement_bind_context *)arg;
38
- const CassDataType* data_type;
39
- CassValueType value_type;
40
- CassError result;
41
- const char *name;
136
+ static void statement_bind_value(statement_bind_target *target, const CassDataType *data_type,
137
+ VALUE value, VALUE key, const char *role);
138
+ static void statement_bind_collection(statement_bind_target *target, const CassDataType *data_type,
139
+ VALUE value, VALUE key);
42
140
 
43
- if (SYMBOL_P(key)) {
44
- key = rb_sym2str(key);
141
+ static void statement_bind_check(CassError result, VALUE key)
142
+ {
143
+ if (result != CASS_OK) {
144
+ rb_raise(eStatementError, "Failed to bind value of %"PRIsVALUE" column: %s", key, cass_error_desc(result));
45
145
  }
46
- name = StringValueCStr(key);
146
+ }
47
147
 
48
- data_type = cass_prepared_parameter_data_type_by_name(ctx->prepared, name);
49
- if (data_type == NULL) {
50
- rb_raise(eStatementError, "Invalid name %s was given.", name);
51
- }
52
- value_type = cass_data_type_type(data_type);
148
+ static void statement_bind_value(statement_bind_target *target, const CassDataType *data_type,
149
+ VALUE value, VALUE key, const char *role)
150
+ {
151
+ const CassValueType value_type = cass_data_type_type(data_type);
152
+ CassError result;
53
153
 
54
154
  if (NIL_P(value)) {
55
- result = cass_statement_bind_null_by_name(ctx->statement, name);
56
- goto result_check;
155
+ if (target->kind == bind_target_kind_collection) {
156
+ // The driver has no cass_collection_append_null: Cassandra
157
+ // collections cannot contain null elements.
158
+ rb_raise(rb_eTypeError, "nil is not allowed as a collection %s in %"PRIsVALUE"", role, key);
159
+ }
160
+ statement_bind_check(cass_statement_bind_null_by_name(target->as.statement.statement, target->as.statement.name), key);
161
+ return;
57
162
  }
58
163
 
59
164
  switch (value_type) {
@@ -64,7 +169,7 @@ static int hash_cb(VALUE key, VALUE value, VALUE arg)
64
169
  if (v < INT8_MIN || v > INT8_MAX) {
65
170
  rb_raise(rb_eRangeError, "Invalid value: %ld", v);
66
171
  }
67
- result = cass_statement_bind_int8_by_name(ctx->statement, name, (cass_int8_t)v);
172
+ result = bind_target_int8(target, (cass_int8_t)v);
68
173
  }
69
174
  break;
70
175
 
@@ -76,7 +181,7 @@ static int hash_cb(VALUE key, VALUE value, VALUE arg)
76
181
  rb_raise(rb_eRangeError, "Invalid value: %ld", v);
77
182
  }
78
183
 
79
- result = cass_statement_bind_int16_by_name(ctx->statement, name, (cass_int16_t)v);
184
+ result = bind_target_int16(target, (cass_int16_t)v);
80
185
  }
81
186
  break;
82
187
 
@@ -88,12 +193,12 @@ static int hash_cb(VALUE key, VALUE value, VALUE arg)
88
193
  rb_raise(rb_eRangeError, "Invalid value: %ld", v);
89
194
  }
90
195
 
91
- result = cass_statement_bind_int32_by_name(ctx->statement, name, (cass_int32_t)v);
196
+ result = bind_target_int32(target, (cass_int32_t)v);
92
197
  }
93
198
  break;
94
199
 
95
200
  case CASS_VALUE_TYPE_BIGINT:
96
- result = cass_statement_bind_int64_by_name(ctx->statement, name, NUM2LONG(value));
201
+ result = bind_target_int64(target, NUM2LONG(value));
97
202
  break;
98
203
 
99
204
  case CASS_VALUE_TYPE_FLOAT:
@@ -104,25 +209,27 @@ static int hash_cb(VALUE key, VALUE value, VALUE arg)
104
209
  rb_raise(rb_eRangeError, "Invalid value: %lf", v);
105
210
  }
106
211
 
107
- result = cass_statement_bind_float_by_name(ctx->statement, name, v);
212
+ result = bind_target_float(target, v);
108
213
  }
109
214
  break;
110
215
 
111
216
  case CASS_VALUE_TYPE_DOUBLE:
112
- result = cass_statement_bind_double_by_name(ctx->statement, name, NUM2DBL(value));
217
+ result = bind_target_double(target, NUM2DBL(value));
113
218
  break;
114
219
 
115
220
  case CASS_VALUE_TYPE_BOOLEAN:
116
- {
117
- cass_bool_t v = RTEST(value) ? cass_true : cass_false;
118
- result = cass_statement_bind_bool_by_name(ctx->statement, name, v);
119
- }
221
+ result = bind_target_bool(target, RTEST(value) ? cass_true : cass_false);
120
222
  break;
121
223
 
122
224
  case CASS_VALUE_TYPE_TEXT:
123
225
  case CASS_VALUE_TYPE_ASCII:
124
226
  case CASS_VALUE_TYPE_VARCHAR:
125
- result = cass_statement_bind_string_by_name(ctx->statement, name, StringValueCStr(value));
227
+ if (SYMBOL_P(value)) {
228
+ // rb_sym2str returns the Symbol's fstring; no allocation, so
229
+ // converting on every execution is fine.
230
+ value = rb_sym2str(value);
231
+ }
232
+ result = bind_target_string(target, StringValueCStr(value));
126
233
  break;
127
234
 
128
235
  case CASS_VALUE_TYPE_TIMESTAMP:
@@ -133,7 +240,7 @@ static int hash_cb(VALUE key, VALUE value, VALUE arg)
133
240
  rb_raise(rb_eTypeError, "no implicit conversion of %"PRIsVALUE" to Time", rb_obj_class(value));
134
241
  }
135
242
  }
136
- result = cass_statement_bind_int64_by_name(ctx->statement, name, (cass_int64_t)(NUM2DBL(rb_Float(value)) * 1000));
243
+ result = bind_target_int64(target, (cass_int64_t)(NUM2DBL(rb_Float(value)) * 1000));
137
244
  break;
138
245
 
139
246
  case CASS_VALUE_TYPE_UUID:
@@ -143,28 +250,255 @@ static int hash_cb(VALUE key, VALUE value, VALUE arg)
143
250
 
144
251
  result = cass_uuid_from_string(uuid_string, &uuid);
145
252
  if (result != CASS_OK) {
146
- rb_raise(eStatementError, "Invalid UUID was given: %s=%"PRIsVALUE"", name, value);
253
+ rb_raise(eStatementError, "Invalid UUID was given: %"PRIsVALUE"=%"PRIsVALUE"", key, value);
147
254
  }
148
255
 
149
- result = cass_statement_bind_uuid_by_name(ctx->statement, name, uuid);
256
+ result = bind_target_uuid(target, uuid);
150
257
  }
151
258
  break;
152
259
 
260
+ case CASS_VALUE_TYPE_LIST:
261
+ case CASS_VALUE_TYPE_SET:
262
+ case CASS_VALUE_TYPE_MAP:
263
+ statement_bind_collection(target, data_type, value, key);
264
+ return;
265
+
153
266
  default:
154
- rb_raise(rb_eTypeError, "Unsupported %"PRIsVALUE" type: %s=%"PRIsVALUE"", rb_obj_class(value), name, value);
267
+ rb_raise(rb_eTypeError, "Unsupported %"PRIsVALUE" type: %"PRIsVALUE"=%"PRIsVALUE"", rb_obj_class(value), key, value);
155
268
  }
156
269
 
157
- result_check:
158
- if (result != CASS_OK) {
159
- rb_raise(eStatementError, "Failed to bind value: %s", cass_error_desc(result));
270
+ statement_bind_check(result, key);
271
+ }
272
+
273
+ typedef struct
274
+ {
275
+ statement_bind_target target;
276
+ const CassDataType *data_type;
277
+ VALUE value;
278
+ VALUE key;
279
+ } statement_bind_collection_args;
280
+
281
+ struct statement_bind_map_ctx {
282
+ statement_bind_target *target;
283
+ const CassDataType *key_type;
284
+ const CassDataType *value_type;
285
+ VALUE key;
286
+ };
287
+
288
+ static int statement_bind_map_cb(VALUE k, VALUE v, VALUE arg)
289
+ {
290
+ struct statement_bind_map_ctx *ctx = (struct statement_bind_map_ctx *)arg;
291
+
292
+ // A map is appended as alternating key, value pairs.
293
+ statement_bind_value(ctx->target, ctx->key_type, k, ctx->key, "key");
294
+ statement_bind_value(ctx->target, ctx->value_type, v, ctx->key, "value");
295
+ return ST_CONTINUE;
296
+ }
297
+
298
+ static VALUE statement_bind_collection_body(VALUE arg)
299
+ {
300
+ statement_bind_collection_args *args = (statement_bind_collection_args *)arg;
301
+
302
+ if (cass_data_type_type(args->data_type) == CASS_VALUE_TYPE_MAP) {
303
+ struct statement_bind_map_ctx ctx;
304
+
305
+ ctx.target = &args->target;
306
+ ctx.key_type = cass_data_type_sub_data_type(args->data_type, 0);
307
+ ctx.value_type = cass_data_type_sub_data_type(args->data_type, 1);
308
+ ctx.key = args->key;
309
+ rb_hash_foreach(args->value, statement_bind_map_cb, (VALUE)&ctx);
310
+ } else {
311
+ const CassDataType *element_type = cass_data_type_sub_data_type(args->data_type, 0);
312
+ long length = RARRAY_LEN(args->value);
313
+
314
+ for (long i = 0; i < length; i++) {
315
+ statement_bind_value(&args->target, element_type, RARRAY_AREF(args->value, i), args->key, "element");
316
+ }
160
317
  }
318
+ return Qnil;
319
+ }
161
320
 
162
- if (!NIL_P(ctx->bound_values)) {
321
+ /*
322
+ * Builds a CassCollection from a snapshotted (frozen) Array or Hash and binds
323
+ * it to the target. The value is guaranteed to be an Array (list/set) or Hash
324
+ * (map) because statement_snapshot_value normalizes it in Statement#bind
325
+ * before the first statement_bind_value call, and executions only replay
326
+ * values stored in bound_values.
327
+ */
328
+ static void statement_bind_collection(statement_bind_target *target, const CassDataType *data_type,
329
+ VALUE value, VALUE key)
330
+ {
331
+ const CassValueType type = cass_data_type_type(data_type);
332
+ statement_bind_collection_args args;
333
+ CassCollection *collection;
334
+ size_t item_count;
335
+ CassError result;
336
+ int state = 0;
337
+
338
+ if (type == CASS_VALUE_TYPE_MAP) {
339
+ if (cass_data_type_sub_type_count(data_type) != 2) {
340
+ rb_raise(eStatementError, "Invalid map type of %"PRIsVALUE" column", key);
341
+ }
342
+ Check_Type(value, T_HASH);
343
+ item_count = (size_t)RHASH_SIZE(value);
344
+ } else {
345
+ if (cass_data_type_sub_type_count(data_type) != 1) {
346
+ rb_raise(eStatementError, "Invalid collection type of %"PRIsVALUE" column", key);
347
+ }
348
+ Check_Type(value, T_ARRAY);
349
+ item_count = (size_t)RARRAY_LEN(value);
350
+ }
351
+
352
+ // cass_collection_new_from_data_type (not cass_collection_new) so that
353
+ // nested element types are propagated to the driver.
354
+ collection = cass_collection_new_from_data_type(data_type, item_count);
355
+ if (collection == NULL) {
356
+ rb_raise(eStatementError, "Failed to create a collection for %"PRIsVALUE" column", key);
357
+ }
358
+
359
+ args.target.kind = bind_target_kind_collection;
360
+ args.target.as.collection = collection;
361
+ args.data_type = data_type;
362
+ args.value = value;
363
+ args.key = key;
364
+
365
+ rb_protect(statement_bind_collection_body, (VALUE)&args, &state);
366
+ if (state) {
367
+ cass_collection_free(collection);
368
+ rb_jump_tag(state);
369
+ }
370
+
371
+ result = bind_target_collection(target, collection);
372
+ cass_collection_free(collection);
373
+ statement_bind_check(result, key);
374
+ }
375
+
376
+ static VALUE statement_snapshot_value(const CassDataType *data_type, VALUE value);
377
+
378
+ struct statement_snapshot_map_ctx {
379
+ const CassDataType *key_type;
380
+ const CassDataType *value_type;
381
+ VALUE snapshot;
382
+ };
383
+
384
+ static int statement_snapshot_map_cb(VALUE k, VALUE v, VALUE arg)
385
+ {
386
+ struct statement_snapshot_map_ctx *ctx = (struct statement_snapshot_map_ctx *)arg;
387
+
388
+ rb_hash_aset(ctx->snapshot,
389
+ statement_snapshot_value(ctx->key_type, k),
390
+ statement_snapshot_value(ctx->value_type, v));
391
+ return ST_CONTINUE;
392
+ }
393
+
394
+ /*
395
+ * Returns a deep-frozen snapshot of the value so a later in-place mutation by
396
+ * the caller (or another thread) doesn't change what gets bound at execution
397
+ * time. Executions then only walk frozen containers, which makes re-binding
398
+ * on execute safe without re-validating.
399
+ */
400
+ static VALUE statement_snapshot_value(const CassDataType *data_type, VALUE value)
401
+ {
402
+ if (NIL_P(value)) {
403
+ return Qnil;
404
+ }
405
+
406
+ switch (cass_data_type_type(data_type)) {
407
+ case CASS_VALUE_TYPE_LIST:
408
+ case CASS_VALUE_TYPE_SET:
409
+ {
410
+ const CassDataType *element_type = cass_data_type_sub_data_type(data_type, 0);
411
+ VALUE array;
412
+ VALUE snapshot;
413
+ long length;
414
+
415
+ if (element_type == NULL) {
416
+ rb_raise(eStatementError, "Invalid collection type: missing element type");
417
+ }
418
+
419
+ if (RB_TYPE_P(value, T_ARRAY)) {
420
+ array = value;
421
+ } else if (rb_obj_is_kind_of(value, cSet)) {
422
+ array = rb_funcall(value, id_to_a, 0);
423
+ // A Set subclass may override to_a to return anything.
424
+ Check_Type(array, T_ARRAY);
425
+ } else {
426
+ rb_raise(rb_eTypeError, "no implicit conversion of %"PRIsVALUE" into Array or Set", rb_obj_class(value));
427
+ }
428
+
429
+ length = RARRAY_LEN(array);
430
+ snapshot = rb_ary_new_capa(length);
431
+ for (long i = 0; i < length; i++) {
432
+ rb_ary_push(snapshot, statement_snapshot_value(element_type, RARRAY_AREF(array, i)));
433
+ }
434
+ return rb_ary_freeze(snapshot);
435
+ }
436
+
437
+ case CASS_VALUE_TYPE_MAP:
438
+ {
439
+ struct statement_snapshot_map_ctx ctx;
440
+
441
+ Check_Type(value, T_HASH);
442
+ ctx.key_type = cass_data_type_sub_data_type(data_type, 0);
443
+ ctx.value_type = cass_data_type_sub_data_type(data_type, 1);
444
+ if (ctx.key_type == NULL || ctx.value_type == NULL) {
445
+ rb_raise(eStatementError, "Invalid collection type: missing map key or value type");
446
+ }
447
+ ctx.snapshot = rb_hash_new();
448
+ rb_hash_foreach(value, statement_snapshot_map_cb, (VALUE)&ctx);
449
+ return rb_hash_freeze(ctx.snapshot);
450
+ }
451
+
452
+ case CASS_VALUE_TYPE_TEXT:
453
+ case CASS_VALUE_TYPE_ASCII:
454
+ case CASS_VALUE_TYPE_VARCHAR:
455
+ if (SYMBOL_P(value)) {
456
+ // Symbols are immutable; no defensive copy needed.
457
+ return value;
458
+ }
459
+ // Also converts to_str objects here so the converted String (not the
460
+ // possibly mutable object) is what gets stored and re-bound.
461
+ StringValue(value);
462
+ return rb_str_new_frozen(value);
463
+
464
+ default:
163
465
  if (RB_TYPE_P(value, T_STRING)) {
164
- // Snapshot the value so a later in-place mutation by the caller
165
- // doesn't change what gets bound at execution time.
166
- value = rb_str_new_frozen(value);
466
+ return rb_str_new_frozen(value);
167
467
  }
468
+ return value;
469
+ }
470
+ }
471
+
472
+ static int hash_cb(VALUE key, VALUE value, VALUE arg)
473
+ {
474
+ statement_bind_context *ctx = (statement_bind_context *)arg;
475
+ const CassDataType* data_type;
476
+ const char *name;
477
+ statement_bind_target target;
478
+
479
+ if (SYMBOL_P(key)) {
480
+ key = rb_sym2str(key);
481
+ }
482
+ name = StringValueCStr(key);
483
+
484
+ data_type = cass_prepared_parameter_data_type_by_name(ctx->prepared, name);
485
+ if (data_type == NULL) {
486
+ rb_raise(eStatementError, "Invalid name %s was given.", name);
487
+ }
488
+
489
+ target.kind = bind_target_kind_statement;
490
+ target.as.statement.statement = ctx->statement;
491
+ target.as.statement.name = name;
492
+
493
+ if (!NIL_P(ctx->bound_values)) {
494
+ // Snapshot before binding so the value bound now and the value
495
+ // replayed by later executions are the same deep-frozen object.
496
+ value = statement_snapshot_value(data_type, value);
497
+ }
498
+
499
+ statement_bind_value(&target, data_type, value, key, NULL);
500
+
501
+ if (!NIL_P(ctx->bound_values)) {
168
502
  rb_hash_aset(ctx->bound_values, key, value);
169
503
  }
170
504
 
data/ilios.gemspec CHANGED
@@ -12,7 +12,7 @@ Gem::Specification.new do |spec|
12
12
  spec.description = 'Cassandra driver written by C language'
13
13
  spec.homepage = 'https://github.com/Watson1978/ilios'
14
14
  spec.license = 'MIT'
15
- spec.required_ruby_version = '>= 3.1.0'
15
+ spec.required_ruby_version = '>= 3.4.0'
16
16
 
17
17
  spec.requirements << 'cmake'
18
18
 
data/lib/ilios/version.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ilios
4
- VERSION = '1.0.6'
4
+ VERSION = '1.1.0'
5
5
  public_constant :VERSION
6
6
 
7
7
  CASSANDRA_CPP_DRIVER_VERSION = '2.17.1'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ilios
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.6
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Watson
@@ -58,7 +58,7 @@ metadata:
58
58
  homepage_uri: https://github.com/Watson1978/ilios
59
59
  source_code_uri: https://github.com/Watson1978/ilios
60
60
  bug_tracker_uri: https://github.com/Watson1978/ilios/issues
61
- documentation_uri: https://www.rubydoc.info/gems/ilios/1.0.6
61
+ documentation_uri: https://www.rubydoc.info/gems/ilios/1.1.0
62
62
  rubygems_mfa_required: 'true'
63
63
  rdoc_options: []
64
64
  require_paths:
@@ -67,7 +67,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
67
67
  requirements:
68
68
  - - ">="
69
69
  - !ruby/object:Gem::Version
70
- version: 3.1.0
70
+ version: 3.4.0
71
71
  required_rubygems_version: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - ">="
@@ -75,7 +75,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
75
75
  version: '0'
76
76
  requirements:
77
77
  - cmake
78
- rubygems_version: 4.0.16
78
+ rubygems_version: 4.0.18
79
79
  specification_version: 4
80
80
  summary: Cassandra driver written by C language
81
81
  test_files: []