appquery 0.7.0 → 0.8.0.rc1

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 (29) hide show
  1. checksums.yaml +4 -4
  2. data/.yard/templates/default/layout/html/footer.erb +7 -0
  3. data/CHANGELOG.md +5 -0
  4. data/lib/app_query/base_query.rb +1 -5
  5. data/lib/app_query/paginatable.rb +79 -2
  6. data/lib/app_query/railtie.rb +12 -0
  7. data/lib/app_query/rspec/helpers.rb +85 -59
  8. data/lib/app_query/rspec.rb +12 -0
  9. data/lib/app_query/version.rb +1 -1
  10. data/lib/app_query.rb +195 -11
  11. data/lib/generators/app_query/USAGE +28 -0
  12. data/lib/generators/app_query/example_generator.rb +38 -0
  13. data/lib/generators/app_query/query_generator.rb +43 -0
  14. data/lib/generators/app_query/templates/application_query.rb.tt +7 -0
  15. data/lib/generators/app_query/templates/example.sql.erb.tt +41 -0
  16. data/lib/generators/app_query/templates/example_query.rb.tt +37 -0
  17. data/lib/generators/app_query/templates/query.rb.tt +9 -0
  18. data/lib/generators/app_query/templates/query.sql.tt +1 -0
  19. data/lib/generators/query_generator.rb +10 -0
  20. data/lib/generators/rspec/app_query_example_generator.rb +13 -0
  21. data/lib/{rails/generators/rspec/query_generator.rb → generators/rspec/app_query_generator.rb} +4 -10
  22. data/lib/generators/rspec/query_generator.rb +13 -0
  23. data/lib/generators/rspec/templates/app_query_example_spec.rb.tt +140 -0
  24. data/lib/generators/rspec/templates/app_query_spec.rb.tt +9 -0
  25. metadata +18 -7
  26. data/lib/rails/generators/query/USAGE +0 -10
  27. data/lib/rails/generators/query/query_generator.rb +0 -20
  28. data/lib/rails/generators/query/templates/query.sql.tt +0 -14
  29. data/lib/rails/generators/rspec/templates/query_spec.rb.tt +0 -12
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f5eeb81f992ea777b44680277fcf8ef0d763aa1e8d4d6b1750eb81f9198d5262
4
- data.tar.gz: d9939585c40818303e0da886c459ad65c030b2501f8399942cf5b0b2a6d308ab
3
+ metadata.gz: 220f74299300b813e054b9ef57172083795b14209322ee9d5fbcb9d3de608a04
4
+ data.tar.gz: '064268f658576f5bf82e05dc97bd51138ccf4c903800296f18eee1c1c4f1c91f'
5
5
  SHA512:
6
- metadata.gz: 8f5a4abb4f4e38477dba038d927ac19bef2c0b252f41d820e78706ab60bd2d22dcd07ee739781266fa52bb86317f44347a98bae1e109159c5a805c778534602c
7
- data.tar.gz: 73f165536629ca28cd7077156ed67ee5b78f6b750fe7c69d62d98c8c6005ba004b770972b600378f7a05be36bb7a5bc214f2bf55a8c7695f509d76971a37dd3d
6
+ metadata.gz: b79adb014601b0f5c414c2f22935010bcac7b98ca9b452bbdb474ff9208d93f4868866f4a4740c5a8cbcf4bf652559bfc6f53927974b747559ae5aa9379fed8c
7
+ data.tar.gz: b30ce78b6188bb08b9013655980e54b9e49243f9a69c2a9e5dddc5384ed73508374cea1cdd35e2fa6fd36c792ca0f594f26180b9ed9ac93e5d0d772c003e5344
@@ -0,0 +1,7 @@
1
+ <div id="footer">
2
+ AppQuery <%= ENV["APPQUERY_VERSION"] || `git describe --tags --abbrev=0 2>/dev/null`.strip %>
3
+ &mdash;
4
+ Generated on <%= Time.now.strftime("%c") %> by
5
+ <a href="https://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
6
+ <%= YARD::VERSION %> (ruby-<%= RUBY_VERSION %>).
7
+ </div>
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## 0.7.0
4
+
5
+ **Releasedate**: 8-1-2026
6
+ **Rubygems**: https://rubygems.org/gems/appquery/versions/0.7.0
7
+
3
8
  ### 💥 Breaking Changes
4
9
 
5
10
  - ⛔ drop Ruby 3.2 support
@@ -154,11 +154,7 @@ module AppQuery
154
154
  end
155
155
  end
156
156
 
157
- delegate :select_all, :select_one, :count, :to_s, :column, :first, :ids, to: :query
158
-
159
- def entries
160
- select_all
161
- end
157
+ delegate :cte, :entries, :with_select, :select_all, :select_one, :count, :to_s, :column, :first, :ids, :copy_to, to: :query
162
158
 
163
159
  def query
164
160
  @query ||= base_query
@@ -46,11 +46,23 @@ module AppQuery
46
46
  extend ActiveSupport::Concern
47
47
 
48
48
  # Kaminari-compatible wrapper for paginated results.
49
+ #
50
+ # Wraps an array of records with pagination metadata, providing a consistent
51
+ # interface for both counted and uncounted pagination modes.
52
+ #
53
+ # Includes Enumerable, so all standard iteration methods work directly.
54
+ #
55
+ # @example
56
+ # result = ArticlesQuery.new.paginate(page: 2).entries
57
+ # result.each { |article| puts article["title"] }
58
+ # result.current_page # => 2
59
+ # result.total_pages # => 5
49
60
  class PaginatedResult
50
61
  include Enumerable
51
62
 
52
63
  delegate :each, :size, :[], :empty?, :first, :last, to: :@records
53
64
 
65
+ # @api private
54
66
  def initialize(records, page:, per_page:, total_count: nil, has_next: nil)
55
67
  @records = records
56
68
  @page = page
@@ -59,23 +71,31 @@ module AppQuery
59
71
  @has_next = has_next
60
72
  end
61
73
 
74
+ # @return [Integer] the current page number
62
75
  def current_page = @page
63
76
 
77
+ # @return [Integer] the number of records per page
64
78
  def limit_value = @per_page
65
79
 
80
+ # @return [Integer, nil] the previous page number, or nil if on first page
66
81
  def prev_page = (@page > 1) ? @page - 1 : nil
67
82
 
