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
@@ -1,4 +1,4 @@
1
- require 'nano/inflect'
1
+ require 'facet/inflect'
2
2
 
3
3
  # The default Tag implementation. A tag attaches semantics to
4
4
  # a given object.
@@ -25,7 +25,10 @@ class Tag
25
25
  #++
26
26
 
27
27
  def tag(obj)
28
- send(obj.class.name.underscore.pluralize.to_sym) << obj
28
+ class_name = obj.class.name
29
+ method_name = class_name.index('::') ? (class_name =~ /.*?\:\:(.*)/; $1) : class_name
30
+
31
+ send(method_name.pluralize.underscore.to_sym) << obj
29
32
  @count += 1
30
33
  save!
31
34
  end
@@ -119,7 +122,7 @@ module Taggable
119
122
  def find_with_tags(*names)
120
123
  info = ogmanager.store.join_table_info(self, Tag)
121
124
  count = names.size
122
- names = names.map { |n| "'#{n}'" }.join(',')
125
+ names = names.map { |n| ogmanager.store.quote(n) }.join(',')
123
126
  sql = %{
124
127
  SELECT o.*
125
128
  FROM
@@ -142,7 +145,7 @@ module Taggable
142
145
  def find_with_any_tag(*names)
143
146
  info = ogmanager.store.join_table_info(self, Tag)
144
147
  count = names.size
145
- names = names.map { |n| "'#{n}'" }.join(',')
148
+ names = names.map { |n| ogmanager.store.quote(n) }.join(',')
146
149
  sql = %{
147
150
  SELECT o.*
148
151
  FROM
@@ -1,4 +1,4 @@
1
- require 'nano/time/stamp'
1
+ require 'facet/time/stamp'
2
2
 
3
3
  require 'glue/aspects'
4
4
 
data/lib/og.rb CHANGED
@@ -1,15 +1,15 @@
1
1
  # = Og
2
2
  #
3
- # Copyright (c) 2004-2005, Navel Ltd (http://www.navel.gr)
4
3
  # Copyright (c) 2004-2005, George Moschovitis (http://www.gmosx.com)
4
+ # Copyright (c) 2004-2005, Navel Ltd (http://www.navel.gr)
5
5
  #
6
6
  # Og (http://www.nitrohq.com) is copyrighted free software
7
7
  # created and maintained by George Moschovitis (mailto:gm@navel.gr)
8
8
  # and released under the standard BSD Licence. For details
9
9
  # consult the file doc/LICENCE.
10
10
 
11
- require 'mega/synchash'
12
- require 'mega/syncarray'
11
+ require 'facet/synchash'
12
+ require 'facet/syncarray'
13
13
 
14
14
  require 'glue'
15
15
  require 'glue/logger'
@@ -43,7 +43,7 @@ module Og
43
43
 
44
44
  # The version.
45
45
 
46
- Version = '0.27.0'
46
+ Version = '0.28.0'
47
47
 
48
48
  # Library path.
49
49
 
@@ -95,6 +95,14 @@ module Og
95
95
 
96
96
  setting :thread_safe, :default => true, :doc => 'Enable/dissable thread safe mode'
97
97
 
98
+ # Address of the Og cache (if distributed caching enabled).
99
+
100
+ setting :cache_address, :default => '127.0.0.1', :doc => 'Address of the Og cache'
101
+
102
+ # Port of the Og cache (if distributed caching enabled).
103
+
104
+ setting :cache_port, :default => '9070', :doc => 'Port of the Og cache'
105
+
98
106
  # The active manager
99
107
 
100
108
  mattr_accessor :manager
@@ -119,7 +127,7 @@ module Og
119
127
 
120
128
  m = @@manager = Manager.new(options)
121
129
  m.manage_classes
122
-
130
+
123
131
  # Allows functionality that requires a store is
124
132
  # finalized to be implemented. A vastly superior
125
133
  # method of constructing foreign key constraints is an
@@ -147,7 +155,6 @@ module Og
147
155
  def escape(str)
148
156
  @@manager.store.escape(str)
149
157
  end
150
-
151
158
  end
152
159
 
153
160
  end
@@ -1,30 +1,55 @@
1
- require 'mega/class_inherit'
2
- require 'nano/kernel/assign_with'
1
+ require 'facet/classinherit'
2
+ require 'facet/kernel/assign_with'
3
3
 
4
4
  require 'glue/property'
5
5
  require 'og/relation'
6
+ require 'og/ez/clause'
7
+ require 'og/ez/condition'
6
8
 
7
9
  module Og
8
10
 
9
11
  # Include this module to classes to make them managable by Og.
12
+ #--
13
+ # gmosx, WARTNING: If you change the methods here, don't
14
+ # forget to update the Cacheable overrides.
15
+ #++
10
16
 
11
17
  module EntityMixin
12
18
 
19
+ def self.included(base)
20
+ # If the after_enchant callback is defined, call it
21
+ # to allow for some customization. Have a look at cacheable
22
+ # for an example.
23
+
24
+ if base.respond_to?(:after_enchant)
25
+ base.after_enchant(base)
26
+ end
27
+ end
28
+
29
+ # Persist the object.
30
+
13
31
  def save(options = nil)
14
32
  self.class.ogmanager.store.save(self, options)
15
33
  end
16
34
  alias_method :save!, :save
17
35
  alias_method :validate_and_save, :save
18
36
 
37
+ # Force saving of the objects, even if the validations
38
+ # don't pass.
39
+
19
40
  def force_save!(options = nil)
20
41
  self.class.ogmanager.store.force_save(self, options)
21
42
  end
22
43
 
44
+ # Insert the object in the store.
45
+
23
46
  def insert
24
47
  self.class.ogmanager.store.insert(self)
25
48
  return self
26
49
  end
27
50
 
51
+ # Update an existing object in the store.
52
+
28
53
  def update(options = nil)
29
54
  self.class.ogmanager.store.update(self, options)
30
55
  end
@@ -59,15 +84,13 @@ module EntityMixin
59
84
  self.class.ogmanager.store.transaction(&block)
60
85
  end
61
86
 
87
+ # Is this object saved in the store?
88
+
62
89
  def saved?
63
90
  not @oid.nil?
64
91
  end
65
92
  alias_method :serialized?, :saved?
66
93
 
67
- def og_quote(obj)
68
- self.class.ogmanager.store.quote(obj)
69
- end
70
-
71
94
  def assign_properties(values, options = {})
72
95
  Property.populate_object(self, values, options)
73
96
  return self
@@ -84,6 +107,14 @@ module EntityMixin
84
107
  end
85
108
  end
86
109
 
110
+ def og_quote(obj)
111
+ self.class.ogmanager.store.quote(obj)
112
+ end
113
+
114
+ def og_clone(*args)
115
+ Og::Entity.clone(self,*args)
116
+ end
117
+
87
118
  include RelationDSL
88
119
 
89
120
  class_inherit do
@@ -129,8 +160,25 @@ module EntityMixin
129
160
 
130
161
  # Find a specific instance of this class according
131
162
  # to the given conditions.
163
+ #
164
+ # Unlike the lower level store.find method it accepts
165
+ # Strings and Arrays instead of an option hash.
166
+ #
167
+ # === Examples
168
+ #
169
+ # User.find :condition => "name LIKE 'g%'", :order => 'name ASC'
170
+ # User.find :where => "name LIKE 'g%'", :order => 'name ASC'
171
+ # User.find :sql => "WHERE name LIKE 'g%' ORDER BY name ASC"
172
+ # User.find :condition => [ 'name LIKE ?', 'g%' ], :order => 'name ASC', :limit => 10
173
+ # User.find "name LIKE 'g%'"
174
+ # User.find "WHERE name LIKE 'g%' LIMIT 10"
175
+ # User.find [ 'name LIKE ?', 'g%' ]
132
176
 
133
- def find(options = {})
177
+ def find(options = {}, &block)
178
+ options = resolve_non_hash_options(options)
179
+
180
+ ez_resolve_options(options, &block) if block_given?
181
+
134
182
  if find_options = self.ann.self[:find_options]
135
183
  options = find_options.dup.update(options)
136
184
  end
@@ -144,17 +192,31 @@ module EntityMixin
144
192
 
145
193
  # Find a single instance of this class.
146
194
 
147
- def find_one(options = {})
195
+ def find_one(options = {}, &block)
196
+ options = resolve_non_hash_options(options)
197
+
198
+ ez_resolve_options(options, &block) if block_given?
199
+
200
+ if find_options = self.ann.self[:find_options]
201
+ options = find_options.dup.update(options)
202
+ end
203
+
148
204
  options[:class] = self
205
+ options[:type] = self if self.schema_inheritance_child?
206
+
149
207
  ogmanager.store.find_one(options)
150
208
  end
151
209
  alias_method :one, :find_one
152
210
  alias_method :first, :find_one
153
211
 
212
+ # Select an object using an sql query.
213
+
154
214
  def select(sql)
155
215
  ogmanager.store.select(sql, self)
156
216
  end
157
217
 
218
+ # Select one instance using an sqll query.
219
+
158
220
  def select_one(sql)
159
221
  ogmanager.store.select_one(sql, self)
160
222
  end
@@ -375,6 +437,60 @@ module EntityMixin
375
437
 
376
438
  private
377
439
 
440
+ # Resolve String/Array options.
441
+ #--
442
+ # FIXME: move to sql store?
443
+ #++
444
+
445
+ def resolve_non_hash_options(options)
446
+ if options.is_a? String
447
+ if options =~ /^WHERE/i
448
+ # pass the string as sql.
449
+ return { :sql => options }
450
+ else
451
+ # pass the string as a condition.
452
+ return { :condition => options }
453
+ end
454
+ elsif options.is_a? Array
455
+ # pass the array as condition (prepared statement style
456
+ # parsing/quoting.
457
+ return { :condition => options }
458
+ end
459
+
460
+ return options
461
+ end
462
+
463
+ # Resolve ez options, ie options provided using the
464
+ # Ruby query language.
465
+ #--
466
+ # gmosx: investigate this.
467
+ #++
468
+
469
+ def ez_resolve_options(options, &block)
470
+ klass = self.name.downcase.to_sym
471
+ # conditions on self first
472
+ # conditions = [ez_condition(:outer => outer_mapping[klass], :inner => inner_mapping[klass])]
473
+ conditions = [ez_condition()]
474
+
475
+ # options[:include].uniq.each do |assoc|
476
+ # conditions << reflect_on_association(assoc).klass.ez_condition(:outer => outer_mapping[assoc], :inner => inner_mapping[assoc])
477
+ # end
478
+
479
+ yield *conditions
480
+
481
+ condition = Caboose::EZ::Condition.new
482
+ conditions.each { |c| condition << c }
483
+ options[:condition] = condition.to_sql
484
+ # p options[:conditions] if $DEBUG
485
+ end
486
+
487
+ # Returns a Condition for this object.
488
+
489
+ def ez_condition(options = {}, &block)
490
+ options[:table_name] ||= table()
491
+ Caboose::EZ::Condition.new(options, &block)
492
+ end
493
+
378
494
  # Helper method for dynamic finders. Finds the object dynamically parsed
379
495
  # method name is after.
380
496
 
@@ -386,7 +502,11 @@ private
386
502
  options = args.pop if args.last.is_a?(Hash)
387
503
 
388
504
  condition = attrs.zip(args).map do |name, value|
389
- %|#{name} #{options.delete("#{name}_op".to_sym) || '='} #{ogmanager.store.quote(value)}|
505
+ field_name = properties[name.to_sym][:field] ||
506
+ properties[name.to_sym][:name] ||
507
+ properties[name.to_sym][:symbol]
508
+
509
+ %|#{field_name} #{options.delete("#{name}_op".to_sym) || '='} #{ogmanager.store.quote(value)}|
390
510
  end.join(' AND ')
391
511
 
392
512
  options.update(
@@ -448,6 +568,102 @@ class Entity
448
568
  res
449
569
  end
450
570
 
571
+ # Entity copying support. Eventually this should all
572
+ # be eval'd in at enchanting stage for the minor
573
+ # speed increase.
574
+ # TODO: Convert to enchantments on objects
575
+
576
+ # Accepts source object, destination and ignore.
577
+ # Source and destination are self explanatory; ignore
578
+ # is a list of properties not to copy (i.e.
579
+ # :create_time,:update_time).
580
+ # By default sets the class variables directly on the
581
+ # remote model instance, if you set use_setter_method to
582
+ # true, uses create_time= style copying tactics,
583
+
584
+ def copy_properties(source, destination, ignore = [], use_setter_method = false)
585
+ property_copier(source, destination, ignore, use_setter_method, false)
586
+ end
587
+
588
+ # Copies relations of one record to another. Only copies
589
+ # has_one, refers_to, belongs_to relationships as
590
+ # has_many requires modifying of other objects and
591
+ # cannot be copied (by design). If you think you need to copy
592
+ # these relations, what you need is a joins_many relationship
593
+ # which can be copied.
594
+
595
+ def copy_inferior_relations(source, destination, ignore = [])
596
+ real_ignore = Array.new
597
+
598
+ # Map relation symbols to foreign keys.
599
+
600
+ ignore.each do |symbol|
601
+ source.class.relations.reject{|r| [Og::JoinsMany, Og::ManyToMany, Og::HasMany].include?(r.class)}.each do |relation|
602
+ if relation.name == symbol.to_s
603
+ real_ignore << relation.foreign_key.to_sym
604
+ break
605
+ end
606
+ end
607
+ end
608
+
609
+ # Use instance variable property copier method.
610
+
611
+ property_copier(source, destination, real_ignore, false, true)
612
+ end
613
+
614
+ def copy_equal_relations(source, destination, ignore = [])
615
+ source.class.relations.reject{|r| not [Og::JoinsMany, Og::ManyToMany].include?(r.class)}.each do |relation|
616
+ next if relation.name == nil or ignore.include?(relation.name)
617
+ source.send(relation.name).each do |related|
618
+ destination.send(relation.name).send(:<<, related)
619
+ end
620
+ end
621
+ end
622
+
623
+ # Copies all relations *except* HasMany which is impossible
624
+ # to copy. Use a JoinsMany relation instead if you need a
625
+ # copyable HasMany (which is irrational).
626
+
627
+ def copy_relations(source, destination, ignore = [])
628
+ copy_inferior_relations(source, destination, ignore)
629
+ copy_equal_relations(source, destination, ignore)
630
+ end
631
+
632
+ # Clones an object in every possible way (cannot copy
633
+ # HasMany but can copy all others - BelongsTo, etc).
634
+ # Provide a source object as first arguments, the rest
635
+ # (if any) are passed along to the initialize constructor
636
+ # when calling new to make the copied object.
637
+
638
+ def clone(source,*args)
639
+ destination = source.class.new(*args)
640
+ copy_properties(source, destination, [], false)
641
+ # Must save here to copy join tables.
642
+ destination.save!
643
+ copy_relations(source, destination, [])
644
+ destination.save!
645
+ destination
646
+ end
647
+
648
+ # Does the work of clone_properties and copy_inferior_relations.
649
+ # Syntax is the same with one extra field to tell the
650
+ # routine what it is copying.
651
+
652
+ def property_copier(source,destination,ignore,use_setter_method,relations)
653
+ primary_key_symbol = source.class.primary_key.symbol
654
+ source.class.properties.to_a.each do |symbol, property|
655
+ next if primary_key_symbol == symbol or ignore.include?(symbol) or
656
+ (relations and not property.relation) or (not relations and property.relation)
657
+
658
+ variable = "@#{symbol}"
659
+ if use_setter_method
660
+ destination.send("#{symbol}=".to_sym,source.instance_variable_get(variable))
661
+ else
662
+ destination.instance_variable_set(variable, source.instance_variable_get(variable))
663
+ end
664
+ end
665
+ end
666
+
451
667
  end
452
668
 
453
669
  end
@@ -456,3 +672,4 @@ end
456
672
 
457
673
  # * George Moschovitis <gm@navel.gr>
458
674
  # * Tom Sawyer <transfire@gmail.com>
675
+ # * Rob Pitt <rob@motionpath.com>
@@ -1,7 +1,7 @@
1
1
  require 'fileutils'
2
2
 
3
- require 'nano/kernel/constant'
4
- require 'nano/kernel/assign_with'
3
+ require 'facet/kernel/constant'
4
+ require 'facet/kernel/assign_with'
5
5
 
6
6
  # $DBG = true
7
7
 
@@ -0,0 +1,147 @@
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 AbstractClause
12
+
13
+ attr_reader :test
14
+ attr_accessor :outer
15
+
16
+ def to_sql; 'TRUE'; end
17
+
18
+ end
19
+
20
+ class Clause < AbstractClause
21
+ # need this so that id doesn't call Object#id
22
+ # left it open to add more methods that
23
+ # conflict when I find them
24
+ [:id].each { |m| undef_method m }
25
+
26
+ attr_reader :name, :test, :value
27
+
28
+ # Initialize a Clause object with the name of the
29
+ # column.
30
+ def initialize(*args)
31
+ @table_prefix = ''
32
+ @negate = false
33
+ case args.length
34
+ when 0:
35
+ raise 'Expected at least one parameter'
36
+ when 1:
37
+ @name = args.first.to_s
38
+ when 2:
39
+ @table_prefix = args[0].to_s + '.' unless args[0].to_s.empty?
40
+ @name = args[1].to_s
41
+ when 3:
42
+ @table_prefix = args[0].to_s + '.' unless args[0].to_s.empty?
43
+ @name = args[1].to_s
44
+ @negate = args[2]
45
+ end
46
+ # append ! to negate the statement
47
+ if @name[-1] == '!'
48
+ @negate = true
49
+ @name = @name.slice(0, @name.length - 1)
50
+ end
51
+ # prefix with esc_ to avoid clashes with standard methods like 'alias'
52
+ @name = @name.slice(4, @name.length) if @name =~ /^esc_.*/
53
+ end
54
+
55
+ # The == operator has been over-ridden here to
56
+ # stand in for an exact match ["foo = ?", "bar"]
57
+ def ==(other)
58
+ @test = :equals
59
+ @value = other
60
+ end
61
+
62
+ # The =~ operator has been over-ridden here to
63
+ # stand in for the sql LIKE "%foobar%" clause.
64
+ def =~(pattern)
65
+ @test = :like
66
+ @value = pattern
67
+ end
68
+
69
+ # The spaceship <=> operator has been over-ridden here to
70
+ # stand in for the sql ["BETWEEN ? AND ?", 1, 5] "%foobar%" clause.
71
+ def <=>(range)
72
+ @test = :between
73
+ @value = range
74
+ end
75
+
76
+ # The === operator has been over-ridden here to
77
+ # stand in for the sql ["IN (?)", [1,2,3]] clause.
78
+ def ===(range)
79
+ @test = :in
80
+ @value = range
81
+ end
82
+
83
+ # switch on @test and build appropriate clause to
84
+ # match the operation.
85
+ def to_sql
86
+ case @test
87
+ when :equals
88
+ if @value == :null
89
+ @negate ? ["#{@table_prefix}#{@name} IS NOT NULL"] : ["#{@table_prefix}#{@name} IS NULL"]
90
+ else
91
+ @negate ? ["#{@table_prefix}#{@name} != ?", @value] : ["#{@table_prefix}#{@name} = ?", @value]
92
+ end
93
+ when :like
94
+ @negate ? ["#{@table_prefix}#{@name} NOT LIKE ?", @value] : ["#{@table_prefix}#{@name} LIKE ?", @value]
95
+ when :between
96
+ @negate ? ["#{@table_prefix}#{@name} NOT BETWEEN ? AND ?", @value.begin, @value.end] : ["#{@table_prefix}#{@name} BETWEEN ? AND ?", @value.begin, @value.end]
97
+ when :in
98
+ @negate ? ["#{@table_prefix}#{@name} NOT IN (?)", @value.to_a] : ["#{@table_prefix}#{@name} IN (?)", @value.to_a]
99
+ else
100
+ ["#{@table_prefix}#{@name} #{@test} ?", @value]
101
+ end
102
+ end
103
+
104
+ # This method_missing takes care of setting
105
+ # @test to any operator thats not covered
106
+ # above. And @value to the value
107
+ def method_missing(name, *args)
108
+ @test = name
109
+ @value = args.first
110
+ end
111
+ end
112
+
113
+ class ArrayClause < AbstractClause
114
+
115
+ # wraps around an Array in ActiveRecord format ['column = ?', 2]
116
+
117
+ def initialize(cond_array)
118
+ @test = :array
119
+ @cond_array = cond_array
120
+ end
121
+
122
+ def to_sql
123
+ return nil if @cond_array.first.to_s.empty?
124
+ query = (@cond_array.first =~ /^\([^\(\)]+\)$/) ? "#{@cond_array.first}" : "(#{@cond_array.first})"
125
+ [query, @cond_array[1..@cond_array.length] ]
126
+ end
127
+
128
+ end
129
+
130
+ class SqlClause < AbstractClause
131
+
132
+ # wraps around a raw SQL string
133
+
134
+ def initialize(sql)
135
+ @test = :sql
136
+ @sql = sql
137
+ end
138
+
139
+ def to_sql
140
+ [@sql]
141
+ end
142
+
143
+ end
144
+
145
+ end # EZ
146
+
147
+ end # Caboose