datamapper 0.2.0 → 0.2.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.
Files changed (72) hide show
  1. data/CHANGELOG +20 -1
  2. data/environment.rb +5 -3
  3. data/lib/data_mapper.rb +42 -23
  4. data/lib/data_mapper/adapters/data_object_adapter.rb +76 -73
  5. data/lib/data_mapper/adapters/mysql_adapter.rb +15 -1
  6. data/lib/data_mapper/adapters/postgresql_adapter.rb +1 -1
  7. data/lib/data_mapper/adapters/sql/commands/load_command.rb +242 -28
  8. data/lib/data_mapper/adapters/sql/mappings/column.rb +18 -1
  9. data/lib/data_mapper/adapters/sql/mappings/table.rb +20 -32
  10. data/lib/data_mapper/adapters/sql/quoting.rb +40 -7
  11. data/lib/data_mapper/adapters/sqlite3_adapter.rb +7 -1
  12. data/lib/data_mapper/associations/has_and_belongs_to_many_association.rb +6 -1
  13. data/lib/data_mapper/associations/has_many_association.rb +1 -1
  14. data/lib/data_mapper/context.rb +4 -2
  15. data/lib/data_mapper/database.rb +2 -12
  16. data/lib/data_mapper/support/active_record_impersonation.rb +1 -1
  17. data/lib/data_mapper/support/blank.rb +2 -2
  18. data/lib/data_mapper/support/serialization.rb +60 -4
  19. data/lib/data_mapper/support/string.rb +24 -2
  20. data/lib/data_mapper/validations/validation_errors.rb +6 -3
  21. data/performance.rb +20 -5
  22. data/plugins/dataobjects/Rakefile +2 -0
  23. data/plugins/dataobjects/do.rb +18 -8
  24. data/plugins/dataobjects/do_mysql.rb +57 -24
  25. data/plugins/dataobjects/do_postgres.rb +3 -7
  26. data/plugins/dataobjects/do_sqlite3.rb +18 -17
  27. data/plugins/dataobjects/swig_mysql/Makefile +146 -0
  28. data/plugins/dataobjects/swig_mysql/extconf.rb +13 -1
  29. data/plugins/dataobjects/swig_mysql/mkmf.log +24 -0
  30. data/plugins/dataobjects/swig_mysql/mysql_c.bundle +0 -0
  31. data/plugins/dataobjects/swig_mysql/mysql_c.c +303 -2501
  32. data/plugins/dataobjects/swig_mysql/mysql_c.i +63 -4
  33. data/plugins/dataobjects/swig_mysql/mysql_c.o +0 -0
  34. data/profile_data_mapper.rb +4 -4
  35. data/rakefile.rb +13 -7
  36. data/spec/acts_as_tree_spec.rb +2 -0
  37. data/spec/associations_spec.rb +12 -0
  38. data/spec/attributes_spec.rb +2 -0
  39. data/spec/base_spec.rb +2 -0
  40. data/spec/callbacks_spec.rb +2 -0
  41. data/spec/can_has_sphinx.rb +0 -1
  42. data/spec/coersion_spec.rb +10 -3
  43. data/spec/column_spec.rb +23 -0
  44. data/spec/conditions_spec.rb +18 -18
  45. data/spec/count_command_spec.rb +2 -0
  46. data/spec/dataobjects_spec.rb +26 -0
  47. data/spec/delete_command_spec.rb +2 -0
  48. data/spec/embedded_value_spec.rb +2 -0
  49. data/spec/fixtures/people.yaml +1 -1
  50. data/spec/fixtures/posts.yaml +3 -0
  51. data/spec/legacy_spec.rb +2 -0
  52. data/spec/load_command_spec.rb +28 -2
  53. data/spec/magic_columns_spec.rb +2 -0
  54. data/spec/models/person.rb +1 -1
  55. data/spec/models/post.rb +8 -0
  56. data/spec/query_spec.rb +2 -0
  57. data/spec/save_command_spec.rb +2 -0
  58. data/spec/schema_spec.rb +2 -0
  59. data/spec/serialization_spec.rb +58 -0
  60. data/spec/single_table_inheritance_spec.rb +2 -0
  61. data/spec/symbolic_operators_spec.rb +2 -0
  62. data/spec/validates_confirmation_of_spec.rb +2 -0
  63. data/spec/validates_format_of_spec.rb +2 -0
  64. data/spec/validates_length_of_spec.rb +2 -0
  65. data/spec/validates_uniqueness_of_spec.rb +2 -0
  66. data/spec/validations_spec.rb +2 -0
  67. data/tasks/fixtures.rb +15 -10
  68. metadata +10 -13
  69. data/lib/data_mapper/adapters/sql/commands/conditions.rb +0 -130
  70. data/lib/data_mapper/adapters/sql/commands/loader.rb +0 -99
  71. data/plugins/dataobjects/swig_mysql/do_mysql.bundle +0 -0
  72. data/spec/conversions_to_yaml_spec.rb +0 -17