83
+ # @return [Boolean] true if this is the first page
68
84
  def first_page? = @page == 1
69
85
 
86
+ # @return [Integer] the total number of records across all pages
87
+ # @raise [RuntimeError] if called in +without_count+ mode
70
88
  def total_count
71
89
  @total_count || raise("total_count not available in without_count mode")
72
90
  end
73
91
 
92
+ # @return [Integer, nil] the total number of pages, or nil in +without_count+ mode
74
93
  def total_pages
75
94
  return nil unless @total_count
76
95
  (@total_count.to_f / @per_page).ceil
77
96
  end
78
97
 
98
+ # @return [Integer, nil] the next page number, or nil if on last page
79
99
  def next_page
80
100
  if @total_count
81
101
  (@page < total_pages) ? @page + 1 : nil
@@ -84,6 +104,7 @@ module AppQuery
84
104
  end
85
105
  end
86
106
 
107
+ # @return [Boolean] true if this is the last page
87
108
  def last_page?
88
109
  if @total_count
89
110
  @page >= total_pages
@@ -92,10 +113,20 @@ module AppQuery
92
113
  end
93
114
  end
94
115
 
116
+ # @return [Boolean] true if the requested page is beyond available data
95
117
  def out_of_range?
96
118
  empty? && @page > 1
97
119
  end
98
120
 
121
+ # Transforms each record in place using the given block.
122
+ #
123
+ # @yield [record] Block to transform each record
124
+ # @yieldparam record [Hash] the record to transform
125
+ # @yieldreturn [Object] the transformed record
126
+ # @return [self] for chaining
127
+ #
128
+ # @example
129
+ # result.transform! { |row| OpenStruct.new(row) }
99
130
  def transform!
100
131
  @records = @records.map { |r| yield(r) }
101
132
  self
@@ -108,6 +139,20 @@ module AppQuery
108
139
  end
109
140
 
110
141
  class_methods do
142
+ # Gets or sets the default number of records per page.
143
+ #
144
+ # When called without arguments, returns the current per_page value
145
+ # (inheriting from superclass if not set, defaulting to 25).
146
+ #
147
+ # @param value [Integer, nil] the number of records per page (setter)
148
+ # @return [Integer] the current per_page value (getter)
149
+ #
150
+ # @example
151
+ # class ArticlesQuery < ApplicationQuery
152
+ # per_page 10
153
+ # end
154
+ #
155
+ # ArticlesQuery.per_page # => 10
111
156
  def per_page(value = nil)
112
157
  if value.nil?
113
158
  return @per_page if defined?(@per_page)
@@ -118,6 +163,18 @@ module AppQuery
118
163
  end
119
164
  end
120
165
 
166
+ # Enables pagination for this query.
167
+ #
168
+ # @param page [Integer] page number, starting at 1
169
+ # @param per_page [Integer] records per page (defaults to class setting)
170
+ # @param without_count [Boolean] skip COUNT query for large datasets
171
+ # @return [self] for chaining
172
+ #
173
+ # @example Standard pagination with total count
174
+ # ArticlesQuery.new.paginate(page: 2, per_page: 20).entries
175
+ #
176
+ # @example Fast pagination without count (for large tables)
177
+ # ArticlesQuery.new.paginate(page: 1, without_count: true).entries
121
178
  def paginate(page: 1, per_page: self.class.per_page, without_count: false)
122
179
  @page = page
123
180
  @per_page = per_page
@@ -125,28 +182,48 @@ module AppQuery
125
182
  self
126
183
  end
127
184
 
185
+ # Disables pagination, returning all results.
186
+ #
187
+ # @return [self] for chaining
188
+ #
189
+ # @example
190
+ # ArticlesQuery.new.unpaginated.entries # => all records
128
191
  def unpaginated
129
192
  @page = nil
130
193
  @per_page = nil
131
194
  self
132
195
  end
133
196
 
197
+ # Executes the query and returns paginated results.
198
+ #
199
+ # @return [PaginatedResult] when pagination is enabled
200
+ # @return [Array<Hash>] when unpaginated
134
201
  def entries
135
202
  @_entries ||= build_paginated_result(super)
136
203
  end
137
204
 
205
+ # Returns the total count of records (without pagination).
206
+ #
207
+ # Executes a separate COUNT query. Result is memoized.
208
+ #
209
+ # @return [Integer] total number of records
138
210
  def total_count
139
211
  @_total_count ||= unpaginated_query.count
140
212
  end
141
213
 
214
+ private
215
+
216
+ # Returns the underlying query without pagination applied.
217
+ #
218
+ # Useful for getting total counts or building derived queries.
219
+ #
220
+ # @return [AppQuery::Q] the unpaginated query object
142
221
  def unpaginated_query
143
222
  base_query
144
223
  .render(**render_vars, page: nil)
145
224
  .with_binds(**bind_vars)
146
225
  end
147
226
 
148
- private
149
-
150
227
  def build_paginated_result(entries)
151
228
  return entries unless @page # No pagination requested
152
229
 
@@ -0,0 +1,12 @@
1
+ module AppQuery
2
+ class Railtie < ::Rails::Railtie # :nodoc:
3
+ generators do
4
+ require_relative "../generators/app_query/query_generator"
5
+ require_relative "../generators/app_query/example_generator"
6
+ require_relative "../generators/query_generator"
7
+ require_relative "../generators/rspec/app_query_generator"
8
+ require_relative "../generators/rspec/app_query_example_generator"
9
+ require_relative "../generators/rspec/query_generator"
10
+ end
11
+ end
12
+ end
@@ -1,56 +1,111 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module AppQuery
2
4
  module RSpec
5
+ # RSpec helpers for testing query classes.
6
+ #
7
+ # @example Basic usage
8
+ # RSpec.describe ProductsQuery, type: :query do
9
+ # it "returns products" do
10
+ # expect(described_query.entries).to be_present
11
+ # end
12
+ # end
13
+ #
14
+ # @example Testing a specific CTE
15
+ # RSpec.describe ProductsQuery, type: :query do
16
+ # describe "cte active_products" do
17
+ # it "only contains active products" do
18
+ # expect(described_query.entries).to all(include("active" => true))
19
+ # end
20
+ # end
21
+ # end
22
+ #
23
+ # @example With required binds
24
+ # RSpec.describe UsersQuery, type: :query, binds: {company_id: 1} do
25
+ # it "returns users for company" do
26
+ # expect(described_query.entries).to be_present
27
+ # end
28
+ # end
29
+ #
30
+ # @example With vars
31
+ # RSpec.describe ProductsQuery, type: :query do
32
+ # describe "as admin", vars: {admin: true} do
33
+ # it "returns all products" do
34
+ # expect(described_query.entries.size).to eq(3)
35
+ # end
36
+ # end
37
+ # end
3
38
  module Helpers
