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
@@ -1,19 +1,36 @@
1
- require 'delegate'
2
-
3
1
  module Cequel
4
-
5
2
  module Metal
6
-
3
+ #
4
+ # Internal representation of a data manipulation statement
5
+ #
6
+ # @abstract Subclasses must implement #write_to_statement, which writes
7
+ # internal state to a Statement instance
8
+ #
9
+ # @since 1.0.0
10
+ # @api private
11
+ #
7
12
  class Writer
8
-
9
13
  extend Forwardable
10
14
 
15
+ #
16
+ # @param data_set [DataSet] data set to write to
17
+ # @param options [Options] options
18
+ # @option options [Integer] :ttl time-to-live in seconds for the written
19
+ # data
20
+ # @option options [Time,Integer] :timestamp the timestamp associated with
21
+ # the column values
22
+ #
11
23
  def initialize(data_set, options = {}, &block)
12
24
  @data_set, @options, @block = data_set, options, block
13
25
  @statements, @bind_vars = [], []
14
26
  SimpleDelegator.new(self).instance_eval(&block) if block
15
27
  end
16
28
 
29
+ #
30
+ # Execute the statement as a write operation
31
+ #
32
+ # @return [void]
33
+ #
17
34
  def execute
18
35
  return if empty?
19
36
  statement = Statement.new
@@ -23,6 +40,7 @@ module Cequel
23
40
  end
24
41
 
25
42
  private
43
+
26
44
  attr_reader :data_set, :options, :statements, :bind_vars
27
45
  def_delegator :data_set, :table_name
28
46
  def_delegator :statements, :empty?
@@ -44,11 +62,6 @@ module Cequel
44
62
  #
45
63
  # Generate CQL option statement for inserts and updates
46
64
  #
47
- # @param [Hash] options options for insert
48
- # @option options [Symbol,String] :consistency required consistency for the write
49
- # @option options [Integer] :ttl time-to-live in seconds for the written data
50
- # @option options [Time,Integer] :timestamp the timestamp associated with the column values
51
- #
52
65
  def generate_upsert_options
53
66
  if options.empty?
54
67
  ''
@@ -57,7 +70,6 @@ module Cequel
57
70
  options.map do |key, value|
58
71
  serialized_value =
59
72
  case key
60
- when :consistency then value.to_s.upcase
61
73
  when :timestamp then (value.to_f * 1_000_000).to_i
62
74
  else value
63
75
  end
@@ -65,9 +77,6 @@ module Cequel
65
77
  end.join(' AND ')
66
78
  end
67
79
  end
68
-
69
80
  end
70
-
71
81
  end
72
-
73
82
  end
data/lib/cequel/record.rb CHANGED
@@ -8,6 +8,7 @@ require 'cequel/record/collection'
8
8
  require 'cequel/record/persistence'
9
9
  require 'cequel/record/bulk_writes'
10
10
  require 'cequel/record/record_set'
11
+ require 'cequel/record/data_set_builder'
11
12
  require 'cequel/record/bound'
12
13
  require 'cequel/record/lazy_record_collection'
13
14
  require 'cequel/record/scoped'
@@ -28,9 +29,50 @@ if defined? Rails
28
29
  end
29
30
 
30
31
  module Cequel
31
-
32
+ #
33
+ # Cequel::Record is an active record-style data modeling library and
34
+ # object-row mapper. Model classes inherit from Cequel::Record, define their
35
+ # columns in the class definition, and have access to a full and robust set
36
+ # of read and write functionality.
37
+ #
38
+ # Individual components are documented in their respective modules. See below
39
+ # for links.
40
+ #
41
+ # @example A Record class showing off many of the possibilities
42
+ # class Post
43
+ # include Cequel::Record
44
+ #
45
+ # belongs_to :blog
46
+ # key :id, :timeuuid, auto: true
47
+ # column :title, :text
48
+ # column :body, :text
49
+ # column :author_id, :uuid, index: true
50
+ # set :categories
51
+ #
52
+ # has_many :comments, dependent: destroy
53
+ #
54
+ # after_create :notify_followers
55
+ #
56
+ # validates :title, presence: true
57
+ #
58
+ # def self.for_author(author_id)
59
+ # where(:author_id, author_id)
60
+ # end
61
+ # end
62
+ #
63
+ # @see Properties Defining properties
64
+ # @see Collection Collection columns
65
+ # @see SecondaryIndexes Defining secondary indexes
66
+ # @see Associations Defining associations between records
67
+ # @see Persistence Creating, updating, and destroying records
68
+ # @see BulkWrites Updating and destroying records in bulk
69
+ # @see RecordSet Loading records from the database
70
+ # @see MassAssignment Mass-assignment protection and strong attributes
71
+ # @see Callbacks Lifecycle hooks
72
+ # @see Validations
73
+ # @see Dirty Dirty attribute tracking
74
+ #
32
75
  module Record
