datamapper 0.2.0 → 0.2.1

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