data/CHANGELOG CHANGED
@@ -94,4 +94,23 @@
94
94
  * Removed TableExistsCommand
95
95
  * Session renamed to Context
96
96
  * Most command implementations moved to methods in SqlAdapter
97
- * Removed UnitOfWork module, instead moving a slightly refactored implementation into Base
97
+ * Removed UnitOfWork module, instead moving a slightly refactored implementation into Base
98
+
99
+ -- 0.2.1
100
+ * Added :float column support
101
+ * Added association proxies: ie: Zoo.first.exhibits.animals
102
+ * Columns stored in SortedSet
103
+ * Swig files are no longer RDOCed
104
+ * Added :date column support
105
+ * BUG: Fixed UTC issues with datetimes
106
+ * Added #to_yaml method
107
+ * Added #to_xml method
108
+ * Added #to_json method
109
+ * BUG: Fixed HasManyAssociation::Set#inspect
110
+ * BUG: Fixed #reload!
111
+ * BUG: Column copy for STI moved into Table#initialize to better handle STI with multiple mapped databases
112
+ * BUG: before_create callbacks moved in the execution flow since they weren't guaranteed to fire before
113
+ * Threading enhancements: Removed single_threaded_mode, #database block form adjusted for thread-safety
114
+ * BUG: Fixed String#blank? when a multi-line string contained a blank line (thanks pimpmaster!)
115
+ * Performance enhancements: (thanks wycats!)
116
+
@@ -1,3 +1,6 @@
1
+ MERB_ROOT = Dir::pwd
2
+ MERB_ENV = 'development'
3
+
1
4
  # Require the DataMapper, and a Mock Adapter.
2
5
  require File.dirname(__FILE__) + '/lib/data_mapper'
3
6
  require File.dirname(__FILE__) + '/spec/mock_adapter'
@@ -6,8 +9,7 @@ adapter = ENV['ADAPTER'] || 'sqlite3'
6
9
 
7
10
  configuration_options = {
8
11
  :adapter => adapter,
9
- :database => (ENV['DATABASE'] || 'data_mapper_1').dup,
10
- :single_threaded => true
12
+ :database => (ENV['DATABASE'] || 'data_mapper_1').dup
11
13
  }
12
14
 
13
15
  # Prepare the log path, and remove the existing spec.log
@@ -42,4 +44,4 @@ DataMapper::Database.setup(:mock, :adapter => MockAdapter)
42
44
  [:default, :mock].each { |name| database(name) { load_models.call } }
43
45
 
44
46
  # Reset the test database.
45
- DataMapper::Base.auto_migrate!
47
+ DataMapper::Base.auto_migrate! unless ENV['AUTO_MIGRATE'] == 'false'
@@ -9,10 +9,11 @@
9
9
 
10
10
  # This line just let's us require anything in the +lib+ sub-folder
11
11
  # without specifying a full path.
12
+ unless defined?(DM_PLUGINS_ROOT)
13
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
12
14
 
