believer 0.1.4 → 0.2

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -12,31 +12,34 @@ The Believer library is heavily inspired by ActiveRecord. Most patterns used in
12
12
  ### Define your class
13
13
  An example:
14
14
 
15
- require 'believer'
15
+ ``` ruby
16
+ require 'believer'
16
17
 
17
- class Artist < Believer::Base
18
- column :name
19
- column :label
18
+ class Artist < Believer::Base
19
+ column :name
20
+ column :label
20
21
 
21
- primary_key :name
22
- end
22
+ primary_key :name
23
+ end
23
24
 
24
- class Album < Believer::Base
25
- column :artist
26
- column :name
27
- column :release_date, :type => :timestamp
25
+ class Album < Believer::Base
26
+ column :artist
27
+ column :name
28
+ column :release_date, :type => :timestamp
28
29
 
29
- primary_key :artist, :name
30
- end
30
+ primary_key :artist, :name
31
+ end
31
32
 
32
- class Song < Believer::Base
33
- column :artist
34
- column :album
35
- column :name
36
- column :track_number, :type => :integer
33
+ class Song < Believer::Base
34
+ column :artist
35
+ column :album
36
+ column :name
37
+ column :track_number, :type => :integer
38
+ column :data, :cql_type => :blob
37
39
 
38
- primary_key :artist, :album, :name
39
- end
40
+ primary_key :artist, :album, :name
41
+ end
42
+ ```
40
43
 
41
44
  #### The Believer::Base class
42
45
  This is the class you should extend from.
@@ -44,14 +47,25 @@ This is the class you should extend from.
44
47
  #### The column class method
45
48
  Defines the mapping between a Ruby object attribute and a Cassandra column. Also defines a getter and setter attribute with the same name.
46
49
  The second argument is a Hash, which support the following keys:
47
- * type: the data type. Supported are: :string, :integer, :float, :timestamp
48
-
50
+ * type: the data type. Supported values are: :string, :integer, :float, :timestamp, :time
51
+ * cql_type: the CQL data type.
52
+ For type determination, you must include either or both the :type or the :cql_type options.
49
53
 
50
54
  #### The primary_key class method
51
55
  Sets the primary key columns of the class.
52
56
  In a situation where you're only querying data, you don't need to set this.
53
57
  However, if you rely on object equality somewhere in your application, it is advisable to set the primary key, as the primary key values are used in the Believer::Base.eql? method.
54
58
 
59
+ If you wish to use a partition key consisting of multiple columns, use an array as the first part of the primary_key list:
60
+
61
+ ``` ruby
62
+ class Song
63
+ ...
64
+ primary_key [:artist, :album], :name, :...
65
+ ...
66
+ end
67
+ ```
68
+
55
69
  ### Query your class
56
70
  The following methods can be used to query class instances.
57
71
  * where: specify query filters
@@ -64,69 +78,170 @@ All methods are chainable, meaning you can
64
78
  #### The where method
65
79
  Use the where method to specify filters on the result set. These filters correspond to the expressions in the WHERE clause of a Cassandra query.
66
80
 
67
- # Using a hash
68
- Artist.where(:name => 'James Brown')
81
+ ``` ruby
82
+ # Using a hash
83
+ Artist.where(:name => 'James Brown')
69
84
 
70
- # Using a hash mapping key to an array of possible values. Maps to the CQL IN operator
71
- Artist.where(:name => ['Coldplay', 'Depeche Mode', 'Eurythmics'])
85
+ # Using a hash mapping key to an array of possible values. Maps to the CQL IN operator
86
+ Artist.where(:name => ['Coldplay', 'Depeche Mode', 'Eurythmics'])
72
87
 
73
- # Using string with interpolation
74
- Artist.where('name = ?', 'Foreigner')
88
+ # Using string with interpolation
89
+ Artist.where('name = ?', 'Foreigner')
90
+ ```
75
91
 
76
92
  #### The select method
77
93
  Using the select method you can define the columns loaded in a query. These fields correspond to the expressions in the SELECT clause of a Cassandra query.
78
94
  This might be handy in the case you have a table with a lot of columns, but only need a few.
79
95
 
