cequel 1.0.0.rc1 → 1.0.0.rc2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/lib/cequel.rb +18 -0
  3. data/lib/cequel/errors.rb +8 -4
  4. data/lib/cequel/metal.rb +14 -0
  5. data/lib/cequel/metal/batch.rb +21 -11
  6. data/lib/cequel/metal/batch_manager.rb +74 -0
  7. data/lib/cequel/metal/cql_row_specification.rb +19 -6
  8. data/lib/cequel/metal/data_set.rb +400 -163
  9. data/lib/cequel/metal/deleter.rb +45 -11
  10. data/lib/cequel/metal/incrementer.rb +23 -10
  11. data/lib/cequel/metal/inserter.rb +19 -6
  12. data/lib/cequel/metal/keyspace.rb +82 -159
  13. data/lib/cequel/metal/logger.rb +71 -0
  14. data/lib/cequel/metal/logging.rb +47 -0
  15. data/lib/cequel/metal/new_relic_instrumentation.rb +26 -0
  16. data/lib/cequel/metal/row.rb +36 -10
  17. data/lib/cequel/metal/row_specification.rb +21 -8
  18. data/lib/cequel/metal/statement.rb +30 -6
  19. data/lib/cequel/metal/updater.rb +89 -12
  20. data/lib/cequel/metal/writer.rb +23 -14
  21. data/lib/cequel/record.rb +52 -6
  22. data/lib/cequel/record/association_collection.rb +13 -6
  23. data/lib/cequel/record/associations.rb +146 -54
  24. data/lib/cequel/record/belongs_to_association.rb +34 -7
  25. data/lib/cequel/record/bound.rb +69 -12
  26. data/lib/cequel/record/bulk_writes.rb +29 -1
  27. data/lib/cequel/record/callbacks.rb +22 -6
  28. data/lib/cequel/record/collection.rb +273 -36
  29. data/lib/cequel/record/configuration_generator.rb +5 -0
  30. data/lib/cequel/record/data_set_builder.rb +86 -0
  31. data/lib/cequel/record/dirty.rb +11 -8
  32. data/lib/cequel/record/errors.rb +38 -4
  33. data/lib/cequel/record/has_many_association.rb +42 -9
  34. data/lib/cequel/record/lazy_record_collection.rb +39 -10
  35. data/lib/cequel/record/mass_assignment.rb +14 -6
  36. data/lib/cequel/record/persistence.rb +157 -20
  37. data/lib/cequel/record/properties.rb +147 -24
  38. data/lib/cequel/record/railtie.rb +15 -2
  39. data/lib/cequel/record/record_set.rb +504 -75
  40. data/lib/cequel/record/schema.rb +77 -13
  41. data/lib/cequel/record/scoped.rb +16 -11
  42. data/lib/cequel/record/secondary_indexes.rb +42 -6
  43. data/lib/cequel/record/tasks.rb +2 -1
  44. data/lib/cequel/record/validations.rb +51 -11
  45. data/lib/cequel/schema.rb +9 -0
  46. data/lib/cequel/schema/column.rb +172 -33
  47. data/lib/cequel/schema/create_table_dsl.rb +62 -31
  48. data/lib/cequel/schema/keyspace.rb +106 -7
  49. data/lib/cequel/schema/migration_validator.rb +128 -0
  50. data/lib/cequel/schema/table.rb +183 -20
  51. data/lib/cequel/schema/table_property.rb +92 -34
  52. data/lib/cequel/schema/table_reader.rb +45 -15
  53. data/lib/cequel/schema/table_synchronizer.rb +101 -43
  54. data/lib/cequel/schema/table_updater.rb +114 -19
  55. data/lib/cequel/schema/table_writer.rb +31 -13
  56. data/lib/cequel/schema/update_table_dsl.rb +71 -40
  57. data/lib/cequel/type.rb +214 -53
  58. data/lib/cequel/util.rb +6 -9
  59. data/lib/cequel/version.rb +2 -1
  60. data/spec/examples/record/associations_spec.rb +12 -12
  61. data/spec/examples/record/persistence_spec.rb +5 -5
  62. data/spec/examples/record/record_set_spec.rb +62 -50
  63. data/spec/examples/schema/table_synchronizer_spec.rb +37 -11
  64. data/spec/examples/schema/table_updater_spec.rb +3 -3
  65. data/spec/examples/spec_helper.rb +2 -11
  66. data/spec/examples/type_spec.rb +3 -3
  67. metadata +23 -4
  68. data/lib/cequel/new_relic_instrumentation.rb +0 -22
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6d15e60b9c3a9725632fe70d140449a03dc1e033
4
- data.tar.gz: cc8dd115c0639388ee13b77437b3a7ae6addb2ef
3
+ metadata.gz: 4988b87b5a5a9fc843dcaaf3bdfb3a5842b20cc8
4
+ data.tar.gz: ce93eaeb725d23f31a22dc3f8e2e84fc4d9ec0e7
5
5
  SHA512:
6
- metadata.gz: 7b710ee3238b02cc47f0b57a9804ee4f729c1cd807e809b7134888f44b066f59330714aaf7460bafae1d702d2eba47f7dff3ac3b8e2f18b30f03bb3e259498c0
7
- data.tar.gz: 870947477a6da3d0f72aae1d6f4154293c41da41b1bf7419c4906737ed86d70ebc9598901a2eec2a121a0412955b7c00e4b5cd03df3dd9228597653b64347bd9
6
+ metadata.gz: b738d10a75bd0f977eeff27c119ccd9d7980c66e2e1bdf004ef74e980dc847514ea7083cd9432f28b4d1099a151c635a71f50cd1f1757d6a20c16c42074504a0
7
+ data.tar.gz: be8264f1142625d8755a149550f68a9c2dc66b93c7a6a562f0c893043dbaf859ab879632911c8b753b96cdd07ea025b2348f12a237e07bdc4c7cad27015093a8
data/lib/cequel.rb CHANGED
@@ -1,3 +1,5 @@
1
+ require 'delegate'
2
+
1
3
  require 'active_support/core_ext'
