hquery 1.0.0
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +133 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE +21 -0
- data/README.md +700 -0
- data/Rakefile +8 -0
- data/lib/hq/context.rb +65 -0
- data/lib/hq/instance.rb +87 -0
- data/lib/hq/page.rb +33 -0
- data/lib/hq/query.rb +306 -0
- data/lib/hq/relations.rb +134 -0
- data/lib/hq/version.rb +3 -0
- data/lib/hq.rb +91 -0
- data/test.md +1466 -0
- metadata +57 -0
data/test.md
ADDED
|
@@ -0,0 +1,1466 @@
|
|
|
1
|
+
|
|
2
|
+
# File: ./context_test.rb
|
|
3
|
+
|
|
4
|
+
require_relative 'test_helper'
|
|
5
|
+
|
|
6
|
+
module Hq
|
|
7
|
+
class TestDefinitionContext < Minitest::Test
|
|
8
|
+
def setup
|
|
9
|
+
@context = DefinitionContext.new(:test_model, 'data')
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def test_from_array_with_valid_array
|
|
13
|
+
@context.from_array([{ id: 1, name: 'Test' }, { id: 2, name: 'Another' }])
|
|
14
|
+
assert_equal 2, @context.loaded_data.size
|
|
15
|
+
assert @context.loaded_data.all?(&:frozen?)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def test_from_array_with_non_array
|
|
19
|
+
error = assert_raises(DataFileError) { @context.from_array('not an array') }
|
|
20
|
+
assert_match /expects an Array of hashes, but got String/, error.message
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def test_from_array_with_non_hash_item
|
|
24
|
+
error = assert_raises(DataFileError) { @context.from_array(['not a hash']) }
|
|
25
|
+
assert_match /array item 1 should be a Hash, but got String/, error.message
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def test_scope_definition
|
|
29
|
+
@context.scope({ test_scope: -> { self } })
|
|
30
|
+
assert @context.scopes.key?(:test_scope)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def test_scope_non_callable
|
|
34
|
+
assert_raises(Hq::ScopeError) { @context.scope({ test_scope: 'not callable' }) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def test_load_with_files
|
|
38
|
+
Dir.mktmpdir do |dir|
|
|
39
|
+
File.write(File.join(dir, 'file1.rb'), "{ id: 1, title: 'First' }")
|
|
40
|
+
File.write(File.join(dir, 'file2.rb'), "{ id: 2, title: 'Second' }")
|
|
41
|
+
|
|
42
|
+
context = DefinitionContext.new(:test, dir)
|
|
43
|
+
context.load('*.rb')
|
|
44
|
+
|
|
45
|
+
assert_equal 2, context.loaded_data.size
|
|
46
|
+
assert_equal({ id: 1, title: 'First' }, context.loaded_data[0])
|
|
47
|
+
assert_equal({ id: 2, title: 'Second' }, context.loaded_data[1])
|
|
48
|
+
assert context.loaded_data.all?(&:frozen?)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def test_load_no_files
|
|
53
|
+
Dir.mktmpdir do |dir|
|
|
54
|
+
context = DefinitionContext.new(:test, dir)
|
|
55
|
+
error = assert_raises(Hq::DataFileError) { context.load('*.rb') }
|
|
56
|
+
assert_match /No files found matching pattern/, error.message
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def test_load_non_hash
|
|
61
|
+
Dir.mktmpdir do |dir|
|
|
62
|
+
File.write(File.join(dir, 'invalid.rb'), "'string'")
|
|
63
|
+
context = DefinitionContext.new(:test, dir)
|
|
64
|
+
error = assert_raises(DataFileError) { context.load('*.rb') }
|
|
65
|
+
assert_match /should return a Hash, but returned String/, error.message
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def test_load_syntax_error
|
|
70
|
+
Dir.mktmpdir do |dir|
|
|
71
|
+
File.write(File.join(dir, 'invalid.rb'), "{ id: 1,, }")
|
|
72
|
+
context = DefinitionContext.new(:test, dir)
|
|
73
|
+
error = assert_raises(DataFileError) { context.load('*.rb') }
|
|
74
|
+
assert_match /has a syntax error/, error.message
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def test_load_generic_error
|
|
79
|
+
Dir.mktmpdir do |dir|
|
|
80
|
+
File.write(File.join(dir, 'error.rb'), "raise 'boom'")
|
|
81
|
+
context = DefinitionContext.new(:test, dir)
|
|
82
|
+
error = assert_raises(DataFileError) { context.load('*.rb') }
|
|
83
|
+
assert_match /couldn't be loaded:\nboom/, error.message
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# File: ./data_test.rb
|
|
91
|
+
|
|
92
|
+
require_relative 'test_helper'
|
|
93
|
+
module Hq
|
|
94
|
+
class TestHqData < Minitest::Test
|
|
95
|
+
def setup
|
|
96
|
+
Hq.reload # Clear collections before each test
|
|
97
|
+
# Reset data path to default
|
|
98
|
+
Hq.configure(data_path: 'data')
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def teardown
|
|
102
|
+
Hq.reload # Clean up after each test
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def test_configure
|
|
106
|
+
Hq.configure(data_path: 'custom_data')
|
|
107
|
+
assert_equal 'custom_data', Hq.instance_variable_get(:@data_path)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def test_define_and_query
|
|
111
|
+
Hq.define :test_model do
|
|
112
|
+
from_array([{ id: 1 }])
|
|
113
|
+
end
|
|
114
|
+
assert_instance_of Query, Hq.query(:test_model)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def test_undefined_model_raises
|
|
118
|
+
error = assert_raises(RuntimeError) { Hq.query(:undefined) }
|
|
119
|
+
assert_equal "Collection undefined not defined. Use Hq.define :undefined to define it.", error.message
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def test_reload
|
|
123
|
+
Hq.define :test_model do
|
|
124
|
+
from_array([])
|
|
125
|
+
end
|
|
126
|
+
Hq.reload
|
|
127
|
+
assert_empty Hq.defined_models
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def test_defined_models
|
|
131
|
+
Hq.define :model1 do
|
|
132
|
+
from_array([])
|
|
133
|
+
end
|
|
134
|
+
Hq.define :model2 do
|
|
135
|
+
from_array([])
|
|
136
|
+
end
|
|
137
|
+
assert_equal [:model1, :model2], Hq.defined_models.sort
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def test_global_helper
|
|
141
|
+
Hq.define :test_model do
|
|
142
|
+
from_array([{ id: 1 }])
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Test the global hq() helper method
|
|
146
|
+
assert_instance_of Query, hq(:test_model)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class TestPagination < Minitest::Test
|
|
159
|
+
def setup
|
|
160
|
+
Hq.reload
|
|
161
|
+
# Create a collection with 23 items for thorough pagination testing
|
|
162
|
+
Hq.define :paginated_posts do
|
|
163
|
+
from_array((1..23).map do |i|
|
|
164
|
+
{
|
|
165
|
+
id: i,
|
|
166
|
+
title: "Post #{i}",
|
|
167
|
+
content: "Content for post #{i}",
|
|
168
|
+
priority: i % 3, # 0, 1, 2, 0, 1, 2, ...
|
|
169
|
+
published: i <= 20 # First 20 are published
|
|
170
|
+
}
|
|
171
|
+
end)
|
|
172
|
+
|
|
173
|
+
scope({
|
|
174
|
+
published: -> { where(published: true) },
|
|
175
|
+
by_priority: ->(p) { where(priority: p) }
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
item({
|
|
179
|
+
slug: -> { "post-#{self[:id]}" },
|
|
180
|
+
excerpt: -> { "#{self[:content][0..20]}..." }
|
|
181
|
+
})
|
|
182
|
+
end
|
|
183
|
+
@query = Hq.query(:paginated_posts)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def teardown
|
|
187
|
+
Hq.reload
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def test_basic_pagination
|
|
191
|
+
pages = @query.paginate(per_page: 5)
|
|
192
|
+
|
|
193
|
+
# Should have 5 pages (23 items / 5 per page = 4.6, rounded up to 5)
|
|
194
|
+
assert_equal 5, pages.length
|
|
195
|
+
|
|
196
|
+
# Test first page
|
|
197
|
+
first_page = pages.first
|
|
198
|
+
assert_instance_of Hq::Page, first_page
|
|
199
|
+
assert_equal 5, first_page.items.size
|
|
200
|
+
assert_equal 1, first_page.current_page
|
|
201
|
+
assert_equal 5, first_page.total_pages
|
|
202
|
+
assert_equal 23, first_page.total_items
|
|
203
|
+
assert_nil first_page.prev_page
|
|
204
|
+
assert_equal 2, first_page.next_page
|
|
205
|
+
assert first_page.is_first_page
|
|
206
|
+
refute first_page.is_last_page
|
|
207
|
+
|
|
208
|
+
# Test middle page
|
|
209
|
+
middle_page = pages[2] # Page 3
|
|
210
|
+
assert_equal 5, middle_page.items.size
|
|
211
|
+
assert_equal 3, middle_page.current_page
|
|
212
|
+
assert_equal 2, middle_page.prev_page
|
|
213
|
+
assert_equal 4, middle_page.next_page
|
|
214
|
+
refute middle_page.is_first_page
|
|
215
|
+
refute middle_page.is_last_page
|
|
216
|
+
|
|
217
|
+
# Test last page (should have only 3 items: 21, 22, 23)
|
|
218
|
+
last_page = pages.last
|
|
219
|
+
assert_equal 3, last_page.items.size
|
|
220
|
+
assert_equal 5, last_page.current_page
|
|
221
|
+
assert_equal 4, last_page.prev_page
|
|
222
|
+
assert_nil last_page.next_page
|
|
223
|
+
refute last_page.is_first_page
|
|
224
|
+
assert last_page.is_last_page
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def test_pagination_with_chaining
|
|
228
|
+
# Test pagination with where conditions
|
|
229
|
+
published_pages = @query.published.paginate(per_page: 8)
|
|
230
|
+
|
|
231
|
+
# Should have 3 pages (20 published items / 8 per page = 2.5, rounded up to 3)
|
|
232
|
+
assert_equal 3, published_pages.length
|
|
233
|
+
assert_equal 20, published_pages.first.total_items
|
|
234
|
+
|
|
235
|
+
# Debug the descending order issue
|
|
236
|
+
ordered_query = @query.order(:id, desc: true)
|
|
237
|
+
|
|
238
|
+
ordered_pages = ordered_query.paginate(per_page: 6)
|
|
239
|
+
first_page_items = ordered_pages.first.items
|
|
240
|
+
expected_ids = [23, 22, 21, 20, 19, 18]
|
|
241
|
+
assert_equal expected_ids, first_page_items.map { |item| item[:id] }
|
|
242
|
+
|
|
243
|
+
# Test pagination with scopes
|
|
244
|
+
priority_pages = @query.by_priority(1).paginate(per_page: 3)
|
|
245
|
+
# Priority 1 items: 1, 4, 7, 10, 13, 16, 19, 22 (8 items total)
|
|
246
|
+
assert_equal 3, priority_pages.length # 8 items / 3 per page = 2.67, rounded up to 3
|
|
247
|
+
assert_equal 8, priority_pages.first.total_items
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def test_single_page_collection
|
|
251
|
+
# Apply limit first, then paginate the limited results
|
|
252
|
+
limited_query = @query.limit(5)
|
|
253
|
+
small_pages = limited_query.paginate(per_page: 10)
|
|
254
|
+
|
|
255
|
+
assert_equal 1, small_pages.length
|
|
256
|
+
page = small_pages.first
|
|
257
|
+
assert_equal 5, page.items.size
|
|
258
|
+
assert_equal 1, page.current_page
|
|
259
|
+
assert_equal 1, page.total_pages
|
|
260
|
+
assert_equal 5, page.total_items
|
|
261
|
+
assert_nil page.prev_page
|
|
262
|
+
assert_nil page.next_page
|
|
263
|
+
assert page.is_first_page
|
|
264
|
+
assert page.is_last_page
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def test_empty_collection_pagination
|
|
268
|
+
empty_pages = @query.where(id: 999).paginate(per_page: 5)
|
|
269
|
+
|
|
270
|
+
assert_equal 1, empty_pages.length
|
|
271
|
+
page = empty_pages.first
|
|
272
|
+
assert_equal [], page.items
|
|
273
|
+
assert_equal 1, page.current_page
|
|
274
|
+
assert_equal 1, page.total_pages
|
|
275
|
+
assert_equal 0, page.total_items
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def test_pagination_error_handling
|
|
279
|
+
# Test with invalid per_page values
|
|
280
|
+
error = assert_raises(ArgumentError) { @query.paginate(per_page: 0) }
|
|
281
|
+
assert_match /per_page must be positive/, error.message
|
|
282
|
+
|
|
283
|
+
error = assert_raises(ArgumentError) { @query.paginate(per_page: -5) }
|
|
284
|
+
assert_match /per_page must be positive/, error.message
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def test_page_object_methods
|
|
288
|
+
pages = @query.paginate(per_page: 7)
|
|
289
|
+
page = pages[1] # Second page
|
|
290
|
+
|
|
291
|
+
# Test item methods work on paginated items
|
|
292
|
+
first_item = page.items.first
|
|
293
|
+
assert_equal "post-#{first_item[:id]}", first_item.slug
|
|
294
|
+
assert_match /Content for post \d+\.\.\./, first_item.excerpt
|
|
295
|
+
|
|
296
|
+
# Test posts alias
|
|
297
|
+
assert_equal page.items, page.posts
|
|
298
|
+
|
|
299
|
+
# Test to_h method
|
|
300
|
+
page_hash = page.to_h
|
|
301
|
+
expected_keys = [:items, :current_page, :total_pages, :total_items,
|
|
302
|
+
:prev_page, :next_page, :is_first_page, :is_last_page]
|
|
303
|
+
assert_equal expected_keys.sort, page_hash.keys.sort
|
|
304
|
+
assert_equal page.current_page, page_hash[:current_page]
|
|
305
|
+
assert_equal page.total_items, page_hash[:total_items]
|
|
306
|
+
assert_equal page.is_first_page, page_hash[:is_first_page]
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def test_pagination_maintains_order
|
|
310
|
+
# Test that pagination preserves ordering - but our implementation only supports single field sorting
|
|
311
|
+
ordered_pages = @query.order(:id).paginate(per_page: 6)
|
|
312
|
+
|
|
313
|
+
all_items = []
|
|
314
|
+
ordered_pages.each { |page| all_items.concat(page.items) }
|
|
315
|
+
|
|
316
|
+
# Should maintain ID ordering across pages (1,2,3,4,5,6,7,8,9,10...)
|
|
317
|
+
ids = all_items.map { |item| item[:id] }
|
|
318
|
+
assert_equal (1..23).to_a, ids
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def test_pagination_edge_cases
|
|
322
|
+
# Test with per_page exactly equal to total items
|
|
323
|
+
exact_pages = @query.limit(10).paginate(per_page: 10)
|
|
324
|
+
assert_equal 1, exact_pages.length
|
|
325
|
+
assert_equal 10, exact_pages.first.items.size
|
|
326
|
+
|
|
327
|
+
# Test with per_page larger than total items - limit to 3 items first
|
|
328
|
+
limited_query = @query.limit(3)
|
|
329
|
+
large_pages = limited_query.paginate(per_page: 10)
|
|
330
|
+
assert_equal 1, large_pages.length
|
|
331
|
+
assert_equal 3, large_pages.first.items.size
|
|
332
|
+
assert_equal 3, large_pages.first.total_items # Total should also be 3
|
|
333
|
+
|
|
334
|
+
# Test with per_page = 1 (maximum pages) - limit to 5 items first
|
|
335
|
+
limited_query_5 = @query.limit(5)
|
|
336
|
+
single_pages = limited_query_5.paginate(per_page: 1)
|
|
337
|
+
assert_equal 5, single_pages.length
|
|
338
|
+
assert_equal 5, single_pages.first.total_items # Total should be 5
|
|
339
|
+
single_pages.each_with_index do |page, index|
|
|
340
|
+
assert_equal 1, page.items.size
|
|
341
|
+
assert_equal index + 1, page.current_page
|
|
342
|
+
assert_equal 5, page.total_pages
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
# File: ./edge_test.rb
|
|
351
|
+
|
|
352
|
+
require_relative 'test_helper'
|
|
353
|
+
class TestEdgeCases < Minitest::Test
|
|
354
|
+
def setup
|
|
355
|
+
Hq.reload
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def teardown
|
|
359
|
+
Hq.reload
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def test_empty_collections
|
|
363
|
+
Hq.define :empty do
|
|
364
|
+
from_array([])
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
query = Hq.query(:empty)
|
|
368
|
+
assert_equal 0, query.size
|
|
369
|
+
assert_equal [], query.to_a
|
|
370
|
+
assert_nil query.first
|
|
371
|
+
assert_nil query.last
|
|
372
|
+
assert_equal [], query.where(id: 1).to_a
|
|
373
|
+
assert_equal [], query.order(:id).to_a
|
|
374
|
+
assert_equal [], query.limit(5).to_a
|
|
375
|
+
assert_equal [], query.offset(2).to_a
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def test_single_item_collection
|
|
379
|
+
Hq.define :single do
|
|
380
|
+
from_array([{ id: 1, title: 'Only One' }])
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
query = Hq.query(:single)
|
|
384
|
+
assert_equal 1, query.count
|
|
385
|
+
assert_equal 1, query.first[:id]
|
|
386
|
+
assert_equal 1, query.last[:id]
|
|
387
|
+
assert_equal [1], query.all.map { |item| item[:id] }
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def test_collections_with_duplicate_data
|
|
391
|
+
Hq.define :duplicates do
|
|
392
|
+
from_array([
|
|
393
|
+
{ id: 1, name: 'John' },
|
|
394
|
+
{ id: 2, name: 'John' },
|
|
395
|
+
{ id: 3, name: 'John' }
|
|
396
|
+
])
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
query = Hq.query(:duplicates)
|
|
400
|
+
johns = query.where(name: 'John')
|
|
401
|
+
assert_equal 3, johns.size
|
|
402
|
+
assert_equal [1, 2, 3], johns.map { |item| item[:id] }.sort
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def test_collections_with_all_nils
|
|
406
|
+
Hq.define :nils do
|
|
407
|
+
from_array([
|
|
408
|
+
{ id: 1, value: nil },
|
|
409
|
+
{ id: 2, value: nil },
|
|
410
|
+
{ id: 3, value: nil }
|
|
411
|
+
])
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
query = Hq.query(:nils)
|
|
415
|
+
results = query.where(value: { exists: false })
|
|
416
|
+
assert_equal 3, results.size
|
|
417
|
+
|
|
418
|
+
results = query.where(value: { empty: true })
|
|
419
|
+
assert_equal 3, results.size
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def test_large_offset_and_limit
|
|
423
|
+
Hq.define :numbers do
|
|
424
|
+
from_array((1..100).map { |n| { id: n, value: n * 2 } })
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
query = Hq.query(:numbers)
|
|
428
|
+
|
|
429
|
+
# Test large offset beyond collection size
|
|
430
|
+
results = query.offset(200)
|
|
431
|
+
assert_equal [], results.to_a
|
|
432
|
+
|
|
433
|
+
# Test large limit on small remaining items
|
|
434
|
+
results = query.offset(95).limit(10)
|
|
435
|
+
assert_equal 5, results.size # Only 5 items left after offset 95
|
|
436
|
+
|
|
437
|
+
# Test zero limit
|
|
438
|
+
results = query.limit(0)
|
|
439
|
+
assert_equal [], results.to_a
|
|
440
|
+
|
|
441
|
+
# Test negative offset (should be treated as 0)
|
|
442
|
+
results = query.offset(-5).limit(3)
|
|
443
|
+
assert_equal 3, results.size
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def test_complex_chaining_with_no_results
|
|
447
|
+
Hq.define :complex do
|
|
448
|
+
from_array([
|
|
449
|
+
{ id: 1, category: 'A', published: true, views: 100 },
|
|
450
|
+
{ id: 2, category: 'B', published: false, views: 200 }
|
|
451
|
+
])
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
query = Hq.query(:complex)
|
|
455
|
+
|
|
456
|
+
# Chain filters that result in no matches
|
|
457
|
+
results = query.where(category: 'A').where(published: false)
|
|
458
|
+
assert_equal [], results.to_a
|
|
459
|
+
|
|
460
|
+
results = query.where(views: { greater_than: 300 })
|
|
461
|
+
assert_equal [], results.to_a
|
|
462
|
+
|
|
463
|
+
results = query.where(id: [10, 20, 30])
|
|
464
|
+
assert_equal [], results.to_a
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
def test_scope_with_parameters_edge_cases
|
|
468
|
+
Hq.define :parameterized do
|
|
469
|
+
from_array([
|
|
470
|
+
{ id: 1, score: 85 },
|
|
471
|
+
{ id: 2, score: 92 },
|
|
472
|
+
{ id: 3, score: 78 },
|
|
473
|
+
{ id: 4, score: 95 }
|
|
474
|
+
])
|
|
475
|
+
|
|
476
|
+
scope({
|
|
477
|
+
by_score: ->(min_score) { where(score: { greater_than_or_equal: min_score }) },
|
|
478
|
+
top_n: ->(n) { order(:score, desc: true).limit(n) },
|
|
479
|
+
score_range: ->(min, max) { where(score: { from: min, to: max }) }
|
|
480
|
+
})
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
query = Hq.query(:parameterized)
|
|
484
|
+
|
|
485
|
+
results = query.by_score(100)
|
|
486
|
+
assert_equal [], results.to_a
|
|
487
|
+
|
|
488
|
+
results = query.by_score(0)
|
|
489
|
+
assert_equal 4, results.size
|
|
490
|
+
|
|
491
|
+
results = query.top_n(0)
|
|
492
|
+
assert_equal [], results.to_a
|
|
493
|
+
|
|
494
|
+
results = query.score_range(90, 100)
|
|
495
|
+
assert_equal [2, 4].sort, results.map { |item| item[:id] }.sort # <-- Corrected
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
def test_method_name_conflicts
|
|
499
|
+
Hq.define :conflicts do
|
|
500
|
+
from_array([{ id: 1, hash: 'test_hash', class: 'test_class' }])
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
query = Hq.query(:conflicts)
|
|
504
|
+
item = query.first
|
|
505
|
+
|
|
506
|
+
# Access conflicting keys via [] - they should NOT have method access
|
|
507
|
+
assert_equal 'test_hash', item[:hash]
|
|
508
|
+
assert_equal 'test_class', item[:class]
|
|
509
|
+
|
|
510
|
+
# Check that conflicting keys do NOT respond to method calls
|
|
511
|
+
# These should be FALSE because we skip defining methods for conflicting keys
|
|
512
|
+
assert item.respond_to?(:hash) # Should be false - we don't define this method
|
|
513
|
+
assert item.respond_to?(:class) # Should be false - we don't define this method
|
|
514
|
+
|
|
515
|
+
# Check built-in methods still work
|
|
516
|
+
assert item.object_id.is_a?(Integer)
|
|
517
|
+
assert item.hash.is_a?(Integer) # Built-in hash method works
|
|
518
|
+
assert_equal Hq::InstanceWrapper, item.class # Built-in class method works
|
|
519
|
+
|
|
520
|
+
# Verify we can't access the hash values as methods (should raise NoMethodError)
|
|
521
|
+
# But since respond_to? returns false, we don't need to test this
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def test_ordering_with_mixed_types
|
|
527
|
+
# Test ordering when values are of different types (should handle gracefully)
|
|
528
|
+
Hq.define :mixed do
|
|
529
|
+
from_array([
|
|
530
|
+
{ id: 1, value: 'string' },
|
|
531
|
+
{ id: 2, value: 42 },
|
|
532
|
+
{ id: 3, value: nil },
|
|
533
|
+
{ id: 4, value: Date.new(2024, 1, 1) }
|
|
534
|
+
])
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
query = Hq.query(:mixed)
|
|
538
|
+
|
|
539
|
+
# Should not raise an error, even with mixed types
|
|
540
|
+
results = query.order(:value)
|
|
541
|
+
assert_equal 4, results.size
|
|
542
|
+
|
|
543
|
+
# nil should be at the end
|
|
544
|
+
assert_equal 3, results.last[:id]
|
|
545
|
+
end
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
# File: ./instance_test.rb
|
|
550
|
+
|
|
551
|
+
require_relative 'test_helper'
|
|
552
|
+
|
|
553
|
+
module Hq
|
|
554
|
+
class TestInstanceWrapper < Minitest::Test
|
|
555
|
+
def setup
|
|
556
|
+
@hash = { id: 1, title: 'Test', content: 'Long content here for testing purpose', views: 100, published_at: Date.new(2024, 1, 1) }.freeze
|
|
557
|
+
|
|
558
|
+
Hq.present :test_model do
|
|
559
|
+
def excerpt(words = 2)
|
|
560
|
+
self[:content].split.take(words).join(' ') + '...'
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
def title_upper
|
|
564
|
+
self[:title].upcase
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
def formatted_date
|
|
568
|
+
self[:published_at]&.strftime("%B %d, %Y")
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
def view_category
|
|
572
|
+
self[:views] && self[:views] > 150 ? 'popular' : 'standard'
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
def word_count
|
|
576
|
+
self[:content].split.size
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
def summary(max_words = 10)
|
|
580
|
+
words = self[:content].split.take(max_words)
|
|
581
|
+
"#{words.join(' ')}#{words.size >= max_words ? '...' : ''}"
|
|
582
|
+
end
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
@wrapper = Hq::InstanceWrapper.new(@hash, :test_model)
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
def teardown
|
|
589
|
+
Hq.reload
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
def test_dot_notation_access_for_hash_keys
|
|
593
|
+
assert_equal @hash[:id], @wrapper.id
|
|
594
|
+
assert_equal @hash[:title], @wrapper.title
|
|
595
|
+
assert_equal @hash[:content], @wrapper.content
|
|
596
|
+
assert_equal @hash[:views], @wrapper.views
|
|
597
|
+
assert_equal @hash[:published_at], @wrapper.published_at
|
|
598
|
+
assert @wrapper.respond_to?(:id)
|
|
599
|
+
assert @wrapper.respond_to?(:title)
|
|
600
|
+
refute @wrapper.respond_to?(:some_random_method_that_does_not_exist)
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
def test_dot_notation_access_for_presenter_methods
|
|
604
|
+
assert_equal 'Long content...', @wrapper.excerpt
|
|
605
|
+
assert_equal 'Long...', @wrapper.excerpt(1)
|
|
606
|
+
assert_equal 'TEST', @wrapper.title_upper
|
|
607
|
+
assert @wrapper.respond_to?(:excerpt)
|
|
608
|
+
assert @wrapper.respond_to?(:title_upper)
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
def test_hash_access
|
|
612
|
+
assert_equal 1, @wrapper[:id]
|
|
613
|
+
assert_equal 'Test', @wrapper[:title]
|
|
614
|
+
assert_equal 'Test', @wrapper['title']
|
|
615
|
+
assert_equal 100, @wrapper[:views]
|
|
616
|
+
assert_nil @wrapper[:non_existent]
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
def test_symbol_string_key_normalization
|
|
620
|
+
assert_equal 'Test', @wrapper[:title]
|
|
621
|
+
assert_equal 'Test', @wrapper['title']
|
|
622
|
+
assert_equal 1, @wrapper[:id]
|
|
623
|
+
assert_equal 1, @wrapper['id']
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
def test_immutability
|
|
627
|
+
error = assert_raises(RuntimeError) { @wrapper[:id] = 2 }
|
|
628
|
+
assert_match /immutable/, error.message
|
|
629
|
+
error = assert_raises(RuntimeError) { @wrapper['title'] = 'New Title' }
|
|
630
|
+
assert_match /immutable/, error.message
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
def test_presenter_methods
|
|
634
|
+
assert_equal 'Long content...', @wrapper.excerpt
|
|
635
|
+
assert_equal 'Long...', @wrapper.excerpt(1)
|
|
636
|
+
assert_equal 'Long content here...', @wrapper.excerpt(3)
|
|
637
|
+
assert_equal 'TEST', @wrapper.title_upper
|
|
638
|
+
assert_equal 'January 01, 2024', @wrapper.formatted_date
|
|
639
|
+
assert_equal 'standard', @wrapper.view_category
|
|
640
|
+
assert_equal 6, @wrapper.word_count
|
|
641
|
+
end
|
|
642
|
+
|
|
643
|
+
def test_presenter_methods_with_parameters
|
|
644
|
+
assert_equal 'Long content...', @wrapper.excerpt(2)
|
|
645
|
+
assert_equal 'Long content here for...', @wrapper.excerpt(4)
|
|
646
|
+
assert_equal 'Long content here for testing...', @wrapper.summary(5)
|
|
647
|
+
assert_equal 'Long content here for testing purpose', @wrapper.summary(10)
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
def test_memoization
|
|
651
|
+
result1 = @wrapper.excerpt
|
|
652
|
+
result2 = @wrapper.excerpt
|
|
653
|
+
assert_equal result1, result2
|
|
654
|
+
assert_equal 'Long content...', result1
|
|
655
|
+
result3 = @wrapper.excerpt(1)
|
|
656
|
+
assert_equal 'Long...', result3
|
|
657
|
+
refute_equal result1, result3
|
|
658
|
+
result4 = @wrapper.excerpt(1)
|
|
659
|
+
assert_equal result3, result4
|
|
660
|
+
date1 = @wrapper.formatted_date
|
|
661
|
+
date2 = @wrapper.formatted_date
|
|
662
|
+
assert_equal date1, date2
|
|
663
|
+
assert_equal 'January 01, 2024', date1
|
|
664
|
+
end
|
|
665
|
+
|
|
666
|
+
def test_presenter_method_with_nil_handling
|
|
667
|
+
nil_hash = { id: 1, title: nil, content: nil, published_at: nil }.freeze
|
|
668
|
+
|
|
669
|
+
Hq.present :nil_test_model do
|
|
670
|
+
def safe_title
|
|
671
|
+
self[:title] || 'Untitled'
|
|
672
|
+
end
|
|
673
|
+
|
|
674
|
+
def safe_date
|
|
675
|
+
self[:published_at]&.strftime("%Y-%m-%d") || 'No date'
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
def content_length
|
|
679
|
+
(self[:content] || '').length
|
|
680
|
+
end
|
|
681
|
+
end
|
|
682
|
+
|
|
683
|
+
wrapper = InstanceWrapper.new(nil_hash, :nil_test_model)
|
|
684
|
+
assert_equal 'Untitled', wrapper.safe_title
|
|
685
|
+
assert_equal 'No date', wrapper.safe_date
|
|
686
|
+
assert_equal 0, wrapper.content_length
|
|
687
|
+
end
|
|
688
|
+
|
|
689
|
+
def test_presenter_method_error
|
|
690
|
+
Hq.present :error_test_model do
|
|
691
|
+
def bad
|
|
692
|
+
raise 'boom'
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
def another_bad
|
|
696
|
+
undefined_method_call
|
|
697
|
+
end
|
|
698
|
+
end
|
|
699
|
+
|
|
700
|
+
wrapper = Hq::InstanceWrapper.new(@hash, :error_test_model)
|
|
701
|
+
error = assert_raises(Hq::ItemMethodError) { wrapper.bad }
|
|
702
|
+
assert_match /Error executing presenter method :bad.*boom/, error.message
|
|
703
|
+
error = assert_raises(Hq::ItemMethodError) { wrapper.another_bad }
|
|
704
|
+
assert_match /Error executing presenter method :another_bad/, error.message
|
|
705
|
+
end
|
|
706
|
+
|
|
707
|
+
def test_hash_methods
|
|
708
|
+
expected_keys = [:id, :title, :content, :views, :published_at]
|
|
709
|
+
assert_equal expected_keys.sort, @wrapper.to_h.keys.sort
|
|
710
|
+
assert_equal @hash.values.size, @wrapper.to_h.values.size
|
|
711
|
+
assert_equal @hash, @wrapper.to_h
|
|
712
|
+
assert_equal @hash, @wrapper.to_hash
|
|
713
|
+
end
|
|
714
|
+
|
|
715
|
+
def test_method_missing_for_hash
|
|
716
|
+
assert_equal 1, @wrapper[:id]
|
|
717
|
+
assert_equal 'Test', @wrapper[:title]
|
|
718
|
+
assert_equal 5, @wrapper.to_h.size
|
|
719
|
+
assert @wrapper.respond_to?(:id)
|
|
720
|
+
assert_equal 1, @wrapper.id
|
|
721
|
+
assert @wrapper.to_h.has_key?(:id)
|
|
722
|
+
assert @wrapper.to_h.has_key?(:title)
|
|
723
|
+
refute @wrapper.to_h.has_key?(:non_existent)
|
|
724
|
+
assert @wrapper.to_h.include?(:id)
|
|
725
|
+
assert_equal @hash.length, @wrapper.to_h.length
|
|
726
|
+
assert_equal false, @wrapper.to_h.empty?
|
|
727
|
+
assert @wrapper.respond_to?(:to_h)
|
|
728
|
+
assert @wrapper.respond_to?(:[])
|
|
729
|
+
end
|
|
730
|
+
|
|
731
|
+
def test_respond_to_for_dynamic_methods
|
|
732
|
+
assert @wrapper.respond_to?(:id)
|
|
733
|
+
assert @wrapper.respond_to?(:title)
|
|
734
|
+
assert @wrapper.respond_to?(:excerpt)
|
|
735
|
+
assert @wrapper.respond_to?(:title_upper)
|
|
736
|
+
assert @wrapper.respond_to?(:formatted_date)
|
|
737
|
+
refute @wrapper.to_h.has_key?(:non_existent)
|
|
738
|
+
assert @wrapper.respond_to?(:class)
|
|
739
|
+
assert @wrapper.respond_to?(:object_id)
|
|
740
|
+
assert @wrapper.respond_to?(:to_h)
|
|
741
|
+
refute @wrapper.respond_to?(:keys)
|
|
742
|
+
refute @wrapper.respond_to?(:values)
|
|
743
|
+
refute @wrapper.respond_to?(:size)
|
|
744
|
+
end
|
|
745
|
+
|
|
746
|
+
def test_inspect
|
|
747
|
+
assert_match /#<Hq::Instance {.*}>/, @wrapper.inspect
|
|
748
|
+
assert_match /id.*1/, @wrapper.inspect
|
|
749
|
+
assert_match /title.*"Test"/, @wrapper.inspect
|
|
750
|
+
end
|
|
751
|
+
|
|
752
|
+
def test_complex_presenter_methods
|
|
753
|
+
complex_hash = {
|
|
754
|
+
tags: ['ruby', 'web', 'programming'],
|
|
755
|
+
metadata: { author: 'John', category: 'tech' },
|
|
756
|
+
scores: [85, 90, 78, 92]
|
|
757
|
+
}.freeze
|
|
758
|
+
|
|
759
|
+
Hq.present :complex_test_model do
|
|
760
|
+
def tag_list
|
|
761
|
+
self[:tags].join(', ')
|
|
762
|
+
end
|
|
763
|
+
|
|
764
|
+
def author_name
|
|
765
|
+
self[:metadata][:author]
|
|
766
|
+
end
|
|
767
|
+
|
|
768
|
+
def average_score
|
|
769
|
+
self[:scores].sum.to_f / self[:scores].length
|
|
770
|
+
end
|
|
771
|
+
|
|
772
|
+
def top_score
|
|
773
|
+
self[:scores].max
|
|
774
|
+
end
|
|
775
|
+
|
|
776
|
+
def tag_count
|
|
777
|
+
self[:tags].length
|
|
778
|
+
end
|
|
779
|
+
end
|
|
780
|
+
|
|
781
|
+
wrapper = InstanceWrapper.new(complex_hash, :complex_test_model)
|
|
782
|
+
assert_equal 'ruby, web, programming', wrapper.tag_list
|
|
783
|
+
assert_equal 'John', wrapper.author_name
|
|
784
|
+
assert_equal 86.25, wrapper.average_score
|
|
785
|
+
assert_equal 92, wrapper.top_score
|
|
786
|
+
assert_equal 3, wrapper.tag_count
|
|
787
|
+
end
|
|
788
|
+
end
|
|
789
|
+
end
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
# File: ./paginate_test.rb
|
|
793
|
+
|
|
794
|
+
require_relative 'test_helper'
|
|
795
|
+
|
|
796
|
+
class TestPagination < Minitest::Test
|
|
797
|
+
def setup
|
|
798
|
+
Hq.reload
|
|
799
|
+
Hq.define :paginated_posts do
|
|
800
|
+
from_array((1..23).map do |i|
|
|
801
|
+
{
|
|
802
|
+
id: i,
|
|
803
|
+
title: "Post #{i}",
|
|
804
|
+
content: "Content for post #{i}",
|
|
805
|
+
priority: i % 3,
|
|
806
|
+
published: i <= 20
|
|
807
|
+
}
|
|
808
|
+
end)
|
|
809
|
+
|
|
810
|
+
scope({
|
|
811
|
+
published: -> { where(published: true) },
|
|
812
|
+
by_priority: ->(p) { where(priority: p) }
|
|
813
|
+
})
|
|
814
|
+
end
|
|
815
|
+
|
|
816
|
+
Hq.present :paginated_posts do
|
|
817
|
+
def slug
|
|
818
|
+
"post-#{self[:id]}"
|
|
819
|
+
end
|
|
820
|
+
|
|
821
|
+
def excerpt
|
|
822
|
+
"#{self[:content][0..20]}..."
|
|
823
|
+
end
|
|
824
|
+
end
|
|
825
|
+
|
|
826
|
+
@query = Hq.query(:paginated_posts)
|
|
827
|
+
end
|
|
828
|
+
|
|
829
|
+
def teardown
|
|
830
|
+
Hq.reload
|
|
831
|
+
end
|
|
832
|
+
|
|
833
|
+
def test_basic_pagination
|
|
834
|
+
pages = @query.paginate(per_page: 5)
|
|
835
|
+
|
|
836
|
+
assert_equal 5, pages.length
|
|
837
|
+
|
|
838
|
+
first_page = pages.first
|
|
839
|
+
assert_instance_of Hq::Page, first_page
|
|
840
|
+
assert_equal 5, first_page.items.size
|
|
841
|
+
assert_equal 1, first_page.current_page
|
|
842
|
+
assert_equal 5, first_page.total_pages
|
|
843
|
+
assert_equal 23, first_page.total_items
|
|
844
|
+
assert_nil first_page.prev_page
|
|
845
|
+
assert_equal 2, first_page.next_page
|
|
846
|
+
assert first_page.is_first_page
|
|
847
|
+
refute first_page.is_last_page
|
|
848
|
+
|
|
849
|
+
middle_page = pages[2]
|
|
850
|
+
assert_equal 5, middle_page.items.size
|
|
851
|
+
assert_equal 3, middle_page.current_page
|
|
852
|
+
assert_equal 2, middle_page.prev_page
|
|
853
|
+
assert_equal 4, middle_page.next_page
|
|
854
|
+
refute middle_page.is_first_page
|
|
855
|
+
refute middle_page.is_last_page
|
|
856
|
+
|
|
857
|
+
last_page = pages.last
|
|
858
|
+
assert_equal 3, last_page.items.size
|
|
859
|
+
assert_equal 5, last_page.current_page
|
|
860
|
+
assert_equal 4, last_page.prev_page
|
|
861
|
+
assert_nil last_page.next_page
|
|
862
|
+
refute last_page.is_first_page
|
|
863
|
+
assert last_page.is_last_page
|
|
864
|
+
end
|
|
865
|
+
|
|
866
|
+
def test_pagination_with_chaining
|
|
867
|
+
published_pages = @query.published.paginate(per_page: 8)
|
|
868
|
+
|
|
869
|
+
assert_equal 3, published_pages.length
|
|
870
|
+
assert_equal 20, published_pages.first.total_items
|
|
871
|
+
|
|
872
|
+
ordered_query = @query.order(:id, desc: true)
|
|
873
|
+
|
|
874
|
+
ordered_pages = ordered_query.paginate(per_page: 6)
|
|
875
|
+
first_page_items = ordered_pages.first.items
|
|
876
|
+
expected_ids = [23, 22, 21, 20, 19, 18]
|
|
877
|
+
assert_equal expected_ids, first_page_items.map { |item| item[:id] }
|
|
878
|
+
|
|
879
|
+
priority_pages = @query.by_priority(1).paginate(per_page: 3)
|
|
880
|
+
assert_equal 3, priority_pages.length
|
|
881
|
+
assert_equal 8, priority_pages.first.total_items
|
|
882
|
+
end
|
|
883
|
+
|
|
884
|
+
def test_single_page_collection
|
|
885
|
+
limited_query = @query.limit(5)
|
|
886
|
+
small_pages = limited_query.paginate(per_page: 10)
|
|
887
|
+
|
|
888
|
+
assert_equal 1, small_pages.length
|
|
889
|
+
page = small_pages.first
|
|
890
|
+
assert_equal 5, page.items.size
|
|
891
|
+
assert_equal 1, page.current_page
|
|
892
|
+
assert_equal 1, page.total_pages
|
|
893
|
+
assert_equal 5, page.total_items
|
|
894
|
+
assert_nil page.prev_page
|
|
895
|
+
assert_nil page.next_page
|
|
896
|
+
assert page.is_first_page
|
|
897
|
+
assert page.is_last_page
|
|
898
|
+
end
|
|
899
|
+
|
|
900
|
+
def test_empty_collection_pagination
|
|
901
|
+
empty_pages = @query.where(id: 999).paginate(per_page: 5)
|
|
902
|
+
|
|
903
|
+
assert_equal 1, empty_pages.length
|
|
904
|
+
page = empty_pages.first
|
|
905
|
+
assert_equal [], page.items
|
|
906
|
+
assert_equal 1, page.current_page
|
|
907
|
+
assert_equal 1, page.total_pages
|
|
908
|
+
assert_equal 0, page.total_items
|
|
909
|
+
end
|
|
910
|
+
|
|
911
|
+
def test_pagination_error_handling
|
|
912
|
+
error = assert_raises(ArgumentError) { @query.paginate(per_page: 0) }
|
|
913
|
+
assert_match /per_page must be positive/, error.message
|
|
914
|
+
|
|
915
|
+
error = assert_raises(ArgumentError) { @query.paginate(per_page: -5) }
|
|
916
|
+
assert_match /per_page must be positive/, error.message
|
|
917
|
+
end
|
|
918
|
+
|
|
919
|
+
def test_page_object_methods
|
|
920
|
+
pages = @query.paginate(per_page: 7)
|
|
921
|
+
page = pages[1]
|
|
922
|
+
|
|
923
|
+
first_item = page.items.first
|
|
924
|
+
assert_equal "post-#{first_item[:id]}", first_item.slug
|
|
925
|
+
assert_match /Content for post \d+\.\.\./, first_item.excerpt
|
|
926
|
+
|
|
927
|
+
assert_equal page.items, page.posts
|
|
928
|
+
|
|
929
|
+
page_hash = page.to_h
|
|
930
|
+
expected_keys = [:items, :current_page, :total_pages, :total_items,
|
|
931
|
+
:prev_page, :next_page, :is_first_page, :is_last_page]
|
|
932
|
+
assert_equal expected_keys.sort, page_hash.keys.sort
|
|
933
|
+
assert_equal page.current_page, page_hash[:current_page]
|
|
934
|
+
assert_equal page.total_items, page_hash[:total_items]
|
|
935
|
+
assert_equal page.is_first_page, page_hash[:is_first_page]
|
|
936
|
+
end
|
|
937
|
+
|
|
938
|
+
def test_pagination_maintains_order
|
|
939
|
+
ordered_pages = @query.order(:id).paginate(per_page: 6)
|
|
940
|
+
|
|
941
|
+
all_items = []
|
|
942
|
+
ordered_pages.each { |page| all_items.concat(page.items) }
|
|
943
|
+
|
|
944
|
+
ids = all_items.map { |item| item[:id] }
|
|
945
|
+
assert_equal (1..23).to_a, ids
|
|
946
|
+
end
|
|
947
|
+
|
|
948
|
+
def test_pagination_edge_cases
|
|
949
|
+
exact_pages = @query.limit(10).paginate(per_page: 10)
|
|
950
|
+
assert_equal 1, exact_pages.length
|
|
951
|
+
assert_equal 10, exact_pages.first.items.size
|
|
952
|
+
|
|
953
|
+
limited_query = @query.limit(3)
|
|
954
|
+
large_pages = limited_query.paginate(per_page: 10)
|
|
955
|
+
assert_equal 1, large_pages.length
|
|
956
|
+
assert_equal 3, large_pages.first.items.size
|
|
957
|
+
assert_equal 3, large_pages.first.total_items
|
|
958
|
+
|
|
959
|
+
limited_query_5 = @query.limit(5)
|
|
960
|
+
single_pages = limited_query_5.paginate(per_page: 1)
|
|
961
|
+
assert_equal 5, single_pages.length
|
|
962
|
+
assert_equal 5, single_pages.first.total_items
|
|
963
|
+
single_pages.each_with_index do |page, index|
|
|
964
|
+
assert_equal 1, page.items.size
|
|
965
|
+
assert_equal index + 1, page.current_page
|
|
966
|
+
assert_equal 5, page.total_pages
|
|
967
|
+
end
|
|
968
|
+
end
|
|
969
|
+
end
|
|
970
|
+
|
|
971
|
+
|
|
972
|
+
# File: ./query_test.rb
|
|
973
|
+
|
|
974
|
+
require_relative 'test_helper'
|
|
975
|
+
|
|
976
|
+
module Hq
|
|
977
|
+
class TestQuery < Minitest::Test
|
|
978
|
+
def setup
|
|
979
|
+
Hq.reload
|
|
980
|
+
Hq.define :posts do
|
|
981
|
+
from_array([
|
|
982
|
+
{ id: 1, title: 'Post 1', published_at: Date.new(2024, 1, 1), featured: true, tags: ['ruby', 'web'], views: 100 },
|
|
983
|
+
{ id: 2, title: 'Post 2', published_at: Date.new(2024, 2, 1), featured: false, tags: ['tools'], views: 200 },
|
|
984
|
+
{ id: 3, title: 'Post 3', published_at: Date.new(2023, 12, 1), featured: true, tags: ['ruby'], views: 150 },
|
|
985
|
+
{ id: 4, title: nil, published_at: nil, featured: nil, tags: [], views: nil }
|
|
986
|
+
])
|
|
987
|
+
|
|
988
|
+
scope({
|
|
989
|
+
featured: -> { where(featured: true) },
|
|
990
|
+
recent: ->(n=2) { order(:published_at, desc: true).limit(n) },
|
|
991
|
+
tagged: ->(tag) { where(tags: { contains: tag }) }
|
|
992
|
+
})
|
|
993
|
+
end
|
|
994
|
+
|
|
995
|
+
Hq.present :posts do
|
|
996
|
+
def title_upper
|
|
997
|
+
self[:title]&.upcase
|
|
998
|
+
end
|
|
999
|
+
|
|
1000
|
+
def view_category
|
|
1001
|
+
self[:views] && self[:views] > 150 ? 'popular' : 'standard'
|
|
1002
|
+
end
|
|
1003
|
+
end
|
|
1004
|
+
|
|
1005
|
+
@query = Hq.query(:posts)
|
|
1006
|
+
end
|
|
1007
|
+
|
|
1008
|
+
def teardown
|
|
1009
|
+
Hq.reload
|
|
1010
|
+
end
|
|
1011
|
+
|
|
1012
|
+
def test_all
|
|
1013
|
+
assert_equal 4, @query.all.size
|
|
1014
|
+
assert @query.all.all? { |item| item.is_a?(InstanceWrapper) }
|
|
1015
|
+
end
|
|
1016
|
+
|
|
1017
|
+
def test_first_and_last
|
|
1018
|
+
assert_equal 1, @query.first[:id]
|
|
1019
|
+
assert_equal 4, @query.last[:id]
|
|
1020
|
+
end
|
|
1021
|
+
|
|
1022
|
+
def test_count
|
|
1023
|
+
assert_equal 4, @query.count
|
|
1024
|
+
end
|
|
1025
|
+
|
|
1026
|
+
def test_find_by
|
|
1027
|
+
post = @query.find_by(id: 2)
|
|
1028
|
+
assert_equal 'Post 2', post[:title]
|
|
1029
|
+
end
|
|
1030
|
+
|
|
1031
|
+
def test_where_equality
|
|
1032
|
+
query = @query.where(featured: true)
|
|
1033
|
+
assert_equal [1, 3], query.map { |p| p[:id] }.sort
|
|
1034
|
+
end
|
|
1035
|
+
|
|
1036
|
+
def test_where_hash_conditions
|
|
1037
|
+
results = @query.where(title: { contains: 'Post' })
|
|
1038
|
+
assert_equal 3, results.size
|
|
1039
|
+
|
|
1040
|
+
results = @query.where(published_at: { greater_than: Date.new(2024, 1, 1) })
|
|
1041
|
+
assert_equal [2], results.map { |p| p[:id] }
|
|
1042
|
+
|
|
1043
|
+
results = @query.where(published_at: { less_than: Date.new(2024, 1, 1) })
|
|
1044
|
+
assert_equal [3], results.map { |p| p[:id] }
|
|
1045
|
+
|
|
1046
|
+
results = @query.where(published_at: { greater_than_or_equal: Date.new(2024, 1, 1) })
|
|
1047
|
+
assert_equal [1, 2], results.map { |p| p[:id] }.sort
|
|
1048
|
+
|
|
1049
|
+
results = @query.where(published_at: { less_than_or_equal: Date.new(2024, 1, 1) })
|
|
1050
|
+
assert_equal [1, 3], results.map { |p| p[:id] }.sort
|
|
1051
|
+
|
|
1052
|
+
results = @query.where(published_at: { from: Date.new(2024, 1, 1), to: Date.new(2024, 2, 1) })
|
|
1053
|
+
assert_equal [1, 2], results.map { |p| p[:id] }.sort
|
|
1054
|
+
|
|
1055
|
+
results = @query.where(published_at: { from: Date.new(2024, 1, 1) })
|
|
1056
|
+
assert_equal [1, 2], results.map { |p| p[:id] }.sort
|
|
1057
|
+
|
|
1058
|
+
results = @query.where(published_at: { to: Date.new(2024, 1, 1) })
|
|
1059
|
+
assert_equal [1, 3], results.map { |p| p[:id] }.sort
|
|
1060
|
+
|
|
1061
|
+
results = @query.where(id: { in: [1, 3] })
|
|
1062
|
+
assert_equal [1, 3], results.map { |p| p[:id] }.sort
|
|
1063
|
+
|
|
1064
|
+
results = @query.where(id: { not_in: [1, 3] })
|
|
1065
|
+
assert_equal [2, 4], results.map { |p| p[:id] }.sort
|
|
1066
|
+
|
|
1067
|
+
results = @query.where(featured: { not: true })
|
|
1068
|
+
|
|
1069
|
+
results = @query.where(tags: { empty: true })
|
|
1070
|
+
assert_equal [4], results.map { |p| p[:id] }
|
|
1071
|
+
|
|
1072
|
+
results = @query.where(tags: { empty: false })
|
|
1073
|
+
assert_equal [1, 2, 3], results.map { |p| p[:id] }.sort
|
|
1074
|
+
|
|
1075
|
+
results = @query.where(title: { exists: true })
|
|
1076
|
+
assert_equal 3, results.size
|
|
1077
|
+
|
|
1078
|
+
results = @query.where(title: { exists: false })
|
|
1079
|
+
assert_equal [4], results.map { |p| p[:id] }
|
|
1080
|
+
|
|
1081
|
+
results = @query.where(title: { starts_with: 'Post' })
|
|
1082
|
+
assert_equal 3, results.size
|
|
1083
|
+
|
|
1084
|
+
results = @query.where(title: { ends_with: '1' })
|
|
1085
|
+
assert_equal [1], results.map { |p| p[:id] }
|
|
1086
|
+
end
|
|
1087
|
+
|
|
1088
|
+
def test_where_array
|
|
1089
|
+
results = @query.where(id: [1, 3])
|
|
1090
|
+
assert_equal [1, 3], results.map { |p| p[:id] }.sort
|
|
1091
|
+
end
|
|
1092
|
+
|
|
1093
|
+
def test_where_range
|
|
1094
|
+
results = @query.where(published_at: Date.new(2024, 1, 1)..Date.new(2024, 12, 31))
|
|
1095
|
+
assert_equal [1, 2], results.map { |p| p[:id] }.sort
|
|
1096
|
+
end
|
|
1097
|
+
|
|
1098
|
+
def test_order_asc
|
|
1099
|
+
results = @query.order(:published_at)
|
|
1100
|
+
expected_order = [3, 1, 2, 4]
|
|
1101
|
+
assert_equal expected_order, results.map { |p| p[:id] }
|
|
1102
|
+
end
|
|
1103
|
+
|
|
1104
|
+
def test_order_desc
|
|
1105
|
+
results = @query.order(:published_at, desc: true)
|
|
1106
|
+
expected_order = [2, 1, 3, 4]
|
|
1107
|
+
assert_equal expected_order, results.map { |p| p[:id] }
|
|
1108
|
+
end
|
|
1109
|
+
|
|
1110
|
+
def test_limit_and_offset
|
|
1111
|
+
results = @query.order(:id).limit(2)
|
|
1112
|
+
assert_equal [1, 2], results.map { |p| p[:id] }
|
|
1113
|
+
|
|
1114
|
+
results = @query.order(:id).offset(2).limit(1)
|
|
1115
|
+
assert_equal [3], results.map { |p| p[:id] }
|
|
1116
|
+
|
|
1117
|
+
results = @query.order(:id).offset(1).limit(2)
|
|
1118
|
+
assert_equal [2, 3], results.map { |p| p[:id] }
|
|
1119
|
+
|
|
1120
|
+
results = @query.order(:id).offset(10)
|
|
1121
|
+
assert_equal [], results.map { |p| p[:id] }
|
|
1122
|
+
end
|
|
1123
|
+
|
|
1124
|
+
def test_chained_queries
|
|
1125
|
+
results = @query.where(featured: true).order(:published_at, desc: true).limit(1)
|
|
1126
|
+
assert_equal [1], results.map { |p| p[:id] }
|
|
1127
|
+
|
|
1128
|
+
results = @query.where(views: { greater_than: 120 }).where(featured: true)
|
|
1129
|
+
assert_equal [3], results.map { |p| p[:id] }
|
|
1130
|
+
end
|
|
1131
|
+
|
|
1132
|
+
def test_scopes
|
|
1133
|
+
featured = @query.featured.all
|
|
1134
|
+
assert_equal [1, 3], featured.map { |p| p[:id] }.sort
|
|
1135
|
+
|
|
1136
|
+
recent = @query.recent.all
|
|
1137
|
+
assert_equal [2, 1], recent.map { |p| p[:id] }
|
|
1138
|
+
|
|
1139
|
+
recent_3 = @query.recent(3)
|
|
1140
|
+
assert_equal [2, 1, 3], recent_3.map { |p| p[:id] }
|
|
1141
|
+
|
|
1142
|
+
tagged = @query.tagged('ruby')
|
|
1143
|
+
assert_equal [1, 3], tagged.map { |p| p[:id] }.sort
|
|
1144
|
+
|
|
1145
|
+
featured_ruby = @query.featured.tagged('ruby')
|
|
1146
|
+
assert_equal [1, 3], featured_ruby.map { |p| p[:id] }.sort
|
|
1147
|
+
end
|
|
1148
|
+
def test_scope_error_handling
|
|
1149
|
+
Hq.define :error_model do
|
|
1150
|
+
from_array([])
|
|
1151
|
+
scope({ bad: -> { raise 'boom' } })
|
|
1152
|
+
end
|
|
1153
|
+
query = Hq.query(:error_model)
|
|
1154
|
+
error = assert_raises(Hq::ScopeError) { query.bad }
|
|
1155
|
+
assert_match /Error in scope :bad for model :error_model/, error.message
|
|
1156
|
+
end
|
|
1157
|
+
|
|
1158
|
+
def test_enumerable
|
|
1159
|
+
assert_equal 4, @query.size
|
|
1160
|
+
|
|
1161
|
+
titles = @query.select { |p| p[:title] }.map { |p| p[:title] }
|
|
1162
|
+
assert_equal ['Post 1', 'Post 2', 'Post 3'], titles.sort
|
|
1163
|
+
|
|
1164
|
+
featured_ids = @query.select { |p| p[:featured] }.map { |p| p[:id] }
|
|
1165
|
+
assert_equal [1, 3], featured_ids.sort
|
|
1166
|
+
|
|
1167
|
+
assert @query.any? { |p| p[:featured] }
|
|
1168
|
+
assert @query.all? { |p| p[:id].is_a?(Integer) }
|
|
1169
|
+
end
|
|
1170
|
+
|
|
1171
|
+
def test_symbol_normalization_in_conditions
|
|
1172
|
+
results = @query.where(title: { contains: 'Post' })
|
|
1173
|
+
assert_equal 3, results.size
|
|
1174
|
+
|
|
1175
|
+
results = @query.where('id' => 1, :featured => true)
|
|
1176
|
+
assert_equal [1], results.map { |p| p[:id] }
|
|
1177
|
+
end
|
|
1178
|
+
|
|
1179
|
+
def test_complex_conditions
|
|
1180
|
+
results = @query.where(featured: false)
|
|
1181
|
+
assert_equal [2], results.map { |p| p[:id] }
|
|
1182
|
+
|
|
1183
|
+
results = @query.where(featured: nil)
|
|
1184
|
+
assert_equal [4], results.map { |p| p[:id] }
|
|
1185
|
+
|
|
1186
|
+
results = @query.where(tags: ['ruby', 'web'])
|
|
1187
|
+
assert_equal [1], results.map { |p| p[:id] }
|
|
1188
|
+
|
|
1189
|
+
results = @query.where(tags: ['tools'])
|
|
1190
|
+
assert_equal [2], results.map { |p| p[:id] }
|
|
1191
|
+
end
|
|
1192
|
+
|
|
1193
|
+
def test_or_where_hash
|
|
1194
|
+
results = @query.where(featured: true).or_where(views: { greater_than: 150 })
|
|
1195
|
+
assert_equal [1, 2, 3], results.map { |p| p.id }.sort
|
|
1196
|
+
end
|
|
1197
|
+
|
|
1198
|
+
def test_or_where_block
|
|
1199
|
+
results = @query.where { |q| q.where(id: 1).where(featured: false) }
|
|
1200
|
+
.or_where { |q| q.where(views: { greater_than_or_equal: 200 }) }
|
|
1201
|
+
|
|
1202
|
+
assert_equal [2], results.map { |p| p.id }
|
|
1203
|
+
end
|
|
1204
|
+
|
|
1205
|
+
def test_where_block_for_grouping
|
|
1206
|
+
results = @query.where(featured: true).where do |q|
|
|
1207
|
+
q.where(views: { less_than: 120 }).or_where(tags: { contains: 'ruby' })
|
|
1208
|
+
end
|
|
1209
|
+
assert_equal [1, 3], results.map { |p| p.id }.sort
|
|
1210
|
+
end
|
|
1211
|
+
|
|
1212
|
+
def test_negation_with_where_not_hash
|
|
1213
|
+
results = @query.where(featured: { not: true })
|
|
1214
|
+
assert_equal [2, 4], results.map { |p| p.id }.sort
|
|
1215
|
+
end
|
|
1216
|
+
|
|
1217
|
+
def test_negation_with_or_where_not_hash
|
|
1218
|
+
results = @query.where(featured: true).or_where(title: { not: 'Post 2' })
|
|
1219
|
+
assert_equal [1, 3, 4], results.map { |p| p.id }.sort
|
|
1220
|
+
end
|
|
1221
|
+
|
|
1222
|
+
def test_complex_and_or_not
|
|
1223
|
+
results = @query.where { |q| q.where(featured: true).where(views: { less_than_or_equal: 150 }) }
|
|
1224
|
+
.or_where { |q| q.where(tags: { contains: 'web' }).where(views: { less_than: 150 }) }
|
|
1225
|
+
|
|
1226
|
+
assert_equal [1, 3], results.map { |p| p.id }.sort
|
|
1227
|
+
end
|
|
1228
|
+
end
|
|
1229
|
+
end
|
|
1230
|
+
|
|
1231
|
+
|
|
1232
|
+
# File: ./relations_test.rb
|
|
1233
|
+
|
|
1234
|
+
require_relative 'test_helper'
|
|
1235
|
+
|
|
1236
|
+
class TestRelations < Minitest::Test
|
|
1237
|
+
def setup
|
|
1238
|
+
Hq.reload
|
|
1239
|
+
|
|
1240
|
+
Hq.define :authors do
|
|
1241
|
+
from_array([
|
|
1242
|
+
{ id: 1, name: 'Alice' },
|
|
1243
|
+
{ id: 2, name: 'Bob' },
|
|
1244
|
+
{ id: 3, name: 'Charlie' }
|
|
1245
|
+
])
|
|
1246
|
+
has_many :posts
|
|
1247
|
+
has_one :profile
|
|
1248
|
+
end
|
|
1249
|
+
|
|
1250
|
+
Hq.define :posts do
|
|
1251
|
+
from_array([
|
|
1252
|
+
{ id: 101, title: 'Intro to Hq', author_id: 1, published: true, content: '...' },
|
|
1253
|
+
{ id: 102, title: 'Advanced Ruby', author_id: 1, published: false, content: '...' },
|
|
1254
|
+
{ id: 103, title: 'Web Development', author_id: 2, published: true, content: '...' },
|
|
1255
|
+
{ id: 104, title: 'Orphaned Post', author_id: nil, published: true, content: '...' }
|
|
1256
|
+
])
|
|
1257
|
+
belongs_to :author
|
|
1258
|
+
has_many :post_tags
|
|
1259
|
+
has_many :tags, through: :post_tags
|
|
1260
|
+
end
|
|
1261
|
+
|
|
1262
|
+
Hq.define :profiles do
|
|
1263
|
+
from_array([
|
|
1264
|
+
{ id: 201, bio: 'Ruby Developer', author_id: 1 },
|
|
1265
|
+
{ id: 202, bio: 'Web Enthusiast', author_id: 2 }
|
|
1266
|
+
])
|
|
1267
|
+
belongs_to :author
|
|
1268
|
+
end
|
|
1269
|
+
|
|
1270
|
+
Hq.define :tags do
|
|
1271
|
+
from_array([
|
|
1272
|
+
{ id: 301, name: 'ruby' },
|
|
1273
|
+
{ id: 302, name: 'web' },
|
|
1274
|
+
{ id: 303, name: 'performance' }
|
|
1275
|
+
])
|
|
1276
|
+
has_many :post_tags
|
|
1277
|
+
has_many :posts, through: :post_tags
|
|
1278
|
+
end
|
|
1279
|
+
|
|
1280
|
+
Hq.define :post_tags do
|
|
1281
|
+
from_array([
|
|
1282
|
+
{ id: 401, post_id: 101, tag_id: 301 },
|
|
1283
|
+
{ id: 402, post_id: 101, tag_id: 302 },
|
|
1284
|
+
{ id: 403, post_id: 102, tag_id: 301 },
|
|
1285
|
+
{ id: 404, post_id: 103, tag_id: 302 }
|
|
1286
|
+
])
|
|
1287
|
+
belongs_to :post
|
|
1288
|
+
belongs_to :tag
|
|
1289
|
+
end
|
|
1290
|
+
|
|
1291
|
+
Hq.define :users do
|
|
1292
|
+
from_array([ { user_pk: 501, username: 'admin' } ])
|
|
1293
|
+
has_many :articles, class_name: :articles, foreign_key: :creator_id, owner_key: :user_pk
|
|
1294
|
+
end
|
|
1295
|
+
|
|
1296
|
+
Hq.define :articles do
|
|
1297
|
+
from_array([
|
|
1298
|
+
{ id: 601, title: 'User Article', creator_id: 501 }
|
|
1299
|
+
])
|
|
1300
|
+
belongs_to :creator, class_name: :users, foreign_key: :creator_id, primary_key: :user_pk
|
|
1301
|
+
end
|
|
1302
|
+
end
|
|
1303
|
+
|
|
1304
|
+
def teardown
|
|
1305
|
+
Hq.reload
|
|
1306
|
+
end
|
|
1307
|
+
|
|
1308
|
+
def test_belongs_to_finds_parent
|
|
1309
|
+
post = hq(:posts).find_by(id: 101)
|
|
1310
|
+
author = post.author
|
|
1311
|
+
|
|
1312
|
+
assert_instance_of Hq::InstanceWrapper, author
|
|
1313
|
+
assert_equal 1, author.id
|
|
1314
|
+
assert_equal 'Alice', author.name
|
|
1315
|
+
assert post.respond_to?(:author)
|
|
1316
|
+
end
|
|
1317
|
+
|
|
1318
|
+
def test_belongs_to_returns_nil_if_fk_nil
|
|
1319
|
+
post = hq(:posts).find_by(id: 104)
|
|
1320
|
+
assert_nil post.author
|
|
1321
|
+
end
|
|
1322
|
+
|
|
1323
|
+
def test_belongs_to_returns_nil_if_parent_missing
|
|
1324
|
+
post = Hq::InstanceWrapper.new({ id: 105, title: 'Missing Author Post', author_id: 999 }, :posts)
|
|
1325
|
+
author = post.author
|
|
1326
|
+
assert_nil author
|
|
1327
|
+
end
|
|
1328
|
+
|
|
1329
|
+
def test_belongs_to_with_custom_keys
|
|
1330
|
+
article = hq(:articles).find_by(id: 601)
|
|
1331
|
+
creator = article.creator
|
|
1332
|
+
|
|
1333
|
+
assert_instance_of Hq::InstanceWrapper, creator
|
|
1334
|
+
assert_equal 'admin', creator.username
|
|
1335
|
+
assert article.respond_to?(:creator)
|
|
1336
|
+
end
|
|
1337
|
+
|
|
1338
|
+
def test_has_many_returns_query
|
|
1339
|
+
alice = hq(:authors).find_by(id: 1)
|
|
1340
|
+
posts_query = alice.posts
|
|
1341
|
+
|
|
1342
|
+
assert_instance_of Hq::Query, posts_query
|
|
1343
|
+
assert alice.respond_to?(:posts)
|
|
1344
|
+
end
|
|
1345
|
+
|
|
1346
|
+
def test_has_many_retrieves_children
|
|
1347
|
+
alice = hq(:authors).find_by(id: 1)
|
|
1348
|
+
posts = alice.posts.order(:id)
|
|
1349
|
+
|
|
1350
|
+
assert_equal 2, posts.size
|
|
1351
|
+
assert_equal [101, 102], posts.map { |p| p.id }
|
|
1352
|
+
assert_equal ['Intro to Hq', 'Advanced Ruby'], posts.map { |p| p.title }
|
|
1353
|
+
end
|
|
1354
|
+
|
|
1355
|
+
def test_has_many_allows_chaining
|
|
1356
|
+
alice = hq(:authors).find_by(id: 1)
|
|
1357
|
+
published_posts = alice.posts.where(published: true)
|
|
1358
|
+
|
|
1359
|
+
assert_equal 1, published_posts.size
|
|
1360
|
+
assert_equal 101, published_posts.first.id
|
|
1361
|
+
end
|
|
1362
|
+
|
|
1363
|
+
def test_has_many_returns_empty_query_if_no_children
|
|
1364
|
+
charlie = hq(:authors).find_by(id: 3)
|
|
1365
|
+
posts_query = charlie.posts
|
|
1366
|
+
|
|
1367
|
+
assert_instance_of Hq::Query, posts_query
|
|
1368
|
+
assert_equal 0, posts_query.size
|
|
1369
|
+
assert_equal [], posts_query.to_a
|
|
1370
|
+
end
|
|
1371
|
+
|
|
1372
|
+
def test_has_many_with_custom_keys
|
|
1373
|
+
user = hq(:users).first
|
|
1374
|
+
articles_query = user.articles
|
|
1375
|
+
|
|
1376
|
+
assert_instance_of Hq::Query, articles_query
|
|
1377
|
+
assert_equal 1, articles_query.size
|
|
1378
|
+
assert_equal 601, articles_query.first.id
|
|
1379
|
+
assert user.respond_to?(:articles)
|
|
1380
|
+
end
|
|
1381
|
+
|
|
1382
|
+
def test_has_one_finds_child
|
|
1383
|
+
alice = hq(:authors).find_by(id: 1)
|
|
1384
|
+
profile = alice.profile
|
|
1385
|
+
|
|
1386
|
+
assert_instance_of Hq::InstanceWrapper, profile
|
|
1387
|
+
assert_equal 201, profile.id
|
|
1388
|
+
assert_equal 'Ruby Developer', profile.bio
|
|
1389
|
+
assert alice.respond_to?(:profile)
|
|
1390
|
+
end
|
|
1391
|
+
|
|
1392
|
+
def test_has_one_returns_nil_if_no_child
|
|
1393
|
+
charlie = hq(:authors).find_by(id: 3)
|
|
1394
|
+
profile = charlie.profile
|
|
1395
|
+
|
|
1396
|
+
assert_nil profile
|
|
1397
|
+
end
|
|
1398
|
+
|
|
1399
|
+
def test_has_many_through_returns_query
|
|
1400
|
+
post = hq(:posts).find_by(id: 101)
|
|
1401
|
+
tags_query = post.tags
|
|
1402
|
+
|
|
1403
|
+
assert_instance_of Hq::Query, tags_query
|
|
1404
|
+
assert post.respond_to?(:tags)
|
|
1405
|
+
end
|
|
1406
|
+
|
|
1407
|
+
def test_has_many_through_retrieves_targets
|
|
1408
|
+
post = hq(:posts).find_by(id: 101)
|
|
1409
|
+
tags = post.tags.order(:id)
|
|
1410
|
+
|
|
1411
|
+
assert_equal 2, tags.size
|
|
1412
|
+
assert_equal [301, 302], tags.map { |t| t.id }
|
|
1413
|
+
assert_equal ['ruby', 'web'], tags.map { |t| t.name }
|
|
1414
|
+
end
|
|
1415
|
+
|
|
1416
|
+
def test_has_many_through_allows_chaining
|
|
1417
|
+
post = hq(:posts).find_by(id: 101)
|
|
1418
|
+
ruby_tag = post.tags.where(name: 'ruby')
|
|
1419
|
+
|
|
1420
|
+
assert_equal 1, ruby_tag.size
|
|
1421
|
+
assert_equal 301, ruby_tag.first.id
|
|
1422
|
+
end
|
|
1423
|
+
|
|
1424
|
+
def test_has_many_through_returns_empty_query_if_no_targets
|
|
1425
|
+
post = hq(:posts).find_by(id: 104)
|
|
1426
|
+
tags_query = post.tags
|
|
1427
|
+
|
|
1428
|
+
assert_instance_of Hq::Query, tags_query
|
|
1429
|
+
assert_equal 0, tags_query.size
|
|
1430
|
+
assert_equal [], tags_query.to_a
|
|
1431
|
+
end
|
|
1432
|
+
|
|
1433
|
+
def test_has_many_through_inverse
|
|
1434
|
+
tag = hq(:tags).find_by(id: 301)
|
|
1435
|
+
posts_query = tag.posts
|
|
1436
|
+
|
|
1437
|
+
assert_instance_of Hq::Query, posts_query
|
|
1438
|
+
assert tag.respond_to?(:posts)
|
|
1439
|
+
|
|
1440
|
+
posts = posts_query.order(:id)
|
|
1441
|
+
assert_equal 2, posts.size
|
|
1442
|
+
assert_equal [101, 102], posts.map { |p| p.id }
|
|
1443
|
+
end
|
|
1444
|
+
end
|
|
1445
|
+
|
|
1446
|
+
|
|
1447
|
+
# File: ./run_all.rb
|
|
1448
|
+
|
|
1449
|
+
require_relative "relations_test"
|
|
1450
|
+
require_relative "query_test"
|
|
1451
|
+
require_relative "context_test"
|
|
1452
|
+
require_relative "instance_test"
|
|
1453
|
+
require_relative "edge_test"
|
|
1454
|
+
require_relative "paginate_test"
|
|
1455
|
+
require_relative "data_test"
|
|
1456
|
+
|
|
1457
|
+
|
|
1458
|
+
# File: ./test_helper.rb
|
|
1459
|
+
|
|
1460
|
+
require 'minitest/autorun'
|
|
1461
|
+
require 'minitest/pride'
|
|
1462
|
+
require_relative '../lib/hq'
|
|
1463
|
+
require 'date'
|
|
1464
|
+
require 'tmpdir' # For temporary file tests
|
|
1465
|
+
require 'fileutils'
|
|
1466
|
+
|