extendi-cassandra_object 1.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 +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
|