Class: AppQuery::Q

Inherits:
Object
  • Object
show all
Defined in:
lib/app_query.rb

Overview

Query object for building, rendering, and executing SQL queries.

Q wraps a SQL string (optionally with ERB templating) and provides methods for query execution, CTE manipulation, and result handling.

Method Groups

  • Rendering — Process ERB templates to produce executable SQL.
  • Query Execution — Execute queries against the database. These methods wrap the equivalent ActiveRecord::Base.connection methods (select_all, insert, update, delete).
  • Query Introspection — Inspect and analyze the structure of the query.
  • Query Transformation — Create modified copies of the query. All transformation methods are immutable—they return a new Q instance and leave the original unchanged.
  • CTE Manipulation — Add, replace, or reorder Common Table Expressions (CTEs). Like transformation methods, these return a new Q instance.

Examples:

Basic query

AppQuery("SELECT * FROM users WHERE id = $1").select_one(binds: [1])

ERB templating

AppQuery("SELECT * FROM users WHERE name = <%= bind(name) %>")
  .render(name: "Alice")
  .select_all

CTE manipulation

AppQuery("WITH base AS (SELECT 1) SELECT * FROM base")
  .append_cte("extra AS (SELECT 2)")
  .select_all

Instance Attribute Summary collapse

Rendering collapse

Query Execution collapse

Query Introspection collapse

Query Transformation collapse

CTE Manipulation collapse

Instance Method Summary collapse

Constructor Details

#initialize(sql, name: nil, filename: nil, binds: {}, cast: true, cte_depth: 0) ⇒ Q

Creates a new query object.

Examples:

Simple query

Q.new("SELECT * FROM users")

With ERB and binds

Q.new("SELECT * FROM users WHERE id = :id", binds: {id: 1})

Parameters:

  • sql (String)

    the SQL query string (may contain ERB)

  • name (String, nil) (defaults to: nil)

    optional name for logging

  • filename (String, nil) (defaults to: nil)

    optional filename for ERB error reporting

  • binds (Hash, nil) (defaults to: {})

    bind parameters for the query

  • cast (Boolean, Hash, Array) (defaults to: true)

    type casting configuration



227
228
229
230
231
232
233
234
235
# File 'lib/app_query.rb', line 227

def initialize(sql, name: nil, filename: nil, binds: {}, cast: true, cte_depth: 0)
  @sql = sql
  @name = name
  @filename = filename
  @binds = binds
  @cast = cast
  @cte_depth = cte_depth
  @binds = binds_with_defaults(sql, binds)
end

Instance Attribute Details

#bindsString, ... (readonly)

Returns:

  • (String, nil)

    optional name for the query (used in logs)

  • (String)

    the SQL string

  • (Array, Hash)

    bind parameters

  • (Boolean, Hash, Array)

    casting configuration



212
213
214
# File 'lib/app_query.rb', line 212

def binds
  @binds
end

#castString, ... (readonly)

Returns:

  • (String, nil)

    optional name for the query (used in logs)

  • (String)

    the SQL string

  • (Array, Hash)

    bind parameters

  • (Boolean, Hash, Array)

    casting configuration



212
213
214
# File 'lib/app_query.rb', line 212

def cast
  @cast
end

#cte_depthObject (readonly)

Returns the value of attribute cte_depth.



237
238
239
# File 'lib/app_query.rb', line 237

def cte_depth
  @cte_depth
end

#filenameString, ... (readonly)

Returns:

  • (String, nil)

    optional name for the query (used in logs)

  • (String)

    the SQL string

  • (Array, Hash)

    bind parameters

  • (Boolean, Hash, Array)

    casting configuration



212
213
214
# File 'lib/app_query.rb', line 212

def filename
  @filename
end

#nameString, ... (readonly)

Returns:

  • (String, nil)

    optional name for the query (used in logs)

  • (String)

    the SQL string

  • (Array, Hash)

    bind parameters

  • (Boolean, Hash, Array)

    casting configuration



212
213
214
# File 'lib/app_query.rb', line 212

def name
  @name
