dm-core 0.9.2

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 (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