Class: ActivePostgrest::Relation

Inherits:
Object
  • Object
show all
Includes:
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

Constructor Details

#initialize(table, client, model_class) ⇒ Relation

Returns a new instance of Relation.



26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# File 'lib/active_postgrest/relation.rb', line 26

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



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

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)



93
94
95
96
# File 'lib/active_postgrest/relation.rb', line 93

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

#anonymousObject



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

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

#any?(&block) ⇒ Boolean

Returns:

  • (Boolean)


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

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

#average(col) ⇒ Object



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

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

#count(mode = :exact) ⇒ Object



135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/active_postgrest/relation.rb', line 135

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



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

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

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

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



64
65
66
# File 'lib/active_postgrest/relation.rb', line 64

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

#exists?Boolean

Returns:

  • (Boolean)


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

def exists?         = any?

#explainObject



193
194
195
# File 'lib/active_postgrest/relation.rb', line 193

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

#firstObject



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

def first
  limit(1).to_a.first
end

#inspectObject



219
220
221
# File 'lib/active_postgrest/relation.rb', line 219

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



52
53
54
55
# File 'lib/active_postgrest/relation.rb', line 52

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



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

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)



58
59
60
61
# File 'lib/active_postgrest/relation.rb', line 58

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



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

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

#many?Boolean

Returns:

  • (Boolean)


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

def many?           = count > 1

#maximum(col) ⇒ Object



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

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

#minimum(col) ⇒ Object



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

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

#noneObject



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

def none                 = clone_with { @null = true }

#none?(&block) ⇒ Boolean

Returns:

  • (Boolean)


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

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

#not_where(filters) ⇒ Object



82
83
84
# File 'lib/active_postgrest/relation.rb', line 82

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

#offset(n) ⇒ Object



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

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

#one?(&block) ⇒ Boolean

Returns:

  • (Boolean)


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

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

#or_where(conditions) ⇒ Object

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



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

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



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

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

#pick(*cols) ⇒ Object



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

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

#pluck(*cols) ⇒ Object



159
160
161
162
163
164
165
# File 'lib/active_postgrest/relation.rb', line 159

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



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

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)


215
216
217
# File 'lib/active_postgrest/relation.rb', line 215

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

#select(*cols) ⇒ Object



42
43
44
# File 'lib/active_postgrest/relation.rb', line 42

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

#spread(*tables) ⇒ Object



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

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

#sum(col) ⇒ Object



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

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

#to_aObject



118
119
120
121
122
# File 'lib/active_postgrest/relation.rb', line 118

def to_a
  return [] if @null

  Array(@client.get(@table, build_params, schema: @schema).body).map { |attrs| @model_class.new(attrs) }
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.



180
181
182
183
184
185
186
187
188
189
190
191
# File 'lib/active_postgrest/relation.rb', line 180

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



197
198
199
200
201
202
203
204
# File 'lib/active_postgrest/relation.rb', line 197

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



76
77
78
79
80
# File 'lib/active_postgrest/relation.rb', line 76

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

  clone_with { encode_filters!(filters) }
end

#with_schema(name) ⇒ Object



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

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

#with_token(jwt) ⇒ Object



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

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