cequel 1.0.0.rc1 → 1.0.0.rc2

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 (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