Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions app/models/edition.rb
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,10 @@ def web_url
Plek.website_root + base_path
end

def linked_editions
Queries::RecursiveLinkExpansion::RecursiveLinkExpansion.new(self).call
end

private

def renderable_content?
Expand Down
25 changes: 25 additions & 0 deletions app/queries/recursive_link_expansion/base_edition.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
module Queries
module RecursiveLinkExpansion
##
# Selects the columns required for recursive link expansion from the base edition (the root edition in the tree)
class BaseEdition
def initialize(edition_with_document, links = [])
@edition = edition_with_document
@links = links
end

def call
Edition.with_document.where(id: @edition.id).select(
"'0 base' as type",
"'{}'::text[] as path",
"documents.content_id",
"documents.locale",
"editions.id as edition_id",
"0 as position",
"editions.state",
ActiveRecord::Base.send(:sanitize_sql_array, ["?::jsonb as links", @links.to_json]),
)
end
end
end
end
24 changes: 24 additions & 0 deletions app/queries/recursive_link_expansion/forward_edition_links.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
module Queries
module RecursiveLinkExpansion
class ForwardEditionLinks
def call
# TODO: - parameterize locale / state
Edition
.from("lookahead")
.joins("INNER JOIN links ON (links.edition_id = lookahead.edition_id AND links.link_type = lookahead.type AND lookahead.reverse = false)")
.joins("INNER JOIN documents ON (documents.content_id = links.target_content_id AND documents.locale='en')")
.joins("INNER JOIN editions ON (editions.document_id = documents.id AND editions.state='published')")
.select(
"'1 forward edition link' as type",
"path || lookahead.content_id::text || lookahead.type AS path",
"documents.content_id",
"documents.locale",
"editions.id as edition_id",
"links.position",
"editions.state",
"lookahead.links",
)
end
end
end
end
24 changes: 24 additions & 0 deletions app/queries/recursive_link_expansion/forward_link_set_links.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
module Queries
module RecursiveLinkExpansion
class ForwardLinkSetLinks
def call
# TODO: - parameterize locale / state
Edition
.from("lookahead")
.joins("INNER JOIN links ON (links.link_set_content_id = lookahead.content_id AND links.link_type = lookahead.type AND lookahead.reverse = false)")
.joins("INNER JOIN documents ON (documents.content_id = links.target_content_id AND documents.locale='en')")
.joins("INNER JOIN editions ON (editions.document_id = documents.id AND editions.state='published')")
.select(
"'2 forward link set link' as type",
"path || lookahead.content_id::text || lookahead.type AS path",
"documents.content_id",
"documents.locale",
"editions.id as edition_id",
"links.position",
"editions.state",
"lookahead.links",
)
end
end
end
end
22 changes: 22 additions & 0 deletions app/queries/recursive_link_expansion/link_expansion_rules.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
module Queries
module RecursiveLinkExpansion
class LinkExpansionRules
RULES_BY_SCHEMA_NAME = {
ministers_index: [
{ "type": "ordered_cabinet_ministers", "reverse": false, "links": [{ "type": "person", "reverse": true, "links": [{ "type": "role", "reverse": false, "links": [] }] }] },
{ "type": "ordered_also_attends_cabinet", "reverse": false, "links": [{ "type": "person", "reverse": true, "links": [{ "type": "role", "reverse": false, "links": [] }] }] },
{ "type": "ordered_ministerial_departments", "reverse": false, "links": [{ "type": "ordered_ministers", "reverse": false, "links": [{ "type": "person", "reverse": true, "links": [{ "type": "role", "reverse": false, "links": [{ "type": "ordered_roles", "reverse": true, "links": [] }] }] }] }] },
{ "type": "ordered_assistant_whips", "reverse": false, "links": [{ "type": "person", "reverse": true, "links": [{ "type": "role", "reverse": false, "links": [] }] }] },
{ "type": "ordered_baronesses_and_lords_in_waiting_whips", "reverse": false, "links": [{ "type": "person", "reverse": true, "links": [{ "type": "role", "reverse": false, "links": [] }] }] },
{ "type": "ordered_house_lords_whips", "reverse": false, "links": [{ "type": "person", "reverse": true, "links": [{ "type": "role", "reverse": false, "links": [] }] }] },
{ "type": "ordered_house_of_commons_whips", "reverse": false, "links": [{ "type": "person", "reverse": true, "links": [{ "type": "role", "reverse": false, "links": [] }] }] },
{ "type": "ordered_junior_lords_of_the_treasury_whips", "reverse": false, "links": [{ "type": "person", "reverse": true, "links": [{ "type": "role", "reverse": false, "links": [] }] }] },
],
}.with_indifferent_access.freeze