2
4
  require 'cassandra-cql/1.2'
3
5
  require 'connection_pool'
@@ -9,7 +11,23 @@ require 'cequel/type'
9
11
  require 'cequel/util'
10
12
  require 'cequel/record'
11
13
 
14
+ #
15
+ # Cequel is a library providing robust data modeling and query building
16
+ # capabilities for Cassandra using CQL3.
17
+ #
18
+ # @see Cequel::Record Cequel::Record, an object-row mapper for CQL3
19
+ # @see Cequel::Metal Cequel::Metal, a query builder for CQL3 statements
20
+ # @see Cequel::Schema Cequel::Schema::Keyspace, which provides full read-write
21
+ # access to the database schema defined in Cassandra
22
+ #
12
23
  module Cequel
24
+ #
25
+ # Get a handle to a keyspace
26
+ #
27
+ # @param (see Metal::Keyspace#initialize)
28
+ # @option (see Metal::Keyspace#initialize)
29
+ # @return [Metal::Keyspace] a handle to a keyspace
30
+ #
13
31
  def self.connect(configuration = nil)
14
32
  Metal::Keyspace.new(configuration || {})
15
33
  end
data/lib/cequel/errors.rb CHANGED
@@ -1,6 +1,10 @@
1
1
  module Cequel
2
- Error = Class.new(StandardError)
3
- EmptySubquery = Class.new(Error)
4
- InvalidSchemaMigration = Class.new(Error)
5
- MissingKeyError = Class.new(Error)
2
+ #
3
+ # @since 1.0.0
4
+ #
5
+ # Raised when the schema defined in Cassandra cannot be modified to match
6
+ # the schema defined in the application (e.g., changing the type of a primary
7
+ # key)
8
+ #
9
+ InvalidSchemaMigration = Class.new(StandardError)
6
10
  end
data/lib/cequel/metal.rb CHANGED
@@ -1,7 +1,10 @@
1
1
  require 'cequel/metal/batch'
2
+ require 'cequel/metal/batch_manager'
2
3
  require 'cequel/metal/cql_row_specification'
3
4
  require 'cequel/metal/data_set'
5
+ require 'cequel/metal/logging'
4
6
  require 'cequel/metal/keyspace'
7
+ require 'cequel/metal/logger'
5
8
  require 'cequel/metal/row'
6
9
  require 'cequel/metal/row_specification'
7
10
  require 'cequel/metal/statement'
@@ -12,6 +15,17 @@ require 'cequel/metal/inserter'
12
15
  require 'cequel/metal/updater'
13
16
 
14
17
  module Cequel
18
+ #
19
+ # The Cequel::Metal layer provides a low-level interface to the Cassandra
20
+ # database. Most of the functionality is exposed via the DataSet class, which
21
+ # encapsulates a table with optional filtering, and provides an interface for
22
+ # constructing read and write queries. The Metal layer is not schema-aware,
23
+ # and relies on the user to construct valid CQL queries.
24
+ #
25
+ # @see Keyspace
26
+ # @see DataSet
27
+ # @since 1.0.0
28
+ #
15
29
  module Metal
16
30
  end
17
31
  end
@@ -1,26 +1,30 @@
1
1
  require 'stringio'
2
2
 
3
3
  module Cequel
4
-
5
4
  module Metal
6
-
7
5
  #
8
6
  # Encapsulates a batch operation
9
7
  #
10
8
  # @see Keyspace::batch
9
+ # @api private
11
10
  #
12
11
  class Batch
13
-
14
12
  #
15
- # @param keyspace [Keyspace] the keyspace that this batch will be executed on
13
+ # @param keyspace [Keyspace] the keyspace that this batch will be
14
+ # executed on
16
15
  # @param options [Hash]
17
- # @option options (see Keyspace#batch)
16
+ # @option options [Integer] :auto_apply If specified, flush the batch
17
+ # after this many statements have been added.
18
+ # @option options [Boolean] :unlogged (false) Whether to use an [unlogged
19
+ # batch](http://www.datastax.com/documentation/cql/3.0/webhelp/cql/cql_reference/batch_r.html).
20
+ # Logged batches guarantee atomicity (but not isolation) at the
21
+ # cost of a performance penalty; unlogged batches are useful for bulk
22
+ # write operations but behave the same as discrete writes.
18
23
  # @see Keyspace#batch
19
- # @todo support batch-level consistency options
20
24
  #
21
25
  def initialize(keyspace, options = {})
22
26
  @keyspace = keyspace
23
- @auto_apply = options.fetch(:auto_apply, false)
27
+ @auto_apply = options[:auto_apply]
24
28
  @unlogged = options.fetch(:unlogged, false)
25
29
  reset
26
30
  end
@@ -39,7 +43,7 @@ module Cequel
39
43
  end
40
44
  end
41
45
 
42
- #
46
+ #
43
47
  # Send the batch to Cassandra
44
48
  #
45
49
  def apply
@@ -51,10 +55,19 @@ module Cequel
51
55
  @keyspace.execute(*@statement.args)
52
56
  end
53
57
 
58
+ #
59
+ # Is this an unlogged batch?
60
+ #
61
+ # @return [Boolean]
54
62
  def unlogged?
55
63
  @unlogged
56
64
  end
57
65
 
66
+ #
67
+ # Is this a logged batch?
68
+ #
69
+ # @return [Boolean]
70
+ #
58
71
  def logged?
59
72
  !unlogged?
60
73
  end
@@ -69,9 +82,6 @@ module Cequel
69
82
  def begin_statement
70
83
  "BEGIN #{"UNLOGGED " if unlogged?}BATCH\n"
71
84
  end
72
-
73
85
  end
74
-
75
86
  end
76
-
77
87
  end