13
- $LOAD_PATH.unshift(File.dirname(__FILE__))
14
-
15
- DM_PLUGINS_ROOT = (File.dirname(__FILE__) + '/../plugins')
15
+ DM_PLUGINS_ROOT = (File.dirname(__FILE__) + '/../plugins')
16
+ end
16
17
 
17
18
  # Require the basics...
18
19
  require 'yaml'
@@ -29,27 +30,45 @@ require 'data_mapper/base'
29
30
  # This block of code is for compatibility with Ruby On Rails' or Merb's database.yml
30
31
  # file, allowing you to simply require the data_mapper.rb in your
31
32
  # Rails application's environment.rb to configure the DataMapper.
32
-
33
- application_root, application_environment = *if defined?(MERB_ROOT)
34
- [MERB_ROOT, MERB_ENV]
35
- elsif defined?(RAILS_ROOT)
36
- [RAILS_ROOT, RAILS_ENV]
37
- end
38
-
39
- DM_APP_ROOT = application_root || Dir::pwd
40
-
41
- if application_root && File.exists?(application_root + '/config/database.yml')
33
+ unless defined?(DM_APP_ROOT)
34
+ application_root, application_environment = *if defined?(MERB_ROOT)
35
+ [MERB_ROOT, MERB_ENV]
36
+ elsif defined?(RAILS_ROOT)
37
+ [RAILS_ROOT, RAILS_ENV]
38
+ end
42
39
 
43
- database_configurations = YAML::load_file(application_root + '/config/database.yml')
44
- current_database_config = database_configurations[application_environment] || database_configurations[application_environment.to_sym]
40
+ DM_APP_ROOT = application_root || Dir::pwd
45
41
 
46
- default_database_config = {
47
- :adapter => current_database_config['adapter'] || current_database_config[:adapter],
48
- :host => current_database_config['host'] || current_database_config[:host],
49
- :database => current_database_config['database'] || current_database_config[:database],
50
- :username => current_database_config['username'] || current_database_config[:username],
51
- :password => current_database_config['password'] || current_database_config[:password]
52
- }
42
+ if application_root && File.exists?(application_root + '/config/database.yml')
43
+
44
+ database_configurations = YAML::load_file(application_root + '/config/database.yml')
45
+ current_database_config = database_configurations[application_environment] || database_configurations[application_environment.to_sym]
46
+
47
+ config = lambda { |key| current_database_config[key.to_s] || current_database_config[key] }
48
+
49
+ default_database_config = {
50
+ :adapter => config[:adapter],
51
+ :host => config[:host],
52
+ :database => config[:database],
53
+ :username => config[:username],
54
+ :password => config[:password]
55
+ }
53
56
 
54
- DataMapper::Database.setup(default_database_config)
57
+ DataMapper::Database.setup(default_database_config)
58
+
59
+ elsif application_root && FileTest.directory?(application_root + '/config')
60
+
61
+ %w(development testing production).map do |environment|
62
+ <<-EOS.margin
63
+ #{environment}:
64
+ adapter: mysql
65
+ username: root
66
+ password:
67
+ host: localhost
68
+ database: #{File.dirname(DM_APP_ROOT).split('/').last}_#{environment}
69
+ EOS
70
+ end
71
+
72
+ #File::open(application_root + '/config/database.yml')
73
+ end
55
74
  end
@@ -49,10 +49,7 @@ module DataMapper
49
49
 
50
50
  def initialize(configuration)
51
51
  super
52
-
53
- unless @configuration.single_threaded?
54
- @connection_pool = Support::ConnectionPool.new { create_connection }
55
- end
52
+ @connection_pool = Support::ConnectionPool.new(4) { create_connection }
56
53
  end
57
54
 
58
55
  def create_connection
@@ -64,11 +61,7 @@ module DataMapper
64
61
  def connection
65
62
  begin
66
63
  # Yield the appropriate connection
