pluckr 0.1.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 +66 -0
- data/LICENSE.txt +21 -0
- data/PROMPT.md +112 -0
- data/README.md +436 -0
- data/lib/pluckr/aliases.rb +37 -0
- data/lib/pluckr/batch.rb +216 -0
- data/lib/pluckr/compiler/sql.rb +363 -0
- data/lib/pluckr/errors.rb +24 -0
- data/lib/pluckr/query.rb +207 -0
- data/lib/pluckr/reflection/association.rb +109 -0
- data/lib/pluckr/relation.rb +414 -0
- data/lib/pluckr/result/builder.rb +100 -0
- data/lib/pluckr/result/object.rb +78 -0
- data/lib/pluckr/schema/aggregate.rb +58 -0
- data/lib/pluckr/schema/conditions.rb +45 -0
- data/lib/pluckr/schema/definition.rb +289 -0
- data/lib/pluckr/schema/exists.rb +28 -0
- data/lib/pluckr/schema/field.rb +16 -0
- data/lib/pluckr/schema/node.rb +21 -0
- data/lib/pluckr/schema/one.rb +37 -0
- data/lib/pluckr/schema/scope.rb +34 -0
- data/lib/pluckr/version.rb +5 -0
- data/lib/pluckr.rb +45 -0
- metadata +125 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pluckr
|
|
4
|
+
module Schema
|
|
5
|
+
# Evaluates the `schema do ... end` block and builds a Scope of AST nodes.
|
|
6
|
+
#
|
|
7
|
+
# All validation happens here, at class-definition time, so mistakes blow up
|
|
8
|
+
# when the query is loaded rather than when it is executed.
|
|
9
|
+
class Definition
|
|
10
|
+
# @param model [Class<ActiveRecord::Base>, nil] nil for aggregate-only queries
|
|
11
|
+
def self.build(model, label: "schema", &block)
|
|
12
|
+
raise ConfigurationError, "`#{label}` requires a block" unless block
|
|
13
|
+
|
|
14
|
+
definition = new(model)
|
|
15
|
+
definition.instance_eval(&block)
|
|
16
|
+
scope = definition.to_scope
|
|
17
|
+
|
|
18
|
+
raise ConfigurationError, "`#{label}` block is empty - declare at least one node" if scope.nodes.empty?
|
|
19
|
+
|
|
20
|
+
scope
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def initialize(model)
|
|
24
|
+
@model = model
|
|
25
|
+
@nodes = []
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def to_scope
|
|
29
|
+
Scope.new(@model, @nodes)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# ---------------------------------------------------------------- DSL --
|
|
33
|
+
|
|
34
|
+
def field(name, as: nil)
|
|
35
|
+
require_source!("field :#{name}")
|
|
36
|
+
validate_column!(@model, name)
|
|
37
|
+
add Field.new(column: name, output: as || name)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def one(name, via: nil, &block)
|
|
41
|
+
require_source!("one :#{name}")
|
|
42
|
+
raise ConfigurationError, "`one :#{name}` requires a block" unless block
|
|
43
|
+
|
|
44
|
+
association = Reflection::Association.for(@model, via || name)
|
|
45
|
+
unless association.singular?
|
|
46
|
+
raise InvalidAssociation,
|
|
47
|
+
"`#{@model.name}##{association.name}` is a #{association.macro} association " \
|
|
48
|
+
"and cannot be used with `one`"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
add One.new(
|
|
52
|
+
output: name,
|
|
53
|
+
association: association,
|
|
54
|
+
scope: Definition.build(association.klass, label: "one :#{name}", &block)
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# `first :comment, via: :comments do ... end` - the earliest row of a
|
|
59
|
+
# collection, by primary key unless `order:` says otherwise.
|
|
60
|
+
def first(name, via: nil, order: nil, &block)
|
|
61
|
+
pick(:first, name, via: via, order: order, &block)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# `last :comment, via: :comments, order: :created_at do ... end`
|
|
65
|
+
def last(name, via: nil, order: nil, &block)
|
|
66
|
+
pick(:last, name, via: via, order: order, &block)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def exists(name, via: nil, as: nil, where: nil, scope: nil)
|
|
70
|
+
require_source!("exists :#{name}")
|
|
71
|
+
association = Reflection::Association.for(@model, via || name)
|
|
72
|
+
validate_where!(association.klass, where)
|
|
73
|
+
validate_scope!("exists :#{name}", scope)
|
|
74
|
+
|
|
75
|
+
add Exists.new(
|
|
76
|
+
output: as || :"#{name}_exists",
|
|
77
|
+
association: association,
|
|
78
|
+
where: where,
|
|
79
|
+
scope: scope
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def count(name, from: nil, via: nil, as: nil, where: nil, column: nil, scope: nil)
|
|
84
|
+
aggregate(:count, name, from: from, via: via, as: as, where: where, column: column, scope: scope)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def sum(name, from: nil, via: nil, as: nil, where: nil, column: nil, scope: nil)
|
|
88
|
+
aggregate(:sum, name, from: from, via: via, as: as, where: where, column: column, scope: scope)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def avg(name, from: nil, via: nil, as: nil, where: nil, column: nil, scope: nil)
|
|
92
|
+
aggregate(:avg, name, from: from, via: via, as: as, where: where, column: column, scope: scope)
|
|
93
|
+
end
|
|
94
|
+
alias average avg
|
|
95
|
+
|
|
96
|
+
def min(name, from: nil, via: nil, as: nil, where: nil, column: nil, scope: nil)
|
|
97
|
+
aggregate(:min, name, from: from, via: via, as: as, where: where, column: column, scope: scope)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def max(name, from: nil, via: nil, as: nil, where: nil, column: nil, scope: nil)
|
|
101
|
+
aggregate(:max, name, from: from, via: via, as: as, where: where, column: column, scope: scope)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# ------------------------------------------------------------ internals --
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
def pick(direction, name, via:, order:, &block)
|
|
109
|
+
require_source!("#{direction} :#{name}")
|
|
110
|
+
raise ConfigurationError, "`#{direction} :#{name}` requires a block" unless block
|
|
111
|
+
|
|
112
|
+
association = Reflection::Association.for(@model, via || name)
|
|
113
|
+
scope = Definition.build(association.klass, label: "#{direction} :#{name}", &block)
|
|
114
|
+
validate_picked_scope!(direction, name, scope)
|
|
115
|
+
|
|
116
|
+
add One.new(
|
|
117
|
+
output: name,
|
|
118
|
+
association: association,
|
|
119
|
+
scope: scope,
|
|
120
|
+
pick: direction,
|
|
121
|
+
order: pick_order(direction, name, order, association.klass)
|
|
122
|
+
)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Every column of the picked row comes from its own subquery, so nesting
|
|
126
|
+
# anything that needs a joined table cannot work here.
|
|
127
|
+
def validate_picked_scope!(direction, name, scope)
|
|
128
|
+
offender = scope.nodes.reject { |node| node.is_a?(Field) }.first
|
|
129
|
+
return unless offender
|
|
130
|
+
|
|
131
|
+
raise ConfigurationError,
|
|
132
|
+
"`#{direction} :#{name}` can only contain `field` in v0.1, " \
|
|
133
|
+
"got `#{offender.class.name.split("::").last.downcase} :#{offender.output}`"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Normalises `order:` into [[column, direction], ...].
|
|
137
|
+
#
|
|
138
|
+
# `last` reverses it, so `last ..., order: { created_at: :asc }` is the
|
|
139
|
+
# final row of that ordering - what `relation.order(...).last` returns. The
|
|
140
|
+
# primary key always ends the list, so ties cannot make two columns come
|
|
141
|
+
# from two different rows.
|
|
142
|
+
def pick_order(direction, name, order, model)
|
|
143
|
+
entries = Array(order_entries(direction, name, order))
|
|
144
|
+
entries.each { |column, _| validate_column!(model, column) }
|
|
145
|
+
entries = entries.map { |column, dir| [column, invert(dir)] } if direction == :last
|
|
146
|
+
|
|
147
|
+
primary_key = model.primary_key&.to_sym
|
|
148
|
+
tiebreaker = direction == :last ? :desc : :asc
|
|
149
|
+
entries << [primary_key, tiebreaker] if primary_key && entries.none? { |column, _| column == primary_key }
|
|
150
|
+
entries.freeze
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def invert(direction)
|
|
154
|
+
direction == :asc ? :desc : :asc
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def order_entries(direction, name, order)
|
|
158
|
+
case order
|
|
159
|
+
when nil then []
|
|
160
|
+
when Symbol, String then [[order.to_sym, :asc]]
|
|
161
|
+
when Hash then order.map { |column, dir| [column.to_sym, order_direction(direction, name, dir)] }
|
|
162
|
+
when Array then order.flat_map { |entry| order_entries(direction, name, entry) }
|
|
163
|
+
else
|
|
164
|
+
raise ConfigurationError,
|
|
165
|
+
"`#{direction} :#{name}, order:` expects a column, a Hash or an Array, got #{order.inspect}"
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def order_direction(direction, name, value)
|
|
170
|
+
normalized = value.to_s.downcase.to_sym
|
|
171
|
+
return normalized if %i[asc desc].include?(normalized)
|
|
172
|
+
|
|
173
|
+
raise ConfigurationError,
|
|
174
|
+
"`#{direction} :#{name}, order:` expects :asc or :desc, got #{value.inspect}"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def aggregate(operation, name, from:, via:, as:, where:, column:, scope:)
|
|
178
|
+
validate_scope!("#{operation} :#{name}", scope)
|
|
179
|
+
|
|
180
|
+
if from
|
|
181
|
+
independent_aggregate(operation, name, from: from, as: as, where: where,
|
|
182
|
+
column: column, scope: scope)
|
|
183
|
+
else
|
|
184
|
+
correlated_aggregate(operation, name, via: via, as: as, where: where,
|
|
185
|
+
column: column, scope: scope)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def independent_aggregate(operation, name, from:, as:, where:, column:, scope:)
|
|
190
|
+
unless from.is_a?(Class) && from < ActiveRecord::Base
|
|
191
|
+
raise ConfigurationError,
|
|
192
|
+
"`#{operation} :#{name}, from:` expects an ActiveRecord model, got #{from.inspect}"
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
column = require_column!(operation, name, column, from)
|
|
196
|
+
validate_where!(from, where)
|
|
197
|
+
|
|
198
|
+
add Aggregate.new(
|
|
199
|
+
output: as || name,
|
|
200
|
+
operation: operation,
|
|
201
|
+
model: from,
|
|
202
|
+
column: column,
|
|
203
|
+
where: where,
|
|
204
|
+
scope: scope
|
|
205
|
+
)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def correlated_aggregate(operation, name, via:, as:, where:, column:, scope:)
|
|
209
|
+
require_source!("#{operation} :#{name}")
|
|
210
|
+
|
|
211
|
+
association = Reflection::Association.for(@model, via || name)
|
|
212
|
+
column = require_column!(operation, name, column, association.klass)
|
|
213
|
+
validate_where!(association.klass, where)
|
|
214
|
+
|
|
215
|
+
add Aggregate.new(
|
|
216
|
+
output: as || :"#{name}_#{operation}",
|
|
217
|
+
operation: operation,
|
|
218
|
+
association: association,
|
|
219
|
+
column: column,
|
|
220
|
+
where: where,
|
|
221
|
+
scope: scope
|
|
222
|
+
)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# count is the only operation that works without a column (COUNT(*)).
|
|
226
|
+
def require_column!(operation, name, column, model)
|
|
227
|
+
if column.nil?
|
|
228
|
+
return nil if operation == :count
|
|
229
|
+
|
|
230
|
+
raise ConfigurationError, "`#{operation} :#{name}` requires `column:`"
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
validate_column!(model, column)
|
|
234
|
+
column
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def validate_column!(model, name)
|
|
238
|
+
return if model.column_names.include?(name.to_s)
|
|
239
|
+
|
|
240
|
+
raise UnknownField, "#{model.name} does not have column `#{name}`"
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# A Hash is checked now; a callable can only be checked when it runs, on
|
|
244
|
+
# every compilation.
|
|
245
|
+
def validate_where!(model, where)
|
|
246
|
+
return if where.nil?
|
|
247
|
+
|
|
248
|
+
if where.respond_to?(:call)
|
|
249
|
+
return if !where.respond_to?(:arity) || where.arity <= 0
|
|
250
|
+
|
|
251
|
+
raise ConfigurationError,
|
|
252
|
+
"a `where:` callable takes no arguments and returns a Hash - use `scope:` if you " \
|
|
253
|
+
"need the relation"
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
unless where.is_a?(Hash)
|
|
257
|
+
raise ConfigurationError,
|
|
258
|
+
"`where:` expects a Hash of column => value or a callable returning one, " \
|
|
259
|
+
"got #{where.inspect}"
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
where.each_key { |key| validate_column!(model, key) }
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# `scope:` takes the target model's relation and returns a narrowed one:
|
|
266
|
+
# count :active_accounts, from: Account, scope: ->(rel) { rel.active }
|
|
267
|
+
def validate_scope!(node, scope)
|
|
268
|
+
return if scope.nil? || scope.respond_to?(:call)
|
|
269
|
+
|
|
270
|
+
raise ConfigurationError, "`#{node}, scope:` expects a callable, got #{scope.inspect}"
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def require_source!(node)
|
|
274
|
+
return unless @model.nil?
|
|
275
|
+
|
|
276
|
+
raise MissingSource, "`#{node}` needs a root record - declare `source SomeModel` on the query"
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def add(node)
|
|
280
|
+
if @nodes.any? { |existing| existing.output == node.output }
|
|
281
|
+
raise ConfigurationError, "duplicate output name `#{node.output}` in schema"
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
@nodes << node
|
|
285
|
+
node
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pluckr
|
|
4
|
+
module Schema
|
|
5
|
+
# `exists :photos` -> correlated `EXISTS (SELECT 1 FROM photos WHERE ...)`
|
|
6
|
+
class Exists < Node
|
|
7
|
+
include Conditions
|
|
8
|
+
|
|
9
|
+
attr_reader :association, :where, :scope
|
|
10
|
+
|
|
11
|
+
def initialize(output:, association:, where: nil, scope: nil)
|
|
12
|
+
super(output: output)
|
|
13
|
+
@association = association
|
|
14
|
+
@where = where.respond_to?(:call) ? where : where&.dup&.freeze
|
|
15
|
+
@scope = scope
|
|
16
|
+
freeze
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def target_model
|
|
20
|
+
association.klass
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def relation_scoped?
|
|
24
|
+
!scope.nil? || !where.nil?
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pluckr
|
|
4
|
+
module Schema
|
|
5
|
+
# `field :email, as: :contact_email`
|
|
6
|
+
class Field < Node
|
|
7
|
+
attr_reader :column
|
|
8
|
+
|
|
9
|
+
def initialize(column:, output:)
|
|
10
|
+
super(output: output)
|
|
11
|
+
@column = column.to_sym
|
|
12
|
+
freeze
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pluckr
|
|
4
|
+
module Schema
|
|
5
|
+
# Base class for every AST node.
|
|
6
|
+
#
|
|
7
|
+
# `output` is the key the node produces on the result object.
|
|
8
|
+
class Node
|
|
9
|
+
attr_reader :output
|
|
10
|
+
|
|
11
|
+
def initialize(output:)
|
|
12
|
+
@output = output.to_sym
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Nodes that map to exactly one selected column/expression.
|
|
16
|
+
def scalar?
|
|
17
|
+
true
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pluckr
|
|
4
|
+
module Schema
|
|
5
|
+
# `one :plan, via: :subscription do ... end`
|
|
6
|
+
#
|
|
7
|
+
# Compiles to a LEFT OUTER JOIN plus a hidden presence marker so that
|
|
8
|
+
# "association missing" and "association present with NULL columns" stay
|
|
9
|
+
# distinguishable.
|
|
10
|
+
#
|
|
11
|
+
# `first :comment, via: :comments do ... end` and its `last` counterpart use
|
|
12
|
+
# the same node with `pick` and `order` set: one row out of a collection,
|
|
13
|
+
# compiled as correlated subqueries instead of a join.
|
|
14
|
+
class One < Node
|
|
15
|
+
attr_reader :association, :scope, :pick, :order
|
|
16
|
+
|
|
17
|
+
def initialize(output:, association:, scope:, pick: nil, order: nil)
|
|
18
|
+
super(output: output)
|
|
19
|
+
@association = association
|
|
20
|
+
@scope = scope
|
|
21
|
+
@pick = pick
|
|
22
|
+
@order = order&.freeze
|
|
23
|
+
freeze
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# True when this node picks one row out of many rather than following a
|
|
27
|
+
# singular association.
|
|
28
|
+
def picked?
|
|
29
|
+
!pick.nil?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def scalar?
|
|
33
|
+
false
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pluckr
|
|
4
|
+
module Schema
|
|
5
|
+
# A level of the AST: the nodes selected for one model (the root source, or
|
|
6
|
+
# one nested singular association).
|
|
7
|
+
class Scope
|
|
8
|
+
attr_reader :model, :nodes
|
|
9
|
+
|
|
10
|
+
def initialize(model, nodes)
|
|
11
|
+
@model = model
|
|
12
|
+
@nodes = nodes.freeze
|
|
13
|
+
freeze
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def fields
|
|
17
|
+
nodes.grep(Field)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def ones
|
|
21
|
+
nodes.grep(One)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def outputs
|
|
25
|
+
nodes.map(&:output)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# True when anything here needs a root record to hang off.
|
|
29
|
+
def rooted?
|
|
30
|
+
nodes.any? { |node| !(node.is_a?(Aggregate) && node.independent?) }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
data/lib/pluckr.rb
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_record"
|
|
4
|
+
require "active_support/core_ext/object/json"
|
|
5
|
+
require "active_support/notifications"
|
|
6
|
+
|
|
7
|
+
require "pluckr/version"
|
|
8
|
+
require "pluckr/errors"
|
|
9
|
+
require "pluckr/aliases"
|
|
10
|
+
require "pluckr/reflection/association"
|
|
11
|
+
require "pluckr/schema/node"
|
|
12
|
+
require "pluckr/schema/conditions"
|
|
13
|
+
require "pluckr/schema/field"
|
|
14
|
+
require "pluckr/schema/one"
|
|
15
|
+
require "pluckr/schema/exists"
|
|
16
|
+
require "pluckr/schema/aggregate"
|
|
17
|
+
require "pluckr/schema/scope"
|
|
18
|
+
require "pluckr/schema/definition"
|
|
19
|
+
require "pluckr/compiler/sql"
|
|
20
|
+
require "pluckr/result/object"
|
|
21
|
+
require "pluckr/result/builder"
|
|
22
|
+
require "pluckr/relation"
|
|
23
|
+
require "pluckr/batch"
|
|
24
|
+
require "pluckr/query"
|
|
25
|
+
|
|
26
|
+
# A declarative read-query layer for ActiveRecord.
|
|
27
|
+
#
|
|
28
|
+
# DSL -> schema AST -> reflection -> SQL compiler -> flat row -> result object
|
|
29
|
+
module Pluckr
|
|
30
|
+
# Every read of a read model goes through here, so one subscription covers
|
|
31
|
+
# `fetch`, `find`, `for` and `Pluckr.batch` alike. The statements Pluckr does
|
|
32
|
+
# not compile are absent by design: `count`/`exists?` and the primary keys of
|
|
33
|
+
# a paginated `.for` are ActiveRecord's own, and arrive as `sql.active_record`.
|
|
34
|
+
#
|
|
35
|
+
# ActiveSupport::Notifications.subscribe("fetch.pluckr") do |*, payload|
|
|
36
|
+
# payload[:name] # => "UserSummary"
|
|
37
|
+
# payload[:sql] # => "SELECT ..."
|
|
38
|
+
# payload[:rows] # => 100
|
|
39
|
+
# end
|
|
40
|
+
def self.select_all(connection, sql, name:, label: "#{name} Pluckr")
|
|
41
|
+
ActiveSupport::Notifications.instrument("fetch.pluckr", name: name, sql: sql) do |payload|
|
|
42
|
+
connection.select_all(sql, label).tap { |result| payload[:rows] = result.length }
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: pluckr
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Igor Kasyanchuk
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: activerecord
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '7.1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '7.1'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: activesupport
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '7.1'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '7.1'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: irb
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rdoc
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0'
|
|
68
|
+
description: Pluckr lets you declare the shape of the data you need and compiles it
|
|
69
|
+
into a single SQL statement, returning lightweight immutable result objects instead
|
|
70
|
+
of ActiveRecord models. Works on PostgreSQL, MySQL and SQLite.
|
|
71
|
+
email:
|
|
72
|
+
- igorkasyanchuk@gmail.com
|
|
73
|
+
executables: []
|
|
74
|
+
extensions: []
|
|
75
|
+
extra_rdoc_files: []
|
|
76
|
+
files:
|
|
77
|
+
- CHANGELOG.md
|
|
78
|
+
- LICENSE.txt
|
|
79
|
+
- PROMPT.md
|
|
80
|
+
- README.md
|
|
81
|
+
- lib/pluckr.rb
|
|
82
|
+
- lib/pluckr/aliases.rb
|
|
83
|
+
- lib/pluckr/batch.rb
|
|
84
|
+
- lib/pluckr/compiler/sql.rb
|
|
85
|
+
- lib/pluckr/errors.rb
|
|
86
|
+
- lib/pluckr/query.rb
|
|
87
|
+
- lib/pluckr/reflection/association.rb
|
|
88
|
+
- lib/pluckr/relation.rb
|
|
89
|
+
- lib/pluckr/result/builder.rb
|
|
90
|
+
- lib/pluckr/result/object.rb
|
|
91
|
+
- lib/pluckr/schema/aggregate.rb
|
|
92
|
+
- lib/pluckr/schema/conditions.rb
|
|
93
|
+
- lib/pluckr/schema/definition.rb
|
|
94
|
+
- lib/pluckr/schema/exists.rb
|
|
95
|
+
- lib/pluckr/schema/field.rb
|
|
96
|
+
- lib/pluckr/schema/node.rb
|
|
97
|
+
- lib/pluckr/schema/one.rb
|
|
98
|
+
- lib/pluckr/schema/scope.rb
|
|
99
|
+
- lib/pluckr/version.rb
|
|
100
|
+
homepage: https://github.com/igorkasyanchuk/pluckr
|
|
101
|
+
licenses:
|
|
102
|
+
- MIT
|
|
103
|
+
metadata:
|
|
104
|
+
homepage_uri: https://github.com/igorkasyanchuk/pluckr
|
|
105
|
+
source_code_uri: https://github.com/igorkasyanchuk/pluckr
|
|
106
|
+
changelog_uri: https://github.com/igorkasyanchuk/pluckr/blob/main/CHANGELOG.md
|
|
107
|
+
rubygems_mfa_required: 'true'
|
|
108
|
+
rdoc_options: []
|
|
109
|
+
require_paths:
|
|
110
|
+
- lib
|
|
111
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
112
|
+
requirements:
|
|
113
|
+
- - ">="
|
|
114
|
+
- !ruby/object:Gem::Version
|
|
115
|
+
version: 3.2.0
|
|
116
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
117
|
+
requirements:
|
|
118
|
+
- - ">="
|
|
119
|
+
- !ruby/object:Gem::Version
|
|
120
|
+
version: '0'
|
|
121
|
+
requirements: []
|
|
122
|
+
rubygems_version: 3.7.2
|
|
123
|
+
specification_version: 4
|
|
124
|
+
summary: A declarative read-query layer for ActiveRecord.
|
|
125
|
+
test_files: []
|