lotus-model 0.3.0 → 0.3.1

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