@@ -0,0 +1,74 @@
1
+ module Cequel
2
+ module Metal
3
+ #
4
+ # Manage a current batch per thread. Used by {Keyspace}
5
+ #
6
+ # @api private
7
+ #
8
+ class BatchManager
9
+ #
10
+ # @param keyspace [Keyspace] keyspace to make writes to
11
+ # @api private
12
+ #
13
+ def initialize(keyspace)
14
+ @keyspace = keyspace
15
+ end
16
+
17
+ #
18
+ # Execute write operations in a batch. Any inserts, updates, and deletes
19
+ # inside this method's block will be executed inside a CQL BATCH
20
+ # operation.
21
+ #
22
+ # @param options [Hash]
23
+ # @option (see Batch#initialize)
24
+ # @yield context within which all write operations will be batched
25
+ # @return return value of block
26
+ # @raise [ArgumentError] if attempting to start a logged batch while
27
+ # already in an unlogged batch, or vice versa.
28
+ #
29
+ # @example Perform inserts in a batch
30
+ # DB.batch do
31
+ # DB[:posts].insert(:id => 1, :title => 'One')
32
+ # DB[:posts].insert(:id => 2, :title => 'Two')
33
+ # end
34
+ #
35
+ # @note If this method is created while already in a batch of the same
36
+ # type (logged or unlogged), this method is a no-op.
37
+ #
38
+ def batch(options = {})
39
+ new_batch = Batch.new(keyspace, options)
40
+
41
+ if current_batch
42
+ if current_batch.unlogged? && new_batch.logged?
43
+ fail ArgumentError,
44
+ "Already in an unlogged batch; can't start a logged batch."
45
+ end
46
+ return yield
47
+ end
48
+
49
+ begin
50
+ self.current_batch = new_batch
51
+ yield.tap { new_batch.apply }
52
+ ensure
53
+ self.current_batch = nil
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ attr_reader :keyspace
60
+
61
+ def current_batch
62
+ ::Thread.current[batch_key]
63
+ end
64
+
65
+ def current_batch=(batch)
66
+ ::Thread.current[batch_key] = batch
67
+ end
68
+
69
+ def batch_key
70
+ :"cequel-batch-#{object_id}"
71
+ end
72
+ end
73
+ end
74
+ end
@@ -1,26 +1,39 @@
1
1
  module Cequel
2
-
3
2
  module Metal
4
-
3
+ #
4
+ # Encapsulates a row specification (`WHERE` clause) specified by a CQL
5
+ # string
5
6
  #
6
7
  # @api private
7
8
  #
8
9
  class CqlRowSpecification
9
-
10
+ #
11
+ # Build a new row specification
12
+ #
13
+ # @param (see #initialize)
14
+ # @return [Array<CqlRowSpecification>]
15
+ #
10
16
  def self.build(condition, bind_vars)
11
17
  [new(condition, bind_vars)]
12
18
  end
13
19
 
20
+ #
21
+ # Create a new row specification
22
+ #
23
+ # @param [String] condition CQL string representing condition
24
+ # @param [Array] bind_vars Bind variables
25
+ #
14
26
  def initialize(condition, bind_vars)
15
27
  @condition, @bind_vars = condition, bind_vars
16
28
  end
17
29
 
30
+ #
31
+ # CQL and bind variables for this condition
32
+ #
33
+ # @return [Array] CQL string followed by zero or more bind variables
18
34
  def cql
19
35
  [@condition, *@bind_vars]
20
36
  end
21
-
22
37
  end
23
-
24
38
  end
25
-
26
39
  end
@@ -1,29 +1,58 @@
1
- module Cequel
1
+ require 'forwardable'
2
2
 
3
+ module Cequel
3
4
  module Metal
4
-
5
5
  #
6
- # Encapsulates a data set, specified as a column family and optionally
6
+ # Encapsulates a data set, specified as a table and optionally
7
7
  # various query elements.
8
8
  #
9
- # @todo Support ALTER, CREATE, CREATE INDEX, DROP
9
+ # @example Data set representing entire contents of a table
10
+ # data_set = database[:posts]
11
+ #
12
+ # @example Data set limiting rows returned
13
+ # data_set = database[:posts].limit(10)
14
+ #
15
+ # @example Data set targeting only one partition
16
+ # data_set = database[:posts].where(blog_subdomain: 'cassandra')
17
+ #
18
+ # @see http://www.datastax.com/documentation/cql/3.0/webhelp/index.html#cql/cql_reference/select_r.html CQL documentation for SELECT
10
19
  #
11
20
  class DataSet
12
-
13
21
  include Enumerable
14
22
  extend Forwardable
15
23
 
16
- attr_reader :keyspace, :table_name, :select_columns, :ttl_columns,
17
- :writetime_columns, :row_specifications, :sort_order, :row_limit
24
+ # @return [Keyspace] keyspace that this data set's table resides in
25
+ attr_reader :keyspace
26
+ # @return [Symbol] name of the table that this data set retrieves data
27
+ # from
28
+ attr_reader :table_name
29
+ # @return [Array<Symbol>] columns that this data set restricts result
30
+ # rows to; empty if none
31
+ attr_reader :select_columns
32
+ # @return [Array<Symbol>] columns that this data set will select the TTLs
33
+ # of
34
+ attr_reader :ttl_columns
35
+ # @return [Array<Symbol>] columns that this data set will select the
36
+ # writetimes of
37
+ attr_reader :writetime_columns
38
+ # @return [Array<RowSpecification>] row specifications limiting the
39
+ # result rows returned by this data set
40
+ attr_reader :row_specifications
41
+ # @return [Hash<Symbol,Symbol>] map of column names to sort directions
42
+ attr_reader :sort_order
43
+ # @return [Integer] maximum number of rows to return, `nil` if no limit
44
+ attr_reader :row_limit
18
45
 
19
46
  def_delegator :keyspace, :execute, :execute_cql
47
+ private :execute_cql
20
48
  def_delegator :keyspace, :write
21
49
 
22
50
  #
