datamapper 0.2.4 → 0.2.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. data/CHANGELOG +16 -1
  2. data/README +10 -8
  3. data/environment.rb +1 -1
  4. data/example.rb +13 -5
  5. data/lib/data_mapper.rb +2 -0
  6. data/lib/data_mapper/adapters/abstract_adapter.rb +61 -0
  7. data/lib/data_mapper/adapters/data_object_adapter.rb +33 -6
  8. data/lib/data_mapper/adapters/mysql_adapter.rb +5 -0
  9. data/lib/data_mapper/adapters/postgresql_adapter.rb +12 -0
  10. data/lib/data_mapper/adapters/sql/commands/load_command.rb +6 -14
  11. data/lib/data_mapper/adapters/sql/mappings/column.rb +37 -26
  12. data/lib/data_mapper/adapters/sql/mappings/table.rb +50 -8
  13. data/lib/data_mapper/associations.rb +4 -5
  14. data/lib/data_mapper/associations/has_and_belongs_to_many_association.rb +2 -2
  15. data/lib/data_mapper/associations/has_many_association.rb +40 -13
  16. data/lib/data_mapper/associations/has_n_association.rb +1 -1
  17. data/lib/data_mapper/base.rb +5 -448
  18. data/lib/data_mapper/callbacks.rb +12 -2
  19. data/lib/data_mapper/context.rb +4 -0
  20. data/lib/data_mapper/database.rb +1 -1
  21. data/lib/data_mapper/identity_map.rb +2 -2
  22. data/lib/data_mapper/persistence.rb +538 -0
  23. data/lib/data_mapper/support/active_record_impersonation.rb +21 -3
  24. data/lib/data_mapper/support/errors.rb +2 -0
  25. data/lib/data_mapper/support/serialization.rb +7 -10
  26. data/lib/data_mapper/support/struct.rb +7 -0
  27. data/lib/data_mapper/validatable_extensions/validations/validates_uniqueness_of.rb +11 -4
  28. data/performance.rb +23 -10
  29. data/rakefile.rb +1 -1
  30. data/spec/active_record_impersonation_spec.rb +2 -6
  31. data/spec/acts_as_tree_spec.rb +3 -1
  32. data/spec/associations_spec.rb +40 -160
  33. data/spec/attributes_spec.rb +1 -1
  34. data/spec/base_spec.rb +41 -13
  35. data/spec/callbacks_spec.rb +32 -0
  36. data/spec/coersion_spec.rb +1 -1
  37. data/spec/column_spec.rb +22 -12
  38. data/spec/dependency_spec.rb +5 -3
  39. data/spec/embedded_value_spec.rb +33 -17
  40. data/spec/has_many_association_spec.rb +173 -0
  41. data/spec/legacy_spec.rb +2 -2
  42. data/spec/load_command_spec.rb +59 -7
  43. data/spec/models/animal.rb +6 -2
  44. data/spec/models/animals_exhibit.rb +3 -1
  45. data/spec/models/career.rb +2 -1
  46. data/spec/models/comment.rb +3 -1
  47. data/spec/models/exhibit.rb +3 -1
  48. data/spec/models/fruit.rb +3 -1
  49. data/spec/models/person.rb +10 -1
  50. data/spec/models/post.rb +3 -1
  51. data/spec/models/project.rb +3 -1
  52. data/spec/models/section.rb +3 -1
  53. data/spec/models/serializer.rb +3 -1
  54. data/spec/models/user.rb +3 -1
  55. data/spec/models/zoo.rb +3 -1
  56. data/spec/paranoia_spec.rb +3 -1
  57. data/spec/postgres_spec.rb +54 -0
  58. data/spec/save_command_spec.rb +9 -5
  59. data/spec/schema_spec.rb +0 -91
  60. data/spec/single_table_inheritance_spec.rb +8 -0
  61. data/spec/table_spec.rb +46 -0
  62. data/spec/validates_uniqueness_of_spec.rb +19 -1
  63. metadata +8 -10
  64. data/lib/data_mapper/associations/has_one_association.rb +0 -77
  65. data/plugins/dataobjects/do_rb +0 -0
data/CHANGELOG CHANGED
@@ -123,4 +123,19 @@
123
123
  * MAJOR: Resolve issue with non-unique-hash values and #dirty?; now frozen original values are stored instead
124
124
  * Added Base#update_attributes
125
125
  * MAJOR: Queries are now passed to the database drivers in a parameterized fashion
