datamapper 0.2.2 → 0.2.3

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 (42) hide show
  1. data/CHANGELOG +10 -2
  2. data/environment.rb +0 -3
  3. data/lib/data_mapper.rb +2 -0
  4. data/lib/data_mapper/adapters/abstract_adapter.rb +2 -2
  5. data/lib/data_mapper/adapters/data_object_adapter.rb +63 -73
  6. data/lib/data_mapper/adapters/mysql_adapter.rb +2 -13
  7. data/lib/data_mapper/adapters/postgresql_adapter.rb +3 -2
  8. data/lib/data_mapper/adapters/sql/coersion.rb +29 -11
  9. data/lib/data_mapper/adapters/sql/commands/load_command.rb +13 -9
  10. data/lib/data_mapper/adapters/sql/mappings/table.rb +7 -3
  11. data/lib/data_mapper/adapters/sql/quoting.rb +0 -53
  12. data/lib/data_mapper/adapters/sqlite3_adapter.rb +2 -1
  13. data/lib/data_mapper/base.rb +25 -9
  14. data/lib/data_mapper/context.rb +2 -2
  15. data/lib/data_mapper/database.rb +6 -6
  16. data/lib/data_mapper/support/active_record_impersonation.rb +1 -1
  17. data/lib/data_mapper/support/serialization.rb +10 -2
  18. data/lib/data_mapper/support/silence.rb +10 -0
  19. data/lib/data_mapper/support/string.rb +21 -1
  20. data/lib/data_mapper/validations/confirmation_validator.rb +1 -3
  21. data/lib/data_mapper/validations/format_validator.rb +6 -6
  22. data/lib/data_mapper/validations/generic_validator.rb +0 -5
  23. data/lib/data_mapper/validations/length_validator.rb +14 -13
  24. data/lib/data_mapper/validations/required_field_validator.rb +2 -8
  25. data/lib/data_mapper/validations/unique_validator.rb +1 -7
  26. data/plugins/dataobjects/do.rb +94 -2
  27. data/plugins/dataobjects/do_mysql.rb +17 -17
  28. data/plugins/dataobjects/do_postgres.rb +34 -15
  29. data/plugins/dataobjects/do_sqlite3.rb +9 -6
  30. data/rakefile.rb +3 -2
  31. data/spec/base_spec.rb +28 -0
  32. data/spec/dataobjects_spec.rb +2 -1
  33. data/spec/load_command_spec.rb +1 -1
  34. data/spec/serialization_spec.rb +2 -2
  35. data/spec/spec_helper.rb +1 -1
  36. data/spec/support_spec.rb +7 -0
  37. data/spec/validations_spec.rb +13 -0
  38. metadata +14 -6
  39. data/plugins/dataobjects/swig_mysql/Makefile +0 -146
  40. data/plugins/dataobjects/swig_mysql/mysql_c.o +0 -0
  41. data/plugins/dataobjects/swig_postgres/Makefile +0 -146
  42. data/plugins/dataobjects/swig_sqlite/db +0 -0
@@ -32,6 +32,14 @@ module DataObject
32
32
  def initialize(connection_string)
33
33
  end
34
34
 
35
+ def logger
36
+ @logger || @logger = Logger.new(nil)
37
+ end
38
+
39
+ def logger=(value)
40
+ @logger = value
41
+ end
42
+
35
43
  def begin_transaction
36
44
  # TODO: Hook this up
37
45
  Transaction.new
@@ -50,6 +58,7 @@ module DataObject
50
58
  end
51
59
 
52
60
  def create_command(text)
61
+ logger.debug { text }
53
62
  Command.new(self, text)
54
63
  end
55
64
 
@@ -215,11 +224,11 @@ module DataObject
215
224
  @connection, @text = connection, text
216
225
  end
217
226
 
218
- def execute_non_query
227
+ def execute_non_query(*args)
219
228
  raise LostConnectionError, "the connection to the database has been lost" if @connection.closed?
220
229
  end
221
230
 
222
- def execute_reader
231
+ def execute_reader(*args)
223
232
  raise LostConnectionError, "the connection to the database has been lost" if @connection.closed?
224
233
  end
225
234
 
@@ -227,6 +236,89 @@ module DataObject
227
236
  raise NotImplementedError
228
237
  end
229
238
 
