extendi-cassandra_object 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +5 -0
- data/.travis.yml +23 -0
- data/CHANGELOG +0 -0
- data/Gemfile +17 -0
- data/LICENSE +13 -0
- data/MIT-LICENSE +20 -0
- data/README.md +177 -0
- data/Rakefile +12 -0
- data/extendi-cassandra_object.gemspec +26 -0
- data/lib/cassandra_object.rb +73 -0
- data/lib/cassandra_object/adapters/abstract_adapter.rb +61 -0
- data/lib/cassandra_object/adapters/cassandra_adapter.rb +269 -0
- data/lib/cassandra_object/adapters/cassandra_schemaless_adapter.rb +306 -0
- data/lib/cassandra_object/attribute_methods.rb +96 -0
- data/lib/cassandra_object/attribute_methods/definition.rb +22 -0
- data/lib/cassandra_object/attribute_methods/dirty.rb +36 -0
- data/lib/cassandra_object/attribute_methods/primary_key.rb +25 -0
- data/lib/cassandra_object/attribute_methods/typecasting.rb +59 -0
- data/lib/cassandra_object/base.rb +33 -0
- data/lib/cassandra_object/base_schema.rb +11 -0
- data/lib/cassandra_object/base_schemaless.rb +11 -0
- data/lib/cassandra_object/base_schemaless_dynamic.rb +11 -0
- data/lib/cassandra_object/belongs_to.rb +63 -0
- data/lib/cassandra_object/belongs_to/association.rb +49 -0
- data/lib/cassandra_object/belongs_to/builder.rb +40 -0
- data/lib/cassandra_object/belongs_to/reflection.rb +30 -0
- data/lib/cassandra_object/callbacks.rb +29 -0
- data/lib/cassandra_object/core.rb +63 -0
- data/lib/cassandra_object/errors.rb +10 -0
- data/lib/cassandra_object/identity.rb +26 -0
- data/lib/cassandra_object/inspect.rb +25 -0
- data/lib/cassandra_object/log_subscriber.rb +44 -0
- data/lib/cassandra_object/model.rb +60 -0
- data/lib/cassandra_object/persistence.rb +203 -0
- data/lib/cassandra_object/railtie.rb +33 -0
- data/lib/cassandra_object/railties/controller_runtime.rb +45 -0
- data/lib/cassandra_object/schema.rb +83 -0
- data/lib/cassandra_object/schemaless.rb +83 -0
- data/lib/cassandra_object/scope.rb +86 -0
- data/lib/cassandra_object/scope/finder_methods.rb +54 -0
- data/lib/cassandra_object/scope/query_methods.rb +69 -0
- data/lib/cassandra_object/scoping.rb +27 -0
- data/lib/cassandra_object/serialization.rb +6 -0
- data/lib/cassandra_object/tasks/ks.rake +54 -0
- data/lib/cassandra_object/timestamps.rb +19 -0
- data/lib/cassandra_object/type.rb +16 -0
- data/lib/cassandra_object/types.rb +8 -0
- data/lib/cassandra_object/types/array_type.rb +16 -0
- data/lib/cassandra_object/types/base_type.rb +26 -0
- data/lib/cassandra_object/types/boolean_type.rb +20 -0
- data/lib/cassandra_object/types/date_type.rb +22 -0
- data/lib/cassandra_object/types/float_type.rb +16 -0
- data/lib/cassandra_object/types/integer_type.rb +20 -0
- data/lib/cassandra_object/types/json_type.rb +13 -0
- data/lib/cassandra_object/types/string_type.rb +19 -0
- data/lib/cassandra_object/types/time_type.rb +16 -0
- data/lib/cassandra_object/types/type_helper.rb +39 -0
- data/lib/cassandra_object/validations.rb +44 -0
- data/test/support/cassandra.rb +63 -0
- data/test/support/issue.rb +12 -0
- data/test/support/issue_dynamic.rb +12 -0
- data/test/support/issue_schema.rb +17 -0
- data/test/support/issue_schema_child.rb +17 -0
- data/test/support/issue_schema_father.rb +13 -0
- data/test/test_helper.rb +41 -0
- data/test/unit/active_model_test.rb +18 -0
- data/test/unit/adapters/adapter_test.rb +6 -0
- data/test/unit/attribute_methods/definition_test.rb +13 -0
- data/test/unit/attribute_methods/dirty_test.rb +72 -0
- data/test/unit/attribute_methods/primary_key_test.rb +26 -0
- data/test/unit/attribute_methods/typecasting_test.rb +119 -0
- data/test/unit/attribute_methods_test.rb +51 -0
- data/test/unit/base_test.rb +20 -0
- data/test/unit/belongs_to/reflection_test.rb +12 -0
- data/test/unit/belongs_to_test.rb +63 -0
- data/test/unit/callbacks_test.rb +46 -0
- data/test/unit/connection_test.rb +6 -0
- data/test/unit/connections/connections_test.rb +55 -0
- data/test/unit/core_test.rb +55 -0
- data/test/unit/identity_test.rb +26 -0
- data/test/unit/inspect_test.rb +26 -0
- data/test/unit/log_subscriber_test.rb +25 -0
- data/test/unit/persistence_schema_test.rb +156 -0
- data/test/unit/persistence_test.rb +266 -0
- data/test/unit/railties/controller_runtime_test.rb +48 -0
- data/test/unit/schema/tasks_test.rb +32 -0
- data/test/unit/schema_test.rb +115 -0
- data/test/unit/schemaless_test.rb +100 -0
- data/test/unit/scope/finder_methods_test.rb +117 -0
- data/test/unit/scope/query_methods_test.rb +32 -0
- data/test/unit/scoping_test.rb +7 -0
- data/test/unit/timestamps_test.rb +27 -0
- data/test/unit/types/array_type_test.rb +17 -0
- data/test/unit/types/base_type_test.rb +19 -0
- data/test/unit/types/boolean_type_test.rb +24 -0
- data/test/unit/types/date_type_test.rb +15 -0
- data/test/unit/types/float_type_test.rb +17 -0
- data/test/unit/types/integer_type_test.rb +19 -0
- data/test/unit/types/json_type_test.rb +23 -0
- data/test/unit/types/string_type_test.rb +25 -0
- data/test/unit/types/time_type_test.rb +14 -0
- data/test/unit/validations_test.rb +27 -0
- metadata +202 -0
@@ -0,0 +1,269 @@
|
|
1
|
+
gem 'cassandra-driver'
|
2
|
+
require 'cassandra'
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
module CassandraObject
|
6
|
+
module Adapters
|
7
|
+
class CassandraAdapter < AbstractAdapter
|
8
|
+
class QueryBuilder
|
9
|
+
|
10
|
+
def initialize(adapter, scope)
|
11
|
+
@adapter = adapter
|
12
|
+
@scope = scope
|
13
|
+
end
|
14
|
+
|
15
|
+
def select_string
|
16
|
+
if @scope.select_values.any?
|
17
|
+
(['KEY'] | @scope.select_values) * ','
|
18
|
+
else
|
19
|
+
'*'
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_query_async
|
24
|
+
# empty ids
|
25
|
+
if @scope.id_values.empty?
|
26
|
+
str = [
|
27
|
+
"SELECT #{select_string} FROM #{@scope.klass.column_family}",
|
28
|
+
where_string_async(nil)
|
29
|
+
]
|
30
|
+
str << "ALLOW FILTERING" if @scope.klass.allow_filtering
|
31
|
+
return [] << str.delete_if(&:blank?) * ' '
|
32
|
+
end
|
33
|
+
@scope.id_values.map { |id|
|
34
|
+
str = [
|
35
|
+
"SELECT #{select_string} FROM #{@scope.klass.column_family}",
|
36
|
+
where_string_async(id)
|
37
|
+
]
|
38
|
+
str << "ALLOW FILTERING" if @scope.klass.allow_filtering
|
39
|
+
str.delete_if(&:blank?) * ' '
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
def where_string_async(id)
|
44
|
+
wheres = @scope.where_values.dup
|
45
|
+
wheres << "#{@scope._key} = '#{id}'" if !id.nil?
|
46
|
+
"WHERE #{wheres * ' AND '}" if wheres.any?
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
def cassandra_cluster_options
|
52
|
+
cluster_options = config.slice(*[
|
53
|
+
:auth_provider,
|
54
|
+
:client_cert,
|
55
|
+
:compression,
|
56
|
+
:compressor,
|
57
|
+
:connect_timeout,
|
58
|
+
:connections_per_local_node,
|
59
|
+
:connections_per_remote_node,
|
60
|
+
:consistency,
|
61
|
+
:credentials,
|
62
|
+
:futures_factory,
|
63
|
+
:hosts,
|
64
|
+
:load_balancing_policy,
|
65
|
+
:logger,
|
66
|
+
:page_size,
|
67
|
+
:passphrase,
|
68
|
+
:password,
|
69
|
+
:port,
|
70
|
+
:private_key,
|
71
|
+
:protocol_version,
|
72
|
+
:reconnection_policy,
|
73
|
+
:retry_policy,
|
74
|
+
:schema_refresh_delay,
|
75
|
+
:schema_refresh_timeout,
|
76
|
+
:server_cert,
|
77
|
+
:ssl,
|
78
|
+
:timeout,
|
79
|
+
:trace,
|
80
|
+
:username
|
81
|
+
])
|
82
|
+
|
83
|
+
{
|
84
|
+
load_balancing_policy: 'Cassandra::LoadBalancing::Policies::%s',
|
85
|
+
reconnection_policy: 'Cassandra::Reconnection::Policies::%s',
|
86
|
+
retry_policy: 'Cassandra::Retry::Policies::%s'
|
87
|
+
}.each do |policy_key, class_template|
|
88
|
+
if cluster_options[policy_key]
|
89
|
+
cluster_options[policy_key] = (class_template % [policy_key.classify]).constantize
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Setting defaults
|
94
|
+
cluster_options.merge!({
|
95
|
+
consistency: cluster_options[:consistency] || :quorum,
|
96
|
+
protocol_version: cluster_options[:protocol_version] || 3,
|
97
|
+
page_size: cluster_options[:page_size] || 10000
|
98
|
+
})
|
99
|
+
return cluster_options
|
100
|
+
end
|
101
|
+
|
102
|
+
|
103
|
+
def connection
|
104
|
+
@connection ||= begin
|
105
|
+
cluster = Cassandra.cluster cassandra_cluster_options
|
106
|
+
cluster.connect config[:keyspace]
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def execute(statement, arguments = [])
|
111
|
+
ActiveSupport::Notifications.instrument('cql.cassandra_object', cql: statement) do
|
112
|
+
type_hints = []
|
113
|
+
arguments.map { |a| type_hints << CassandraObject::Types::TypeHelper.guess_type(a) } if !arguments.nil?
|
114
|
+
connection.execute statement, arguments: arguments, type_hints: type_hints, consistency: consistency, page_size: config[:page_size]
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def execute_async(queries, arguments = [])
|
119
|
+
futures = queries.map { |q|
|
120
|
+
ActiveSupport::Notifications.instrument('cql.cassandra_object', cql: q) do
|
121
|
+
connection.execute_async q, arguments: arguments, consistency: consistency, page_size: config[:page_size]
|
122
|
+
end
|
123
|
+
}
|
124
|
+
futures.map do |future|
|
125
|
+
rows = future.get
|
126
|
+
rows
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def select(scope)
|
131
|
+
queries = QueryBuilder.new(self, scope).to_query_async
|
132
|
+
# todo paginate
|
133
|
+
cql_rows = execute_async(queries).map{|item| item.rows.map{|x| x}}.flatten!
|
134
|
+
cql_rows.each do |cql_row|
|
135
|
+
attributes = cql_row.to_hash
|
136
|
+
key = attributes.delete(scope._key)
|
137
|
+
yield(key, attributes) unless attributes.empty?
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def insert(table, id, attributes, ttl = nil)
|
142
|
+
write(table, id, attributes, ttl)
|
143
|
+
end
|
144
|
+
|
145
|
+
def update(table, id, attributes, ttl = nil)
|
146
|
+
write_update(table, id, attributes)
|
147
|
+
end
|
148
|
+
|
149
|
+
def write(table, id, attributes, ttl = nil)
|
150
|
+
statement = "INSERT INTO #{table} (#{(attributes.keys).join(',')}) VALUES (#{(['?'] * attributes.size).join(',')})"
|
151
|
+
statement += " USING TTL #{ttl.to_s}" if ttl.present?
|
152
|
+
arguments = attributes.values
|
153
|
+
execute(statement, arguments)
|
154
|
+
end
|
155
|
+
|
156
|
+
def write_update(table, id, attributes)
|
157
|
+
queries =[]
|
158
|
+
# id here is the name of the key of the model
|
159
|
+
id_value = attributes[id]
|
160
|
+
if (not_nil_attributes = attributes.reject { |key, value| value.nil? }).any?
|
161
|
+
statement = "INSERT INTO #{table} (#{(not_nil_attributes.keys).join(',')}) VALUES (#{(['?'] * not_nil_attributes.size).join(',')})"
|
162
|
+
queries << {query: statement, arguments: not_nil_attributes.values}
|
163
|
+
end
|
164
|
+
if (nil_attributes = attributes.select { |key, value| value.nil? }).any?
|
165
|
+
queries << {query: "DELETE #{nil_attributes.keys.join(',')} FROM #{table} WHERE #{id} = ?", arguments: [id_value.to_s]}
|
166
|
+
end
|
167
|
+
execute_batchable(queries)
|
168
|
+
end
|
169
|
+
|
170
|
+
def delete(table, key, ids)
|
171
|
+
ids = [ids] if !ids.is_a?(Array)
|
172
|
+
statement = "DELETE FROM #{table} WHERE #{key} IN (#{ids.map { |id| "'#{id}'" }.join(',')})"
|
173
|
+
execute(statement, nil)
|
174
|
+
end
|
175
|
+
|
176
|
+
def execute_batch(statements)
|
177
|
+
raise 'No can do' if statements.empty?
|
178
|
+
batch = connection.batch do |b|
|
179
|
+
statements.each do |statement|
|
180
|
+
b.add(statement[:query], arguments: statement[:arguments])
|
181
|
+
end
|
182
|
+
end
|
183
|
+
connection.execute(batch, page_size: config[:page_size])
|
184
|
+
end
|
185
|
+
|
186
|
+
# SCHEMA
|
187
|
+
def create_table(table_name, params = {})
|
188
|
+
stmt = "CREATE TABLE #{table_name} "
|
189
|
+
if params.any? && !params[:attributes].present?
|
190
|
+
raise 'No attributes for the table'
|
191
|
+
elsif !params[:attributes].include? 'PRIMARY KEY'
|
192
|
+
raise 'No PRIMARY KEY defined'
|
193
|
+
end
|
194
|
+
|
195
|
+
stmt += "(#{params[:attributes]})"
|
196
|
+
# WITH COMPACT STORAGE
|
197
|
+
schema_execute statement_create_with_options(stmt, params[:options]), config[:keyspace]
|
198
|
+
end
|
199
|
+
|
200
|
+
def drop_table(table_name, confirm = false)
|
201
|
+
count = (schema_execute "SELECT count(*) FROM #{table_name}", config[:keyspace]).rows.first['count']
|
202
|
+
if confirm || count == 0
|
203
|
+
schema_execute "DROP TABLE #{table_name}", config[:keyspace]
|
204
|
+
else
|
205
|
+
raise "The table #{table_name} is not empty! If you want to drop it add the option confirm = true"
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
def schema_execute(cql, keyspace)
|
210
|
+
schema_db = Cassandra.cluster cassandra_cluster_options
|
211
|
+
connection = schema_db.connect keyspace
|
212
|
+
connection.execute cql, consistency: consistency
|
213
|
+
end
|
214
|
+
|
215
|
+
def cassandra_version
|
216
|
+
@cassandra_version ||= execute('select release_version from system.local').rows.first['release_version'].to_f
|
217
|
+
end
|
218
|
+
|
219
|
+
# /SCHEMA
|
220
|
+
|
221
|
+
def consistency
|
222
|
+
defined?(@consistency) ? @consistency : nil
|
223
|
+
end
|
224
|
+
|
225
|
+
def consistency=(val)
|
226
|
+
@consistency = val
|
227
|
+
end
|
228
|
+
|
229
|
+
def statement_create_with_options(stmt, options = '')
|
230
|
+
if !options.nil?
|
231
|
+
statement_with_options stmt, options
|
232
|
+
else
|
233
|
+
# standard
|
234
|
+
if cassandra_version < 3
|
235
|
+
"#{stmt} WITH bloom_filter_fp_chance = 0.001
|
236
|
+
AND caching = '{\"keys\":\"ALL\", \"rows_per_partition\":\"NONE\"}'
|
237
|
+
AND comment = ''
|
238
|
+
AND compaction = {'min_sstable_size': '52428800', 'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy'}
|
239
|
+
AND compression = {'chunk_length_kb': '64', 'sstable_compression': 'org.apache.cassandra.io.compress.LZ4Compressor'}
|
240
|
+
AND dclocal_read_repair_chance = 0.0
|
241
|
+
AND default_time_to_live = 0
|
242
|
+
AND gc_grace_seconds = 864000
|
243
|
+
AND max_index_interval = 2048
|
244
|
+
AND memtable_flush_period_in_ms = 0
|
245
|
+
AND min_index_interval = 128
|
246
|
+
AND read_repair_chance = 1.0
|
247
|
+
AND speculative_retry = 'NONE';"
|
248
|
+
else
|
249
|
+
"#{stmt} WITH read_repair_chance = 0.0
|
250
|
+
AND dclocal_read_repair_chance = 0.1
|
251
|
+
AND gc_grace_seconds = 864000
|
252
|
+
AND bloom_filter_fp_chance = 0.01
|
253
|
+
AND caching = { 'keys' : 'ALL', 'rows_per_partition' : 'NONE' }
|
254
|
+
AND comment = ''
|
255
|
+
AND compaction = { 'class' : 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold' : 32, 'min_threshold' : 4 }
|
256
|
+
AND compression = { 'chunk_length_in_kb' : 64, 'class' : 'org.apache.cassandra.io.compress.LZ4Compressor' }
|
257
|
+
AND default_time_to_live = 0
|
258
|
+
AND speculative_retry = '99PERCENTILE'
|
259
|
+
AND min_index_interval = 128
|
260
|
+
AND max_index_interval = 2048
|
261
|
+
AND crc_check_chance = 1.0;
|
262
|
+
"
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
end
|
268
|
+
end
|
269
|
+
end
|
@@ -0,0 +1,306 @@
|
|
1
|
+
gem 'cassandra-driver'
|
2
|
+
require 'cassandra'
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
module CassandraObject
|
6
|
+
module Adapters
|
7
|
+
class CassandraSchemalessAdapter < AbstractAdapter
|
8
|
+
class QueryBuilder
|
9
|
+
|
10
|
+
def initialize(adapter, scope)
|
11
|
+
@adapter = adapter
|
12
|
+
@scope = scope
|
13
|
+
end
|
14
|
+
|
15
|
+
def select_string
|
16
|
+
selected_values = @scope.select_values.select { |sv| sv == :column1 || sv == :values }
|
17
|
+
if selected_values.any?
|
18
|
+
(['KEY'] | selected_values) * ','
|
19
|
+
else
|
20
|
+
'*'
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_query_async
|
25
|
+
# empty ids
|
26
|
+
return nil if !@scope.id_values.present? && !@scope.where_values.present? && !@scope.is_all && !@scope.limit_value.present?
|
27
|
+
|
28
|
+
if @scope.id_values.empty?
|
29
|
+
str = [
|
30
|
+
"SELECT #{select_string} FROM #{@scope.klass.column_family}",
|
31
|
+
where_string_async(nil)
|
32
|
+
]
|
33
|
+
str << "ALLOW FILTERING" if @scope.klass.allow_filtering
|
34
|
+
return [] << str.delete_if(&:blank?) * ' '
|
35
|
+
end
|
36
|
+
@scope.id_values.map { |id|
|
37
|
+
str = [
|
38
|
+
"SELECT #{select_string} FROM #{@scope.klass.column_family}",
|
39
|
+
where_string_async(id)
|
40
|
+
]
|
41
|
+
str << "ALLOW FILTERING" if @scope.klass.allow_filtering
|
42
|
+
str.delete_if(&:blank?) * ' '
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
46
|
+
def where_string_async(id)
|
47
|
+
conditions = []
|
48
|
+
conditions << "#{@adapter.primary_key_column} = '#{id}'" if !id.nil?
|
49
|
+
conditions += @scope.select_values.select { |sv| sv != :column1 }.map { |sv| 'column1 = ?' }
|
50
|
+
conditions += @scope.where_values.select.each_with_index { |_, i| i.even? }
|
51
|
+
return conditions.any? ? "WHERE #{conditions.join(' AND ')}" : nil
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
def primary_key_column
|
57
|
+
'key'
|
58
|
+
end
|
59
|
+
|
60
|
+
def cassandra_cluster_options
|
61
|
+
cluster_options = config.slice(*[
|
62
|
+
:auth_provider,
|
63
|
+
:client_cert,
|
64
|
+
:compression,
|
65
|
+
:compressor,
|
66
|
+
:connect_timeout,
|
67
|
+
:connections_per_local_node,
|
68
|
+
:connections_per_remote_node,
|
69
|
+
:consistency,
|
70
|
+
:credentials,
|
71
|
+
:futures_factory,
|
72
|
+
:hosts,
|
73
|
+
:load_balancing_policy,
|
74
|
+
:logger,
|
75
|
+
:page_size,
|
76
|
+
:passphrase,
|
77
|
+
:password,
|
78
|
+
:port,
|
79
|
+
:private_key,
|
80
|
+
:protocol_version,
|
81
|
+
:reconnection_policy,
|
82
|
+
:retry_policy,
|
83
|
+
:schema_refresh_delay,
|
84
|
+
:schema_refresh_timeout,
|
85
|
+
:server_cert,
|
86
|
+
:ssl,
|
87
|
+
:timeout,
|
88
|
+
:trace,
|
89
|
+
:username
|
90
|
+
])
|
91
|
+
|
92
|
+
|
93
|
+
{
|
94
|
+
load_balancing_policy: 'Cassandra::LoadBalancing::Policies::%s',
|
95
|
+
reconnection_policy: 'Cassandra::Reconnection::Policies::%s',
|
96
|
+
retry_policy: 'Cassandra::Retry::Policies::%s'
|
97
|
+
}.each do |policy_key, class_template|
|
98
|
+
if cluster_options[policy_key]
|
99
|
+
cluster_options[policy_key] = (class_template % [policy_key.classify]).constantize
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Setting defaults
|
104
|
+
cluster_options.merge!({
|
105
|
+
max_schema_agreement_wait: 1,
|
106
|
+
consistency: cluster_options[:consistency]||:quorum,
|
107
|
+
protocol_version: cluster_options[:protocol_version]||3,
|
108
|
+
page_size: cluster_options[:page_size] || 10000
|
109
|
+
})
|
110
|
+
return cluster_options
|
111
|
+
end
|
112
|
+
|
113
|
+
|
114
|
+
def connection
|
115
|
+
@connection ||= begin
|
116
|
+
cluster = Cassandra.cluster cassandra_cluster_options
|
117
|
+
cluster.connect config[:keyspace]
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def execute(statement, arguments = [])
|
122
|
+
ActiveSupport::Notifications.instrument('cql.cassandra_object', cql: statement) do
|
123
|
+
connection.execute statement, arguments: arguments, consistency: consistency, page_size: config[:page_size]
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def execute_async(queries, arguments = [], per_page = nil, next_cursor = nil)
|
128
|
+
per_page ||= config[:page_size]
|
129
|
+
futures = queries.map { |q|
|
130
|
+
ActiveSupport::Notifications.instrument('cql.cassandra_object', cql: q) do
|
131
|
+
connection.execute_async q, arguments: arguments, consistency: consistency, page_size: per_page, paging_state: next_cursor
|
132
|
+
end
|
133
|
+
}
|
134
|
+
futures.map do |future|
|
135
|
+
rows = future.get
|
136
|
+
rows
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def pre_select(scope, per_page = nil, next_cursor = nil)
|
141
|
+
query = "SELECT DISTINCT #{primary_key_column} FROM #{scope.klass.column_family}"
|
142
|
+
ids = []
|
143
|
+
new_next_cursor = nil
|
144
|
+
execute_async([query], nil, per_page, next_cursor).map do |item|
|
145
|
+
item.rows.map { |x| ids << x[primary_key_column] }
|
146
|
+
new_next_cursor = item.paging_state if !item.last_page?
|
147
|
+
end
|
148
|
+
return {ids: ids, new_next_cursor: new_next_cursor}
|
149
|
+
end
|
150
|
+
|
151
|
+
def select(scope)
|
152
|
+
queries = QueryBuilder.new(self, scope).to_query_async
|
153
|
+
queries.compact! if queries.present?
|
154
|
+
raise CassandraObject::RecordNotFound if !queries.present?
|
155
|
+
|
156
|
+
arguments = scope.select_values.select { |sv| sv != :column1 }.map(&:to_s)
|
157
|
+
arguments += scope.where_values.select.each_with_index { |_, i| i.odd? }.reject { |c| c.empty? }.map(&:to_s)
|
158
|
+
records = execute_async(queries, arguments).map do |item|
|
159
|
+
# pagination
|
160
|
+
elems = []
|
161
|
+
loop do
|
162
|
+
item.rows.map { |x| elems << x }
|
163
|
+
break if item.last_page?
|
164
|
+
item = item.next_page
|
165
|
+
end
|
166
|
+
elems
|
167
|
+
end
|
168
|
+
records.flatten!
|
169
|
+
end
|
170
|
+
|
171
|
+
def insert(table, id, attributes, ttl = nil)
|
172
|
+
write(table, id, attributes, ttl)
|
173
|
+
end
|
174
|
+
|
175
|
+
def update(table, id, attributes, ttl = nil)
|
176
|
+
write(table, id, attributes, ttl)
|
177
|
+
end
|
178
|
+
|
179
|
+
def write(table, id, attributes, ttl)
|
180
|
+
queries = []
|
181
|
+
# puts attributes
|
182
|
+
attributes.each do |column, value|
|
183
|
+
if !value.nil?
|
184
|
+
query = "INSERT INTO #{table} (#{primary_key_column},column1,value) VALUES (?,?,?)"
|
185
|
+
query += " USING TTL #{ttl.to_s}" if !ttl.nil?
|
186
|
+
args = [id.to_s, column.to_s, value.to_s]
|
187
|
+
|
188
|
+
queries << {query: query, arguments: args}
|
189
|
+
else
|
190
|
+
queries << {query: "DELETE FROM #{table} WHERE #{primary_key_column} = ? AND column1= ?", arguments: [id.to_s, column.to_s]}
|
191
|
+
end
|
192
|
+
end
|
193
|
+
execute_batchable(queries)
|
194
|
+
end
|
195
|
+
|
196
|
+
|
197
|
+
def delete(table, ids)
|
198
|
+
ids = [ids] if !ids.is_a?(Array)
|
199
|
+
arguments = nil
|
200
|
+
arguments = ids if ids.size == 1
|
201
|
+
statement = "DELETE FROM #{table} WHERE #{create_ids_where_clause(ids)}" #.gsub('?', ids.map { |id| "'#{id}'" }.join(','))
|
202
|
+
execute(statement, arguments)
|
203
|
+
end
|
204
|
+
|
205
|
+
def execute_batch(statements)
|
206
|
+
raise 'No can do' if statements.empty?
|
207
|
+
batch = connection.batch do |b|
|
208
|
+
statements.each do |statement|
|
209
|
+
b.add(statement[:query], arguments: statement[:arguments])
|
210
|
+
end
|
211
|
+
end
|
212
|
+
connection.execute(batch, page_size: config[:page_size])
|
213
|
+
end
|
214
|
+
|
215
|
+
# SCHEMA
|
216
|
+
def create_table(table_name, params = {})
|
217
|
+
stmt = "CREATE TABLE #{table_name} (" +
|
218
|
+
'key text,' +
|
219
|
+
'column1 text,' +
|
220
|
+
'value text,' +
|
221
|
+
'PRIMARY KEY (key, column1)' +
|
222
|
+
')'
|
223
|
+
# WITH COMPACT STORAGE
|
224
|
+
schema_execute statement_with_options(stmt, params[:options]), config[:keyspace]
|
225
|
+
end
|
226
|
+
|
227
|
+
def drop_table(table_name, confirm = false)
|
228
|
+
count = (schema_execute "SELECT count(*) FROM #{table_name}", config[:keyspace]).rows.first['count']
|
229
|
+
if confirm || count == 0
|
230
|
+
schema_execute "DROP TABLE #{table_name}", config[:keyspace]
|
231
|
+
else
|
232
|
+
raise "The table #{table_name} is not empty! If you want to drop it add the option confirm = true"
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
def schema_execute(cql, keyspace)
|
237
|
+
schema_db = Cassandra.cluster cassandra_cluster_options
|
238
|
+
connection = schema_db.connect keyspace
|
239
|
+
connection.execute cql, consistency: consistency
|
240
|
+
end
|
241
|
+
|
242
|
+
def cassandra_version
|
243
|
+
@cassandra_version ||= execute('select release_version from system.local').rows.first['release_version'].to_f
|
244
|
+
end
|
245
|
+
|
246
|
+
# /SCHEMA
|
247
|
+
|
248
|
+
def consistency
|
249
|
+
defined?(@consistency) ? @consistency : nil
|
250
|
+
end
|
251
|
+
|
252
|
+
def consistency=(val)
|
253
|
+
@consistency = val
|
254
|
+
end
|
255
|
+
|
256
|
+
def statement_create_with_options(stmt, options)
|
257
|
+
if !options.nil?
|
258
|
+
statement_with_options stmt, options
|
259
|
+
else
|
260
|
+
# standard
|
261
|
+
if cassandra_version < 3
|
262
|
+
"#{stmt} WITH COMPACT STORAGE
|
263
|
+
AND bloom_filter_fp_chance = 0.001
|
264
|
+
AND CLUSTERING ORDER BY (column1 ASC)
|
265
|
+
AND caching = '{\"keys\":\"ALL\", \"rows_per_partition\":\"NONE\"}'
|
266
|
+
AND comment = ''
|
267
|
+
AND compaction = {'min_sstable_size': '52428800', 'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy'}
|
268
|
+
AND compression = {'chunk_length_kb': '64', 'sstable_compression': 'org.apache.cassandra.io.compress.LZ4Compressor'}
|
269
|
+
AND dclocal_read_repair_chance = 0.0
|
270
|
+
AND default_time_to_live = 0
|
271
|
+
AND gc_grace_seconds = 864000
|
272
|
+
AND max_index_interval = 2048
|
273
|
+
AND memtable_flush_period_in_ms = 0
|
274
|
+
AND min_index_interval = 128
|
275
|
+
AND read_repair_chance = 1.0
|
276
|
+
AND speculative_retry = 'NONE';"
|
277
|
+
else
|
278
|
+
"#{stmt} WITH read_repair_chance = 0.0
|
279
|
+
AND dclocal_read_repair_chance = 0.1
|
280
|
+
AND gc_grace_seconds = 864000
|
281
|
+
AND bloom_filter_fp_chance = 0.01
|
282
|
+
AND caching = { 'keys' : 'ALL', 'rows_per_partition' : 'NONE' }
|
283
|
+
AND comment = ''
|
284
|
+
AND compaction = { 'class' : 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold' : 32, 'min_threshold' : 4 }
|
285
|
+
AND compression = { 'chunk_length_in_kb' : 64, 'class' : 'org.apache.cassandra.io.compress.LZ4Compressor' }
|
286
|
+
AND default_time_to_live = 0
|
287
|
+
AND speculative_retry = '99PERCENTILE'
|
288
|
+
AND min_index_interval = 128
|
289
|
+
AND max_index_interval = 2048
|
290
|
+
AND crc_check_chance = 1.0;
|
291
|
+
"
|
292
|
+
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
def create_ids_where_clause(ids)
|
298
|
+
return ids if ids.empty?
|
299
|
+
ids = ids.first if ids.is_a?(Array) && ids.one?
|
300
|
+
sql = ids.is_a?(Array) ? "#{primary_key_column} IN (#{ids.map { |id| "'#{id}'" }.join(',')})" : "#{primary_key_column} = ?"
|
301
|
+
return sql
|
302
|
+
end
|
303
|
+
|
304
|
+
end
|
305
|
+
end
|
306
|
+
end
|