og 0.27.0 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. data/ProjectInfo +2 -2
  2. data/README +8 -4
  3. data/Rakefile +1 -1
  4. data/doc/AUTHORS +1 -1
  5. data/doc/RELEASES +81 -0
  6. data/examples/README +7 -0
  7. data/lib/glue/cacheable.rb +152 -0
  8. data/lib/glue/hierarchical.rb +5 -4
  9. data/lib/glue/optimistic_locking.rb +0 -1
  10. data/lib/glue/orderable.rb +46 -44
  11. data/lib/glue/taggable.rb +7 -4
  12. data/lib/glue/timestamped.rb +1 -1
  13. data/lib/og.rb +13 -6
  14. data/lib/og/entity.rb +226 -9
  15. data/lib/og/evolution.rb +2 -2
  16. data/lib/og/ez/clause.rb +147 -0
  17. data/lib/og/ez/condition.rb +181 -0
  18. data/lib/og/manager.rb +31 -30
  19. data/lib/og/relation.rb +5 -5
  20. data/lib/og/relation/has_many.rb +3 -1
  21. data/lib/og/relation/joins_many.rb +1 -1
  22. data/lib/og/store.rb +6 -3
  23. data/lib/og/store/kirby.rb +3 -5
  24. data/lib/og/store/mysql.rb +0 -1
  25. data/lib/og/store/sql.rb +43 -7
  26. data/lib/og/store/sqlite.rb +97 -11
  27. data/lib/og/store/sqlite2.rb +231 -0
  28. data/lib/og/test/testcase.rb +1 -1
  29. data/lib/og/vendor/mysql.rb +103 -25
  30. data/test/glue/tc_revisable.rb +11 -11
  31. data/test/og/CONFIG.rb +20 -8
  32. data/test/og/mixin/tc_hierarchical.rb +5 -3
  33. data/test/og/mixin/tc_optimistic_locking.rb +6 -4
  34. data/test/og/mixin/tc_orderable.rb +22 -22
  35. data/test/og/mixin/tc_taggable.rb +15 -11
  36. data/test/og/mixin/tc_timestamped.rb +4 -2
  37. data/test/og/multi_validations_model.rb +8 -0
  38. data/test/og/store/tc_filesys.rb +15 -12
  39. data/test/og/store/tc_kirby.rb +14 -11
  40. data/test/og/tc_accumulator.rb +1 -3
  41. data/test/og/tc_cacheable.rb +58 -0
  42. data/test/og/tc_delete_all.rb +13 -16
  43. data/test/og/tc_ez.rb +33 -0
  44. data/test/og/tc_finder.rb +2 -4
  45. data/test/og/tc_inheritance.rb +3 -3
  46. data/test/og/tc_inheritance2.rb +2 -3
  47. data/test/og/tc_join.rb +3 -2
  48. data/test/og/tc_multi_validations.rb +3 -3
  49. data/test/og/tc_multiple.rb +3 -6
  50. data/test/og/tc_override.rb +19 -13
  51. data/test/og/tc_polymorphic.rb +1 -3
  52. data/test/og/tc_resolve.rb +32 -0
  53. data/test/og/tc_reverse.rb +27 -28
  54. data/test/og/tc_scoped.rb +2 -4
  55. data/test/og/tc_select.rb +1 -3
  56. data/test/og/tc_store.rb +3 -8
  57. data/test/og/tc_validation.rb +2 -2
  58. data/test/og/tc_validation2.rb +56 -58
  59. data/test/og/tc_validation_loop.rb +2 -5
  60. metadata +15 -7
  61. data/lib/og/vendor/mysql411.rb +0 -306
