cequel 0.0.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. data/lib/cequel.rb +16 -0
  2. data/lib/cequel/batch.rb +58 -0
  3. data/lib/cequel/cql_row_specification.rb +22 -0
  4. data/lib/cequel/data_set.rb +346 -0
  5. data/lib/cequel/errors.rb +4 -0
  6. data/lib/cequel/keyspace.rb +106 -0
  7. data/lib/cequel/model.rb +95 -0
  8. data/lib/cequel/model/associations.rb +120 -0
  9. data/lib/cequel/model/callbacks.rb +32 -0
  10. data/lib/cequel/model/class_internals.rb +48 -0
  11. data/lib/cequel/model/column.rb +20 -0
  12. data/lib/cequel/model/dictionary.rb +202 -0
  13. data/lib/cequel/model/dirty.rb +53 -0
  14. data/lib/cequel/model/dynamic.rb +31 -0
  15. data/lib/cequel/model/errors.rb +13 -0
  16. data/lib/cequel/model/inheritable.rb +48 -0
  17. data/lib/cequel/model/instance_internals.rb +23 -0
  18. data/lib/cequel/model/local_association.rb +42 -0
  19. data/lib/cequel/model/magic.rb +79 -0
  20. data/lib/cequel/model/mass_assignment_security.rb +21 -0
  21. data/lib/cequel/model/naming.rb +17 -0
  22. data/lib/cequel/model/observer.rb +42 -0
  23. data/lib/cequel/model/persistence.rb +173 -0
  24. data/lib/cequel/model/properties.rb +143 -0
  25. data/lib/cequel/model/railtie.rb +33 -0
  26. data/lib/cequel/model/remote_association.rb +40 -0
  27. data/lib/cequel/model/scope.rb +362 -0
  28. data/lib/cequel/model/scoped.rb +50 -0
  29. data/lib/cequel/model/subclass_internals.rb +45 -0
  30. data/lib/cequel/model/timestamps.rb +52 -0
  31. data/lib/cequel/model/translation.rb +17 -0
  32. data/lib/cequel/model/validations.rb +50 -0
  33. data/lib/cequel/new_relic_instrumentation.rb +22 -0
  34. data/lib/cequel/row_specification.rb +63 -0
  35. data/lib/cequel/statement.rb +23 -0
  36. data/lib/cequel/version.rb +3 -0
  37. data/spec/environment.rb +3 -0
  38. data/spec/examples/data_set_spec.rb +382 -0
  39. data/spec/examples/keyspace_spec.rb +63 -0
  40. data/spec/examples/model/associations_spec.rb +109 -0
  41. data/spec/examples/model/callbacks_spec.rb +79 -0
  42. data/spec/examples/model/dictionary_spec.rb +413 -0
  43. data/spec/examples/model/dirty_spec.rb +39 -0
  44. data/spec/examples/model/dynamic_spec.rb +41 -0
  45. data/spec/examples/model/inheritable_spec.rb +45 -0
  46. data/spec/examples/model/magic_spec.rb +199 -0
  47. data/spec/examples/model/mass_assignment_security_spec.rb +13 -0
  48. data/spec/examples/model/naming_spec.rb +9 -0
  49. data/spec/examples/model/observer_spec.rb +86 -0
  50. data/spec/examples/model/persistence_spec.rb +201 -0
  51. data/spec/examples/model/properties_spec.rb +81 -0
  52. data/spec/examples/model/scope_spec.rb +677 -0
  53. data/spec/examples/model/serialization_spec.rb +20 -0
  54. data/spec/examples/model/spec_helper.rb +12 -0
  55. data/spec/examples/model/timestamps_spec.rb +52 -0
  56. data/spec/examples/model/translation_spec.rb +23 -0
  57. data/spec/examples/model/validations_spec.rb +86 -0
  58. data/spec/examples/spec_helper.rb +9 -0
  59. data/spec/models/asset.rb +21 -0
  60. data/spec/models/asset_observer.rb +5 -0
  61. data/spec/models/blog.rb +14 -0
  62. data/spec/models/blog_posts.rb +6 -0
  63. data/spec/models/category.rb +9 -0
  64. data/spec/models/comment.rb +12 -0
  65. data/spec/models/photo.rb +5 -0
  66. data/spec/models/post.rb +88 -0
  67. data/spec/models/post_comments.rb +14 -0
  68. data/spec/models/post_observer.rb +43 -0
  69. data/spec/support/helpers.rb +26 -0
  70. data/spec/support/result_stub.rb +27 -0
  71. metadata +125 -23
