Skip to content
Open
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
"""add pricing, applies, solution and approach to AIAsset

Revision ID: 9k2m4n6p8q0r
Revises: 8f9ac801a283
Create Date: 2025-11-25 17:00:00.000000

"""

from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = "9k2m4n6p8q0r"
down_revision: Union[str, None] = "8f9ac801a283"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


# AIAsset subclass table names
AI_ASSET_TABLES = [
"dataset",
"ml_model",
"experiment",
"computational_asset",
"case_study",
"publication",
]


def upgrade() -> None:
"""
1. Create solution and approach taxonomy tables
2. Add pricing_info and applies_to fields to AIAsset base tables
3. Create link tables for solution and approach many-to-many relationships
"""
# Create solution taxonomy table
op.create_table(
"solution",
sa.Column("identifier", sa.Integer(), nullable=False),
sa.Column("name", sa.String(200), nullable=False),
sa.Column("definition", sa.String(2000), nullable=True),
sa.Column("official", sa.Boolean(), nullable=False, server_default="0"),
sa.Column("parent_id", sa.Integer(), nullable=True),
sa.PrimaryKeyConstraint("identifier"),
sa.ForeignKeyConstraint(["parent_id"], ["solution.identifier"]),
sa.UniqueConstraint("name"),
)
op.create_index("ix_solution_name", "solution", ["name"])

# Create approach taxonomy table
op.create_table(
"approach",
sa.Column("identifier", sa.Integer(), nullable=False),
sa.Column("name", sa.String(200), nullable=False),
sa.Column("definition", sa.String(2000), nullable=True),
sa.Column("official", sa.Boolean(), nullable=False, server_default="0"),
sa.Column("parent_id", sa.Integer(), nullable=True),
sa.PrimaryKeyConstraint("identifier"),
sa.ForeignKeyConstraint(["parent_id"], ["approach.identifier"]),
sa.UniqueConstraint("name"),
)
op.create_index("ix_approach_name", "approach", ["name"])

# Add pricing_info and applies_to columns to each AIAsset subclass table
for table_name in AI_ASSET_TABLES:
op.add_column(table_name, sa.Column("pricing_info", sa.String(200), nullable=True))
op.add_column(table_name, sa.Column("applies_to", sa.String(200), nullable=True))

# Create link tables for solution many-to-many relationship
for table_name in AI_ASSET_TABLES:
link_table_name = f"solution_{table_name}_link"
op.create_table(
link_table_name,
sa.Column("from_identifier", sa.String(30), nullable=False),
sa.Column("linked_identifier", sa.Integer(), nullable=False),
sa.PrimaryKeyConstraint("from_identifier", "linked_identifier"),
sa.ForeignKeyConstraint(
["from_identifier"],
[f"{table_name}.identifier"],
name=f"{link_table_name}_ibfk_1",
ondelete="CASCADE",
onupdate="CASCADE",
),
sa.ForeignKeyConstraint(
["linked_identifier"],
["solution.identifier"],
name=f"{link_table_name}_ibfk_2",
onupdate="CASCADE",
),
)
op.create_index(f"ix_{link_table_name}_from", link_table_name, ["from_identifier"])
op.create_index(f"ix_{link_table_name}_linked", link_table_name, ["linked_identifier"])

# Create link tables for approach many-to-many relationship
for table_name in AI_ASSET_TABLES:
link_table_name = f"approach_{table_name}_link"
op.create_table(
link_table_name,
sa.Column("from_identifier", sa.String(30), nullable=False),
sa.Column("linked_identifier", sa.Integer(), nullable=False),
sa.PrimaryKeyConstraint("from_identifier", "linked_identifier"),
sa.ForeignKeyConstraint(
["from_identifier"],
[f"{table_name}.identifier"],
name=f"{link_table_name}_ibfk_1",
ondelete="CASCADE",
onupdate="CASCADE",
),
sa.ForeignKeyConstraint(
["linked_identifier"],
["approach.identifier"],
name=f"{link_table_name}_ibfk_2",
onupdate="CASCADE",
),
)
op.create_index(f"ix_{link_table_name}_from", link_table_name, ["from_identifier"])
op.create_index(f"ix_{link_table_name}_linked", link_table_name, ["linked_identifier"])


def downgrade() -> None:
"""
Reverse all changes from upgrade()
"""
# Drop approach link tables
for table_name in AI_ASSET_TABLES:
op.drop_table(f"approach_{table_name}_link")

# Drop solution link tables
for table_name in AI_ASSET_TABLES:
op.drop_table(f"solution_{table_name}_link")

# Remove pricing_info and applies_to columns
for table_name in AI_ASSET_TABLES:
op.drop_column(table_name, "applies_to")
op.drop_column(table_name, "pricing_info")

# Drop taxonomy tables
op.drop_table("approach")
op.drop_table("solution")
32 changes: 32 additions & 0 deletions alembic/alembic/versions/hjz8nxd6azj7_add_pid_to_publication.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""Add pid to publication

Revision ID: hjz8nxd6azj7
Revises: 79b2dda7e3be
Create Date: 2025-12-03 03:30:00.000000

