Class: Pluckr::Relation

Inherits:
Object
  • Object
show all
Includes:
Enumerable
Defined in:
lib/pluckr/relation.rb

Overview

Immutable, chainable wrapper around the root ActiveRecord relation.

Root filtering/ordering/pagination is delegated to ActiveRecord - Pluckr only adds its projections and joins when the query is compiled.

Defined Under Namespace

Classes: WhereChain

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(query_class, relation) ⇒ Relation

Returns a new instance of Relation.



13
14
15
16
17
# File 'lib/pluckr/relation.rb', line 13

def initialize(query_class, relation)
  @query_class = query_class
  @relation = relation
  freeze
end

Instance Attribute Details

#query_classObject (readonly)

Returns the value of attribute query_class.



11
12
13
# File 'lib/pluckr/relation.rb', line 11

def query_class
  @query_class
end

#relationObject (readonly)

Returns the value of attribute relation.



11
12
13
# File 'lib/pluckr/relation.rb', line 11

def relation
  @relation
end

Instance Method Details

#any?(*args) ⇒ Boolean

Returns:

  • (Boolean)


72
73
74
75
76
# File 'lib/pluckr/relation.rb', line 72

def any?(*args)
  return super if args.any? || block_given?

  exists?
end

#count(*args) ⇒ Object Also known as: size

Enumerable would answer these by building every result object first, which is the opposite of the point. They are questions about the root rows, so ActiveRecord answers them with COUNT(*)/SELECT 1, without Pluckr's projections. A block (or an Enumerable pattern) is a question about the results, so it goes back to Enumerable - exactly where ActiveRecord sends it.



61
62
63
64
65
# File 'lib/pluckr/relation.rb', line 61

def count(*args)
  return super if block_given?

  relation.count(*args)
end

#each(&block) ⇒ Object



51
52
53
# File 'lib/pluckr/relation.rb', line 51

def each(&block)
  fetch.each(&block)
end

#empty?Boolean

Returns:

  • (Boolean)


84
85
86
# File 'lib/pluckr/relation.rb', line 84

def empty?
  !exists?
end

#exists?Boolean

Returns:

  • (Boolean)


68
69
70
# File 'lib/pluckr/relation.rb', line 68

def exists?(...)
  relation.exists?(...)
end

#explainObject



88
89
90
# File 'lib/pluckr/relation.rb', line 88

def explain(...)
  query_class.compiler.apply(relation).explain(...)
end

#fetchObject Also known as: to_a

One statement, one round trip, plain result objects.



44
45
46
47
48
# File 'lib/pluckr/relation.rb', line 44

def fetch
  builder = query_class.result_builder
  rows = Pluckr.select_all(query_class.compiler.connection, to_sql, name: query_class.name)
  rows.map { |row| builder.build(row) }
end

#find(*args, &block) ⇒ Object

find is the primary key lookup; a block is Enumerable's find, which is where ActiveRecord sends it too. Several ids (or an array of them) come back as an array, in the order they were asked for.

Raises:

  • (ArgumentError)


148
149
150
151
152
153
154
155
156
157
158
# File 'lib/pluckr/relation.rb', line 148

def find(*args, &block)
  return super if block
  raise ArgumentError, "wrong number of arguments (given 0, expected 1+)" if args.empty?

  key = single_primary_key
  ids = args.size == 1 ? args.first : args
  return for_ids(ids, key) if ids.is_a?(Array)
  raise ActiveRecord::RecordNotFound, "Couldn't find #{model.name} without an ID" if ids.nil?

  where(key => ids).first || raise(not_found(key, ids))
end

#find_by(arg, *args) ⇒ Object

The arity is ActiveRecord's: at least one condition, or ArgumentError.



161
162
163
# File 'lib/pluckr/relation.rb', line 161

def find_by(arg, *args)
  where(arg, *args).first
end

#find_by!(arg, *args) ⇒ Object



165
166
167
168
169
# File 'lib/pluckr/relation.rb', line 165

def find_by!(arg, *args)
  find_by(arg, *args) ||
    raise(ActiveRecord::RecordNotFound,
          "Couldn't find #{model.name} with #{[arg, *args].map(&:inspect).join(", ")}")
end

#find_each(batch_size: 1_000, &block) ⇒ Object

Reads the whole chain in pages, so an export does not build every result object at once. Like ActiveRecord's, the batches are ordered by primary key and any order of your own is ignored - unlike ActiveRecord's, the page after the first is a keyset seek (id > last), never an OFFSET.



175
176
177
178
179
180
# File 'lib/pluckr/relation.rb', line 175

def find_each(batch_size: 1_000, &block)
  assert_batchable!(batch_size)
  return to_enum(:find_each, batch_size: batch_size) unless block_given?

  in_batches(of: batch_size) { |results| results.each(&block) }
end

#first(limit = nil) ⇒ Object

ActiveRecord orders by primary key when the relation has no order of its own, so that first means something. Same here.



94
95
96
97
98
# File 'lib/pluckr/relation.rb', line 94