239
+ # Escape a string of SQL with a set of arguments.
240
+ # The first argument is assumed to be the SQL to escape,
241
+ # the remaining arguments (if any) are assumed to be
242
+ # values to escape and interpolate.
243
+ #
244
+ # ==== Examples
245
+ # escape_sql("SELECT * FROM zoos")
246
+ # # => "SELECT * FROM zoos"
247
+ #
248
+ # escape_sql("SELECT * FROM zoos WHERE name = ?", "Dallas")
249
+ # # => "SELECT * FROM zoos WHERE name = `Dallas`"
250
+ #
251
+ # escape_sql("SELECT * FROM zoos WHERE name = ? AND acreage > ?", "Dallas", 40)
252
+ # # => "SELECT * FROM zoos WHERE name = `Dallas` AND acreage > 40"
253
+ #
254
+ # ==== Warning
255
+ # This method is meant mostly for adapters that don't support
256
+ # bind-parameters.
257
+ def escape_sql(args)
258
+ sql = text.dup
259
+
260
+ unless args.empty?
261
+ sql.gsub!(/\?/) do |x|
262
+ quote_value(args.shift)
263
+ end
264
+ end
265
+
266
+ sql
267
+ end
268
+
269
+ def quote_value(value)
270
+ return 'NULL' if value.nil?
271
+
272
+ case value
273
+ when Numeric then quote_numeric(value)
274
+ when String then quote_string(value)
275
+ when Class then quote_class(value)
276
+ when Time then quote_time(value)
277
+ when DateTime then quote_datetime(value)
278
+ when Date then quote_date(value)
279
+ when TrueClass, FalseClass then quote_boolean(value)
280
+ when Array then quote_array(value)
281
+ else
282
+ if value.respond_to?(:to_sql)
283
+ value.to_sql
284
+ else
285
+ raise "Don't know how to quote #{value.inspect}"
286
+ end
287
+ end
288
+ end
289
+
290
+ def quote_numeric(value)
291
+ value.to_s
292
+ end
293
+
294
+ def quote_string(value)
295
+ "'#{value.gsub("'", "''")}'"
296
+ end
297
+
298
+ def quote_class(value)
299
+ "'#{value.name}'"
300
+ end
301
+
302
+ def quote_time(value)
303
+ "'#{value.xmlschema}'"
304
+ end
305
+
306
+ def quote_datetime(value)
307
+ "'#{value.dup}'"
308
+ end
309
+
310
+ def quote_date(value)
311
+ "'#{value.strftime("%Y-%m-%d")}'"
312
+ end
313
+
314
+ def quote_boolean(value)
315
+ value.to_s.upcase
316
+ end
317
+
318
+ def quote_array(value)
319
+ "(#{value.map { |entry| quote_value(entry) }.join(', ')})"
320
+ end
321
+
230
322
  end
231
323
 
232
324
  class NotImplementedError < StandardError
@@ -10,21 +10,9 @@ module DataObject
10
10
 
11
11
  class Connection < DataObject::Connection
12
12
 
13
- def self.conns
14
- @conns
15
- end
16
-
17
- def self.conns=(val)
18
- @conns = val
19
- end
20
-
21
- self.conns = 0
22
-
23
13
  attr_reader :db
24
14
 
25
- def initialize(connection_string)
26
- @num = (self.class.conns += 1)
27
-
15
+ def initialize(connection_string)
28
16
  @state = STATE_CLOSED
29
17
  @connection_string = connection_string
30
18
  opts = connection_string.split(" ")
@@ -60,6 +48,7 @@ module DataObject
60
48
  end
61
49
 
62
50
  def create_command(text)
51
+ logger.debug { text }
63
52
  Command.new(self, text)
64
53
  end
65
54
 
@@ -180,9 +169,9 @@ module DataObject
180
169
 
181
170
  class Command < DataObject::Command
182
171
 
183
- def execute_reader
172
+ def execute_reader(*args)
184
173
  super
185
- result = Mysql_c.mysql_query(@connection.db, @text)
174
+ result = Mysql_c.mysql_query(@connection.db, escape_sql(args))
186
175
  # TODO: Real Error
187
176
  raise QueryError, "Your query failed.\n#{Mysql_c.mysql_error(@connection.db)}\n#{@text}" unless result == 0
188
177
  reader = Reader.new(@connection.db, Mysql_c.mysql_use_result(@connection.db))
@@ -195,9 +184,9 @@ module DataObject
195
184
  end
196
185
  end
197
186
 
198
- def execute_non_query
187
+ def execute_non_query(*args)
199
188
  super
