dm-core 0.9.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (101) hide show
  1. data/CHANGELOG +144 -0
  2. data/FAQ +74 -0
  3. data/MIT-LICENSE +22 -0
  4. data/QUICKLINKS +12 -0
  5. data/README +143 -0
  6. data/lib/dm-core.rb +213 -0
  7. data/lib/dm-core/adapters.rb +4 -0
  8. data/lib/dm-core/adapters/abstract_adapter.rb +202 -0
  9. data/lib/dm-core/adapters/data_objects_adapter.rb +701 -0
  10. data/lib/dm-core/adapters/mysql_adapter.rb +132 -0
  11. data/lib/dm-core/adapters/postgres_adapter.rb +179 -0
  12. data/lib/dm-core/adapters/sqlite3_adapter.rb +105 -0
  13. data/lib/dm-core/associations.rb +172 -0
  14. data/lib/dm-core/associations/many_to_many.rb +138 -0
  15. data/lib/dm-core/associations/many_to_one.rb +101 -0
  16. data/lib/dm-core/associations/one_to_many.rb +275 -0
  17. data/lib/dm-core/associations/one_to_one.rb +61 -0
  18. data/lib/dm-core/associations/relationship.rb +116 -0
  19. data/lib/dm-core/associations/relationship_chain.rb +74 -0
  20. data/lib/dm-core/auto_migrations.rb +64 -0
  21. data/lib/dm-core/collection.rb +604 -0
  22. data/lib/dm-core/hook.rb +11 -0
  23. data/lib/dm-core/identity_map.rb +45 -0
  24. data/lib/dm-core/is.rb +16 -0
  25. data/lib/dm-core/logger.rb +233 -0
  26. data/lib/dm-core/migrations/destructive_migrations.rb +17 -0
  27. data/lib/dm-core/migrator.rb +29 -0
  28. data/lib/dm-core/model.rb +399 -0
  29. data/lib/dm-core/naming_conventions.rb +52 -0
  30. data/lib/dm-core/property.rb +611 -0
  31. data/lib/dm-core/property_set.rb +158 -0
  32. data/lib/dm-core/query.rb +590 -0
  33. data/lib/dm-core/repository.rb +159 -0
  34. data/lib/dm-core/resource.rb +618 -0
  35. data/lib/dm-core/scope.rb +35 -0
  36. data/lib/dm-core/support.rb +7 -0
  37. data/lib/dm-core/support/array.rb +13 -0
  38. data/lib/dm-core/support/assertions.rb +8 -0
  39. data/lib/dm-core/support/errors.rb +23 -0
  40. data/lib/dm-core/support/kernel.rb +7 -0
  41. data/lib/dm-core/support/symbol.rb +41 -0
  42. data/lib/dm-core/transaction.rb +267 -0
  43. data/lib/dm-core/type.rb +160 -0
  44. data/lib/dm-core/type_map.rb +80 -0
  45. data/lib/dm-core/types.rb +19 -0
  46. data/lib/dm-core/types/boolean.rb +7 -0
  47. data/lib/dm-core/types/discriminator.rb +32 -0
  48. data/lib/dm-core/types/object.rb +20 -0
  49. data/lib/dm-core/types/paranoid_boolean.rb +23 -0
  50. data/lib/dm-core/types/paranoid_datetime.rb +22 -0
  51. data/lib/dm-core/types/serial.rb +9 -0
  52. data/lib/dm-core/types/text.rb +10 -0
  53. data/spec/integration/association_spec.rb +1215 -0
  54. data/spec/integration/association_through_spec.rb +150 -0
  55. data/spec/integration/associations/many_to_many_spec.rb +171 -0
  56. data/spec/integration/associations/many_to_one_spec.rb +123 -0
  57. data/spec/integration/associations/one_to_many_spec.rb +66 -0
  58. data/spec/integration/auto_migrations_spec.rb +398 -0
  59. data/spec/integration/collection_spec.rb +1015 -0
  60. data/spec/integration/data_objects_adapter_spec.rb +32 -0
  61. data/spec/integration/model_spec.rb +68 -0
  62. data/spec/integration/mysql_adapter_spec.rb +85 -0
  63. data/spec/integration/postgres_adapter_spec.rb +732 -0
  64. data/spec/integration/property_spec.rb +224 -0
  65. data/spec/integration/query_spec.rb +376 -0
  66. data/spec/integration/repository_spec.rb +57 -0
  67. data/spec/integration/resource_spec.rb +324 -0
  68. data/spec/integration/sqlite3_adapter_spec.rb +352 -0
  69. data/spec/integration/sti_spec.rb +185 -0
  70. data/spec/integration/transaction_spec.rb +75 -0
  71. data/spec/integration/type_spec.rb +149 -0
  72. data/spec/lib/mock_adapter.rb +27 -0
  73. data/spec/spec_helper.rb +112 -0
  74. data/spec/unit/adapters/abstract_adapter_spec.rb +133 -0
  75. data/spec/unit/adapters/adapter_shared_spec.rb +15 -0
  76. data/spec/unit/adapters/data_objects_adapter_spec.rb +627 -0
  77. data/spec/unit/adapters/postgres_adapter_spec.rb +125 -0
  78. data/spec/unit/associations/many_to_many_spec.rb +14 -0
  79. data/spec/unit/associations/many_to_one_spec.rb +138 -0
  80. data/spec/unit/associations/one_to_many_spec.rb +385 -0
  81. data/spec/unit/associations/one_to_one_spec.rb +7 -0
  82. data/spec/unit/associations/relationship_spec.rb +67 -0
  83. data/spec/unit/associations_spec.rb +205 -0
  84. data/spec/unit/auto_migrations_spec.rb +110 -0
  85. data/spec/unit/collection_spec.rb +174 -0
  86. data/spec/unit/data_mapper_spec.rb +21 -0
  87. data/spec/unit/identity_map_spec.rb +126 -0
  88. data/spec/unit/is_spec.rb +80 -0
  89. data/spec/unit/migrator_spec.rb +33 -0
  90. data/spec/unit/model_spec.rb +339 -0
  91. data/spec/unit/naming_conventions_spec.rb +28 -0
  92. data/spec/unit/property_set_spec.rb +96 -0
  93. data/spec/unit/property_spec.rb +447 -0
  94. data/spec/unit/query_spec.rb +485 -0
  95. data/spec/unit/repository_spec.rb +93 -0
  96. data/spec/unit/resource_spec.rb +557 -0
  97. data/spec/unit/scope_spec.rb +131 -0
  98. data/spec/unit/transaction_spec.rb +493 -0
  99. data/spec/unit/type_map_spec.rb +114 -0
  100. data/spec/unit/type_spec.rb +119 -0
  101. metadata +187 -0