def first(limit = nil)
  results = ordered_by_primary_key.limit(within_limit(limit || 1)).fetch

  limit ? results : results.first
end

#first!Object



100
101
102
# File 'lib/pluckr/relation.rb', line 100

def first!
  first || raise(ActiveRecord::RecordNotFound, "Couldn't find #{model.name}")
end

#for(records) ⇒ Object

See Query.for. An unloaded Relation is filtered in SQL (subquery), never loaded into Ruby; a loaded one already has its records, so they are used. A paginated one is resolved to primary keys first - see for_paginated_relation.



207
208
209
210
211
212
213
214
215
216
217
218
# File 'lib/pluckr/relation.rb', line 207

def for(records)
  key = single_primary_key

  if records.is_a?(self.class)
    raise ConfigurationError, "`for` expects #{model.name} records, not another Pluckr query; use `fetch`"
  end

  return for_relation(records, key) if records.is_a?(ActiveRecord::Relation) && !records.loaded?
  return for_many(records.to_a, key) if records.is_a?(Enumerable)

  for_one(records, key)
end

#in_batches(of: 1_000) ⇒ Object



182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
# File 'lib/pluckr/relation.rb', line 182

def in_batches(of: 1_000)
  assert_batchable!(of)
  return to_enum(:in_batches, of: of) unless block_given?

  key = single_primary_key
  output = primary_key_output(key)
  scope = spawn(relation.reorder(key => :asc))
  cursor = nil

  loop do
    page = cursor ? scope.where(model.arel_table[key].gt(cursor)) : scope
    results = page.limit(of).fetch
    break if results.empty?

    yield results
    break if results.size < of

    cursor = results.last.public_send(output)
  end
end

#inspectObject



220
221
222
# File 'lib/pluckr/relation.rb', line 220

def inspect
  "#<Pluckr::Relation #{query_class.name} #{to_sql}>"
end

#last(limit = nil) ⇒ Object

The other end of the same order, read in one LIMIT rather than by hydrating the chain and dropping all but the tail. A chain that already pages is the exception ActiveRecord makes too: its own limit cannot be replaced by ours, so the page is read and the tail taken from it.



108
109
110
111
112
113
114
# File 'lib/pluckr/relation.rb', line 108

def last(limit = nil)
  return last_of_page(limit) if paginated?(relation)

  results = spawn(ordered_by_primary_key.relation.reverse_order).limit(limit || 1).fetch

  limit ? results.reverse : results.first
end

#last!Object



116
117
118
# File 'lib/pluckr/relation.rb', line 116

def last!
  last || raise(ActiveRecord::RecordNotFound, "Couldn't find #{model.name}")
end

#limit(value) ⇒ Object



31
32
33
# File 'lib/pluckr/relation.rb', line 31

def limit(value)
  spawn(relation.limit(value))
end

#many?(*args) ⇒ Boolean

Returns:

  • (Boolean)


139
140
141
142
143
# File 'lib/pluckr/relation.rb', line 139

def many?(*args)
  return super if args.any? || block_given?

  limited_count > 1
end

#none?(*args) ⇒ Boolean

Returns:

  • (Boolean)


78
79
80
81
82
# File 'lib/pluckr/relation.rb', line 78

def none?(*args)
  return super if args.any? || block_given?

  !exists?
end

#offset(value) ⇒ Object



35
36
37
# File 'lib/pluckr/relation.rb', line 35

def offset(value)
  spawn(relation.offset(value))
end

#one?(*args) ⇒ Boolean

Two rows is all it takes to answer either question - unless the chain already limits itself to fewer, in which case that limit is the answer.

Returns:

  • (Boolean)


133
134
135
136
137
# File 'lib/pluckr/relation.rb', line 133

def one?(*args)
  return super if args.any? || block_given?

  limited_count == 1
end

#orderObject



27
28
29
# File 'lib/pluckr/relation.rb', line 27

def order(...)
  spawn(relation.order(...))
end

#spawn(new_relation) ⇒ Object



224
225
226
# File 'lib/pluckr/relation.rb', line 224

def spawn(new_relation)
  self.class.new(query_class, new_relation)
end

#take(limit = nil) ⇒ Object

Enumerable's take would read the whole chain to keep the front of it.



121
122
123
124
125
# File 'lib/pluckr/relation.rb', line 121

def take(limit = nil)
  results = self.limit(limit || 1).fetch

  limit ? results : results.first
end

#take!Object



127
128
129
# File 'lib/pluckr/relation.rb', line 127

def take!
  take || raise(ActiveRecord::RecordNotFound, "Couldn't find #{model.name}")
end

#to_sqlObject



39
40
41
# File 'lib/pluckr/relation.rb', line 39

def to_sql
  query_class.compiler.apply(relation).to_sql
end

#where(*args, **options, &block) ⇒ Object

where with no arguments returns a chain, so where.not(...) works like it does on an ActiveRecord relation.



21
22
23
24
25
# File 'lib/pluckr/relation.rb', line 21

def where(*args, **options, &block)
  return WhereChain.new(self) if args.empty? && options.empty? && block.nil?

  spawn(relation.where(*args, **options, &block))
end