67
- if @configuration.single_threaded?
68
- yield(@active_connection || @active_connection = create_connection)
69
- else
70
- @connection_pool.hold { |active_connection| yield(active_connection) }
71
- end
64
+ @connection_pool.hold { |active_connection| yield(active_connection) }
72
65
  rescue => execution_error
73
66
  # Log error on failure
74
67
  @configuration.log.error(execution_error)
@@ -76,31 +69,28 @@ module DataMapper
76
69
  # Close all open connections, assuming that if one
77
70
  # had an error, it's likely due to a lost connection,
78
71
  # in which case all connections are likely broken.
79
- begin
80
- if @configuration.single_threaded?
81
- @active_connection.close
82
- else
83
- @connection_pool.available_connections.each do |active_connection|
84
- active_connection.close
85
- end
86
- end
87
- rescue => close_connection_error
88
- # An error on closing the connection is almost expected
89
- # if the socket is broken.
90
- @configuration.log.warn(close_connection_error)
91
- end
92
-
93
- # Reopen fresh connections.
94
- if @configuration.single_threaded?
95
- @active_connection = create_connection
96
- else
97
- @connection_pool.available_connections.clear
98
- end
72
+ flush_connections!
99
73
 
100
74
  raise execution_error
101
75
  end
102
76
  end
77
+
78
+ # Close any open connections.
79
+ def flush_connections!
80
+ begin
81
+ @connection_pool.available_connections.each do |active_connection|
82
+ active_connection.close
83
+ end
84
+ rescue => close_connection_error
85
+ # An error on closing the connection is almost expected
86
+ # if the socket is broken.
87
+ @configuration.log.warn(close_connection_error)
88
+ end
103
89
 
90
+ # Reopen fresh connections.
91
+ @connection_pool.available_connections.clear
92
+ end
93
+
104
94
  def transaction(&block)
105
95
  raise NotImplementedError.new
106
96
  end
@@ -109,43 +99,45 @@ module DataMapper
109
99
  connection do |db|
110
100
  sql = escape_sql(*args)
111
101
  log.debug { sql }
112
- result = nil
113
-
102
+
114
103
  command = db.create_command(sql)
115
-
104
+
116
105
  if block_given?
117
- reader = command.execute_reader
118
- result = yield(reader)
119
- reader.close
106
+ command.execute_reader { |reader| yield(reader) }
120
107
  else
121
- result = command.execute_non_query
108
+ command.execute_non_query
122
109
  end
123
-
124
- result
125
110
  end
126
111
  rescue => e
127
112
  handle_error(e)
128
113
  end
129
114
 
130
- def query(*args)
131
- execute(*args) do |reader|
132
- fields = reader.fields.map { |field| Inflector.underscore(field).to_sym }
133
-
134
- results = []
135
-
136
- if fields.size > 1
137
- struct = Struct.new(*fields)
115
+ def query(*args)
116
+ connection do |db|
117
+ sql = escape_sql(*args)
118
+ log.debug { sql }
119
+
120
+ command = db.create_command(sql)
121
+
122
+ command.execute_reader do |reader|
123
+ fields = reader.fields.map { |field| Inflector.underscore(field).to_sym }
138
124
 
139
- reader.each do
140
- results << struct.new(*reader.current_row)
141
- end
142
- else
143
- reader.each do
144
- results << reader.item(0)
125
+ results = []
126
+
127
+ if fields.size > 1
128
+ struct = Struct.new(*fields)
129
+
130
+ reader.each do
131
+ results << struct.new(*reader.current_row)
132
+ end
133
+ else
134
+ reader.each do
135
+ results << reader.item(0)
136
+ end
145
137
  end
138
+
139
+ results
146
140
  end
147
-
148
- results
149
141
  end
150
142
  end
151
143
 
@@ -174,7 +166,13 @@ module DataMapper
174
166
  end
175
167
 
176
168
  def create_table(name)
177
- execute(table(name).to_create_table_sql); true
169
+ table = self.table(name)
170
+
171
+ if table.exists?
172
+ false
173
+ else
174
+ execute(table.to_create_table_sql); true
175
+ end
178
176
  end
