diff --git a/spec/model/model_spec.cr b/spec/model/model_spec.cr index 0293c1620..79a3607f5 100644 --- a/spec/model/model_spec.cr +++ b/spec/model/model_spec.cr @@ -328,5 +328,36 @@ module ModelSpec end end end + + context "with pagination" do + it "can pull the next 5 users from page 2" do + temporary do + reinit + + 15.times do |x| + User.create!({first_name: "user#{x}"}) + end + + users = User.query.paginate(page: 2, per_page: 5) + users.map(&.first_name).should eq ["user5", "user6", "user7", "user8", "user9"] + users.total_entries.should eq 15 + end + end + + it "can paginate with where clause" do + temporary do + reinit + last_names = ["smith", "jones"] + 15.times do |x| + last_name = last_names[x % 2]? + User.create!({first_name: "user#{x}", last_name: last_name}) + end + + users = User.query.where { last_name == "smith" }.paginate(page: 1, per_page: 5) + users.map(&.first_name).should eq ["user0", "user2", "user4", "user6", "user8"] + users.total_entries.should eq 8 + end + end + end end end diff --git a/spec/sql/select_spec.cr b/spec/sql/select_spec.cr index e9586c5ec..90b6f470d 100644 --- a/spec/sql/select_spec.cr +++ b/spec/sql/select_spec.cr @@ -267,6 +267,81 @@ module SelectSpec end end end + + describe "WithPagination" do + context "when there's 1901902 records and limit of 25" do + it "sets the per_page to 25" do + r = select_request.from(:users).offset(0).limit(25) + r.total_entries = 1901902_i64 + r.per_page.should eq 25 + end + + it "returns 1 for current_page with no limit set" do + r = select_request.from(:users) + r.total_entries = 1901902_i64 + r.current_page.should eq 1 + end + + it "returns 5 for current_page when offset is 100" do + r = select_request.from(:users).offset(100).limit(25) + r.total_entries = 1901902_i64 + r.current_page.should eq 5 + end + + it "returns 1 for total_pages when there's no limit" do + r = select_request.from(:users) + r.total_entries = 1901902_i64 + r.total_pages.should eq 1 + end + + it "returns 76077 total_pages when 25 per_page" do + r = select_request.from(:users).offset(100).limit(25) + r.total_entries = 1901902_i64 + r.total_pages.should eq 76077 + end + + it "returns 4 as previous_page when on page 5" do + r = select_request.from(:users).offset(100).limit(25) + r.total_entries = 1901902_i64 + r.current_page.should eq 5 + r.previous_page.should eq 4 + end + + it "returns nil for previous_page when on page 1" do + r = select_request.from(:users).offset(0).limit(25) + r.total_entries = 1901902_i64 + r.current_page.should eq 1 + r.previous_page.should eq nil + end + + it "returns 6 as next_page when on page 5" do + r = select_request.from(:users).offset(100).limit(25) + r.total_entries = 1901902_i64 + r.current_page.should eq 5 + r.next_page.should eq 6 + end + + it "returns nil for next_page when on page 76077" do + r = select_request.from(:users).offset(1901900).limit(25) + r.total_entries = 1901902_i64 + r.current_page.should eq 76077 + r.next_page.should eq nil + end + + it "returns true for out_of_bounds? when current_page is 76078" do + r = select_request.from(:users).offset(1901925).limit(25) + r.total_entries = 1901902_i64 + r.current_page.should eq 76078 + r.out_of_bounds?.should eq true + end + + it "returns false for out_of_bounds? when current_page is in normal range" do + r = select_request.from(:users).offset(925).limit(25) + r.total_entries = 1901902_i64 + r.out_of_bounds?.should eq false + end + end + end end end end diff --git a/src/clear/sql/query/aggregate.cr b/src/clear/sql/query/aggregate.cr index 6e9a2f3a4..d95ee5601 100644 --- a/src/clear/sql/query/aggregate.cr +++ b/src/clear/sql/query/aggregate.cr @@ -1,11 +1,16 @@ module Clear::SQL::Query::Aggregate # Use SQL `COUNT` over your query, and return this number as a Int64 def count(type : X.class = Int64) forall X - if (@offset || @limit) - X.new(Clear::SQL.select("COUNT(*)").from({query_count: self.clear_select.select("1")}).scalar(Int64)) - else - X.new(self.clear_select.select("COUNT(*)").scalar(Int64)) - end + # save the `select` column clause to ensure non-mutability of the query + columns = @columns.dup + o = if (@offset || @limit) + X.new(Clear::SQL.select("COUNT(*)").from({query_count: self.clear_select.select("1")}).scalar(Int64)) + else + X.new(self.clear_select.select("COUNT(*)").scalar(Int64)) + end + @columns = columns + + return o end # Call an custom aggregation function, like MEDIAN or other diff --git a/src/clear/sql/query/with_pagination.cr b/src/clear/sql/query/with_pagination.cr new file mode 100644 index 000000000..be85ac7fc --- /dev/null +++ b/src/clear/sql/query/with_pagination.cr @@ -0,0 +1,58 @@ +module Clear::SQL::Query::WithPagination + DEFAULT_LIMIT = 50 + DEFAULT_PAGE = 1 + + macro included + property total_entries : Int64? = nil + end + + # Maybe this goes on the Collection? + def paginate(page : Int32 = DEFAULT_PAGE, per_page : Int32 = DEFAULT_LIMIT) + # Need to clear these values to get total count first + clear_limit.clear_offset + @total_entries = count + + # Calculate proper offset and set limit + page = page < 1 ? 1 : page + @limit = per_page.to_i64 + @offset = (per_page * (page - 1)).to_i64 + change! + end + + def per_page + limit + end + + def current_page + if offset.nil? || limit.nil? + 1 + else + (offset.as(Int64) / limit.as(Int64)) + 1 + end + end + + def total_pages + if limit.nil? || total_entries.nil? + 1 + else + (total_entries.as(Int64) / limit.as(Int64).to_f).ceil.to_i + end + end + + # current_page - 1 or nil if there is no previous page + def previous_page + current_page > 1 ? (current_page - 1) : nil + end + + # current_page + 1 or nil if there is no next page + def next_page + current_page < total_pages ? (current_page + 1) : nil + end + + # Helper method that is true when someone tries to fetch a page with a + # larger number than the last page. Can be used in combination with flashes + # and redirecting. + def out_of_bounds? + current_page > total_pages + end +end diff --git a/src/clear/sql/select_builder.cr b/src/clear/sql/select_builder.cr index 647f8474b..ba9ed446e 100644 --- a/src/clear/sql/select_builder.cr +++ b/src/clear/sql/select_builder.cr @@ -29,6 +29,7 @@ module Clear::SQL::SelectBuilder include Query::Fetch include Query::BeforeQuery include Query::CTE + include Query::WithPagination include Query::Aggregate def dup : self