end

#sqlString, ... (readonly)

Returns:

  • (String, nil)

    optional name for the query (used in logs)

  • (String)

    the SQL string

  • (Array, Hash)

    bind parameters

  • (Boolean, Hash, Array)

    casting configuration



212
213
214
# File 'lib/app_query.rb', line 212

def sql
  @sql
end

Instance Method Details

#add_binds(**binds) ⇒ Q

Returns a new query with binds added.

Examples:

query = AppQuery("SELECT :foo, :bar", binds: {foo: 1})
query.add_binds(bar: 2).binds
# => {foo: 1, bar: 2}

Parameters:

  • binds (Hash, nil)

    the bind parameters to add

Returns:

  • (Q)

    a new query object with the added binds



640
641
642
# File 'lib/app_query.rb', line 640

def add_binds(**binds)
  deep_dup(binds: self.binds.merge(binds))
end

#append_cte(cte) ⇒ Q

Appends a CTE to the end of the WITH clause.

If the query has no CTEs, wraps it with WITH. If the query already has CTEs, adds the new CTE at the end.

Examples:

Adding a CTE to a simple query

AppQuery("SELECT 1").append_cte("foo AS (SELECT 2)")
# => "WITH foo AS (SELECT 2) SELECT 1"

Appending to existing CTEs

AppQuery("WITH bar AS (SELECT 2) SELECT * FROM bar")
  .append_cte("foo AS (SELECT 1)")
# => "WITH bar AS (SELECT 2), foo AS (SELECT 1) SELECT * FROM bar"

Parameters:

  • cte (String)

    the CTE definition (e.g., "foo AS (SELECT 1)")

Returns:

  • (Q)

    a new query object with the appended CTE



784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
# File 'lib/app_query.rb', line 784

def append_cte(cte)
  # early raise when cte is not valid sql
  add_recursive, to_append = Tokenizer.tokenize(cte, state: :lex_append_cte).then do |tokens|
    [!recursive? && tokens.find { _1[:t] == "RECURSIVE" },
      tokens.reject { _1[:t] == "RECURSIVE" }]
  end

  if cte_names.none?
    with_sql("WITH #{cte}\n#{self}")
  else
    nof_ctes = cte_names.size

    with_sql(tokens.map do |token|
      nof_ctes -= 1 if token[:t] == "CTE_SELECT"

      if nof_ctes.zero?
        nof_ctes -= 1
        token[:v] + to_append.map { _1[:v] }.join
      elsif token[:t] == "WITH" && add_recursive
        token[:v] + add_recursive[:v]
      else
        token[:v]
      end
    end.join)
  end
end

#column(c, s = nil, binds: {}) ⇒ Array

Returns an array of values for a single column.

Wraps the query in a CTE and selects only the specified column, which is more efficient than fetching all columns via select_all.column(name). The column name is safely quoted, making this method safe for user input.

Examples:

Extract a single column

AppQuery("SELECT id, name FROM users").column(:name)
# => ["Alice", "Bob", "Charlie"]

With additional filtering

AppQuery("SELECT * FROM users").column(:email, "SELECT * FROM :_ WHERE active")
# => ["alice@example.com", "bob@example.com"]

Parameters:

  • c (String, Symbol)

    the column name to extract

  • s (String, nil) (defaults to: nil)

    optional SELECT to apply before extracting

  • binds (Hash, nil) (defaults to: {})

    bind parameters to add

Returns:

  • (Array)

    the column values



445
446
447
448
# File 'lib/app_query.rb', line 445

def column(c, s = nil, binds: {})
  quoted_column = ActiveRecord::Base.connection.quote_column_name(c)
  with_select(s).select_all("SELECT #{quoted_column} AS column FROM :_", binds:).column("column")
end

#count(s = nil, binds: {}) ⇒ Integer

Returns the count of rows from the query.

Wraps the query in a CTE and selects only the count, which is more efficient than fetching all rows via select_all.count.

Examples:

Simple count

AppQuery("SELECT * FROM users").count
# => 42

Count with filtering