23
51
  # @param table_name [Symbol] column family for this data set
24
- # @param keyspace [Keyspace] keyspace this data set's column family lives in
52
+ # @param keyspace [Keyspace] keyspace this data set's table lives in
25
53
  #
26
54
  # @see Keyspace#[]
55
+ # @api private
27
56
  #
28
57
  def initialize(table_name, keyspace)
29
58
  @table_name, @keyspace = table_name, keyspace
@@ -34,22 +63,61 @@ module Cequel
34
63
  #
35
64
  # Insert a row into the column family.
36
65
  #
37
- # @param [Hash] data column-value pairs. The first entry *must* be the key column.
38
- # @param [Options] options options for persisting the row
39
- # @option (see #generate_upsert_options)
40
- # @note if a enclosed in a Keyspace#batch block, this method will be executed as part of the batch.
66
+ # @param data [Hash] column-value pairs
67
+ # @param options [Options] options for persisting the row
68
+ # @option (see Writer#initialize)
69
+ # @return [void]
70
+ #
71
+ # @note `INSERT` statements will succeed even if a row at the specified
72
+ # primary key already exists. In this case, column values specified in
73
+ # the insert will overwrite the existing row.
74
+ # @note If a enclosed in a Keyspace#batch block, this method will be
75
+ # executed as part of the batch.
76
+ # @see http://www.datastax.com/documentation/cql/3.0/webhelp/index.html#cql/cql_reference/insert_r.html CQL documentation for INSERT
41
77
  #
42
78
  def insert(data, options = {})
43
79
  inserter(options) { insert(data) }.execute
44
80
  end
45
81
 
46
82
  #
47
- # Update rows
83
+ # Upsert data into one or more rows
84
+ #
85
+ # @overload update(column_values, options = {})
86
+ # Update the rows specified in the data set with new values
87
+ #
88
+ # @param column_values [Hash] map of column names to new values
89
+ # @param options [Options] options for persisting the column data
90
+ # @option (see #generate_upsert_options)
91
+ #
92
+ # @example
93
+ # posts.where(blog_subdomain: 'cassandra', permalink: 'cequel').
94
+ # update(title: 'Announcing Cequel 1.0')
95
+ #
96
+ # @overload update(options = {}, &block)
97
+ # Construct an update statement consisting of multiple operations
98
+ #
99
+ # @param options [Options] options for persisting the data
100
+ # @option (see #generate_upsert_options)
101
+ # @yield DSL context for adding write operations
102
+ #
103
+ # @see Updater
104
+ # @since 1.0.0
105
+ #
106
+ # @example
107
+ # posts.where(blog_subdomain: 'bigdata', permalink: 'cql').update do
108
+ # set(title: 'Announcing Cequel 1.0')
109
+ # list_append(categories: 'ORMs')
110
+ # end
48
111
  #
49
- # @param [Hash] data column-value pairs
50
- # @param [Options] options options for persisting the column data
51
- # @option (see #generate_upsert_options)
52
- # @note if a enclosed in a Keyspace#batch block, this method will be executed as part of the batch.
112
+ # @return [void]
113
+ #
114
+ # @note `UPDATE` statements will succeed even if targeting a row that
115
+ # does not exist. In this case a new row will be created.
116
+ # @note This statement will fail unless one or more rows are fully
117
+ # specified by primary key using `where`
118
+ # @note If a enclosed in a Keyspace#batch block, this method will be
119
+ # executed as part of the batch.
120
+ # @see http://www.datastax.com/documentation/cql/3.0/webhelp/index.html#cql/cql_reference/update_r.html CQL documentation for UPDATE
53
121
  #
54
122
  def update(*args, &block)
55
123
  if block
@@ -60,24 +128,63 @@ module Cequel
60
128
  end
61
129
  end
62
130
 
63
- def increment(data, options = {})
64
- incrementer(options) { increment(data) }.execute
131
+ #
132
+ # Increment one or more counter columns
133
+ #
134
+ # @param deltas [Hash<Symbol,Integer>] map of counter column names to
135
+ # amount by which to increment each column
136
+ # @return [void]
137
+ #
138
+ # @example
139
+ # post_analytics.
140
+ # where(blog_subdomain: 'cassandra', permalink: 'cequel').
141
+ # increment(pageviews: 10, tweets: 2)
142
+ #
143
+ # @note This can only be used on counter tables
144
+ # @since 0.5.0
145
+ # @see #decrement
146
+ # @see http://www.datastax.com/documentation/cql/3.0/webhelp/index.html#cql/cql_reference/../cql_using/use_counter_t.html CQL documentation for counter columns
147
+ #
148
+ def increment(deltas, options = {})
149
+ incrementer(options) { increment(deltas) }.execute
65
150
  end
151
+ alias_method :incr, :increment
66
152
 
67
- def decrement(data, options = {})
68
- incrementer(options) { decrement(data) }.execute
153
+ #
154
+ # Decrement one or more counter columns
155
+ #
156
+ # @param deltas [Hash<Symbol,Integer>] map of counter column names to
157
+ # amount by which to decrement each column
158
+ # @return [void]
159
+ #
160
+ # @see #increment
161
+ # @see http://www.datastax.com/documentation/cql/3.0/webhelp/index.html#cql/cql_reference/../cql_using/use_counter_t.html CQL documentation for counter columns
162
+ # @since 0.5.0
163
+ #
164
+ def decrement(deltas, options = {})
165
+ incrementer(options) { decrement(deltas) }.execute
69
166
  end
70
167
  alias_method :decr, :decrement
71
168
 
72
169
  #
73
170
  # Prepend element(s) to a list in the row(s) matched by this data set.
74
171
  #
