cequel 0.0.0 → 0.4.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.
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