@@ -0,0 +1,181 @@
1
+ # gmosx: Work in progress.
2
+
3
+ module Caboose
4
+
5
+ module EZ
6
+ # EZ::Condition plugin for generating the :conditions where clause
7
+ # for ActiveRecord::Base.find. And an extension to ActiveRecord::Base
8
+ # called AR::Base.find_with_conditions that takes a block and builds
9
+ # the where clause dynamically for you.
10
+
11
+ class Condition
12
+ # need this so that id doesn't call Object#id
13
+ # left it open to add more methods that
14
+ # conflict when I find them
15
+ [:id].each { |m| undef_method m }
16
+
17
+ # these are also reserved words regarding SQL column names
18
+ # use esc_* prefix to circumvent any issues
19
+ attr_reader :clauses
20
+ attr_accessor :inner
21
+ attr_accessor :outer
22
+
23
+ # Initialize @clauses and eval the block so
24
+ # it invokes method_missing.
25
+ def initialize(*args, &block)
26
+ options = args.last.is_a?(Hash) ? args.last : {}
27
+ options[:table_name] = args.first if args.first.kind_of? Symbol
28
+ @table_name = options.delete(:table_name) || nil
29
+ @outer = options.delete(:outer) || :and
30
+ @inner = options.delete(:inner) || :and
31
+ @clauses = []
32
+ instance_eval(&block) if block_given?
33
+ end
34
+
35
+ # When invoked with the name of the column in each statement inside the block:
36
+ # A new Clause instance is created and recieves the args. Then the operator
37
+ # hits method_missing and gets sent to a new Clause instance where it either
38
+ # matches one of the defined ops or hits method_missing there.
39
+ #
40
+ # When invoked with an attached block a subcondition is created. The name
41
+ # is regarded as the table_name, additional parameters for outer and inner
42
+ # are passed on.
43
+ def method_missing(name, *args, &block)
44
+ if block_given?
45
+ # handle name as table_name and create a subcondition
46
+ options = args.last.is_a?(Hash) ? args.last : {}
47
+ options[:table_name] ||= name
48
+ define_sub(options, &block)
49
+ else
50
+ clause(name, *args)
51
+ end
52
+ end
53
+
54
+ # You can define clauses dynamicly using this method. It will take a
55
+ # clause and create the correct Clause object to process the conditions
56
+ def clause(name, *args)
57
+ if name.kind_of?(Array)
58
+ c = Clause.new(name.first, name.last)
59
+ elsif args.last.kind_of?(Symbol)
60
+ c = Clause.new(args.pop, name)
61
+ else
62
+ c = Clause.new(@table_name, name)
63
+ end
64
+ @clauses << c
65
+ c
66
+ end
67
+
68
+ # Create subcondition from a block, optionally specifying table_name, outer and inner.
69
+ # :outer determines how the subcondition is added to the condition, while :inner
70
+ # determines the internal 'joining' of conditions inside the subcondition. Both
71
+ # :inner & :outer defult to 'AND'
72
+ def define_sub(*args, &block)
73
+ options = args.last.is_a?(Hash) ? args.last : {}
74
+ options[:table_name] = args.first if args.first.kind_of? Symbol
75
+ options[:table_name] ||= @table_name
76
+ cond = Condition.new(options, &block)
77
+ self << cond
78
+ end
79
+
80
+ # Aliases for syntax convenience. :sub or :condition map to :define_sub
81
+ alias :sub :define_sub
82
+ alias :condition :define_sub
83
+
84
+ # Shortcut for adding a :and boolean joined subcondition
85
+ def and_condition(*args, &block)
86
+ options = args.last.is_a?(Hash) ? args.last : {}
87
+ options[:table_name] = args.first if args.first.kind_of? Symbol
88
+ options[:outer] ||= @outer
89
+ options[:inner] ||= :and
90
+ define_sub(options, &block)
91
+ end
92
+
93
+ # Alias :all to be shorthand for :and_condition
94
+ alias :all :and_condition
95
+
96
+ # Shortcut for adding a :or boolean joined subcondition
97
+ def or_condition(*args, &block)
98
+ options = args.last.is_a?(Hash) ? args.last : {}
99
+ options[:table_name] = args.first if args.first.kind_of? Symbol
100
+ options[:outer] ||= @outer
101
+ options[:inner] ||= :or
102
+ define_sub(options, &block)
103
+ end
104
+
105
+ # Alias :any to stand in for :or_condition
106
+ alias :any :or_condition
107
+
108
+ # Append a condition element, which can be one of the following:
109
+ # - String: raw sql string
110
+ # - ActiveRecord instance, for attribute or PK cloning
111
+ # - Condition or Clause with to_sql method and outer property
112
+ # - Array in ActiveRecord format ['column = ?', 2]
113
+ def <<(condition, outer = nil)
114
+ if condition.kind_of?(String) and not condition.to_s.empty?
115
+ cond = SqlClause.new(condition)
116
+ cond.outer = outer || :and
117
+ @clauses << cond
118
+ elsif condition.kind_of?(Og::EntityMixin)
119
+ if condition.attributes[condition.class.primary_key].nil?
120
+ condition.attributes.each { |k, v| clause([condition.class.table_name, k]) == v unless v.to_s.empty? }
121
+ else
122
+ clause([condition.class.table_name, condition.class.primary_key]) == condition.attributes[condition.class.primary_key]
123
+ end
124
+ else
125
+ if condition.kind_of?(Condition) or condition.kind_of?(AbstractClause)
126
+ logic = condition.outer if outer.nil?
127
+ condition = condition.to_sql
128
+ else
129
+ logic = outer
130
+ end
131
+ if condition.kind_of?(Array) and not condition.empty?
132
+ array_clause = ArrayClause.new(condition)
133
+ array_clause.outer = logic
134
+ @clauses << array_clause
135
+ end
136
+ end
137
+ end
138
+
139
+ # Aliases for :<<, the method itself deals with what kind
140
+ # of condition you are appending to the chain so these
141
+ # aliases are for a nicer syntax's sake.
142
+ alias :sql_condition :<<
143
+ alias :add_sql :<<
144
+ alias :clone_from :<<
145
+ alias :append :<<
146
+
147
+ # Loop over all Clause onjects in @clauses array
148
+ # and call to_sql on each instance. Then join
149
+ # the queries and params into the :conditions
150
+ # array with logic defaulting to AND.
151
+ # Subqueries are joined together using their
152
+ # individual outer property setting if present.
153
+ # Also defaults to AND.
154
+ def to_sql(logic=@inner)
155
+ params = []; query = []
156
+ @clauses.each do |cv|
157
+ q, p, e = cv.to_sql
158
+ unless q.to_s.empty?
159
+ logic = cv.outer ? cv.outer : logic
160
+ logic = logic.to_s.upcase
161
+ logic = 'AND NOT' if logic == 'NOT'
162
+ query << logic unless query.empty?
163
+ query << q
164
+ if cv.test == :in
165
+ params << p if p.respond_to?(:map)
166
+ elsif p.kind_of?(Array)
167
+ p.flatten! unless q =~ /IN/
168
+ params += p
169
+ else
170
+ params << p unless p.nil?
171
+ params << e unless e.nil?
172
+ end
173
+ end
174
+ end
175
+ [query.join(' '), *params ]
176
+ end
177
+ end
178
+
179
+ end # EZ module
180
+
181
+ end # Caboose module
@@ -1,5 +1,5 @@
1
- require 'mega/pool'
2
- require 'nano/class/descendents'
1
+ require 'facet/pool'
2
+ require 'facet/class/descendents'
3
3
 