75
- # @param [Symbol] column name of list column to prepend to
76
- # @param [Object,Array] elements one element or an array of elements to prepend
77
- # @param [Options] options options for persisting the column data
78
- # @option (see #generate_upsert_options)
79
- # @note If multiple elements are passed, they will appear in the list in reverse order.
80
- # @note if a enclosed in a Keyspace#batch block, this method will be executed as part of the batch.
172
+ # @param column [Symbol] name of list column to prepend to
173
+ # @param elements [Object,Array] one element or an array of elements to
174
+ # prepend
175
+ # @param options [Options] options for persisting the column data
176
+ # @option (see Writer#initialize)
177
+ # @return [void]
178
+ #
179
+ # @example
180
+ # posts.list_prepend(:categories, ['CQL', 'ORMs'])
181
+ #
182
+ # @note If multiple elements are passed, they will appear in the list in
183
+ # reverse order.
184
+ # @note If a enclosed in a Keyspace#batch block, this method will be
185
+ # executed as part of the batch.
186
+ # @see #list_append
187
+ # @see #update
81
188
  #
82
189
  def list_prepend(column, elements, options = {})
83
190
  updater(options) { list_prepend(column, elements) }.execute
@@ -86,11 +193,21 @@ module Cequel
86
193
  #
87
194
  # Append element(s) to a list in the row(s) matched by this data set.
88
195
  #
89
- # @param [Symbol] column name of list column to append to
90
- # @param [Object,Array] elements one element or an array of elements to append
91
- # @param [Options] options options for persisting the column data
92
- # @option (see #generate_upsert_options)
93
- # @note if a enclosed in a Keyspace#batch block, this method will be executed as part of the batch.
196
+ # @param column [Symbol] name of list column to append to
197
+ # @param elements [Object,Array] one element or an array of elements to
198
+ # append
199
+ # @param options [Options] options for persisting the column data
200
+ # @option (see Writer#initialize)
201
+ # @return [void]
202
+ #
203
+ # @example
204
+ # posts.list_append(:categories, ['CQL', 'ORMs'])
205
+ #
206
+ # @note If a enclosed in a Keyspace#batch block, this method will be
207
+ # executed as part of the batch.
208
+ # @see #list_append
209
+ # @see #update
210
+ # @since 1.0.0
94
211
  #
95
212
  def list_append(column, elements, options = {})
96
213
  updater(options) { list_append(column, elements) }.execute
@@ -99,12 +216,20 @@ module Cequel
99
216
  #
100
217
  # Replace a list element at a specified index with a new value
101
218
  #
102
- # @param [Symbol] column name of list column
103
- # @param [Integer] index which element to replace
104
- # @param [Object] value new value at this index
105
- # @param [Options] options options for persisting the data
106
- # @option (see #generate_upsert_options)
107
- # @note if a enclosed in a Keyspace#batch block, this method will be executed as part of the batch.
219
+ # @param column [Symbol] name of list column
220
+ # @param index [Integer] which element to replace
221
+ # @param value [Object] new value at this index
222
+ # @param options [Options] options for persisting the data
223
+ # @option (see Writer#initialize)
224
+ # @return [void]
225
+ #
226
+ # @example
227
+ # posts.list_replace(:categories, 2, 'Object-Relational Mapper')
228
+ #
229
+ # @note if a enclosed in a Keyspace#batch block, this method will be
230
+ # executed as part of the batch.
231
+ # @see #update
232
+ # @since 1.0.0
108
233
  #
109
234
  def list_replace(column, index, value, options = {})
110
235
  updater(options) { list_replace(column, index, value) }.execute
@@ -113,24 +238,43 @@ module Cequel
113
238
  #
114
239
  # Remove all occurrences of a given value from a list column
115
240
  #
116
- # @param [Symbol] column name of list column
117
- # @param [Object] value value to remove
118
- # @param [Options] options for persisting the data
119
- # @option (see #generate_upsert_options)
120
- # @note if a enclosed in a Keyspace#batch block, this method will be executed as part of the batch.
241
+ # @param column [Symbol] name of list column
242
+ # @param value [Object] value to remove
243
+ # @param options [Options] options for persisting the data
244
+ # @option (see Writer#initialize)
245
+ # @return [void]
246
+ #
247
+ # @example
248
+ # posts.list_remove(:categories, 'CQL3')
249
+ #
250
+ # @note If enclosed in a Keyspace#batch block, this method will be
251
+ # executed as part of the batch.
252
+ # @see #list_remove_at
253
+ # @see #update
254
+ # @since 1.0.0
121
255
  #
122
256
  def list_remove(column, value, options = {})
123
257
  updater(options) { list_remove(column, value) }.execute
124
258
  end
125
259
 
126
260
  #
127
- # Remove all occurrences of a given value from a list column
261
+ # @overload list_remove_at(column, *positions, options = {})
262
+ # Remove the value from a given position or positions in a list column
263
+ #
264
+ # @param column [Symbol] name of list column
265
+ # @param positions [Integer] position(s) in list to remove value from
266
+ # @param options [Options] options for persisting the data
267
+ # @option (see Writer#initialize)
268
+ # @return [void]
269
+ #
270
+ # @example
271
+ # posts.list_remove_at(:categories, 2)
128
272
  #
129
- # @param [Symbol] column name of list column
130
- # @param [Object] position position in list to remove value from
131
- # @param [Options] options for persisting the data
132
- # @option (see #generate_upsert_options)
133
- # @note if a enclosed in a Keyspace#batch block, this method will be executed as part of the batch.
273
+ # @note If enclosed in a Keyspace#batch block, this method will be
274
+ # executed as part of the batch.
275
+ # @see #list_remove
276
+ # @see #update
277
+ # @since 1.0.0
134
278
  #
135
279
  def list_remove_at(column, *positions)
136
280
  options = positions.extract_options!
@@ -138,13 +282,22 @@ module Cequel
138
282
  end
139
283
 
140
284
  #
141
- # Remove a given key from a map column
285
+ # @overload map_remove(column, *keys, options = {})
286
+ # Remove a given key from a map column
142
287
  #