4
- def default_binds
5
- self.class.default_binds
6
- end
7
-
8
- def default_vars
9
- self.class.default_vars
10
- end
11
-
12
- def expand_select(s)
13
- s.gsub(":cte", cte_name)
14
- end
15
-
16
- def select_all(select: nil, binds: default_binds, **kws)
17
- @query_result = described_query(select:).select_all(binds:, **kws)
39
+ # Returns the query instance, optionally focused on a CTE.
40
+ #
41
+ # When inside a `describe "cte xxx"` block, returns a query
42
+ # that selects from that CTE instead of the full query.
43
+ #
44
+ # @param kwargs [Hash] arguments passed to {#build_query}
45
+ # @return [AppQuery::BaseQuery, AppQuery::Q] the query instance
46
+ #
47
+ # @example Override binds per-test
48
+ # expect(described_query(user_id: 123).entries).to include(...)
49
+ def described_query(**kwargs)
50
+ query = build_query(**kwargs)
51
+ cte_name ? query.query.cte(cte_name) : query
18
52
  end
19
53
 
20
- def select_one(select: nil, binds: default_binds, **kws)
21
- @query_result = described_query(select:).select_one(binds:, **kws)
54
+ # Builds the query instance. Override this to customize instantiation.
55
+ #
56
+ # @param kwargs [Hash] merged with {#query_binds} and {#query_vars}
57
+ # @return [AppQuery::BaseQuery] the query instance
58
+ #
59
+ # @example Custom build method
60
+ # def build_query(**kwargs)
61
+ # described_class.build(**query_binds.merge(query_vars).merge(kwargs))
62
+ # end
63
+ def build_query(**kwargs)
64
+ described_class.new(**query_binds.merge(query_vars).merge(kwargs))
22
65
  end
23
66
 
24
- def select_value(select: nil, binds: default_binds, **kws)
25
- @query_result = described_query(select:).select_value(binds:, **kws)
67
+ # Returns binds from RSpec metadata.
68
+ #
69
+ # @return [Hash] the binds hash
70
+ def query_binds
71
+ metadata_value(:binds) || {}
26
72
  end
27
73
 
28
- def described_query(select: nil)
29
- select ||= "SELECT * FROM :cte" if cte_name
30
- select &&= expand_select(select) if cte_name
31
- self.class.described_query.render(default_vars).with_select(select)
74
+ # Returns vars from RSpec metadata.
75
+ #
76
+ # @return [Hash] the vars hash
77
+ def query_vars
78
+ metadata_value(:vars) || {}
32
79
  end
33
80
 
81
+ # Returns the CTE name if inside a "cte xxx" describe block.
82
+ #
83
+ # @return [String, nil] the CTE name
34
84
  def cte_name
35
85
  self.class.cte_name
36
86
  end
37
87
 
38
- def query_name
39
- self.class.query_name
88
+ def self.included(klass)
89
+ klass.extend(ClassMethods)
40
90
  end
41
91
 
42
- def query_result
43
- @query_result
92
+ private
93
+
94
+ def metadata_value(key)
95
+ self.class.metadata_value(key)
44
96
  end
45
97
 
46
98
  module ClassMethods
47
- def described_query
48
- AppQuery[query_name]
99
+ def cte_name
100
+ descriptions.find { _1[/\Acte\s/i] }&.split&.last
101
+ end
102
+
103
+ def metadata_value(key)
104
+ metadatas.find { _1[key] }&.[](key)
49
105
  end
50
106
 
51
107
  def metadatas
52
- scope = is_a?(Class) ? self : self.class
53
- metahash = scope.metadata
108
+ metahash = metadata
54
109
  result = []
55
110
  loop do
56
111
  result << metahash
@@ -63,36 +118,7 @@ module AppQuery
63
118
  def descriptions
64
119
  metadatas.map { _1[:description] }
65
120
  end
66
-
67
- def query_name
68
- descriptions.find { _1[/(app)?query\s/i] }&.then { _1.split.last }
69
- end
70
-
71
- def cte_name
72
- descriptions.find { _1[/cte\s/i] }&.then { _1.split.last }
73
- end
74
-
75
- def default_binds
76
- metadatas.find { _1[:default_binds] }&.[](:default_binds) || []
77
- end
78
-
79
- def default_vars
80
- metadatas.find { _1[:default_vars] }&.[](:default_vars) || {}
81
- end
82
-
83
- def included(klass)
84
- super
85
- # Inject classmethods into the group.
86
- klass.extend(ClassMethods)
87
- # If the describe block is aimed at string or resource/provider class
88
- # then set the default subject to be the Chef run.
89
- # if klass.described_class.nil? || klass.described_class.is_a?(Class) && (klass.described_class < Chef::Resource || klass.described_class < Chef::Provider)
90
- # klass.subject { chef_run }
91
- # end
92
- end
93
121
  end
94
-
95
- extend ClassMethods
96
122
  end
97
123
  end
98
124
  end
@@ -2,4 +2,16 @@ require_relative "rspec/helpers"
2
2
 
3
3
  RSpec.configure do |config|
4
4
  config.include AppQuery::RSpec::Helpers, type: :query
5
+
6
+ # Enable SQL logging with `log: true` metadata
7
+ config.around(:each, type: :query) do |example|
8
+ if example.metadata[:log]
9
+ old_logger = ActiveRecord::Base.logger
10
+ ActiveRecord::Base.logger = Logger.new($stdout)
11
+ example.run
12
+ ActiveRecord::Base.logger = old_logger
13
+ else
14
+ example.run
15
+ end
16
+ end
5
17
  end
@@ -3,5 +3,5 @@
3
3
  module AppQuery
4
4
  # This should just contain the .dev of the upcoming version.
5
5
  # When doing the actual release, CI will write the tag here before pushing the gem.
6
- VERSION = "0.7.0"
6
+ VERSION = "0.8.0.rc1"
7
7
  end