4
4
  require 'og/entity'
5
5
  require 'og/store'
@@ -10,6 +10,15 @@ module Og
10
10
  # managed by Og.
11
11
 
12
12
  class Manager
13
+ def self.managers
14
+ managers = []
15
+ ObjectSpace.each_object(self) { |o| managers << o }
16
+ managers
17
+ end
18
+
19
+ def self.managed?(klass)
20
+ self.managers.any? { |m| m.managed? klass }
21
+ end
13
22
 
14
23
  # Information about an Entity class.
15
24
 
@@ -34,9 +43,15 @@ class Manager
34
43
 
35
44
  attr_accessor :store
36
45
 
37
- # The collection of Entities managed by this manager.
46
+ # The collection of Entities (managed classes) managed by
47
+ # this manager.
38
48
 
39
49
  attr_accessor :entities
50
+
51
+ # The managed object cache. This cache is optional. When
52
+ # used it improves object lookups.
53
+
54
+ attr_accessor :cache
40
55
 
41
56
  def initialize(options)
42
57
  @options = options
@@ -135,7 +150,7 @@ class Manager
135
150
  #++
136
151
 
137
152
  def manageable?(klass)
138
- klass.respond_to?(:properties) and (!klass.properties.empty?) # and klass.ann.self.polymorphic.nil?
153
+ klass.respond_to?(:properties) and (!klass.properties.empty?) # and !self.class.managed?(klass) # and klass.ann.self.polymorphic.nil?
139
154
  end