143
- # @param [Symbol] column name of map column
144
- # @param [Object] key map key to remove
145
- # @param [Options] options for persisting the data
146
- # @option (see #generate_upsert_options)
147
- # @note if a enclosed in a Keyspace#batch block, this method will be executed as part of the batch.
288
+ # @param column [Symbol] name of map column
289
+ # @param keys [Object] map key to remove
290
+ # @param options [Options] options for persisting the data
291
+ # @option (see Writer#initialize)
292
+ # @return [void]
293
+ #
294
+ # @example
295
+ # posts.map_remove(:credits, 'editor')
296
+ #
297
+ # @note If enclosed in a Keyspace#batch block, this method will be
298
+ # executed as part of the batch.
299
+ # @see #update
300
+ # @since 1.0.0
148
301
  #
149
302
  def map_remove(column, *keys)
150
303
  options = keys.extract_options!
@@ -152,50 +305,112 @@ module Cequel
152
305
  end
153
306
 
154
307
  #
155
- # Add one or more elements to a set
308
+ # Add one or more elements to a set column
309
+ #
310
+ # @param column [Symbol] name of set column
311
+ # @param values [Object,Set] value or values to add
312
+ # @param options [Options] options for persisting the data
313
+ # @option (see Writer#initialize)
314
+ # @return [void]
315
+ #
316
+ # @example
317
+ # posts.set_add(:tags, 'cql3')
156
318
  #
157
- # @param [Symbol] column name of set column
158
- # @param [Object,Set] value value or values to add
159
- # @param [Options] options for persisting the data
160
- # @option (see #generate_upsert_options)
161
- # @note if a enclosed in a Keyspace#batch block, this method will be executed as part of the batch.
319
+ # @note If enclosed in a Keyspace#batch block, this method will be
320
+ # executed as part of the batch.
321
+ # @see #update
322
+ # @since 1.0.0
162
323
  #
163
324
  def set_add(column, values, options = {})
164
325
  updater(options) { set_add(column, values) }.execute
165
326
  end
166
327
 
167
328
  #
168
- # Remove one or more elements from a set
329
+ # Remove an element from a set
169
330
  #
170
- # @param [Symbol] column name of set column
171
- # @param [Object,Set] value value or values to add
172
- # @param [Options] options for persisting the data
173
- # @option (see #generate_upsert_options)
174
- # @note if a enclosed in a Keyspace#batch block, this method will be executed as part of the batch.
331
+ # @param column [Symbol] name of set column
332
+ # @param value [Object] value to remove
333
+ # @param options [Options] options for persisting the data
334
+ # @option (see Writer#initialize)
335
+ # @return [void]
336
+ #
337
+ # @example
338
+ # posts.set_remove(:tags, 'cql3')
339
+ #
340
+ # @note If enclosed in a Keyspace#batch block, this method will be
341
+ # executed as part of the batch.
342
+ # @see #update
343
+ # @since 1.0.0
175
344
  #
176
345
  def set_remove(column, value, options = {})
177
346
  updater(options) { set_remove(column, value) }.execute
178
347
  end
179
348
 
180
349
  #
181
- # Update one or more map elements
350
+ # Update one or more keys in a map column
351
+ #
352
+ # @param column [Symbol] name of set column
353
+ # @param updates [Hash] map of map keys to new values
354
+ # @param options [Options] options for persisting the data
355
+ # @option (see Writer#initialize)
356
+ # @return [void]
182
357
  #
183
- # @param [Symbol] column name of set column
184
- # @param [Hash] map updates
185
- # @param [Options] options for persisting the data
186
- # @option (see #generate_upsert_options)
187
- # @note if a enclosed in a Keyspace#batch block, this method will be executed as part of the batch.
358
+ # @example
359
+ # posts.map_update(:credits, 'editor' => 34)
360
+ #
361
+ # @note If enclosed in a Keyspace#batch block, this method will be
362
+ # executed as part of the batch.
363
+ # @see #update
364
+ # @since 1.0.0
188
365
  #
189
366
  def map_update(column, updates, options = {})
190
367
  updater(options) { map_update(column, updates) }.execute
191
368
  end
192
369
 
193
370
  #
194
- # Delete data from the column family
371
+ # @overload delete(options = {})
372
+ # Delete one or more rows from the table
373
+ #
374
+ # @param options [Options] options for persistence
375
+ # @option (See Writer#initialize)
376
+ #
377
+ # @example
378
+ # posts.where(blog_subdomain: 'cassandra', permalink: 'cequel').
379
+ # delete
380
+ #
381
+ # @overload delete(*columns, options = {})
382
+ # Delete data from given columns in the specified rows. This is
383
+ # equivalent to setting columns to `NULL` in an SQL database.
384
+ #
385
+ # @param columns [Symbol] columns to remove
386
+ # @param options [Options] options for persistence
387
+ # @option (see Writer#initialize)
388
+ #
389
+ # @example
390
+ # posts.where(blog_subdomain: 'cassandra', permalink: 'cequel').
391
+ # delete(:body)
392
+ #
393
+ # @overload delete(options = {}, &block)
394
+ # Construct a `DELETE` statement with multiple operations (column
395
+ # deletions, collection element removals, etc.)
195
396
  #
196
- # @param columns zero or more columns to delete. Deletes the entire row if none specified.
197
- # @param options persistence options
198
- # @note if a enclosed in a Keyspace#batch block, this method will be executed as part of the batch.
397
+ # @param options [Options] options for persistence
398
+ # @option (see Writer#initialize)
399
+ # @yield DSL context for construction delete statement
400
+ #
401
+ # @example
402
+ # posts.where(blog_subdomain: 'bigdata', permalink: 'cql').delete do
403
+ # delete_columns :body
404
+ # list_remove_at :categories, 2
405
+ # end
406
+ #
407
+ # @see Deleter
408
+ #
409
+ # @return [void]
410
+ #
411
+ # @note If enclosed in a Keyspace#batch block, this method will be
412
+ # executed as part of the batch.
413
+ # @see http://www.datastax.com/documentation/cql/3.0/webhelp/index.html#cql/cql_reference/delete_r.html CQL documentation for DELETE
199
414
  #