200
- result = Mysql_c.mysql_query(@connection.db, @text)
189
+ result = Mysql_c.mysql_query(@connection.db, escape_sql(args))
201
190
  raise QueryError, "Your query failed.\n#{Mysql_c.mysql_error(@connection.db)}\n#{@text}" unless result == 0
202
191
  reader = Mysql_c.mysql_store_result(@connection.db)
203
192
  raise QueryError, "You called execute_non_query on a query: #{@text}" if reader
@@ -206,6 +195,17 @@ module DataObject
206
195
  return ResultData.new(@connection, rows_affected, Mysql_c.mysql_insert_id(@connection.db))
207
196
  end
208
197
 
198
+ def quote_time(value)
199
+ "DATE('#{value.xmlschema}')"
200
+ end
201
+
202
+ def quote_datetime(value)
203
+ "DATE('#{value}')"
204
+ end
205
+
206
+ def quote_date(value)
207
+ "DATE('#{value.strftime("%Y-%m-%d")}')"
208
+ end
209
209
  end
210
210
 
211
211
  end
@@ -60,10 +60,8 @@ module DataObject
60
60
  end
61
61
  end
62
62
 
63
- def close
63
+ def real_close
64
64
  Postgres_c.PQclear(@reader)
65
- @state = STATE_CLOSED
66
- true
67
65
  end
68
66
 
69
67
  def data_type_name(col)
@@ -91,6 +89,15 @@ module DataObject
91
89
  typecast(val, @field_types[idx])
92
90
  end
93
91
 
92
+ def each
93
+ return unless has_rows?
94
+
95
+ while(true) do
96
+ yield
97
+ break unless self.next
98
+ end
99
+ end
100
+
94
101
  def next
95
102
  super
96
103
  if @cursor >= @rows - 1
@@ -117,11 +124,11 @@ module DataObject
117
124
  when "FLOAT4", "FLOAT8", "NUMERIC", "CASH"
118
125
  val.to_f
119
126
  when "TIMESTAMP", "TIMETZ", "TIMESTAMPTZ"
120
- DateTime.parse(val)
127
+ DateTime.parse(val) rescue nil
121
128
  when "TIME"
122
- DateTime.parse(val).to_time
129
+ DateTime.parse(val).to_time rescue nil
123
130
  when "DATE"
124
- Date.parse(val)
131
+ Date.parse(val) rescue nil
125
132
  else
126
133
  val
127
134
  end
@@ -146,25 +153,37 @@ module DataObject
146
153
 
147
154
  class Command < DataObject::Command
148
155
 
149
- def execute_reader
156
+ def execute_reader(*args)
150
157
  super
151
- reader = Postgres_c.PQexec(@connection.db, @text)
152
- unless [Postgres_c::PGRES_COMMAND_OK, Postgres_c::PGRES_TUPLES_OK].include?(Postgres_c.PQresultStatus(reader))
153
- raise QueryError, "Your query failed.\n#{Postgres_c.PQerrorMessage(@connection.db)}QUERY: \"#{@text}\""
158
+ sql = escape_sql(args)
159
+ @connection.logger.debug { sql }
160
+ ptr = Postgres_c.PQexec(@connection.db, sql)
161
+ unless [Postgres_c::PGRES_COMMAND_OK, Postgres_c::PGRES_TUPLES_OK].include?(Postgres_c.PQresultStatus(ptr))
162
+ raise QueryError, "Your query failed.\n#{Postgres_c.PQerrorMessage(@connection.db)}QUERY: \"#{sql}\""
163
+ else
164
+ reader = Reader.new(@connection.db, ptr)
165
+ if block_given?
166
+ return_value = yield(reader)
167
+ reader.close
168
+ return_value
169
+ else
170
+ reader
171
+ end
154
172
  end
155
- Reader.new(@connection.db, reader)
156
173
  end
157
174
 
158
- def execute_non_query
175
+ def execute_non_query(*args)
159
176
  super
160
- results = Postgres_c.PQexec(@connection.db, @text)
177
+ sql = escape_sql(args)
178
+ @connection.logger.debug { sql }
179
+ results = Postgres_c.PQexec(@connection.db, sql)
161
180
  status = Postgres_c.PQresultStatus(results)
162
181
  if status == Postgres_c::PGRES_TUPLES_OK
163
182
  Postgres_c.PQclear(results)