140
155
 
141
156
  # Is the class managed by Og?
@@ -145,9 +160,8 @@ class Manager
145
160
  end
146
161
  alias_method :entity?, :managed?
147
162
 
148
- # ==
149
163
  # Returns an array containing all classes managed by this manager.
150
- # ==
164
+
151
165
  def managed_classes
152
166
  @entities.map {|e| e[0]}
153
167
  end
@@ -186,29 +200,6 @@ class Manager
186
200
  classes.each { |c| Relation.resolve_targets(c) }
187
201
  classes.each { |c| Relation.resolve_names(c) }
188
202
  classes.each { |c| manage(c) }
189
- =begin
190
- # gmosx: LETS better investigate this.
191
-
192
- # Checks for obsolete tables lying around in the og store.
193
- # rp: this shouldn't really be here but I can't think of a better
194
- # way to add this functionality.
195
-
196
- store = get_store()
197
-
198
- if store.respond_to? :unmanaged_tables
199
- unmanaged_tables = store.unmanaged_tables(self)
200
- unmanaged_tables.each do |table|
201
-
202
- if @options[:evolve_schema] == true and @options[:evolve_schema_purge_tables] == true
203
- sql = "DROP TABLE #{table}"
204
- Logger.debug "Dropping unmanaged database table #{table}"
205
- store.conn.exec(sql)
206
- else
207
- Logger.info "There is a table within the database named '#{table}' that is not managed by an Og class."
208
- end
209
- end
210
- end
211
- =end
212
203
  end
213
204
  alias_method :manage_class, :manage_classes
214
205
 
@@ -233,7 +224,17 @@ class Manager
233
224
  def post_setup
234
225
  store.post_setup if store.respond_to?(:post_setup)
235
226
  end
236
-
227
+
228
+ # Dump a nice name for this store.
229
+ =begin
230
+ def to_s
231
+ if store = get_store
232
+ store.to_s
233
+ else
234
+ 'Uninitialized'
235
+ end
236
+ end
237
+ =end
237
238
  end
238
239
 
239
240
  end
@@ -1,8 +1,8 @@
1
- require 'nano/kernel/constant'
2
- require 'nano/string/capitalized'
3
- require 'mega/orm_support'
4
- require 'mega/inheritor'
5
- require 'mega/annotation'
1
+ require 'facet/kernel/constant'
2
+ require 'facet/string/capitalized'
3
+ require 'facet/ormsupport'
4
+ require 'facet/inheritor'
5
+ require 'facet/annotation'
6
6
 
7
7
  module Og
8
8
 
@@ -1,4 +1,4 @@
1
- require 'mega/orm_support'
1
+ require 'facet/ormsupport'
2
2
 
3
3
  require 'og/relation'