33
-
34
76
  extend ActiveSupport::Concern
35
77
  extend Forwardable
36
78
 
@@ -48,18 +90,22 @@ module Cequel
48
90
  extend ActiveModel::Naming
49
91
  include ActiveModel::Serializers::JSON
50
92
  include ActiveModel::Serializers::Xml
51
-
52
93
  end
53
94
 
54
95
  class <<self
96
+ # @return [Metal::Keyspace] the keyspace used for record persistence
55
97
  attr_accessor :connection
56
98
 
99
+ #
100
+ # Establish a connection with the given configuration
101
+ #
102
+ # @param (see Cequel.connect)
103
+ # @option (see Cequel.connect)
104
+ # @return [void]
105
+ #
57
106
  def establish_connection(configuration)
58
107
  self.connection = Cequel.connect(configuration)
59
108
  end
60
-
61
109
  end
62
-
63
110
  end
64
-
65
111
  end
@@ -1,11 +1,21 @@
1
1
  module Cequel
2
-
3
2
  module Record
4
-
3
+ #
4
+ # Collection of records from a
5
+ # {Associations::ClassMethods#has_many has_many} associaiton. Encapsulates
6
+ # and behaves like a {RecordSet}, but unlike a normal RecordSet the loaded
7
+ # records are held in memory after they are loaded.
8
+ #
9
+ # @see Associations::ClassMethods#has_many
10
+ # @since 1.0.0
11
+ #
5
12
  class AssociationCollection < DelegateClass(RecordSet)
6
-
7
13
  include Enumerable
8
14
 
15
+ #
16
+ # @yield [Record]
17
+ # @return [void]
18
+ #
9
19
  def each(&block)
10
20
  target.each(&block)
11
21
  end
@@ -15,9 +25,6 @@ module Cequel
15
25
  def target
16
26
  @target ||= __getobj__.entries
17
27
  end
18
-
19
28
  end
20
-
21
29
  end
22
-
23
30
  end
@@ -1,9 +1,55 @@
1
1
  module Cequel
2
-
3
2
  module Record
4
-
3
+ #
4
+ # Cequel records can have parent-child relationships defined by
5
+ # {ClassMethods#belongs_to belongs_to} and {ClassMethods#has_many has_many}
6
+ # associations. Unlike in a relational database ORM, associations are not
7
+ # represented by foreign keys; instead they use CQL3's compound primary
8
+ # keys. A child object's primary key begins with it's parent's primary key.
9
+ #
10
+ # In the below example, the `blogs` table has a one-column primary key
11
+ # `(subdomain)`, and the `posts` table has a two-column primary key
12
+ # `(blog_subdomain, permalink)`. All posts that belong to the blog with
13
+ # subdomain `"cassandra"` will have `"cassandra"` as their
14
+ # `blog_subdomain`.
15
+ #
16
+ # @example Blogs and Posts
17
+ #
18
+ # class Blog
19
+ # include Cequel::Record
20
+ #
21
+ # key :subdomain, :text
22
+ #
23
+ # column :name, :text
24
+ #
25
+ # has_many :posts
26
+ # end
27
+ #
28
+ # class Post
29
+ # include Cequel::Record
30
+ #
31
+ # # This defines the first primary key column as `blog_subdomain`.
32
+ # # Because `belongs_to` associations implicitly define columns in the
33
+ # # primary key, it must come before any explicit key definition. For
34
+ # # the same reason, a Record class can only have a single `belongs_to`
35
+ # # declaration.
36
+ # belongs_to :blog
37
+ #
38
+ # # We also define an additional primary key column so that each post
39
+ # # has a unique compound primary key
40
+ # key :permalink
41
+ #
42
+ # column :title, :text
43
+ # column :body, :text
44
+ # end
45
+ #
46
+ # blog = Blog.new(subdomain: 'cassandra')
47
+ # post = blog.posts.new(permalink: 'cequel')
48
+ # post.blog_subdomain #=> "cassandra"
49
+ #
50
+ # @since 1.0.0
51
+ #
5
52
  module Associations
