og 0.27.0 → 0.28.0

Sign up to get free protection for your applications and to get access to all the features.
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