From fa030613d4e4e9af9dd500742310dfa45c1f4972 Mon Sep 17 00:00:00 2001 From: Jeremy Woertink Date: Mon, 18 Jun 2018 11:59:36 -0700 Subject: [PATCH 1/4] initial setup for pagination on sql builder --- src/clear/sql/query/with_pagination.cr | 43 ++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 src/clear/sql/query/with_pagination.cr diff --git a/src/clear/sql/query/with_pagination.cr b/src/clear/sql/query/with_pagination.cr new file mode 100644 index 000000000..68e362b8f --- /dev/null +++ b/src/clear/sql/query/with_pagination.cr @@ -0,0 +1,43 @@ +module Clear::SQL::Query::WithPagination + + macro included + property total_entries : Int64? = nil + 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 From 0b53f8493c600a015077d119ddedbd044a9e1591 Mon Sep 17 00:00:00 2001 From: Jeremy Woertink Date: Mon, 18 Jun 2018 11:59:52 -0700 Subject: [PATCH 2/4] include pagination on select builder and added specs --- spec/sql/select_spec.cr | 75 +++++++++++++++++++++++++++++++++ src/clear/sql/select_builder.cr | 1 + 2 files changed, 76 insertions(+) diff --git a/spec/sql/select_spec.cr b/spec/sql/select_spec.cr index ee144e72c..8fc4e8a67 100644 --- a/spec/sql/select_spec.cr +++ b/spec/sql/select_spec.cr @@ -261,6 +261,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/select_builder.cr b/src/clear/sql/select_builder.cr index 4e09a4f8d..775b8292f 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 def dup : self self.class.new(columns: @columns.dup, From 1f7c562c468e0c9ba98864397bea68ff2d0a05e3 Mon Sep 17 00:00:00 2001 From: Jeremy Woertink Date: Wed, 20 Jun 2018 13:44:23 -0700 Subject: [PATCH 3/4] adding in paginate method for proper limit and offset calculation --- spec/model/model_spec.cr | 31 ++++++++++++++++++++++++++ src/clear/sql/query/with_pagination.cr | 17 ++++++++++++++ 2 files changed, 48 insertions(+) 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/src/clear/sql/query/with_pagination.cr b/src/clear/sql/query/with_pagination.cr index 68e362b8f..5566b055c 100644 --- a/src/clear/sql/query/with_pagination.cr +++ b/src/clear/sql/query/with_pagination.cr @@ -1,9 +1,26 @@ 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 + # TODO: this fails. Maybe something with clear_select? + # @total_entries = count + @total_entries = Clear::SQL.connection.scalar("SELECT COUNT(*) #{print_froms} #{print_wheres}").as(Int64) + + # 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 From d718ef52384e71174fb194801a7658dce20cac4d Mon Sep 17 00:00:00 2001 From: Jeremy Woertink Date: Sat, 23 Jun 2018 08:18:19 -0700 Subject: [PATCH 4/4] apply patch to fix mutation with count --- src/clear/sql/query/aggregate.cr | 15 ++++++++++----- src/clear/sql/query/with_pagination.cr | 4 +--- 2 files changed, 11 insertions(+), 8 deletions(-) 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 index 5566b055c..be85ac7fc 100644 --- a/src/clear/sql/query/with_pagination.cr +++ b/src/clear/sql/query/with_pagination.cr @@ -10,9 +10,7 @@ module Clear::SQL::Query::WithPagination 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 - # TODO: this fails. Maybe something with clear_select? - # @total_entries = count - @total_entries = Clear::SQL.connection.scalar("SELECT COUNT(*) #{print_froms} #{print_wheres}").as(Int64) + @total_entries = count # Calculate proper offset and set limit page = page < 1 ? 1 : page