4
4
  require 'og/collection'
@@ -56,6 +56,8 @@ class HasMany < Relation
56
56
 
57
57
  def add_#{target_singular_name}(obj, options = nil)
58
58
  return unless obj
59
+ # save the object if needed to generate a primary_key.
60
+ self.save unless self.saved?
59
61
  obj.#{foreign_key} = @#{owner_class.primary_key}
60
62
  obj.save
61
63
  end
@@ -1,4 +1,4 @@
1
- require 'mega/orm_support'
1
+ require 'facet/ormsupport'
2
2
 
3
3
  require 'og/relation'
4
4
  require 'og/collection'
@@ -50,9 +50,12 @@ class Store
50
50
  # Enchants a class.
51
51
 
52
52
  def enchant(klass, manager)
53
- klass.class.send(:define_method, :index) do |arg|
54
- meta :index, arg
55
- end
53
+ # gmosx, FIXME:
54
+ # ARGH, have to update this!
55
+ #
56
+ # klass.class.send(:define_method, :index) do |arg|
57
+ # meta :index, arg
58
+ # end
56
59
 
57
60
  pk = klass.primary_key.symbol
58
61
 
@@ -68,14 +68,10 @@ class KirbyStore < SqlStore
68
68
  klass.send :alias_method, :recno, :oid
69
69
  klass.send :alias_method, :recno=, :oid=
70
70
 
71
- unless klass.properties.include? :recno
72
- klass.property :recno, Fixnum
73
- end
74
-
75
71
  symbols = klass.properties.keys
76
72
 
