lotus-model 0.3.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: aa3a89ff8d50e481fffb8cbd68c8cc38bb97e52a
4
- data.tar.gz: c1205c925a3cadac434c381d8f80dd5a15846544
3
+ metadata.gz: b91d99745af6c3080d3e83ff84b369517df2dd77
4
+ data.tar.gz: cda6f8e7128dbb6db125054d0092a6f4fc83b413
5
5
  SHA512:
6
- metadata.gz: 573a4b357ec946e3945e819279fe646f72b22144da46e3d8a1f7adf140d6845e9811fad860bd33cd97afcc5587bac93a470489c73ae7e5355be7e463c453d5cd
7
- data.tar.gz: 2d380bd41f0f1d3f934d1d4b74e036126f34f5fc0207a3066549c0ab9f31fd5c930f06ffea5baa081734b9dd02de19a34028e3a51cc3094ba99f1e1e9a260f46
6
+ metadata.gz: 1ad445fbbf969ebd9dccc1997bd92a10ccc3e4bd3f679d8691b94f9c66940d72afda02d3fcad063001a1905fcae862b5eeb2b0768bc0d85016f10fc3e5e1ef5e
7
+ data.tar.gz: ed8fafcd2491178a6b9646e1a28c07110208c7d8d1dfa15420aeca8482fccc38952058b95d3f86c28e0bc8066a1a9503c11b487348853b442a2bd491e9cf8314
data/CHANGELOG.md CHANGED
@@ -1,6 +1,16 @@
1
1
  # Lotus::Model
2
2
  A persistence layer for Lotus
3
3
 
4
+ ## v0.3.1 - 2015-05-15
5
+ ### Added
6
+ - [Dmitry Tymchuk] Dirty tracking for entities (via `Lotus::Entity::DirtyTracking` module to include)
7
+ - [My Mai] Automatic update of timestamps when an entity is persisted.
8
+ - [Peter Berkenbosch] Introduced `Lotus::Repository#execute`, to execute raw query/commands against database (eg. `BookRepository.execute "SELECT * FROM users"` or `BookRepository.execute "UPDATE users SET admin = 'f'"`)
9
+ - [Guilherme Franco] Memory and File System adapters now accept a block for `where`, `or`, `and` conditions (eg `where { age > 33 }`).
10
+
11
+ ### Fixed
12
+ - [Luca Guidi] Ensure Array coercion to preserve original data structure
13
+
4
14
  ## v0.3.0 - 2015-03-23
5
15
  ### Added
6
16
  - [Linus Pettersson] Database console
data/README.md CHANGED
@@ -150,7 +150,7 @@ However, we suggest to implement this interface by including `Lotus::Entity`, in
150
150
 
