diff --git a/docs/docs/models-and-databases/reference/query-set.md b/docs/docs/models-and-databases/reference/query-set.md index 5d8be5792..6bf4905be 100644 --- a/docs/docs/models-and-databases/reference/query-set.md +++ b/docs/docs/models-and-databases/reference/query-set.md @@ -621,6 +621,17 @@ Post.all.pluck("title", "published") Alias for [`#count`](#count): returns the number of records that are targetted by the query set. +### `sum` + +Calculates the total sum of values in a specific field across all records within a query set. + +Example: + +```crystal +Order.all.sum(:amount) # Calculates the total amount across all orders +# => 7 +``` + ### `update` Updates all the records matched by the current query set with the passed values. diff --git a/spec/marten/db/model/querying_spec.cr b/spec/marten/db/model/querying_spec.cr index 2d3458677..8993796be 100644 --- a/spec/marten/db/model/querying_spec.cr +++ b/spec/marten/db/model/querying_spec.cr @@ -788,6 +788,16 @@ describe Marten::DB::Model::Querying do end end + describe "::sum" do + it "properly calculates the sum" do + user = TestUser.create!(username: "jd1", email: "jd1@example.com", first_name: "John", last_name: "Doe") + Post.create!(author: user, title: "Example post 1", score: 5.0) + Post.create!(author: user, title: "Example post 2", score: 5.0) + + Post.sum(:score).should eq 10.00 + end + end + describe "::using" do before_each do TestUser.using(:other).create!(username: "jd1", email: "jd1@example.com", first_name: "John", last_name: "Doe") diff --git a/spec/marten/db/query/sql/query_spec.cr b/spec/marten/db/query/sql/query_spec.cr index 98145f508..43915e202 100644 --- a/spec/marten/db/query/sql/query_spec.cr +++ b/spec/marten/db/query/sql/query_spec.cr @@ -1913,6 +1913,37 @@ describe Marten::DB::Query::SQL::Query do end end + describe "#sum" do + it "returns 0 if no records are available" do + query = Marten::DB::Query::SQL::Query(Marten::DB::Query::SQL::QuerySpec::Product).new + + query.sum("price").should eq 0 + end + + it "calculates the correct sum" do + Marten::DB::Query::SQL::QuerySpec::Product.create!( + name: "Awesome Product", + price: 1000, + rating: 5.0, + ) + Marten::DB::Query::SQL::QuerySpec::Product.create!( + name: "Normal Product", + price: 500, + rating: 2.5, + ) + Marten::DB::Query::SQL::QuerySpec::Product.create!( + name: "Boring Product", + price: 100, + rating: 1.0, + ) + + query = Marten::DB::Query::SQL::Query(Marten::DB::Query::SQL::QuerySpec::Product).new + + query.sum("price").should eq 1600 + query.sum("rating").should eq 8.5 + end + end + describe "#to_empty" do it "results in a new EmptyQuery object" do query = Marten::DB::Query::SQL::Query(Tag).new diff --git a/src/marten/db/model/querying.cr b/src/marten/db/model/querying.cr index 70c798bf2..28854632d 100644 --- a/src/marten/db/model/querying.cr +++ b/src/marten/db/model/querying.cr @@ -565,6 +565,17 @@ module Marten default_queryset.raw(query, params) end + # Returns the sum of a field for the current model + # + # This method calculates the total sum of the specified field's values for the considered model. For example: + # + # ``` + # Product.sum(:price) # => 2500 (Assuming there are 100 products with prices averaging to 25) + # ``` + def sum(field : String | Symbol) + default_queryset.sum(field) + end + # Returns a queryset that will be evaluated using the specified database. # # A valid database alias must be used here (it must correspond to an ID of a database configured in the diff --git a/src/marten/db/query/set.cr b/src/marten/db/query/set.cr index 71a58c956..55029d614 100644 --- a/src/marten/db/query/set.cr +++ b/src/marten/db/query/set.cr @@ -976,6 +976,21 @@ module Marten raise NotImplementedError.new("#sum is not supported for query sets") end + # Returns the sum of a field for the current query set. + # + # Calculates the total sum of values within the specified field for the records + # included in the query set. For example: + # + # ``` + # order_items = OrderItem.filter(order_id: 123) + # total_price = order_items.sum(:price) + # ``` + # + # This would calculate the total cost of all items within order number 123. + def sum(field : String | Symbol) + @query.sum(field.to_s) + end + # :nodoc: def to_h raise NotImplementedError.new("#to_h is not supported for query sets") diff --git a/src/marten/db/query/sql/query.cr b/src/marten/db/query/sql/query.cr index 96b0730b2..d497cf118 100644 --- a/src/marten/db/query/sql/query.cr +++ b/src/marten/db/query/sql/query.cr @@ -206,6 +206,20 @@ module Marten !(@limit.nil? && @offset.nil?) end + def sum(raw_field : String) + sql, parameters = build_sum_query(solve_field_and_column(raw_field).last) + + connection.open do |db| + result = db.scalar(sql, args: parameters) + sum = result.to_s + + return 0 if sum.empty? + + number = sum.to_i? + number ? number : sum.to_f + end + end + def to_empty EmptyQuery(Model).new( default_ordering: @default_ordering, @@ -432,6 +446,33 @@ module Marten {sql, parameters} end + private def build_sum_query(column_name) + where, parameters = where_clause_and_parameters + limit = connection.limit_value(@limit) + + sql = build_sql do |s| + s << "SELECT SUM(#{column_name ? column_name.split(".")[-1] : '*'})" + s << "FROM (" + s << "SELECT" + + if distinct + s << connection.distinct_clause_for(distinct_columns) + s << columns + end + + s << column_name + + s << "FROM #{table_name}" + s << build_joins + s << where + s << "LIMIT #{limit}" unless limit.nil? + s << "OFFSET #{@offset}" unless @offset.nil? + s << ") subquery" + end + + {sql, parameters} + end + private def build_update_query(local_values) where, where_parameters = where_clause_and_parameters(offset: local_values.size)