179
177
 
180
178
  def delete(session, instance)
@@ -202,46 +200,51 @@ module DataMapper
202
200
  when DataMapper::Base then
203
201
  return false unless instance.dirty? && instance.valid?
204
202
 
205
- callback(instance, :before_save)
206
-
207
- table = self.table(instance)
208
- attributes = instance.dirty_attributes
203
+ callback(instance, :before_save)
209
204
 
210
- unless attributes.empty?
211
- attributes[:type] = instance.class.name if table.multi_class?
212
-
213
- # INSERT
214
- result = if instance.new_record?
215
- callback(instance, :before_create)
205
+ # INSERT
206
+ result = if instance.new_record?
207
+ callback(instance, :before_create)
216
208
 
209
+ table = self.table(instance)
210
+ attributes = instance.dirty_attributes
211
+
212
+ unless attributes.empty?
213
+ attributes[:type] = instance.class.name if table.multi_class?
214
+
217
215
  keys = []
218
216
  values = []
219
217
  attributes.each_pair do |key, value|
220
218
  keys << table[key].to_sql
221
219
  values << value
222
220
  end
223
-
221
+
224
222
  # Formatting is a bit off here, but it looks nicer in the log this way.
225
223
  insert_id = execute("INSERT INTO #{table.to_sql} (#{keys.join(', ')}) VALUES (#{values.map { |v| quote_value(v) }.join(', ')})").last_insert_row
226
224
  instance.instance_variable_set(:@new_record, false)
227
225
  instance.key = insert_id if table.key.serial? && !attributes.include?(table.key.name)
228
226
  session.identity_map.set(instance)
229
227
  callback(instance, :after_create)
230
- # UPDATE
231
- else
232
- callback(instance, :before_update)
228
+ end
229
+ # UPDATE
230
+ else
231
+ callback(instance, :before_update)
232
+
233
+ table = self.table(instance)
234
+ attributes = instance.dirty_attributes
233
235
 
236
+ unless attributes.empty?
234
237
  sql = "UPDATE " << table.to_sql << " SET "
235
-
238
+
236
239
  sql << attributes.map do |key, value|
237
240
  "#{table[key].to_sql} = #{quote_value(value)}"
238
241
  end.join(', ')
239
-
242
+
240
243
  sql << " WHERE #{table.key.to_sql} = " << quote_value(instance.key)
241
-
244
+
242
245
  execute(sql).to_i > 0 && callback(instance, :after_update)
243
246
  end
244
- end # unless attributes.empty?
247
+ end
245
248
 
246
249
  instance.attributes.each_pair do |name, value|
247
250
  instance.original_hashes[name] = value.hash
@@ -1,7 +1,7 @@
1
1
  require 'data_mapper/adapters/data_object_adapter'
2
2
  begin
3
3
  require 'do_mysql'
4
- rescue
4
+ rescue LoadError
5
5
  STDERR.puts <<-EOS
6
6
  You must install the DataObjects::Mysql driver.
7
7
  rake dm:install:mysql
@@ -24,6 +24,8 @@ module DataMapper
24
24
  builder['dbname', :database]
25
25
  builder['socket', :socket]
26
26
 
27
+ log.debug { connection_string.strip }
28
+
27
29
  conn = DataObject::Mysql::Connection.new(connection_string.strip)
28
30
  conn.open
29
31
  cmd = conn.create_command("SET NAMES UTF8")
@@ -31,6 +33,18 @@ module DataMapper
31
33
  return conn
32
34
  end
33
35
 
36
+ def quote_time(value)
37
+ "DATE('#{value.xmlschema}')"
38
+ end
39
+
40
+ def quote_datetime(value)
41
+ "DATE('#{value}')"
42
+ end
43
+
44
+ def quote_date(value)
45
+ "DATE('#{value.strftime("%Y-%m-%d")}')"
46
+ end
47
+
34
48
  module Mappings
35
49
 
36
50
  def to_create_table_sql