77
73
  klass.module_eval %{
78
- def self.kb_create(#{symbols.join(', ')})
74
+ def self.kb_create(recno, #{symbols.join(', ')})
79
75
  obj = self.allocate
80
76
  obj.recno = recno
81
77
  #{ symbols.map { |s| "obj.#{s} = #{s}; "} }
@@ -281,6 +277,8 @@ private
281
277
 
282
278
  def eval_og_insert(klass)
283
279
  pk = klass.primary_key.symbol
280
+ props = klass.properties.values.dup
281
+ values = props.collect { |p| write_prop(p) }.join(',')
284
282
 
285
283
  if klass.schema_inheritance?
286
284
  props << Property.new(:symbol => :ogtype, :klass => String)
@@ -6,7 +6,6 @@ rescue Object => ex
6
6
  begin
7
7
  # Attempt to use the included pure ruby version.
8
8
  require 'og/vendor/mysql'
9
- require 'og/vendor/mysql411'
10
9
  rescue Object => ex
11
10
  Logger.error ex
12
11
  end
@@ -1,10 +1,12 @@
1
1
  require 'yaml'
2
2
  require 'time'
3
3
 
4
- require 'nano/kernel/constant'
4
+ require 'facet/kernel/constant'
5
5
 
6
6
  module Og
7
7
 
8
+ # A collection of useful SQL utilities.
9
+
8
10
  module SqlUtils
9
11
 
10
12
  # Escape an SQL string
@@ -174,7 +176,6 @@ module SqlUtils
174
176
  end
175
177
 
176
178
  def join_table_info(owner_class, target_class, postfix = nil)
177
-
178
179
  # some fixes for schema inheritance.
179
180
 
180
181
  raise "Undefined owner_class in #{target_class}" unless owner_class
@@ -304,7 +305,6 @@ class SqlStore < Store
304
305
  # Enchants a class.
305
306
 
306
307
  def enchant(klass, manager)
307
-
308
308
  # setup the table where this class is mapped.
309
309
 
310
310
  if klass.schema_inheritance_child?
@@ -314,6 +314,11 @@ class SqlStore < Store
314
314
  klass.const_set 'OGTABLE', table(klass)
315
315
  end
316
316
 
317
+ #--
318
+ # FIXME: use an SQL agnostic name like schema instead
319
+ # of table.
320
+ #++
321
+
317
322
  klass.module_eval 'def self.table; OGTABLE; end'
318
323
 
319
324
  eval_og_allocate(klass)
@@ -322,12 +327,14 @@ class SqlStore < Store
322
327
 
323
328
  unless klass.polymorphic_parent?
324
329
  # precompile class specific lifecycle methods.
330
+
325
331
  eval_og_create_schema(klass)
326
332
  eval_og_insert(klass)
327
333
  eval_og_update(klass)
328
334
  eval_og_delete(klass)
329
335
 
330
336
  # create the table if needed.
337
+
331
338
  klass.allocate.og_create_schema(self)
332
339
 
333
340
  # finish up with eval_og_read, since we can't do that
@@ -335,7 +342,8 @@ class SqlStore < Store
335
342
  # Possible FIXME: This means you can't do any find-type
336
343
  # operations in og_create_schema. Luckily, you're most
337
344
  # likely to want to do .create, which is covered by
338
- # og_insert.
345
+ # og_insert.
346
+
339
347
  eval_og_read(klass)
340
348
  end
341
349
  end
@@ -828,8 +836,21 @@ private
828
836
  end
829
837
 
830
838
  # Resolve the finder options. Also takes scope into account.
839
+ # This method handles among other the following cases:
840
+ #
841
+ # User.find :condition => "name LIKE 'g%'", :order => 'name ASC'
842
+ # User.find :where => "name LIKE 'g%'", :order => 'name ASC'
843
+ # User.find :sql => "WHERE name LIKE 'g%' ORDER BY name ASC"
844
+ # User.find :condition => [ 'name LIKE ?', 'g%' ], :order => 'name ASC', :limit => 10
845
+ #
846
+ # If an array is passed as a condition, use prepared statement
847
+ # style escaping. For example:
848
+ #
849
+ # User.find :condition => [ 'name = ? AND age > ?', 'gmosx', 12 ]
850
+ #
851
+ # Proper escaping is performed to avoid SQL injection attacks.
831
852
  #--
832
- # FIXME: cleanup/refactor.
853
+ # FIXME: cleanup/refactor, this is an IMPORTANT method.
833
854
  #++
834
855
 
835
856
  def resolve_options(klass, options)
@@ -888,7 +909,9 @@ private
888
909
 
889
910
  update_condition(options, scond) if scond
890
911
 
891
- # rp: type is not set in all instances such as Class.first so this fix goes here for now.
912
+ # rp: type is not set in all instances such as Class.first
913
+ # so this fix goes here for now.
914
+
892
915
  if ogtype = options[:type] || (klass.schema_inheritance_child? ? "#{klass}" : nil)
893
916
  update_condition options, "ogtype='#{ogtype}'"
894
917
  end
@@ -896,6 +919,16 @@ private
896
919
  sql = "SELECT #{fields} FROM #{tables.join(',')}"
897
920
 
898
921
  if condition = options[:condition] || options[:where]
922
+ # If an array is passed as a condition, use prepared
923
+ # statement style escaping.
924
+
925
+ if condition.is_a?(Array)
926
+ args = condition.dup
927
+ str = args.shift
928
+ args.each { |arg| str.sub!(/\?/, quote(arg)) }
929
+ condition = str
930
+ end
931
+
899
932
  sql << " WHERE #{condition}"
900
933
  end
901
934
 
@@ -969,7 +1002,10 @@ private
969
1002
  end
970
1003
 
971
1004
  res_row = res.next
972
- # causes STI classes to come back as the correct child class if accessed from the superclass
1005
+
1006
+ # causes STI classes to come back as the correct child class
1007
+ # if accessed from the superclass.
1008
+
973
1009
  klass = Og::Entity::entity_from_string(res_row.result.flatten[res_row.fieldnum('ogtype')]) if klass.schema_inheritance?
974
1010
  obj = klass.og_allocate(res_row, 0)
975
1011