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”时它不起作用并且我得到“堆栈级别太深”(这两个替换几乎相同)。
Arel 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"
首先,我想描述一下我正在尝试做的事情。我有“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”时它不起作用并且我得到“堆栈级别太深”(这两个替换几乎相同)。
Arel 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"