Skip to content

Commit

Permalink
Implement sum to query sets (#160)
Browse files Browse the repository at this point in the history
* Add the ability to compute the sum of a given column in query sets

* Fix ameba finding

* Fix findings
  • Loading branch information
treagod authored Feb 19, 2024
1 parent f5c978b commit 49ca861
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 0 deletions.
11 changes: 11 additions & 0 deletions docs/docs/models-and-databases/reference/query-set.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
10 changes: 10 additions & 0 deletions spec/marten/db/model/querying_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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: "[email protected]", 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: "[email protected]", first_name: "John", last_name: "Doe")
Expand Down
31 changes: 31 additions & 0 deletions spec/marten/db/query/sql/query_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions src/marten/db/model/querying.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions src/marten/db/query/set.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
41 changes: 41 additions & 0 deletions src/marten/db/query/sql/query.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)

Expand Down

0 comments on commit 49ca861

Please sign in to comment.