data/lib/app_query.rb CHANGED
@@ -116,6 +116,26 @@ module AppQuery
116
116
  Q.new(full_path.read, name: "AppQuery #{query_name}", filename: full_path.to_s, **opts)
117
117
  end
118
118
 
119
+ # Creates a query that selects all columns from a table.
120
+ #
121
+ # Convenience method for quickly querying a table without writing SQL.
122
+ #
123
+ # @param name [Symbol, String] the table name
124
+ # @param opts [Hash] additional options passed to {Q#initialize}
125
+ # @return [Q] a new query object selecting from the table
126
+ #
127
+ # @example Basic usage
128
+ # AppQuery.table(:products).count
129
+ # AppQuery.table(:products).take(5)
130
+ #
131
+ # @example With binds
132
+ # AppQuery.table(:users, binds: {active: true})
133
+ # .select_all("SELECT * FROM :_ WHERE active = :active")
134
+ def self.table(name, **opts)
135
+ quoted = ActiveRecord::Base.connection.quote_table_name(name)
136
+ Q.new("SELECT * FROM #{quoted}", name: "AppQuery.table(#{name})", **opts)
137
+ end
138
+
119
139
  class Result < ActiveRecord::Result
120
140
  attr_accessor :cast
121
141
  alias_method :cast?, :cast
@@ -127,13 +147,13 @@ module AppQuery
127
147
  @hash_rows = [] if columns.empty?
128
148
  end
129
149
 
130
- def column(name = nil)
150
+ def column(name = nil, unique: false)
131
151
  return [] if empty?
132
152
  unless name.nil? || includes_column?(name)
133
153
  raise ArgumentError, "Unknown column #{name.inspect}. Should be one of #{columns.inspect}."
134
154
  end
135
155
  ix = name.nil? ? 0 : columns.index(name)
136
- rows.map { _1[ix] }
156
+ rows.map { _1[ix] }.then { unique ? _1.uniq! : _1 }
137
157
  end
138
158
 
139
159
  def size
@@ -184,7 +204,7 @@ module AppQuery
184
204
 
185
205
  def self.from_ar_result(r, cast = nil)
186
206
  if r.empty?
187
- EMPTY
207
+ r.columns.empty? ? EMPTY : new(r.columns, [], r.column_types)
188
208
  else
189
209
  cast &&= case cast
190
210
  when Array
@@ -433,6 +453,24 @@ module AppQuery
433
453
  end
434
454
  alias_method :first, :select_one
435
455
 
456
+ # Executes the query and returns the first n rows.
457
+ #
458
+ # @param n [Integer] the number of rows to return
459
+ # @param s [String, nil] optional SELECT to apply before taking
460
+ # @param binds [Hash, nil] bind parameters to add
461
+ # @param cast [Boolean, Hash, Array] type casting configuration
462
+ # @return [Array<Hash>] the first n rows as an array of hashes
463
+ #
464
+ # @example
465
+ # AppQuery("SELECT * FROM users ORDER BY created_at").take(5)
466
+ # # => [{"id" => 1, ...}, {"id" => 2, ...}, ...]
467
+ #
468
+ # @see #first
469
+ def take(n, s = nil, binds: {}, cast: self.cast)
470
+ with_select(s).select_all("SELECT * FROM :_ LIMIT #{n.to_i}", binds:, cast:).entries
471
+ end
472
+ alias_method :limit, :take
473
+
436
474
  # Executes the query and returns the first value of the first row.
437
475
  #
438
476
  # @param binds [Hash, nil] named bind parameters
@@ -515,6 +553,7 @@ module AppQuery
515
553
  # @param c [String, Symbol] the column name to extract
516
554
  # @param s [String, nil] optional SELECT to apply before extracting
517
555
  # @param binds [Hash, nil] bind parameters to add
556
+ # @param unique [Boolean] whether to have unique values
518
557
  # @return [Array] the column values
519
558
  #
520
559
  # @example Extract a single column
@@ -524,9 +563,32 @@ module AppQuery
524
563
  # @example With additional filtering
525
564
  # AppQuery("SELECT * FROM users").column(:email, "SELECT * FROM :_ WHERE active")
526
565
  # # => ["alice@example.com", "bob@example.com"]
527
- def column(c, s = nil, binds: {})
566
+ #
567
+ # @example Extract unique values
568
+ # AppQuery("SELECT * FROM products").column(:category, unique: true)
569
+ # # => ["Electronics", "Clothing", "Home"]
570
+ def column(c, s = nil, binds: {}, unique: false)
528
571
  quoted_column = ActiveRecord::Base.connection.quote_column_name(c)
529
- with_select(s).select_all("SELECT #{quoted_column} AS column FROM :_", binds:).column("column")
572
+ with_select(s).select_all("SELECT #{unique ? "DISTINCT" : ""} #{quoted_column} AS column FROM :_", binds:).column("column")
573
+ end
574
+
575
+ # Returns the column names from the query without fetching any rows.
576
+ #
577
+ # Uses `LIMIT 0` to get column metadata efficiently.
578
+ #
579
+ # @param s [String, nil] optional SELECT to apply before extracting
580
+ # @param binds [Hash, nil] bind parameters to add
581
+ # @return [Array<String>] the column names
582
+ #
583
+ # @example Get column names
584
+ # AppQuery("SELECT id, name, email FROM users").columns
585
+ # # => ["id", "name", "email"]
586
+ #
587
+ # @example From a CTE
588
+ # AppQuery("WITH t(a, b) AS (VALUES (1, 2)) SELECT * FROM t").columns
589
+ # # => ["a", "b"]
590
+ def columns(s = nil, binds: {})
591
+ with_select(s).select_all("SELECT * FROM :_ LIMIT 0", binds:).columns
530
592
  end
531
593
 
532
594
  # Returns an array of id values from the query.
@@ -571,11 +633,6 @@ module AppQuery
571
633
  # @param returning [String, nil] columns to return (Rails 7.1+ only)
572
634
  # @return [Integer, Object] the inserted ID or returning value
573
635
  #
574
- # @example With positional binds
575
- # AppQuery(<<~SQL).insert(binds: ["Let's learn SQL!"])
576
- # INSERT INTO videos(title, created_at, updated_at) VALUES($1, now(), now())
577
- # SQL
578
- #
579
636
  # @example With values helper
580
637
  # articles = [{title: "First", created_at: Time.current}]
581
638
  # AppQuery(<<~SQL).render(articles:).insert
