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.
data/README.md ADDED
@@ -0,0 +1,700 @@
1
+ # HashQuery - Advanced Querying & Presentation Layer for Ruby Hashes
2
+
3
+ You know that feeling when you're prototyping and you just need to store some structured data without the ceremony of setting up a database, migrations, and ORMs?
4
+
5
+ Or when you're building a static site and you're tired of wrestling with YAML parsing errors and JSON schema headaches?
6
+
7
+ Meet HashQuery. It's the Ruby library you didn't know you were missing.
8
+
9
+ ## The "Aha!" Moment
10
+
11
+ ```ruby
12
+ # Instead of this YAML nightmare:
13
+ # data/posts.yml
14
+ # ---
15
+ # - id: 1
16
+ # title: "My Post" # YAML gotcha: this needs quotes
17
+ # published_at: 2024-01-01 # Is this a string? Date? Who knows!
18
+
19
+ # You write pure Ruby (like you wanted all along):
20
+ Hq.define :posts do
21
+ from_array([
22
+ { id: 1, title: 'Hello World', published: true, created_at: Date.new(2024, 1, 1) },
23
+ { id: 2, title: 'Ruby Magic', published: false, created_at: Date.new(2024, 1, 15) }
24
+ ])
25
+ end
26
+
27
+ # Query it like you always wished you could:
28
+ hq(:posts).where(published: true).order(:created_at, desc: true).first[:title]
29
+ # => "Hello World"
30
+ ```
31
+
32
+ No schema. No migrations. No database setup. Just Ruby being Ruby.
33
+
34
+ ---
35
+
36
+ ## Installation
37
+
38
+ ```ruby
39
+ # Gemfile
40
+ gem 'joys'
41
+
42
+ # Or if you're feeling spontaneous:
43
+ gem install joys
44
+ ```
45
+
46
+ ## Quick Start: From Zero to Querying in 30 Seconds
47
+
48
+ ```ruby
49
+ require 'hq'
50
+
51
+ # Define your data (arrays, files, whatever)
52
+ Hq.define :posts do
53
+ from_array([
54
+ { id: 1, title: 'Why I Love Ruby', tags: ['ruby', 'opinion'], views: 1337 },
55
+ { id: 2, title: 'JavaScript Fatigue', tags: ['js', 'rant'], views: 9001 },
56
+ { id: 3, title: 'The Perfect Deploy', tags: ['ops', 'ruby'], views: 500 }
57
+ ])
58
+ end
59
+
60
+ # Query like you always wanted to:
61
+ popular_ruby_posts = hq(:posts)
62
+ .where(tags: { contains: 'ruby' })
63
+ .where(views: { greater_than: 1000 })
64
+ .order(:views, desc: true)
65
+ .all
66
+
67
+ puts popular_ruby_posts.first[:title] # => "Why I Love Ruby"
68
+ ```
69
+
70
+ That global `hq()` helper? It's there because life's too short to type `Hq.query()` every time.
71
+
72
+ ---
73
+
74
+ ## Data Sources: Choose Your Own Adventure
75
+
76
+ ### Option 1: Inline Arrays (Perfect for Prototyping)
77
+
78
+ When you just need to get something working:
79
+
80
+ ```ruby
81
+ Hq.define :users do
82
+ from_array([
83
+ { id: 1, name: 'Alice', role: 'admin', coffee_preference: 'black' },
84
+ { id: 2, name: 'Bob', role: 'user', coffee_preference: 'latte' },
85
+ { id: 3, name: 'Carol', role: 'editor', coffee_preference: 'espresso' }
86
+ ])
87
+ end
88
+ ```
89
+
90
+ ### Option 2: File-Based Data (The Grown-Up Way)
91
+
92
+ Your data files are just Ruby scripts that return hashes. No parsing, no gotchas:
93
+
94
+ ```ruby
95
+ # data/posts/001-hello-world.rb
96
+ {
97
+ id: 1,
98
+ title: 'Hello World',
99
+ slug: 'hello-world',
100
+ published_at: Date.new(2024, 1, 1),
101
+ tags: ['ruby', 'beginnings'],
102
+ meta: {
103
+ author: 'You',
104
+ reading_time: '2 min'
105
+ }
106
+ }
107
+
108
+ # data/posts/002-advanced-stuff.rb
109
+ {
110
+ id: 2,
111
+ title: 'Advanced Ruby Wizardry',
112
+ slug: 'advanced-ruby-wizardry',
113
+ published_at: Date.new(2024, 1, 15),
114
+ tags: ['ruby', 'advanced'],
115
+ meta: {
116
+ author: 'You (but smarter)',
117
+ reading_time: '15 min'
118
+ }
119
+ }
120
+ ```
121
+
122
+ ```ruby
123
+ Hq.define :posts do
124
+ load 'posts/*.rb' # Loads in sorted filename order
125
+ end
126
+
127
+ # Want a different data directory? No problem:
128
+ Hq.configure(data_path: 'content/data')
129
+ ```
130
+
131
+ Pro tip: Your editor will syntax highlight these files, catch typos, and even provide autocomplete. Try doing that with YAML.
132
+
133
+ ---
134
+
135
+ ## Scopes: Make Your Queries Reusable
136
+
137
+ Remember writing the same `.where()` chains over and over? Those days are over:
138
+
139
+ ```ruby
140
+ Hq.define :posts do
141
+ from_array([...]) # Your data
142
+
143
+ scope({
144
+ published: -> { where(published: true) },
145
+ featured: -> { where(featured: true) },
146
+ recent: ->(n=10) { order(:published_at, desc: true).limit(n) },
147
+ tagged: ->(tag) { where(tags: { contains: tag }) },
148
+ popular: -> { where(views: { greater_than: 100 }).order(:views, desc: true) }
149
+ })
150
+ end
151
+
152
+ # Now your queries read like English:
153
+ trending_ruby_posts = hq(:posts).published.tagged('ruby').popular.recent(5)
154
+ featured_content = hq(:posts).published.featured.recent
155
+ ```
156
+
157
+ Scopes are chainable, composable, and parameterizable. They're basically custom query methods that don't suck.
158
+
159
+ ---
160
+
161
+ ## Querying: The Good Stuff
162
+
163
+ HashQuery provides a fluent, chainable API for filtering and sorting your data. Queries are lazy—they only execute when you ask for the results (e.g., by calling `.first`, `.map`, `.to_a`, `.each`, `.size`).
164
+
165
+ ### Basic Queries (The Classics)
166
+
167
+ ```ruby
168
+ posts = hq(:posts) # Get the query builder for :posts
169
+
170
+ # Get results
171
+ posts.to_a # Get all matching records as an array of Hq::InstanceWrapper objects
172
+ posts.first # First match or nil
173
+ posts.last # Last match (respects order) or nil
174
+ posts.count # Fast count of matching records
175
+
176
+ # Find by specific attribute(s)
177
+ posts.find_by(slug: 'hello-world') # Convenience for where(slug: '...').first
178
+ posts.find_by(id: 1)
179
+ ```
180
+
181
+ **Note:** You no longer need to call `.all`. The query object itself acts like an array when you iterate (`.each`, `.map`) or access its size (`.size`, `.count`, `.length`). Use `.to_a` if you specifically need a plain `Array`.
182
+
183
+ ### Where Clauses (AND Logic)
184
+
185
+ Chain multiple `.where` calls or provide multiple conditions in a hash to combine filters with `AND`.
186
+
187
+ ```ruby
188
+ # Find published posts tagged 'ruby'
189
+ hq(:posts).where(published: true).where(tags: { contains: 'ruby' })
190
+
191
+ # Equivalent using a single hash
192
+ hq(:posts).where(published: true, tags: { contains: 'ruby' })
193
+ ```
194
+
195
+ ### OR Logic (`or_where`)
196
+
197
+ Use `.or_where` to add conditions combined with `OR`.
198
+
199
+ ```ruby
200
+ # Find posts that are featured OR have more than 1000 views
201
+ hq(:posts).where(featured: true).or_where(views: { greater_than: 1000 })
202
+ # => WHERE featured = true OR views > 1000
203
+ ```
204
+
205
+ ### Grouping Conditions with Blocks
206
+
207
+ Use blocks with `where` and `or_where` to create nested logical groups. Conditions inside a block are implicitly joined by `AND` unless `or_where` is used within that block.
208
+
209
+ ```ruby
210
+ # Find posts where (author_id = 1 AND published = true)
211
+ hq(:posts).where do |q|
212
+ q.where(author_id: 1).where(published: true)
213
+ end
214
+
215
+ # Find posts where (author_id = 1 AND (published = true OR featured = true))
216
+ hq(:posts).where(author_id: 1).where do |q|
217
+ q.where(published: true).or_where(featured: true)
218
+ end
219
+ # => WHERE author_id = 1 AND (published = true OR featured = true)
220
+
221
+ # Find posts where (author_id = 1 AND published = true) OR (views > 1000)
222
+ hq(:posts).where { |q| q.where(author_id: 1).where(published: true) }
223
+ .or_where(views: { greater_than: 1000 })
224
+ # => WHERE (author_id = 1 AND published = true) OR views > 1000
225
+
226
+ # Find posts where (author_id = 1 AND published = true) OR (author_id = 2 AND featured = true)
227
+ hq(:posts).where { |q| q.where(author_id: 1).where(published: true) }
228
+ .or_where { |q| q.where(author_id: 2).where(featured: true) }
229
+ # => WHERE (author_id = 1 AND published = true) OR (author_id = 2 AND featured = true)
230
+ ```
231
+
232
+ ### Operators & Negation (Hash Conditions)
233
+
234
+ Use hash conditions within `where` or `or_where` for powerful comparisons and negation.
235
+
236
+ ```ruby
237
+ # Text searches
238
+ hq(:posts).where(title: { contains: 'Ruby' })
239
+ hq(:posts).where(title: { starts_with: 'How to' })
240
+ hq(:posts).where(title: { ends_with: '101' })
241
+
242
+ # Numeric comparisons
243
+ hq(:posts).where(views: { greater_than: 1000 })
244
+ hq(:posts).where(views: { less_than_or_equal: 500 })
245
+ hq(:posts).where(score: { from: 7.5, to: 9.0 }) # Inclusive range
246
+ hq(:posts).where(views: 100..500) # Ruby Range works too
247
+
248
+ # Existence and emptiness
249
+ hq(:posts).where(featured_image: { exists: true }) # Key is present and not nil
250
+ hq(:posts).where(featured_image: { exists: false }) # Key is missing or nil
251
+ hq(:posts).where(tags: { empty: false }) # Not nil, not '', not []
252
+ hq(:posts).where(tags: { empty: true }) # Is nil, '', or []
253
+
254
+ # Array / Set operations
255
+ hq(:posts).where(id: { in: [1, 3, 5] }) # Value is one of these
256
+ hq(:posts).where(id: [1, 3, 5]) # Shortcut for :in
257
+ hq(:posts).where(status: { not_in: ['draft', 'archived'] }) # Value is NOT one of these
258
+ hq(:posts).where(tags: ['ruby', 'web']) # Exact array match (order matters)
259
+
260
+ # Negation
261
+ hq(:posts).where(published: { not: true }) # Value is not true (false or nil)
262
+ hq(:posts).where(title: { not: 'Hello' }) # Value is not 'Hello'
263
+ hq(:posts).where(views: { not: nil }) # Same as { exists: true }
264
+ ```
265
+
266
+ ### Ordering (Nil-Safe and Type-Aware)
267
+
268
+ Sort your results using `.order`. Nils are always sorted last.
269
+
270
+ ```ruby
271
+ # Ascending (default)
272
+ hq(:posts).order(:published_at)
273
+
274
+ # Descending
275
+ hq(:posts).order(:views, desc: true)
276
+
277
+ # Strings sort naturally
278
+ hq(:posts).order(:title)
279
+ ```
280
+
281
+ Mixed types? No problem. HashQuery handles the type coercion so you don't have to think about it during sorting.
282
+
283
+ -----
284
+ ### Limiting and Pagination
285
+
286
+ ```ruby
287
+ # Classic pagination
288
+ hq(:posts).limit(10) # First 10
289
+ hq(:posts).offset(20).limit(10) # Items 21-30
290
+
291
+ # Modern pagination with metadata
292
+ pages = hq(:posts).order(:created_at, desc: true).paginate(per_page: 5)
293
+
294
+ page = pages.first
295
+ page.items # Array of posts for this page
296
+ page.current_page # 1
297
+ page.total_pages # 4
298
+ page.total_items # 18
299
+ page.next_page # 2 (or nil if last page)
300
+ page.prev_page # nil (or previous page number)
301
+ page.is_first_page # true
302
+ page.is_last_page # false
303
+
304
+ # Perfect for building pagination UI
305
+ pages.each do |page|
306
+ puts "Page #{page.current_page}: #{page.items.size} posts"
307
+ end
308
+ ```
309
+
310
+ The Page object has everything you need for pagination UI without any mental math.
311
+
312
+ ---
313
+
314
+ ## Instance Wrappers: Hash-Like, But Better
315
+
316
+ Results aren't plain hashes—they're immutable wrappers that feel like hashes but prevent accidents:
317
+
318
+ ```ruby
319
+ post = hq(:posts).first
320
+
321
+ # Access like a hash (symbol or string keys both work)
322
+ post[:title] # => "Hello World"
323
+ post['title'] # => "Hello World" (same thing)
324
+
325
+ # All the hash methods you expect
326
+ post.keys # => [:id, :title, :content, ...]
327
+ post.size # => 5
328
+ post.has_key?(:id) # => true
329
+ post.include?('title') # => true
330
+ post.to_h # Raw hash if you need it
331
+
332
+ # But immutable (this raises an error)
333
+ post[:title] = 'New Title' # RuntimeError: collections are immutable
334
+
335
+ # Custom methods work too
336
+ post.excerpt(20) # Your custom methods
337
+ post.reading_time # Computed properties
338
+ ```
339
+
340
+ It's like getting a hash that went to finishing school.
341
+
342
+ ---
343
+
344
+ ## Development Helpers (For Your Sanity)
345
+
346
+ ```ruby
347
+ # Reload everything during development
348
+ Hq.reload
349
+
350
+ # See what models you've defined
351
+ Hq.defined_models # => [:posts, :users, :tags]
352
+
353
+ # Use the explicit API when you need it
354
+ Hq.query(:posts).where(...) # Same as hq(:posts).where(...)
355
+ ```
356
+
357
+ Error messages are actually helpful:
358
+ - Syntax errors in data files show the exact file and line
359
+ - Missing files tell you the exact pattern that failed
360
+ - Type errors explain what was expected vs. what was found
361
+
362
+ ---
363
+
364
+ ## Relationships: Connecting Your Data
365
+
366
+ HashQuery makes it easy to define relationships between your data collections, similar to ActiveRecord associations. Define them right inside your `Hq.define` block.
367
+
368
+ ### Defining Relationships
369
+
370
+ Use `belongs_to`, `has_many`, and `has_one` to link your data. HashQuery uses conventions for keys but allows overrides.
371
+
372
+ ```ruby
373
+ # --- Authors ---
374
+ Hq.define :authors do
375
+ from_array([
376
+ { id: 1, name: 'Alice' },
377
+ { id: 2, name: 'Bob' }
378
+ ])
379
+
380
+ # Author has many posts (looks for :author_id in :posts)
381
+ has_many :posts
382
+
383
+ # Author has one profile (looks for :author_id in :profiles)
384
+ has_one :profile
385
+ end
386
+
387
+ # --- Posts ---
388
+ Hq.define :posts do
389
+ from_array([
390
+ { id: 101, title: 'Intro to Hq', author_id: 1 },
391
+ { id: 102, title: 'Advanced Ruby', author_id: 1 },
392
+ { id: 103, title: 'Web Development', author_id: 2 }
393
+ ])
394
+
395
+ # Post belongs to an author (looks for :author_id here, links to :authors using :id)
396
+ belongs_to :author
397
+ end
398
+
399
+ # --- Profiles ---
400
+ Hq.define :profiles do
401
+ from_array([
402
+ { profile_id: 201, bio: 'Ruby Developer', author_id: 1 }, # Note: primary key is :profile_id
403
+ { profile_id: 202, bio: 'Web Enthusiast', author_id: 2 }
404
+ ])
405
+
406
+ # Profile belongs to an author (looks for :author_id here)
407
+ # Target model (:authors) uses :id as primary key by default
408
+ belongs_to :author
409
+ end
410
+ ```
411
+
412
+ ### Accessing Relationships
413
+
414
+ Access related data using simple dot notation on your `Hq::InstanceWrapper` objects.
415
+
416
+ ```ruby
417
+ alice = hq(:authors).find_by(id: 1)
418
+ post = hq(:posts).find_by(id: 101)
419
+ profile = hq(:profiles).find_by(profile_id: 201)
420
+
421
+ # Belongs To (returns InstanceWrapper or nil)
422
+ author_name = post.author.name
423
+ # => "Alice"
424
+ author_bio = profile.author.profile.bio # Chain through relations
425
+ # => "Ruby Developer"
426
+
427
+ # Has One (returns InstanceWrapper or nil)
428
+ alices_bio = alice.profile.bio
429
+ # => "Ruby Developer"
430
+
431
+ # Has Many (returns a Hq::Query object)
432
+ alices_posts = alice.posts
433
+ # => <Hq::Query @model_name=:posts ...>
434
+
435
+ # You can chain queries on has_many results
436
+ published_titles = alice.posts.where(published: true).map { |p| p.title }
437
+ # => ["Intro to Hq"]
438
+ ```
439
+
440
+ ### Overriding Conventions
441
+
442
+ Need custom keys or class names? No problem.
443
+
444
+ ```ruby
445
+ Hq.define :users do
446
+ from_array([{ user_pk: 501, username: 'admin' }]) # Custom primary key
447
+
448
+ # Specify foreign_key in posts (:creator_id) and owner_key here (:user_pk)
449
+ has_many :articles, class_name: :posts, foreign_key: :creator_id, owner_key: :user_pk
450
+ end
451
+
452
+ Hq.define :posts do
453
+ # ... other posts ...
454
+ from_array([{ id: 105, title: 'Admin Post', creator_id: 501 }]) # Custom foreign key
455
+
456
+ # Specify foreign_key here (:creator_id) and primary_key on users (:user_pk)
457
+ belongs_to :creator, class_name: :users, foreign_key: :creator_id, primary_key: :user_pk
458
+ end
459
+
460
+ admin = hq(:users).first
461
+ admin_article_title = admin.articles.first.title
462
+ # => "Admin Post"
463
+
464
+ post = hq(:posts).find_by(id: 105)
465
+ creator_name = post.creator.username
466
+ # => "admin"
467
+ ```
468
+
469
+ ### Many-to-Many (`has_many :through`)
470
+
471
+ Define the intermediate `has_many` first, then the `through` relationship.
472
+
473
+ ```ruby
474
+ Hq.define :posts do
475
+ # ... (fields, belongs_to :author) ...
476
+ has_many :post_tags # Link to the join collection
477
+ has_many :tags, through: :post_tags # Go through :post_tags to find :tags
478
+ # (infers :tag on PostTag model)
479
+ end
480
+
481
+ Hq.define :tags do
482
+ from_array([ { id: 301, name: 'ruby' }, { id: 302, name: 'web' } ])
483
+ has_many :post_tags
484
+ has_many :posts, through: :post_tags # Go through :post_tags to find :posts
485
+ # (infers :post on PostTag model)
486
+ end
487
+
488
+ Hq.define :post_tags do # The join collection
489
+ from_array([
490
+ { id: 401, post_id: 101, tag_id: 301 },
491
+ { id: 402, post_id: 101, tag_id: 302 },
492
+ { id: 403, post_id: 102, tag_id: 301 }
493
+ ])
494
+ belongs_to :post # Link back to posts
495
+ belongs_to :tag # Link back to tags
496
+ end
497
+
498
+ # Usage:
499
+ post = hq(:posts).find_by(id: 101)
500
+ tag_names = post.tags.map { |t| t.name }
501
+ # => ["ruby", "web"]
502
+
503
+ tag = hq(:tags).find_by(name: 'ruby')
504
+ post_titles = tag.posts.map { |p| p.title }
505
+ # => ["Intro to Hq", "Advanced Ruby"]
506
+
507
+ # Override source if needed:
508
+ # has_many :categories, through: :post_categories, source: :category
509
+ ```
510
+
511
+ Relationships in HashQuery provide a powerful yet simple way to navigate your connected data directly within Ruby.
512
+
513
+ ---
514
+
515
+ ## Presenters: Adding Behavior to Your Data
516
+
517
+ While `Hq` focuses on querying, you often need helper methods for formatting or deriving information from your data records. Instead of cluttering your view logic, you can define these directly using `Hq.present`. This replaces the older, less ergonomic `item` block.
518
+
519
+ ### Defining Presenter Methods
520
+
521
+ Use standard Ruby `def` syntax within a `Hq.present` block associated with your model name. Inside these methods, `self` refers to the `Hq::InstanceWrapper` object, allowing you to access the underlying data using `self[:key]` or just `key` (if the key doesn't clash with a method name).
522
+
523
+ ```ruby
524
+ Hq.define :posts do
525
+ from_array([
526
+ { id: 1, title: 'Hello World', slug: 'hello-world', content: 'This is the first post.', published_at: Date.today }
527
+ ])
528
+ belongs_to :author # Example relation
529
+ end
530
+
531
+ # Define presenter methods for the :posts model
532
+ Hq.present :posts do
533
+ # Simple formatting
534
+ def formatted_date
535
+ self[:published_at]&.strftime('%B %d, %Y') # Access data with self[:key]
536
+ end
537
+
538
+ # Derived data
539
+ def url
540
+ "/posts/#{slug}/" # Access data directly via dynamically defined method 'slug'
541
+ end
542
+
543
+ # Methods with arguments
544
+ def excerpt(word_count = 25)
545
+ words = content.split # Access data via 'content' method
546
+ return content if words.size <= word_count
547
+ words.take(word_count).join(' ') + '...'
548
+ end
549
+
550
+ # Accessing relations within presenters
551
+ def author_name
552
+ author&.name || 'Anonymous' # Call the 'author' relation method
553
+ end
554
+ end
555
+ ```
556
+
557
+ ### Using Presenter Methods
558
+
559
+ Presenter methods are automatically available directly on the `InstanceWrapper` objects returned by your queries.
560
+
561
+ ```ruby
562
+ post = hq(:posts).first
563
+
564
+ # Access hash data keys
565
+ puts post.id # => 1
566
+ puts post[:title] # => "Hello World" (Hash access still works)
567
+
568
+ # Call presenter methods
569
+ puts post.url # => "/posts/hello-world/"
570
+ puts post.formatted_date # => "October 27, 2025"
571
+ puts post.excerpt(3) # => "This is the..."
572
+ puts post.author_name # => (Assuming author relation works) "Alice" or "Anonymous"
573
+ ```
574
+
575
+ ### Benefits
576
+
577
+ * **Clean Ruby Syntax:** Uses standard `def`
578
+ * **Clear Separation:** Keeps presentation logic separate from the core data definition (`Hq.define`).
579
+ * **Precedence:** Presenter methods will override accessor methods created for hash keys if they share the same name. Access the original hash value using `self[:key]` if needed.
580
+
581
+ Use `Hq.present` to keep your data definitions clean and add reusable display logic directly to your data objects.
582
+
583
+ ---
584
+
585
+ ## Auto-Loading Data Files
586
+
587
+ Instead of manually requiring data files, use `Hq.load_from` to automatically discover and load all data definitions:
588
+
589
+ ```ruby
590
+ # Directory structure:
591
+ # data/
592
+ # users.rb
593
+ # posts.rb
594
+ # categories.rb
595
+
596
+ # config.ru or boot file
597
+ require 'hq'
598
+
599
+ Hq.load_from('data') # Loads all .rb files in data/
600
+
601
+ # Now all collections are available
602
+ hq(:users).where(active: true)
603
+ hq(:posts).order(:created_at)
604
+ ```
605
+
606
+ **Development mode reloading:**
607
+
608
+ ```ruby
609
+ if ENV['RACK_ENV'] == 'development'
610
+ require 'listen'
611
+
612
+ listener = Listen.to('data') do |modified, added, removed|
613
+ puts "Data changed, reloading..."
614
+ Hq.reload_from('data')
615
+ end
616
+ listener.start
617
+ end
618
+ ```
619
+
620
+ Files are loaded in alphabetical order. Use nested directories to organize large data sets:
621
+
622
+ ```ruby
623
+ data/
624
+ blog/
625
+ posts.rb
626
+ categories.rb
627
+ users/
628
+ admins.rb
629
+ customers.rb
630
+
631
+ Hq.load_from('data') # Loads everything recursively
632
+ ```
633
+
634
+ ---
635
+
636
+ ## Performance Notes (For the Curious)
637
+
638
+ **The Good News:** Hq is fast enough for most use cases. We're talking thousands of items with complex queries in milliseconds.
639
+
640
+ **The Technical Details:**
641
+ - Everything lives in memory (no I/O after initial load)
642
+ - Queries are lazy—filters only apply when you call `.all`, `.first`, etc.
643
+ - Data is frozen at load time (immutable and thread-safe)
644
+ - Method results are memoized automatically
645
+ - Under 500 lines of core code (minimal overhead)
646
+
647
+ **Sweet Spot:** Hundreds to low thousands of items. Perfect for:
648
+ - Blog posts and pages
649
+ - Product catalogs
650
+ - Team directories
651
+ - Configuration data
652
+ - Documentation sites
653
+ - Prototype datasets
654
+
655
+ **When to Graduate to a Real Database:**
656
+ - Tens of thousands of items
657
+ - Real-time updates needed
658
+ - Complex relationships and joins
659
+ - Multi-user concurrent writes
660
+
661
+ ---
662
+
663
+ ## When Hq Shines
664
+
665
+ **Perfect For:**
666
+ - Static site content management
667
+ - Rapid prototyping with structured data
668
+ - Configuration and settings management
669
+ - Small-to-medium datasets that don't change often
670
+ - Replacing hand-rolled JSON/YAML parsers
671
+ - When you want SQL-like querying without SQL complexity
672
+
673
+ **Not Ideal For:**
674
+ - Large-scale data (stick with SQLite/PostgreSQL/MySQL)
675
+ - Real-time analytics
676
+ - Data that changes frequently
677
+ - Multi-table joins and complex relationships
678
+ - When you actually need ACID transactions
679
+
680
+ ---
681
+
682
+ ## The Philosophy
683
+
684
+ We built Hq because we were tired of the false choice between "simple but limited" and "powerful but complex."
685
+
686
+ Why should prototyping with structured data require setting up a database? Why should static sites need a complex build pipeline just to query some content? Why can't data files be as expressive as the rest of our Ruby code?
687
+
688
+ Hq is our answer: a data layer that grows with your project. Start with arrays, move to files, add scopes and methods as needed. When you outgrow it, you'll have learned exactly what you need from a real database.
689
+
690
+ It's the tool we always wished existed. Now it does.
691
+
692
+ ---
693
+
694
+ **Ready to feel the joy of simple, powerful data?**
695
+
696
+ ```bash
697
+ gem install hq
698
+ ```
699
+
700
+ Your future self will thank you.
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ task default: :test