@@ -1,7 +1,7 @@
1
1
  require 'data_mapper/adapters/data_object_adapter'
2
2
  begin
3
3
  require 'do_postgres'
4
- rescue
4
+ rescue LoadError
5
5
  STDERR.puts <<-EOS
6
6
  You must install the DataObjects::PostgreSQL driver.
7
7
  rake dm:install:postgresql
@@ -1,26 +1,144 @@
1
- require 'data_mapper/adapters/sql/commands/conditions'
2
- require 'data_mapper/adapters/sql/commands/loader'
3
-
4
1
  module DataMapper
5
2
  module Adapters
6
3
  module Sql
7
4
  module Commands
8
5
 
9
6
  class LoadCommand
10
-
7
+
8
+ class Loader
9
+
10
+ def initialize(load_command, klass)
11
+ @load_command, @klass = load_command, klass
12
+ @columns = {}
13
+ @key = nil
14
+ @key_index = nil
15
+ @type_override_present = false
16
+ @type_override_index = nil
17
+ @type_override = nil
18
+ @session = load_command.session
19
+ @reload = load_command.reload?
20
+ @set = []
21
+ end
22
+
23
+ def add_column(column, index)
24
+ if column.key?
25
+ @key = column
26
+ @key_index = index
27
+ end
28
+
29
+ if column.type == :class
30
+ @type_override_present = true
31
+ @type_override_index = index
32
+ @type_override = column
33
+ end
34
+
35
+ @columns[index] = column
36
+
37
+ self
38
+ end
39
+
40
+ def materialize(values)
41
+
42
+ instance_id = @key.type_cast_value(values[@key_index])
43
+ instance = if @type_override_present
44
+ create_instance(instance_id, @type_override.type_cast_value(values[@type_override_index]))
45
+ else
46
+ create_instance(instance_id)
47
+ end
48
+
49
+ @klass.callbacks.execute(:before_materialize, instance)
50
+
51
+ original_hashes = instance.original_hashes
52
+
53
+ @columns.each_pair do |index, column|
54
+ # This may be a little confusing, but we're
55
+ # setting both the original-hash value, and the
56
+ # instance-variable through method chaining to avoid
57
+ # lots of extra short-lived local variables.
58
+ original_hashes[column.name] = instance.instance_variable_set(
59
+ column.instance_variable_name,
60
+ column.type_cast_value(values[index])
61
+ ).hash
62
+ end
63
+
64
+ instance.instance_variable_set(:@loaded_set, @set)
65
+ @set << instance
66
+
67
+ @klass.callbacks.execute(:after_materialize, instance)
68
+
69
+ return instance
70
+ end
71
+
72
+ def loaded_set
73
+ @set
74
+ end
75
+
76
+ private
77
+
78
+ def create_instance(instance_id, instance_type = @klass)
79
+ instance = @session.identity_map.get(@klass, instance_id)
80
+
81
+ if instance.nil? || @reload
82
+ instance = instance_type.new() if instance.nil?
83
+ instance.instance_variable_set(:@__key, instance_id)
84
+ instance.instance_variable_set(:@new_record, false)
85
+ @session.identity_map.set(instance)
86
+ elsif instance.new_record?
87
+ instance.instance_variable_set(:@__key, instance_id)
88
+ instance.instance_variable_set(:@new_record, false)
89
+ end
90
+
91
+ instance.session = @session
92
+
93
+ return instance
94
+ end
95
+
96
+ end
97
+
98
+ class ConditionsError < StandardError
99
+
100
+ attr_reader :inner_error
101
+
102
+ def initialize(clause, value, inner_error)
103
+ @clause, @value, @inner_error = clause, value, inner_error
104
+ end
105
+
106
+ def message
107
+ "Conditions (:clause => #{@clause.inspect}, :value => #{@value.inspect}) failed: #{@inner_error}"
108
+ end
109
+
110
+ def backtrace
111
+ @inner_error.backtrace
112
+ end
113
+
114
+ end
115
+
11
116
  attr_reader :conditions, :session, :options
