-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
DEV-1096 Rights API: translate OFFSET queries into inequalities #13
Draft
moseshll
wants to merge
18
commits into
main
Choose a base branch
from
DEV-1096_offset_optimizer
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
16caa04
DEV-1096 Rights API: translate OFFSET queries into inequalities
moseshll 447a612
Use lru_redux cache instead of homebrew
moseshll 5962ff7
Add QueryOptimizer#add_to_cache spec
moseshll 328f8b6
Fix ''undefined method 'parse' for CGI:Class'' error
moseshll bfbb08a
Include cached true/false in output JSON
moseshll af2a36f
Add 'cached' to list of required result fields
moseshll 2115329
Remove unneeded 'require pry'
moseshll 279295c
Add build workflow
moseshll 95d5c0e
Remove Build workflow to be added in a separate branch.
moseshll 0f26c92
Cursor implementation; allow order= parameters
moseshll 7561192
Get rid of deprecated version tag
moseshll abd73c4
Restore total test coverage to 100%
moseshll dc738b3
Revert docker-compose.yml for GH issue #15 -- rein in scope creep
moseshll e0665ba
Undo scope creep: un-alphabetize gem list
moseshll 843542f
Moar comments
moseshll fa1d688
Moar tests
moseshll 10684bf
Back unneeded changes to services.rb
moseshll eab1c00
Back out scope creep on database spec
moseshll File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
# frozen_string_literal: true | ||
|
||
require "base64" | ||
require "json" | ||
require "sequel" | ||
|
||
# Given a list of sort fields -- Array of symbols | ||
# and a list of field -> value mappings (the last result) | ||
# create a string that can be decoded into | ||
# a WHERE list | ||
|
||
# Tries to store just enough information so the next query can | ||
# pick up where the current one left off. The Base64-encoded cursor string | ||
# is a serialized array containing the current offset and one value for each | ||
# sort parameter (explicit or default) used in the query. | ||
|
||
# CURSOR LIFECYCLE | ||
# - At the beginning of the query a Cursor is created with the "cursor" URL parameter, or | ||
# nil if none was supplied (indicating first page of results, i.e. no previous query). | ||
# - Query calls `cursor.where` to create a WHERE clause based on the decoded values | ||
# from the previous result (in effect saying "WHERE fields > last_result") | ||
# - Query calls `cursor.offset` for the current page of results. | ||
# - Query calls `cursor.encode` to calculate a new offset and new last_result values | ||
# dictated by the current ORDER BY. | ||
|
||
# IVAR SEMANTICS | ||
# - `offset` the (zero-based) offset into the overall results set produced by `where`, or perhaps | ||
# "give me the results at offset N (by using values X Y and Z)" | ||
# - `values` the X Y and Z from above, these are the relevant values from the previous result | ||
# if there was one. | ||
# It is possibly counterintuitive that X Y and Z are NOT at offset N. Offset N is the location | ||
# of the NEXT record. | ||
|
||
# CAVEATS | ||
# Relies on the search parameters being unchanged between queries, | ||
# if they do change then the results are undefined. | ||
module RightsAPI | ||
class Cursor | ||
OFFSET_KEY = "off" | ||
LAST_ROW_KEY = "last" | ||
attr_reader :values, :offset | ||
|
||
# @param cursor_string [String] the URL parameter to decode | ||
# @return [Array] of the form [offset, "val1", "val2" ...] | ||
def self.decode(cursor_string) | ||
JSON.parse(Base64.urlsafe_decode64(cursor_string)) | ||
end | ||
|
||
# JSON-encode and Base64-encode an object | ||
# @param arg [Object] a serializable object (always an Array in this class) | ||
# @return [String] | ||
def self.encode(arg) | ||
Base64.urlsafe_encode64(JSON.generate(arg)) | ||
end | ||
|
||
def initialize(cursor_string: nil) | ||
@offset = 0 | ||
@values = [] | ||
if cursor_string | ||
@values = self.class.decode cursor_string | ||
@offset = @values.shift | ||
end | ||
end | ||
|
||
# Generate zero or one WHERE clauses that will generate a pseudo-OFFSET | ||
# based on ORDER BY parameters. | ||
# ORDER BY a, b, c TRANSLATES TO | ||
# WHERE (a > 1) | ||
# OR (a = 1 AND b > 2) | ||
# OR (a = 1 AND b = 2 AND c > 3) | ||
# @param model [Class] Sequel::Model subclass for the table being queried, | ||
# only used for qualifying column names in the WHERE clause. | ||
# @param order [Array<RightsAPI::Order>] the current query's ORDER BY | ||
# @return [Array<Sequel::LiteralString>] zero or one Sequel literals | ||
def where(model:, order:) | ||
return [] if values.empty? | ||
|
||
# Create one OR clause for each ORDER. | ||
# Each OR clause is a series of AND clauses. | ||
# The last element of each AND clause is < or >, the others are = | ||
# The first AND clause has only the first ORDER parameter. | ||
# Each subsequent one adds one ORDER parameter. | ||
or_clause = [] | ||
order.count.times do |order_index| | ||
# Take a slice of ORDER of size order_index + 1 | ||
and_clause = order[0, order_index + 1].each_with_index.map do |ord, i| | ||
# in which each element is a "col op val" string and the last is an inequality | ||
op = if i == order_index | ||
ord.asc? ? ">" : "<" | ||
else | ||
"=" | ||
end | ||
"#{model.table_name}.#{ord.column}#{op}'#{values[i]}'" | ||
end | ||
or_clause << "(" + and_clause.join(" AND ") + ")" | ||
end | ||
[Sequel.lit(or_clause.join(" OR "))] | ||
end | ||
|
||
# Encode the offset and the relevant values from the last result row | ||
# (i.e. those used in the current ORDER BY) | ||
# @param order [Array<RightsAPI::Order>] the current query's ORDER BY | ||
# @param rows [Sequel::Dataset] the result of the current query | ||
# @return [String] | ||
def encode(order:, rows:) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I get sweaty seeing |
||
data = [offset + rows.count] | ||
row = rows.last | ||
order.each do |ord| | ||
data << row[ord.column] | ||
end | ||
self.class.encode data | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
# frozen_string_literal: true | ||
|
||
# A class that encapsulates the field and ASC/DESC properties of a | ||
# single ORDER BY argument. | ||
|
||
module RightsAPI | ||
class Order | ||
attr_reader :column | ||
# @param column [Symbol] the field to ORDER BY | ||
# @param asc [Boolean] true if ASC, false if DESC | ||
def initialize(column:, asc: true) | ||
@column = column | ||
@asc = asc | ||
end | ||
|
||
# @return [Boolean] is the order direction ASC? | ||
def asc? | ||
@asc | ||
end | ||
|
||
# @return [Sequel::SQL::OrderedExpression] | ||
def to_sequel(model:) | ||
if asc? | ||
Sequel.asc(model.qualify(field: column)) | ||
else | ||
Sequel.desc(model.qualify(field: column)) | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would like to see this, in more detail, up top in the documentation.
Because this is the meat and potatoes of this class, no?
With an actual 5-ish line example using the code...