151
151
  See [Dependency Inversion Principle](http://en.wikipedia.org/wiki/Dependency_inversion_principle) for more on interfaces.
152
152
 
153
- When a class extend an entity class, it will also *inherit* its mother's attributes.
153
+ When a class extends a `Lotus::Entity` class, it will also *inherit* its mother's attributes.
154
154
 
155
155
  ```ruby
156
156
  require 'lotus/model'
@@ -349,7 +349,7 @@ For advanced mapping and legacy databases, please have a look at the API doc.
349
349
 
350
350
  **Known limitations**
351
351
 
352
- Please be noted there are limitations with inherited entities:
352
+ Note there are limitations with inherited entities:
353
353
 
354
354
  ```ruby
355
355
  require 'lotus/model'
@@ -374,7 +374,7 @@ mapper = Lotus::Model::Mapper.new do
374
374
  end
375
375
  ```
376
376
 
377
- In the example above, there are few problems:
377
+ In the example above, there are a few problems:
378
378
 
379
379
  * `Article` could not be fetched because mapping could not map `price`.
380
380
  * Finding a persisted `RareArticle` record, for eg. `ArticleRepository.find(123)`,
@@ -447,6 +447,108 @@ end
447
447
 
448
448
  **This is not necessary, when Lotus::Model is used within a Lotus application.**
449
449
 
450
+ ## Features
451
+
452
+ ### Timestamps
453
+
454
+ If an entity has the following accessors: `:created_at` and `:updated_at`, they will be automatically updated when the entity is persisted.
455
+
456
+ ```ruby
457
+ require 'lotus/model'
458
+
459
+ class User
460
+ include Lotus::Entity
461
+ attributes :name, :created_at, :updated_at
462
+ end
463
+
464
+ class UserRepository
465
+ include Lotus::Repository
466
+ end
467
+
468
+ Lotus::Model.configure do
469
+ adapter type: :memory, uri: 'memory://localhost/timestamps'
470
+
471
+ mapping do
472
+ collection :users do
473
+ entity User
474
+ repository UserRepository
475
+
476
+ attribute :id, Integer
477
+ attribute :name, String
478
+ attribute :created_at, DateTime
479
+ attribute :updated_at, DateTime
480
+ end
481
+ end
482
+ end.load!
483
+
484
+ user = User.new(name: 'L')
485
+ puts user.created_at # => nil
486
+ puts user.updated_at # => nil
487
+
488
+ user = UserRepository.create(user)
489
+ puts user.created_at.to_s # => "2015-05-15T10:12:20+00:00"
490
+ puts user.updated_at.to_s # => "2015-05-15T10:12:20+00:00"
491
+
492
+ sleep 3
493
+ user.name = "Luca"
494
+ user = UserRepository.update(user)
495
+ puts user.created_at.to_s # => "2015-05-15T10:12:20+00:00"
496
+ puts user.updated_at.to_s # => "2015-05-15T10:12:23+00:00"
497
+ ```
498
+
499
+ ### Dirty Tracking
500
+
501
+ Entities are able to track changes of their data, if `Lotus::Entity::DirtyTracking` is included.
502
+
503
+ ```ruby
504
+ require 'lotus/model'
505
+
506
+ class User
507
+ include Lotus::Entity
508
+ include Lotus::Entity::DirtyTracking
509
+ attributes :name, :age
510
+ end
511
+
512
+ class UserRepository
513
+ include Lotus::Repository
514
+ end
515
+
516
+ Lotus::Model.configure do
517
+ adapter type: :memory, uri: 'memory://localhost/dirty_tracking'
518
+
519
+ mapping do
520
+ collection :users do
521
+ entity User
522
+ repository UserRepository
523
+
524
+ attribute :id, Integer
525
+ attribute :name, String
526
+ attribute :age, String
527
+ end
528
+ end
529
+ end.load!
530
+
531
+ user = User.new(name: 'L')
532
+ user.changed? # => false
533
+
534
+ user.age = 33
535
+ user.changed? # => true
536
+ user.changed_attributes # => {:age=>33}
537
+
538
+ user = UserRepository.create(user)
539
+ user.changed? # => false
540
+
541
+ user.update(name: 'Luca')
542
+ user.changed? # => true
543
+ user.changed_attributes # => {:name=>"Luca"}
544
+
545
+ user = UserRepository.update(user)
546
+ user.changed? # => false
547
+
548
+ result = UserRepository.find(user.id)
549
+ result.changed? # => false
550
+ ```
551
+
450
552
  ## Example
451
553
 
452
554
  For a full working example, have a look at [EXAMPLE.md](https://github.com/lotus/model/blob/master/EXAMPLE.md).
data/lib/lotus/entity.rb CHANGED
@@ -138,24 +138,35 @@ module Lotus
138
138
  #
139
139
  # User.attributes => #<Set: {:id, :name}>
140
140
  # DeletedUser.attributes => #<Set: {:id, :name, :deleted_at}>
141
+ #
141
142
  def attributes(*attrs)
142
143
  if attrs.any?
143
144
  attrs = Lotus::Utils::Kernel.Array(attrs)
144
145
  self.attributes.merge attrs
145
146
 
146
147
  attrs.each do |attr|
147
- attr_accessor(attr) if define_attribute?(attr)
148
+ define_attr_accessor(attr) if defined_attribute?(attr)
148
149
  end
149
150
  else
150
151
  @attributes ||= Set.new
151
152
  end
152
153
  end
153
154
 
155
+ # Define setter/getter methods for attributes.
156
+ #
157
+ # @params attr [Symbol] an attribute name
158
+ #
159
+ # @since 0.3.1
160
+ # @api private
161
+ def define_attr_accessor(attr)
162
+ attr_accessor(attr)
163
+ end
164
+
154
165
  # Check if attr_reader define the given attribute
155
166
  #
156
- # @since 0.2.1
167
+ # @since 0.3.1
157
168
  # @api private
158
- def define_attribute?(name)
169
+ def defined_attribute?(name)
159
170
  name == :id ||
160
171
  !instance_methods.include?(name)
161
172
  end
@@ -237,4 +248,3 @@ module Lotus
237
248
 
238
249
  end
239
250
  end
240
-
@@ -0,0 +1,126 @@
1
+ module Lotus
2
+ module Entity
3
+ # Dirty tracking for entities
4
+ #
5
+ # @since 0.3.1
6
+ module DirtyTracking
7
+ # Override setters for attributes to support dirty tracking
8
+ #
9
+ # @since 0.3.1
10
+ #
11
+ # @example Dirty tracking
12
+ # require 'lotus/model'
13
+ #
14
+ # class User
15
+ # include Lotus::Entity
16
+ # include Lotus::Entity::DirtyTracking
17
+ #
18
+ # attributes :name
19
+ # end
20
+ #
21
+ # article = Article.new(title: 'Generation P')
22
+ # article.changed? # => false
23
+ #
24
+ # article.title = 'Master and Margarita'
25
+ # article.changed? # => true
26
+ #
27
+ # article.changed_attributes # => {:title => "Generation P"}
28
+ def self.included(base)
29
+ base.class_eval do
30
+ extend ClassMethods
31
+ end
32
+ end
33
+
34
+ module ClassMethods
35
+ # Override attribute accessors function.
36
+ # Create setter methods with attribute values checking.
37
+ # If the new value or a changed, added to @changed_attributes.
38
+ #
39
+ # @params attr [Symbol] an attribute name
40
+ #
41
+ # @since 0.3.1
42
+ # @api private
43
+ #
44
+ # @see Lotus::Entity::ClassMethods#define_attr_accessor
45
+ def define_attr_accessor(attr)
46
+ attr_reader(attr)
47
+
48
+ class_eval %{
49
+ def #{ attr }=(value)
50
+ _attribute_changed(:#{ attr }, @#{ attr }, value)
51
+ @#{ attr } = value
52
+ end
53
+ }
54
+ end
55
+ end
56
+
57
+ # Override initialize process.
58
+ #
59
+ # @param attributes [Hash] a set of attribute names and values
60
+ #
61
+ # @since 0.3.1
62
+ #
63
+ # @see Lotus::Entity#initialize
64
+ def initialize(attributes = {})
65
+ _clear_changes_information
66
+ super
67
+ _clear_changes_information
68
+ end
69
+
70
+ # Getter for hash of changed attributes.
71
+ # Return empty hash, if there is no changes
72
+ # Getter for hash of changed attributes. Value in it is the previous one.
73
+ #
74
+ # @return [::Hash] the changed attributes
75
+ #
76
+ # @since 0.3.1
77
+ #
78
+ # @example
79
+ # require 'lotus/model'
80
+ #
81
+ # class User
82
+ # include Lotus::Entity
83
+ # include Lotus::Entity::DirtyTracking
84
+ #
85
+ # attributes :title
86
+ # end
87
+ #
88
+ # article = Article.new(title: 'The crime and punishment')
89
+ # article.changed_attributes # => {}
90
+ #
91
+ # article.title = 'Master and Margarita'
92
+ # article.changed_attributes # => {:title => "The crime and punishment"}
93
+ def changed_attributes
94
+ @changed_attributes.dup
95
+ end
96
+
97
+ # Checks if the attributes were changed
98
+ #
99
+ # @return [TrueClass, FalseClass] the result of the check
100
+ #
101
+ # @since 0.3.1
102
+ def changed?
103
+ @changed_attributes.size > 0
104
+ end
105
+
106
+ private
107
+
108
+ # Set changed attributes in Hash with their old values.
109
+ #
110
+ # @params attrs [Symbol] an attribute name
111
+ #
112
+ # @since 0.3.1
113
+ # @api private
114
+ def _attribute_changed(attr, current_value, new_value)
115
+ @changed_attributes[attr] = new_value if current_value != new_value
116
+ end
117
+
118
+ # Clear all information about dirty data
119
+ #
120
+ # @since 0.3.1
121
+ def _clear_changes_information
122
+ @changed_attributes = {}
123
+ end
124
+ end
125
+ end
126
+ end
data/lib/lotus/model.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require 'lotus/model/version'
2
2
  require 'lotus/entity'
3
+ require 'lotus/entity/dirty_tracking'
3
4
  require 'lotus/repository'
4
5
  require 'lotus/model/mapper'
5
6
  require 'lotus/model/configuration'
@@ -27,6 +28,16 @@ module Lotus
27
28
  class InvalidMappingError < ::StandardError
28
29
  end
29
30
 
31
+ # Error for invalid query
32
+ # It's raised when a query is malformed
33
+ #
34
+ # @since 0.3.1
35
+ class InvalidQueryError < ::StandardError
36
+ def initialize(message = "Invalid query")
37
+ super
38
+ end
39
+ end
40
+
30
41
  include Utils::ClassAttribute
31
42
 
32
43
  # Framework configuration
@@ -201,6 +201,16 @@ module Lotus
201
201
  def connection_string
202
202
  raise NotSupportedError
203
203
  end
204
+
205
+ # Executes a raw statement directly on the connection
206
+ #
207
+ # @param raw [String] the raw statement to execute on the connection
208
+ # @return [Object]
209
+ #
210
+ # @since 0.3.1
211
+ def execute(raw)
212
+ raise NotImplementedError
213
+ end
204
214
  end
205
215
  end
206
216
  end
@@ -1,4 +1,5 @@
1
1
  require 'forwardable'
2
+ require 'ostruct'
2
3
  require 'lotus/utils/kernel'
3
4
 
4
5
  module Lotus
@@ -17,7 +18,7 @@ module Lotus
17
18
  #
18
19
  # query.where(language: 'ruby')
19
20
  # .and(framework: 'lotus')
20
- # .desc(:users_count).all
21
+ # .reverse_order(:users_count).all
21
22
  #
22
23
  # # the records are fetched only when we invoke #all
23
24
  #
@@ -94,13 +95,42 @@ module Lotus
94
95
  #
95
96
  # query.where(year: 1900..1982)
96
97
  #
98
+ # @example Using block
99
+ #
100
+ # query.where { age > 31 }
101
+ #
97
102
  # @example Multiple conditions
98
103
  #
99
104
  # query.where(language: 'ruby')
100
105
  # .where(framework: 'lotus')
101
- def where(condition)
102
- column, value = _expand_condition(condition)
103
- conditions.push([:where, Proc.new{ find_all{|r| r.fetch(column, nil) == value} }])
106
+ #
107
+ # @example Multiple conditions with blocks
108
+ #
109
+ # query.where { language == 'ruby' }
110
+ # .where { framework == 'lotus' }
111
+ #
112
+ # @example Mixed hash and block conditions
113
+ #
114
+ # query.where(language: 'ruby')
115
+ # .where { framework == 'lotus' }
116
+ def where(condition = nil, &blk)
117
+ if blk
118
+ _push_evaluated_block_condition(:where, blk, :find_all)
119
+ elsif condition
120
+ _push_to_expanded_condition(:where, condition) do |column, value|
121
+ Proc.new {
122
+ find_all { |r|
123
+ case value
124
+ when Array,Set,Range
125
+ value.include?(r.fetch(column, nil))
126
+ else
127
+ r.fetch(column, nil) == value
128
+ end
129
+ }
130
+ }
131
+ end
132
+ end
133
+
104
134
  self
105
135
  end
106
136
 
@@ -131,9 +161,24 @@ module Lotus
131
161
  # @example Range
132
162
  #
133
163
  # query.where(country: 'italy').or(year: 1900..1982)
134
- def or(condition=nil, &blk)
135
- column, value = _expand_condition(condition)
136
- conditions.push([:or, Proc.new{ find_all{|r| r.fetch(column) == value} }])
164
+ #
165
+ # @example Using block
166
+ #
167
+ # query.where { age == 31 }.or { age == 32 }
168
+ #
169
+ # @example Mixed hash and block conditions
170
+ #
171
+ # query.where(language: 'ruby')
172
+ # .or { framework == 'lotus' }
173
+ def or(condition = nil, &blk)
174
+ if blk
175
+ _push_evaluated_block_condition(:or, blk, :find_all)
176
+ elsif condition
177
+ _push_to_expanded_condition(:or, condition) do |column, value|
178
+ Proc.new { find_all { |r| r.fetch(column) == value} }
179
+ end
180
+ end
181
+
137
182
  self
138
183
  end
139
184
 
@@ -165,9 +210,29 @@ module Lotus
165
210
  #
166
211
  # query.exclude(language: 'java')
167
212
  # .exclude(company: 'enterprise')
168
- def exclude(condition)
169
- column, value = _expand_condition(condition)
170
- conditions.push([:where, Proc.new{ reject {|r| r.fetch(column) == value} }])
213
+ #
214
+ # @example Using block
215
+ #
216
+ # query.exclude { age > 31 }
217
+ #
218
+ # @example Multiple conditions with blocks
219
+ #
220
+ # query.exclude { language == 'java' }
221
+ # .exclude { framework == 'spring' }
222
+ #
223
+ # @example Mixed hash and block conditions
224
+ #
225
+ # query.exclude(language: 'java')
226
+ # .exclude { framework == 'spring' }
227
+ def exclude(condition = nil, &blk)
228
+ if blk
229
+ _push_evaluated_block_condition(:where, blk, :reject)
230
+ elsif condition
231
+ _push_to_expanded_condition(:where, condition) do |column, value|
232
+ Proc.new { reject { |r| r.fetch(column) == value} }
233
+ end
234
+ end
235
+
171
236
  self
172
237
  end
173
238
 
@@ -204,7 +269,7 @@ module Lotus
204
269
  #
205
270
  # @since 0.1.0
206
271
  #
207
- # @see Lotus::Model::Adapters::Sql::Query#desc
272
+ # @see Lotus::Model::Adapters::Memory::Query#reverse_order
208
273
  #
209
274
  # @example Single column
210
275
  #
@@ -225,6 +290,23 @@ module Lotus
225
290
  self
226
291
  end
227
292
 
293
+ # Alias for order
294
+ #
295
+ # @since 0.1.0
296
+ #
297
+ # @see Lotus::Model::Adapters::Memory::Query#order
298
+ #
299
+ # @example Single column
300
+ #
301
+ # query.asc(:name)
302
+ #
303
+ # @example Multiple columns
304
+ #
305
+ # query.asc(:name, :year)
306
+ #
307
+ # @example Multiple invokations
308
+ #
309
+ # query.asc(:name).asc(:year)
228
310
  alias_method :asc, :order
229
311
 
230
312
  # Specify the descending order of the records, sorted by the given
@@ -234,22 +316,22 @@ module Lotus
234
316
  #
235
317
  # @return self
236
318
  #
237
- # @since 0.1.0
319
+ # @since 0.3.1
238
320
  #
239
- # @see Lotus::Model::Adapters::Sql::Query#order
321
+ # @see Lotus::Model::Adapters::Memory::Query#order
240
322
  #
241
323
  # @example Single column
242
324
  #
243
- # query.desc(:name)
325
+ # query.reverse_order(:name)
244
326
  #
245
327
  # @example Multiple columns
246
328
  #
247
- # query.desc(:name, :year)
329
+ # query.reverse_order(:name, :year)
248
330
  #
249
331
  # @example Multiple invokations
250
332
  #
251
- # query.desc(:name).desc(:year)
252
- def desc(*columns)
333
+ # query.reverse_order(:name).reverse_order(:year)
334
+ def reverse_order(*columns)
253
335
  Lotus::Utils::Kernel.Array(columns).each do |column|
254
336
  modifiers.push(Proc.new{ sort_by!{|r| r.fetch(column)}.reverse! })
255
337
  end
@@ -257,6 +339,25 @@ module Lotus
257
339
  self
258
340
  end
259
341
 
342
+ # Alias for reverse_order
343
+ #
344
+ # @since 0.1.0
345
+ #
346
+ # @see Lotus::Model::Adapters::Memory::Query#reverse_order
347
+ #
348
+ # @example Single column
349
+ #
350
+ # query.desc(:name)
351
+ #
352
+ # @example Multiple columns
353
+ #
354
+ # query.desc(:name, :year)
355
+ #
356
+ # @example Multiple invokations
357
+ #
358
+ # query.desc(:name).desc(:year)
359
+ alias_method :desc, :reverse_order
360
+
260
361
  # Limit the number of records to return.
261
362
  #
262
363
  # @param number [Fixnum]
@@ -486,8 +587,51 @@ module Lotus
486
587
  all.map {|record| record.public_send(column) }.compact
487
588
  end
488
589
 
489
- def _expand_condition(condition)
490
- Array(condition).flatten
590
+ # Expands and yields keys and values of a query hash condition and
591
+ # stores the result and condition type in the conditions array.
592
+ #
593
+ # It yields condition's keys and values to allow the caller to create a proc
594
+ # object to be stored and executed later performing the actual query.
595
+ #
596
+ # @param condition_type [Symbol] the condition type. (eg. `:where`, `:or`)
597
+ # @param condition [Hash] the query condition to be expanded.
598
+ #
599
+ # @return [Array<Array>] the conditions array itself.
600
+ #
601
+ # @api private
602
+ # @since 0.3.1
603
+ def _push_to_expanded_condition(condition_type, condition)
604
+ proc = yield Array(condition).flatten(1)
605
+ conditions.push([condition_type, proc])
606
+ end
607
+
608
+ # Evaluates a block condition of a specified type and stores it in the
609
+ # conditions array.
610
+ #
611
+ # @param condition_type [Symbol] the condition type. (eg. `:where`, `:or`)
612
+ # @param condition [Proc] the query condition to be evaluated and stored.
613
+ # @param strategy [Symbol] the iterator method to be executed.
614
+ # (eg. `:find_all`, `:reject`)
615
+ #
616
+ # @return [Array<Array>] the conditions array itself.
617
+ #
618
+ # @raise [Lotus::Model::InvalidQueryError] if block raises error when
619
+ # evaluated.
620
+ #
621
+ # @api private
622
+ # @since 0.3.1
623
+ def _push_evaluated_block_condition(condition_type, condition, strategy)
624
+ conditions.push([condition_type, Proc.new {
625
+ send(strategy) { |r|
626
+ begin
627
+ OpenStruct.new(r).instance_eval(&condition)
628
+ rescue NoMethodError
629
+ # TODO improve the error message, informing which
630
+ # attributes are invalid
631
+ raise Lotus::Model::InvalidQueryError.new
632
+ end
633
+ }
634
+ }])
491
635
  end
492
636
  end
493
637
  end
@@ -17,7 +17,7 @@ module Lotus
17
17
  #
18
18
  # query.where(language: 'ruby')
19
19
  # .and(framework: 'lotus')
20
- # .desc(:users_count).all
20
+ # .reverse_order(:users_count).all
21
21
  #
22
22
  # # the records are fetched only when we invoke #all
23
23
  #
@@ -70,9 +70,14 @@ module Lotus
70
70
  #
71
71
  # @return [Array] a collection of entities
72
72
  #
73
+ # @raise [Lotus::Model::InvalidQueryError] if there is some issue when
74
+ # hitting the database for fetching records
75
+ #
73
76
  # @since 0.1.0
74
77
  def all
75
78
  Lotus::Utils::Kernel.Array(run)
79
+ rescue Sequel::DatabaseError => e
80
+ raise Lotus::Model::InvalidQueryError.new(e.message)
76
81
  end
77
82
 
78
83
  # Adds a SQL `WHERE` condition.
@@ -117,9 +122,8 @@ module Lotus
117
122
  # query.where{ age > 10 }
118
123
  #
119
124
  # # => SELECT * FROM `users` WHERE (`age` > 31)
120
- def where(condition=nil, &blk)
121
- condition = (condition or blk or raise ArgumentError.new('You need to specify a condition.'))
122
- conditions.push([:where, condition])
125
+ def where(condition = nil, &blk)
126
+ _push_to_conditions(:where, condition || blk)
123
127
  self
124
128
  end
125
129
 
@@ -162,9 +166,8 @@ module Lotus
162
166
  # query.where(name: 'John').or{ age > 31 }
163
167
  #
164
168
  # # => SELECT * FROM `users` WHERE ((`name` = 'John') OR (`age` < 32))
165
- def or(condition=nil, &blk)
166
- condition = (condition or blk or raise ArgumentError.new('You need to specify a condition.'))
167
- conditions.push([:or, condition])
169
+ def or(condition = nil, &blk)
170
+ _push_to_conditions(:or, condition || blk)
168
171
  self
169
172
  end
170
173
 
@@ -209,9 +212,8 @@ module Lotus
209
212
  # query.exclude{ age > 31 }
210
213
  #
211
214
  # # => SELECT * FROM `users` WHERE (`age` <= 31)
212
- def exclude(condition=nil, &blk)
213
- condition = (condition or blk or raise ArgumentError.new('You need to specify a condition.'))
214
- conditions.push([:exclude, condition])
215
+ def exclude(condition = nil, &blk)
216
+ _push_to_conditions(:exclude, condition || blk).inspect
215
217
  self
216
218
  end
217
219
 
@@ -294,7 +296,7 @@ module Lotus
294
296
  #
295
297
  # @since 0.1.0
296
298
  #
297
- # @see Lotus::Model::Adapters::Sql::Query#desc
299
+ # @see Lotus::Model::Adapters::Sql::Query#reverse_order
298
300
  #
299
301
  # @example Single column
300
302
  #
@@ -318,6 +320,29 @@ module Lotus
318
320
  self
319
321
  end
320
322
 
323
+ # Alias for order
324
+ #
325
+ # @since 0.1.0
326
+ #
327
+ # @see Lotus::Model::Adapters::Sql::Query#order
328
+ #
329
+ # @example Single column
330
+ #
331
+ # query.asc(:name)
332
+ #
333
+ # # => SELECT * FROM `people` ORDER BY (`name`)
334
+ #
335
+ # @example Multiple columns
336
+ #
337
+ # query.asc(:name, :year)
338
+ #
339
+ # # => SELECT * FROM `people` ORDER BY `name`, `year`
340
+ #
341
+ # @example Multiple invokations
342
+ #
343
+ # query.asc(:name).asc(:year)
344
+ #
345
+ # # => SELECT * FROM `people` ORDER BY `name`, `year`
321
346
  alias_method :asc, :order
322
347
 
323
348
  # Specify the descending order of the records, sorted by the given
@@ -327,28 +352,28 @@ module Lotus
327
352
  #
328
353
  # @return self
329
354
  #
330
- # @since 0.1.0
355
+ # @since 0.3.1
331
356
  #
332
357
  # @see Lotus::Model::Adapters::Sql::Query#order
333
358
  #
334
359
  # @example Single column
335
360
  #
336
- # query.desc(:name)
361
+ # query.reverse_order(:name)
337
362
  #
338
363
  # # => SELECT * FROM `people` ORDER BY (`name`) DESC
339
364
  #
340
365
  # @example Multiple columns
341
366
  #
342
- # query.desc(:name, :year)
367
+ # query.reverse_order(:name, :year)
343
368
  #
344
369
  # # => SELECT * FROM `people` ORDER BY `name`, `year` DESC
345
370
  #
346
371
  # @example Multiple invokations
347
372
  #
348
- # query.desc(:name).desc(:year)
373
+ # query.reverse_order(:name).reverse_order(:year)
349
374
  #
350
375
  # # => SELECT * FROM `people` ORDER BY `name`, `year` DESC
351
- def desc(*columns)
376
+ def reverse_order(*columns)
352
377
  Array(columns).each do |column|
353
378
  conditions.push([_order_operator, Sequel.desc(column)])
354
379
  end
@@ -356,6 +381,25 @@ module Lotus
356
381
  self
357
382
  end
358
383
 
384
+ # Alias for reverse_order
385
+ #
386
+ # @since 0.1.0
387
+ #
388
+ # @see Lotus::Model::Adapters::Sql::Query#reverse_order
389
+ #
390
+ # @example Single column
391
+ #
392
+ # query.desc(:name)
393
+ #
394
+ # @example Multiple columns
395
+ #
396
+ # query.desc(:name, :year)
397
+ #
398
+ # @example Multiple invokations
399
+ #
400
+ # query.desc(:name).desc(:year)
401
+ alias_method :desc, :reverse_order
402
+
359
403
  # Returns the sum of the values for the given column.
360
404
  #
361
405
  # @param column [Symbol] the column name
@@ -590,7 +634,7 @@ module Lotus
590
634
  # end
591
635
  #
592
636
  # def self.rank
593
- # query.desc(:comments_count)
637
+ # query.reverse_order(:comments_count)
594
638
  # end
595
639
  #
596
640
  # def self.rank_by_author(author)
@@ -621,6 +665,22 @@ module Lotus
621
665
  end
622
666
  end
623
667
 
668
+ # Stores a query condition of a specified type in the conditions array.
669
+ #
670
+ # @param condition_type [Symbol] the condition type. (eg. `:where`, `:or`)
671
+ # @param condition [Hash, Proc] the query condition to be stored.
672
+ #
673
+ # @return [Array<Array>] the conditions array itself.
674
+ #
675
+ # @raise [ArgumentError] if condition is not specified.
676
+ #
677
+ # @api private
678
+ # @since 0.3.1
679
+ def _push_to_conditions(condition_type, condition)
680
+ raise ArgumentError.new('You need to specify a condition.') if condition.nil?
681
+ conditions.push([condition_type, condition])
682
+ end
683
+
624
684
  def _order_operator
625
685
  if conditions.any? {|c, _| c == :order }
626
686
  :order_more
@@ -227,6 +227,20 @@ module Lotus
227
227
  Sql::Console.new(@uri).connection_string
228
228
  end
229
229
 
230
+ # Executes raw sql directly on the connection
231
+ #
232
+ # @param raw [String] the raw sql statement to execute on the connection
233
+ # @return [Object]
234
+ #
235
+ # @since 0.3.1
236
+ def execute(raw)
237
+ begin
238
+ @connection.execute(raw)
239
+ rescue Sequel::DatabaseError => e
240
+ raise Lotus::Model::InvalidQueryError.new(e.message)
241
+ end
242
+ end
243
+
230
244
  private
231
245
 
232
246
  # Returns a collection from the given name.
@@ -17,9 +17,9 @@ module Lotus
17
17
  #
18
18
  # @since 0.1.1
19
19
  #
20
- # @see http://rdoc.info/gems/lotus-utils/Lotus/Utils/Kernel#Array-class_method
20
+ # @see http://ruby-doc.org/core/Kernel.html#method-i-Array
21
21
  def self.Array(arg)
22
- Utils::Kernel.Array(arg) unless arg.nil?
22
+ ::Kernel.Array(arg) unless arg.nil?
23
23
  end
24
24
 
25
25
  # Coerce into a Boolean, unless the argument is nil
@@ -236,6 +236,7 @@ module Lotus
236
236
  # * Float
237
237
  # * Hash
238
238
  # * Integer
239
+ # * BigDecimal
239
240
  # * Set
240
241
  # * String
241
242
  # * Symbol
@@ -3,6 +3,6 @@ module Lotus
3
3
  # Defines the version
4
4
  #
5
5
  # @since 0.1.0
6
- VERSION = '0.3.0'.freeze
6
+ VERSION = '0.3.1'.freeze
7
7
  end
8
8
  end
@@ -252,6 +252,7 @@ module Lotus
252
252
  # article = ArticleRepository.find(23)
253
253
  # article.title # => "Launching Lotus::Model"
254
254
  def persist(entity)
255
+ _touch(entity)
255
256
  @adapter.persist(collection, entity)
256
257
  end
257
258
 
@@ -283,7 +284,8 @@ module Lotus
283
284
  #
284
285
  # ArticleRepository.create(article) # no-op
285
286
  def create(entity)
286
- unless entity.id
287
+ unless _persisted?(entity)
288
+ _touch(entity)
287
289
  @adapter.create(collection, entity)
288
290
  end
289
291
  end
@@ -331,7 +333,8 @@ module Lotus
331
333
  #
332
334
  # ArticleRepository.update(article) # raises Lotus::Model::NonPersistedEntityError
333
335
  def update(entity)
334
- if entity.id
336
+ if _persisted?(entity)
337
+ _touch(entity)
335
338
  @adapter.update(collection, entity)
336
339
  else
337
340
  raise Lotus::Model::NonPersistedEntityError
@@ -379,7 +382,7 @@ module Lotus
379
382
  #
380
383
  # ArticleRepository.delete(article) # raises Lotus::Model::NonPersistedEntityError
381
384
  def delete(entity)
382
- if entity.id
385
+ if _persisted?(entity)
383
386
  @adapter.delete(collection, entity)
384
387
  else
385
388
  raise Lotus::Model::NonPersistedEntityError
@@ -548,6 +551,50 @@ module Lotus
548
551
  end
549
552
  end
550
553
 
554
+ # Executes the given raw statement on the adapter.
555
+ #
556
+ # Please note that it's only supported by some databases,
557
+ # a `NotImplementedError` will be raised when the adapter does not
558
+ # responds to the `execute` method.
559
+ #
560
+ # For advanced scenarios, please check the documentation of each adapter.
561
+ #
562
+ # @param raw [String] the raw statement to execute on the connection
563
+ # @return [Object] the raw result set from SQL adapter
564
+ #
565
+ # @raise [NotImplementedError] if current Lotus::Model adapter doesn't
566
+ # implement `execute`.
567
+ #
568
+ # @see Lotus::Model::Adapters::Abstract#execute
569
+ # @see Lotus::Model::Adapters::SqlAdapter#execute
570
+ #
571
+ # @since 0.3.1
572
+ #
573
+ # @example Basic usage with SQL adapter
574
+ # require 'lotus/model'
575
+ #
576
+ # class Article
577
+ # include Lotus::Entity
578
+ # attributes :title, :body
579
+ # end
580
+ #
581
+ # class ArticleRepository
582
+ # include Lotus::Repository
583
+ # end
584
+ #
585
+ # article = Article.new(title: 'Introducing transactions',
586
+ # body: 'lorem ipsum')
587
+ #
588
+ # result_set = ArticleRepository.execute("SELECT * FROM articles")
589
+ # result_set.each_hash do |result|
590
+ # result # -> { id: 123, title: "Introducing transactions", body: "lorem ipsum"}
591
+ # end
592
+ #
593
+ # puts result_set.class # => SQLite3::ResultSet
594
+ def execute(raw)
595
+ @adapter.execute(raw)
596
+ end
597
+
551
598
  private
552
599
  # Fabricates a query and yields the given block to access the low level
553
600
  # APIs exposed by the query itself.
@@ -661,6 +708,47 @@ module Lotus
661
708
  query.negate!
662
709
  query
663
710
  end
711
+
712
+ # This is a method to check entity persited or not
713
+ #
714
+ # @param entity
715
+ # @return a boolean value
716
+ # @since 0.3.1
717
+ def _persisted?(entity)
718
+ !!entity.id
719
+ end
720
+
721
+ # Update timestamps
722
+ #
723
+ # @param entity [Object, Lotus::Entity] the entity
724
+ #
725
+ # @api private
726
+ # @since 0.3.1
727
+ def _touch(entity)
728
+ now = Time.now.utc
729
+
730
+ if _has_timestamp?(entity, :created_at)
731
+ entity.created_at ||= now
732
+ end
733
+
734
+ if _has_timestamp?(entity, :updated_at)
735
+ entity.updated_at = now
736
+ end
737
+ end
738
+
739
+ # Check if the given entity has the given timestamp
740
+ #
741
+ # @param entity [Object, Lotus::Entity] the entity
742
+ # @param timestamp [Symbol] the timestamp name
743
+ #
744
+ # @return [TrueClass,FalseClass]
745
+ #
746
+ # @api private
747
+ # @since 0.3.1
748
+ def _has_timestamp?(entity, timestamp)
749
+ entity.respond_to?(timestamp) &&
750
+ entity.respond_to?("#{ timestamp }=")
751
+ end
664
752
  end
665
753
  end
666
754
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lotus-model
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Luca Guidi
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2015-03-23 00:00:00.000000000 Z
12
+ date: 2015-05-15 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: lotus-utils
@@ -96,6 +96,7 @@ files:
96
96
  - README.md
97
97
  - lib/lotus-model.rb
98
98
  - lib/lotus/entity.rb
99
+ - lib/lotus/entity/dirty_tracking.rb
99
100
  - lib/lotus/model.rb
100
101
  - lib/lotus/model/adapters/abstract.rb
101
102
  - lib/lotus/model/adapters/file_system_adapter.rb