@@ -666,6 +723,104 @@ module AppQuery
666
723
  raise UnrenderedQueryError, "Query is ERB. Use #render before deleting."
667
724
  end
668
725
 
726
+ # Executes COPY TO STDOUT for efficient data export.
727
+ #
728
+ # PostgreSQL-only. Uses raw connection for streaming. Raises an error
729
+ # when used with SQLite or other non-PostgreSQL adapters.
730
+ #
731
+ # @param s [String, nil] optional SELECT to apply before extracting
732
+ # @param format [:csv, :text, :binary] output format (default: :csv)
733
+ # @param header [Boolean] include column headers (default: true, CSV only)
734
+ # @param delimiter [Symbol, nil] field delimiter - :tab, :comma, :pipe, :semicolon (default: format's default)
735
+ # @param dest [String, IO, nil] destination - file path, IO object, or nil to return string
736
+ # @param binds [Hash] bind parameters
737
+ # @return [String, Integer, nil] CSV string if dest: nil, bytes written if dest: path, nil if dest: IO
738
+ #
739
+ # @example Return as string
740
+ # csv = AppQuery[:users].copy_to
741
+ #
742
+ # @example Write to file path
743
+ # AppQuery[:users].copy_to(dest: "export.csv")
744
+ #
745
+ # @example Write to IO object
746
+ # File.open("export.csv", "w") { |f| query.copy_to(dest: f) }
747
+ #
748
+ # @example Export in Rails controller
749
+ # respond_to do |format|
750
+ # format.html do
751
+ # @invoices = query.entries
752
+ #
753
+ # render :index
754
+ # end
755
+ #
756
+ # format.csv do
757
+ # response.headers['Content-Type'] = 'text/csv'
758
+ # response.headers['Content-Disposition'] = 'attachment; filename="invoices.csv"'
759
+ #
760
+ # query.unpaginated.copy_to(dest: response.stream)
761
+ # end
762
+ # end
763
+ #
764
+ # @raise [AppQuery::Error] if adapter is not PostgreSQL
765
+ def copy_to(s = nil, format: :csv, header: true, delimiter: nil, dest: nil, binds: {})
766
+ raw_conn = ActiveRecord::Base.connection.raw_connection
767
+ unless raw_conn.respond_to?(:copy_data)
768
+ raise Error, "copy_to requires PostgreSQL (current adapter does not support COPY)"
769
+ end
770
+
771
+ allowed_formats = %i[csv text binary]
772
+ unless allowed_formats.include?(format)
773
+ raise ArgumentError, "Invalid format: #{format.inspect}. Allowed: #{allowed_formats.join(", ")}"
774
+ end
775
+
776
+ delimiters = {tab: "\t", comma: ",", pipe: "|", semicolon: ";"}
777
+ if delimiter
778
+ if !delimiters.key?(delimiter)
779
+ raise ArgumentError, "Invalid delimiter: #{delimiter.inspect}. Allowed: #{delimiters.keys.join(", ")}"
780
+ elsif format == :binary
781
+ raise ArgumentError, "Delimiter not allowed for format :binary"
782
+ end
783
+ end
784
+
785
+ add_binds(**binds).with_select(s).render({}).then do |aq|
786
+ options = ["FORMAT #{format.to_s.upcase}"]
787
+ options << "HEADER" if header && format == :csv
788
+ options << "DELIMITER E'#{delimiters[delimiter]}'" if delimiter
789
+
790
+ inner_sql = ActiveRecord::Base.sanitize_sql_array([aq.to_s, aq.binds])
791
+ copy_sql = "COPY (#{inner_sql}) TO STDOUT WITH (#{options.join(", ")})"
792
+
793
+ case dest
794
+ when NilClass
795
+ output = +""
796
+ raw_conn.copy_data(copy_sql) do
797
+ while (row = raw_conn.get_copy_data)
798
+ output << row
799
+ end
800
+ end
801
+ # pg returns ASCII-8BIT, but CSV/text is UTF-8; binary stays as-is
802
+ (format == :binary) ? output : output.force_encoding(Encoding::UTF_8)
803
+ when String
804
+ bytes = 0
805
+ File.open(dest, "wb") do |f|
806
+ raw_conn.copy_data(copy_sql) do
807
+ while (row = raw_conn.get_copy_data)
808
+ bytes += f.write(row)
809
+ end
810
+ end
811
+ end
812
+ bytes
813
+ else
814
+ raw_conn.copy_data(copy_sql) do
815
+ while (row = raw_conn.get_copy_data)
816
+ dest.write(row)
817
+ end
818
+ end
819
+ nil
820
+ end
821
+ end
822
+ end
823
+
669
824
  # @!group Query Introspection
670
825
 
671
826
  # Returns the tokenized representation of the SQL.
@@ -690,8 +845,12 @@ module AppQuery
690
845
  # @example
691
846
  # AppQuery("WITH a AS (SELECT 1), b AS (SELECT 2) SELECT * FROM a, b").cte_names
692
847
  # # => ["a", "b"]
848
+ #
849
+ # @example Quoted identifiers are returned without quotes
850
+ # AppQuery('WITH "special*name" AS (SELECT 1) SELECT * FROM "special*name"').cte_names
851
+ # # => ["special*name"]
693
852
  def cte_names
694
- tokens.filter { _1[:t] == "CTE_IDENTIFIER" }.map { _1[:v] }
853
+ tokens.filter { _1[:t] == "CTE_IDENTIFIER" }.map { _1[:v].delete_prefix('"').delete_suffix('"') }
695
854
  end
696
855
 
697
856
  # @!group Query Transformation
@@ -812,6 +971,30 @@ module AppQuery
812
971
 
813
972
  # @!group CTE Manipulation
814
973
 
974
+ # Returns a new query focused on the specified CTE.
975
+ #
976
+ # Wraps the query to select from the named CTE, allowing you to
977
+ # inspect or test individual CTEs in isolation.
978
+ #
979
+ # @param name [Symbol, String] the CTE name to select from
980
+ # @return [Q] a new query selecting from the CTE
981
+ # @raise [ArgumentError] if the CTE doesn't exist
982
+ #
983
+ # @example Focus on a specific CTE
984
+ # query = AppQuery("WITH published AS (SELECT * FROM articles WHERE published) SELECT * FROM published")
985
+ # query.cte(:published).entries
986
+ #
987
+ # @example Chain with other methods
988
+ # ArticleQuery.new.cte(:active_articles).take(5)
989
+ def cte(name)
990
+ name = name.to_s
991
+ unless cte_names.include?(name)
992
+ raise ArgumentError, "Unknown CTE #{name.inspect}. Available: #{cte_names.inspect}"
993
+ end
994
+ quoted = ActiveRecord::Base.connection.quote_table_name(name)
995
+ with_select("SELECT * FROM #{quoted}")
996
+ end
997
+
815
998
  # Prepends a CTE to the beginning of the WITH clause.