def self.for(schema_name)
RULES_BY_SCHEMA_NAME.fetch(schema_name)
end
end
end
end
41 changes: 41 additions & 0 deletions app/queries/recursive_link_expansion/lookahead.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
module Queries
module RecursiveLinkExpansion
##
# Expects to be called with a CTE called "linked_editions" already defined.
# Defines a "lookahead" from a content id or an edition id and a link type.
#
# This allows the various types of link query (forward / reverse link set / edition links) to
# follow the link type from the edition / content id to the next edition / content_id.
#
# links is required in the constructor because we need to provide the maximum number of links that
# this CTE could return, as otherwise the postgresql planner will assume jsonb_to_recordset() will return 100 rows
# which pushes it towards inefficient plans.
class Lookahead
def initialize(links)
@links = links.map(&:deep_symbolize_keys)
end

def call
Arel.sql(<<~SQL,
SELECT content_id, edition_id, path, lookahead.type, lookahead.reverse, lookahead.links
FROM linked_editions
CROSS JOIN LATERAL (
SELECT * from jsonb_to_recordset(linked_editions.links) AS lookahead(type varchar, reverse boolean, links jsonb)
LIMIT #{max_links_count(@links)}
) AS lookahead
SQL
)
end

private

def max_links_count(links)
case links
in { links: Array => ls } then max_links_count(ls)
in Hash | [] then 0
in Array then [links.length, links.map { max_links_count(_1) }.max].max
end
end
end
end
end
35 changes: 35 additions & 0 deletions app/queries/recursive_link_expansion/recursive_link_expansion.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
module Queries
module RecursiveLinkExpansion
class RecursiveLinkExpansion
def initialize(edition)
@edition = edition
@links = ::Queries::RecursiveLinkExpansion::LinkExpansionRules.for(edition.schema_name)
end

def call
base_case = BaseEdition.new(@edition, @links).call

recursive_case = Edition.with(
lookahead: Lookahead.new(@links).call,
forward_link_set_links: ForwardLinkSetLinks.new.call,
forward_edition_links: ForwardEditionLinks.new.call,
reverse_link_set_links: ReverseLinkSetLinks.new.call,
reverse_edition_links: ReverseEditionLinks.new.call,
all_links: [
Arel.sql("SELECT * from forward_link_set_links"),
Arel.sql("SELECT * from forward_edition_links"),
Arel.sql("SELECT * from reverse_link_set_links"),
Arel.sql("SELECT * from reverse_edition_links"),
],
).from("all_links").select("all_links.*")

Edition.with_recursive(
linked_editions: [
base_case,
Arel.sql(recursive_case.to_sql), # Workaround to ensure the recursive case is wrapped in parens
],
).from("linked_editions").select("linked_editions.*")
end
end
end
end
24 changes: 24 additions & 0 deletions app/queries/recursive_link_expansion/reverse_edition_links.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
module Queries
module RecursiveLinkExpansion
class ReverseEditionLinks
def call
# TODO: - parameterize locale / state
Edition
.from("lookahead")
.joins("INNER JOIN links ON (links.target_content_id = lookahead.content_id AND links.link_type = lookahead.type AND lookahead.reverse = true)")
.joins("INNER JOIN editions ON (editions.id = links.edition_id AND editions.state='published')")
.joins("INNER JOIN documents ON (documents.id = editions.document_id AND documents.locale='en')")
.select(
"'1 forward edition link' as type",
"path || lookahead.content_id::text || lookahead.type AS path",
"documents.content_id",
"documents.locale",
"editions.id as edition_id",
"links.position",
"editions.state",
"lookahead.links",
)
end
end
end
end
24 changes: 24 additions & 0 deletions app/queries/recursive_link_expansion/reverse_link_set_links.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
module Queries
module RecursiveLinkExpansion
class ReverseLinkSetLinks
def call
# TODO: - parameterize locale / state
Edition
.from("lookahead")
.joins("INNER JOIN links ON links.target_content_id = lookahead.content_id and links.link_type = lookahead.type and lookahead.reverse = true")
.joins("INNER JOIN documents ON documents.content_id = links.link_set_content_id and documents.locale IN ('en')")
.joins("INNER JOIN editions ON editions.document_id = documents.id AND editions.state IN ('published')")
.select(
"'4 reverse link set link' as type",
"path || lookahead.content_id::text || lookahead.type as path",
"documents.content_id",
"documents.locale",
"editions.id as edition_id",
"links.position as position",
"editions.state as state",
"lookahead.links",
)
end
end
end
end