Arel:当我尝试从 Arel 获取 SQL 时出现 "stack level too deep" 错误

Arel: I get "stack level too deep" error when I try to get SQL from Arel

首先,我想描述一下我正在尝试做的事情。我有“jobstat_jobs”table,我在其中存储有关计算工作绩效的信息。我正在尝试编写 2 个查询:1) 按项目分组的工作 2) 按项目和状态分组的工作。然后这些查询是内部连接的,我想显示所有工作中每个州的工作份额。我使用 ActiveRecord 和 raw sql 实现了它,但我不能用 arel 来实现它。我在“joined.to_sql”行上得到“堆栈级别太深”。

      members = Core::Member.arel_table
      jobs = Perf::Job.arel_table
      cool_relation = jobs.where(jobs[:state].not_in(%w[COMPLETETED RUNNING unknown]))
      relation = cool_relation.join( Arel::Nodes::SqlLiteral.new <<-SQL
              INNER JOIN core_members ON core_members.login = jobstat_jobs.login
      SQL
      ).join(Arel::Nodes::SqlLiteral.new <<-SQL
          RIGHT JOIN sessions_projects_in_sessions ON
          sessions_projects_in_sessions.project_id = core_members.project_id
        SQL
      ).group(members[:project_id]).project(members[:project_id].as('id'))

      hours = '(extract(epoch from (end_time - start_time))/ 3600)'
      selections = {
        node_hours: "(sum((#{hours})*num_nodes))",
        jobs: "count(jobstat_jobs.id)"
      }
      selections.each do |key, value|
        relation = relation.project(
          Arel::Nodes::SqlLiteral.new(value).as(key.to_s)
        )
      end
      state_relation = relation.project(jobs[:state].as('state'))
                               .group(jobs[:state])
      s = state_relation.as('s')
      pp ActiveRecord::Base.connection.exec_query(state_relation.to_sql).to_a
      joined = relation.join(s)
                       .on(jobs[:id].eq(s[:id]))
                       .project(s[:id], s[:state])
      puts joined.to_sql
      joined

我注意到了奇怪的事情。当我用“jobs.where(jobs[:state].not_in(%w[COMPLETETED 运行 unknown]))”替换“joined = relation”时,它起作用了。但是当我用“joined = cool_relation”替换“joined = relation”时它不起作用并且我得到“堆栈级别太深”(这两个替换几乎相同)。

A​​rel v 9.0.0,Postgresql

我的问题是每次链接方法时我都希望 arel 创建一个新对象(如 ActiveRecord::Relation)。 只需在此处添加#clone 方法:

joined = relation.clone.join(s)
                       .on(jobs[:id].eq(s[:id]))
                       .project(s[:id], s[:state])

我得到了 SQL 字符串,但它是错误的,并且在数据库级别存在异常。现在我的代码如下:

      members = Core::Member.arel_table
      jobs = Perf::Job.arel_table
      cool_relation = jobs.where(jobs[:state].not_in(%w[COMPLETETED RUNNING unknown]))
      relation = cool_relation.join( Arel::Nodes::SqlLiteral.new <<-SQL
              INNER JOIN core_members ON core_members.login = jobstat_jobs.login
      SQL
      .gsub("\n", ' ')).join(Arel::Nodes::SqlLiteral.new <<-SQL
          RIGHT JOIN sessions_projects_in_sessions ON
          sessions_projects_in_sessions.project_id = core_members.project_id
        SQL
      .gsub("\n", ' ')).group(members[:project_id]).project(members[:project_id].as('id'))

      hours = '(extract(epoch from (end_time - start_time))/ 3600)'
      selections = {
        node_hours: "(sum((#{hours})*num_nodes))",
        jobs: "count(jobstat_jobs.id)"
      }
      selections.each do |key, value|
        relation = relation.project(
          # Arel::Nodes::SqlLiteral.new(value).as(key.to_s)
          Arel::Nodes::SqlLiteral.new("(CAST(#{value} AS decimal))").as(key.to_s)
        )
      end
      state_relation = relation.clone.project(jobs[:state].as('state'))
                               .group(jobs[:state])
      s = state_relation.as('s')
      n = relation.as('n')
      pp ActiveRecord::Base.connection.exec_query(state_relation.to_sql).to_a
      pp ActiveRecord::Base.connection.exec_query(relation.to_sql).to_a


      manager = Arel::SelectManager.new
      joined = manager.project(s[:id], s[:state])
                      .from(s)
                      .join(n).on(s[:id].eq(n[:id]))

      selections.keys.each do |key|
        joined = joined.project(s[key].as("s_#{key}"), n[key].as("n_#{key}"))
                       .project(s[key] / n[key].as("share_#{key}"))
      end
      puts joined.to_sql
      joined

也请注意这里使用的#clone 方法。当我删除#clone 时,项目方法也会影响关系变量,因此我出错了 SQL。

joined.to_sql 行产生以下内容并按预期工作:

SELECT s."id", s."state", s."node_hours" AS s_node_hours, 
n."node_hours" AS n_node_hours, s."node_hours" / n."node_hours" AS
share_node_hours, s."jobs" AS s_jobs, n."jobs" AS n_jobs, 
s."jobs" / n."jobs" AS share_jobs FROM (SELECT "core_members".
"project_id" AS id, (CAST((sum(((extract(epoch from (end_time - start_time))/ 3600))*num_nodes)) AS decimal)) AS node_hours, 
(CAST(count(jobstat_jobs.id) AS decimal)) AS jobs, 
"jobstat_jobs"."state" AS state FROM "jobstat_jobs"   INNER JOIN 
core_members ON core_members.login = jobstat_jobs.login            
RIGHT JOIN sessions_projects_in_sessions ON           sessions_projects_in_sessions.project_id = core_members.project_id  
WHERE "jobstat_jobs"."state" NOT IN ('COMPLETETED', 'RUNNING', 'unknown') 
GROUP BY "core_members"."project_id", "jobstat_jobs"."state") s INNER JOIN 
(SELECT "core_members"."project_id" AS id, (CAST((sum(((extract(epoch from 
(end_time - start_time))/ 3600))*num_nodes)) AS decimal)) AS node_hours, 
(CAST(count(jobstat_jobs.id) AS decimal)) AS jobs FROM "jobstat_jobs"     
          INNER JOIN core_members ON core_members.login = jobstat_jobs.login            RIGHT JOIN sessions_projects_in_sessions ON           
sessions_projects_in_sessions.project_id = core_members.project_id  WHERE
 "jobstat_jobs"."state" NOT IN ('COMPLETETED', 'RUNNING', 'unknown') GROUP BY
 "core_members"."project_id") n ON s."id" = n."id"

既然我了解了这里所需的输出,那么我将如何处理此问题

class Report

  JOB_STATS = Arel::Table.new('jobstat_jobs')
  CORE_MEMBERS = Arel::Table.new('core_members')
  SESSIONS = Arel::Table.new('sessions_projects_in_sessions')

  def additions
    # This could be ported too if I knew the tables for end_time, start_time, and num_nodes
    {
      node_hours: Arel.sql("((extract(epoch from (end_time - start_time))/ 3600))*num_nodes").sum,
      jobs: JOB_STATS[:id].count
    }
  end 
  
  def n
    @n ||= _base_query.as('n')
  end 

  def s 
    @s ||= _base_query
            .project(JOB_STATS[:state])
            .group(JOB_STATS[:state]).as('s')
  end 

  def alias_columns 
    additions.keys.flat_map do |key|
      [s[key].as("s_#{key}"), 
       n[key].as("n_#{key}"),
       (s[key] / n[key]).as("share_#{key}")]
    end 
  end

  def query 
    Arel::SelectManager.new.project(
           s[:project_id].as('id'), 
           s[:state],
           *alias_columns 
         )
         .from(s)
         .join(n).on(s[:project_id].eq(n[:project_id]))
  end 

  def to_sql
    query.to_sql 
  end 

  private
    def cast_as_decimal(value,alias_name:)
      Arel::Nodes::NamedFunction.new(
        "CAST",
        [Arel::Nodes::As.new(value, Arel.sql('DECIMAL'))]
      ).as(alias_name.to_s)
    end 
    def _base_query
      JOB_STATS
        .project(
          CORE_MEMBERS[:project_id],
          *additions.map {|k,v| cast_as_decimal(v, alias_name: k)})
        .join(CORE_MEMBERS).on(CORE_MEMBERS[:login].eq(JOB_STATS[:login]))
        .outer_join(SESSIONS).on(SESSIONS[:project_id].eq(CORE_MEMBERS[:project_id]))
        .where(JOB_STATS[:state].not_in(['COMPLETETED', 'RUNNING', 'unknown']))
        .group(CORE_MEMBERS[:project_id])
    end 
end

Report.new.to_sql

的结果
SELECT 
  s."project_id" AS id, 
  s."state", 
  s."node_hours" AS s_node_hours, 
  n."node_hours" AS n_node_hours, 
  s."node_hours" / n."node_hours" AS share_node_hours, 
  s."jobs" AS s_jobs, 
  n."jobs" AS n_jobs, 
  s."jobs" / n."jobs" AS share_jobs 
FROM 
(
  SELECT 
    "core_members"."project_id", 
    CAST(SUM(((extract(epoch from (end_time - start_time))/ 3600))*num_nodes) AS DECIMAL) AS node_hours, 
    CAST(COUNT("jobstat_jobs"."id") AS DECIMAL) AS jobs, 
    "jobstat_jobs"."state" 
  FROM 
    "jobstat_jobs" 
    INNER JOIN "core_members" ON "core_members"."login" = "jobstat_jobs"."login" 
    LEFT OUTER JOIN "sessions_projects_in_sessions" ON "sessions_projects_in_sessions"."project_id" = "core_members"."project_id" 
  WHERE 
    "jobstat_jobs"."state" NOT IN (N'COMPLETETED', N'RUNNING', N'unknown') 
  GROUP BY 
    "core_members"."project_id", 
    "jobstat_jobs"."state"
) s 
INNER JOIN (
  SELECT 
    "core_members"."project_id", 
    CAST(SUM(((extract(epoch from (end_time - start_time))/ 3600))*num_nodes) AS DECIMAL) AS node_hours, 
    CAST(COUNT("jobstat_jobs"."id") AS DECIMAL) AS jobs 
  FROM 
    "jobstat_jobs" 
    INNER JOIN "core_members" ON "core_members"."login" = "jobstat_jobs"."login" 
    LEFT OUTER JOIN "sessions_projects_in_sessions" ON "sessions_projects_in_sessions"."project_id" = "core_members"."project_id" 
  WHERE 
    "jobstat_jobs"."state" NOT IN (N'COMPLETETED', N'RUNNING', N'unknown') 
  GROUP BY 
    "core_members"."project_id"
) n ON s."project_id" = n."project_id"

这还允许您像这样进一步过滤结果查询:

rpt = Report.new 
q = rpt.query.where(rpt.n[:jobs].gt(12))
q.to_sql 
#=> "...same as above...WHERE n.\"jobs\" > 12"