200
415
  def delete(*columns, &block)
201
416
  options = columns.extract_options!
@@ -211,7 +426,7 @@ module Cequel
211
426
  #
212
427
  # Select specified columns from this data set.
213
428
  #
214
- # @param *columns [Symbol,Array] columns to select
429
+ # @param columns [Symbol] columns columns to select
215
430
  # @return [DataSet] new data set scoped to specified columns
216
431
  #
217
432
  def select(*columns)
@@ -223,9 +438,11 @@ module Cequel
223
438
  #
224
439
  # Return the remaining TTL for the specified columns from this data set.
225
440
  #
226
- # @param *columns [Symbol,Array] columns to select
441
+ # @param columns [Symbol] columns to select
227
442
  # @return [DataSet] new data set scoped to specified columns
228
443
  #
444
+ # @since 1.0.0
445
+ #
229
446
  def select_ttl(*columns)
230
447
  clone.tap do |data_set|
231
448
  data_set.ttl_columns.concat(columns.flatten)
@@ -235,9 +452,11 @@ module Cequel
235
452
  #
236
453
  # Return the write time for the specified columns in the data set
237
454
  #
238
- # @param *columns [Symbol,Array] columns to select
455
+ # @param columns [Symbol] columns to select
239
456
  # @return [DataSet] new data set scoped to specified columns
240
457
  #
458
+ # @since 1.0.0
459
+ #
241
460
  def select_writetime(*columns)
242
461
  clone.tap do |data_set|
243
462
  data_set.writetime_columns.concat(columns.flatten)
@@ -247,7 +466,7 @@ module Cequel
247
466
  #
248
467
  # Select specified columns from this data set, overriding chained scope.
249
468
  #
250
- # @param *columns [Symbol,Array] columns to select
469
+ # @param columns [Symbol,Array] columns to select
251
470
  # @return [DataSet] new data set scoped to specified columns
252
471
  #
253
472
  def select!(*columns)
@@ -257,31 +476,40 @@ module Cequel
257
476
  end
258
477
 
259
478
  #
260
- # Add a row_specification to this data set
479
+ # Filter this data set with a row specification
480
+ #
481
+ # @overload where(column_values)
482
+ # @param column_values [Hash] Map of column name to values to match
261
483
  #
262
- # @param row_specification [Hash, String] row_specification statement
263
- # @param *bind_vars bind variables, only if using a CQL string row_specification
264
- # @return [DataSet] new data set scoped to this row_specification
265
- # @example Using a simple hash
266
- # DB[:posts].where(:title => 'Hey')
267
- # @example Using a CQL string
268
- # DB[:posts].where("title = 'Hey'")
269
- # @example Using a CQL string with bind variables
270
- # DB[:posts].where('title = ?', 'Hey')
271
- # @example Use another data set as an input -- inner data set must return a single column per row!
272
- # DB[:blogs].where(:id => DB[:posts].select(:blog_id).where(:title => 'Hey'))
484
+ # @example
485
+ # database[:posts].where(title: 'Hey')
486
+ #
487
+ # @overload where(cql, *bind_vars)
488
+ # @param cql [String] CQL fragment representing `WHERE` statement
489
+ # @param bind_vars [Object] Bind variables for the CQL fragment
490
+ #
491
+ # @example
492
+ # DB[:posts].where('title = ?', 'Hey')
493
+ #
494
+ # @return [DataSet] New data set scoped to the row specification
273
495
  #
274
496
  def where(row_specification, *bind_vars)
275
497
  clone.tap do |data_set|
276
- data_set.row_specifications.
277
- concat(build_row_specifications(row_specification, bind_vars))
498
+ data_set.row_specifications
499
+ .concat(build_row_specifications(row_specification, bind_vars))
278
500
  end
279
501
  end
280
502
 
503
+ #
504
+ # Replace existing row specifications
505
+ #
506
+ # @see #where
507
+ # @return [DataSet] New data set with only row specifications given
508
+ #
281
509
  def where!(row_specification, *bind_vars)
282
510
  clone.tap do |data_set|
283
- data_set.row_specifications.
284
- replace(build_row_specifications(row_specification, bind_vars))
511
+ data_set.row_specifications
512
+ .replace(build_row_specifications(row_specification, bind_vars))
285
513
  end
286
514
  end
287
515
 
@@ -296,10 +524,13 @@ module Cequel
296
524
  end
297
525
 
298
526
  #
299
- # Control how the result rows are sorted. Note that you can only sort by
300
- # clustering keys, and in the case of multiple clustering keys you can only
301
- # sort by the schema's clustering order or the reverse of the clustering
302
- # order for all keys.
527
+ # Control how the result rows are sorted
528
+ #
529
+ # @param pairs [Hash] Map of column name to sort direction
530
+ # @return [DataSet] new data set with the specified ordering
531
+ #
532
+ # @note The only valid ordering column is the first clustering column
533
+ # @since 1.0.0
303
534
  #
304
535
  def order(pairs)
305
536
  clone.tap do |data_set|
@@ -311,89 +542,74 @@ module Cequel
311
542
  # Enumerate over rows in this data set. Along with #each, all other
312
543
  # Enumerable methods are implemented.
313
544
  #
314
- # @yield [Hash] result rows
315
- # @return [Enumerator] enumerator for rows, if no block given
545
+ # @overload each
546
+ # @return [Enumerator] enumerator for rows, if no block given
547
+ #
548
+ # @overload each(&block)
549
+ # @yield [Hash] result rows
550
+ # @return [void]
551
+ #
552
+ # @return [Enumerator,void]
316
553
  #
317
554
  def each
318
- if block_given?
319
- begin
320
- keyspace.execute(*cql).fetch do |row|
321
- yield Row.from_result_row(row)
322
- end
323
- rescue EmptySubquery
324
- # Noop -- yield no results
325
- end
326
- else
327
- enum_for(:each)
328
- end
555
+ return enum_for(:each) unless block_given?
556
+ execute_cql(*cql).fetch { |row| yield Row.from_result_row(row) }
329
557
  end