@@ -0,0 +1,133 @@
1
+ require 'monitor'
2
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'spec_helper'))
3
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'adapters', 'adapter_shared_spec'))
4
+
5
+ describe DataMapper::Adapters::AbstractAdapter do
6
+
7
+ before do
8
+ @adapter = DataMapper::Adapters::AbstractAdapter.new(:default, 'mock_uri_string')
9
+ end
10
+
11
+ it_should_behave_like 'a DataMapper Adapter'
12
+
13
+ describe "when handling transactions" do
14
+ before :each do
15
+ @transaction = DataMapper::Transaction.new(@adapter)
16
+ end
17
+ it "should be able to push and pop transactions on the current stack" do
18
+ @adapter.current_transaction.should == nil
19
+ @adapter.within_transaction?.should == false
20
+ @adapter.push_transaction(@transaction)
21
+ @adapter.current_transaction.should == @transaction
22
+ @adapter.within_transaction?.should == true
23
+ @adapter.push_transaction(@transaction)
24
+ @adapter.current_transaction.should == @transaction
25
+ @adapter.within_transaction?.should == true
26
+ @adapter.pop_transaction
27
+ @adapter.current_transaction.should == @transaction
28
+ @adapter.within_transaction?.should == true
29
+ @adapter.pop_transaction
30
+ @adapter.current_transaction.should == nil
31
+ @adapter.within_transaction?.should == false
32
+ end
33
+ it "should let each Thread have its own transaction stack" do
34
+ lock = Monitor.new
35
+ transaction2 = DataMapper::Transaction.new(@adapter)
36
+ @adapter.within_transaction?.should == false
37
+ @adapter.current_transaction.should == nil
38
+ @adapter.push_transaction(transaction2)
39
+ @adapter.within_transaction?.should == true
40
+ @adapter.current_transaction.should == transaction2
41
+ lock.synchronize do
42
+ Thread.new do
43
+ @adapter.within_transaction?.should == false
44
+ @adapter.current_transaction.should == nil
45
+ @adapter.push_transaction(@transaction)
46
+ @adapter.within_transaction?.should == true
47
+ @adapter.current_transaction.should == @transaction
48
+ lock.synchronize do
49
+ @adapter.within_transaction?.should == true
50
+ @adapter.current_transaction.should == @transaction
51
+ @adapter.pop_transaction
52
+ @adapter.within_transaction?.should == false
53
+ @adapter.current_transaction.should == nil
54
+ end
55
+ end
56
+ @adapter.within_transaction?.should == true
57
+ @adapter.current_transaction.should == transaction2
58
+ @adapter.pop_transaction
59
+ @adapter.within_transaction?.should == false
60
+ @adapter.current_transaction.should == nil
61
+ end
62
+ end
63
+ end
64
+
65
+ it "should raise NotImplementedError when #create is called" do
66
+ lambda { @adapter.create([ :resource ]) }.should raise_error(NotImplementedError)
67
+ end
68
+
69
+ it "should raise NotImplementedError when #read_many is called" do
70
+ lambda { @adapter.read_many(:query) }.should raise_error(NotImplementedError)
71
+ end
72
+
73
+ it "should raise NotImplementedError when #read_one is called" do
74
+ lambda { @adapter.read_one(:query) }.should raise_error(NotImplementedError)
75
+ end
76
+
77
+ it "should raise NotImplementedError when #update is called" do
78
+ lambda { @adapter.update(:attributes, :query) }.should raise_error(NotImplementedError)
79
+ end
80
+
81
+ it "should raise NotImplementedError when #delete is called" do
82
+ lambda { @adapter.delete(:query) }.should raise_error(NotImplementedError)
83
+ end
84
+
85
+ it "should raise NotImplementedError when #upgrade_model_storage is called" do
86
+ lambda { @adapter.upgrade_model_storage(:repository, :resource) }.should raise_error(NotImplementedError)
87
+ end
88
+
89
+ it "should raise NotImplementedError when #storage_exists? is called" do
90
+ lambda { @adapter.storage_exists?("hehu") }.should raise_error(NotImplementedError)
91
+ end
92
+
93
+ it "should raise NotImplementedError when #create_model_storage is called" do
94
+ lambda { @adapter.create_model_storage(:repository, :resource) }.should raise_error(NotImplementedError)
95
+ end
96
+
97
+ it "should raise NotImplementedError when #destroy_model_storage is called" do
98
+ lambda { @adapter.destroy_model_storage(:repository, :resource) }.should raise_error(NotImplementedError)
99
+ end
100
+
101
+ it "should raise NotImplementedError when #alter_model_storage is called" do
102
+ lambda { @adapter.alter_model_storage(:repository, :resource) }.should raise_error(NotImplementedError)
103
+ end
104
+
105
+ it "should raise NotImplementedError when #create_property_storage is called" do
106
+ lambda { @adapter.create_property_storage(:repository, :property) }
107
+ end
108
+
109
+ it "should raise NotImplementedError when #destroy_property_storage is called" do
110
+ lambda { @adapter.destroy_property_storage(:repository, :property) }
111
+ end
112
+
113
+ it "should raise NotImplementedError when #alter_property_storage is called" do
114
+ lambda { @adapter.alter_property_storage(:repository, :property) }
115
+ end
116
+
117
+ it "should raise NotImplementedError when #transaction_primitive is called" do
118
+ lambda { @adapter.transaction_primitive }.should raise_error(NotImplementedError)
119
+ end
120
+
121
+ it "should clean out dead threads from @transactions" do
122
+ @adapter.instance_eval do @transactions end.size.should == 0
123
+ t = Thread.new do
124
+ @adapter.push_transaction("plur")
125
+ end
126
+ while t.alive?
127
+ sleep 0.1
128
+ end
129
+ @adapter.instance_eval do @transactions end.size.should == 1
130
+ @adapter.push_transaction("ploj")
131
+ @adapter.instance_eval do @transactions end.size.should == 1
132
+ end
133
+ end
@@ -0,0 +1,15 @@
1
+
2
+ describe "a DataMapper Adapter", :shared => true do
3
+
4
+ it "should initialize the connection uri" do
5
+ new_adapter = @adapter.class.new(:default, Addressable::URI.parse('some://uri/string'))
6
+ new_adapter.instance_variable_get('@uri').to_s.should == Addressable::URI.parse('some://uri/string').to_s
7
+ end
8
+
9
+ %w{create read_many read_one update delete create_model_storage alter_model_storage destroy_model_storage create_property_storage alter_property_storage destroy_property_storage} .each do |meth|
10
+ it "should have a #{meth} method" do
11
+ @adapter.should respond_to(meth.intern)
12
+ end
13
+ end
14
+
15
+ end
@@ -0,0 +1,627 @@
1
+ require 'monitor'
2
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'spec_helper'))
3
+
4
+ require DataMapper.root / 'spec' / 'unit' / 'adapters' / 'adapter_shared_spec'
5
+
6
+ # TODO: make a shared adapter spec for all the DAO objects to adhere to
7
+
8
+ describe DataMapper::Adapters::DataObjectsAdapter do
9
+ before :all do
10
+ class Cheese
11
+ include DataMapper::Resource
12
+ property :id, Serial
13
+ property :name, String, :nullable => false
14
+ property :color, String, :default => 'yellow'
15
+ property :notes, String, :length => 100, :lazy => true
16
+ end
17
+ end
18
+
19
+ before do
20
+ @uri = Addressable::URI.parse('mock://localhost')
21
+ @adapter = DataMapper::Adapters::DataObjectsAdapter.new(:default, @uri)
22
+ end
23
+
24
+ it_should_behave_like 'a DataMapper Adapter'
25
+
26
+ describe "#find_by_sql" do
27
+
28
+ before do
29
+ class Plupp
30
+ include DataMapper::Resource
31
+ property :id, Integer, :key => true
32
+ property :name, String
33
+ end
34
+ end
35
+
36
+ it "should be added to DataMapper::Model" do
37
+ DataMapper::Model.instance_methods.include?("find_by_sql").should == true
38
+ Plupp.should respond_to(:find_by_sql)
39
+ end
40
+
41
+ describe "when called" do
42
+
43
+ before do
44
+ @reader = mock("reader")
45
+ @reader.stub!(:next!).and_return(false)
46
+ @reader.stub!(:close)
47
+ @connection = mock("connection")
48
+ @connection.stub!(:close)
49
+ @command = mock("command")
50
+ @adapter = Plupp.repository.adapter
51
+ @repository = Plupp.repository
52
+ @repository.stub!(:adapter).and_return(@adapter)
53
+ @adapter.stub!(:create_connection).and_return(@connection)
54
+ @adapter.should_receive(:is_a?).any_number_of_times.with(DataMapper::Adapters::DataObjectsAdapter).and_return(true)
55
+ end
56
+
57
+ it "should accept a single String argument with or without options hash" do
58
+ @connection.should_receive(:create_command).twice.with("SELECT * FROM plupps").and_return(@command)
59
+ @command.should_receive(:execute_reader).twice.and_return(@reader)
60
+ Plupp.should_receive(:repository).any_number_of_times.and_return(@repository)
61
+ Plupp.should_receive(:repository).any_number_of_times.with(:plupp_repo).and_return(@repository)
62
+ Plupp.find_by_sql("SELECT * FROM plupps").to_a
63
+ Plupp.find_by_sql("SELECT * FROM plupps", :repository => :plupp_repo).to_a
64
+ end
65
+
66
+ it "should accept an Array argument with or without options hash" do
67
+ @connection.should_receive(:create_command).twice.with("SELECT * FROM plupps WHERE plur = ?").and_return(@command)
68
+ @command.should_receive(:execute_reader).twice.with("my pretty plur").and_return(@reader)
69
+ Plupp.should_receive(:repository).any_number_of_times.and_return(@repository)
70
+ Plupp.should_receive(:repository).any_number_of_times.with(:plupp_repo).and_return(@repository)
71
+ Plupp.find_by_sql(["SELECT * FROM plupps WHERE plur = ?", "my pretty plur"]).to_a
72
+ Plupp.find_by_sql(["SELECT * FROM plupps WHERE plur = ?", "my pretty plur"], :repository => :plupp_repo).to_a
73
+ end
74
+
75
+ it "should accept a Query argument with or without options hash" do
76
+ @connection.should_receive(:create_command).twice.with('SELECT "name" FROM "plupps" WHERE "name" = ? ORDER BY "id"').and_return(@command)
77
+ @command.should_receive(:execute_reader).twice.with('my pretty plur').and_return(@reader)
78
+ Plupp.should_receive(:repository).any_number_of_times.and_return(@repository)
79
+ Plupp.should_receive(:repository).any_number_of_times.with(:plupp_repo).and_return(@repository)
80
+ Plupp.find_by_sql(DataMapper::Query.new(@repository, Plupp, "name" => "my pretty plur", :fields => ["name"])).to_a
81
+ Plupp.find_by_sql(DataMapper::Query.new(@repository, Plupp, "name" => "my pretty plur", :fields => ["name"]), :repository => :plupp_repo).to_a
82
+ end
83
+
84
+ it "requires a Repository that is a DataObjectsRepository to work" do
85
+ non_do_adapter = mock("non do adapter")
86
+ non_do_repo = mock("non do repo")
87
+ non_do_repo.stub!(:adapter).and_return(non_do_adapter)
88
+ Plupp.should_receive(:repository).any_number_of_times.with(:plupp_repo).and_return(non_do_repo)
89
+ Proc.new do
90
+ Plupp.find_by_sql(:repository => :plupp_repo)
91
+ end.should raise_error(Exception, /DataObjectsAdapter/)
92
+ end
93
+
94
+ it "requires some kind of query to work at all" do
95
+ Plupp.should_receive(:repository).any_number_of_times.with(:plupp_repo).and_return(@repository)
96
+ Proc.new do
97
+ Plupp.find_by_sql(:repository => :plupp_repo)
98
+ end.should raise_error(Exception, /requires a query/)
99
+ end
100
+
101
+ end
102
+
103
+ end
104
+
105
+ describe '#uri options' do
106
+ it 'should transform a fully specified option hash into a URI' do
107
+ options = {
108
+ :adapter => 'mysql',
109
+ :host => 'davidleal.com',
110
+ :username => 'me',
111
+ :password => 'mypass',
112
+ :port => 5000,
113
+ :database => 'you_can_call_me_al',
114
+ :socket => 'nosock'
115
+ }
116
+
117
+ adapter = DataMapper::Adapters::DataObjectsAdapter.new(:spec, options)
118
+ adapter.uri.should ==
119
+ Addressable::URI.parse("mysql://me:mypass@davidleal.com:5000/you_can_call_me_al?socket=nosock")
120
+ end
121
+
122
+ it 'should transform a minimal options hash into a URI' do
123
+ options = {
124
+ :adapter => 'mysql',
125
+ :database => 'you_can_call_me_al'
126
+ }
127
+
128
+ adapter = DataMapper::Adapters::DataObjectsAdapter.new(:spec, options)
129
+ adapter.uri.should == Addressable::URI.parse("mysql:you_can_call_me_al")
130
+ end
131
+
132
+ it 'should accept the uri when no overrides exist' do
133
+ uri = Addressable::URI.parse("protocol:///")
134
+ DataMapper::Adapters::DataObjectsAdapter.new(:spec, uri).uri.should == uri
135
+ end
136
+ end
137
+
138
+ describe '#create' do
139
+ before do
140
+ @result = mock('result', :to_i => 1, :insert_id => 1)
141
+
142
+ @adapter.stub!(:execute).and_return(@result)
143
+ @adapter.stub!(:supports_returning?).and_return(false)
144
+
145
+ @property = mock('property', :kind_of? => true, :serial? => true, :name => :property, :field => 'property', :custom? => false, :typecast => 'bind value')
146
+ @properties = [ @property ]
147
+ @bind_values = [ 'bind value' ]
148
+ @attributes = mock('attributes', :keys => @properties, :values => @bind_values)
149
+ @model = mock('model', :kind_of? => true, :key => [ @property ], :storage_name => 'models')
150
+ @resource = mock('resource', :model => @model, :dirty_attributes => @attributes)
151
+
152
+ @property.stub!(:set!).and_return(@resource)
153
+
154
+ @statement = 'INSERT INTO "models" ("property") VALUES (?)'
155
+ end
156
+
157
+ def do_create
158
+ @adapter.create([ @resource ])
159
+ end
160
+
161
+ it 'should use only dirty properties' do
162
+ @resource.should_receive(:dirty_attributes).with(no_args).and_return(@attributes)
163
+ do_create.should == 1
164
+ end
165
+
166
+ it 'should use the bind values' do
167
+ @attributes.should_receive(:values).with(no_args).and_return(@bind_values)
168
+
169
+ @adapter.should_receive(:execute).with(@statement, *@bind_values).and_return(@result)
170
+
171
+ do_create.should == 1
172
+ end
173
+
174
+ it 'should generate an SQL statement when supports_returning? is true' do
175
+ @property.should_receive(:serial?).with(no_args).and_return(true)
176
+ @adapter.should_receive(:supports_returning?).with(no_args).and_return(true)
177
+
178
+ @statement = 'INSERT INTO "models" ("property") VALUES (?) RETURNING "property"'
179
+ @adapter.should_receive(:execute).with(@statement, 'bind value').and_return(@result)
180
+
181
+ do_create.should == 1
182
+ end
183
+
184
+ it 'should generate an SQL statement when supports_default_values? is true' do
185
+ @bind_values.clear
186
+ @properties.clear
187
+ @adapter.should_receive(:supports_default_values?).with(no_args).and_return(true)
188
+
189
+ @statement = 'INSERT INTO "models" DEFAULT VALUES'
190
+ @adapter.should_receive(:execute).with(@statement).and_return(@result)
191
+
192
+ do_create.should == 1
193
+ end
194
+
195
+ it 'should generate an SQL statement when supports_default_values? is false' do
196
+ @bind_values.clear
197
+ @properties.clear
198
+ @adapter.should_receive(:supports_default_values?).with(no_args).and_return(false)
199
+
200
+ @statement = 'INSERT INTO "models" () VALUES ()'
201
+ @adapter.should_receive(:execute).with(@statement).and_return(@result)
202
+
203
+ do_create.should == 1
204
+ end
205
+
206
+ it 'should return 0 if no rows created' do
207
+ @result.should_receive(:to_i).with(no_args).and_return(0)
208
+ do_create.should == 0
209
+ end
210
+
211
+ it 'should return 1 if number of rows created is 1' do
212
+ @result.should_receive(:to_i).with(no_args).and_return(1)
213
+ do_create.should == 1
214
+ end
215
+
216
+ it 'should set the resource primary key if the model key size is 1 and the key is serial' do
217
+ @model.key.size.should == 1
218
+ @property.should_receive(:serial?).and_return(true)
219
+ @result.should_receive(:insert_id).and_return(777)
220
+ @property.should_receive(:set!).with(@resource, 777)
221
+ do_create.should == 1
222
+ end
223
+ end
224
+
225
+ [ :read_many, :read_one ].each do |method|
226
+ describe "##{method}" do
227
+ before do
228
+ @key = mock('key')
229
+ @model = mock('model', :key => @key, :storage_name => 'models', :relationships => {})
230
+ @primitive = mock('primitive')
231
+ @property = mock('property', :kind_of? => true, :model => @model, :field => 'property', :primitive => @primitive)
232
+
233
+ @child_model = @model
234
+ @parent_model = mock('parent model', :storage_name => 'parents')
235
+ @parent_property = mock('parent id', :kind_of? => true, :model => @parent_model, :field => 'id')
236
+
237
+ @child_key = [ @property ]
238
+ @parent_key = [ @parent_property ]
239
+ @relationship = mock('relationship', :child_model => @child_model, :parent_model => @parent_model, :child_key => @child_key, :parent_key => @parent_key)
240
+ @links = [ @relationship ]
241
+
242
+ @fields = [ @property ]
243
+ @bind_values = [ 'bind value' ]
244
+ @conditions = [ [ :eql, @property, @bind_values[0] ] ]
245
+
246
+ @direction = mock('direction', :property => @property, :direction => :desc)
247
+ @order = [ @direction ]
248
+
249
+ @query = mock('query', :model => @model, :kind_of? => true, :links => @links, :fields => @fields, :conditions => @conditions, :order => @order, :limit => 111, :offset => 222, :bind_values => @bind_values)
250
+
251
+ @reader = mock('reader', :close => true, :next! => false)
252
+ @command = mock('command', :set_types => nil, :execute_reader => @reader)
253
+ @connection = mock('connection', :close => true, :create_command => @command)
254
+
255
+ DataObjects::Connection.stub!(:new).and_return(@connection)
256
+ DataMapper::Query::Direction.stub!(:===).and_return(true)
257
+ end
258
+
259
+ if method == :read_one
260
+ before do
261
+ @query.should_receive(:limit).with(no_args).twice.and_return(1)
262
+
263
+ @values = @bind_values.dup
264
+
265
+ @reader.should_receive(:next!).with(no_args).and_return(true)
266
+ @reader.should_receive(:values).with(no_args).and_return(@values)
267
+
268
+ @resource = mock('resource')
269
+ @resource.should_receive(:kind_of?).with(DataMapper::Resource).any_number_of_times.and_return(true)
270
+
271
+ @model.should_receive(:load).with(@values, @query).and_return(@resource)
272
+
273
+ @statement = 'SELECT "models"."property" FROM "models" INNER JOIN "parents" ON "parents"."id" = "models"."property" WHERE "models"."property" = ? ORDER BY "models"."property" DESC LIMIT 1 OFFSET 222'
274
+ end
275
+
276
+ define_method(:do_read) do
277
+ resource = @adapter.read_one(@query)
278
+ resource.should == @resource
279
+ resource
280
+ end
281
+ elsif method == :read_many
282
+ before do
283
+ @statement = 'SELECT "models"."property" FROM "models" INNER JOIN "parents" ON "parents"."id" = "models"."property" WHERE "models"."property" = ? ORDER BY "models"."property" DESC LIMIT 111 OFFSET 222'
284
+ end
285
+
286
+ define_method(:do_read) do
287
+ collection = @adapter.read_many(@query)
288
+ collection.to_a
289
+ collection
290
+ end
291
+ end
292
+
293
+ it 'should use the bind values' do
294
+ @command.should_receive(:execute_reader).with(*@bind_values).and_return(@reader)
295
+ do_read
296
+ end
297
+
298
+ it 'should generate an SQL statement' do
299
+ @connection.should_receive(:create_command).with(@statement).and_return(@command)
300
+ do_read
301
+ end
302
+
303
+ it 'should generate an SQL statement with composite keys' do
304
+ other_property = mock('other property', :kind_of? => true)
305
+ other_property.should_receive(:field).with(:default).and_return('other')
306
+ other_property.should_receive(:model).with(no_args).and_return(@model)
307
+
308
+ other_value = 'other value'
309
+ @bind_values << other_value
310
+ @conditions << [ :eql, other_property, other_value ]
311
+
312
+ @statement = %[SELECT "models"."property" FROM "models" INNER JOIN "parents" ON "parents"."id" = "models"."property" WHERE "models"."property" = ? AND "models"."other" = ? ORDER BY "models"."property" DESC LIMIT #{method == :read_one ? '1' : '111'} OFFSET 222]
313
+ @query.should_receive(:conditions).with(no_args).twice.and_return(@conditions)
314
+
315
+ @connection.should_receive(:create_command).with(@statement).and_return(@command)
316
+
317
+ do_read
318
+ end
319
+
320
+ it 'should set the return types to the property primitives' do
321
+ @command.should_receive(:set_types).with([ @primitive ])
322
+ do_read
323
+ end
324
+
325
+ it 'should close the reader' do
326
+ @reader.should_receive(:close).with(no_args)
327
+ do_read
328
+ end
329
+
330
+ it 'should close the connection' do
331
+ @connection.should_receive(:close).with(no_args)
332
+ do_read
333
+ end
334
+
335
+ if method == :read_one
336
+ it 'should return a DataMapper::Resource' do
337
+ do_read.should == be_kind_of(DataMapper::Resource)
338
+ end
339
+ else
340
+ it 'should return a DataMapper::Collection' do
341
+ do_read.should be_kind_of(DataMapper::Collection)
342
+ end
343
+ end
344
+ end
345
+ end
346
+
347
+ describe '#update' do
348
+ before do
349
+ @result = mock('result', :to_i => 1)
350
+
351
+ @adapter.stub!(:execute).and_return(@result)
352
+
353
+ @values = %w[ new ]
354
+ @model = mock('model', :storage_name => 'models')
355
+ @property = mock('property', :kind_of? => true, :field => 'property')
356
+ @bind_values = [ 'bind value' ]
357
+ @conditions = [ [ :eql, @property, @bind_values[0] ] ]
358
+ @attributes = mock('attributes', :kind_of? => true, :empty? => false, :keys => [ @property ], :values => @values)
359
+ @query = mock('query', :kind_of? => true, :model => @model, :links => [], :conditions => @conditions, :bind_values => @bind_values)
360
+ @statement = 'UPDATE "models" SET "property" = ? WHERE "property" = ?'
361
+ end
362
+
363
+ def do_update
364
+ @adapter.update(@attributes, @query)
365
+ end
366
+
367
+ it 'should use the bind values' do
368
+ @attributes.should_receive(:values).with(no_args).and_return(@values)
369
+ @query.should_receive(:bind_values).with(no_args).and_return(@bind_values)
370
+
371
+ @adapter.should_receive(:execute).with(@statement, *@values + @bind_values).and_return(@result)
372
+
373
+ do_update.should == 1
374
+ end
375
+
376
+ it 'should generate an SQL statement' do
377
+ other_property = mock('other property', :kind_of? => true)
378
+ other_property.should_receive(:field).with(:default).and_return('other')
379
+ other_property.should_receive(:model).with(no_args).and_return(@model)
380
+
381
+ other_value = 'other value'
382
+ @bind_values << other_value
383
+ @conditions << [ :eql, other_property, other_value ]
384
+
385
+ @query.should_receive(:conditions).with(no_args).twice.and_return(@conditions)
386
+
387
+ @statement = 'UPDATE "models" SET "property" = ? WHERE "property" = ? AND "other" = ?'
388
+ @adapter.should_receive(:execute).with(@statement, *%w[ new ] + @bind_values).and_return(@result)
389
+
390
+ do_update.should == 1
391
+ end
392
+
393
+ it 'should return 0 if no rows updated' do
394
+ @result.should_receive(:to_i).with(no_args).and_return(0)
395
+ do_update.should == 0
396
+ end
397
+
398
+ it 'should return 1 if number of rows updated is 1' do
399
+ @result.should_receive(:to_i).with(no_args).and_return(1)
400
+ do_update.should == 1
401
+ end
402
+ end
403
+
404
+ describe '#delete' do
405
+ before do
406
+ @result = mock('result', :to_i => 1)
407
+
408
+ @adapter.stub!(:execute).and_return(@result)
409
+
410
+ @model = mock('model', :storage_name => 'models')
411
+ @property = mock('property', :kind_of? => true, :field => 'property')
412
+ @bind_values = [ 'bind value' ]
413
+ @conditions = [ [ :eql, @property, @bind_values[0] ] ]
414
+ @query = mock('query', :kind_of? => true, :model => @model, :links => [], :conditions => @conditions, :bind_values => @bind_values)
415
+ @resource = mock('resource', :to_query => @query)
416
+ @statement = 'DELETE FROM "models" WHERE "property" = ?'
417
+ end
418
+
419
+ def do_delete
420
+ @adapter.delete(@resource.to_query(@repository))
421
+ end
422
+
423
+ it 'should use the bind values' do
424
+ @query.should_receive(:bind_values).with(no_args).and_return(@bind_values)
425
+
426
+ @adapter.should_receive(:execute).with(@statement, *@bind_values).and_return(@result)
427
+
428
+ do_delete.should == 1
429
+ end
430
+
431
+ it 'should generate an SQL statement' do
432
+ other_property = mock('other property', :kind_of? => true)
433
+ other_property.should_receive(:field).with(:default).and_return('other')
434
+ other_property.should_receive(:model).with(no_args).and_return(@model)
435
+
436
+ other_value = 'other value'
437
+ @bind_values << other_value
438
+ @conditions << [ :eql, other_property, other_value ]
439
+
440
+ @query.should_receive(:conditions).with(no_args).twice.and_return(@conditions)
441
+
442
+ @statement = 'DELETE FROM "models" WHERE "property" = ? AND "other" = ?'
443
+ @adapter.should_receive(:execute).with(@statement, *@bind_values).and_return(@result)
444
+
445
+ do_delete.should == 1
446
+ end
447
+
448
+ it 'should return 0 if no rows deleted' do
449
+ @result.should_receive(:to_i).with(no_args).and_return(0)
450
+ do_delete.should == 0
451
+ end
452
+
453
+ it 'should return 1 if number of rows deleted is 1' do
454
+ @result.should_receive(:to_i).with(no_args).and_return(1)
455
+ do_delete.should == 1
456
+ end
457
+ end
458
+
459
+ describe "when upgrading tables" do
460
+ it "should raise NotImplementedError when #storage_exists? is called" do
461
+ lambda { @adapter.storage_exists?("cheeses") }.should raise_error(NotImplementedError)
462
+ end
463
+
464
+ describe "#upgrade_model_storage" do
465
+ it "should call #create_model_storage" do
466
+ @adapter.should_receive(:create_model_storage).with(repository, Cheese).and_return(true)
467
+ @adapter.upgrade_model_storage(repository, Cheese).should == Cheese.properties
468
+ end
469
+
470
+ it "should check if all properties of the model have columns if the table exists" do
471
+ @adapter.should_receive(:field_exists?).with("cheeses", "id").and_return(true)
472
+ @adapter.should_receive(:field_exists?).with("cheeses", "name").and_return(true)
473
+ @adapter.should_receive(:field_exists?).with("cheeses", "color").and_return(true)
474
+ @adapter.should_receive(:field_exists?).with("cheeses", "notes").and_return(true)
475
+ @adapter.should_receive(:storage_exists?).with("cheeses").and_return(true)
476
+ @adapter.upgrade_model_storage(repository, Cheese).should == []
477
+ end
478
+
479
+ it "should create and execute add column statements for columns that dont exist" do
480
+ @adapter.should_receive(:field_exists?).with("cheeses", "id").and_return(true)
481
+ @adapter.should_receive(:field_exists?).with("cheeses", "name").and_return(true)
482
+ @adapter.should_receive(:field_exists?).with("cheeses", "color").and_return(true)
483
+ @adapter.should_receive(:field_exists?).with("cheeses", "notes").and_return(false)
484
+ @adapter.should_receive(:storage_exists?).with("cheeses").and_return(true)
485
+ connection = mock("connection")
486
+ connection.should_receive(:close)
487
+ @adapter.should_receive(:create_connection).and_return(connection)
488
+ statement = mock("statement")
489
+ command = mock("command")
490
+ result = mock("result")
491
+ command.should_receive(:execute_non_query).and_return(result)
492
+ connection.should_receive(:create_command).with(statement).and_return(command)
493
+ @adapter.should_receive(:alter_table_add_column_statement).with("cheeses",
494
+ {
495
+ :nullable? => true,
496
+ :name => "notes",
497
+ :serial? => false,
498
+ :primitive => "VARCHAR",
499
+ :size => 100
500
+ }).and_return(statement)
501
+ @adapter.upgrade_model_storage(repository, Cheese).should == [Cheese.notes]
502
+ end
503
+ end
504
+ end
505
+
506
+ describe '#execute' do
507
+ before do
508
+ @mock_command = mock('Command', :execute_non_query => nil)
509
+ @mock_db = mock('DB Connection', :create_command => @mock_command, :close => true)
510
+
511
+ @adapter.stub!(:create_connection).and_return(@mock_db)
512
+ end
513
+
514
+ it 'should #create_command from the sql passed' do
515
+ @mock_db.should_receive(:create_command).with('SQL STRING').and_return(@mock_command)
516
+ @adapter.execute('SQL STRING')
517
+ end
518
+
519
+ it 'should pass any additional args to #execute_non_query' do
520
+ @mock_command.should_receive(:execute_non_query).with(:args)
521
+ @adapter.execute('SQL STRING', :args)
522
+ end
523
+
524
+ it 'should return the result of #execute_non_query' do
525
+ @mock_command.should_receive(:execute_non_query).and_return(:result_set)
526
+
527
+ @adapter.execute('SQL STRING').should == :result_set
528
+ end
529
+
530
+ it 'should log any errors, then re-raise them' do
531
+ @mock_command.should_receive(:execute_non_query).and_raise("Oh Noes!")
532
+ DataMapper.logger.should_receive(:error)
533
+
534
+ lambda { @adapter.execute('SQL STRING') }.should raise_error("Oh Noes!")
535
+ end
536
+
537
+ it 'should always close the db connection' do
538
+ @mock_command.should_receive(:execute_non_query).and_raise("Oh Noes!")
539
+ @mock_db.should_receive(:close)
540
+
541
+ lambda { @adapter.execute('SQL STRING') }.should raise_error("Oh Noes!")
542
+ end
543
+ end
544
+
545
+ describe '#query' do
546
+ before do
547
+ @mock_reader = mock('Reader', :fields => ['id', 'UserName', 'AGE'],
548
+ :values => [1, 'rando', 27],
549
+ :close => true)
550
+ @mock_command = mock('Command', :execute_reader => @mock_reader)
551
+ @mock_db = mock('DB Connection', :create_command => @mock_command, :close => true)
552
+
553
+ #make the while loop run exactly once
554
+ @mock_reader.stub!(:next!).and_return(true, nil)
555
+ @adapter.stub!(:create_connection).and_return(@mock_db)
556
+ end
557
+
558
+ it 'should #create_command from the sql passed' do
559
+ @mock_db.should_receive(:create_command).with('SQL STRING').and_return(@mock_command)
560
+ @adapter.query('SQL STRING')
561
+ end
562
+
563
+ it 'should pass any additional args to #execute_reader' do
564
+ @mock_command.should_receive(:execute_reader).with(:args).and_return(@mock_reader)
565
+ @adapter.query('SQL STRING', :args)
566
+ end
567
+
568
+ describe 'returning multiple fields' do
569
+
570
+ it 'should underscore the field names as members of the result struct' do
571
+ @mock_reader.should_receive(:fields).and_return(['id', 'UserName', 'AGE'])
572
+
573
+ result = @adapter.query('SQL STRING')
574
+
575
+ result.first.members.should == %w{id user_name age}
576
+ end
577
+
578
+ it 'should convert each row into the struct' do
579
+ @mock_reader.should_receive(:values).and_return([1, 'rando', 27])
580
+
581
+ @adapter.query('SQL STRING')
582
+ end
583
+
584
+ it 'should add the row structs into the results array' do
585
+ results = @adapter.query('SQL STRING')
586
+
587
+ results.should be_kind_of(Array)
588
+
589
+ row = results.first
590
+ row.should be_kind_of(Struct)
591
+
592
+ row.id.should == 1
593
+ row.user_name.should == 'rando'
594
+ row.age.should == 27
595
+ end
596
+
597
+ end
598
+
599
+ describe 'returning a single field' do
600
+
601
+ it 'should add the value to the results array' do
602
+ @mock_reader.should_receive(:fields).and_return(['username'])
603
+ @mock_reader.should_receive(:values).and_return(['rando'])
604
+
605
+ results = @adapter.query('SQL STRING')
606
+
607
+ results.should be_kind_of(Array)
608
+ results.first.should == 'rando'
609
+ end
610
+
611
+ end
612
+
613
+ it 'should log any errors, then re-raise them' do
614
+ @mock_command.should_receive(:execute_non_query).and_raise("Oh Noes!")
615
+ DataMapper.logger.should_receive(:error)
616
+
617
+ lambda { @adapter.execute('SQL STRING') }.should raise_error("Oh Noes!")
618
+ end
619
+
620
+ it 'should always close the db connection' do
621
+ @mock_command.should_receive(:execute_non_query).and_raise("Oh Noes!")
622
+ @mock_db.should_receive(:close)
623
+
624
+ lambda { @adapter.execute('SQL STRING') }.should raise_error("Oh Noes!")
625
+ end
626
+ end
627
+ end