126
- * Updated PostgreSQL driver and adapter to current
126
+ * Updated PostgreSQL driver and adapter to current
127
+
128
+ -- 0.2.4
129
+ * Bug fixes
130
+ * Added paranoia
131
+
132
+ -- 0.2.5
133
+ * has_one bugfixes
134
+ * Added syntax for setting CHECK-constraints directly in your properties (Postgres)
135
+ * You can now set indexes with :index => true and :index => :unique
136
+ * Support for composite indexes (thanks to Jeffrey Gelens)
137
+ * Add composite scope to validates_uniqueness
138
+ * Added private/protected properties
139
+ * Remove HasOneAssociation, Make HasManyAssociation impersonate has_one relationships
140
+ * Added #get method
141
+ * Persistence module added, inheriting from DataMapper::Base no longer necessary
data/README CHANGED
@@ -93,10 +93,10 @@ Ruby on Rails project, and have a database.yml, DataMapper should pick it up aut
93
93
 
94
94
  If you'd like to set the database connection manually before using it, do something like this:
95
95
  DataMapper::Database.setup({
96
- :adapter => 'mysql'
97
- :host => 'localhost'
98
- :username => 'root'
99
- :password => 'R00tPaswooooord'
96
+ :adapter => 'mysql',
97
+ :host => 'localhost',
98
+ :username => 'root',
99
+ :password => 'R00tPaswooooord',
100
100
  :database => 'selecta_development'
101
101
  })
102
102
 
@@ -109,7 +109,7 @@ SQLite3:: (sqlite3 adaptor)
109
109
 
110
110
  Defining your models requires a few steps.
111
111
 
112
- * Inherit your class from DataMapper::Base
112
+ * Include the module DataMapper::Persistence in your class
113
113
  * Define your properties you wish to access from the database
114
114
  * Define any relationships (optional)
115
115
 
@@ -119,7 +119,9 @@ which are too numerous to go into here.
119
119
 
120
120
  Your models might end up looking something like this:
121
121
 
122
- class Animal < DataMapper::Base
122
+ class Animal
123
+ include DataMapper::Persistence
124
+
123
125
  property :name, :string
124
126
  property :notes, :text, :lazy => true
125
127
 
@@ -187,7 +189,7 @@ This will generate the table structure for your model automatically.
187
189
 
188
190
  You can also execute the following to generate ALL of your models' tables:
189
191
 
190
- DataMapper::Base.auto_migrate!
192
+ DataMapper::Persistence.auto_migrate!
191
193
 
192
194
  === NOTE
193
195
  Both of these methods are DESTRUCTIVE. If you are working in anything other than a pre-production environment,
@@ -198,4 +200,4 @@ stay far away from these methods!
198
200
  Merb's coding conventions are a fantastic start: (http://merb.devjavu.com/#ContributingtoMerb)
199
201
 
200
202
  DataMapper could use some documentation help. If you'd like to contribute, the example you'll
201
- find on the above link is top-notch.
203
+ find on the above link is top-notch.
data/environment.rb CHANGED
@@ -41,4 +41,4 @@ DataMapper::Database.setup(:mock, :adapter => MockAdapter)
41
41
  [:default, :mock].each { |name| database(name) { load_models.call } }
42
42
 
43
43
  # Reset the test database.
44
- DataMapper::Base.auto_migrate! unless ENV['AUTO_MIGRATE'] == 'false'
44
+ DataMapper::Persistence.auto_migrate! unless ENV['AUTO_MIGRATE'] == 'false'
data/example.rb CHANGED
@@ -26,7 +26,8 @@ end
26
26
 
27
27
  require 'irb'
28
28
 
29
- database { IRB::start }
29
+ # database { IRB::start }
30
+ IRB::start
30
31
 
31
32
  if false
32
33
 
@@ -37,7 +38,9 @@ DataMapper::Database.setup({
37
38
  :username => 'root'
38
39
  })
39
40
 
40
- class Animal < DataMapper::Base #:nodoc:
41
+ class Animal #:nodoc:
42
+ include DataMapper::Persistence
43
+
41
44
  set_table_name 'animals' # Just as an example. Same inflector as Rails,
42
45
  # so this really isn't necessary.
43
46
 
@@ -47,17 +50,22 @@ class Animal < DataMapper::Base #:nodoc:
47
50
  has_and_belongs_to_many :exhibits
48
51
  end
49
52
 
50
- class Exhibit < DataMapper::Base #:nodoc:
53
+ class Exhibit #:nodoc:
54
+ include DataMapper::Persistence
55
+
51
56
  property :name, :string
