Class: ActivePostgrest::Relation

Inherits:
Object
  • Object
show all
Includes:
Mutations, SqlBuilder, Enumerable
Defined in:
lib/active_postgrest/relation.rb

Defined Under Namespace

Classes: WhereChain

Constant Summary

Constants included from SqlBuilder

SqlBuilder::FILTER_OPS, SqlBuilder::KNOWN_OP_KEYS, SqlBuilder::NEGATED_OPS

Instance Method Summary collapse

Methods included from Mutations

#create, #create!, #delete_all, #insert, #insert_all, #update_all, #upsert, #upsert_all

Constructor Details

#initialize(table, client, model_class) ⇒ Relation

Returns a new instance of Relation.



18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# File 'lib/active_postgrest/relation.rb', line 18

def initialize(table, client, model_class)
  @table = table
  @client = client
  @model_class = model_class
  @selects = []
  @joins = []
  @filters = {}
  @or_conditions = []
  @and_conditions = []
  @limit_val = nil
  @offset_val = nil
  @order_val = nil
  @null = false
  @schema = nil
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(name) ⇒ Object



216
217
218
219
220
221
222
223
# File 'lib/active_postgrest/relation.rb', line 216

def method_missing(name, *, **)
  return super unless @model_class.respond_to?(name)

  scope = @model_class.public_send(name, *, **)
  return super unless scope.is_a?(ActivePostgrest::Relation)

  merge(scope)
end

Instance Method Details

#and_where(conditions) ⇒ Object

and_where([{ age: { gt: 18 } }, { status: “active” }]) → and=(age.gt.18,status.eq.active)



102
103
104
105
# File 'lib/active_postgrest/relation.rb', line 102

def and_where(conditions)
  parts = Array(conditions).flat_map { |f| condition_parts(f) }
  clone_with { @and_conditions.concat(parts) }
end

#anonymousObject



119
# File 'lib/active_postgrest/relation.rb', line 119

def anonymous            = clone_with { @client = @client.anonymous }

#any?(&block) ⇒ Boolean

Returns:

  • (Boolean)


158
# File 'lib/active_postgrest/relation.rb', line 158

def any?(&block)    = block ? super : count.positive?

#average(col) ⇒ Object



164
# File 'lib/active_postgrest/relation.rb', line 164

def average(col)  = aggregate_value("#{col}.avg()", 'avg')

#count(mode = :exact) ⇒ Object



145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/active_postgrest/relation.rb', line 145

def count(mode = :exact)
  return 0 if @null

  response = @client.get(@table, build_params.merge(limit: 0), count: mode, schema: @schema)
  raw   = response.headers['content-range']&.split('/')&.last
  total = raw&.delete_prefix('~')
  if total.nil? || total == '*'
    raise CountNotAvailable, "count=#{mode} not available (Content-Range: #{raw.inspect})"
  end

  total.to_i
end

#eachObject



123
124
125
# File 'lib/active_postgrest/relation.rb', line 123

def each(&)
  to_a.each(&)
end

#embed(name, fields: []) ⇒ Object

embed(:mother, fields: [:id, :first_name]) — computed relationship



56
57
58
# File 'lib/active_postgrest/relation.rb', line 56

def embed(name, fields: [])
  add_embed(name.to_s, name.to_s, fields.map(&:to_s), {})
end

#exists?Boolean

Returns:

  • (Boolean)


162
# File 'lib/active_postgrest/relation.rb', line 162

def exists?         = any?

#explainObject



203
204
205
# File 'lib/active_postgrest/relation.rb', line 203

def explain
  @client.explain(@table, build_params, schema: @schema)
end

#firstObject



134
135
136
# File 'lib/active_postgrest/relation.rb', line 134

def first
  limit(1).to_a.first
end

#inspectObject



229
230
231
# File 'lib/active_postgrest/relation.rb', line 229

def inspect
  to_a.inspect
end

#joins(table, as: nil, foreign_key: nil, select: [], where: {}) ⇒ Object

joins(:companies) → INNER JOIN (excludes rows with no match) joins(:users, as: :mother, foreign_key: :mother_id) → aliased FK join



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