"""

from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa
from sqlalchemy import Column, String

from database.model.field_length import NORMAL

# revision identifiers, used by Alembic.
revision: str = "hjz8nxd6azj7"
down_revision: Union[str, None] = "79b2dda7e3be"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
op.add_column(
table_name="publication",
column=Column("pid", String(NORMAL), nullable=True),
)


def downgrade() -> None:
op.drop_column(table_name="publication", column_name="pid")
49 changes: 49 additions & 0 deletions src/database/model/ai_asset/ai_asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
from sqlmodel import Field, Relationship

from database.model.ai_asset.ai_asset_table import AIAssetTable
from database.model.ai_asset.approach import Approach
from database.model.ai_asset.distribution import Distribution, distribution_factory
from database.model.ai_asset.license import License
from database.model.ai_asset.solution import Solution
from database.model.ai_resource.resource import AIResourceBase, AIResource
from database.model.field_length import NORMAL, IDENTIFIER_LENGTH
from database.model.helper_functions import many_to_many_link_factory
Expand All @@ -19,6 +21,7 @@
from database.model.serializers import (
AttributeSerializer,
FindByNameDeserializer,
FindByNameDeserializerList,
CastDeserializerList,
)

Expand All @@ -36,6 +39,18 @@ class AIAssetBase(AIResourceBase, metaclass=abc.ABCMeta):
default=None,
schema_extra={"example": "1.1.0"},
)
pricing_info: str | None = Field(
description="Information about the pricing model or cost structure for accessing this asset.",
max_length=NORMAL,
default=None,
schema_extra={"example": "Free for academic use, €50/month for commercial use"},
)
applies_to: str | None = Field(
description="The context or domain where this asset can be applied.",
max_length=NORMAL,
default=None,
schema_extra={"example": "Computer vision tasks, natural language processing"},
)


class AIAsset(AIAssetBase, AIResource, metaclass=abc.ABCMeta):
Expand All @@ -52,6 +67,9 @@ class AIAsset(AIAssetBase, AIResource, metaclass=abc.ABCMeta):
license_identifier: int | None = Field(foreign_key=License.__tablename__ + ".identifier")
license: Optional[License] = Relationship() # type: ignore[valid-type]

solution: list[Solution] = Relationship() # type: ignore[valid-type]
approach: list[Approach] = Relationship() # type: ignore[valid-type]

def __init_subclass__(cls):
"""
Fixing problems with the inheritance of relationships, and creating linking tables.
Expand Down Expand Up @@ -86,6 +104,20 @@ class RelationshipConfig(AIResource.RelationshipConfig):
default_factory_pydantic=list,
example=[],
)
solution: list[str] = ManyToMany(
description="The solution categories or types that this asset implements or provides.",
_serializer=AttributeSerializer("name"),
deserializer=FindByNameDeserializerList(Solution),
default_factory_pydantic=list,
example=["machine learning model", "data processing pipeline"],
)
approach: list[str] = ManyToMany(
description="The methodological approaches or techniques employed by this asset.",
_serializer=AttributeSerializer("name"),
deserializer=FindByNameDeserializerList(Approach),
default_factory_pydantic=list,
example=["deep learning theory", "representation learning"],
)

@classmethod
def update_relationships_asset(cls, relationships: dict):
Expand All @@ -109,6 +141,23 @@ def update_relationships_asset(cls, relationships: dict):
from_identifier_type=str,
to_identifier_type=str,
)

relationships["solution"].link_model = many_to_many_link_factory(
table_from=cls.__tablename__,
table_to=Solution.__tablename__,
table_prefix="solution",
from_identifier_type=str,
to_identifier_type=int,
)

relationships["approach"].link_model = many_to_many_link_factory(
table_from=cls.__tablename__,
table_to=Approach.__tablename__,
table_prefix="approach",
from_identifier_type=str,
to_identifier_type=int,
)

if cls.__tablename__ == "publication":

def get_identifier():
Expand Down
9 changes: 9 additions & 0 deletions src/database/model/ai_asset/approach.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from typing import Type

from database.model.named_relation import create_taxonomy, Taxonomy

Approach: Type[Taxonomy] = create_taxonomy(
class_name="Approach",
table_name="approach",
plural_name="approaches",
)
9 changes: 9 additions & 0 deletions src/database/model/ai_asset/solution.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from typing import Type

from database.model.named_relation import create_taxonomy, Taxonomy

Solution: Type[Taxonomy] = create_taxonomy(
class_name="Solution",
table_name="solution",
plural_name="solutions",
)
7 changes: 7 additions & 0 deletions src/database/model/knowledge_asset/publication.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ class PublicationBase(KnowledgeAssetBase):
schema_extra={"example": "http://dx.doi.org/10.1093/ajae/aaq063"},
default=None,
)
pid: str | None = Field(
description="A permanent identifier for the publication, for example a digital object "
"identifier (DOI). Ideally a url.",
max_length=NORMAL,
default=None,
schema_extra={"example": "https://doi.org/10.1093/ajae/aaq063"},
)
isbn: str | None = Field(
description="The International Standard Book Number, ISBN, used to identify published "
"books or, more rarely, journal issues.",
Expand Down
18 changes: 18 additions & 0 deletions src/tests/routers/resource_routers/test_router_organisation.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

from database.model.agent.contact import Contact
from database.model.agent.organisation import Organisation, Turnover,NumberOfEmployees
from database.model.ai_asset.solution import Solution
from database.model.ai_asset.approach import Approach
from database.session import DbSession

import pytest
Expand Down Expand Up @@ -37,6 +39,22 @@ def with_organisation_taxonomies():
],
session
)
synchronize(
Solution,
[
Solution(name="machine learning model", definition="A trained ML model", official=True, children=[]),
Solution(name="data processing pipeline", definition="Data transformation pipeline", official=True, children=[]),
],
session
)
synchronize(
Approach,
[
Approach(name="deep learning theory", definition="Theoretical foundations of deep learning", official=True, children=[]),
Approach(name="representation learning", definition="Learning data representations", official=True, children=[]),
],
session
)
session.commit()
yield

Expand Down
Loading