AppQuery("SELECT * FROM users")
  .with_select("SELECT * FROM :_ WHERE active")
  .count
# => 10

Parameters:

  • s (String, nil) (defaults to: nil)

    optional SELECT to apply before counting

  • binds (Hash, nil) (defaults to: {})

    bind parameters to add

Returns:

  • (Integer)

    the count of rows



423
424
425
# File 'lib/app_query.rb', line 423

def count(s = nil, binds: {})
  with_select(s).select_all("SELECT COUNT(*) c FROM :_", binds:).column("c").first
end

#cte_namesArray<String>

Returns the names of all CTEs (Common Table Expressions) in the query.

Examples:

AppQuery("WITH a AS (SELECT 1), b AS (SELECT 2) SELECT * FROM a, b").cte_names
# => ["a", "b"]

Returns:

  • (Array<String>)

    the CTE names in order of appearance



611
612
613
# File 'lib/app_query.rb', line 611

def cte_names
  tokens.filter { _1[:t] == "CTE_IDENTIFIER" }.map { _1[:v] }
end

#deep_dup(sql: self.sql, name: self.name, filename: self.filename, binds: self.binds.dup, cast: self.cast, cte_depth: self.cte_depth) ⇒ Object



256
257
258
# File 'lib/app_query.rb', line 256

def deep_dup(sql: self.sql, name: self.name, filename: self.filename, binds: self.binds.dup, cast: self.cast, cte_depth: self.cte_depth)
  self.class.new(sql, name:, filename:, binds:, cast:, cte_depth:)
end

#delete(binds: {}) ⇒ Integer

Executes a DELETE query.

Examples:

With named binds

AppQuery("DELETE FROM videos WHERE id = :id").delete(binds: {id: 1})

With positional binds

AppQuery("DELETE FROM videos WHERE id = $1").delete(binds: [1])

Parameters:

  • binds (Hash, nil) (defaults to: {})

    bind parameters for the query

Returns:

  • (Integer)

    the number of deleted rows

Raises:



573
574
575
576
577
578
579
580
581
582
583
584
585
# File 'lib/app_query.rb', line 573

def delete(binds: {})
  with_binds(**binds).render({}).then do |aq|
    sql = if ActiveRecord::VERSION::STRING.to_f >= 7.1
      aq.to_arel
    else
      ActiveRecord::Base.sanitize_sql_array([aq.to_s, **aq.binds])
    end
    ActiveRecord::Base.connection.delete(sql, name)
  end
rescue NameError => e
  raise e unless e.instance_of?(NameError)
  raise UnrenderedQueryError, "Query is ERB. Use #render before deleting."
end

#entriesArray<Hash>

Executes the query and returns results as an Array of Hashes.

Shorthand for select_all(...).entries. Accepts the same arguments as #select_all.

Examples:

AppQuery("SELECT * FROM users").entries
# => [{"id" => 1, "name" => "Alice"}, {"id" => 2, "name" => "Bob"}]

Returns:

  • (Array<Hash>)

    the query results as an array

See Also:



482
483
484
# File 'lib/app_query.rb', line 482

def entries(...)
  select_all(...).entries
end

#ids(s = nil, binds: {}) ⇒ Array

Returns an array of id values from the query.

Convenience method equivalent to column(:id). More efficient than fetching all columns via select_all.column("id").

Examples:

Get all user IDs

AppQuery("SELECT * FROM users").ids
# => [1, 2, 3]

With filtering

AppQuery("SELECT * FROM users").ids("SELECT * FROM :_ WHERE active")
# => [1, 3]

Parameters:

  • s (String, nil) (defaults to: nil)

    optional SELECT to apply before extracting

  • binds (Hash, nil) (defaults to: {})

    bind parameters to add

Returns:

  • (Array)

    the id values



466
467
468
# File 'lib/app_query.rb', line 466

def ids(s = nil, binds: {})
  column(:id, s, binds:)
end

#insert(binds: {}, returning: nil) ⇒ Integer, Object

Executes an INSERT query.

Examples:

With positional binds