164
- raise QueryError, "Your query failed or you tried to execute a SELECT query through execute_non_reader\n#{Postgres_c.PQerrorMessage(@connection.db)}\nQUERY: \"#{@text}\""
183
+ raise QueryError, "Your query failed or you tried to execute a SELECT query through execute_non_reader\n#{Postgres_c.PQerrorMessage(@connection.db)}\nQUERY: \"#{sql}\""
165
184
  elsif status != Postgres_c::PGRES_COMMAND_OK
166
185
  Postgres_c.PQclear(results)
167
- raise QueryError, "Your query failed.\n#{Postgres_c.PQerrorMessage(@connection.db)}\nQUERY: \"#{@text}\""
186
+ raise QueryError, "Your query failed.\n#{Postgres_c.PQerrorMessage(@connection.db)}\nQUERY: \"#{sql}\""
168
187
  end
169
188
  rows_affected = Postgres_c.PQcmdTuples(results).to_i
170
189
  Postgres_c.PQclear(results)
@@ -35,6 +35,7 @@ module DataObject
35
35
  end
36
36
 
37
37
  def create_command(text)
38
+ logger.debug { text }
38
39
  Command.new(self, text)
39
40
  end
40
41
 
@@ -113,11 +114,12 @@ module DataObject
113
114
 
114
115
  class Command < DataObject::Command
115
116
 
116
- def execute_reader
117
+ def execute_reader(*args)
117
118
  super
118
- result, ptr = Sqlite3_c.sqlite3_prepare_v2(@connection.db, @text, @text.size + 1)
119
+ sql = escape_sql(args)
120
+ result, ptr = Sqlite3_c.sqlite3_prepare_v2(@connection.db, sql, sql.size + 1)
119
121
  unless result == Sqlite3_c::SQLITE_OK
120
- raise QueryError, "Your query failed.\n#{Sqlite3_c.sqlite3_errmsg(@connection.db)}\nQUERY: \"#{@text}\""
122
+ raise QueryError, "Your query failed.\n#{Sqlite3_c.sqlite3_errmsg(@connection.db)}\nQUERY: \"#{sql}\""
121
123
  else
122
124
  reader = Reader.new(@connection.db, ptr)
123
125
 
@@ -131,12 +133,13 @@ module DataObject
131
133
  end
132
134
  end
133
135
 
134
- def execute_non_query
136
+ def execute_non_query(*args)
135
137
  super
136
- result, reader = Sqlite3_c.sqlite3_prepare_v2(@connection.db, @text, -1)
138
+ sql = escape_sql(args)
139
+ result, reader = Sqlite3_c.sqlite3_prepare_v2(@connection.db, sql, -1)
137
140
  unless result == Sqlite3_c::SQLITE_OK
138
141
  Sqlite3_c.sqlite3_finalize(reader)
139
- raise QueryError, "Your query failed.\n#{Sqlite3_c.sqlite3_errmsg(@connection.db)}\nQUERY: \"#{@text}\""
142
+ raise QueryError, "Your query failed.\n#{Sqlite3_c.sqlite3_errmsg(@connection.db)}\nQUERY: \"#{sql}\""
140
143
  else
141
144
  exec_result = Sqlite3_c.sqlite3_step(reader)
142
145
  Sqlite3_c.sqlite3_finalize(reader)
data/rakefile.rb CHANGED
@@ -37,7 +37,7 @@ namespace :dm do
37
37
 
38
38
  end
39
39
 
40
- PACKAGE_VERSION = '0.2.2'
40
+ PACKAGE_VERSION = '0.2.3'
41
41
 
42
42
  PACKAGE_FILES = FileList[
43
43
  'README',
@@ -48,7 +48,7 @@ PACKAGE_FILES = FileList[
48
48
  'spec/**/*.{rb,yaml}',
49
49
  'tasks/**/*',
50
50
  'plugins/**/*'
51
- ].to_a.reject { |path| path =~ /\.(bundle|log)$/ }
51
+ ].to_a.reject { |path| path =~ /(\/db|Makefile|\.bundle|\.log|\.o)$/ }
52
52
 
53
53
  DOCUMENTED_FILES = PACKAGE_FILES.reject do |path|
54
54
  FileTest.directory?(path) || path =~ /(^spec|\/spec|\/swig\_)/
@@ -86,6 +86,7 @@ gem_spec = Gem::Specification.new do |s|
86
86
  s.requirements << 'none'
87
87
  s.autorequire = 'data_mapper'
88
88
  s.add_dependency('fastthread')