6
-
7
53
  extend ActiveSupport::Concern
8
54
 
9
55
  included do
@@ -12,43 +58,83 @@ module Cequel
12
58
  self.child_associations = {}
13
59
  end
14
60
 
61
+ #
62
+ # Class macros for declaring associations
63
+ #
64
+ # @see Associations
65
+ #
15
66
  module ClassMethods
16
-
17
67
  include Forwardable
18
68
 
19
- def belongs_to(name)
69
+ # @!attribute parent_association
70
+ # @return [BelongsToAssociation] association declared by
71
+ # {#belongs_to}
72
+ # @!attribute child_associations
73
+ # @return [Hash<Symbol,HasManyAssociation>] associations declared by
74
+ # {#has_many}
75
+
76
+ #
77
+ # Declare the parent association for this record. The name of the class
78
+ # is inferred from the name of the association. The `belongs_to`
79
+ # declaration also serves to define key columns, which are derived from
80
+ # the key columns of the parent class. So, if the parent class `Blog`
81
+ # has a primary key `(subdomain)`, this will declare a key column
82
+ # `blog_subdomain` of the same type.
83
+ #
84
+ # Parent associations are read/write, so declaring `belongs_to :blog`
85
+ # will define a `blog` getter and `blog=` setter, which will update the
86
+ # underlying key column. Note that a record's parent cannot be changed
87
+ # once the record has been saved.
88
+ #
89
+ # @param name [Symbol] name of the parent association
90
+ # @param options [Options] options for association
91
+ # @option (see BelongsToAssociation#initialize)
92
+ # @return [void]
93
+ #
94
+ # @see Associations
95
+ #
96
+ def belongs_to(name, options = {})
20
97
  if parent_association
21
- raise InvalidRecordConfiguration,
22
- "Can't declare more than one belongs_to association"
98
+ fail InvalidRecordConfiguration,
99
+ "Can't declare more than one belongs_to association"
23
100
  end
24
101
  if table_schema.key_columns.any?
25
- raise InvalidRecordConfiguration,
26
- "belongs_to association must be declared before declaring key(s)"
102
+ fail InvalidRecordConfiguration,
103
+ "belongs_to association must be declared before declaring " \
104
+ "key(s)"
27
105
  end
28
- self.parent_association = BelongsToAssociation.new(self, name.to_sym)
106
+
107
+ self.parent_association =
108
+ BelongsToAssociation.new(self, name.to_sym, options)
109
+
29
110
  parent_association.association_key_columns.each do |column|
30
111
  key :"#{name}_#{column.name}", column.type
31
112
  end
32
113
  def_parent_association_accessors
33
114
  end
34
115
 
116
+ #
117
+ # Declare a child association. The child association should have a
118
+ # `belongs_to` referencing this class or, at a minimum, must have a
119
+ # primary key whose first N columns have the same types as the N
120
+ # columns in this class's primary key.
121
+ #
122
+ # `has_many` associations are read-only, so `has_many :posts` will
123
+ # define a `posts` reader but not a `posts=` writer; and the collection
124
+ # returned by `posts` will be immutable.
125
+ #
126
+ # @param name [Symbol] plural name of association
127
+ # @param options [Options] options for association
128
+ # @option (see HasManyAssociation#initialize)
129
+ # @return [void]
130
+ #
131
+ # @see Associations
132
+ #
35
133
  def has_many(name, options = {})
36
- options.assert_valid_keys(:dependent)
37
-
38
- association = HasManyAssociation.new(self, name.to_sym)
134
+ association = HasManyAssociation.new(self, name.to_sym, options)
39
135
  self.child_associations =
40
136
  child_associations.merge(name => association)
41
137
  def_child_association_reader(association)
42
-
43
- case options[:dependent]
44
- when :destroy
45
- after_destroy { delete_children(name, true) }
46
- when :delete
47
- after_destroy { delete_children(name) }
48
- when nil
49
- else
50
- raise ArgumentError, "Invalid option #{options[:dependent].inspect} provided for :dependent. Specify :destroy or :delete."
51
- end
52
138
  end