AppQuery(<<~SQL).insert(binds: ["Let's learn SQL!"])
  INSERT INTO videos(title, created_at, updated_at) VALUES($1, now(), now())
SQL

With values helper

articles = [{title: "First", created_at: Time.current}]
AppQuery(<<~SQL).render(articles:).insert
  INSERT INTO articles(title, created_at) <%= values(articles) %>
SQL

With returning (Rails 7.1+)

AppQuery("INSERT INTO users(name) VALUES($1)")
  .insert(binds: ["Alice"], returning: "id, created_at")

Parameters:

  • binds (Hash, nil) (defaults to: {})

    bind parameters for the query

  • returning (String, nil) (defaults to: nil)

    columns to return (Rails 7.1+ only)

Returns:

  • (Integer, Object)

    the inserted ID or returning value

Raises:

  • (UnrenderedQueryError)

    if the query contains unrendered ERB

  • (ArgumentError)

    if returning is used with Rails < 7.1



509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
# File 'lib/app_query.rb', line 509

def insert(binds: {}, returning: nil)
  # ActiveRecord::Base.connection.insert(sql, name, _pk = nil, _id_value = nil, _sequence_name = nil, binds, returning: nil)
  if returning && ActiveRecord::VERSION::STRING.to_f < 7.1
    raise ArgumentError, "The 'returning' option requires Rails 7.1+. Current version: #{ActiveRecord::VERSION::STRING}"
  end

  with_binds(**binds).render({}).then do |aq|
    sql = if ActiveRecord::VERSION::STRING.to_f >= 7.1
      aq.to_arel
    else
      ActiveRecord::Base.sanitize_sql_array([aq.to_s, **aq.binds])
    end
    if ActiveRecord::VERSION::STRING.to_f >= 7.1
      ActiveRecord::Base.connection.insert(sql, name, returning:)
    else
      ActiveRecord::Base.connection.insert(sql, name)
    end
  end
rescue NameError => e
  # Prevent any subclasses, e.g. NoMethodError
  raise e unless e.instance_of?(NameError)
  raise UnrenderedQueryError, "Query is ERB. Use #render before select-ing."
end

#prepend_cte(cte) ⇒ Q

Prepends a CTE to the beginning of the WITH clause.

If the query has no CTEs, wraps it with WITH. If the query already has CTEs, adds the new CTE at the beginning.

Examples:

Adding a CTE to a simple query

AppQuery("SELECT 1").prepend_cte("foo AS (SELECT 2)")
# => "WITH foo AS (SELECT 2) SELECT 1"

Prepending to existing CTEs

AppQuery("WITH bar AS (SELECT 2) SELECT * FROM bar")
  .prepend_cte("foo AS (SELECT 1)")
# => "WITH foo AS (SELECT 1), bar AS (SELECT 2) SELECT * FROM bar"

Parameters:

  • cte (String)

    the CTE definition (e.g., "foo AS (SELECT 1)")

Returns:

  • (Q)

    a new query object with the prepended CTE



748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
# File 'lib/app_query.rb', line 748

def prepend_cte(cte)
  # early raise when cte is not valid sql
  to_append = Tokenizer.tokenize(cte, state: :lex_prepend_cte).then do |tokens|
    recursive? ? tokens.reject { _1[:t] == "RECURSIVE" } : tokens
  end

  if cte_names.none?
    with_sql("WITH #{cte}\n#{self}")
  else
    split_at_type = recursive? ? "RECURSIVE" : "WITH"
    with_sql(tokens.map do |token|
      if token[:t] == split_at_type
        token[:v] + to_append.map { _1[:v] }.join
      else
        token[:v]
      end
    end.join)
  end
end

#recursive?Boolean

Checks if the query uses RECURSIVE CTEs.

Examples:

AppQuery("WITH RECURSIVE t AS (...) SELECT * FROM t").recursive?
# => true

Returns:

  • (Boolean)

    true if the query contains WITH RECURSIVE



726
727
728
# File 'lib/app_query.rb', line 726

def recursive?
  !!tokens.find { _1[:t] == "RECURSIVE" }