80
- # Select a single field
81
- Artist.select(:name)
96
+ ``` ruby
97
+ # Select a single field
98
+ Artist.select(:name)
82
99
 
83
- # Select a multiple fields
84
- Artist.select(:name, :label)
100
+ # Select a multiple fields
101
+ Artist.select(:name, :label)
102
+ ```
85
103
 
86
104
  #### The limit method
87
105
  Limits the amount of records returned to the specified maximum
88
106
 
89
- # Yield at most 20 class instances
90
- Artist.limit(20)
107
+ ``` ruby
108
+ # Yield at most 20 class instances
109
+ Artist.limit(20)
110
+ ```
91
111
 
92
112
  #### The order method
93
113
  Order the results in using the specified column
94
114
 
95
- # Order ascending by name
96
- Album.order(:name)
97
- Album.order(:name, :asc)
115
+ ``` ruby
116
+ # Order ascending by name
117
+ Album.order(:name)
118
+ Album.order(:name, :asc)
98
119
 
99
- # Order descending by name
100
- Album.order(:name, :desc)
120
+ # Order descending by name
121
+ Album.order(:name, :desc)
122
+ ```
101
123
 
102
124
  #### Method chaining
103
125
  All query methods can be chained.
104
126
  This is done by creating and returning a clone of the receiver. The clone is the receiver of the query method.
105
127
 
106
- # 'Echoes'....
107
- Song.where(:artist => 'Pink Floyd').where(:album => 'Meddle').order(:track_number, :desc).limit(1)
128
+ ``` ruby
129
+ # 'Echoes'....
130
+ Song.where(:artist => 'Pink Floyd').where(:album => 'Meddle').order(:track_number, :desc).limit(1)
131
+ ```
108
132
 
109
133
  ### Configuration
110
134
  If using Rails, place a believer.yml file in the configuration directory of your application.
111
135
  The file structure starts with the the environment name, followed by the connection configuration.
112
136
  This is the client connection configuration passed to the cql-rb gem.
113
137
 
114
- development:
115
- host: 127.0.0.1
116
- port: 9042
117
- keyspace: my_keyspace
138
+ ``` yaml
139
+ development:
140
+ host: 127.0.0.1
141
+ port: 9042
142
+ keyspace: my_keyspace
143
+
144
+ staging:
145
+ host: 'staging.mynetwork.local'
146
+ port: 9042
147
+ keyspace: my_keyspace
148
+ credentials:
149
+ username: john
150
+ password: $FDFD%@#&*
151
+ ```
118
152
 
119
- staging:
120
- host: 'staging.mynetwork.local'
121
- port: 9042
122
- keyspace: my_keyspace
123
- credentials:
124
- username: john
125
- password: $FDFD%@#&*
153
+ In other cases, you will have to programatically set the environment:
126
154
 
