Arel: Add support for FILTER clause (SQL:2003)

Currently supported by PostgreSQL 9.4+ and SQLite 3.30+

See:

 - http://modern-sql.com/feature/filter
 - https://www.postgresql.org/docs/9.4/static/sql-expressions.html#SYNTAX-AGGREGATES
 - https://sqlite.org/lang_aggfunc.html#aggfilter

Example:

    Model.all.pluck(
      Arel.star.count.as('records_total').to_sql,
      Arel.star.count.filter(Model.arel_table[:some_column].not_eq(nil)).as('records_filtered').to_sql,
    )
This commit is contained in:
Andrey Novikov 2020-10-30 20:11:19 +03:00
parent c0911e9a36
commit a738295805
No known key found for this signature in database
GPG Key ID: 301AD540051205A2
8 changed files with 86 additions and 0 deletions

@ -1,3 +1,23 @@
* Arel: Add support for FILTER clause (SQL:2003)
Currently supported by PostgreSQL 9.4+ and SQLite 3.30+
Example usage:
Model.all.pluck(
Arel.star.count.as('records_total').to_sql,
Arel.star.count.filter(Model.arel_table[:some_column].not_eq(nil)).as('records_filtered').to_sql,
)
Example result:
SELECT
COUNT(*) AS records_total,
COUNT(*) FILTER (WHERE "some_column" IS NOT NULL) AS records_filtered
FROM models
*Andrey Novikov*
* Two change tracking methods are added for `belongs_to` associations.
The `association_changed?` method (assuming an association named `:association`) returns true

@ -7,6 +7,7 @@
require "arel/expressions"
require "arel/predications"
require "arel/filter_predications"
require "arel/window_predications"
require "arel/math"
require "arel/alias_predication"

@ -0,0 +1,9 @@
# frozen_string_literal: true
module Arel
module FilterPredications
def filter(expr)
Nodes::Filter.new(self, expr)
end
end
end

@ -28,6 +28,7 @@
# binary
require "arel/nodes/binary"
require "arel/nodes/equality"
require "arel/nodes/filter"
require "arel/nodes/in"
require "arel/nodes/join_source"
require "arel/nodes/delete_statement"

@ -0,0 +1,10 @@
# frozen_string_literal: true
module Arel
module Nodes
class Filter < Binary
include Arel::WindowPredications
include Arel::AliasPredication
end
end
end

@ -4,6 +4,7 @@ module Arel # :nodoc: all
module Nodes
class Function < Arel::Nodes::NodeExpression
include Arel::WindowPredications
include Arel::FilterPredications
attr_accessor :expressions, :alias, :distinct
def initialize(expr, aliaz = nil)

@ -245,6 +245,13 @@ def visit_Arel_Nodes_Window(o, collector)
collector << ")"
end
def visit_Arel_Nodes_Filter(o, collector)
visit o.left, collector
collector << " FILTER (WHERE "
visit o.right, collector
collector << ")"
end
def visit_Arel_Nodes_Rows(o, collector)
if o.expr
collector << "ROWS "

@ -0,0 +1,37 @@
# frozen_string_literal: true
require_relative "../helper"
module Arel
module Nodes
class ::FilterTest < Arel::Spec
describe "Filter" do
it "should add filter to expression" do
table = Arel::Table.new :users
_(table[:id].count.filter(table[:income].gteq(40_000)).to_sql).must_be_like %{
COUNT("users"."id") FILTER (WHERE "users"."income" >= 40000)
}
end
describe "as" do
it "should alias the expression" do
table = Arel::Table.new :users
_(table[:id].count.filter(table[:income].gteq(40_000)).as("rich_users_count").to_sql).must_be_like %{
COUNT("users"."id") FILTER (WHERE "users"."income" >= 40000) AS rich_users_count
}
end
end
describe "over" do
it "should reference the window definition by name" do
table = Arel::Table.new :users
window = Arel::Nodes::Window.new.partition(table[:year])
_(table[:id].count.filter(table[:income].gteq(40_000)).over(window).to_sql).must_be_like %{
COUNT("users"."id") FILTER (WHERE "users"."income" >= 40000) OVER (PARTITION BY "users"."year")
}
end
end
end
end
end
end