52
57
  belongs_to :zoo
53
58
  end
54
59
 
55
- class Zoo < DataMapper::Base #:nodoc:
60
+ class Zoo #:nodoc:
61
+ include DataMapper::Persistence
62
+
56
63
  property :name, :string
57
64
  has_many :exhibits
58
65
  end
59
66
 
60
- class Person < DataMapper::Base #:nodoc:
67
+ class Person #:nodoc:
68
+ include DataMapper::Persistence
61
69
 
62
70
  property :name, :string
63
71
  property :age, :integer
data/lib/data_mapper.rb CHANGED
@@ -29,8 +29,10 @@ require 'data_mapper/support/silence'
29
29
  require 'data_mapper/support/inflector'
30
30
  require 'data_mapper/support/errors'
31
31
  require 'data_mapper/database'
32
+ require 'data_mapper/persistence'
32
33
  require 'data_mapper/base'
33
34
 
35
+
34
36
  begin
35
37
  # This block of code is for compatibility with Ruby On Rails' or Merb's database.yml
36
38
  # file, allowing you to simply require the data_mapper.rb in your
@@ -29,10 +29,71 @@ module DataMapper
29
29
  raise NotImplementedError.new
30
30
  end
31
31
 
32
+ def get(database_context, klass, *keys)
33
+ raise NotImplementedError.new
34
+ end
35
+
32
36
  def logger
33
37
  @logger || @logger = @configuration.logger
34
38
  end
35
39
 
40
+ protected
41
+
42
+ def materialize(database_context, klass, values, reload, loaded_set)
43
+
44
+ table = self.table(klass)
45
+
46
+ instance_id = table.key.type_cast_value(values[table.key.name])
47
+
48
+ instance_type = if table.multi_class? && table.type_column
49
+ values[table.type_column.name].blank? ? klass :
50
+ table.type_column.type_cast_value(values[table.type_column.name])
51
+ else
52
+ klass
53
+ end
54
+
55
+ instance = create_instance(database_context, instance_type, instance_id, reload)
56
+
57
+ instance_type.callbacks.execute(:before_materialize, instance)
58
+
59
+ type_casted_values = {}
60
+
61
+ values.each_pair do |k,v|
62
+ column = table[k]
63
+ type_cast_value = column.type_cast_value(v)
64
+ type_casted_values[k] = type_cast_value
65
+ instance.instance_variable_set(column.instance_variable_name, type_cast_value)
66
+ end
67
+
68
+ instance.original_values = type_casted_values
69
+ instance.loaded_set = loaded_set
70
+
71
+ instance_type.callbacks.execute(:after_materialize, instance)
72
+
73
+ return instance
74
+
75
+ #rescue => e
76
+ # raise MaterializationError.new("Failed to materialize row: #{values.inspect}\n#{e.to_yaml}")
77
+ end
78
+
79
+ def create_instance(database_context, instance_type, instance_id, reload)
80
+ instance = database_context.identity_map.get(instance_type, instance_id)
81
+
82
+ if instance.nil? || reload
83
+ instance = instance_type.new() if instance.nil?
84
+ instance.instance_variable_set(:@__key, instance_id)
85
+ instance.instance_variable_set(:@new_record, false)
86
+ database_context.identity_map.set(instance)
87
+ elsif instance.new_record?
88
+ instance.instance_variable_set(:@__key, instance_id)
89
+ instance.instance_variable_set(:@new_record, false)
90
+ end
91
+
92
+ instance.database_context = database_context
93
+
94
+ return instance
95
+ end
96
+
36
97
  end # class AbstractAdapter
37
98
 
38
99
  end # module Adapters
@@ -138,6 +138,17 @@ module DataMapper
138
138
  db.close
139
139
  end
140
140
 
141
+ def execute(*args)
142
+ db = create_connection
143
+ command = db.create_command(args.shift)
144
+ return command.execute_non_query(*args)
145
+ rescue => e
146
+ logger.error { e }
147
+ raise e
148
+ ensure
149
+ db.close
150
+ end
151
+
141
152
  def handle_error(error)
142
153
  raise error
143
154
  end
@@ -186,14 +197,15 @@ module DataMapper
186
197
  case instance
187
198
  when Class then table(instance).create!
188
199
  when Mappings::Table then instance.create!
189
- when DataMapper::Base then
190
- return false unless instance.new_record? || instance.dirty?
191
-
200
+ when DataMapper::Persistence then
192
201
  event = instance.new_record? ? :create : :update