53
139
 
54
140
  private
@@ -60,12 +146,12 @@ module Cequel
60
146
 
61
147
  def def_parent_association_reader
62
148
  def_delegator 'self', :read_parent_association,
63
- parent_association.name
149
+ parent_association.name
64
150
  end
65
151
 
66
152
  def def_parent_association_writer
67
153
  def_delegator 'self', :write_parent_association,
68
- "#{parent_association.name}="
154
+ "#{parent_association.name}="
69
155
  end
70
156
 
71
157
  def def_child_association_reader(association)
@@ -75,7 +161,22 @@ module Cequel
75
161
  end
76
162
  RUBY
77
163
  end
164
+ end
78
165
 
166
+ #
167
+ # @private
168
+ #
169
+ def destroy(*)
170
+ super.tap do
171
+ self.class.child_associations.each_value do |association|
172
+ case association.dependent
173
+ when :destroy
174
+ __send__(association.name).destroy_all
175
+ when :delete
176
+ __send__(association.name).delete_all
177
+ end
178
+ end
179
+ end
79
180
  end
80
181
 
81
182
  private
@@ -85,11 +186,11 @@ module Cequel
85
186
  if instance_variable_defined?(ivar_name)
86
187
  return instance_variable_get(ivar_name)
87
188
  end
88
- parent_key_values = key_values.
89
- first(parent_association.association_key_columns.length)
189
+ parent_key_values = key_values
190
+ .first(parent_association.association_key_columns.length)
90
191
  if parent_key_values.none? { |value| value.nil? }
91
192
  clazz = parent_association.association_class
92
- parent = parent_key_values.inject(clazz) do |record_set, key_value|
193
+ parent = parent_key_values.reduce(clazz) do |record_set, key_value|
93
194
  record_set[key_value]
94
195
  end
95
196
  instance_variable_set(ivar_name, parent)
@@ -98,19 +199,20 @@ module Cequel
98
199
 
99
200
  def write_parent_association(parent)
100
201
  unless parent.is_a?(parent_association.association_class)
101
- raise ArgumentError,
102
- "Wrong class for #{parent_association.name}; expected " +
103
- "#{parent_association.association_class.name}, got " +
104
- "#{parent.class.name}"
202
+ fail ArgumentError,
203
+ "Wrong class for #{parent_association.name}; expected " \
204
+ "#{parent_association.association_class.name}, got " \
205
+ "#{parent.class.name}"
105
206
  end
106
207
  instance_variable_set "@#{parent_association.name}", parent
107
208
  key_column_names = self.class.key_column_names
108
- parent.key_attributes.
109
- zip(key_column_names) do |(parent_column_name, value), column_name|
209
+ parent.key_attributes
210
+ .zip(key_column_names) do |(parent_column_name, value), column_name|
110
211
  if value.nil?
111
- raise ArgumentError,
112
- "Can't set parent association #{parent_association.name.inspect} " +
113
- "without value in key #{parent_column_name.inspect}"
212
+ fail ArgumentError,
213
+ "Can't set parent association " \
214
+ "#{parent_association.name.inspect} " \
215
+ "without value in key #{parent_column_name.inspect}"
114
216
  end
115
217
  write_attribute(column_name, value)
116
218
  end
@@ -122,26 +224,16 @@ module Cequel
122
224
  if !reload && instance_variable_defined?(ivar)
123
225
  return instance_variable_get(ivar)
124
226
  end
125
- association_record_set = key_values.inject(association.association_class) do |record_set, key_value|
126
- record_set[key_value]
127
- end
128
- instance_variable_set(
129
- ivar, AssociationCollection.new(association_record_set))
130
- end
131
227
 
132
- def delete_children(association_name, run_callbacks = false)
133
- if run_callbacks
134
- self.send(association_name).each do |c|
135
- c.run_callbacks(:destroy)
228
+ base_scope = association.association_class
229
+ association_record_set =
230
+ key_values.reduce(base_scope) do |record_set, key_value|
231
+ record_set[key_value]
136
232
  end
137
- end
138
- connection[association_name].where(
139
- send(association_name).scoped_key_attributes
140
- ).delete
141
- end
142
233
 
234
+ instance_variable_set(
235
+ ivar, AssociationCollection.new(association_record_set))
236
+ end
143
237
  end
144
-
145
238
  end
146
-
147
239
  end