816
999
  #
817
1000
  # If the query has no CTEs, wraps it with WITH. If the query already has
@@ -964,3 +1147,4 @@ rescue LoadError
964
1147
  end
965
1148
 
966
1149
  require_relative "app_query/rspec" if Object.const_defined? :RSpec
1150
+ require_relative "app_query/railtie" if defined?(Rails::Railtie)
@@ -0,0 +1,28 @@
1
+ Description:
2
+ Generates a query class with SQL file and spec.
3
+
4
+ Also generates ApplicationQuery if it doesn't exist.
5
+
6
+ Example:
7
+ rails generate app_query:query products
8
+ rails generate query products # shorthand alias
9
+
10
+ This creates:
11
+ Base: app/queries/application_query.rb (if not exists)
12
+ Class: app/queries/products_query.rb
13
+ SQL: app/queries/products.sql
14
+ Spec: spec/queries/products_query_spec.rb
15
+
16
+ With namespace:
17
+ rails generate query admin/reports
18
+
19
+ Creates:
20
+ Class: app/queries/admin/reports_query.rb
21
+ SQL: app/queries/admin/reports.sql
22
+ Spec: spec/queries/admin/reports_query_spec.rb
23
+
24
+ See also:
25
+ rails generate app_query:example
26
+
27
+ Generates a fully annotated example query demonstrating
28
+ binds, vars, CTEs, and RSpec testing patterns.
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppQuery
4
+ module Generators
5
+ class ExampleGenerator < Rails::Generators::Base
6
+ desc <<~DESC
7
+ Generates annotated example query demonstrating binds, vars, CTEs, and testing patterns.
8
+
9
+ See also:
10
+ rails generate query --help
11
+ DESC
12
+ source_root File.expand_path("templates", __dir__)
13
+
14
+ def create_application_query
15
+ return if File.exist?(application_query_path)
16
+
17
+ template "application_query.rb", application_query_path
18
+ end
19
+
20
+ def create_example_files
21
+ template "example_query.rb", File.join(query_path, "example_query.rb")
22
+ template "example.sql.erb", File.join(query_path, "example.sql.erb")
23
+ end
24
+
25
+ hook_for :test_framework, as: :app_query_example
26
+
27
+ private
28
+
29
+ def query_path
30
+ ::AppQuery.configuration.query_path
31
+ end
32
+
33
+ def application_query_path
34
+ File.join(query_path, "application_query.rb")
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppQuery
4
+ module Generators
5
+ class QueryGenerator < Rails::Generators::NamedBase
6
+ desc <<~DESC
7
+ Generates a query class with SQL file and spec.
8
+
9
+ See also:
10
+ rails generate query --help
11
+ DESC
12
+ source_root File.expand_path("templates", __dir__)
13
+
14
+ def create_application_query
15
+ return if File.exist?(application_query_path)
16
+
17
+ template "application_query.rb", application_query_path
18
+ end
19
+
20
+ def create_query_class
21
+ template "query.rb",
22
+ File.join(query_path, class_path, "#{file_name}_query.rb")
23
+ end
24
+
25
+ def create_query_file
26
+ template "query.sql",
27
+ File.join(query_path, class_path, "#{file_name}.sql")
28
+ end
29
+
30
+ hook_for :test_framework
31
+
32
+ private
33
+
34
+ def query_path
35
+ ::AppQuery.configuration.query_path
36
+ end
37
+
38
+ def application_query_path
39
+ File.join(query_path, "application_query.rb")
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ApplicationQuery < AppQuery::BaseQuery
4
+ # Include middleware for all queries:
5
+ # include AppQuery::Paginatable
6
+ # include AppQuery::Mappable
7
+ end
@@ -0,0 +1,41 @@
1
+ -- =============================================================================
2
+ -- ExampleQuery SQL Template
3
+ --
4
+ -- This file demonstrates AppQuery SQL patterns. Delete once familiar.
5
+ --
6
+ -- Key concepts:
7
+ -- - CTEs (WITH clauses) for organizing complex queries
8
+ -- - ∶bind_name for safe parameter binding
9
+ -- - ERB for conditional SQL generation
10
+ -- =============================================================================
11
+
12
+ -- == CTEs (Common Table Expressions)
13
+ -- Break complex queries into readable, testable pieces.
14
+ -- Each CTE can be tested independently in specs.
15
+ WITH products(id, name, supplier_id, active) AS (
16
+ -- In real code, this would be: SELECT * FROM products
17
+ VALUES (1, 'Widget', 100, true),
18
+ (2, 'Gadget', 100, false),
19
+ (3, 'Gizmo', 200, true)
20
+ ),
21
+ -- == Derived CTEs
22
+ -- Build on previous CTEs to filter/transform data.
23
+ active_products AS (
24
+ SELECT * FROM products WHERE active
25
+ )
26
+ -- == Final SELECT
27
+ -- The main query that uses the CTEs above.
28
+ SELECT * FROM active_products
29
+ -- == Conditional SQL with ERB
30
+ -- Use vars (like @admin) to conditionally include SQL.
31
+ -- Remember: vars are for trusted values only, not user input!
32
+ <%% unless @admin -%>
33
+ -- == Bind Parameters
34
+ -- Use :name syntax for safe parameter binding.
35
+ -- Binds are defined in the query class with `bind :supplier_id`.
36
+ -- non-admins: MUST bring supplier_id
37
+ WHERE (:supplier_id::INTEGER IS NOT NULL AND supplier_id = :supplier_id)
38
+ <%% else -%>
39
+ -- admin: MAY bring supplier_id
40
+ WHERE (:supplier_id::INTEGER IS NULL OR supplier_id = :supplier_id)
41
+ <%% end -%>
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ExampleQuery demonstrates all AppQuery::BaseQuery features.
4
+ #
5
+ # Usage:
6
+ # ExampleQuery.new(supplier_id: 1).entries
7
+ # ExampleQuery.new(supplier_id: 1, admin: true).entries
8
+ # ExampleQuery.new(supplier_id: 1).query.to_s # inspect SQL
9
+ #
10
+ # Delete this file once you're familiar with the patterns.
11
+ #
12
+ class ExampleQuery < ApplicationQuery
13
+ # == Binds
14
+ #
15
+ # Bind parameters are safe from SQL injection. Use for WHERE clauses.
16
+ # In SQL: WHERE supplier_id = :supplier_id
17
+ #
18
+ bind :supplier_id, default: nil
19
+
20
+ # == Vars
21
+ #
22
+ # Template variables for dynamic SQL generation (ERB).
23
+ # In SQL: <%% if @admin %>...<%% end %>
24
+ #
25
+ # WARNING: vars are NOT safe from injection - only use for trusted values
26
+ # like sorting options, not user input.
27
+ #
28
+ var :admin, default: false
29
+
30
+ # == Casts
31
+ #
32
+ # Automatically cast result columns to Ruby types.
33
+ # Supported: :date, :datetime, :time, :integer, :float, :decimal,
34
+ # :boolean, :json, :array (PostgreSQL)
35
+ #
36
+ # cast metadata: :json, published_at: :datetime
37
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ <% module_namespacing do -%>
4
+ class <%= class_name %>Query < ApplicationQuery
5
+ # bind :user_id # SQL: WHERE user_id = :user_id
6
+ # var :admin, default: false # ERB: <%% if @admin %>
7
+ # cast published_at: :date # Cast column to Ruby type
8
+ end
9
+ <% end -%>
@@ -0,0 +1 @@
1
+ SELECT * FROM <%= file_name %>
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "app_query/query_generator"
4
+
5
+ class QueryGenerator < AppQuery::Generators::QueryGenerator
6
+ # Hidden alias for app_query:query
7
+ # Usage: rails g query products
8
+ source_root AppQuery::Generators::QueryGenerator.source_root
9
+ hide!
10
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rspec
4
+ module Generators
5
+ class AppQueryExampleGenerator < Rails::Generators::Base
6
+ source_root File.expand_path("templates", __dir__)
7
+
8
+ def create_test_file
9
+ template "app_query_example_spec.rb", "spec/queries/example_query_spec.rb"
10
+ end
11
+ end
12
+ end
13
+ end
@@ -1,20 +1,14 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rspec
2
4
  module Generators