12
117
 
13
118
  def initialize(adapter, session, primary_class, options = {})
14
119
  @adapter, @session, @primary_class = adapter, session, primary_class
15
120
 
16
- @options, conditions_hash = partition_options(options)
121
+ # BEGIN: Partion out the options hash into general options,
122
+ # and conditions.
123
+ standard_find_options = @adapter.class::FIND_OPTIONS
124
+ conditions_hash = {}
125
+ @options = {}
126
+
127
+ options.each do |key,value|
128
+ if standard_find_options.include?(key) && key != :conditions
129
+ @options[key] = value
130
+ else
131
+ conditions_hash[key] = value
132
+ end
133
+ end
134
+ # END
17
135
 
18
136
  @order = @options[:order]
19
137
  @limit = @options[:limit]
20
138
  @offset = @options[:offset]
21
139
  @reload = @options[:reload]
22
140
  @instance_id = conditions_hash[:id]
23
- @conditions = Conditions.new(@adapter, self, conditions_hash)
141
+ @conditions = parse_conditions(conditions_hash)
24
142
  @loaders = Hash.new { |h,k| h[k] = Loader.new(self, k) }
25
143
  end
26
144
 
@@ -75,7 +193,7 @@ module DataMapper
75
193
  unless reload? || @instance_id.blank? || @instance_id.is_a?(Array)
76
194
  # If the id is for only a single record, attempt to find it.
77
195
  if instance = @session.identity_map.get(@primary_class, @instance_id)
78
- return instance
196
+ return [instance]
79
197
  end
80
198
  end
81
199
 
@@ -92,11 +210,7 @@ module DataMapper
92
210
 
93
211
  results += @loaders[@primary_class].loaded_set
94
212
 
95
- if @limit == 1 || (@instance_id && !@instance_id.is_a?(Array))
96
- results.first
97
- else
98
- results
99
- end
213
+ return results
100
214
  end
101
215
 
102
216
  def load(reader)
@@ -120,6 +234,11 @@ module DataMapper
120
234
  end
121
235
  end
122
236
 
237
+ # Are any conditions present?
238
+ def conditions_empty?
239
+ @conditions.empty?
240
+ end
241
+
123
242
  # Generate a select statement based on the initialization
124
243
  # arguments.
125
244
  def to_parameterized_sql
@@ -136,10 +255,29 @@ module DataMapper
136
255
  sql << ' ' << association.to_shallow_sql
137
256
  end
138
257
 
139
- unless conditions.empty?
140
- where_clause, *parameters = conditions.to_parameterized_sql
141
- sql << ' WHERE ' << where_clause
142
- end
258
+ unless conditions_empty?
259
+ sql << ' WHERE ('
260
+
261
+ last_index = @conditions.size
262
+ current_index = 0
263
+
264
+ @conditions.each do |condition|
265
+ case condition
266
+ when String then sql << condition
267
+ when Array then
268
+ sql << condition.shift
269
+ parameters += condition
270
+ else
271
+ raise "Unable to parse condition: #{condition.inspect}" if condition
272
+ end
273
+
274
+ if (current_index += 1) == last_index
275
+ sql << ')'
276
+ else
277
+ sql << ') AND ('
278
+ end
279
+ end
280
+ end # unless conditions_empty?
143
281
 
144
282
  unless @order.nil?
145
283
  sql << ' ORDER BY ' << @order.to_s
@@ -156,9 +294,62 @@ module DataMapper
156
294
  parameters.unshift(sql)
157
295
  end
158
296
 
297
+ # If more than one table is involved in the query, the column definitions should
298
+ # be qualified by the table name. ie: people.name
299
+ # This method determines wether that needs to happen or not.
300
+ # Note: After the first call, the calculations are avoided by overwriting this
301
+ # method with a simple getter.
159
302
  def qualify_columns?
160
- return @qualify_columns unless @qualify_columns.nil?
161
303
  @qualify_columns = !(included_associations.empty? && shallow_included_associations.empty?)