89
+ s.add_dependency('rspec')
89
90
 
90
91
  s.has_rdoc = true
91
92
  s.rdoc_options << '--line-numbers' << '--inline-source' << '--main' << 'README'
data/spec/base_spec.rb CHANGED
@@ -18,6 +18,34 @@ describe DataMapper::Base do
18
18
  a.test.should == 'testing!'
19
19
  end
20
20
 
21
+ it "should be dirty" do
22
+ x = Person.create(:name => 'a')
23
+ x.should_not be_dirty
24
+ x.name = 'dslfay'
25
+ x.should be_dirty
26
+ end
27
+
28
+ it "should return a diff" do
29
+ x = Person.new(:name => 'Sam', :age => 30, :occupation => 'Programmer')
30
+ y = Person.new(:name => 'Amy', :age => 21, :occupation => 'Programmer')
31
+
32
+ diff = (x ^ y)
33
+ diff.should include(:name)
34
+ diff.should include(:age)
35
+ diff[:name].should eql(['Sam', 'Amy'])
36
+ diff[:age].should eql([30, 21])
37
+
38
+ x.destroy!
39
+ y.destroy!
40
+ end
41
+
42
+ it "should update attributes" do
43
+ x = Person.create(:name => 'Sam')
44
+ x.update_attributes(:age => 30).should eql(true)
45
+ x.age.should eql(30)
46
+ x.should_not be_dirty
47
+ end
48
+
21
49
  end
22
50
 
23
51
  describe 'A new record' do
@@ -15,7 +15,8 @@ describe DataObject do
15
15
  # SELECT * FROM tbl WHERE auto IS NULL;
16
16
  it "should return an empty reader" do
17
17
  database.adapter.connection do |connection|
18
- command = connection.create_command('SELECT `id`, `name` FROM `zoos` WHERE (`id` IS NULL)')
18
+ sql = 'SELECT `id`, `name` FROM `zoos` WHERE (`id` IS NULL)'.gsub(/\`/, database.adapter.class::COLUMN_QUOTING_CHARACTER)
19
+ command = connection.create_command(sql)
19
20
 
20
21
  command.execute_reader do |reader|
21
22
  reader.has_rows?.should eql(ENV['ADAPTER'] == 'mysql')
@@ -104,7 +104,7 @@ describe DataMapper::Adapters::Sql::Commands::LoadCommand do
104
104
  end
105
105
 
106
106
  it 'non-standard options should be considered part of the conditions' do
107
- database.log.debug { 'non-standard options should be considered part of the conditions' }
107
+ database.logger.debug { 'non-standard options should be considered part of the conditions' }
108
108
  zebra = Animal.first(:name => 'Zebra')
109
109
  zebra.name.should == 'Zebra'
110
110
 
@@ -29,7 +29,7 @@ describe DataMapper::Support::Serialization do
29
29
  <zoo id="2">
30
30
  <name>San Diego</name>
31
31
  <notes/>
32
- <updated_at>#{san_diego_zoo.updated_at}</updated_at>
32
+ <updated_at>#{san_diego_zoo.updated_at.dup}</updated_at>
33
33
  </zoo>
34
34
  EOS
35
35
  end
@@ -50,7 +50,7 @@ describe DataMapper::Support::Serialization do
50
50
  "id": 2,
51
51
  "name": "San Diego",
52
52
  "notes": null,
53
- "updated_at": #{san_diego_zoo.updated_at.to_json}
53
+ "updated_at": #{san_diego_zoo.updated_at.dup.to_json}
54
54
  }
55
55
  EOS
56
56
  end
data/spec/spec_helper.rb CHANGED
@@ -6,7 +6,7 @@ def fixtures(name)
6
6
  entry = YAML::load_file(File.dirname(__FILE__) + "/fixtures/#{name}.yaml")
7
7
  klass = Kernel::const_get(Inflector.classify(Inflector.singularize(name)))
8
8
 
9
- database.log.debug { "AUTOMIGRATE: #{klass}" }
9
+ database.logger.debug { "AUTOMIGRATE: #{klass}" }
10
10
  klass.auto_migrate!
11
11
 
12
12
  (entry.kind_of?(Array) ? entry : [entry]).each do |hash|
@@ -0,0 +1,7 @@
1
+ describe DataMapper::Support do
2
+
3
+ it "a String should translate" do
4
+ "%s is great!".t('DataMapper').should eql("DataMapper is great!")
5
+ end
6
+
7
+ end