diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 75bdf1f557..cfc9cd9f83 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -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 diff --git a/activerecord/lib/arel.rb b/activerecord/lib/arel.rb index 148508461c..bd3845c946 100644 --- a/activerecord/lib/arel.rb +++ b/activerecord/lib/arel.rb @@ -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" diff --git a/activerecord/lib/arel/filter_predications.rb b/activerecord/lib/arel/filter_predications.rb new file mode 100644 index 0000000000..23a758d769 --- /dev/null +++ b/activerecord/lib/arel/filter_predications.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Arel + module FilterPredications + def filter(expr) + Nodes::Filter.new(self, expr) + end + end +end diff --git a/activerecord/lib/arel/nodes.rb b/activerecord/lib/arel/nodes.rb index 5f15d39854..7e9804d31b 100644 --- a/activerecord/lib/arel/nodes.rb +++ b/activerecord/lib/arel/nodes.rb @@ -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" diff --git a/activerecord/lib/arel/nodes/filter.rb b/activerecord/lib/arel/nodes/filter.rb new file mode 100644 index 0000000000..8cb3d17bd9 --- /dev/null +++ b/activerecord/lib/arel/nodes/filter.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Arel + module Nodes + class Filter < Binary + include Arel::WindowPredications + include Arel::AliasPredication + end + end +end diff --git a/activerecord/lib/arel/nodes/function.rb b/activerecord/lib/arel/nodes/function.rb index 0a439b39f5..49dbb0eee5 100644 --- a/activerecord/lib/arel/nodes/function.rb +++ b/activerecord/lib/arel/nodes/function.rb @@ -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) diff --git a/activerecord/lib/arel/visitors/to_sql.rb b/activerecord/lib/arel/visitors/to_sql.rb index 1c3d977739..629b834f12 100644 --- a/activerecord/lib/arel/visitors/to_sql.rb +++ b/activerecord/lib/arel/visitors/to_sql.rb @@ -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 " diff --git a/activerecord/test/cases/arel/nodes/filter_test.rb b/activerecord/test/cases/arel/nodes/filter_test.rb new file mode 100644 index 0000000000..117ad8458e --- /dev/null +++ b/activerecord/test/cases/arel/nodes/filter_test.rb @@ -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