This commit is contained in:
2025-04-29 00:00:47 -04:00
parent 56ba750aad
commit c9bba8a26e
25 changed files with 1266 additions and 152052 deletions

View File

@ -29,6 +29,7 @@ from app.models.tcgplayer_order import (
)
from app.models.tcgplayer_inventory import TCGPlayerInventory
from app.models.manabox_import_staging import ManaboxImportStaging
from app.models.pricing import PricingEvent
__all__ = [
@ -56,5 +57,6 @@ __all__ = [
'TCGPlayerOrderProduct',
'TCGPlayerOrderRefund',
'TCGPlayerPriceHistory',
'MostRecentTCGPlayerPrice'
'MostRecentTCGPlayerPrice',
'PricingEvent'
]

View File

@ -1,4 +1,4 @@
from sqlalchemy import Column, Integer, String, Float, DateTime, ForeignKey, CheckConstraint
from sqlalchemy import Column, Integer, String, Float, DateTime, ForeignKey, CheckConstraint, Index, Boolean, Table, UniqueConstraint
from sqlalchemy.orm import relationship
from app.db.database import Base
from sqlalchemy import event
@ -7,6 +7,17 @@ from sqlalchemy import func
from sqlalchemy.ext.hybrid import hybrid_property
from datetime import datetime
from app.models.critical_error_log import CriticalErrorLog
import logging
logger = logging.getLogger(__name__)
open_event_resulting_items = Table(
"open_event_resulting_items",
Base.metadata,
Column("event_id", Integer, ForeignKey("open_events.id"), primary_key=True),
Column("item_id", Integer, ForeignKey("physical_items.id"), primary_key=True)
)
class PhysicalItem(Base):
__tablename__ = "physical_items"
@ -38,21 +49,32 @@ class PhysicalItem(Base):
sku = relationship("MTGJSONSKU", back_populates="physical_items", primaryjoin="PhysicalItem.tcgplayer_sku_id == MTGJSONSKU.tcgplayer_sku_id")
product_direct = relationship("TCGPlayerProduct",
back_populates="physical_items_direct",
primaryjoin="PhysicalItem.tcgplayer_product_id == foreign(TCGPlayerProduct.tcgplayer_product_id)")
primaryjoin="PhysicalItem.tcgplayer_product_id == foreign(TCGPlayerProduct.tcgplayer_product_id)", uselist=False)
inventory_item = relationship("InventoryItem", uselist=False, back_populates="physical_item")
transaction_items = relationship("TransactionItem", back_populates="physical_item")
#transaction_items = relationship("TransactionItem", back_populates="physical_item")
source_open_events = relationship(
"OpenEvent",
back_populates="source_item",
foreign_keys="[OpenEvent.source_item_id]"
)
resulting_open_events = relationship(
"OpenEvent",
secondary=open_event_resulting_items,
back_populates="resulting_items"
)
@hybrid_property
def is_sealed(self):
return not self.source_open_events
@hybrid_property
def products(self):
"""
Dynamically resolve the associated TCGPlayerProduct(s):
- If the SKU is set, return all linked products.
- Else, return a list containing a single product from direct link.
"""
# TODO IS THIS EVEN CORRECT OH GOD
if self.sku and self.sku.products:
return self.sku.products # This is a list of TCGPlayerProduct
return [self.product_direct] if self.product_direct else []
if self.sku and self.sku.product:
return self.sku.product
elif self.product_direct:
return self.product_direct
else:
return None
class InventoryItem(Base):
__tablename__ = "inventory_items"
@ -69,7 +91,8 @@ class InventoryItem(Base):
physical_item = relationship("PhysicalItem", back_populates="inventory_item")
parent = relationship("InventoryItem", remote_side=[id], back_populates="children")
children = relationship("InventoryItem", back_populates="parent", overlaps="parent")
marketplace_listings = relationship("MarketplaceListing", back_populates="inventory_item")
marketplace_listing = relationship("MarketplaceListing", back_populates="inventory_item")
transaction_items = relationship("TransactionItem", back_populates="inventory_item")
@property
def products(self):
@ -78,7 +101,7 @@ class InventoryItem(Base):
Returns:
list[TCGPlayerProduct] or [] if no physical item or no linked products.
"""
return self.physical_item.product if self.physical_item else []
return self.physical_item.products if self.physical_item else None
def soft_delete(self, timestamp=None):
if not timestamp:
@ -87,92 +110,68 @@ class InventoryItem(Base):
for child in self.children:
child.soft_delete(timestamp)
class SealedBox(PhysicalItem):
__tablename__ = "sealed_boxes"
class Box(PhysicalItem):
__tablename__ = "boxes"
id = Column(Integer, ForeignKey('physical_items.id'), primary_key=True)
case_id = Column(Integer, ForeignKey("sealed_cases.id"), nullable=True)
expected_value = Column(Float)
__mapper_args__ = {
'polymorphic_identity': 'sealed_box'
'polymorphic_identity': 'box'
}
# Relationships
case = relationship("SealedCase", back_populates="boxes", foreign_keys=[case_id])
open_event = relationship("OpenEvent", uselist=False, back_populates="sealed_box")
class SealedCase(PhysicalItem):
__tablename__ = "sealed_cases"
class Case(PhysicalItem):
__tablename__ = "cases"
id = Column(Integer, ForeignKey('physical_items.id'), primary_key=True)
expected_value = Column(Float)
num_boxes = Column(Integer)
__mapper_args__ = {
'polymorphic_identity': 'sealed_case'
'polymorphic_identity': 'case'
}
# Relationships
boxes = relationship("SealedBox", back_populates="case", foreign_keys=[SealedBox.case_id])
open_event = relationship("OpenEvent", uselist=False, back_populates="sealed_case")
class Card(PhysicalItem):
__tablename__ = "cards"
id = Column(Integer, ForeignKey('physical_items.id'), primary_key=True)
__mapper_args__ = {
'polymorphic_identity': 'card'
}
class OpenEvent(Base):
__tablename__ = "open_events"
id = Column(Integer, primary_key=True, index=True)
sealed_case_id = Column(Integer, ForeignKey("sealed_cases.id"), nullable=True)
sealed_box_id = Column(Integer, ForeignKey("sealed_boxes.id"), nullable=True)
source_item_id = Column(Integer, ForeignKey("physical_items.id"))
open_date = Column(DateTime(timezone=True))
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
deleted_at = Column(DateTime(timezone=True), nullable=True)
# Relationships
sealed_case = relationship("SealedCase", back_populates="open_event")
sealed_box = relationship("SealedBox", back_populates="open_event")
resulting_boxes = relationship("OpenBox", back_populates="open_event")
resulting_cards = relationship("OpenCard", back_populates="open_event")
class OpenCard(PhysicalItem):
__tablename__ = "open_cards"
id = Column(Integer, ForeignKey('physical_items.id'), primary_key=True)
open_event_id = Column(Integer, ForeignKey("open_events.id"))
box_id = Column(Integer, ForeignKey("open_boxes.id"), nullable=True)
__mapper_args__ = {
'polymorphic_identity': 'open_card'
}
__table_args__ = (
UniqueConstraint("source_item_id", name="uq_openevent_one_per_source"),
)
# Relationships
open_event = relationship("OpenEvent", back_populates="resulting_cards")
box = relationship("OpenBox", back_populates="cards", foreign_keys=[box_id])
class OpenBox(PhysicalItem):
__tablename__ = "open_boxes"
id = Column(Integer, ForeignKey('physical_items.id'), primary_key=True)
open_event_id = Column(Integer, ForeignKey("open_events.id"))
sealed_box_id = Column(Integer, ForeignKey("sealed_boxes.id"))
__mapper_args__ = {
'polymorphic_identity': 'open_box'
}
# Relationships
open_event = relationship("OpenEvent", back_populates="resulting_boxes")
sealed_box = relationship("SealedBox", foreign_keys=[sealed_box_id])
cards = relationship("OpenCard", back_populates="box", foreign_keys=[OpenCard.box_id])
source_item = relationship(
"PhysicalItem",
back_populates="source_open_events",
foreign_keys=[source_item_id]
)
resulting_items = relationship(
"PhysicalItem",
secondary=open_event_resulting_items,
back_populates="resulting_open_events"
)
class SealedExpectedValue(Base):
__tablename__ = "sealed_expected_values"
__table_args__ = (
Index('idx_sealed_expected_value_product_id_deleted_at', 'tcgplayer_product_id', 'deleted_at', unique=True),
)
id = Column(Integer, primary_key=True, index=True)
tcgplayer_product_id = Column(Integer, nullable=False)
@ -185,64 +184,56 @@ class SealedExpectedValue(Base):
product = relationship(
"TCGPlayerProduct",
primaryjoin="SealedExpectedValue.tcgplayer_product_id == foreign(TCGPlayerProduct.tcgplayer_product_id)",
viewonly=True,
backref="sealed_expected_values"
)
viewonly=True)
# helper for ev
def assign_expected_value(target, session):
products = target.product # uses hybrid property
if not products:
raise ValueError(f"No product found for item ID {target.id}")
#def assign_expected_value(target, session):
# products = target.products
# if not products:
# raise ValueError(f"No product found for item ID {target.id}")
if len(products) > 1:
product_names = [p.name for p in products]
critical_error = CriticalErrorLog(
error_type="multiple_products_found",
error_message=f"Multiple products found when assigning expected value for item ID {target.id} product names {product_names}"
)
session.add(critical_error)
session.commit()
raise ValueError(f"Multiple products found when assigning expected value for item ID {target.id} product names {product_names}")
# if len(products) > 1:
# product_names = [p.name for p in products]
# critical_error = CriticalErrorLog(
# error_type="multiple_products_found",
# error_message=f"Multiple products found when assigning expected value for item ID {target.id} product names {product_names}"
# )
# session.add(critical_error)
# session.commit()
# raise ValueError(f"Multiple products found when assigning expected value for item ID {target.id} product names {product_names}")
product_id = products[0].tcgplayer_product_id # reliable lookup key
# product_id = products[0].tcgplayer_product_id # reliable lookup key
expected_value_entry = session.query(SealedExpectedValue).filter(
SealedExpectedValue.tcgplayer_product_id == product_id,
SealedExpectedValue.deleted_at == None
).order_by(SealedExpectedValue.created_at.desc()).first()
# expected_value_entry = session.query(SealedExpectedValue).filter(
# SealedExpectedValue.tcgplayer_product_id == product_id,
# SealedExpectedValue.deleted_at == None
# ).order_by(SealedExpectedValue.created_at.desc()).first()
if expected_value_entry:
target.expected_value = expected_value_entry.expected_value
else:
critical_error = CriticalErrorLog(
error_type="no_expected_value_found",
error_message=f"No expected value found for product {products[0].name}"
)
session.add(critical_error)
session.commit()
raise ValueError(f"No expected value found for product {products[0].name}")
# if expected_value_entry:
# target.expected_value = expected_value_entry.expected_value
# else:
# critical_error = CriticalErrorLog(
# error_type="no_expected_value_found",
# error_message=f"No expected value found for product {products[0].name}"
# )
# session.add(critical_error)
# session.commit()
# raise ValueError(f"No expected value found for product {products[0].name}")
# event listeners
@event.listens_for(SealedBox, 'before_insert')
def sealed_box_before_insert(mapper, connection, target):
session = Session.object_session(target)
if session:
assign_expected_value(target, session)
@event.listens_for(SealedCase, 'before_insert')
def sealed_case_before_insert(mapper, connection, target):
session = Session.object_session(target)
if session:
assign_expected_value(target, session)
#@event.listens_for(InventoryItem, 'before_insert')
#def ev_before_insert(mapper, connection, target):
# session = Session.object_session(target)
# if session:
# assign_expected_value(target, session)
class TransactionItem(Base):
__tablename__ = "transaction_items"
id = Column(Integer, primary_key=True, index=True)
transaction_id = Column(Integer, ForeignKey("transactions.id"))
physical_item_id = Column(Integer, ForeignKey("physical_items.id"))
inventory_item_id = Column(Integer, ForeignKey("inventory_items.id"))
unit_price = Column(Float, nullable=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
@ -250,7 +241,7 @@ class TransactionItem(Base):
# Relationships
transaction = relationship("Transaction", back_populates="transaction_items")
physical_item = relationship("PhysicalItem", back_populates="transaction_items")
inventory_item = relationship("InventoryItem", back_populates="transaction_items")
class Vendor(Base):
__tablename__ = "vendors"
@ -305,13 +296,16 @@ class MarketplaceListing(Base):
id = Column(Integer, primary_key=True, index=True)
inventory_item_id = Column(Integer, ForeignKey("inventory_items.id"), nullable=False)
marketplace_id = Column(Integer, ForeignKey("marketplaces.id"), nullable=False)
listing_date = Column(DateTime(timezone=True))
recommended_price_id = Column(Integer, ForeignKey("pricing_events.id"), nullable=True)
listed_price_id = Column(Integer, ForeignKey("pricing_events.id"), nullable=True)
listing_date = Column(DateTime(timezone=True), nullable=True)
delisting_date = Column(DateTime(timezone=True), nullable=True)
listed_price = Column(Float)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
deleted_at = Column(DateTime(timezone=True), nullable=True)
# Relationships
inventory_item = relationship("InventoryItem", back_populates="marketplace_listings")
marketplace = relationship("Marketplace", back_populates="listings")
inventory_item = relationship("InventoryItem", back_populates="marketplace_listing")
marketplace = relationship("Marketplace", back_populates="listings")
recommended_price = relationship("PricingEvent", foreign_keys=[recommended_price_id])
listed_price = relationship("PricingEvent", foreign_keys=[listed_price_id])

View File

@ -8,6 +8,7 @@ class ManaboxImportStaging(Base):
id = Column(Integer, primary_key=True)
file_id = Column(Integer, ForeignKey("files.id"))
tcgplayer_product_id = Column(Integer)
tcgplayer_sku_id = Column(Integer)
quantity = Column(Integer)
created_at = Column(DateTime(timezone=True), server_default=func.now())

29
app/models/pricing.py Normal file
View File

@ -0,0 +1,29 @@
from sqlalchemy import Column, Integer, String, Float, DateTime, ForeignKey, CheckConstraint, Index, Boolean, Table, UniqueConstraint
from sqlalchemy.orm import relationship
from app.db.database import Base
from sqlalchemy import event
from sqlalchemy.orm import Session
from sqlalchemy import func
import logging
logger = logging.getLogger(__name__)
class PricingEvent(Base):
__tablename__ = "pricing_events"
id = Column(Integer, primary_key=True)
inventory_item_id = Column(Integer, ForeignKey("inventory_items.id"))
price = Column(Float)
price_used = Column(String)
price_reason = Column(String)
free_shipping_adjustment = Column(Boolean, default=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
deleted_at = Column(DateTime(timezone=True), nullable=True)
# Relationships
inventory_item = relationship(
"InventoryItem",
primaryjoin="PricingEvent.inventory_item_id == foreign(InventoryItem.id)",
viewonly=True
)

View File

@ -1,12 +1,13 @@
from sqlalchemy import Column, Integer, String, Float, DateTime, ForeignKey
from sqlalchemy.sql import func
from app.db.database import Base
from sqlalchemy.orm import relationship
class TCGPlayerInventory(Base):
__tablename__ = "tcgplayer_inventory"
id = Column(Integer, primary_key=True, index=True)
tcgplayer_sku_id = Column(Integer, ForeignKey("mtgjson_skus.tcgplayer_sku_id"), unique=True, index=True)
tcgplayer_sku_id = Column(Integer, unique=True, index=True)
product_line = Column(String)
set_name = Column(String)
product_name = Column(String)
@ -23,4 +24,38 @@ class TCGPlayerInventory(Base):
tcg_marketplace_price = Column(Float)
photo_url = Column(String)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
# relationships
unmanaged_inventory = relationship("UnmanagedTCGPlayerInventory", back_populates="tcgplayer_inventory")
sku = relationship("MTGJSONSKU", primaryjoin="foreign(MTGJSONSKU.tcgplayer_sku_id) == TCGPlayerInventory.tcgplayer_sku_id", viewonly=True)
class UnmanagedTCGPlayerInventory(Base):
__tablename__ = "unmanaged_tcgplayer_inventory"
id = Column(Integer, primary_key=True, index=True)
tcgplayer_inventory_id = Column(Integer, ForeignKey("tcgplayer_inventory.id"), unique=True, index=True)
tcgplayer_sku_id = Column(Integer, unique=True, index=True)
product_line = Column(String)
set_name = Column(String)
product_name = Column(String)
title = Column(String)
number = Column(String)
rarity = Column(String)
condition = Column(String)
tcg_market_price = Column(Float)
tcg_direct_low = Column(Float)
tcg_low_price_with_shipping = Column(Float)
tcg_low_price = Column(Float)
total_quantity = Column(Integer)
add_to_quantity = Column(Integer)
tcg_marketplace_price = Column(Float)
photo_url = Column(String)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
# relationships
tcgplayer_inventory = relationship("TCGPlayerInventory", back_populates="unmanaged_inventory")
sku = relationship("MTGJSONSKU", primaryjoin="foreign(MTGJSONSKU.tcgplayer_sku_id) == UnmanagedTCGPlayerInventory.tcgplayer_sku_id", viewonly=True)

View File

@ -2,43 +2,44 @@ from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Table, Flo
from sqlalchemy.sql import func
from app.db.database import Base
from sqlalchemy.orm import relationship
from sqlalchemy.orm import Session
from sqlalchemy import and_
# =============================================================================
# Core Models
# =============================================================================
class MTGJSONSKU(Base):
"""Represents the most granular level of card identification.
THIS WORKS EVEN IF ITS A BOX SOMEHOW
"""
__tablename__ = "mtgjson_skus"
id = Column(Integer, primary_key=True, index=True)
mtgjson_uuid = Column(String, ForeignKey("mtgjson_cards.mtgjson_uuid"), unique=True, index=True)
mtgjson_uuid = Column(String, ForeignKey("mtgjson_cards.mtgjson_uuid"), index=True)
tcgplayer_sku_id = Column(Integer, index=True, unique=True)
tcgplayer_product_id = Column(Integer, nullable=False)
normalized_printing = Column(String, nullable=False) # normalized for FK
condition = Column(String) # for boxes, condition = unopened
finish = Column(String, nullable=True) # TODO MAKE THESE ENUMS
normalized_printing = Column(String, nullable=False)
condition = Column(String)
finish = Column(String, nullable=True)
language = Column(String)
printing = Column(String) # original unnormalized field ##### for boxes, printing = NON FOIL
printing = Column(String)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# Foreign key to tcgplayer_products via composite key
__table_args__ = (
ForeignKeyConstraint(
["tcgplayer_product_id", "normalized_printing"],
["tcgplayer_products.tcgplayer_product_id", "tcgplayer_products.normalized_sub_type_name"],
name="fk_sku_to_product_composite"
),
Index('idx_sku_product_printing', 'tcgplayer_product_id', 'normalized_printing', unique=True),
Index('idx_sku_product_printing', 'tcgplayer_product_id', 'normalized_printing'),
)
product = relationship("TCGPlayerProduct", back_populates="skus")
physical_items = relationship("PhysicalItem", back_populates="sku", primaryjoin="PhysicalItem.tcgplayer_sku_id == MTGJSONSKU.tcgplayer_sku_id")
card = relationship("MTGJSONCard", back_populates="skus", primaryjoin="MTGJSONCard.mtgjson_uuid == MTGJSONSKU.mtgjson_uuid")
physical_items = relationship(
"PhysicalItem",
back_populates="sku",
primaryjoin="PhysicalItem.tcgplayer_sku_id == MTGJSONSKU.tcgplayer_sku_id"
)
card = relationship("MTGJSONCard", back_populates="skus")
class MTGJSONCard(Base):
@ -72,13 +73,17 @@ class MTGJSONCard(Base):
scryfall_card_back_id = Column(String, nullable=True)
scryfall_oracle_id = Column(String, nullable=True)
scryfall_illustration_id = Column(String, nullable=True)
tcgplayer_product_id = Column(String, nullable=True)
tcgplayer_etched_product_id = Column(String, nullable=True)
tcgplayer_product_id = Column(Integer, nullable=True)
tcgplayer_etched_product_id = Column(Integer, nullable=True)
tnt_id = Column(String, nullable=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
skus = relationship("MTGJSONSKU", back_populates="card", primaryjoin="MTGJSONSKU.mtgjson_uuid == MTGJSONCard.mtgjson_uuid")
__table_args__ = (
UniqueConstraint("mtgjson_uuid", name="uq_card_mtgjson_uuid"),
)
skus = relationship("MTGJSONSKU", back_populates="card")
class TCGPlayerProduct(Base):
@ -117,13 +122,16 @@ class TCGPlayerProduct(Base):
# Enforce uniqueness for composite key
__table_args__ = (
UniqueConstraint("tcgplayer_product_id", "normalized_sub_type_name", name="uq_product_subtype"),
Index('idx_product_subtype', 'tcgplayer_product_id', 'normalized_sub_type_name'),
)
# Backref to SKUs that link via composite FK
skus = relationship("MTGJSONSKU", back_populates="product")
physical_items_direct = relationship("PhysicalItem",
back_populates="product_direct",
primaryjoin="foreign(TCGPlayerProduct.tcgplayer_product_id) == PhysicalItem.tcgplayer_product_id")
primaryjoin="foreign(TCGPlayerProduct.tcgplayer_product_id) == PhysicalItem.tcgplayer_product_id",
viewonly=True,
uselist=False)
category = relationship("TCGPlayerCategory", back_populates="products")
group = relationship("TCGPlayerGroup", back_populates="products")
price_history = relationship("TCGPlayerPriceHistory",
@ -136,6 +144,11 @@ class TCGPlayerProduct(Base):
primaryjoin="and_(foreign(TCGPlayerProduct.tcgplayer_product_id) == MostRecentTCGPlayerPrice.product_id, "
"foreign(TCGPlayerProduct.sub_type_name) == MostRecentTCGPlayerPrice.sub_type_name)")
sealed_expected_value = relationship("SealedExpectedValue",
primaryjoin="and_(TCGPlayerProduct.tcgplayer_product_id == foreign(SealedExpectedValue.tcgplayer_product_id), "
"foreign(SealedExpectedValue.deleted_at) == None)",
viewonly=True,
uselist=False)
# =============================================================================
# Supporting Models
# =============================================================================
@ -213,9 +226,7 @@ class TCGPlayerPriceHistory(Base):
class MostRecentTCGPlayerPrice(Base):
"""Represents the most recent price for a product.
This is a materialized view that contains the latest price data for each product.
It is maintained through triggers on the price_history table.
THIS ISNT A MATERIALIZED VIEW ANYMORE FUCK IT
"""
__tablename__ = "most_recent_tcgplayer_price"
@ -232,51 +243,53 @@ class MostRecentTCGPlayerPrice(Base):
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
__table_args__ = (
Index('idx_most_recent_price_product_subtype', 'product_id', 'sub_type_name', unique=True),
{'info': {'is_view': True}}
)
Index('idx_most_recent_price_product_subtype', 'product_id', 'sub_type_name', unique=True),
)
product = relationship("TCGPlayerProduct",
back_populates="most_recent_tcgplayer_price",
primaryjoin="and_(MostRecentTCGPlayerPrice.product_id == foreign(TCGPlayerProduct.tcgplayer_product_id), "
"MostRecentTCGPlayerPrice.sub_type_name == foreign(TCGPlayerProduct.sub_type_name))")
def create_most_recent_price_view():
"""Creates the materialized view for most recent prices."""
return DDL("""
CREATE MATERIALIZED VIEW IF NOT EXISTS most_recent_tcgplayer_price AS
SELECT DISTINCT ON (ph.product_id, ph.sub_type_name)
ph.id,
ph.product_id,
ph.sub_type_name,
ph.date,
ph.low_price,
ph.mid_price,
ph.high_price,
ph.market_price,
ph.direct_low_price,
ph.created_at,
ph.updated_at
FROM tcgplayer_price_history ph
ORDER BY ph.product_id, ph.sub_type_name, ph.date DESC;
@classmethod
def update_most_recent_prices(cls, db: Session) -> None:
"""Update the most recent prices from the price history table."""
# Delete all existing records
db.query(cls).delete()
CREATE UNIQUE INDEX IF NOT EXISTS idx_most_recent_price_product_subtype
ON most_recent_tcgplayer_price (product_id, sub_type_name);
""")
# Get the most recent price for each product and sub_type_name
subquery = db.query(
TCGPlayerPriceHistory.product_id,
TCGPlayerPriceHistory.sub_type_name,
func.max(TCGPlayerPriceHistory.date).label('max_date')
).group_by(
TCGPlayerPriceHistory.product_id,
TCGPlayerPriceHistory.sub_type_name
).subquery()
# Register the view creation with SQLAlchemy
event.listen(
MostRecentTCGPlayerPrice.__table__,
'after_create',
create_most_recent_price_view()
)
# Join with price history to get the full records
latest_prices = db.query(TCGPlayerPriceHistory).join(
subquery,
and_(
TCGPlayerPriceHistory.product_id == subquery.c.product_id,
TCGPlayerPriceHistory.sub_type_name == subquery.c.sub_type_name,
TCGPlayerPriceHistory.date == subquery.c.max_date
)
).all()
# Add a method to refresh the view
@classmethod
def refresh_view(cls, session):
"""Refreshes the materialized view."""
session.execute(text("REFRESH MATERIALIZED VIEW CONCURRENTLY most_recent_tcgplayer_price"))
session.commit()
# Create new MostRecentTCGPlayerPrice records
for price in latest_prices:
most_recent = cls(
product_id=price.product_id,
sub_type_name=price.sub_type_name,
date=price.date,
low_price=price.low_price,
mid_price=price.mid_price,
high_price=price.high_price,
market_price=price.market_price,
direct_low_price=price.direct_low_price
)
db.add(most_recent)
MostRecentTCGPlayerPrice.refresh_view = refresh_view
db.commit()