330
558
 
331
559
  #
332
560
  # @return [Hash] the first row in this data set
333
561
  #
334
562
  def first
335
- row = keyspace.execute(*limit(1).cql).fetch_row
563
+ row = execute_cql(*limit(1).cql).fetch_row
336
564
  Row.from_result_row(row)
337
- rescue EmptySubquery
338
- nil
339
565
  end
340
566
 
341
567
  #
342
568
  # @return [Fixnum] the number of rows in this data set
343
569
  #
344
570
  def count
345
- keyspace.execute(*count_cql).fetch_row['count']
346
- rescue EmptySubquery
347
- 0
571
+ execute_cql(*count_cql).fetch_row['count']
348
572
  end
349
573
 
350
574
  #
351
- # @return [String] CQL select statement encoding this data set's scope.
575
+ # @return [String] CQL `SELECT` statement encoding this data set's scope.
352
576
  #
353
577
  def cql
354
- statement = Statement.new.
355
- append(select_cql).
356
- append(" FROM #{table_name}").
357
- append(*row_specifications_cql).
358
- append(sort_order_cql).
359
- append(limit_cql).
360
- args
578
+ statement = Statement.new
579
+ .append(select_cql)
580
+ .append(" FROM #{table_name}")
581
+ .append(*row_specifications_cql)
582
+ .append(sort_order_cql)
583
+ .append(limit_cql)
584
+ .args
361
585
  end
362
586
 
363
587
  #
364
588
  # @return [String] CQL statement to get count of rows in this data set
365
589
  #
366
590
  def count_cql
367
- Statement.new.
368
- append("SELECT COUNT(*) FROM #{table_name}").
369
- append(*row_specifications_cql).
370
- append(limit_cql).args
591
+ Statement.new
592
+ .append("SELECT COUNT(*) FROM #{table_name}")
593
+ .append(*row_specifications_cql)
594
+ .append(limit_cql).args
371
595
  end
372
596
 
597
+ #
598
+ # @return [String]
599
+ #
373
600
  def inspect
374
- "#<#{self.class.name}: #{CassandraCQL::Statement.sanitize(cql.first, cql[1..-1])}>"
601
+ "#<#{self.class.name}: " \
602
+ "#{CassandraCQL::Statement.sanitize(cql.first, cql[1..-1])}>"
375
603
  end
376
604
 
605
+ #
606
+ # @return [Boolean]
607
+ #
377
608
  def ==(other)
378
609
  cql == other.cql
379
610
  end
380
611
 
381
- def inserter(options = {}, &block)
382
- Inserter.new(self, options, &block)
383
- end
384
-
385
- def updater(options = {}, &block)
386
- Updater.new(self, options, &block)
387
- end
388
-
389
- def incrementer(options = {}, &block)
390
- Incrementer.new(self, options, &block)
391
- end
392
-
393
- def deleter(options = {}, &block)
394
- Deleter.new(self, options, &block)
395
- end
396
-
612
+ # @private
397
613
  def row_specifications_cql
398
614
  if row_specifications.any?
399
615
  cql_fragments, bind_vars = [], []
@@ -407,11 +623,30 @@ module Cequel
407
623
  end
408
624
  end
409
625
 
626
+ # @private
627
+ def updater(options = {}, &block)
628
+ Updater.new(self, options, &block)
629
+ end
630
+
631
+ # @private
632
+ def deleter(options = {}, &block)
633
+ Deleter.new(self, options, &block)
634
+ end
635
+
410
636
  protected
637
+
411
638
  attr_writer :row_limit
412
639
 
413
640
  private
414
641
 
642
+ def inserter(options = {}, &block)
643
+ Inserter.new(self, options, &block)
644
+ end
645
+
646
+ def incrementer(options = {}, &block)
647
+ Incrementer.new(self, options, &block)
648
+ end
649
+
415
650
  def initialize_copy(source)
416
651
  super
417
652
  @select_columns = source.select_columns.clone
@@ -426,11 +661,11 @@ module Cequel
426
661
  ttl_columns.map { |column| "TTL(#{column})" } +
427
662
  writetime_columns.map { |column| "WRITETIME(#{column})" }
428
663
 
429
- if all_columns.any?
430
- "SELECT #{all_columns.join(',')}"
431
- else
432
- 'SELECT *'
433
- end
664
+ if all_columns.any?
665
+ "SELECT #{all_columns.join(',')}"
666
+ else
667
+ 'SELECT *'
668
+ end
434
669
  end
435
670
 
436
671
  def limit_cql
@@ -439,23 +674,25 @@ module Cequel
439
674
 
440
675
  def sort_order_cql
441
676
  if sort_order.any?
442
- order = sort_order.
443
- map { |column, direction| "#{column} #{direction.to_s.upcase}" }.
444
- join(', ')
677
+ order = sort_order
678
+ .map { |column, direction| "#{column} #{direction.to_s.upcase}" }
679
+ .join(', ')
445
680
  " ORDER BY #{order}"
446
681
  end
447
682
  end
448
683
 
449
684
  def build_row_specifications(row_specification, bind_vars)
450
685
  case row_specification
451
- when Hash then RowSpecification.build(row_specification)
452
- when String then CqlRowSpecification.build(row_specification, bind_vars)
453
- else raise ArgumentError, "Invalid argument #{row_specification.inspect}; expected Hash or String"
686
+ when Hash
687
+ RowSpecification.build(row_specification)
688
+ when String
689
+ CqlRowSpecification.build(row_specification, bind_vars)
690
+ else
691
+ fail ArgumentError,
692
+ "Invalid argument #{row_specification.inspect}; " \
693
+ "expected Hash or String"
454
694
  end
455
695
  end
456
-
457
696
  end
458
-
459
697
  end
460
-
461
698
  end