def joins(table, as: nil, foreign_key: nil, select: [], where: {})
  embed = build_embed(table.to_s, as&.to_s, foreign_key&.to_s, inner: true)
  add_embed(embed, table.to_s, select.map(&:to_s), where)
end

#last(n = nil) ⇒ Object



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

def last(n = nil)
  pk = @model_class.primary_key
  return order(pk, :desc).limit(n).to_a.reverse if n

  order(pk, :desc).limit(1).to_a.first
end

#left_joins(table, as: nil, foreign_key: nil, select: [], where: {}) ⇒ Object

left_joins(:companies) → LEFT JOIN (includes rows even with no match)



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

def left_joins(table, as: nil, foreign_key: nil, select: [], where: {})
  embed = build_embed(table.to_s, as&.to_s, foreign_key&.to_s, inner: false)
  add_embed(embed, table.to_s, select.map(&:to_s), where)
end

#limit(n) ⇒ Object



107
# File 'lib/active_postgrest/relation.rb', line 107

def limit(n)       = clone_with { @limit_val = n }

#many?Boolean

Returns:

  • (Boolean)


161
# File 'lib/active_postgrest/relation.rb', line 161

def many?           = count > 1

#maximum(col) ⇒ Object



167
# File 'lib/active_postgrest/relation.rb', line 167

def maximum(col)  = aggregate_value("#{col}.max()", 'max')

#minimum(col) ⇒ Object



166
# File 'lib/active_postgrest/relation.rb', line 166

def minimum(col)  = aggregate_value("#{col}.min()", 'min')

#noneObject



118
# File 'lib/active_postgrest/relation.rb', line 118

def none                 = clone_with { @null = true }

#none?(&block) ⇒ Boolean

Returns:

  • (Boolean)


159
# File 'lib/active_postgrest/relation.rb', line 159

def none?(&block)   = block ? super : count.zero?

#not_where(filters) ⇒ Object



74
75
76
# File 'lib/active_postgrest/relation.rb', line 74

def not_where(filters)
  clone_with { encode_filters!(filters, negate: true) }
end

#offset(n) ⇒ Object



108
# File 'lib/active_postgrest/relation.rb', line 108

def offset(n)      = clone_with { @offset_val = n }

#one?(&block) ⇒ Boolean

Returns:

  • (Boolean)


160
# File 'lib/active_postgrest/relation.rb', line 160

def one?(&block)    = block ? super : count == 1

#or(other) ⇒ Object

where(a: 1).or(where(b: 2)) → or=(a.eq.1,b.eq.2) where(a: 1).where(c: 3).or(where(b: 2)) → or=(and(a.eq.1,c.eq.3),b.eq.2)



86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/active_postgrest/relation.rb', line 86

def or(other)
  other_or  = other.instance_variable_get(:@or_conditions)
  other_and = other.instance_variable_get(:@and_conditions)
  if @or_conditions.any? || @and_conditions.any? || other_or.any? || other_and.any?
    raise ArgumentError, '#or does not support a relation with existing or_where/and_where conditions'
  end

  left  = or_group(encoded_filter_conditions)
  right = or_group(other.encoded_filter_conditions)
  clone_with do
    @filters = {}
    @or_conditions.concat([left, right].compact)
  end
end

#or_where(conditions) ⇒ Object

or_where([{ age: { lt: 18 } }, { status: “active” }]) → or=(age.lt.18,status.eq.active)



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

def or_where(conditions)
  parts = Array(conditions).flat_map { |f| condition_parts(f) }
  clone_with { @or_conditions.concat(parts) }
end

#order(col, dir = :asc, nulls: nil) ⇒ Object



110
111
112
# File 'lib/active_postgrest/relation.rb', line 110

def order(col, dir = :asc, nulls: nil)
  clone_with { @order_val = build_order(col, dir, nulls) }
end

#pick(*cols) ⇒ Object



177
178
179
# File 'lib/active_postgrest/relation.rb', line 177

def pick(*cols)
  pluck(*cols).first
end

#pluck(*cols) ⇒ Object



169
170
171
172
173
174
175
# File 'lib/active_postgrest/relation.rb', line 169

