Class: Cybertrain::Relation

Inherits:
Object
  • Object
show all
Defined in:
cybertrain/relation.rb

Overview

A lazily built SELECT over one table: nothing runs until a method that needs rows (#count, #exists?, to_a, first, each, …).

Each generated model gets its own subclass, <Model>Relation, which adds the chainable query methods (where, where_sql, order, order_sql, limit, offset) and the methods that return records (to_a, each, first, last, find, find_by, size); see ArticleRelation. The methods documented here are the ones every relation shares.

The chain methods change the relation they are called on and return it (they do not copy it), so keep one relation per query:

scope = Comment.where(article_id: article.id)
recent = scope.order("created_at DESC").limit(10).to_a
# scope itself is now ordered and limited too

Implementation: the subclass’s chain methods wrap the setters below and return self, so Post.where(...).first is typed Post|nil: a base-class method that returned self would be typed as the base (spikes/NOTES.md rule 5), which is why these setters return nil.

Direct Known Subclasses

ArticleRelation, CommentRelation

Instance Method Summary collapse

Instance Method Details

#count ⇒ Integer

Counts the rows the relation would return: with a limit or offset the count runs over the windowed subquery (Post.limit(2).count is 2).

Examples:

Comment.where(article_id: article.id).count

Returns:

  • (Integer)


175
176
177
178
179
180
181
182
183
184
# File 'cybertrain/relation.rb', line 175

def count
  sql = if @limit >= 0 || @offset > 0
          "SELECT COUNT(*) AS n FROM (#{select_sql("1", "", @limit, @offset)})"
        else
          select_sql("COUNT(*) AS n", "", -1, 0)
        end
  binds = @binds
  found = Cybertrain::DB.with { |c| c.execute(sql, binds) }
  Cast.int(found[0]["n"])
end

#delete_all ⇒ Integer

Deletes the matching rows and returns how many went, with one DELETE: no model is loaded and no callback runs. With a limit or offset only the rows of that window go (Post.limit(1).delete_all is one row, as count/exists? read the same window): SQLite has no DELETE … LIMIT in a default build, so the window is picked by an id subselect (the quoted primary key); the where binds appear once, inside it.

Examples:

Comment.where(article_id: article.id).delete_all

Returns:

  • (Integer) —

    the number of rows deleted



209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
# File 'cybertrain/relation.rb', line 209

def delete_all
  sql = +"DELETE FROM #{Relation.quote_ident(@table)}"
  if @limit >= 0 || @offset > 0
    # By id when the relation has no order, as first_row/last_row do, so
    # Post.limit(1).delete_all removes the row Post.limit(1).first returns
    # rather than whichever row SQLite scans first.
    id = Relation.quote_ident("id")
    order = @order == "" ? id : @order
    sql << " WHERE #{id} IN (" << select_sql(id, order, @limit, @offset) << ")"
  else
    sql << " WHERE " << @wheres.join(" AND ") unless @wheres.empty?
  end
  binds = @binds
  Cybertrain::DB.with do |c|
    c.execute(sql, binds)
    c.changes
  end
end

#exists? ⇒ Boolean

Whether the relation matches any row (a SELECT 1 ... LIMIT 1). Conditions go on the relation, not the call: Post.where(slug: s).exists?.

Returns:

  • (Boolean)


190
191
192
193
194
195
196
197
# File 'cybertrain/relation.rb', line 190

def exists?
  return false if @limit == 0

  sql = select_sql("1 AS one", "", 1, @offset)
  binds = @binds
  found = Cybertrain::DB.with { |c| c.execute(sql, binds) }
  !found.empty?
end

#to_sql ⇒ String

The SELECT this relation runs, with ? placeholders (see binds). Handy in a test or a log line.

Returns:

  • (String)


156
157
158
# File 'cybertrain/relation.rb', line 156

def to_sql
  select_sql("*", @order, @limit, @offset)
end