@@ -0,0 +1,16 @@
1
+ require 'active_support/core_ext'
2
+ require 'cassandra-cql'
3
+
4
+ require 'cequel/batch'
5
+ require 'cequel/errors'
6
+ require 'cequel/cql_row_specification'
7
+ require 'cequel/data_set'
8
+ require 'cequel/keyspace'
9
+ require 'cequel/row_specification'
10
+ require 'cequel/statement'
11
+
12
+ module Cequel
13
+ def self.connect(configuration = nil)
14
+ Keyspace.new(configuration || {})
15
+ end
16
+ end
@@ -0,0 +1,58 @@
1
+ require 'stringio'
2
+
3
+ module Cequel
4
+
5
+ #
6
+ # Encapsulates a batch operation
7
+ #
8
+ # @see Keyspace::batch
9
+ #
10
+ class Batch
11
+
12
+ #
13
+ # @param keyspace [Keyspace] the keyspace that this batch will be executed on
14
+ # @param options [Hash]
15
+ # @option options (see Keyspace#batch)
16
+ # @see Keyspace#batch
17
+ # @todo support batch-level consistency options
18
+ #
19
+ def initialize(keyspace, options = {})
20
+ @keyspace = keyspace
21
+ @auto_apply = options[:auto_apply]
22
+ reset
23
+ end
24
+
25
+ #
26
+ # Add a statement to the batch.
27
+ #
28
+ # @param (see Keyspace#execute)
29
+ #
30
+ def execute(cql, *bind_vars)
31
+ @statement.append("#{cql}\n", *bind_vars)
32
+ @statement_count += 1
33
+ if @auto_apply && @statement_count >= @auto_apply
34
+ apply
35
+ reset
36
+ end
37
+ end
38
+
39
+ #
40
+ # Send the batch to Cassandra
41
+ #
42
+ def apply
43
+ return if @statement_count.zero?
44
+ @statement.append("APPLY BATCH\n")
45
+ @keyspace.execute(*@statement.args)
46
+ end
47
+
48
+ private
49
+
50
+ def reset
51
+ @statement = Statement.new
52
+ @statement.append("BEGIN BATCH\n")
53
+ @statement_count = 0
54
+ end
55
+
56
+ end
57
+
58
+ end
@@ -0,0 +1,22 @@
1
+ module Cequel
2
+
3
+ #
4
+ # @api private
5
+ #
6
+ class CqlRowSpecification
7
+
8
+ def self.build(condition, bind_vars)
9
+ [new(condition, bind_vars)]
10
+ end
11
+
12
+ def initialize(condition, bind_vars)
13
+ @condition, @bind_vars = condition, bind_vars
14
+ end
15
+
16
+ def cql
17
+ [@condition, *@bind_vars]
18
+ end
19
+
20
+ end
21
+
22
+ end
@@ -0,0 +1,346 @@
1
+ module Cequel
2
+
3
+ #
4
+ # Encapsulates a data set, specified as a column family and optionally
5
+ # various query elements.
6
+ #
7
+ # @todo Support ALTER, CREATE, CREATE INDEX, DROP
8
+ #
9
+ class DataSet
10
+
11
+ include Enumerable
12
+
13
+ # @return [Keyspace] the keyspace this data set lives in
14
+ attr_reader :keyspace
15
+
16
+ # @return [Symbol] the name of the column family this data set draws from
17
+ attr_reader :column_family
18
+
19
+ #
20
+ # @param column_family [Symbol] column family for this data set
21
+ # @param keyspace [Keyspace] keyspace this data set's column family lives in
22
+ #
23
+ # @see Keyspace#[]
24
+ #
25
+ def initialize(column_family, keyspace)
26
+ @column_family, @keyspace = column_family, keyspace
27
+ @select_columns, @select_options, @row_specifications = [], {}, []
28
+ end
29
+
30
+ #
31
+ # Insert a row into the column family.
32
+ #
33
+ # @param [Hash] data column-value pairs. The first entry *must* be the key column.
34
+ # @param [Options] options options for persisting the row
35
+ # @option (see #generate_upsert_options)
36
+ # @note if a enclosed in a Keyspace#batch block, this method will be executed as part of the batch.
37
+ #
38
+ def insert(data, options = {})
39
+ options.symbolize_keys!
40
+ cql = "INSERT INTO #{@column_family}" <<
41
+ " (?) VALUES (?)" <<
42
+ generate_upsert_options(options)
43
+
44
+ @keyspace.write(cql, data.keys, data.values)
45
+ end
46
+
47
+ #
48
+ # Update rows
49
+ #
50
+ # @param [Hash] data column-value pairs
51
+ # @param [Options] options options for persisting the column data
52
+ # @option (see #generate_upsert_options)
53
+ # @note if a enclosed in a Keyspace#batch block, this method will be executed as part of the batch.
54
+ #
55
+ # @todo support counter columns
56
+ #
57
+ def update(data, options = {})
58
+ statement = Statement.new.
59
+ append("UPDATE #{@column_family}").
60
+ append(generate_upsert_options(options)).
61
+ append(" SET " << data.keys.map { |k| "? = ?" }.join(', '), *data.to_a.flatten).
62
+ append(*row_specifications_cql)
63
+
64
+ @keyspace.write(*statement.args)
65
+ rescue EmptySubquery
66
+ # Noop -- no rows to update
67
+ end
68
+
69
+ #
70
+ # Delete data from the column family
71
+ #
72
+ # @param columns zero or more columns to delete. Deletes the entire row if none specified.
73
+ # @param options persistence options
74
+ # @note if a enclosed in a Keyspace#batch block, this method will be executed as part of the batch.
75
+ #
76
+ def delete(*columns)
77
+ options = columns.extract_options!
78
+ column_aliases = columns.empty? ? '' : " #{columns.join(', ')}"
79
+ statement = Statement.new.append('DELETE')
80
+ statement = statement.append(' ?', columns) if columns.any?
81
+ statement = statement.
82
+ append(" FROM #{@column_family}").
83
+ append(generate_upsert_options(options)).
84
+ append(*row_specifications_cql)
85
+
86
+ @keyspace.write(*statement.args)
87
+ rescue EmptySubquery
88
+ # Noop -- no rows to delete
89
+ end
90
+
91
+ #
92
+ # Remove all data from the column family.
93
+ #
94
+ # @note This method always executes immediately, even if called within a batch block. This method does not respect scoped row specifications.
95
+ # @see #delete
96
+ #
97
+ def truncate
98
+ @keyspace.execute("TRUNCATE #{@column_family}")
99
+ end
100
+
101
+ #
102
+ # Select specified columns from this data set.
103
+ #
104
+ # @param *columns [Symbol,Array] columns to select
105
+ # @return [DataSet] new data set scoped to specified columns
106
+ #
107
+ def select(*columns)
108
+ options = columns.extract_options!.symbolize_keys
109
+ clone.tap do |data_set|
110
+ if columns.length == 1 && Range === columns.first
111
+ range = columns.first
112
+ options[:from] = range.first
113
+ options[:to] = range.last
114
+ else
115
+ data_set.select_columns.concat(columns.flatten)
116
+ end
117
+ data_set.select_options.merge!(options)
118
+ end
119
+ end
120
+
121
+ #
122
+ # Select specified columns from this data set, overriding chained scope.
123
+ #
124
+ # @param *columns [Symbol,Array] columns to select
125
+ # @return [DataSet] new data set scoped to specified columns
126
+ #
127
+ def select!(*columns)
128
+ clone.tap do |data_set|
129
+ data_set.select_columns.replace(columns.flatten)
130
+ end
131
+ end
132
+
133
+ #
134
+ # Add consistency option for data set retrieval
135
+ #
136
+ # @param consistency [:one,:quorum,:local_quorum,:each_quorum]
137
+ # @return [DataSet] new data set with specified consistency
138
+ #
139
+ def consistency(consistency)
140
+ clone.tap { |data_set| data_set.consistency = consistency.to_sym }
141
+ end
142
+
143
+ #
144
+ # Add a row_specification to this data set
145
+ #
146
+ # @param row_specification [Hash, String] row_specification statement
147
+ # @param *bind_vars bind variables, only if using a CQL string row_specification
148
+ # @return [DataSet] new data set scoped to this row_specification
149
+ # @example Using a simple hash
150
+ # DB[:posts].where(:title => 'Hey')
151
+ # @example Using a CQL string
152
+ # DB[:posts].where("title = 'Hey'")
153
+ # @example Using a CQL string with bind variables
154
+ # DB[:posts].where('title = ?', 'Hey')
155
+ # @example Use another data set as an input -- inner data set must return a single column per row!
156
+ # DB[:blogs].where(:id => DB[:posts].select(:blog_id).where(:title => 'Hey'))
157
+ #
158
+ def where(row_specification, *bind_vars)
159
+ clone.tap do |data_set|
160
+ data_set.row_specifications.
161
+ concat(build_row_specifications(row_specification, bind_vars))
162
+ end
163
+ end
164
+
165
+ def where!(row_specification, *bind_vars)
166
+ clone.tap do |data_set|
167
+ data_set.row_specifications.
168
+ replace(build_row_specifications(row_specification, bind_vars))
169
+ end
170
+ end
171
+
172
+ #
173
+ # Limit the number of rows returned by this data set
174
+ #
175
+ # @param limit [Integer] maximum number of rows to return
176
+ # @return [DataSet] new data set scoped with given limit
177
+ #
178
+ def limit(limit)
179
+ clone.tap { |data_set| data_set.limit = limit }
180
+ end
181
+
182
+ #
183
+ # Enumerate over rows in this data set. Along with #each, all other
184
+ # Enumerable methods are implemented.
185
+ #
186
+ # @yield [Hash] result rows
187
+ # @return [Enumerator] enumerator for rows, if no block given
188
+ #
189
+ def each
190
+ if block_given?
191
+ begin
192
+ @keyspace.execute(*cql).fetch do |row|
193
+ yield row.to_hash.with_indifferent_access
194
+ end
195
+ rescue EmptySubquery
196
+ # Noop -- yield no results
197
+ end
198
+ else
199
+ enum_for(:each)
200
+ end
201
+ end
202
+
203
+ #
204
+ # @return [Hash] the first row in this data set
205
+ #
206
+ def first
207
+ row = @keyspace.execute(*limit(1).cql).fetch_row
208
+ row.to_hash.with_indifferent_access if row
209
+ rescue EmptySubquery
210
+ nil
211
+ end
212
+
213
+ #
214
+ # @return [Fixnum] the number of rows in this data set
215
+ #
216
+ def count
217
+ @keyspace.execute(*count_cql).fetch_row['count']
218
+ rescue EmptySubquery
219
+ 0
220
+ end
221
+
222
+ #
223
+ # @raise [EmptySubquery] if row specifications use a subquery that returns no results
224
+ # @return [String] CQL select statement encoding this data set's scope.
225
+ #
226
+ def cql
227
+ statement = Statement.new.
228
+ append(*select_cql).
229
+ append(" FROM #{@column_family}").
230
+ append(consistency_cql).
231
+ append(*row_specifications_cql).
232
+ append(limit_cql).
233
+ args
234
+ end
235
+
236
+ #
237
+ # @return [String] CQL statement to get count of rows in this data set
238
+ #
239
+ def count_cql
240
+ Statement.new.
241
+ append("SELECT COUNT(*) FROM #{@column_family}").
242
+ append(consistency_cql).
243
+ append(*row_specifications_cql).
244
+ append(limit_cql).args
245
+ end
246
+
247
+ def inspect
248
+ "#<#{self.class.name}: #{CassandraCQL::Statement.sanitize(cql.first, cql[1..-1])}>"
249
+ end
250
+
251
+ def ==(other)
252
+ cql == other.cql
253
+ end
254
+
255
+ attr_reader :select_columns, :select_options, :row_specifications
256
+ attr_writer :consistency, :limit
257
+
258
+ private
259
+
260
+ def initialize_copy(source)
261
+ super
262
+ @select_columns = source.select_columns.clone
263
+ @select_options = source.select_options.clone
264
+ @row_specifications = source.row_specifications.clone
265
+ end
266
+
267
+ #
268
+ # Generate CQL option statement for inserts and updates
269
+ #
270
+ # @param [Hash] options options for insert
271
+ # @option options [Symbol,String] :consistency required consistency for the write
272
+ # @option options [Integer] :ttl time-to-live in seconds for the written data
273
+ # @option options [Time,Integer] :timestamp the timestamp associated with the column values
274
+ #
275
+ def generate_upsert_options(options)
276
+ if options.empty?
277
+ ''
278
+ else
279
+ ' USING ' <<
280
+ options.map do |key, value|
281
+ serialized_value =
282
+ case key
283
+ when :consistency then value.to_s.upcase
284
+ when :timestamp then value.to_i
285
+ else value
286
+ end
287
+ "#{key.to_s.upcase} #{serialized_value}"
288
+ end.join(' AND ')
289
+ end
290
+ end
291
+
292
+ def select_cql
293
+ ['SELECT '].tap do |args|
294
+ cql = args.first
295
+ if @select_options[:first]
296
+ cql << "FIRST #{@select_options[:first]} "
297
+ elsif @select_options[:last]
298
+ cql << "FIRST #{@select_options[:last]} REVERSED "
299
+ end
300
+ if @select_options[:from] || @select_options[:to]
301
+ cql << '?..?'
302
+ args << (@select_options[:from] || '') << (@select_options[:to] || '')
303
+ elsif @select_columns.any?
304
+ cql << '?'
305
+ args << @select_columns
306
+ else
307
+ cql << '*'
308
+ end
309
+ end
310
+ end
311
+
312
+ def consistency_cql
313
+ if @consistency
314
+ " USING CONSISTENCY #{@consistency.upcase}"
315
+ else ''
316
+ end
317
+ end
318
+
319
+ def row_specifications_cql
320
+ if @row_specifications.any?
321
+ cql_fragments, bind_vars = [], []
322
+ @row_specifications.each do |spec|
323
+ cql_with_vars = spec.cql
324
+ cql_fragments << cql_with_vars.shift
325
+ bind_vars.concat(cql_with_vars)
326
+ end
327
+ [" WHERE #{cql_fragments.join(' AND ')}", *bind_vars]
328
+ else ['']
329
+ end
330
+ end
331
+
332
+ def limit_cql
333
+ @limit ? " LIMIT #{@limit}" : ''
334
+ end
335
+
336
+ def build_row_specifications(row_specification, bind_vars)
337
+ case row_specification
338
+ when Hash then RowSpecification.build(row_specification)
339
+ when String then CqlRowSpecification.build(row_specification, bind_vars)
340
+ else raise ArgumentError, "Invalid argument #{row_specification.inspect}; expected Hash or String"
341
+ end
342
+ end
343
+
344
+ end
345
+
346
+ end
@@ -0,0 +1,4 @@
1
+ module Cequel
2
+ Error = Class.new(StandardError)
3
+ EmptySubquery = Class.new(Error)
4
+ end