end

#render(vars = {}) ⇒ Q

Renders the ERB template with the given variables.

Processes ERB tags in the SQL and collects any bind parameters created by helpers like RenderHelpers#bind and RenderHelpers#values.

Examples:

Rendering with variables

AppQuery("SELECT * FROM users WHERE name = <%= bind(name) %>")
  .render(name: "Alice")
# => Q with SQL: "SELECT * FROM users WHERE name = :b1"
#    and binds: {b1: "Alice"}

Using instance variables

AppQuery("SELECT * FROM users WHERE active = <%= @active %>")
  .render(active: true)

vars are available as local and instance variable.

# This fails as `ordering` is not provided:
AppQuery(<<~SQL).render
  SELECT * FROM articles
  <%= order_by(ordering) %>
SQL

# ...but this query works without `ordering` being passed to render:
AppQuery(<<~SQL).render
  SELECT * FROM articles
  <%= @ordering.presence && order_by(ordering) %>
SQL
# NOTE that `@ordering.present? && ...` would render as `false`.
# Use `@ordering.presence` instead.

Parameters:

  • vars (Hash) (defaults to: {})

    variables to make available in the ERB template

Returns:

  • (Q)

    a new query object with rendered SQL and collected binds

See Also:



297
298
299
300
301
302
303
304
# File 'lib/app_query.rb', line 297

def render(vars = {})
  vars ||= {}
  helper = render_helper(vars)
  sql = to_erb.result(helper.get_binding)
  collected = helper.collected_binds

  with_sql(sql).add_binds(**collected)
end

#replace_cte(cte) ⇒ Q

Replaces an existing CTE with a new definition.

Examples:

AppQuery("WITH foo AS (SELECT 1) SELECT * FROM foo")
  .replace_cte("foo AS (SELECT 2)")
# => "WITH foo AS (SELECT 2) SELECT * FROM foo"

Parameters:

  • cte (String)

    the new CTE definition (must have same name as existing CTE)

Returns:

  • (Q)

    a new query object with the replaced CTE

Raises:

  • (ArgumentError)

    if the CTE name doesn't exist in the query



822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
# File 'lib/app_query.rb', line 822

def replace_cte(cte)
  add_recursive, to_append = Tokenizer.tokenize(cte, state: :lex_recursive_cte).then do |tokens|
    [!recursive? && tokens.find { _1[:t] == "RECURSIVE" },
      tokens.reject { _1[:t] == "RECURSIVE" }]
  end

  cte_name = to_append.find { _1[:t] == "CTE_IDENTIFIER" }&.[](:v)
  unless cte_names.include?(cte_name)
    raise ArgumentError, "Unknown cte #{cte_name.inspect}. Options: #{cte_names}."
  end
  cte_ix = cte_names.index(cte_name)

  return self unless cte_ix

  cte_found = false

  with_sql(tokens.map do |token|
    if cte_found ||= token[:t] == "CTE_IDENTIFIER" && token[:v] == cte_name
      unless (cte_found = (token[:t] != "CTE_SELECT"))
        next to_append.map { _1[:v] }.join
      end

      next
    elsif token[:t] == "WITH" && add_recursive
      token[:v] + add_recursive[:v]
    else
      token[:v]
    end
  end.join)
end

#selectString?

Returns the SELECT clause of the query.

Examples:

AppQuery("SELECT id, name FROM users").select
# => "SELECT id, name FROM users"

Returns:

  • (String, nil)

    the SELECT clause, or nil if not found



715
716
717
# File 'lib/app_query.rb', line 715

def select
  tokens.find { _1[:t] == "SELECT" }&.[](:v)
end

#select_all(s = nil, binds: {}, cast: self.cast) ⇒ Result

Executes the query and returns all matching rows.

TODO: have aliases for common casts: select_all(cast: => :date)

Examples:

(Named) binds

AppQuery("SELECT * FROM users WHERE id = :id").select_all(binds: {id: 1})

With type casting

AppQuery("SELECT created_at FROM users")
  .select_all(cast: {created_at: ActiveRecord::Type::DateTime.new})

