diff --git a/app/models/card_card_subtype.rb b/app/models/card_card_subtype.rb index 5b4ea170..6d97a373 100644 --- a/app/models/card_card_subtype.rb +++ b/app/models/card_card_subtype.rb @@ -9,4 +9,7 @@ class CardCardSubtype < ApplicationRecord belongs_to :card_subtype, :primary_key => :id, :foreign_key => :card_subtype_id -end + belongs_to :unified_card, + :primary_key => :id, + :foreign_key => :card_id + end diff --git a/app/models/card_cycle.rb b/app/models/card_cycle.rb index 823d5b47..ccc87e7a 100644 --- a/app/models/card_cycle.rb +++ b/app/models/card_cycle.rb @@ -4,6 +4,7 @@ class CardCycle < ApplicationRecord has_many :card_sets has_many :printings, :through => :card_sets has_many :cards, :through => :printings + has_many :unified_cards, :through => :printings has_many :card_pool_card_cycles has_many :card_pools, :through => :card_pool_card_cycles end diff --git a/app/models/card_pool.rb b/app/models/card_pool.rb index 519c0557..3bceed02 100644 --- a/app/models/card_pool.rb +++ b/app/models/card_pool.rb @@ -7,6 +7,7 @@ class CardPool < ApplicationRecord has_many :card_sets, :through => :card_pool_card_sets has_many :card_pool_cards has_many :cards, :through => :card_pool_cards + has_many :unified_cards, :through => :card_pool_cards, primary_key: :card_id, foreign_key: :id has_many :snapshots belongs_to :format, diff --git a/app/models/card_pool_card.rb b/app/models/card_pool_card.rb index 523f0620..e54a75cd 100644 --- a/app/models/card_pool_card.rb +++ b/app/models/card_pool_card.rb @@ -9,4 +9,7 @@ class CardPoolCard < ApplicationRecord belongs_to :card_pool, :primary_key => :id, :foreign_key => :card_pool_id -end + belongs_to :unified_card, + :primary_key => :id, + :foreign_key => :card_id + end diff --git a/app/models/card_set.rb b/app/models/card_set.rb index 810da419..aaae612b 100644 --- a/app/models/card_set.rb +++ b/app/models/card_set.rb @@ -6,4 +6,5 @@ class CardSet < ApplicationRecord has_many :printings has_many :cards, :through => :printings + has_many :unified_cards, :through => :printings end diff --git a/app/models/card_subtype.rb b/app/models/card_subtype.rb index c5ec96af..d68b5c28 100644 --- a/app/models/card_subtype.rb +++ b/app/models/card_subtype.rb @@ -4,4 +4,5 @@ class CardSubtype < ApplicationRecord has_many :card_card_subtypes has_many :cards, :through => :card_card_subtypes has_many :printings, :through => :cards + has_many :unified_cards, :through => :card_card_subtypes, primary_key: :card_id, foreign_key: :id end diff --git a/app/models/card_type.rb b/app/models/card_type.rb index f1287ff9..1fb055d7 100644 --- a/app/models/card_type.rb +++ b/app/models/card_type.rb @@ -3,4 +3,5 @@ class CardType < ApplicationRecord belongs_to :side has_many :cards + has_many :unified_cards end diff --git a/app/models/faction.rb b/app/models/faction.rb index 5f63d3ff..51dfe616 100644 --- a/app/models/faction.rb +++ b/app/models/faction.rb @@ -4,4 +4,5 @@ class Faction < ApplicationRecord belongs_to :side has_many :cards has_many :printings, :through => :cards + has_many :unified_cards end diff --git a/app/models/printing.rb b/app/models/printing.rb index 895d5e50..f3b37cc5 100644 --- a/app/models/printing.rb +++ b/app/models/printing.rb @@ -3,6 +3,7 @@ class Printing < ApplicationRecord belongs_to :card belongs_to :card_set + belongs_to :unified_card, primary_key: :id, foreign_key: :card_id has_one :faction, :through => :card has_one :card_cycle, :through => :card_set has_one :side, :through => :card diff --git a/app/models/side.rb b/app/models/side.rb index e3490c12..248b5398 100644 --- a/app/models/side.rb +++ b/app/models/side.rb @@ -4,5 +4,6 @@ class Side < ApplicationRecord has_many :factions has_many :card_types has_many :cards + has_many :unified_cards has_many :printings, :through => :cards end diff --git a/app/models/snapshot.rb b/app/models/snapshot.rb index 361d412a..f1e90b85 100644 --- a/app/models/snapshot.rb +++ b/app/models/snapshot.rb @@ -5,4 +5,5 @@ class Snapshot < ApplicationRecord belongs_to :card_pool belongs_to :restriction, optional: :true has_many :cards, through: :card_pool + has_many :unified_cards, through: :card_pool, primary_key: :card_id, foreign_key: :id end diff --git a/app/models/unified_card.rb b/app/models/unified_card.rb new file mode 100644 index 00000000..d931140c --- /dev/null +++ b/app/models/unified_card.rb @@ -0,0 +1,3 @@ +class UnifiedCard < ApplicationRecord + self.primary_key = :id +end diff --git a/app/resources/api/v3/public/card_cycle_resource.rb b/app/resources/api/v3/public/card_cycle_resource.rb index f00aa2fe..6b695705 100644 --- a/app/resources/api/v3/public/card_cycle_resource.rb +++ b/app/resources/api/v3/public/card_cycle_resource.rb @@ -9,7 +9,8 @@ class Api::V3::Public::CardCycleResource < JSONAPI::Resource has_many :card_sets has_many :printings - has_many :cards + + has_many :cards, relation_name: :unified_cards paginator :none end diff --git a/app/resources/api/v3/public/card_pool_resource.rb b/app/resources/api/v3/public/card_pool_resource.rb index dcf10c1a..28d61c62 100644 --- a/app/resources/api/v3/public/card_pool_resource.rb +++ b/app/resources/api/v3/public/card_pool_resource.rb @@ -14,7 +14,7 @@ class Api::V3::Public::CardPoolResource < JSONAPI::Resource has_one :format has_many :card_cycles has_many :card_sets - has_many :cards + has_many :cards, relation_name: :unified_cards has_many :snapshots def num_cards diff --git a/app/resources/api/v3/public/card_resource.rb b/app/resources/api/v3/public/card_resource.rb index 2a5dcdc2..8cb5e689 100644 --- a/app/resources/api/v3/public/card_resource.rb +++ b/app/resources/api/v3/public/card_resource.rb @@ -4,12 +4,14 @@ module Public class Api::V3::Public::CardResource < JSONAPI::Resource immutable + model_name 'UnifiedCard' + attributes :stripped_title, :title, :card_type_id, :side_id, :faction_id attributes :advancement_requirement, :agenda_points, :base_link, :cost - attributes :deck_limit, :influence_cost, :influence_limit, :memory_cost - attributes :minimum_deck_size, :strength, :stripped_text, :text, :trash_cost - attributes :is_unique, :display_subtypes, :updated_at - attributes :card_abilities, :latest_printing_id + attributes :deck_limit, :in_restriction, :influence_cost, :influence_limit, :memory_cost + attributes :minimum_deck_size, :latest_printing_id, :num_printings, :printing_ids, :restriction_ids, :restrictions, :strength, :stripped_text, :text, :trash_cost + attributes :is_unique, :card_subtype_ids, :display_subtypes, :updated_at + attributes :card_abilities, :format_ids, :card_pool_ids, :snapshot_ids key_type :string @@ -20,7 +22,7 @@ class Api::V3::Public::CardResource < JSONAPI::Resource has_many :printings def latest_printing_id - @model.printings.max_by { |p| p.date_release } ['id'] + @model.printing_ids[0] end def card_abilities @@ -39,6 +41,25 @@ def card_abilities } end + def packed_restriction_to_map(packed) + m = {} + packed.each do |p| + x = p.split('=') + m[x[0]] = x[1].to_i + end + return m + end + + def restrictions + { + banned: @model.restrictions_banned, + global_penalty: @model.restrictions_global_penalty, + points: packed_restriction_to_map(@model.restrictions_points), + restricted: @model.restrictions_restricted, + universal_faction_cost: packed_restriction_to_map(@model.restrictions_universal_faction_cost) + } + end + filters :title, :card_type_id, :side_id, :faction_id, :advancement_requirement filters :agenda_points, :base_link, :cost, :deck_limit, :influence_cost filters :influence_limit, :memory_cost, :minimum_deck_size, :strength, :trash_cost, :is_unique diff --git a/app/resources/api/v3/public/card_set_resource.rb b/app/resources/api/v3/public/card_set_resource.rb index d89b3096..a0a6aa77 100644 --- a/app/resources/api/v3/public/card_set_resource.rb +++ b/app/resources/api/v3/public/card_set_resource.rb @@ -12,7 +12,7 @@ class Api::V3::Public::CardSetResource < JSONAPI::Resource has_one :card_cycle has_one :card_set_type has_many :printings - has_many :cards + has_many :cards, relation_name: :unified_cards filters :card_cycle_id, :card_set_type_id end diff --git a/app/resources/api/v3/public/card_subtype_resource.rb b/app/resources/api/v3/public/card_subtype_resource.rb index 29fbc6d8..fd7a3ae7 100644 --- a/app/resources/api/v3/public/card_subtype_resource.rb +++ b/app/resources/api/v3/public/card_subtype_resource.rb @@ -9,7 +9,7 @@ class Api::V3::Public::CardSubtypeResource < JSONAPI::Resource paginator :none - has_many :cards + has_many :cards, relation_name: :unified_cards has_many :printings end end diff --git a/app/resources/api/v3/public/card_type_resource.rb b/app/resources/api/v3/public/card_type_resource.rb index 5040f573..34fb3531 100644 --- a/app/resources/api/v3/public/card_type_resource.rb +++ b/app/resources/api/v3/public/card_type_resource.rb @@ -8,7 +8,7 @@ class Api::V3::Public::CardTypeResource < JSONAPI::Resource key_type :string has_one :side - has_many :cards + has_many :cards, relation_name: :unified_cards paginator :none filter :side_id diff --git a/app/resources/api/v3/public/faction_resource.rb b/app/resources/api/v3/public/faction_resource.rb index 3f89eca3..1195343b 100644 --- a/app/resources/api/v3/public/faction_resource.rb +++ b/app/resources/api/v3/public/faction_resource.rb @@ -10,7 +10,7 @@ class Api::V3::Public::FactionResource < JSONAPI::Resource paginator :none has_one :side - has_many :cards + has_many :cards, relation_name: :unified_cards has_many :printings filters :side_id, :is_mini diff --git a/app/resources/api/v3/public/side_resource.rb b/app/resources/api/v3/public/side_resource.rb index ca13cf39..023979a4 100644 --- a/app/resources/api/v3/public/side_resource.rb +++ b/app/resources/api/v3/public/side_resource.rb @@ -11,7 +11,7 @@ class Api::V3::Public::SideResource < JSONAPI::Resource has_many :factions has_many :card_types - has_many :cards + has_many :cards, relation_name: :unified_cards has_many :printings end end diff --git a/app/resources/api/v3/public/snapshot_resource.rb b/app/resources/api/v3/public/snapshot_resource.rb index 07ff2e99..e896b673 100644 --- a/app/resources/api/v3/public/snapshot_resource.rb +++ b/app/resources/api/v3/public/snapshot_resource.rb @@ -15,7 +15,7 @@ class Api::V3::Public::SnapshotResource < JSONAPI::Resource has_one :card_pool has_one :restriction - has_many :cards + has_many :cards, relation_name: :unified_cards filters :active, :format_id diff --git a/db/migrate/20220926010740_create_unified_cards.rb b/db/migrate/20220926010740_create_unified_cards.rb new file mode 100644 index 00000000..60e203fb --- /dev/null +++ b/db/migrate/20220926010740_create_unified_cards.rb @@ -0,0 +1,5 @@ +class CreateUnifiedCards < ActiveRecord::Migration[7.0] + def change + create_view :unified_cards, materialized: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 2ce4b2f9..d84b1ff1 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2022_09_25_225117) do +ActiveRecord::Schema[7.0].define(version: 2022_09_26_010740) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -320,4 +320,160 @@ add_index "unified_restrictions", ["restriction_id"], name: "index_unified_restrictions_on_restriction_id" add_index "unified_restrictions", ["snapshot_id"], name: "index_unified_restrictions_on_snapshot_id" + create_view "unified_cards", materialized: true, sql_definition: <<-SQL + WITH card_cycles_summary AS ( + SELECT c_1.id, + array_agg(cc.id ORDER BY cc.id) AS card_cycle_ids, + array_agg(lower(cc.name) ORDER BY (lower(cc.name))) AS card_cycle_names + FROM (((cards c_1 + JOIN printings p_1 ON (((c_1.id)::text = p_1.card_id))) + JOIN card_sets cs ON ((p_1.card_set_id = (cs.id)::text))) + JOIN card_cycles cc ON (((cc.id)::text = cs.card_cycle_id))) + GROUP BY c_1.id + ), card_sets_summary AS ( + SELECT c_1.id, + array_agg(cs.id ORDER BY cs.id) AS card_set_ids, + array_agg(lower(cs.name) ORDER BY (lower(cs.name))) AS card_set_names + FROM ((cards c_1 + JOIN printings p_1 ON (((c_1.id)::text = p_1.card_id))) + JOIN card_sets cs ON ((p_1.card_set_id = (cs.id)::text))) + GROUP BY c_1.id + ), card_subtype_ids AS ( + SELECT cards_card_subtypes.card_id, + array_agg(cards_card_subtypes.card_subtype_id ORDER BY 1::integer) AS card_subtype_ids + FROM cards_card_subtypes + GROUP BY cards_card_subtypes.card_id + ), card_subtype_names AS ( + SELECT ccs_1.card_id, + array_agg(lower(cs.name) ORDER BY (lower(cs.name))) AS lower_card_subtype_names, + array_agg(cs.name ORDER BY cs.name) AS card_subtype_names + FROM (cards_card_subtypes ccs_1 + JOIN card_subtypes cs ON ((ccs_1.card_subtype_id = (cs.id)::text))) + GROUP BY ccs_1.card_id + ), card_printing_ids AS ( + SELECT printings.card_id, + array_agg(printings.id ORDER BY printings.date_release DESC) AS printing_ids + FROM printings + GROUP BY printings.card_id + ), card_restriction_ids AS ( + SELECT unified_restrictions.card_id, + array_agg(unified_restrictions.restriction_id ORDER BY unified_restrictions.restriction_id) AS restriction_ids + FROM unified_restrictions + WHERE unified_restrictions.in_restriction + GROUP BY unified_restrictions.card_id + ), restrictions_banned_summary AS ( + SELECT restrictions_cards_banned.card_id, + array_agg(restrictions_cards_banned.restriction_id ORDER BY restrictions_cards_banned.restriction_id) AS restrictions_banned + FROM restrictions_cards_banned + GROUP BY restrictions_cards_banned.card_id + ), restrictions_global_penalty_summary AS ( + SELECT restrictions_cards_global_penalty.card_id, + array_agg(restrictions_cards_global_penalty.restriction_id ORDER BY restrictions_cards_global_penalty.restriction_id) AS restrictions_global_penalty + FROM restrictions_cards_global_penalty + GROUP BY restrictions_cards_global_penalty.card_id + ), restrictions_points_summary AS ( + SELECT restrictions_cards_points.card_id, + array_agg(concat(restrictions_cards_points.restriction_id, '=', (restrictions_cards_points.value)::text) ORDER BY (concat(restrictions_cards_points.restriction_id, '=', (restrictions_cards_points.value)::text))) AS restrictions_points + FROM restrictions_cards_points + GROUP BY restrictions_cards_points.card_id + ), restrictions_restricted_summary AS ( + SELECT restrictions_cards_restricted.card_id, + array_agg(restrictions_cards_restricted.restriction_id ORDER BY restrictions_cards_restricted.restriction_id) AS restrictions_restricted + FROM restrictions_cards_restricted + GROUP BY restrictions_cards_restricted.card_id + ), restrictions_universal_faction_cost_summary AS ( + SELECT restrictions_cards_universal_faction_cost.card_id, + array_agg(concat(restrictions_cards_universal_faction_cost.restriction_id, '=', (restrictions_cards_universal_faction_cost.value)::text) ORDER BY (concat(restrictions_cards_universal_faction_cost.restriction_id, '=', (restrictions_cards_universal_faction_cost.value)::text))) AS restrictions_universal_faction_cost + FROM restrictions_cards_universal_faction_cost + GROUP BY restrictions_cards_universal_faction_cost.card_id + ), format_ids AS ( + SELECT cpc_1.card_id, + array_agg(DISTINCT s_1.format_id ORDER BY s_1.format_id) AS format_ids + FROM (card_pools_cards cpc_1 + JOIN snapshots s_1 ON ((cpc_1.card_pool_id = s_1.card_pool_id))) + GROUP BY cpc_1.card_id + ), card_pool_ids AS ( + SELECT cpc_1.card_id, + array_agg(DISTINCT s_1.card_pool_id ORDER BY s_1.card_pool_id) AS card_pool_ids + FROM (card_pools_cards cpc_1 + JOIN snapshots s_1 ON ((cpc_1.card_pool_id = s_1.card_pool_id))) + GROUP BY cpc_1.card_id + ), snapshot_ids AS ( + SELECT cpc_1.card_id, + array_agg(DISTINCT s_1.id ORDER BY s_1.id) AS snapshot_ids + FROM (card_pools_cards cpc_1 + JOIN snapshots s_1 ON ((cpc_1.card_pool_id = s_1.card_pool_id))) + GROUP BY cpc_1.card_id + ) + SELECT c.id, + c.title, + c.stripped_title, + c.card_type_id, + c.side_id, + c.faction_id, + c.advancement_requirement, + c.agenda_points, + c.base_link, + c.cost, + c.deck_limit, + c.influence_cost, + c.influence_limit, + c.memory_cost, + c.minimum_deck_size, + c.strength, + c.stripped_text, + c.text, + c.trash_cost, + c.is_unique, + c.display_subtypes, + c.created_at, + c.updated_at, + c.additional_cost, + c.advanceable, + c.gains_subroutines, + c.interrupt, + c.link_provided, + c.mu_provided, + c.num_printed_subroutines, + c.on_encounter_effect, + c.performs_trace, + c.recurring_credits_provided, + c.rez_effect, + c.trash_ability, + COALESCE(csi.card_subtype_ids, ARRAY[]::text[]) AS card_subtype_ids, + COALESCE(csn.lower_card_subtype_names, ARRAY[]::text[]) AS lower_card_subtype_names, + COALESCE(csn.card_subtype_names, ARRAY[]::text[]) AS card_subtype_names, + p.printing_ids, + array_length(p.printing_ids, 1) AS num_printings, + ccs.card_cycle_ids, + ccs.card_cycle_names, + css.card_set_ids, + css.card_set_names, + COALESCE(r.restriction_ids, (ARRAY[]::text[])::character varying[]) AS restriction_ids, + (r.restriction_ids IS NOT NULL) AS in_restriction, + COALESCE(r_b.restrictions_banned, ARRAY[]::text[]) AS restrictions_banned, + COALESCE(r_g_p.restrictions_global_penalty, ARRAY[]::text[]) AS restrictions_global_penalty, + COALESCE(r_p.restrictions_points, ARRAY[]::text[]) AS restrictions_points, + COALESCE(r_r.restrictions_restricted, ARRAY[]::text[]) AS restrictions_restricted, + COALESCE(r_u_f_c.restrictions_universal_faction_cost, ARRAY[]::text[]) AS restrictions_universal_faction_cost, + COALESCE(f.format_ids, ARRAY[]::text[]) AS format_ids, + COALESCE(cpc.card_pool_ids, ARRAY[]::text[]) AS card_pool_ids, + COALESCE(s.snapshot_ids, (ARRAY[]::text[])::character varying[]) AS snapshot_ids + FROM ((((((((((((((cards c + JOIN card_printing_ids p ON (((c.id)::text = p.card_id))) + JOIN card_cycles_summary ccs ON (((c.id)::text = (ccs.id)::text))) + JOIN card_sets_summary css ON (((c.id)::text = (css.id)::text))) + LEFT JOIN card_subtype_ids csi ON (((c.id)::text = csi.card_id))) + LEFT JOIN card_subtype_names csn ON (((c.id)::text = csn.card_id))) + LEFT JOIN card_restriction_ids r ON (((c.id)::text = (r.card_id)::text))) + LEFT JOIN restrictions_banned_summary r_b ON (((c.id)::text = r_b.card_id))) + LEFT JOIN restrictions_global_penalty_summary r_g_p ON (((c.id)::text = r_g_p.card_id))) + LEFT JOIN restrictions_points_summary r_p ON (((c.id)::text = r_p.card_id))) + LEFT JOIN restrictions_restricted_summary r_r ON (((c.id)::text = r_r.card_id))) + LEFT JOIN restrictions_universal_faction_cost_summary r_u_f_c ON (((c.id)::text = r_u_f_c.card_id))) + LEFT JOIN format_ids f ON (((c.id)::text = f.card_id))) + LEFT JOIN card_pool_ids cpc ON (((c.id)::text = cpc.card_id))) + LEFT JOIN snapshot_ids s ON (((c.id)::text = s.card_id))) + GROUP BY c.id, c.title, c.stripped_title, c.card_type_id, c.side_id, c.faction_id, c.advancement_requirement, c.agenda_points, c.base_link, c.cost, c.deck_limit, c.influence_cost, c.influence_limit, c.memory_cost, c.minimum_deck_size, c.strength, c.stripped_text, c.text, c.trash_cost, c.is_unique, c.display_subtypes, c.created_at, c.updated_at, c.additional_cost, c.advanceable, c.gains_subroutines, c.interrupt, c.link_provided, c.mu_provided, c.num_printed_subroutines, c.on_encounter_effect, c.performs_trace, c.recurring_credits_provided, c.rez_effect, c.trash_ability, csi.card_subtype_ids, csn.lower_card_subtype_names, csn.card_subtype_names, p.printing_ids, ccs.card_cycle_ids, ccs.card_cycle_names, css.card_set_ids, css.card_set_names, r.restriction_ids, r_b.restrictions_banned, r_g_p.restrictions_global_penalty, r_p.restrictions_points, r_r.restrictions_restricted, r_u_f_c.restrictions_universal_faction_cost, f.format_ids, cpc.card_pool_ids, s.snapshot_ids; + SQL end diff --git a/db/views/denormalized_cards.sql b/db/views/denormalized_cards.sql new file mode 100644 index 00000000..f58c28b3 --- /dev/null +++ b/db/views/denormalized_cards.sql @@ -0,0 +1,280 @@ +DROP MATERIALIZED VIEW IF EXISTS unified_cards; + +CREATE MATERIALIZED VIEW unified_cards AS WITH card_subtype_ids AS ( + SELECT + card_id, + ARRAY_AGG( + card_subtype_id + ORDER BY + 1 + ) as card_subtype_ids + FROM + cards_card_subtypes + GROUP BY + card_id +), +card_subtype_names AS ( + SELECT + ccs.card_id, + ARRAY_AGG( + cs.name + ORDER BY + cs.name + ) as card_subtype_names + FROM + cards_card_subtypes ccs + JOIN card_subtypes cs ON ccs.card_subtype_id = cs.id + GROUP BY + ccs.card_id +), +card_printing_ids AS ( + SELECT + card_id, + ARRAY_AGG( + id + ORDER BY + id DESC + ) as printing_ids + FROM + printings + GROUP BY + card_id +), +card_restriction_ids AS ( + SELECT + card_id, + ARRAY_AGG( + restriction_id + ORDER BY + 1 + ) as restriction_ids + FROM + unified_restrictions + WHERE + in_restriction + GROUP BY + 1 +), +restrictions_banned_summary AS ( + SELECT + card_id, + ARRAY_AGG( + restriction_id + ORDER BY + 1 + ) as restrictions_banned + FROM + restrictions_cards_banned + GROUP BY + card_id +), +restrictions_global_penalty_summary AS ( + SELECT + card_id, + ARRAY_AGG( + restriction_id + ORDER BY + 1 + ) as restrictions_global_penalty + FROM + restrictions_cards_global_penalty + GROUP BY + card_id +), +restrictions_points_summary AS ( + SELECT + card_id, + ARRAY_AGG( + ARRAY [restriction_id, CAST (value AS text)] + ORDER BY + restriction_id + ) as restrictions_points + FROM + restrictions_cards_points + GROUP BY + card_id +), +restrictions_restricted_summary AS ( + SELECT + card_id, + ARRAY_AGG( + restriction_id + ORDER BY + 1 + ) as restrictions_restricted + FROM + restrictions_cards_restricted + GROUP BY + card_id +), +restrictions_universal_faction_cost_summary AS ( + SELECT + card_id, + ARRAY_AGG( + ARRAY [restriction_id, CAST (value AS text)] + ORDER BY + restriction_id + ) as restrictions_universal_faction_cost + FROM + restrictions_cards_universal_faction_cost + GROUP BY + card_id +), +format_ids AS ( + SELECT + cpc.card_id, + ARRAY_AGG( + DISTINCT s.format_id + ORDER BY + s.format_id + ) as format_ids + FROM + card_pools_cards cpc + JOIN snapshots s ON cpc.card_pool_id = s.card_pool_id + GROUP BY + cpc.card_id +), +card_pool_ids AS ( + SELECT + cpc.card_id, + ARRAY_AGG( + DISTINCT s.card_pool_id + ORDER BY + s.card_pool_id + ) as card_pool_ids + FROM + card_pools_cards cpc + JOIN snapshots s ON cpc.card_pool_id = s.card_pool_id + GROUP BY + cpc.card_id +), +snapshot_ids AS ( + SELECT + cpc.card_id, + ARRAY_AGG( + DISTINCT s.id + ORDER BY + s.id + ) as snapshot_ids + FROM + card_pools_cards cpc + JOIN snapshots s ON cpc.card_pool_id = s.card_pool_id + GROUP BY + cpc.card_id +) +SELECT + c.id as card_id, + c.title, + c.stripped_title, + c.card_type_id, + c.side_id, + c.faction_id, + c.advancement_requirement, + c.agenda_points, + c.base_link, + c.cost, + c.deck_limit, + c.influence_cost, + c.influence_limit, + c.memory_cost, + c.minimum_deck_size, + c.strength, + c.stripped_text, + c.text, + c.trash_cost, + c.is_unique, + c.display_subtypes, + c.created_at, + c.updated_at, + c.additional_cost, + c.advanceable, + c.gains_subroutines, + c.interrupt, + c.link_provided, + c.mu_provided, + c.num_printed_subroutines, + c.on_encounter_effect, + c.performs_trace, + c.recurring_credits_provided, + c.rez_effect, + c.trash_ability, + COALESCE(csi.card_subtype_ids, ARRAY [] :: text []) as card_subtype_ids, + COALESCE(csn.card_subtype_names, ARRAY [] :: text []) as card_subtype_names, + p.printing_ids, + COALESCE(r.restriction_ids, ARRAY [] :: text []) as restriction_ids, + COALESCE(r_b.restrictions_banned, ARRAY [] :: text []) as restrictions_banned, + COALESCE( + r_g_p.restrictions_global_penalty, + ARRAY [] :: text [] + ) as restrictions_global_penalty, + COALESCE(r_p.restrictions_points, ARRAY [] :: text []) as restrictions_points, + COALESCE(r_r.restrictions_restricted, ARRAY [] :: text []) as restrictions_restricted, + COALESCE( + r_u_f_c.restrictions_universal_faction_cost, + ARRAY [] :: text [] + ) as restrictions_universal_faction_cost, + COALESCE(f.format_ids, ARRAY [] :: text []) as format_ids, + COALESCE(cpc.card_pool_ids, ARRAY [] :: text []) as card_pool_ids, + COALESCE(s.snapshot_ids, ARRAY [] :: text []) as snapshot_ids +FROM + cards c + JOIN card_printing_ids p ON c.id = p.card_id + LEFT JOIN card_subtype_ids csi ON c.id = csi.card_id + LEFT JOIN card_subtype_names csn ON c.id = csn.card_id + LEFT JOIN card_restriction_ids r ON c.id = r.card_id + LEFT JOIN restrictions_banned_summary r_b ON c.id = r_b.card_id + LEFT JOIN restrictions_global_penalty_summary r_g_p ON c.id = r_g_p.card_id + LEFT JOIN restrictions_points_summary r_p ON c.id = r_p.card_id + LEFT JOIN restrictions_restricted_summary r_r ON c.id = r_r.card_id + LEFT JOIN restrictions_universal_faction_cost_summary r_u_f_c ON c.id = r_u_f_c.card_id + LEFT JOIN format_ids f ON c.id = f.card_id + LEFT JOIN card_pool_ids cpc ON c.id = cpc.card_id + LEFT JOIN snapshot_ids s ON c.id = s.card_id +GROUP BY + c.id, + c.title, + c.stripped_title, + c.card_type_id, + c.side_id, + c.faction_id, + c.advancement_requirement, + c.agenda_points, + c.base_link, + c.cost, + c.deck_limit, + c.influence_cost, + c.influence_limit, + c.memory_cost, + c.minimum_deck_size, + c.strength, + c.stripped_text, + c.text, + c.trash_cost, + c.is_unique, + c.display_subtypes, + c.created_at, + c.updated_at, + c.additional_cost, + c.advanceable, + c.gains_subroutines, + c.interrupt, + c.link_provided, + c.mu_provided, + c.num_printed_subroutines, + c.on_encounter_effect, + c.performs_trace, + c.recurring_credits_provided, + c.rez_effect, + c.trash_ability, + csi.card_subtype_ids, + csn.card_subtype_names, + p.printing_ids, + r.restriction_ids, + r_b.restrictions_banned, + r_g_p.restrictions_global_penalty, + r_p.restrictions_points, + r_r.restrictions_restricted, + r_u_f_c.restrictions_universal_faction_cost, + f.format_ids, + cpc.card_pool_ids, + s.snapshot_ids; \ No newline at end of file diff --git a/db/views/unified_cards_v01.sql b/db/views/unified_cards_v01.sql new file mode 100644 index 00000000..27955173 --- /dev/null +++ b/db/views/unified_cards_v01.sql @@ -0,0 +1,310 @@ +WITH card_cycles_summary AS ( + SELECT + c.id, + ARRAY_AGG( + cc.id ORDER BY cc.id + ) as card_cycle_ids, + ARRAY_AGG( + LOWER(cc.name) ORDER BY LOWER(cc.name) + ) as card_cycle_names + FROM + cards c + JOIN printings p ON c.id = p.card_id + JOIN card_sets cs ON p.card_set_id = cs.id + JOIN card_cycles cc ON cc.id = cs.card_cycle_id + GROUP BY + c.id +), +card_sets_summary AS ( + SELECT + c.id, + ARRAY_AGG( + cs.id ORDER BY cs.id + ) as card_set_ids, + ARRAY_AGG( + LOWER(cs.name) ORDER BY LOWER(cs.name) + ) as card_set_names + FROM + cards c + JOIN printings p ON c.id = p.card_id + JOIN card_sets cs ON p.card_set_id = cs.id + GROUP BY + c.id +), +card_subtype_ids AS ( + SELECT + card_id, + ARRAY_AGG( + card_subtype_id + ORDER BY + 1 + ) as card_subtype_ids + FROM + cards_card_subtypes + GROUP BY + card_id +), +card_subtype_names AS ( + SELECT + ccs.card_id, + -- lower used for filtering + ARRAY_AGG( + LOWER(cs.name) ORDER BY LOWER(cs.name) + ) as lower_card_subtype_names, + -- proper case used for display + ARRAY_AGG( + cs.name ORDER BY cs.name + ) as card_subtype_names + FROM + cards_card_subtypes ccs + JOIN card_subtypes cs ON ccs.card_subtype_id = cs.id + GROUP BY + ccs.card_id +), +card_printing_ids AS ( + SELECT + card_id, + ARRAY_AGG( + id ORDER BY date_release DESC + ) as printing_ids + FROM + printings + GROUP BY + card_id +), +card_restriction_ids AS ( + SELECT + card_id, + ARRAY_AGG( + restriction_id ORDER BY restriction_id + ) as restriction_ids + FROM + unified_restrictions + WHERE + in_restriction + GROUP BY + 1 +), +restrictions_banned_summary AS ( + SELECT + card_id, + ARRAY_AGG( + restriction_id ORDER BY restriction_id + ) as restrictions_banned + FROM + restrictions_cards_banned + GROUP BY + card_id +), +restrictions_global_penalty_summary AS ( + SELECT + card_id, + ARRAY_AGG( + restriction_id ORDER BY restriction_id + ) as restrictions_global_penalty + FROM + restrictions_cards_global_penalty + GROUP BY + card_id +), +restrictions_points_summary AS ( + SELECT + card_id, + ARRAY_AGG( + CONCAT(restriction_id, '=', CAST (value AS text)) + ORDER BY CONCAT(restriction_id, '=', CAST (value AS text)) + ) as restrictions_points + FROM + restrictions_cards_points + GROUP BY + card_id +), +restrictions_restricted_summary AS ( + SELECT + card_id, + ARRAY_AGG( + restriction_id ORDER BY restriction_id + ) as restrictions_restricted + FROM + restrictions_cards_restricted + GROUP BY + card_id +), +restrictions_universal_faction_cost_summary AS ( + SELECT + card_id, + ARRAY_AGG( + CONCAT(restriction_id, '=', CAST (value AS text)) + ORDER BY CONCAT(restriction_id, '=', CAST (value AS text)) + ) as restrictions_universal_faction_cost + FROM + restrictions_cards_universal_faction_cost + GROUP BY + card_id +), +format_ids AS ( + SELECT + cpc.card_id, + ARRAY_AGG( + DISTINCT s.format_id ORDER BY s.format_id + ) as format_ids + FROM + card_pools_cards cpc + JOIN snapshots s ON cpc.card_pool_id = s.card_pool_id + GROUP BY + cpc.card_id +), +card_pool_ids AS ( + SELECT + cpc.card_id, + ARRAY_AGG( + DISTINCT s.card_pool_id ORDER BY s.card_pool_id + ) as card_pool_ids + FROM + card_pools_cards cpc + JOIN snapshots s ON cpc.card_pool_id = s.card_pool_id + GROUP BY + cpc.card_id +), +snapshot_ids AS ( + SELECT + cpc.card_id, + ARRAY_AGG( + DISTINCT s.id ORDER BY s.id + ) as snapshot_ids + FROM + card_pools_cards cpc + JOIN snapshots s ON cpc.card_pool_id = s.card_pool_id + GROUP BY + cpc.card_id +) +SELECT + c.id as id, + c.title, + c.stripped_title, + c.card_type_id, + c.side_id, + c.faction_id, + c.advancement_requirement, + c.agenda_points, + c.base_link, + c.cost, + c.deck_limit, + c.influence_cost, + c.influence_limit, + c.memory_cost, + c.minimum_deck_size, + c.strength, + c.stripped_text, + c.text, + c.trash_cost, + c.is_unique, + c.display_subtypes, + c.created_at, + c.updated_at, + c.additional_cost, + c.advanceable, + c.gains_subroutines, + c.interrupt, + c.link_provided, + c.mu_provided, + c.num_printed_subroutines, + c.on_encounter_effect, + c.performs_trace, + c.recurring_credits_provided, + c.rez_effect, + c.trash_ability, + COALESCE(csi.card_subtype_ids, ARRAY [] :: text []) as card_subtype_ids, + COALESCE(csn.lower_card_subtype_names, ARRAY [] :: text []) as lower_card_subtype_names, + COALESCE(csn.card_subtype_names, ARRAY [] :: text []) as card_subtype_names, + p.printing_ids, + ARRAY_LENGTH(p.printing_ids, 1) AS num_printings, + ccs.card_cycle_ids, + ccs.card_cycle_names, + css.card_set_ids, + css.card_set_names, + COALESCE(r.restriction_ids, ARRAY [] :: text []) as restriction_ids, + r.restriction_ids IS NOT NULL as in_restriction, + COALESCE(r_b.restrictions_banned, ARRAY [] :: text []) as restrictions_banned, + COALESCE( + r_g_p.restrictions_global_penalty, + ARRAY [] :: text [] + ) as restrictions_global_penalty, + COALESCE(r_p.restrictions_points, ARRAY [] :: text []) as restrictions_points, + COALESCE(r_r.restrictions_restricted, ARRAY [] :: text []) as restrictions_restricted, + COALESCE( + r_u_f_c.restrictions_universal_faction_cost, + ARRAY [] :: text [] + ) as restrictions_universal_faction_cost, + COALESCE(f.format_ids, ARRAY [] :: text []) as format_ids, + COALESCE(cpc.card_pool_ids, ARRAY [] :: text []) as card_pool_ids, + COALESCE(s.snapshot_ids, ARRAY [] :: text []) as snapshot_ids +FROM + cards c + JOIN card_printing_ids p ON c.id = p.card_id + JOIN card_cycles_summary ccs ON c.id = ccs.id + JOIN card_sets_summary css ON c.id = css.id + LEFT JOIN card_subtype_ids csi ON c.id = csi.card_id + LEFT JOIN card_subtype_names csn ON c.id = csn.card_id + LEFT JOIN card_restriction_ids r ON c.id = r.card_id + LEFT JOIN restrictions_banned_summary r_b ON c.id = r_b.card_id + LEFT JOIN restrictions_global_penalty_summary r_g_p ON c.id = r_g_p.card_id + LEFT JOIN restrictions_points_summary r_p ON c.id = r_p.card_id + LEFT JOIN restrictions_restricted_summary r_r ON c.id = r_r.card_id + LEFT JOIN restrictions_universal_faction_cost_summary r_u_f_c ON c.id = r_u_f_c.card_id + LEFT JOIN format_ids f ON c.id = f.card_id + LEFT JOIN card_pool_ids cpc ON c.id = cpc.card_id + LEFT JOIN snapshot_ids s ON c.id = s.card_id +GROUP BY + c.id, + c.title, + c.stripped_title, + c.card_type_id, + c.side_id, + c.faction_id, + c.advancement_requirement, + c.agenda_points, + c.base_link, + c.cost, + c.deck_limit, + c.influence_cost, + c.influence_limit, + c.memory_cost, + c.minimum_deck_size, + c.strength, + c.stripped_text, + c.text, + c.trash_cost, + c.is_unique, + c.display_subtypes, + c.created_at, + c.updated_at, + c.additional_cost, + c.advanceable, + c.gains_subroutines, + c.interrupt, + c.link_provided, + c.mu_provided, + c.num_printed_subroutines, + c.on_encounter_effect, + c.performs_trace, + c.recurring_credits_provided, + c.rez_effect, + c.trash_ability, + csi.card_subtype_ids, + csn.lower_card_subtype_names, + csn.card_subtype_names, + p.printing_ids, + ccs.card_cycle_ids, + ccs.card_cycle_names, + css.card_set_ids, + css.card_set_names, + r.restriction_ids, + r_b.restrictions_banned, + r_g_p.restrictions_global_penalty, + r_p.restrictions_points, + r_r.restrictions_restricted, + r_u_f_c.restrictions_universal_faction_cost, + f.format_ids, + cpc.card_pool_ids, + s.snapshot_ids; \ No newline at end of file diff --git a/lib/card_search_parser.rb b/lib/card_search_parser.rb index f095501c..79789275 100644 --- a/lib/card_search_parser.rb +++ b/lib/card_search_parser.rb @@ -23,7 +23,10 @@ class CardSearchParser < Parslet::Parser str('advancement_cost') | str('agenda_points') | str('base_link') | + str('card_cycle') | str('card_pool') | + str('card_set') | + str('card_subtype_id') | str('card_subtype') | str('card_type') | str('cost') | @@ -43,11 +46,14 @@ class CardSearchParser < Parslet::Parser str('memory_usage') | str('mu_provided') | str('num_printed_subroutines') | + str('num_printings') | str('on_encounter_effect') | str('performs_trace') | + str('printing_id') | str('recurring_credits_provided') | str('restriction_id') | str('side') | + str('snapshot') | str('strength') | str('text') | str('title') | diff --git a/lib/card_search_query_builder.rb b/lib/card_search_query_builder.rb index 305e9230..e0eee3e6 100644 --- a/lib/card_search_query_builder.rb +++ b/lib/card_search_query_builder.rb @@ -1,16 +1,30 @@ class CardSearchQueryBuilder @@parser = CardSearchParser.new + @@array_keywords = [ + 'card_cycle', + 'card_pool', + 'card_set', + 'card_subtype', + 'card_subtype_id', + 'eternal_points', + 'format', + 'has_global_penalty', + 'is_banned', + 'is_restricted', + 'printing_id', + 'restriction_id', + 's', + 'snapshot', + 'universal_faction_cost', + ] @@boolean_keywords = [ 'additional_cost', 'advanceable', 'b', 'banlist', 'gains_subroutines', - 'has_global_penalty', 'in_restriction', 'interrupt', - 'is_banned', - 'is_restricted', 'is_unique', 'on_encounter_effect', 'performs_trace', @@ -22,7 +36,6 @@ class CardSearchQueryBuilder 'agenda_points', 'base_link', 'cost', - 'eternal_points', 'g', 'h', 'influence_cost', @@ -33,12 +46,12 @@ class CardSearchQueryBuilder 'mu_provided', 'n', 'num_printed_subroutines', + 'num_printings', 'o', 'p', 'recurring_credits_provided', 'strength', 'trash_cost', - 'universal_faction_cost', 'v', ] @@string_keywords = [ @@ -47,13 +60,16 @@ class CardSearchQueryBuilder 'd', 'f', 'faction', - 'restriction_id', 'side', 't', 'text', 'title', 'x', ] + @@array_operators = { + ':' => '', + '!' => 'NOT', + } @@boolean_operators = { ':' => '=', '!' => '!=', @@ -70,72 +86,69 @@ class CardSearchQueryBuilder ':' => 'LIKE', '!' => 'NOT LIKE', } + # TODO(plural): figure out how to do name matches that are LIKEs over elements of an array. @@term_to_field_map = { # format should implicitly use the currently active card pool and restriction lists unless another is specified. - '_' => 'cards.stripped_title', - 'additional_cost' => 'cards.additional_cost', - 'advanceable' => 'cards.advanceable', - 'advancement_cost' => 'cards.advancement_requirement', - 'agenda_points' => 'cards.agenda_points', - 'base_link' => 'cards.base_link', - 'card_pool' => 'card_pools_cards.card_pool_id', - 'card_subtype' => 'card_subtypes.name', - 'card_type' => 'cards.card_type_id', - 'cost' => 'cards.cost', - 'd' => 'cards.side_id', - 'eternal_points' => 'unified_restrictions.eternal_points', - 'f' => 'cards.faction_id', - 'faction' => 'cards.faction_id', - 'format' => 'unified_restrictions.format_id', - 'g' => 'cards.advancement_requirement', - 'gains_subroutines' => 'cards.gains_subroutines', - 'h' => 'cards.trash_cost', - 'has_global_penalty' => 'unified_restrictions.has_global_penalty', - 'in_restriction' => 'unified_restrictions.in_restriction', - 'influence_cost' => 'cards.influence_cost', - 'interrupt' => 'cards.interrupt', - 'is_banned' => 'unified_restrictions.is_banned', - 'is_restricted' => 'unified_restrictions.is_restricted', - 'is_unique' => 'cards.is_unique', - 'l' => 'cards.base_link', - 'link_provided' => 'cards.link_provided', - 'm' => 'cards.memory_cost', - 'memory_usage' => 'cards.memory_cost', - 'mu_provided' => 'cards.mu_provided', - 'n' => 'cards.influence_cost', - 'num_printed_subroutines' => 'cards.num_printed_subroutines', - 'o' => 'cards.cost', - 'on_encounter_effect' => 'cards.on_encounter_effect', - 'p' => 'cards.strength', - 'performs_trace' => 'cards.performs_trace', - 'recurring_credits_provided' => 'cards.recurring_credits_provided', - 'restriction_id' => 'unified_restrictions.restriction_id', - 's' => 'card_subtypes.name', - 'side' => 'cards.card_side_id', - 'strength' => 'cards.strength', - 't' => 'cards.card_type_id', - 'text' => 'cards.stripped_text', - 'title' => 'cards.stripped_title', - 'trash_ability' => 'cards.trash_ability', - 'trash_cost' => 'cards.trash_cost', - 'u' => 'cards.is_unique', - 'universal_faction_cost' => 'unified_restrictions.universal_faction_cost', - 'v' => 'cards.agenda_points', - 'x' => 'cards.stripped_text', + '_' => 'unified_cards.stripped_title', + 'additional_cost' => 'unified_cards.additional_cost', + 'advanceable' => 'unified_cards.advanceable', + 'advancement_cost' => 'unified_cards.advancement_requirement', + 'agenda_points' => 'unified_cards.agenda_points', + 'base_link' => 'unified_cards.base_link', + 'card_cycle' => 'unified_cards.card_cycle_ids', + 'card_pool' => 'unified_cards.card_pool_ids', + 'card_set' => 'unified_cards.card_set_ids', + 'card_subtype' => 'unified_cards.lower_card_subtype_names', + 'card_subtype_id' => 'unified_cards.card_subtype_ids', + 'card_type' => 'unified_cards.card_type_id', + 'cost' => 'unified_cards.cost', + 'd' => 'unified_cards.side_id', + 'eternal_points' => 'unified_cards.restrictions_points', + 'f' => 'unified_cards.faction_id', + 'faction' => 'unified_cards.faction_id', + 'format' => 'unified_cards.format_ids', + 'g' => 'unified_cards.advancement_requirement', + 'gains_subroutines' => 'unified_cards.gains_subroutines', + 'h' => 'unified_cards.trash_cost', + 'has_global_penalty' => 'unified_cards.restrictions_global_penalty', + 'in_restriction' => 'unified_cards.in_restriction', + 'influence_cost' => 'unified_cards.influence_cost', + 'interrupt' => 'unified_cards.interrupt', + 'is_banned' => 'unified_cards.restrictions_banned', + 'is_restricted' => 'unified_cards.restrictions_restricted', + 'is_unique' => 'unified_cards.is_unique', + 'l' => 'unified_cards.base_link', + 'link_provided' => 'unified_cards.link_provided', + 'm' => 'unified_cards.memory_cost', + 'memory_usage' => 'unified_cards.memory_cost', + 'mu_provided' => 'unified_cards.mu_provided', + 'n' => 'unified_cards.influence_cost', + 'num_printed_subroutines' => 'unified_cards.num_printed_subroutines', + 'num_printings' => 'unified_cards.num_printings', + 'o' => 'unified_cards.cost', + 'on_encounter_effect' => 'unified_cards.on_encounter_effect', + 'p' => 'unified_cards.strength', + 'performs_trace' => 'unified_cards.performs_trace', + 'printing_id' => 'unified_cards.printing_ids', + 'recurring_credits_provided' => 'unified_cards.recurring_credits_provided', + 'restriction_id' => 'unified_cards.restriction_ids', + 's' => 'unified_cards.lower_card_subtype_names', + 'side' => 'unified_cards.side_id', + 'snapshot' => 'unified_cards.snapshot_ids', + 'strength' => 'unified_cards.strength', + 't' => 'unified_cards.card_type_id', + 'text' => 'unified_cards.stripped_text', + 'title' => 'unified_cards.stripped_title', + 'trash_ability' => 'unified_cards.trash_ability', + 'trash_cost' => 'unified_cards.trash_cost', + 'u' => 'unified_cards.is_unique', + 'universal_faction_cost' => 'unified_cards.restrictions_universal_faction_cost', + 'v' => 'unified_cards.agenda_points', + 'x' => 'unified_cards.stripped_text', } @@term_to_left_join_map = { - 'card_pool' => :card_pool_cards, - 'card_subtype' => :card_subtypes, - 'eternal_points' => :unified_restrictions, - 'has_global_penalty' => :unified_restrictions, - 'in_restriction' => :unified_restrictions, - 'is_banned' => :unified_restrictions, - 'is_restricted' => :unified_restrictions, - 'restriction_id' => :unified_restrictions, - 's' => :card_subtypes, - 'universal_faction_cost' => :unified_restrictions, - } + } def initialize(query) @query = query @@ -162,7 +175,19 @@ def initialize(query) keyword = f[:search_term][:keyword].to_s match_type = f[:search_term][:match_type].to_s value = f[:search_term][:value][:string].to_s.downcase - if @@boolean_keywords.include?(keyword) + if @@array_keywords.include?(keyword) + if @@array_operators.include?(match_type) + operator = @@array_operators[match_type] + else + @parse_error = 'Invalid array operator "%s"' % match_type + return + end + if value.match?(/\A(\w+)-(\d+)\Z/i) + value.gsub!('-', '=') + end + constraints << '%s (? = ANY(%s))' % [operator, @@term_to_field_map[keyword]] + where << value + elsif @@boolean_keywords.include?(keyword) if !['true', 'false', 't', 'f', '1', '0'].include?(value) @parse_error = 'Invalid value "%s" for boolean field "%s"' % [value, keyword] return diff --git a/lib/tasks/cards.rake b/lib/tasks/cards.rake index 788f48d9..652a5d1c 100644 --- a/lib/tasks/cards.rake +++ b/lib/tasks/cards.rake @@ -742,6 +742,9 @@ namespace :cards do puts 'Refreshing materialized view for restrictions...' Scenic.database.refresh_materialized_view(:unified_restrictions, concurrently: false, cascade: false) + puts 'Refreshing materialized view for cards...' + Scenic.database.refresh_materialized_view(:unified_cards, concurrently: false, cascade: false) + puts 'Done!' end end