304
+ def self.qualify_columns?
305
+ @qualify_columns
306
+ end
307
+ @qualify_columns
308
+ end
309
+
310
+ # expression_to_sql takes a set of arguments, and turns them into a an
311
+ # Array of generated SQL, followed by optional Values to interpolate as SQL-Parameters.
312
+ #
313
+ # Parameters:
314
+ # +clause+ The name of the column as a Symbol, a raw-SQL String, a Mappings::Column
315
+ # instance, or a Symbol::Operator.
316
+ # +value+ The Value for the condition.
317
+ # +collector+ An Array representing all conditions that is appended to by expression_to_sql
318
+ #
319
+ # Returns: Undefined Output. The work performed is added to the +collector+ argument.
320
+ # Example:
321
+ # conditions = []
322
+ # expression_to_sql(:name, 'Bob', conditions)
323
+ # => +undefined return value+
324
+ # conditions.inspect
325
+ # => ["name = ?", 'Bob']
326
+ def expression_to_sql(clause, value, collector)
327
+ qualify_columns = qualify_columns?
328
+
329
+ case clause
330
+ when Symbol::Operator then
331
+ operator = case clause.type
332
+ when :gt then '>'
333
+ when :gte then '>='
334
+ when :lt then '<'
335
+ when :lte then '<='
336
+ when :not then inequality_operator(value)
337
+ when :eql then equality_operator(value)
338
+ when :like then equality_operator(value, 'LIKE')
339
+ when :in then equality_operator(value)
340
+ else raise ArgumentError.new('Operator type not supported')
341
+ end
342
+ collector << ["#{primary_class_table[clause].to_sql(qualify_columns)} #{operator} ?", value]
343
+ when Symbol then
344
+ collector << ["#{primary_class_table[clause].to_sql(qualify_columns)} #{equality_operator(value)} ?", value]
345
+ when String then
346
+ collector << [clause, value]
347
+ when Mappings::Column then
348
+ collector << ["#{clause.to_sql(qualify_columns)} #{equality_operator(value)} ?", value]
349
+ else raise "CAN HAS CRASH? #{clause.inspect}"
350
+ end
351
+ rescue => e
352
+ raise ConditionsError.new(clause, value, e)
162
353
  end
163
354
 
164
355
  private
@@ -277,19 +468,42 @@ module DataMapper
277
468
  @primary_class_table || (@primary_class_table = @adapter.table(@primary_class))
278
469
  end
279
470
 
280
- def partition_options(options)
281
- find_options = @adapter.class::FIND_OPTIONS
282
- conditions_hash = {}
283
- options_hash = {}
284
- options.each do |key,value|
285
- if key != :conditions && find_options.include?(key)
286
- options_hash[key] = value
287
- else
288
- conditions_hash[key] = value
471
+ def parse_conditions(conditions_hash)
472
+ collection = []
473
+
474
+ case x = conditions_hash.delete(:conditions)
475
+ when Array then
476
+ clause = x.shift
477
+ expression_to_sql(clause, x, collection)
478
+ when Hash then
479
+ x.each_pair do |key,value|
480
+ expression_to_sql(key, value, collection)
289
481
  end
482
+ else
483
+ raise "Unable to parse conditions: #{x.inspect}" if x
484
+ end
485
+
486
+ conditions_hash.each_pair do |key,value|
487
+ expression_to_sql(key, value, collection)
488
+ end
489
+
490
+ collection
491
+ end
492
+
493
+ def equality_operator(value, default = '=')
494
+ case value
495
+ when NilClass then 'IS'
496
+ when Array then 'IN'
497
+ else default
498
+ end
499
+ end
500
+
501
+ def inequality_operator(value, default = '<>')
502
+ case value
503
+ when NilClass then 'IS NOT'
504
+ when Array then 'NOT IN'
505
+ else default
290
506
  end
291
-
292
- [ options_hash, conditions_hash ]
293
507
  end
294
508
 
295
509
  end # class LoadCommand