datamapper 0.2.0 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +20 -1
- data/environment.rb +5 -3
- data/lib/data_mapper.rb +42 -23
- data/lib/data_mapper/adapters/data_object_adapter.rb +76 -73
- data/lib/data_mapper/adapters/mysql_adapter.rb +15 -1
- data/lib/data_mapper/adapters/postgresql_adapter.rb +1 -1
- data/lib/data_mapper/adapters/sql/commands/load_command.rb +242 -28
- data/lib/data_mapper/adapters/sql/mappings/column.rb +18 -1
- data/lib/data_mapper/adapters/sql/mappings/table.rb +20 -32
- data/lib/data_mapper/adapters/sql/quoting.rb +40 -7
- data/lib/data_mapper/adapters/sqlite3_adapter.rb +7 -1
- data/lib/data_mapper/associations/has_and_belongs_to_many_association.rb +6 -1
- data/lib/data_mapper/associations/has_many_association.rb +1 -1
- data/lib/data_mapper/context.rb +4 -2
- data/lib/data_mapper/database.rb +2 -12
- data/lib/data_mapper/support/active_record_impersonation.rb +1 -1
- data/lib/data_mapper/support/blank.rb +2 -2
- data/lib/data_mapper/support/serialization.rb +60 -4
- data/lib/data_mapper/support/string.rb +24 -2
- data/lib/data_mapper/validations/validation_errors.rb +6 -3
- data/performance.rb +20 -5
- data/plugins/dataobjects/Rakefile +2 -0
- data/plugins/dataobjects/do.rb +18 -8
- data/plugins/dataobjects/do_mysql.rb +57 -24
- data/plugins/dataobjects/do_postgres.rb +3 -7
- data/plugins/dataobjects/do_sqlite3.rb +18 -17
- data/plugins/dataobjects/swig_mysql/Makefile +146 -0
- data/plugins/dataobjects/swig_mysql/extconf.rb +13 -1
- data/plugins/dataobjects/swig_mysql/mkmf.log +24 -0
- data/plugins/dataobjects/swig_mysql/mysql_c.bundle +0 -0
- data/plugins/dataobjects/swig_mysql/mysql_c.c +303 -2501
- data/plugins/dataobjects/swig_mysql/mysql_c.i +63 -4
- data/plugins/dataobjects/swig_mysql/mysql_c.o +0 -0
- data/profile_data_mapper.rb +4 -4
- data/rakefile.rb +13 -7
- data/spec/acts_as_tree_spec.rb +2 -0
- data/spec/associations_spec.rb +12 -0
- data/spec/attributes_spec.rb +2 -0
- data/spec/base_spec.rb +2 -0
- data/spec/callbacks_spec.rb +2 -0
- data/spec/can_has_sphinx.rb +0 -1
- data/spec/coersion_spec.rb +10 -3
- data/spec/column_spec.rb +23 -0
- data/spec/conditions_spec.rb +18 -18
- data/spec/count_command_spec.rb +2 -0
- data/spec/dataobjects_spec.rb +26 -0
- data/spec/delete_command_spec.rb +2 -0
- data/spec/embedded_value_spec.rb +2 -0
- data/spec/fixtures/people.yaml +1 -1
- data/spec/fixtures/posts.yaml +3 -0
- data/spec/legacy_spec.rb +2 -0
- data/spec/load_command_spec.rb +28 -2
- data/spec/magic_columns_spec.rb +2 -0
- data/spec/models/person.rb +1 -1
- data/spec/models/post.rb +8 -0
- data/spec/query_spec.rb +2 -0
- data/spec/save_command_spec.rb +2 -0
- data/spec/schema_spec.rb +2 -0
- data/spec/serialization_spec.rb +58 -0
- data/spec/single_table_inheritance_spec.rb +2 -0
- data/spec/symbolic_operators_spec.rb +2 -0
- data/spec/validates_confirmation_of_spec.rb +2 -0
- data/spec/validates_format_of_spec.rb +2 -0
- data/spec/validates_length_of_spec.rb +2 -0
- data/spec/validates_uniqueness_of_spec.rb +2 -0
- data/spec/validations_spec.rb +2 -0
- data/tasks/fixtures.rb +15 -10
- metadata +10 -13
- data/lib/data_mapper/adapters/sql/commands/conditions.rb +0 -130
- data/lib/data_mapper/adapters/sql/commands/loader.rb +0 -99
- data/plugins/dataobjects/swig_mysql/do_mysql.bundle +0 -0
- 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
|
+
|
data/environment.rb
CHANGED
@@ -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'
|
data/lib/data_mapper.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
35
|
-
elsif defined?(RAILS_ROOT)
|
36
|
-
|
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
|
-
|
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
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
113
|
-
|
102
|
+
|
114
103
|
command = db.create_command(sql)
|
115
|
-
|
104
|
+
|
116
105
|
if block_given?
|
117
|
-
reader
|
118
|
-
result = yield(reader)
|
119
|
-
reader.close
|
106
|
+
command.execute_reader { |reader| yield(reader) }
|
120
107
|
else
|
121
|
-
|
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
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
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
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
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
|
-
|
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
|
-
|
211
|
-
|
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
|
-
|
231
|
-
|
232
|
-
|
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
|
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,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
|
-
|
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 =
|
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
|
-
|
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
|
140
|
-
|
141
|
-
|
142
|
-
|
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
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
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
|