3
- class QueryGenerator < ::Rails::Generators::NamedBase
5
+ class AppQueryGenerator < Rails::Generators::NamedBase
4
6
  source_root File.expand_path("templates", __dir__)
5
7
 
6
8
  def create_test_file
7
- template "query_spec.rb",
9
+ template "app_query_spec.rb",
8
10
  File.join("spec/queries", class_path, "#{file_name}_query_spec.rb")
9
11
  end
10
-
11
- hide!
12
-
13
- private
14
-
15
- def query_path
16
- AppQuery.configuration.query_path
17
- end
18
12
  end
19
13
  end
20
14
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "app_query_generator"
4
+
5
+ module Rspec
6
+ module Generators
7
+ class QueryGenerator < AppQueryGenerator
8
+ # Hidden hook for query generator alias
9
+ source_root AppQueryGenerator.source_root
10
+ hide!
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails_helper"
4
+
5
+ # ExampleQuery Spec
6
+ #
7
+ # This demonstrates all AppQuery RSpec helpers. Delete once familiar.
8
+ #
9
+ # Setup: Add to spec/rails_helper.rb:
10
+ # require "app_query/rspec"
11
+ #
12
+ RSpec.describe ExampleQuery, type: :query do
13
+ # ============================================================================
14
+ # Basic Usage
15
+ # ============================================================================
16
+ #
17
+ # `described_query` instantiates the query class and returns results.
18
+ # It automatically uses binds/vars from metadata.
19
+
20
+ describe "basic query" do
21
+ it "returns an array of hashes" do
22
+ expect(described_query(admin: true).entries).to be_an(Array)
23
+ expect(described_query(admin: true).first).to be_a(Hash)
24
+ end
25
+ end
26
+
27
+ # ============================================================================
28
+ # Testing CTEs
29
+ # ============================================================================
30
+ #
31
+ # Use `describe "cte <name>"` to test a specific CTE in isolation.
32
+ # `described_query` will SELECT from that CTE instead of the full query.
33
+
34
+ describe "cte products" do
35
+ it "contains all products (including inactive)" do
36
+ expect(described_query.count).to eq(3)
37
+ end
38
+
39
+ it "has the expected columns" do
40
+ expect(described_query.columns).to contain_exactly(
41
+ "id", "name", "supplier_id", "active"
42
+ )
43
+ end
44
+ end
45
+
46
+ describe "cte active_products" do
47
+ it "only contains active products" do
48
+ expect(described_query.entries).to all(include("active" => true))
49
+ end
50
+
51
+ it "excludes inactive products" do
52
+ expect(described_query.entries.size).to eq(2)
53
+ end
54
+ end
55
+
56
+ # ============================================================================
57
+ # Passing Binds via Metadata
58
+ # ============================================================================
59
+ #
60
+ # Use `binds: {key: value}` in describe/context metadata.
61
+ # These are passed to the query class constructor.
62
+
63
+ describe "filtered by supplier", binds: {supplier_id: 100} do
64
+ it "returns only products for that supplier" do
65
+ expect(described_query.entries).to all(include("supplier_id" => 100))
66
+ end
67
+ end
68
+
69
+ # ============================================================================
70
+ # Passing Vars via Metadata
71
+ # ============================================================================
72
+ #
73
+ # Use `vars: {key: value}` for template variables.
74
+ # These control conditional SQL generation.
75
+
76
+ describe "as admin", vars: {admin: true} do
77
+ it "returns all active products regardless of supplier" do
78
+ expect(described_query.entries.size).to eq(2)
79
+ end
80
+
81
+ it "includes products from multiple suppliers" do
82
+ expect(described_query.column(:supplier_id, unique: true).count).to be > 1
83
+ end
84
+ end
85
+
86
+ # ============================================================================
87
+ # Combining Binds and Vars
88
+ # ============================================================================
89
+
90
+ describe "non-admin with supplier", binds: {supplier_id: 100}, vars: {admin: false} do
91
+ it "filters by supplier" do
92
+ expect(described_query.entries).to all(include("supplier_id" => 100))
93
+ end
94
+ end
95
+
96
+ # ============================================================================
97
+ # Per-Test Overrides
98
+ # ============================================================================
99
+ #
100
+ # Pass arguments to `described_query()` to override for a single test.
101
+
102
+ describe "per-test customization" do
103
+ it "can override binds inline" do
104
+ result = described_query(supplier_id: 200, admin: false)
105
+ expect(result.entries).to all(include("supplier_id" => 200))
106
+ end
107
+ end
108
+
109
+ # ============================================================================
110
+ # Custom Query Building
111
+ # ============================================================================
112
+ #
113
+ # Override `build_query` for custom instantiation patterns.
114
+
115
+ describe "custom build" do
116
+ def build_query(**kwargs)
117
+ # Example: always set admin to true in this context
118
+ described_class.new(admin: true, **kwargs)
119
+ end
120
+
121
+ it "uses the custom build method" do
122
+ # This will use admin: true from build_query
123
+ expect(described_query.entries.size).to eq(2)
124
+ end
125
+ end
126
+
127
+ # ============================================================================
128
+ # SQL Logging
129
+ # ============================================================================
130
+ #
131
+ # Add `log: true` to see the actual SQL being executed.
132
+ # Useful for debugging.
133
+
134
+ describe "with logging", log: true do
135
+ it "logs SQL to stdout" do
136
+ # Check your terminal - you'll see the SQL query
137
+ described_query.entries
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails_helper"
4
+
5
+ RSpec.describe <%= class_name %>Query, type: :query do
6
+ it "returns results" do
7
+ expect(described_query.entries).to be_an(Array)
8
+ end
9
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: appquery
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.8.0.rc1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gert Goet
@@ -46,6 +46,7 @@ files:
46
46
  - ".yard/templates/default/fulldoc/html/css/dark.css"
