quo 1.0.0.beta2 → 2.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.
Files changed (101) hide show
  1. checksums.yaml +4 -4
  2. data/.devcontainer/Dockerfile +17 -0
  3. data/.devcontainer/compose.yml +10 -0
  4. data/.devcontainer/devcontainer.json +12 -0
  5. data/Appraisals +4 -12
  6. data/CHANGELOG.md +112 -1
  7. data/CLAUDE.md +19 -0
  8. data/Gemfile +7 -1
  9. data/LICENSE.txt +1 -1
  10. data/README.md +496 -203
  11. data/Rakefile +66 -6
  12. data/UPGRADING.md +216 -0
  13. data/badges/coverage_badge_total.svg +35 -0
  14. data/badges/rubycritic_badge_score.svg +35 -0
  15. data/claude-skill/README.md +100 -0
  16. data/claude-skill/SKILL.md +442 -0
  17. data/claude-skill/references/API_REFERENCE.md +462 -0
  18. data/claude-skill/references/COMPOSITION.md +396 -0
  19. data/claude-skill/references/PAGINATION.md +396 -0
  20. data/claude-skill/references/QUERY_TYPES.md +297 -0
  21. data/claude-skill/references/TRANSFORMERS.md +282 -0
  22. data/context/01-core-architecture.md +247 -0
  23. data/context/02-query-types-implementation.md +355 -0
  24. data/context/03-composition-transformation.md +441 -0
  25. data/context/04-pagination-results.md +485 -0
  26. data/context/05-testing-configuration.md +491 -0
  27. data/context/06-advanced-patterns-examples.md +153 -0
  28. data/gemfiles/rails_8.0.gemfile +10 -5
  29. data/gemfiles/rails_8.1.gemfile +20 -0
  30. data/lib/generators/quo/install/USAGE +21 -0
  31. data/lib/generators/quo/install/install_generator.rb +63 -0
  32. data/lib/quo/collection_backed_query.rb +21 -15
  33. data/lib/quo/collection_results.rb +1 -0
  34. data/lib/quo/composed_collection_backed_query.rb +42 -0
  35. data/lib/quo/composed_instance.rb +144 -0
  36. data/lib/quo/composed_query.rb +43 -178
  37. data/lib/quo/composed_relation_backed_query.rb +42 -0
  38. data/lib/quo/composing/base_strategy.rb +22 -0
  39. data/lib/quo/composing/class_strategy.rb +86 -0
  40. data/lib/quo/composing/class_strategy_registry.rb +31 -0
  41. data/lib/quo/composing/query_classes_strategy.rb +38 -0
  42. data/lib/quo/composing.rb +81 -0
  43. data/lib/quo/engine.rb +1 -0
  44. data/lib/quo/minitest/helpers.rb +14 -24
  45. data/lib/quo/preloadable.rb +1 -0
  46. data/lib/quo/query.rb +22 -5
  47. data/lib/quo/relation_backed_query.rb +24 -18
  48. data/lib/quo/relation_backed_query_specification.rb +44 -25
  49. data/lib/quo/relation_results.rb +1 -0
  50. data/lib/quo/results.rb +31 -2
  51. data/lib/quo/rspec/helpers.rb +15 -26
  52. data/lib/quo/testing/collection_backed_fake.rb +1 -0
  53. data/lib/quo/testing/fake_helpers.rb +30 -0
  54. data/lib/quo/testing/relation_backed_fake.rb +1 -0
  55. data/lib/quo/version.rb +1 -1
  56. data/lib/quo/wrapped_collection_backed_query.rb +21 -0
  57. data/lib/quo/wrapped_relation_backed_query.rb +21 -0
  58. data/lib/quo.rb +8 -0
  59. data/quo.png +0 -0
  60. data/sig/generated/quo/collection_backed_query.rbs +10 -4
  61. data/sig/generated/quo/collection_results.rbs +1 -0
  62. data/sig/generated/quo/composed_collection_backed_query.rbs +25 -0
  63. data/sig/generated/quo/composed_instance.rbs +61 -0
  64. data/sig/generated/quo/composed_query.rbs +23 -56
  65. data/sig/generated/quo/composed_relation_backed_query.rbs +25 -0
  66. data/sig/generated/quo/composing/base_strategy.rbs +16 -0
  67. data/sig/generated/quo/composing/class_strategy.rbs +38 -0
  68. data/sig/generated/quo/composing/class_strategy_registry.rbs +16 -0
  69. data/sig/generated/quo/composing/query_classes_strategy.rbs +24 -0
  70. data/sig/generated/quo/composing.rbs +40 -0
  71. data/sig/generated/quo/engine.rbs +1 -0
  72. data/sig/generated/quo/minitest/helpers.rbs +12 -0
  73. data/sig/generated/quo/preloadable.rbs +1 -0
  74. data/sig/generated/quo/query.rbs +15 -4
  75. data/sig/generated/quo/relation_backed_query.rbs +15 -5
  76. data/sig/generated/quo/relation_backed_query_specification.rbs +47 -39
  77. data/sig/generated/quo/relation_results.rbs +1 -0
  78. data/sig/generated/quo/results.rbs +11 -0
  79. data/sig/generated/quo/rspec/helpers.rbs +12 -0
  80. data/sig/generated/quo/testing/collection_backed_fake.rbs +1 -0
  81. data/sig/generated/quo/testing/fake_helpers.rbs +14 -0
  82. data/sig/generated/quo/testing/relation_backed_fake.rbs +1 -0
  83. data/sig/generated/quo/wrapped_collection_backed_query.rbs +13 -0
  84. data/sig/generated/quo/wrapped_relation_backed_query.rbs +13 -0
  85. data/sig/generated/quo.rbs +1 -0
  86. data/website/.gitignore +6 -0
  87. data/website/.nojekyll +0 -0
  88. data/website/404.html +26 -0
  89. data/website/Gemfile +24 -0
  90. data/website/_config.yml +50 -0
  91. data/website/_data/navigation.yml +8 -0
  92. data/website/_data/sidebar.yml +2 -0
  93. data/website/_data/social_links.yml +3 -0
  94. data/website/_docs/api.md +261 -0
  95. data/website/_docs/get-started.md +289 -0
  96. data/website/assets/quo.png +0 -0
  97. data/website/index.md +141 -0
  98. metadata +70 -13
  99. data/gemfiles/rails_7.0.gemfile +0 -15
  100. data/gemfiles/rails_7.1.gemfile +0 -15
  101. data/gemfiles/rails_7.2.gemfile +0 -15
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f3e56abbd1094d91702a10b327e82b05a28d7fc44e491316c438059de2c0c229
4
- data.tar.gz: 7ebdcfb6b774c274ae30e7eaff556dae898be1f2a8478c5ee1544cfde41dcd25
3
+ metadata.gz: 5075f8c25c83bed882ac8bdcf8d4d8134ba9855c86daa719c00bc55bc4e1c2f0
4
+ data.tar.gz: 0f1ddf08726dab47fe471241a577891444f2a50007587c65889e189d3cfb48b7
5
5
  SHA512:
6
- metadata.gz: ff1b0856ab186d6adfab0274aac9b94a61a0ece421716726b15c762a097f46d2566bcbe53fb04c589bb6d9eb708d91b65303b5cee419c0ae0cdeab193483089f
7
- data.tar.gz: 4befea0da9dcbdb6a71f4e447b871e9f0424738f50908bef479f3fd89a2401a5fc861a7ddf124c36ffbfdec5e871b03a0fd75ffcf2941e861465e0ede1ef0f6e
6
+ metadata.gz: 8a9ccd8cefa74d620d371c0b4a6981acce0335e9122c3552c6273804ad4bd9081868b98da0497214bf93fae921e1394a692ccd87f0369c7666a38a3cae3bc36d
7
+ data.tar.gz: efafd7e1e2749187cc1cb1788ab59bb80d119f1476b4d0d3172abf2e08f17beaae5f254a620571b1b23a87e0af36d1776a0f584e89a83fdc435da31de501a05b
@@ -0,0 +1,17 @@
1
+ # Make sure RUBY_VERSION matches the Ruby version in .ruby-version or gemspec
2
+ ARG RUBY_VERSION=3.4.2
3
+ FROM ghcr.io/rails/devcontainer/images/ruby:$RUBY_VERSION
4
+
5
+ USER root
6
+
7
+ # Install pkg-config and SQLite development libraries
8
+ RUN apt-get update -qq && \
9
+ apt-get install -y pkg-config libsqlite3-dev && \
10
+ apt-get clean && \
11
+ rm -rf /var/lib/apt/lists/*
12
+
13
+ USER vscode
14
+
15
+ # Ensure binding is always 0.0.0.0
16
+ # Binds the server to all IP addresses of the container, so it can be accessed from outside the container.
17
+ ENV BINDING="0.0.0.0"
@@ -0,0 +1,10 @@
1
+ name: "quo"
2
+
3
+ services:
4
+ quo-dev-env:
5
+ container_name: quo-dev-env
6
+ build:
7
+ context: ..
8
+ dockerfile: .devcontainer/Dockerfile
9
+ ports:
10
+ - "3000"
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "Quo Gem Development",
3
+ "dockerComposeFile": "compose.yml",
4
+ "service": "quo-dev-env",
5
+ "containerEnv": {
6
+ "RAILS_ENV": "development"
7
+ },
8
+ "forwardPorts": [3000],
9
+ "postCreateCommand": "bundle install && bundle exec appraisal install",
10
+ "postStartCommand": "bundle exec appraisal rake test",
11
+ "remoteUser": "vscode"
12
+ }
data/Appraisals CHANGED
@@ -1,15 +1,7 @@
1
- appraise "rails-7.0" do
2
- gem "rails", "~> 7.0"
3
- end
4
-
5
- appraise "rails-7.1" do
6
- gem "rails", "~> 7.1"
7
- end
8
-
9
- appraise "rails-7.2" do
10
- gem "rails", "~> 7.2"
1
+ appraise "rails-8.0" do
2
+ gem "rails", "~> 8.0.0"
11
3
  end
12
4
 
13
- appraise "rails-8.0" do
14
- gem "rails", "~> 8.0"
5
+ appraise "rails-8.1" do
6
+ gem "rails", "~> 8.1.0"
15
7
  end
data/CHANGELOG.md CHANGED
@@ -1,7 +1,117 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [2.0.0] - 2026-05-12
3
4
 
4
- ## [1.0.0.rc1] - Unreleased
5
+ ### Breaking
6
+
7
+ - Minimum Rails 8.0; minimum Literal 1.9.
8
+ - Instance composition (`q1 + q2`) returns a value (`Quo::ComposedRelationBackedQuery` / `Quo::ComposedCollectionBackedQuery`), not an anonymous class. No more per-call class allocation.
9
+ - Each operand keeps its own constructor-time props; merge is the AND of each side's filter (1.x's prop fan-out is gone).
10
+ - Pagination inherits as a coupled `(page, page_size)` pair from whichever operand is paginated (right wins).
11
+ - `wrap` is type-strict at definition time. `Quo::RelationBackedQuery.wrap` requires an `ActiveRecord::Relation` or `Quo::RelationBackedQuery`; `Quo::CollectionBackedQuery.wrap` requires `Enumerable` or `Quo::CollectionBackedQuery`. Block forms still defer.
12
+
13
+ ### Added
14
+
15
+ - `Quo::RelationBackedQuery.from(relation)` and `Quo::CollectionBackedQuery.from(enumerable)` value-form constructors. Zero class allocation per call.
16
+ - `copy(prop: value)` on a composed instance fans the override across every operand declaring the prop, recursing into composed operands.
17
+ - Composed and wrapped value-form classes inherit from `Quo.relation_backed_query_base_class` / `Quo.collection_backed_query_base_class`.
18
+
19
+ ### Fixed
20
+
21
+ - #5 — `Quo::RelationBackedQuerySpecification#joins` / `#left_outer_joins` accept multiple tables.
22
+ - #6 — `_specification` survives `Query#copy` and merge paths.
23
+ - #7 — `Quo::Results` Enumerable delegation returns transformed items for filter-style methods (`select`, `reject`, `find`, `sort_by`, `partition`, …) where the block already saw transformed items.
24
+ - #8 — `wrap` rejects the wrong type at definition time with a message pointing at the matching constructor.
25
+
26
+ ### Removed
27
+
28
+ - Unused per-call `Quo::Composing::InstanceStrategyRegistry` + the four instance-strategy classes.
29
+
30
+ ## [1.0.1] - 2026-05-12
31
+
32
+ ### Fixed
33
+
34
+ - Pin the `literal` dependency to `~> 1.6.0` in the gemspec. Quo's
35
+ internals reference `Literal::Null` (renamed to `Literal::Undefined`
36
+ in Literal 1.7) and `Literal::Types::ConstraintType.new` (signature
37
+ changed in Literal 1.7). Users whose lockfile resolved Literal to
38
+ 1.7+ saw `NameError: uninitialized constant Literal::Null` or
39
+ `ArgumentError` at runtime / in CI. The 1.x line is now firmly on
40
+ Literal 1.6.x; a broader range targeting newer Literal versions is
41
+ planned for 2.0.
42
+ - Defensive handling of the sentinel rename in `Quo::Query` so the
43
+ `LITERAL_UNSET` constant resolves to whichever name exists at load
44
+ time. This is belt-and-braces on top of the pin.
45
+
46
+ ## [1.0.0] - 2026-05-06
47
+
48
+ First stable release. Folds in the perf work and tooling shipped in beta3.
49
+
50
+ ### Added
51
+
52
+ - **Claude Code skill bundled with the gem.** A skill at `claude-skill/`
53
+ teaches Claude Code how to build composable, type-safe query objects
54
+ with Quo. The skill's `SKILL.md` covers the most important Quo concept
55
+ to get right — class-vs-instance composition, when to use each, and the
56
+ performance trade-offs involved. Reference files in
57
+ `claude-skill/references/` go deep on query types, composition, pagination,
58
+ transformers, and the API surface.
59
+ - **Rails install generator.** `bin/rails generate quo:install` copies the
60
+ bundled skill into the host app's `.claude/skills/quo/` directory.
61
+ Re-running with `--force` refreshes the skill after a Quo upgrade.
62
+ Optional `--with-claude-md` opt-in appends a short pointer to the
63
+ project's top-level `CLAUDE.md`. The fragment is idempotent — safe to
64
+ re-run.
65
+
66
+ ## [1.0.0.beta3] - 2026-05-06
67
+
68
+ ### Performance
69
+
70
+ Composition is materially cheaper. Constructing and resolving a composed query
71
+ no longer reallocates per-call infrastructure that the Ruby/Literal class
72
+ hierarchy already provides for free. On a representative composition graph
73
+ (four-level tree, 16 page-renders), measured improvements over `1.0.0.beta2`:
74
+
75
+ - ~46% lower wall-clock time
76
+ - ~51% less time spent in GC
77
+ - ~38% fewer total allocations
78
+
79
+ No public API changes. The wins come from five internal fixes:
80
+
81
+ - `Quo::Composing` now reuses singleton class- and instance-strategy
82
+ registries instead of allocating fresh ones on every `composer` /
83
+ `merge_instances` call.
84
+ - The class-level helpers on composed query classes (`_composing_joins`,
85
+ `_left_query`, `inspect`, `quo_operand_desc`, etc.) live on a new
86
+ `Quo::ComposedQuery::ClassMethods` module that is `extend`ed via the
87
+ `included` hook, rather than being re-defined inside the per-class
88
+ `Class.new { class << self; ... end }` block.
89
+ - `Quo::Composing::ClassStrategy#collect_properties` now skips properties
90
+ that the composed class's chosen superclass already declares — Literal
91
+ inherits these automatically, and re-registering them on every anonymous
92
+ class did a full `Literal::Property` allocation, schema dup, and
93
+ `module_eval` for no behavioural gain. This is the dominant win.
94
+ - `Quo::RelationBackedQuery#respond_to_missing?` now reuses the memoized
95
+ `RelationBackedQuerySpecification.blank` singleton instead of allocating
96
+ a fresh specification on every probe (ActiveRecord's delegation chain
97
+ hits `respond_to?` heavily).
98
+ - `Quo::ComposedQuery#left` / `#right` now pass `_specification:` to the
99
+ child constructor directly. Previously they always allocated the child
100
+ via `.new`, then allocated a second copy via `.with_specification(...)`
101
+ (which calls `copy(...)`), even when the specification was `nil`.
102
+
103
+ ## [1.0.0.beta2] - 2025-04-01
104
+
105
+ ### Breaking Changes
106
+
107
+ - `Quo::ComposedQuery.composer` is now `Quo::Composing.composer`
108
+ - `Quo::ComposedQuery.merge_instances` is now `Quo::Composing.merge_instances`
109
+
110
+ ### Fixed
111
+
112
+ - Fixed issue with handling of query specifications in the query composer
113
+
114
+ ## [1.0.0.beta1] - 2025-04-01
5
115
 
6
116
  ### Breaking Changes
7
117
 
@@ -25,6 +135,7 @@ Nearly everything has had changes. Porting will require some effort.
25
135
  ### Added
26
136
 
27
137
  - Helpers `stub_query` and `mock_query` for Minitest
138
+ - Support for Rails 8
28
139
 
29
140
  ## [0.5.0] - 2022-12-23
30
141
 
data/CLAUDE.md ADDED
@@ -0,0 +1,19 @@
1
+ # Quo Development Guide
2
+
3
+ ## Build & Test Commands
4
+ - Run all tests: `bundle exec rake test`
5
+ - Run a single test: `bundle exec ruby -Ilib:test test/path/to/test_file.rb -n test_method_name`
6
+ - Run tests across Rails versions: `bundle exec appraisal rake test`
7
+ - Type checking: `bundle exec steep check`
8
+ - Lint code: `bundle exec standardrb`
9
+ - Fix lint issues: `bundle exec standardrb --fix`
10
+
11
+ ## Code Style Guidelines
12
+ - **Frozen String Literals**: Include `# frozen_string_literal: true` at the top of every file
13
+ - **Types**: Use RBS for type annotations with `# rbs_inline: enabled` and `@rbs` annotations
14
+ - **Naming**: Use snake_case for methods/variables, CamelCase for classes, and SCREAMING_CASE for constants
15
+ - **Error Handling**: Raise specific errors with clear messages
16
+ - **Indentation**: 2 spaces (default Standard Ruby style)
17
+ - **Testing**: Use Minitest for tests
18
+ - **Framework**: Built on Literal gem - use Literal::Struct and Literal::Types
19
+ - **Documentation**: Document public methods with comments
data/Gemfile CHANGED
@@ -5,7 +5,7 @@ source "https://rubygems.org"
5
5
  # Specify your gem's dependencies in quo.gemspec
6
6
  gemspec
7
7
 
8
- gem "rails", "~> 7.2"
8
+ gem "rails", "~> 8"
9
9
 
10
10
  group :development, :test do
11
11
  gem "sqlite3"
@@ -14,9 +14,15 @@ group :development, :test do
14
14
 
15
15
  gem "minitest", "~> 5.0"
16
16
 
17
+ gem "simplecov", require: false
18
+
17
19
  gem "standard", require: false
18
20
 
19
21
  gem "steep", require: false
20
22
 
21
23
  gem "rbs-inline", "~> 0.11.0", require: false
24
+
25
+ gem "simplecov-small-badge", require: false
26
+ gem "rubycritic-small-badge", require: false
27
+ gem "repo-small-badge"
22
28
  end
data/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2022-2024 Stephen Ierodiaconou
3
+ Copyright (c) 2022-2025 Stephen Ierodiaconou
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal