Files
romm/backend/alembic/versions/0034_virtual_collections_db_view.py
2026-02-17 15:12:33 -05:00

295 lines
12 KiB
Python

"""Introduce collections_roms table and virtual_collections view
Revision ID: 0034_virtual_collections_db_view
Revises: 0033_rom_file_and_hashes
Create Date: 2024-08-08 12:00:00.000000
"""
import sqlalchemy as sa
from alembic import op
from utils.database import CustomJSON, is_postgresql
# revision identifiers, used by Alembic.
revision = "0034_virtual_collections_db_view"
down_revision = "0033_rom_file_and_hashes"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.create_table(
"collections_roms",
sa.Column("collection_id", sa.Integer(), nullable=False),
sa.Column("rom_id", sa.Integer(), nullable=False),
sa.Column(
"created_at",
sa.TIMESTAMP(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.Column(
"updated_at",
sa.TIMESTAMP(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.ForeignKeyConstraint(
["collection_id"], ["collections.id"], ondelete="CASCADE"
),
sa.ForeignKeyConstraint(["rom_id"], ["roms.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("collection_id", "rom_id"),
sa.UniqueConstraint("collection_id", "rom_id", name="unique_collection_rom"),
)
connection = op.get_bind()
if is_postgresql(connection):
connection.execute(sa.text("""
INSERT INTO collections_roms (collection_id, rom_id, created_at, updated_at)
SELECT c.id, rom_id::INT, NOW(), NOW()
FROM collections c,
LATERAL jsonb_array_elements_text(c.roms) AS rom_id
LEFT JOIN roms r ON rom_id::INT = r.id
WHERE r.id IS NOT NULL;
"""))
connection.execute(
sa.text("""
CREATE OR REPLACE VIEW virtual_collections AS
WITH genres_collection AS (
SELECT
r.id as rom_id,
r.path_cover_s as path_cover_s,
r.path_cover_l as path_cover_l,
jsonb_array_elements_text(igdb_metadata -> 'genres') as collection_name,
'genre' as collection_type
FROM roms r
WHERE igdb_metadata->'genres' IS NOT NULL
),
franchises_collection AS (
SELECT
r.id as rom_id,
r.path_cover_s as path_cover_s,
r.path_cover_l as path_cover_l,
jsonb_array_elements_text(igdb_metadata -> 'franchises') as collection_name,
'franchise' as collection_type
FROM roms r
WHERE igdb_metadata->'franchises' IS NOT NULL
),
collection_collection AS (
SELECT
r.id as rom_id,
r.path_cover_s as path_cover_s,
r.path_cover_l as path_cover_l,
jsonb_array_elements_text(igdb_metadata -> 'collections') as collection_name,
'collection' as collection_type
FROM roms r
WHERE igdb_metadata->'collections' IS NOT NULL
),
modes_collection AS (
SELECT
r.id as rom_id,
r.path_cover_s as path_cover_s,
r.path_cover_l as path_cover_l,
jsonb_array_elements_text(igdb_metadata -> 'game_modes') as collection_name,
'mode' as collection_type
FROM roms r
WHERE igdb_metadata->'game_modes' IS NOT NULL
),
companies_collection AS (
SELECT
r.id as rom_id,
r.path_cover_s as path_cover_s,
r.path_cover_l as path_cover_l,
jsonb_array_elements_text(igdb_metadata -> 'companies') as collection_name,
'company' as collection_type
FROM roms r
WHERE igdb_metadata->'companies' IS NOT NULL
)
SELECT
collection_name as name,
collection_type as type,
'Autogenerated ' || collection_type || ' collection' AS description,
NOW() AS created_at,
NOW() AS updated_at,
jsonb_agg(rom_id) as rom_ids,
jsonb_agg(path_cover_s) as path_covers_s,
jsonb_agg(path_cover_l) as path_covers_l
FROM (
SELECT * FROM genres_collection
UNION ALL
SELECT * FROM franchises_collection
UNION ALL
SELECT * FROM collection_collection
UNION ALL
SELECT * FROM modes_collection
UNION ALL
SELECT * FROM companies_collection
) combined
GROUP BY collection_type, collection_name
HAVING COUNT(DISTINCT rom_id) > 2
ORDER BY collection_type, collection_name;
"""), # nosec B608
)
else:
connection.execute(sa.text("""
INSERT INTO collections_roms (collection_id, rom_id, created_at, updated_at)
SELECT c.id, jt.rom_id, NOW(), NOW()
FROM collections c
JOIN JSON_TABLE(c.roms, '$[*]' COLUMNS (rom_id INT PATH '$')) AS jt
LEFT JOIN roms r ON jt.rom_id = r.id
WHERE r.id IS NOT NULL;
"""))
connection.execute(
sa.text("""
CREATE OR REPLACE VIEW virtual_collections AS
WITH genres AS (
SELECT
r.id as rom_id,
r.path_cover_s as path_cover_s,
r.path_cover_l as path_cover_l,
CONCAT(j.genre) as collection_name,
'genre' as collection_type
FROM
roms r
CROSS JOIN JSON_TABLE(
JSON_EXTRACT(igdb_metadata, '$.genres'),
'$[*]' COLUMNS (genre VARCHAR(255) PATH '$')
) j
WHERE
JSON_EXTRACT(igdb_metadata, '$.genres') IS NOT NULL
),
franchises AS (
SELECT
r.id as rom_id,
r.path_cover_s as path_cover_s,
r.path_cover_l as path_cover_l,
CONCAT(j.franchise) as collection_name,
'franchise' as collection_type
FROM
roms r
CROSS JOIN JSON_TABLE(
JSON_EXTRACT(igdb_metadata, '$.franchises'),
'$[*]' COLUMNS (franchise VARCHAR(255) PATH '$')
) j
WHERE
JSON_EXTRACT(igdb_metadata, '$.franchises') IS NOT NULL
),
collections AS (
SELECT
r.id as rom_id,
r.path_cover_s as path_cover_s,
r.path_cover_l as path_cover_l,
CONCAT(j.collection) as collection_name,
'collection' as collection_type
FROM
roms r
CROSS JOIN JSON_TABLE(
JSON_EXTRACT(igdb_metadata, '$.collections'),
'$[*]' COLUMNS (collection VARCHAR(255) PATH '$')
) j
WHERE
JSON_EXTRACT(igdb_metadata, '$.collections') IS NOT NULL
),
modes AS (
SELECT
r.id as rom_id,
r.path_cover_s as path_cover_s,
r.path_cover_l as path_cover_l,
CONCAT(j.mode) as collection_name,
'mode' as collection_type
FROM
roms r
CROSS JOIN JSON_TABLE(
JSON_EXTRACT(igdb_metadata, '$.game_modes'),
'$[*]' COLUMNS (mode VARCHAR(255) PATH '$')
) j
WHERE
JSON_EXTRACT(igdb_metadata, '$.game_modes') IS NOT NULL
),
companies AS (
SELECT
r.id as rom_id,
r.path_cover_s as path_cover_s,
r.path_cover_l as path_cover_l,
CONCAT(j.company) as collection_name,
'company' as collection_type
FROM
roms r
CROSS JOIN JSON_TABLE(
JSON_EXTRACT(igdb_metadata, '$.companies'),
'$[*]' COLUMNS (company VARCHAR(255) PATH '$')
) j
WHERE
JSON_EXTRACT(igdb_metadata, '$.companies') IS NOT NULL
)
SELECT
collection_name as name,
collection_type as type,
CONCAT('Autogenerated ', collection_name, ' collection') AS description,
NOW() AS created_at,
NOW() AS updated_at,
JSON_ARRAYAGG(rom_id) as rom_ids,
JSON_ARRAYAGG(path_cover_s) as path_covers_s,
JSON_ARRAYAGG(path_cover_l) as path_covers_l
FROM
(
SELECT * FROM genres
UNION ALL
SELECT * FROM franchises
UNION ALL
SELECT * FROM collections
UNION ALL
SELECT * FROM modes
UNION ALL
SELECT * FROM companies
) combined
GROUP BY collection_type, collection_name
HAVING COUNT(DISTINCT rom_id) > 2
ORDER BY collection_type, collection_name;
"""),
)
op.drop_column("collections", "roms")
def downgrade() -> None:
with op.batch_alter_table("collections", schema=None) as batch_op:
batch_op.add_column(sa.Column("roms", CustomJSON(), nullable=True))
connection = op.get_bind()
if is_postgresql(connection):
connection.execute(sa.text("""
UPDATE collections c
SET roms = COALESCE(
(SELECT jsonb_agg(rom_id)
FROM collections_roms cr
WHERE cr.collection_id = c.id),
'[]'::jsonb
);
"""))
else:
connection.execute(sa.text("""
UPDATE collections c
JOIN (
SELECT collection_id, IFNULL(JSON_ARRAYAGG(rom_id), JSON_ARRAY()) AS roms
FROM collections_roms
GROUP BY collection_id
) cr
ON c.id = cr.collection_id
SET c.roms = cr.roms;
"""))
with op.batch_alter_table("collections", schema=None) as batch_op:
batch_op.alter_column("roms", existing_type=CustomJSON(), nullable=False)
op.drop_table("collections_roms")
connection.execute(
sa.text("""
DROP VIEW virtual_collections;
"""),
)