Override SELECT clause

AppQuery("SELECT * FROM users").select_all(select: "COUNT(*)")

Parameters:

  • select (String, nil)

    override the SELECT clause

  • binds (Hash, nil) (defaults to: {})

    bind parameters to add

  • cast (Boolean, Hash, Array) (defaults to: self.cast)

    type casting configuration

Returns:

  • (Result)

    the query results with optional type casting

Raises:



355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
# File 'lib/app_query.rb', line 355

def select_all(s = nil, binds: {}, cast: self.cast)
  add_binds(**binds).with_select(s).render({}).then do |aq|
    sql = if ActiveRecord::VERSION::STRING.to_f >= 7.1
      aq.to_arel
    else
      ActiveRecord::Base.sanitize_sql_array([aq.to_s, aq.binds])
    end
    ActiveRecord::Base.connection.select_all(sql, aq.name).then do |result|
      Result.from_ar_result(result, cast)
    end
  end
rescue NameError => e
  # Prevent any subclasses, e.g. NoMethodError
  raise e unless e.instance_of?(NameError)
  raise UnrenderedQueryError, "Query is ERB. Use #render before select-ing."
end

#select_one(s = nil, binds: {}, cast: self.cast) ⇒ Hash? Also known as: first

Executes the query and returns the first row.

Examples:

AppQuery("SELECT * FROM users WHERE id = :id").select_one(binds: {id: 1})
# => {"id" => 1, "name" => "Alice"}

Parameters:

  • binds (Hash, nil) (defaults to: {})

    bind parameters to add

  • select (String, nil)

    override the SELECT clause

  • cast (Boolean, Hash, Array) (defaults to: self.cast)

    type casting configuration

Returns:

  • (Hash, nil)

    the first row as a hash, or nil if no results

See Also:



384
385
386
# File 'lib/app_query.rb', line 384

def select_one(s = nil, binds: {}, cast: self.cast)
  with_select(s).select_all("SELECT * FROM :_ LIMIT 1", binds:, cast:).first
end

#select_value(s = nil, binds: {}, cast: self.cast) ⇒ Object?

Executes the query and returns the first value of the first row.

Examples:

AppQuery("SELECT COUNT(*) FROM users").select_value
# => 42

Parameters:

  • binds (Hash, nil) (defaults to: {})

    named bind parameters

  • select (String, nil)

    override the SELECT clause

  • cast (Boolean, Hash, Array) (defaults to: self.cast)

    type casting configuration

Returns:

  • (Object, nil)

    the first value, or nil if no results

See Also:



401
402
403
# File 'lib/app_query.rb', line 401

def select_value(s = nil, binds: {}, cast: self.cast)
  select_one(s, binds:, cast:)&.values&.first
end

#to_arelObject



239
240
241
242
243
244
245
246
# File 'lib/app_query.rb', line 239

def to_arel
  if binds.presence
    Arel::Nodes::BoundSqlLiteral.new sql, [], binds
  else
    # TODO: add retryable? available from >=7.1
    Arel::Nodes::SqlLiteral.new(sql)
  end
end

#to_sString

Returns the SQL string.

Returns:

  • (String)

    the SQL query string



858
859
860
# File 'lib/app_query.rb', line 858

def to_s
  @sql
end

#tokenizerTokenizer

Returns the tokenizer instance for this query.

Returns:



600
601
602
# File 'lib/app_query.rb', line 600

def tokenizer
  @tokenizer ||= Tokenizer.new(to_s)
end

#tokensArray<Hash>

Returns the tokenized representation of the SQL.

Returns:

  • (Array<Hash>)

    array of token hashes with :t (type) and :v (value) keys

See Also:



593
594
595
# File 'lib/app_query.rb', line 593

def tokens
  @tokens ||= tokenizer.run
end

#update(binds: {}) ⇒ Integer

Executes an UPDATE query.

Examples:

With named binds

AppQuery("UPDATE videos SET title = 'New' WHERE id = :id")
  .update(binds: {id: 1})