155
+ ``` ruby
156
+ Believer::Base.environment = Believer::Environment::BaseEnv.new(:host => '127.0.0.1',
157
+ :keyspace => 'mykeyspace')
158
+ ```
159
+
160
+ ### Connection pooling
161
+ If you wish to use a pool of connections, include a :pool node to the configuration.
162
+ The pool library used is [connection_pool](https://github.com/mperham/connection_pool).
163
+
164
+ ``` yaml
165
+ development:
166
+ host: 127.0.0.1
167
+ port: 9042
168
+ keyspace: my_keyspace
169
+ pool:
170
+ size: 10
171
+ timeout: 5
172
+ ```
173
+
174
+ ## Callbacks
175
+ The Believer::Base supports several callbacks to hook into the lifecycle of the models.
176
+ These callbacks can be included in the body of a Believer::Base subclass, like so:
177
+
178
+ ``` ruby
179
+ class Song < Believer::Base
180
+ after_save :do_something
181
+
182
+ before_destroy do
183
+ puts "About to be destroyed: #{self}"
184
+ end
127
185
 
128
- In other cases, you will have to programatically set the environment:
186
+ def do_something
187
+ puts "Just been saved: #{self}"
188
+ end
189
+ end
190
+ ```
191
+
192
+ Supported callbacks are:
193
+ * after_initialize
194
+ * before_save
195
+ * after_save
196
+ * around_save
197
+ * before_destroy
198
+ * after_destroy
199
+ * around_destroy
200
+
201
+ ## Relations
202
+ If you include Believer::Relation in any class, you can define a relation between the including class and the Believer::Base instances.
203
+
204
+ Supported relations are:
205
+ * one-to-one: use the has_single method in the referencing class
206
+ * one-to-many: use the has_some method in the referencing class
207
+
208
+ Options for both relations are:
209
+ * :class the name of the referenced class. If nil, it will be created from the relation name. Can be a constant or a String
210
+ * :foreign_key the name of the attribute of the referenced class which acts as the key to this object. Can also be an array, in which case the cardinality and order must match with the :key option
211
+ * :key the name of the attribute of the referencing class which acts as the key the referenced records. Can also be an array, in which case the cardinality and order must match with the :foreign_key option
212
+ * :filter a Proc or lambda which is called with a Believer::Query instance as a parameter to tweak the relation query
213
+
214
+ Example(s):
215
+
216
+ ``` ruby
217
+ class Artist
218
+ include Believer::Relation
219
+
220
+ attr_accessor :name
221
+
222
+ has_some :albums, :class => 'Album', :key => :name, :foreign_key => :artist_name
223
+ end
224
+
225
+ class Album < Believer::Base
226
+ column :artist_name
227
+ column :name
228
+ end
229
+
230
+ class Song < Believer::Base
231
+ include Believer::Relation
232
+
233
+ column :artist_name
234
+ column :album_name
129
235
 
130
- Believer::Base.environment = Believer::Environment::BaseEnv.new(:host => '127.0.0.1', :keyspace => 'mykeyspace')
236
+ has_single :album, :class => 'Test::Album', :key => [:artist_name, :album_name], :foreign_key => [:artist_name, :name]
237
+ end
238
+ ```
131
239
 
240
+ ## Test support
241
+ An important aspect to note is that Cassandra does not support transactional rollbacks.
242
+ The consequence of this is that records persisted in a test case are not automatically deleted after a test has executed,
243
+ causing you to 'manually' delete all the garbage.
132
244
 
245
+ To make this a little less labor intensive, you can include the module Believer::Test::TestRunLifeCycle in your test.
246
+ This module will implement an after(:each) hook, which deletes all Believer::Base instance/records created in the span
247
+ of the test.
@@ -13,31 +13,34 @@ require 'cql/client'
13
13
 
14
14
  require 'yaml'
15
15
 
16
- require 'believer/environment.rb'
16
+ require 'believer/cql_helper'
17
+ require 'believer/environment'
17
18
  require 'believer/environment/rails_env'
18
- require 'believer/connection.rb'
19
- require 'believer/values.rb'
20
- require 'believer/columns.rb'
21
- require 'believer/model_schema.rb'
22
- require 'believer/persistence.rb'
23
- require 'believer/command.rb'
24
- require 'believer/querying.rb'
25
- require 'believer/where_clause.rb'
26
- require 'believer/empty_result.rb'
27
- require 'believer/limit.rb'
28
- require 'believer/order_by.rb'
29
- require 'believer/scoped_command.rb'
30
- require 'believer/query.rb'
31
- require 'believer/delete.rb'
32
- require 'believer/insert.rb'
33
- require 'believer/scoping.rb'
34
- require 'believer/batch.rb'
35
- require 'believer/batch_delete.rb'
36
- require 'believer/callbacks.rb'
37
-
38
- require 'believer/observer.rb'
39
- require 'believer/owner.rb'
40
-
41
- require 'believer/ddl.rb'
42
- require 'believer/base.rb'
19
+ require 'believer/environment/merb_env'
20
+ require 'believer/connection'
21
+ require 'believer/values'
22
+ require 'believer/column'
23
+ require 'believer/columns'
24
+ require 'believer/model_schema'
25
+ require 'believer/persistence'
26
+ require 'believer/command'
27
+ require 'believer/querying'
28
+ require 'believer/where_clause'
29
+ require 'believer/empty_result'
30
+ require 'believer/limit'
31
+ require 'believer/order_by'
32
+ require 'believer/scoped_command'
33
+ require 'believer/query'
34
+ require 'believer/delete'
35
+ require 'believer/insert'
36
+ require 'believer/scoping'
37
+ require 'believer/batch'
38
+ require 'believer/batch_delete'
39
+ require 'believer/callbacks'
40
+
41
+ require 'believer/observer'
42
+ require 'believer/relation'
43
+
44
+ require 'believer/ddl'
45
+ require 'believer/base'
43
46
 
@@ -27,6 +27,8 @@ module Believer
27
27
  attrs.each do |name, val|
28
28
  send("#{name}=".to_sym, val)
29
29
  end if attrs.present?
30
+
31
+ yield self if block_given?
30
32
  end
31
33
 
32
34
  def self.instantiate_from_result_rows(row)
@@ -0,0 +1,65 @@
1
+ module Believer
2
+
3
+ # Represents a Cassandra table column
4
+ class Column
5
+ include Values
6
+
7
+ CQL_TYPES = {
8
+ :ascii => {:ruby_type => :string}, # strings US-ASCII character string
9
+ :bigint => {:ruby_type => :integer}, # integers 64-bit signed long
10
+ :blob => {:ruby_type => :string}, # blobs Arbitrary bytes (no validation), expressed as hexadecimal
11
+ :boolean => {:ruby_type => :boolean}, # booleans true or false
12
+ :counter => {:ruby_type => :integer}, # integers Distributed counter value (64-bit long)
13
+ :decimal => {:ruby_type => :float}, # integers, floats Variable-precision decimal
14
+ :double => {:ruby_type => :float}, # integers 64-bit IEEE-754 floating point
15
+ :float => {:ruby_type => :float}, # integers, floats 32-bit IEEE-754 floating point
16
+ :inet => {:ruby_type => :string}, # strings IP address string in IPv4 or IPv6 format*
17
+ :int => {:ruby_type => :integer}, # integers 32-bit signed integer
18
+ :list => {:ruby_type => :array}, # n/a A collection of one or more ordered elements
19
+ :map => {:ruby_type => :hash}, # n/a A JSON-style array of literals: { literal : literal, literal : literal ... }
20
+ :set => {:ruby_type => :array}, # n/a A collection of one or more elements
21
+ :text => {:ruby_type => :string}, # strings UTF-8 encoded string
22
+ :timestamp => {:ruby_type => :time}, # integers, strings Date plus time, encoded as 8 bytes since epoch
23
+ :uuid => {:ruby_type => :string}, # uuids A UUID in standard UUID format
24
+ :timeuuid => {:ruby_type => :integer}, # uuids Type 1 UUID only (CQL 3)
25
+ :varchar => {:ruby_type => :string}, # strings UTF-8 encoded string
26
+ :varint => {:ruby_type => :integer}, # integers Arbitrary-precision integer
27
+ }
28
+
29
+ # Supported Ruby 'types'
30
+ RUBY_TYPES = {
31
+ :integer => {:default_cql_type => :int},
32
+ :string => {:default_cql_type => :varchar},
33
+ :time => {:default_cql_type => :timestamp},
34
+ :timestamp => {:default_cql_type => :timestamp},
35
+ :float => {:default_cql_type => :float}
36
+ }
37
+
38
+ attr_reader :name, :type, :cql_type
39
+
40
+ # Creates a new instance.
41
+ # @param opts [Hash] values options
42
+ # @option opts :name the column name
43
+ # @option opts :type the Ruby type. Can be :integer, :string, :time, :timestamp, :float
44
+ # @option opts :cql_type the CQL type. See Cassandra CQL documentation and {#CQL_TYPES} for supported types
45
+ def initialize(opts)
46
+ raise "Must specify either a :type and/or a :cql_type" if opts[:type].nil? && opts[:cql_type].nil?
47
+ raise "Invalid CQL column type #{opts[:cql_type]}" if opts[:cql_type] && !CQL_TYPES.has_key?(opts[:cql_type])
48
+ raise "Invalid type #{opts[:type]}" unless RUBY_TYPES.has_key?(opts[:type])
49
+
50
+ @name = opts[:name]
51
+ @type = opts[:type].nil? ? CQL_TYPES[opts[:cql_type]][:ruby_type] : opts[:type]
52
+ @cql_type = opts[:cql_type].nil? ? RUBY_TYPES[opts[:type]][:default_cql_type] : opts[:cql_type]
53
+ end
54
+
55
+ # Converts the value to a one that conforms to the type of this column
56
+ # @param v [Object] the value
57
+ def convert_to_type(v)
58
+ convert_method = "convert_to_#{@type}".to_sym
59
+ return self.send(convert_method, v) if respond_to?(convert_method)
60
+ v
61
+ end
62
+
63
+ end
64
+
65
+ end
@@ -19,61 +19,6 @@ module Believer
19
19
 
20
20
  end
21
21
 
22
- class Column
23
- #TYPES = {
24
- # :ascii strings US-ASCII character string
25
- #bigint integers 64-bit signed long
26
- #blob blobs Arbitrary bytes (no validation), expressed as hexadecimal
27
- #boolean booleans true or false
28
- #counter integers Distributed counter value (64-bit long)
29
- #decimal integers, floats Variable-precision decimal
30
- #double integers 64-bit IEEE-754 floating point
31
- #float integers, floats 32-bit IEEE-754 floating point
32
- #inet strings IP address string in IPv4 or IPv6 format*
33
- # int integers 32-bit signed integer
34
- #list n/a A collection of one or more ordered elements
35
- #map n/a A JSON-style array of literals: { literal : literal, literal : literal ... }
36
- #set n/a A collection of one or more elements
37
- #text strings UTF-8 encoded string
38
- #timestamp integers, strings Date plus time, encoded as 8 bytes since epoch
39
- #uuid uuids A UUID in standard UUID format
40
- #timeuuid uuids Type 1 UUID only (CQL 3)
41
- #varchar strings UTF-8 encoded string
42
- #varint integers Arbitrary-precision integer
43
- #}
44
- #
45
- CQL_COL_TYPES = {
46
- :integer => 'INT',
47
- :string => 'VARCHAR',
48
- :timestamp => 'TIMESTAMP',
49
- :float => 'FlOAT'
50
- }
51
-
52
- attr_reader :name, :type
53
-
54
- def initialize(opts)
55
- @name = opts[:name]
56
- @type = opts[:type]
57
- raise "Invalid column type #{@type}" unless CQL_COL_TYPES.has_key?(@type)
58
- @key = opts[:key] == true || !opts[:key].nil?
59
- if @key && opts[:key].is_a?(Hash)
60
- @partition_key = opts[:key][:partition_key]
61
- end
62
- end
63
-
64
- def cql_column_type
65
- CQL_COL_TYPES[@type]
66
- end
67
-
68
- def is_key?
69
- @key
70
- end
71
-
72
- def is_partition_key?
73
- @partition_key
74
- end
75
-
76
- end
77
22
 
78
23
  module ClassMethods
79
24
 
@@ -83,7 +28,7 @@ module Believer
83
28
  @columns ||= {}
84
29
  end
85
30
 
86
- # Defines a column on the model.
31
+ # Defines a column on the model.
87
32
  # The column name must correspond with the Cassandra column name
88
33
  def column(name, opts = {})
89
34
  defaults = {
@@ -91,7 +36,7 @@ module Believer
91
36
  }
92
37
  options = defaults.merge(opts).merge(:name => name)
93
38
 
94
- columns[name] = Column.new(options)
39
+ columns[name] = ::Believer::Column.new(options)
95
40
 
96
41
  self.redefine_method(name) do
97
42
  read_attribute(name)
@@ -152,11 +97,7 @@ module Believer
152
97
  def write_attribute(attr_name, value)
153
98
  v = value
154
99
  # Convert the value to the actual type
155
- unless v.nil? && self.class.columns[attr_name]
156
- value_type = self.class.columns[attr_name].type
157
- convert_method = "convert_to_#{value_type}".to_sym
158
- v = self.send(convert_method, v) if respond_to?(convert_method)
159
- end
100
+ v = self.class.columns[attr_name].convert_to_type(v) unless self.class.columns[attr_name].nil?
160
101
  @attributes[attr_name] = v
161
102
  end
162
103