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