193
202
 
194
203
  return false if validate && !instance.validate_recursively(event, Set.new)
195
204
 
196
205
  callback(instance, :before_save)
206
+
207
+ return false unless instance.new_record? || instance.dirty?
208
+
197
209
  result = send(event, database_context, instance)
198
210
 
199
211
  instance.database_context = database_context
@@ -249,7 +261,7 @@ module DataMapper
249
261
 
250
262
  table = self.table(instance)
251
263
  attributes = instance.dirty_attributes
252
-
264
+
253
265
  if table.multi_class?
254
266
  instance.instance_variable_set(
255
267
  table[:type].instance_variable_name,
@@ -263,7 +275,7 @@ module DataMapper
263
275
  keys << table[key].to_sql
264
276
  values << value
265
277
  end
266
-
278
+
267
279
  sql = if keys.size > 0
268
280
  "INSERT INTO #{table.to_sql} (#{keys.join(', ')}) VALUES ?"
269
281
  else
@@ -283,10 +295,25 @@ module DataMapper
283
295
  self.class::Commands::LoadCommand.new(self, database_context, klass, options).call
284
296
  end
285
297
 
298
+ def get(database_context, klass, keys)
299
+ table = self.table(klass)
300
+ sql = "SELECT #{table.columns.map { |column| column.to_sql }.join(', ')} FROM #{table.to_sql} WHERE #{table.keys.map { |key| "#{key.to_sql} = ?" }.join(' AND ')}"
301
+
302
+ connection do |db|
303
+ db.create_command(sql).execute_reader(*keys) do |reader|
304
+ values = {}
305
+ table.columns.each_with_index do |column, i|
306
+ values[column.name] = reader.item(i)
307
+ end
308
+ materialize(database_context, klass, values, false, [])
309
+ end
310
+ end
311
+ end
312
+
286
313
  def table(instance)
287
314
  case instance
288
315
  when DataMapper::Adapters::Sql::Mappings::Table then instance
289
- when DataMapper::Base then schema[instance.class]
316
+ when DataMapper::Persistence then schema[instance.class]
290
317
  when Class, String then schema[instance]
291
318
  else raise "Don't know how to map #{instance.inspect} to a table."
292
319
  end
@@ -13,6 +13,11 @@ module DataMapper
13
13
  module Adapters
14
14
 
15
15
  class MysqlAdapter < DataObjectAdapter
16
+
17
+ TRUE_ALIASES << "T".freeze << "\004\bT".freeze
18
+ FALSE_ALIASES << "F".freeze << "\004\bF".freeze
19
+
20
+
16
21
  def create_connection
17
22
 
18
23
  connection_string = ""
@@ -149,6 +149,10 @@ module DataMapper
149
149
  "SERIAL"
150
150
  end
151
151
 
152
+ def check_declaration
153
+ "CHECK (" << check << ")"
154
+ end
155
+
152
156
  def to_alter_sql
153
157
  "ALTER TABLE " << table.to_sql << " ALTER COLUMN " << to_alter_form
154
158
  end
@@ -221,6 +225,14 @@ module DataMapper
221
225
  if default && !default_declaration.blank?
222
226
  @to_long_form << " #{default_declaration}"
223
227
  end
228
+
229
+ if unique? && !unique_declaration.blank?
230
+ @to_long_form << " #{unique_declaration}"
231
+ end
232
+
233
+ if check && !check_declaration.blank?
234
+ @to_long_form << " #{check_declaration}"
235
+ end
224
236
  end
225
237
 
226
238
  @to_long_form
@@ -38,7 +38,6 @@ module DataMapper
38
38
  end
39
39
 
40
40
  def materialize(values)
41
-
42
41
  instance_id = @key.type_cast_value(values[@key_index])
43
42
  instance = create_instance(instance_id,
44
43
  if @type_override_present
@@ -47,28 +46,23 @@ module DataMapper
47
46
  @klass
48
47
  end
49
48
  )
50
-
49
+
51
50
  @klass.callbacks.execute(:before_materialize, instance)
52
51
 
53
- original_values = instance.original_values
54
-
52
+ type_casted_values = {}
53
+
55
54
  @columns.each_pair do |index, column|
56
55
  # This may be a little confusing, but we're
57
56
  # setting both the original_value, and the
58
57
  # instance-variable through method chaining to avoid
59
58
  # lots of extra short-lived local variables.
60
- type_cast_value = instance.instance_variable_set(
59
+ type_casted_values[column.name] = instance.instance_variable_set(
61
60
  column.instance_variable_name,
62
61
  column.type_cast_value(values[index])
63
62
  )
64
-
65
- original_values[column.name] = case type_cast_value
66
- when String, Date, Time then type_cast_value.dup
67
- when column.type == :object then Marshal.dump(type_cast_value)
68
- else type_cast_value
69
- end
70
63
  end
71
-
64
+
65
+ instance.original_values = type_casted_values
72
66
  instance.instance_variable_set(:@loaded_set, @set)
73
67
  @set << instance
74
68
 
@@ -85,8 +79,6 @@ module DataMapper
85
79
  end
86
80
 
87
81
  private
88
-
89
- class MaterializationError < StandardError; end
90
82
 
91
83
  def create_instance(instance_id, instance_type)
92
84
  instance = @database_context.identity_map.get(@klass, instance_id)
@@ -6,8 +6,8 @@ module DataMapper
6
6
  # TODO: There are of course many more options to add here.
7
7
  # Ordinal, Length/Size, Nullability are just a few.
8
8
  class Column
9
-
10
- attr_reader :type, :name
9
+ attr_reader :type, :name, :ordinal, :size, :default, :check
10
+ attr_writer :lazy, :index
11
11
  attr_accessor :table, :options
12
12
 
13
13
  def initialize(adapter, table, name, type, ordinal, options = {})
@@ -21,6 +21,21 @@ module DataMapper
21
21
  @lazy = @options.has_key?(:lazy) ? @options[:lazy] : (@type == :text && !@key)
22
22
  @serial = @options[:serial] == true
23
23
  @default = @options[:default]
24
+ @unique = @options.has_value?(:unique)
25
+ @index = @options[:index]
26
+ @check = @options[:check] # only for postgresql
27
+
28
+ @size = if @options.has_key?(:size)
29
+ @options[:size]
30
+ elsif @options.has_key?(:length)
31
+ @options[:length]
32
+ else
33
+ case type
34
+ when :integer then 11
35
+ when :string, :class then 50
36
+ else nil
37
+ end
38
+ end
24
39
  end
25
40
 
26
41
  def type=(value)
@@ -38,15 +53,7 @@ module DataMapper
38
53
  flush_sql_caches!
39
54
  @name = value
40
55
  end
41
-
42
- def ordinal
43
- @ordinal
44
- end
45
-
46
- def lazy=(value)
47
- @lazy = value
48
- end
49
-
56
+
50
57
  # Determines if the field should be lazy loaded.
51
58
  # You can set this explicitly, or accept the default,
52
59
  # which is false for all but text fields.
@@ -66,10 +73,14 @@ module DataMapper
66
73
  @serial
67
74
  end
68
75
 
69
- def default
70
- @default
76
+ def unique?
77
+ @unique
71
78
  end
72
79
 
80
+ def index?
81
+ @index
82
+ end
83
+
73
84
  def default=(value)
74
85
  self.flush_sql_caches!
75
86
  @default = value
@@ -100,18 +111,10 @@ module DataMapper
100
111
  @to_sql || (@to_sql = @adapter.quote_column_name(column_name).freeze)
101
112
  end
102
113
  end
103
-
104
- def size
105
- @size || begin
106
- return @size = @options[:size] if @options.has_key?(:size)
107
- return @size = @options[:length] if @options.has_key?(:length)
108
-
109
- @size = case type
110
- when :integer then 11
111
- when :string, :class then 50
112
- else nil
113
- end
114
- end
114
+
115
+ def size=(val)
116
+ self.flush_sql_caches!
117
+ @size = val
115
118
  end
116
119
 
117
120
  def inspect
@@ -207,7 +210,11 @@ module DataMapper
207
210
  unless default.nil? || (value = default_declaration).blank?
208
211
  @to_long_form << " #{value}"
209
212
  end
210
-
213
+
214
+ if unique? && !unique_declaration.blank?
215
+ @to_long_form << " #{unique_declaration}"
216
+ end
217
+
211
218
  @to_long_form
212
219
  end
213
220
  end
@@ -244,6 +251,10 @@ module DataMapper
244
251
  "AUTO_INCREMENT"
245
252
  end
246
253
 
254
+ def unique_declaration
255
+ "UNIQUE"
256
+ end
257
+
247
258
  def default_declaration
248
259
  @adapter.connection { |db| db.create_command("DEFAULT ?").escape_sql([default]) }
249
260
  end