File: single_table_inheritance_spec.rb

package info (click to toggle)
ruby-sequel 5.63.0-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 10,408 kB
  • sloc: ruby: 113,747; makefile: 3
file content (359 lines) | stat: -rw-r--r-- 16,490 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
require_relative "spec_helper"

describe Sequel::Model, "single table inheritance plugin" do
  before do
    class ::StiTest < Sequel::Model
      columns :id, :kind, :blah
      plugin :single_table_inheritance, :kind
    end 
    class ::StiTestSub1 < StiTest
    end 
    class ::StiTestSub2 < StiTest
    end 
    @ds = StiTest.dataset
    DB.reset
  end
  after do
    Object.send(:remove_const, :StiTestSub1)
    Object.send(:remove_const, :StiTestSub2)
    Object.send(:remove_const, :StiTest)
  end

  describe ".sti_load" do
    it "should load instances of the correct type" do
      StiTest.sti_load(:id => 3).must_be_instance_of StiTest
      StiTest.sti_load(:id => 3, :kind => 'StiTestSub1').must_be_instance_of StiTestSub1
      StiTest.sti_load(:id => 3, :kind => 'StiTestSub2').must_be_instance_of StiTestSub2
    end
  end

  describe ".sti_class_from_sti_key" do
    it "should load the correct subclass based on the key" do
      StiTest.sti_class_from_sti_key('StiTest').must_equal StiTest
      StiTest.sti_class_from_sti_key('StiTestSub1').must_equal StiTestSub1
      StiTest.sti_class_from_sti_key('StiTestSub2').must_equal StiTestSub2
    end
  end

  it "should freeze sti metadata when freezing model class" do
    StiTest.freeze
    StiTest.sti_dataset.frozen?.must_equal true

    StiTestSub1.freeze
    StiTestSub1.sti_key_array.frozen?.must_equal true

    proc{class ::StiTestSub1Sub1 < StiTestSub1; end}.must_raise RuntimeError, TypeError
  end

  it "should have simple_table = nil" do
    StiTest.simple_table.must_equal "sti_tests"
    StiTestSub1.simple_table.must_be_nil
  end
  
  it "should not attempt to use prepared statements" do
    StiTestSub1.plugin :prepared_statements
    StiTestSub1.load(:id=>1, :kind=>'StiTestSub1').save
    DB.sqls.must_equal ["UPDATE sti_tests SET kind = 'StiTestSub1' WHERE ((sti_tests.kind IN ('StiTestSub1')) AND (id = 1))"]

    StiTest.plugin :prepared_statements
    StiTest.load(:id=>2, :kind=>'StiTest').save
    DB.sqls.must_equal ["UPDATE sti_tests SET kind = 'StiTest' WHERE (id = 2)"]
  end

  it "should allow changing the inheritance column via a plugin :single_table_inheritance call" do
    StiTest.plugin :single_table_inheritance, :blah
    Object.send(:remove_const, :StiTestSub1)
    Object.send(:remove_const, :StiTestSub2)
    class ::StiTestSub1 < StiTest; end 
    class ::StiTestSub2 < StiTest; end 
    StiTest.dataset = StiTest.dataset.with_fetch([{:blah=>'StiTest'}, {:blah=>'StiTestSub1'}, {:blah=>'StiTestSub2'}])
    StiTest.all.collect{|x| x.class}.must_equal [StiTest, StiTestSub1, StiTestSub2]
    StiTest.dataset.sql.must_equal "SELECT * FROM sti_tests"
    StiTestSub1.dataset.sql.must_equal "SELECT * FROM sti_tests WHERE (sti_tests.blah IN ('StiTestSub1'))"
    StiTestSub2.dataset.sql.must_equal "SELECT * FROM sti_tests WHERE (sti_tests.blah IN ('StiTestSub2'))"
  end 
  
  it "should return rows with the correct class based on the polymorphic_key value" do
    StiTest.dataset = StiTest.dataset.with_fetch([{:kind=>'StiTest'}, {:kind=>'StiTestSub1'}, {:kind=>'StiTestSub2'}])
    StiTest.all.collect{|x| x.class}.must_equal [StiTest, StiTestSub1, StiTestSub2]
  end 

  it "should return rows with the correct class based on the polymorphic_key value when retreiving by primary key" do
    StiTest.dataset = StiTest.dataset.with_fetch([{:kind=>'StiTestSub1'}])
    StiTest[1].class.must_equal StiTestSub1
  end 

  it "should return rows with the correct class for subclasses based on the polymorphic_key value" do
    class ::StiTestSub1Sub < StiTestSub1; end 
    StiTestSub1.dataset = StiTestSub1.dataset.with_fetch([{:kind=>'StiTestSub1'}, {:kind=>'StiTestSub1Sub'}])
    StiTestSub1.all.collect{|x| x.class}.must_equal [StiTestSub1, StiTestSub1Sub]
  end 

  it "should fallback to the main class if the given class does not exist" do
    StiTest.dataset = StiTest.dataset.with_fetch(:kind=>'StiTestSub3')
    StiTest.all.collect{|x| x.class}.must_equal [StiTest]
  end

  it "should inherit dataset_modules correctly in subclass" do
    StiTest.dataset_module{def foo; 1; end}
    Object.send(:remove_const, :StiTestSub1)
    Object.send(:remove_const, :StiTestSub2)
    class ::StiTestSub1 < StiTest; end 
    StiTestSub1.dataset_module{def bar; 2; end}
    class ::StiTestSub2 < StiTestSub1; end 
    StiTestSub2.dataset_module{def baz; 3; end}

    StiTest.dataset.foo.must_equal 1
    proc{StiTest.dataset.bar}.must_raise NoMethodError
    proc{StiTest.dataset.baz}.must_raise NoMethodError
    StiTestSub1.dataset.foo.must_equal 1
    StiTestSub1.dataset.bar.must_equal 2
    proc{StiTestSub1.dataset.baz}.must_raise NoMethodError
    StiTestSub2.dataset.foo.must_equal 1
    StiTestSub2.dataset.bar.must_equal 2
    StiTestSub2.dataset.baz.must_equal 3
  end

  it "should fallback to the main class if the sti_key field is empty or nil without calling constantize" do
    called = false
    StiTest.define_singleton_method(:constantize){|_| called = true}
    StiTest.singleton_class.send(:private, :constantize)
    StiTest.plugin :single_table_inheritance, :kind
    StiTest.dataset = StiTest.dataset.with_fetch([{:kind=>''}, {:kind=>nil}])
    StiTest.all.collect{|x| x.class}.must_equal [StiTest, StiTest]
    called.must_equal false
  end

  it "should set the model class name when saving" do
    StiTest.new.save
    StiTestSub1.new.save
    StiTestSub2.new.save
    DB.sqls.must_equal ["INSERT INTO sti_tests (kind) VALUES ('StiTest')", "SELECT * FROM sti_tests WHERE id = 10", "INSERT INTO sti_tests (kind) VALUES ('StiTestSub1')", "SELECT * FROM sti_tests WHERE ((sti_tests.kind IN ('StiTestSub1')) AND (id = 10)) LIMIT 1", "INSERT INTO sti_tests (kind) VALUES ('StiTestSub2')", "SELECT * FROM sti_tests WHERE ((sti_tests.kind IN ('StiTestSub2')) AND (id = 10)) LIMIT 1"]
  end

  it "should destroy the model correctly" do
    StiTest.load(:id=>1).destroy
    StiTestSub1.load(:id=>1).destroy
    StiTestSub2.load(:id=>1).destroy
    DB.sqls.must_equal ["DELETE FROM sti_tests WHERE id = 1", "DELETE FROM sti_tests WHERE ((sti_tests.kind IN ('StiTestSub1')) AND (id = 1))", "DELETE FROM sti_tests WHERE ((sti_tests.kind IN ('StiTestSub2')) AND (id = 1))"]
  end

  it "should handle validations on the type column field" do
    o = StiTestSub1.new
    def o.validate
      errors.add(:kind, 'not present') unless kind
    end
    o.valid?.must_equal true
  end

  it "should set type column field even if validations are skipped" do
    StiTestSub1.new.save(:validate=>false)
    DB.sqls.must_equal ["INSERT INTO sti_tests (kind) VALUES ('StiTestSub1')", "SELECT * FROM sti_tests WHERE ((sti_tests.kind IN ('StiTestSub1')) AND (id = 10)) LIMIT 1"]
  end

  it "should override an existing value in the class name field" do
    StiTest.create(:kind=>'StiTestSub1')
    DB.sqls.must_equal ["INSERT INTO sti_tests (kind) VALUES ('StiTestSub1')", "SELECT * FROM sti_tests WHERE id = 10"]
  end

  it "should handle type column with the same name as existing method names" do
    StiTest.plugin :single_table_inheritance, :type
    StiTest.columns :id, :type
    StiTest.create
    DB.sqls.must_equal ["INSERT INTO sti_tests (type) VALUES ('StiTest')", "SELECT * FROM sti_tests WHERE id = 10"]
  end

  it "should add a filter to model datasets inside subclasses hook to only retreive objects with the matching key" do
    StiTest.dataset.sql.must_equal "SELECT * FROM sti_tests"
    StiTestSub1.dataset.sql.must_equal "SELECT * FROM sti_tests WHERE (sti_tests.kind IN ('StiTestSub1'))"
    StiTestSub2.dataset.sql.must_equal "SELECT * FROM sti_tests WHERE (sti_tests.kind IN ('StiTestSub2'))"
  end

  it "should add a correct filter for multiple levels of subclasses" do
    class ::StiTestSub1A < StiTestSub1; end
    StiTestSub1.dataset.sql.must_equal "SELECT * FROM sti_tests WHERE (sti_tests.kind IN ('StiTestSub1', 'StiTestSub1A'))"
    StiTestSub1A.dataset.sql.must_equal "SELECT * FROM sti_tests WHERE (sti_tests.kind IN ('StiTestSub1A'))"
    class ::StiTestSub2A < StiTestSub2; end
    StiTestSub2.dataset.sql.must_equal "SELECT * FROM sti_tests WHERE (sti_tests.kind IN ('StiTestSub2', 'StiTestSub2A'))"
    StiTestSub2A.dataset.sql.must_equal "SELECT * FROM sti_tests WHERE (sti_tests.kind IN ('StiTestSub2A'))"
    class ::StiTestSub1B < StiTestSub1A; end
    StiTestSub1.dataset.sql.must_equal "SELECT * FROM sti_tests WHERE (sti_tests.kind IN ('StiTestSub1', 'StiTestSub1A', 'StiTestSub1B'))"
    StiTestSub1A.dataset.sql.must_equal "SELECT * FROM sti_tests WHERE (sti_tests.kind IN ('StiTestSub1A', 'StiTestSub1B'))"
    StiTestSub1B.dataset.sql.must_equal "SELECT * FROM sti_tests WHERE (sti_tests.kind IN ('StiTestSub1B'))"
  end
  
  it "should work correctly with the :caching plugin" do
    cache_class = Class.new(Hash) do
      attr_accessor :ttl
      def set(k, v, ttl); self[k] = v; @ttl = ttl; end
      def get(k); self[k]; end
    end
    cache = cache_class.new

    StiTest.plugin :caching, cache
    def StiTest.cache_key_prefix; "stitest" end
    c2 = Class.new StiTest
    c2.cache_key(:id).must_equal StiTest.cache_key(:id)

    obj2 = c2.new
    obj2.values[:x] = 2
    obj2.save
    c2[obj2.id]
    c2.cache_get_pk(obj2.id).values.must_equal StiTest.cache_get_pk(obj2.id).values
    obj2.save
    c2.cache_get_pk(obj2.id).must_be_nil
    StiTest.cache_get_pk(obj2.id).must_be_nil
  end

  describe "with custom options" do
    before do
      class ::StiTest2 < Sequel::Model
        columns :id, :kind
        private
        def _save_refresh; end
      end
    end
    after do
      Object.send(:remove_const, :StiTest2)
      Object.send(:remove_const, :StiTest3) if defined?(StiTest3)
      Object.send(:remove_const, :StiTest4) if defined?(StiTest4)
    end

    it "should freeze sti key and model map if given as hashes when freezing model class" do
      StiTest2.plugin :single_table_inheritance, :kind, :model_map=>{0=>StiTest2, 1=>:StiTest3, 2=>'StiTest4'}, :key_map=>{StiTest2=>4, 'StiTest3'=>5, 'StiTest4'=>6}
      StiTest2.freeze
      StiTest2.sti_key_map.frozen?.must_equal true
      StiTest2.sti_model_map.frozen?.must_equal true
    end

    it "should have working row_proc if using set_dataset in subclass to remove columns" do
      StiTest2.plugin :single_table_inheritance, :kind
      class ::StiTest3 < ::StiTest2
        set_dataset(dataset.select(*(columns - [:blah])))
      end
      class ::StiTest4 < ::StiTest3; end
      StiTest3.dataset = StiTest3.dataset.with_fetch(:id=>1, :kind=>'StiTest4')
      StiTest3[1].must_equal StiTest4.load(:id=>1, :kind=>'StiTest4')
    end

    it "should work with custom procs with strings" do
      StiTest2.plugin :single_table_inheritance, :kind, :model_map=>proc{|v| v == 1 ? 'StiTest3' : 'StiTest4'}, :key_map=>proc{|klass| klass.name == 'StiTest3' ? 1 : 2}
      class ::StiTest3 < ::StiTest2; end
      class ::StiTest4 < ::StiTest2; end
      StiTest2.dataset.row_proc.call(:kind=>0).must_be_instance_of(StiTest4)
      StiTest2.dataset.row_proc.call(:kind=>1).must_be_instance_of(StiTest3)
      StiTest2.dataset.row_proc.call(:kind=>2).must_be_instance_of(StiTest4)

      StiTest2.create.kind.must_equal 2
      StiTest3.create.kind.must_equal 1
      StiTest4.create.kind.must_equal 2
    end

    it "should work with custom procs with symbols" do
      StiTest2.plugin :single_table_inheritance, :kind, :model_map=>proc{|v| v == 1 ? :StiTest3 : :StiTest4}, :key_map=>proc{|klass| klass.name == 'StiTest3' ? 1 : 2}
      class ::StiTest3 < ::StiTest2; end
      class ::StiTest4 < ::StiTest2; end
      StiTest2.dataset.row_proc.call(:kind=>0).must_be_instance_of(StiTest4)
      StiTest2.dataset.row_proc.call(:kind=>1).must_be_instance_of(StiTest3)
      StiTest2.dataset.row_proc.call(:kind=>2).must_be_instance_of(StiTest4)

      StiTest2.create.kind.must_equal 2
      StiTest3.create.kind.must_equal 1
      StiTest4.create.kind.must_equal 2
    end

    it "should work with custom hashes" do
      StiTest2.plugin :single_table_inheritance, :kind, :model_map=>{0=>StiTest2, 1=>:StiTest3, 2=>'StiTest4'}, :key_map=>{'StiTest2'=>7, StiTest2=>4, 'StiTest3'=>5, 'StiTest4'=>6}
      class ::StiTest3 < ::StiTest2; end
      class ::StiTest4 < ::StiTest2; end
      StiTest2.dataset.row_proc.call(:kind=>0).must_be_instance_of(StiTest2)
      StiTest2.dataset.row_proc.call(:kind=>1).must_be_instance_of(StiTest3)
      StiTest2.dataset.row_proc.call(:kind=>2).must_be_instance_of(StiTest4)
      StiTest3.sti_model_map.must_equal StiTest2.sti_model_map

      StiTest2.create.kind.must_equal 4
      StiTest3.create.kind.must_equal 5
      StiTest4.create.kind.must_equal 6

      class ::StiTest5 < ::StiTest4; end
      StiTest5.create.kind.must_be_nil
    end

    it "should infer key_map from model_map if provided as a hash" do
      StiTest2.plugin :single_table_inheritance, :kind, :model_map=>{0=>StiTest2, 1=>'StiTest3', 2=>:StiTest4}
      class ::StiTest3 < ::StiTest2; end
      class ::StiTest4 < ::StiTest2; end
      StiTest2.dataset.row_proc.call(:kind=>0).must_be_instance_of(StiTest2)
      StiTest2.dataset.row_proc.call(:kind=>1).must_be_instance_of(StiTest3)
      StiTest2.dataset.row_proc.call(:kind=>2).must_be_instance_of(StiTest4)

      StiTest2.create.kind.must_equal 0
      StiTest3.create.kind.must_equal 1
      StiTest4.create.kind.must_equal 2
    end

    it "should raise exceptions if a bad model value is used" do
      StiTest2.plugin :single_table_inheritance, :kind, :model_map=>{0=>1,1=>1.5, 2=>Date.today}
      class ::StiTest3 < ::StiTest2; end
      class ::StiTest4 < ::StiTest2; end
      proc{StiTest2.dataset.row_proc.call(:kind=>0)}.must_raise(Sequel::Error)
      proc{StiTest2.dataset.row_proc.call(:kind=>1)}.must_raise(Sequel::Error)
      proc{StiTest2.dataset.row_proc.call(:kind=>2)}.must_raise(Sequel::Error)
    end

    it "should work with non-bijective mappings" do
      StiTest2.plugin :single_table_inheritance, :kind, :model_map=>{0=>'StiTest3', 1=>'StiTest3', 2=>'StiTest4'}
      class ::StiTest3 < ::StiTest2; end
      class ::StiTest4 < ::StiTest2; end
      StiTest2.dataset.row_proc.call(:kind=>0).must_be_instance_of(StiTest3)
      StiTest2.dataset.row_proc.call(:kind=>1).must_be_instance_of(StiTest3)
      StiTest2.dataset.row_proc.call(:kind=>2).must_be_instance_of(StiTest4)

      [0,1].must_include(StiTest3.create.kind)
      StiTest4.create.kind.must_equal 2
    end

    it "should work with non-bijective mappings and key map procs" do
      StiTest2.plugin :single_table_inheritance, :kind,
        :key_map=>proc{|model| model.to_s == 'StiTest4' ? 2 : [0,1] }
      class ::StiTest3 < ::StiTest2; end
      class ::StiTest4 < ::StiTest2; end

      StiTest2.dataset.sql.must_equal "SELECT * FROM sti_test2s"
      StiTest3.dataset.sql.must_equal "SELECT * FROM sti_test2s WHERE (sti_test2s.kind IN (0, 1))"
      StiTest4.dataset.sql.must_equal "SELECT * FROM sti_test2s WHERE (sti_test2s.kind IN (2))"
    end

    it "should create correct sql with non-bijective mappings" do
      StiTest2.plugin :single_table_inheritance, :kind, :model_map=>{0=>'StiTest3', 1=>'StiTest3', 2=>'StiTest4'}
      class ::StiTest3 < ::StiTest2; end
      class ::StiTest4 < ::StiTest2; end
    
      StiTest2.dataset.sql.must_equal "SELECT * FROM sti_test2s"
      ["SELECT * FROM sti_test2s WHERE (sti_test2s.kind IN (0, 1))",
       "SELECT * FROM sti_test2s WHERE (sti_test2s.kind IN (1, 0))"].must_include(StiTest3.dataset.sql)
    end

    it "should destroy the model correctly" do
      StiTest2.plugin :single_table_inheritance, :kind, :model_map=>{'sti3'=>'StiTest3', 'sti3b'=>'StiTest3', 'sti4'=>'StiTest4'}
      class ::StiTest3 < ::StiTest2; end
      class ::StiTest4 < ::StiTest2; end
      StiTest2.load(:id=>1).destroy
      StiTest3.load(:id=>1).destroy
      sqls = DB.sqls
      sqls.shift.must_equal "DELETE FROM sti_test2s WHERE id = 1"
      ["DELETE FROM sti_test2s WHERE ((sti_test2s.kind IN ('sti3', 'sti3b')) AND (id = 1))",
       "DELETE FROM sti_test2s WHERE ((sti_test2s.kind IN ('sti3b', 'sti3')) AND (id = 1))"].must_include(sqls.pop)
      sqls.must_equal []
    end

    it "should honor a :key_chooser" do
      StiTest2.plugin :single_table_inheritance, :kind, :key_chooser => proc{|inst| inst.model.to_s.downcase }
      class ::StiTest3 < ::StiTest2; end
      class ::StiTest4 < ::StiTest2; end

      StiTest3.create.kind.must_equal 'stitest3'
      StiTest4.create.kind.must_equal 'stitest4'
    end
  end
end