Is there a way to create an arel_table from a query?

554 Views Asked by At

I have two tables (A and B) and a relatively complicated Active::Record::Relation that selects from a join of these two tables. The query executes correctly with ActiveRecord::Base.connection.exec_query joined.to_sql, that is, it prints out the columns I want from each table (A.id, A.title, b.num).

I would like to then pass this "joined" table as an Arel::Table, to be used in the rest of the program. However, when I run at_j=joined.arel_table, the Arel table is created from the original database A, not from the one resulting from the "joined" query, i.e. I get all the columns from A (not only the selected ones), and none of the columns from B.

I realise that a first step would be to create an arel table from an already filtered table, i.e. if A has columns id, title, c1, c2, c3... I would like to be able to do:

filtered=A.select(:id,:title)
at_f=filtered.arel_table

and only get id and title in at_f, but that is not what happens, I also get c1, c2, c3....

I know I could do

at_f=A.arel_table.project(:id,:title)

but this outputs an Arel::SelectManager, and I need to pass an Arel::Table (that is out of my hands).

I also would rather not build the query in Arel, because I need to modify the table A that was given as an input, and I can do that using _selct! and joins!.

Is there a way to achieve this? I thought of using something like

at_f=Arel::Table.new(filtered.to_sql)

but that fails, unsurprisingly...

Thanks in advance for your help.

................................

In case this is useful, this is how I get the "joined" active record relation:

A._select!(:id,:title,'b.num')
bf=B.where(c1: 'x',c2: 'y')
num=bf.select('id_2 AS A_id, COUNT(id_2) AS num').group(:id_2)
A.joins!("LEFT OUTER JOIN (#{num.to_sql}) b ON A.id = b.A_id")

and this is the query it generates:

# A.to_sql:
SELECT `A`.`id`, `A`.`title`, `b`.`num` 
  FROM `A` LEFT OUTER JOIN 
    (SELECT id_2 AS A_id, COUNT(id_2) AS num 
      FROM `B` WHERE `B`.`c1` = 'x' AND `B`.`c2` = 'y' 
      GROUP BY `B`.`id_2`) b 
    ON A.id = b.A_id
1

There are 1 best solutions below

3
engineersmnky On

Maybe I understand what you are trying for although I am not sure about the whole Arel::Table part but we can get you that AR Relation from A as follows:

  b_table = B.arel_table
  A.joins(
     Arel::Nodes::OuterJoin.new(b, b.create_on(
        b[:id_2].eq(A.arel_table[:id])
          .and(b[:c1].eq('x'))
          .and(b[:c2].eq('y'))
     )))
   .select(:id, :title, b[:id_2].count.as('num'))
   .group(:id,:title)

This will result in an ActiveRecord::Relation object and when executed will return A objects with only the following attributes: id, title, and num.

The SQL will be:

SELECT 
  a.id, 
  a.title,
  COUNT(b.id_2) AS num
FROM 
  a
  LEFT OUTER JOIN b ON b.id_2 = a.id
    AND b.c1 = 'x'
    AND b.c2 = 'y'
GROUP BY 
  a.id,
  a.title

Which is equivalent to what you have now.

If you truly want to build the query you have now we can certainly do that without issue but this is a bit cleaner.

If this is not your intended outcome please clarify and I will update accordingly.

Notes:

  1. I would like to make it clear that at no time will you be able to "...pass an Arel::Table..." that also contains query syntactics as that is not what a Arel::Table is.
  2. We can produce an Arel::Nodes::TableAlias which kind of duck types an Arel::Table for most intents and purposes and will allow for a query (subquery).
  3. It may be helpful to you to know that you can convert an AR query to Arel by simply using the arel method. For Example:
arel_query = A.select(:id,:title).arel 
#=> #<Arel::SelectManager:0x00007fffd541dd90 ...> 
arel_query.to_sql
#=> "SELECT a.id, a.title FROM a"  

UPDATE with Additional Information:

  • You can create an Arel::Table out of nothing t =Arel::Table.new('c')
  • You can use this as a point of reference for table and column constructs e.g. t[:id] will return an Attribute and will generate SQL of c.id
  • Arel::Table#project - is the SELECT command and returns SelectManager (this object is the primary means of interaction with the AST and the table). SelectManager also has project to add to the current projections.
  • You can use a Table or a TableAlias as a source for the SelectManager using #from
c = Arel::Nodes::TableAlias.new([our query from above], t.name) 
sm = Arel::SelectManager.new(c) 
# alternately sm =  Arel::SelectManager.new; sm.from(c)
sm.project(c[:id])
#=> "SELECT [c].[id] FROM (SELECT a.id, a.title, COUNT(b.id_2) AS num FROM a LEFT OUTER JOIN b ON b.id_2 = a.id AND b.c1 = 'x' AND b.c2 = 'y'  GROUP BY a.id, a.title) [c]
  • You can use the t table we created above or the b table alias to add columns, sort, etc.
sm.project(b[:title],t[:num]).order(t[:num])
sm.to_sql 
#=> SELECT [c].[id], [c].[title], [c].[num] FROM (SELECT a.id, a.title, COUNT(b.id_2) AS num FROM a LEFT OUTER JOIN b ON b.id_2 = a.id AND b.c1 = 'x' AND b.c2 = 'y'  GROUP BY a.id, a.title) [c]  ORDER BY [c].[num]
  • You can access the "froms" in the SelectManager using the froms method which will give you access to the TableAlias in this case which may be what you are looking for regarding the need for an Arel::Table
sm.froms[0]
#=> #<Arel::Nodes::TableAlias:0x00007fffbddc7160 ...>