47
47
  - ".yard/templates/default/fulldoc/html/js/app.js"
48
48
  - ".yard/templates/default/fulldoc/html/setup.rb"
49
+ - ".yard/templates/default/layout/html/footer.erb"
49
50
  - ".yard/templates/default/layout/html/setup.rb"
50
51
  - ".yardopts"
51
52
  - Appraisals
@@ -59,17 +60,27 @@ files:
59
60
  - lib/app_query/base_query.rb
60
61
  - lib/app_query/mappable.rb
61
62
  - lib/app_query/paginatable.rb
63
+ - lib/app_query/railtie.rb
62
64
  - lib/app_query/render_helpers.rb
63
65
  - lib/app_query/rspec.rb
64
66
  - lib/app_query/rspec/helpers.rb
65
67
  - lib/app_query/tokenizer.rb
66
68
  - lib/app_query/version.rb
67
69
  - lib/appquery.rb
68
- - lib/rails/generators/query/USAGE
69
- - lib/rails/generators/query/query_generator.rb
70
- - lib/rails/generators/query/templates/query.sql.tt
71
- - lib/rails/generators/rspec/query_generator.rb
72
- - lib/rails/generators/rspec/templates/query_spec.rb.tt
70
+ - lib/generators/app_query/USAGE
71
+ - lib/generators/app_query/example_generator.rb
72
+ - lib/generators/app_query/query_generator.rb
73
+ - lib/generators/app_query/templates/application_query.rb.tt
74
+ - lib/generators/app_query/templates/example.sql.erb.tt
75
+ - lib/generators/app_query/templates/example_query.rb.tt
76
+ - lib/generators/app_query/templates/query.rb.tt
77
+ - lib/generators/app_query/templates/query.sql.tt
78
+ - lib/generators/query_generator.rb
79
+ - lib/generators/rspec/app_query_example_generator.rb
80
+ - lib/generators/rspec/app_query_generator.rb
81
+ - lib/generators/rspec/query_generator.rb
82
+ - lib/generators/rspec/templates/app_query_example_spec.rb.tt
83
+ - lib/generators/rspec/templates/app_query_spec.rb.tt
73
84
  - mise.local.toml.example
74
85
  - mise.toml
75
86
  - rakelib/gem.rake
@@ -89,7 +100,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
89
100
  requirements:
90
101
  - - ">="
91
102
  - !ruby/object:Gem::Version
92
- version: 3.2.0
103
+ version: 3.3.0
93
104
  required_rubygems_version: !ruby/object:Gem::Requirement
94
105
  requirements:
95
106
  - - ">="
@@ -1,10 +0,0 @@
1
- Description:
2
- Generates a new query-file and invokes your template
3
- engine and test framework generators.
4
-
5
- Example:
6
- `bin/rails generate query reports/weekly`
7
-
8
- creates an SQL file and test:
9
- Query: app/queries/reports/weekly.sql
10
- Spec: spec/queries/reports/weekly_query_spec.rb
@@ -1,20 +0,0 @@
1
- module Rails
2
- module Generators
3
- class QueryGenerator < NamedBase
4
- source_root File.expand_path("templates", __dir__)
5
-
6
- def create_query_file
7
- template "query.sql",
8
- File.join(AppQuery.configuration.query_path, class_path, "#{file_name}.sql")
9
-
10
- # in_root do
11
- # if behavior == :invoke && !File.exist?(application_mailbox_file_name)
12
- # template "application_mailbox.rb", application_mailbox_file_name
13
- # end
14
- # end
15
- end
16
-
17
- hook_for :test_framework
18
- end
19
- end
20
- end
@@ -1,14 +0,0 @@
1
- -- Instantiate this query with AppQuery[<%= (class_path << file_name).join("/").inspect %>]
2
-
3
- WITH
4
- articles(article_id, article_title) AS (
5
- VALUES (1, 'Some title'),
6
- (2, 'Another article')
7
- ),
8
- authors(author_id, author_name) AS (
9
- VALUES (1, 'Some Author'),
10
- (2, 'Another Author')
11
- )
12
-
13
- SELECT *
14
- FROM artciles
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "rails_helper"
4
-
5
- RSpec.describe "AppQuery <%= (class_path << file_name).join("/") %>", type: :query, default_binds: nil do
6
- describe "CTE articles" do
7
- specify do
8
- expect(select_all(select: "select * from :cte").cast_entries).to \
9
- include(a_hash_including("article_id" => 1))
10
- end
11
- end
12
- end