With positional binds

AppQuery("UPDATE videos SET title = $1 WHERE id = $2")
  .update(binds: ["New Title", 1])

Parameters:

  • binds (Hash, nil) (defaults to: {})

    bind parameters for the query

Returns:

  • (Integer)

    the number of affected rows

Raises:



547
548
549
550
551
552
553
554
555
556
557
558
559
# File 'lib/app_query.rb', line 547

def update(binds: {})
  with_binds(**binds).render({}).then do |aq|
    sql = if ActiveRecord::VERSION::STRING.to_f >= 7.1
      aq.to_arel
    else
      ActiveRecord::Base.sanitize_sql_array([aq.to_s, **aq.binds])
    end
    ActiveRecord::Base.connection.update(sql, name)
  end
rescue NameError => e
  raise e unless e.instance_of?(NameError)
  raise UnrenderedQueryError, "Query is ERB. Use #render before updating."
end

#with_binds(**binds) ⇒ Q Also known as: replace_binds

Returns a new query with different bind parameters.

Examples:

query = AppQuery("SELECT :foo, :bar", binds: {foo: 1})
query.with_binds(bar: 2).binds
# => {foo: nil, bar: 2}

Parameters:

  • binds (Hash, nil)

    the bind parameters

Returns:

  • (Q)

    a new query object with the binds replaced



626
627
628
# File 'lib/app_query.rb', line 626

def with_binds(**binds)
  deep_dup(binds:)
end

#with_cast(cast) ⇒ Q

Returns a new query with different cast settings.

Examples:

query = AppQuery("SELECT created_at FROM users")
query.with_cast(false).select_all  # disable casting

Parameters:

  • cast (Boolean, Hash, Array)

    the new cast configuration

Returns:

  • (Q)

    a new query object with the specified cast settings



652
653
654
# File 'lib/app_query.rb', line 652

def with_cast(cast)
  deep_dup(cast:)
end

#with_select(sql) ⇒ Q

Returns a new query with a modified SELECT statement.

Wraps the current SELECT in a numbered CTE and applies the new SELECT. CTEs are named _, _1, _2, etc. Use :_ in the new SELECT to reference the previous result.

Examples:

Single transformation

AppQuery("SELECT * FROM users").with_select("SELECT COUNT(*) FROM :_")
# => "WITH _ AS (\n  SELECT * FROM users\n)\nSELECT COUNT(*) FROM _"

Chained transformations

AppQuery("SELECT * FROM users")
  .with_select("SELECT * FROM :_ WHERE active")
  .with_select("SELECT COUNT(*) FROM :_")
# => WITH _ AS (SELECT * FROM users),
#         _1 AS (SELECT * FROM _ WHERE active)
#    SELECT COUNT(*) FROM _1

Parameters:

  • sql (String, nil)

    the new SELECT statement (nil returns self)

Returns:

  • (Q)

    a new query object with the modified SELECT



684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
# File 'lib/app_query.rb', line 684

def with_select(sql)
  return self if sql.nil?

  # First CTE is "_", then "_1", "_2", etc.
  current_cte = (cte_depth == 0) ? "_" : "_#{cte_depth}"

  # Replace :_ with the current CTE name
  processed_sql = sql.gsub(/:_\b/, current_cte)

  # Wrap current SELECT in numbered CTE
  new_cte = "#{current_cte} AS (\n  #{select}\n)"

  append_cte(new_cte).then do |q|
    # Replace the SELECT token with processed_sql and increment depth
    new_sql = q.tokens.each_with_object([]) do |token, acc|
      v = (token[:t] == "SELECT") ? processed_sql : token[:v]
      acc << v
    end.join
    q.deep_dup(sql: new_sql, cte_depth: cte_depth + 1)
  end
end

#with_sql(sql) ⇒ Q

Returns a new query with different SQL.

Parameters:

  • sql (String)

    the new SQL string

Returns:

  • (Q)

    a new query object with the specified SQL



660
661
662
# File 'lib/app_query.rb', line 660

def with_sql(sql)
  deep_dup(sql:)
end