def pluck(*cols)
  return [] if @null

  select(*cols).to_a.map do |record|
    cols.length == 1 ? record[cols.first] : cols.map { record[_1] }
  end
end

#reorder(col, dir = :asc, nulls: nil) ⇒ Object



114
115
116
# File 'lib/active_postgrest/relation.rb', line 114

def reorder(col, dir = :asc, nulls: nil)
  clone_with { @order_val = build_order(col, dir, nulls) }
end

#respond_to_missing?(name, include_private = false) ⇒ Boolean

Returns:

  • (Boolean)


225
226
227
# File 'lib/active_postgrest/relation.rb', line 225

def respond_to_missing?(name, include_private = false)
  @model_class.respond_to?(name) || super
end

#select(*cols) ⇒ Object



34
35
36
# File 'lib/active_postgrest/relation.rb', line 34

def select(*cols)
  clone_with { @selects.concat(cols.map(&:to_s)) }
end

#spread(*tables) ⇒ Object



38
39
40
# File 'lib/active_postgrest/relation.rb', line 38

def spread(*tables)
  clone_with { @selects.concat(tables.map { "...#{_1}" }) }
end

#sum(col) ⇒ Object



165
# File 'lib/active_postgrest/relation.rb', line 165

def sum(col)      = aggregate_value("#{col}.sum()", 'sum')

#to_aObject



127
128
129
130
131
132
# File 'lib/active_postgrest/relation.rb', line 127

def to_a
  return [] if @null

  Array(@client.get(@table, build_params, schema: @schema).body)
    .map { |attrs| @model_class.new(attrs, true, @client) }
end

#to_sqlObject

Returns a human-readable SQL-like representation of the query reconstructed from the relation’s internal state — no database call is made.

Limitations vs actual SQL:

  • Embedded resources use PostgREST notation: companies(*) instead of LEFT JOIN companies ON companies.id = users.company_id.

  • Parameters are shown as literal values, not PostgreSQL placeholders ($1, $2).

  • The actual query PostgREST sends is a CTE (WITH pgrst_source AS (…)) and may differ in structure. Use #explain to see the real execution plan.



190
191
192
193
194
195
196
197
198
199
200
201
# File 'lib/active_postgrest/relation.rb', line 190

def to_sql
  clauses = ["SELECT #{sql_select}", "FROM #{@table}"]

  wheres = sql_where_clauses
  clauses << "WHERE #{wheres.join("\n  AND ")}" if wheres.any?

  clauses << "ORDER BY #{sql_order}" if @order_val
  clauses << "LIMIT #{@limit_val}"   if @limit_val
  clauses << "OFFSET #{@offset_val}" if @offset_val

  clauses.join("\n")
end

#to_urlObject



207
208
209
210
211
212
213
214
# File 'lib/active_postgrest/relation.rb', line 207

def to_url
  params = build_params
  base   = "#{@client.base_url}/#{@table}"
  return base if params.empty?

  query  = params.flat_map { |k, v| Array(v).map { "#{k}=#{_1}" } }.join('&')
  "#{base}?#{query}"
end

#where(filters = nil) ⇒ Object

where(name: “John”) → name=eq.John where(name: nil) → name=is.null where(active: true) → active=is.true where(id: [1, 2, 3]) → id=in.(1,2,3) where(age: 18..30) → age=gte.18&age=lte.30 where(age: { gt: 18, lt: 65 }) → age=gt.18&age=lt.65 where(companies: { name: “Acme” }) → companies.name=eq.Acme (AR-style joins filter) where.not(name: “John”) → name=not.eq.John



68
69
70
71
72
# File 'lib/active_postgrest/relation.rb', line 68

def where(filters = nil)
  return WhereChain.new(self) if filters.nil?

  clone_with { encode_filters!(filters) }
end

#with_schema(name) ⇒ Object



121
# File 'lib/active_postgrest/relation.rb', line 121

def with_schema(name)    = clone_with { @schema = name }

#with_token(jwt) ⇒ Object



120
# File 'lib/active_postgrest/relation.rb', line 120

def with_token(jwt)      = clone_with { @client = @client.with_token(jwt) }