Skip to content

Commit 7c296a4

Browse files
authored
Fix/identifiers (#542)
2 parents 25b0df2 + 33fa729 commit 7c296a4

File tree

8 files changed

+130
-20
lines changed

8 files changed

+130
-20
lines changed
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
"""knowledge_asset_identifiers
2+
3+
Revision ID: 42f747800456
4+
Revises: 459323683348
5+
Create Date: 2025-06-24 13:05:59.728619
6+
7+
"""
8+
9+
import logging
10+
11+
# no user input
12+
# ruff: noqa: S608
13+
from typing import Sequence, Union, NamedTuple
14+
15+
from alembic import op
16+
from sqlalchemy import text
17+
18+
from database.session import DbSession
19+
20+
21+
# revision identifiers, used by Alembic.
22+
revision: str = "42f747800456"
23+
down_revision: Union[str, None] = "459323683348"
24+
branch_labels: Union[str, Sequence[str], None] = None
25+
depends_on: Union[str, Sequence[str], None] = None
26+
27+
logger = logging.getLogger("alembic")
28+
29+
30+
class ParentTable(NamedTuple):
31+
name: str
32+
fk_identifier: str
33+
children: list[str]
34+
35+
36+
def upgrade() -> None:
37+
# Simply forgot to update knowledge asset identifiers last time.
38+
# This script follows the same setup as the last, but only applies to knowledge asset,
39+
# which at this point only has `publication` as a child referencing it.
40+
41+
# Fetch foreign key constraints so we can drop them and add them back later.
42+
with DbSession() as session:
43+
logger.info("Fetching existing foreign key constraints.")
44+
constraints = session.execute(
45+
text(
46+
"SELECT refs.CONSTRAINT_NAME, refs.DELETE_RULE, kcu.TABLE_NAME, kcu.COLUMN_NAME, kcu.REFERENCED_TABLE_NAME, kcu.REFERENCED_COLUMN_NAME "
47+
"FROM information_schema.REFERENTIAL_CONSTRAINTS as refs "
48+
"JOIN information_schema.KEY_COLUMN_USAGE as kcu "
49+
"ON refs.CONSTRAINT_NAME=kcu.CONSTRAINT_NAME "
50+
f"WHERE refs.REFERENCED_TABLE_NAME='knowledge_asset';"
51+
)
52+
)
53+
constraints = list(constraints)
54+
logger.info(f"Dropping {len(constraints)} foreign key constraints.")
55+
for constraint, delete_rule, from_table, from_column, to_table, to_column in constraints:
56+
op.execute(f"ALTER TABLE {from_table} DROP FOREIGN KEY {constraint}")
57+
58+
# Without the foreign key constraints in place, we can update the columns.
59+
updated_columns = set()
60+
for constraint, delete_rule, from_table, from_column, to_table, to_column in constraints:
61+
for table, column in [(to_table, to_column), (from_table, from_column)]:
62+
if (table, column) not in updated_columns:
63+
logger.info(f"Altering {table}.{column} to VARCHAR(30) COLLATE utf8_bin.")
64+
op.execute(
65+
f"ALTER TABLE {table} CHANGE COLUMN {column} {column} VARCHAR(30) COLLATE utf8_bin;"
66+
)
67+
updated_columns.add((table, column))
68+
69+
for constraint, delete_rule, from_table, from_column, to_table, to_column in constraints:
70+
logger.info(f"Adding back constraint {constraint}.")
71+
op.execute(
72+
f"ALTER TABLE {from_table} "
73+
f"ADD CONSTRAINT {constraint} "
74+
f"FOREIGN KEY ({from_column}) REFERENCES {to_table}({to_column}) "
75+
f"ON DELETE {delete_rule} "
76+
f"ON UPDATE CASCADE;"
77+
)
78+
79+
# There was a bug which resulted in knowledge asset identifiers not being generated
80+
logger.info("Creating Identifiers of Knowledge Assets")
81+
op.execute("INSERT INTO knowledge_asset SELECT identifier, 'publication' FROM publication; ")
82+
logger.info("Link publications to their parents")
83+
op.execute("UPDATE publication SET knowledge_asset_id=identifier; ")
84+
85+
86+
def downgrade() -> None:
87+
pass

src/database/deletion/triggers.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,15 @@ def create_identifier_synchronization_triggers(dialect: str = "mysql"):
2525
from database.model.ai_asset.ai_asset_table import AIAssetTable
2626
from database.model.ai_resource.resource import AIResource
2727
from database.model.ai_resource.resource_table import AIResourceORM
28+
from database.model.knowledge_asset.knowledge_asset import KnowledgeAsset
29+
from database.model.knowledge_asset.knowledge_asset_table import KnowledgeAssetTable
2830

2931
triggers = []
3032
for parent_class, reference_table in [
3133
(AIResource, AIResourceORM),
3234
(AIAsset, AIAssetTable),
3335
(Agent, AgentTable),
36+
(KnowledgeAsset, KnowledgeAssetTable),
3437
]:
3538
parent_table_name = reference_table.__tablename__ # type: ignore[attr-defined]
3639
for cls in non_abstract_subclasses(parent_class):

src/database/model/ai_asset/distribution.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22
from typing import Type
33

44
from pydantic import create_model
5-
from sqlalchemy import Column, Integer, ForeignKey
5+
from sqlalchemy import Column, Integer, ForeignKey, String
66
from sqlmodel import Field
77

88
from database.model.concept.concept import AIoDConceptBase
99
from database.model.field_length import LONG, NORMAL, SHORT
10+
from database.model.field_length import IDENTIFIER_LENGTH
1011

1112

1213
class DistributionBase(AIoDConceptBase):
@@ -60,10 +61,11 @@ def distribution_factory(table_from: str, distribution_name="distribution") -> T
6061
__cls_kwargs__=dict(table=True),
6162
identifier=(int | None, Field(primary_key=True)),
6263
asset_identifier=(
63-
int | None,
64+
str | None,
6465
Field(
6566
sa_column=Column(
66-
Integer, ForeignKey(table_from + ".identifier", ondelete="CASCADE")
67+
String(IDENTIFIER_LENGTH),
68+
ForeignKey(table_from + ".identifier", ondelete="CASCADE"),
6769
)
6870
),
6971
),

src/database/model/ai_resource/note.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
from typing import Type
22

33
from pydantic import create_model
4-
from sqlalchemy import Column, Integer, ForeignKey
4+
from sqlalchemy import Column, Integer, ForeignKey, String
55
from sqlmodel import Field, SQLModel
66

77
from database.model.field_length import VERY_LONG
8+
from database.model.field_length import IDENTIFIER_LENGTH
89

910

1011
class NoteBase(SQLModel):
@@ -24,10 +25,11 @@ def note_factory(table_from: str) -> Type:
2425
__cls_kwargs__=dict(table=True),
2526
identifier=(int | None, Field(primary_key=True)),
2627
linked_identifier=(
27-
int | None,
28+
str | None,
2829
Field(
2930
sa_column=Column(
30-
Integer, ForeignKey(table_from + ".identifier", ondelete="CASCADE")
31+
String(IDENTIFIER_LENGTH),
32+
ForeignKey(table_from + ".identifier", ondelete="CASCADE"),
3133
)
3234
),
3335
),

src/database/model/event/event.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,9 @@ class Event(EventBase, AIResource, table=True): # type: ignore [call-arg]
7676
max_length=IDENTIFIER_LENGTH, foreign_key=AgentTable.__tablename__ + ".identifier"
7777
)
7878
organiser: Optional[AgentTable] = Relationship()
79-
status_identifier: str | None = Field(
80-
max_length=IDENTIFIER_LENGTH, foreign_key=EventStatus.__tablename__ + ".identifier"
81-
)
79+
status_identifier: int | None = Field(foreign_key=EventStatus.__tablename__ + ".identifier")
8280
status: Optional[EventStatus] = Relationship()
83-
mode_identifier: str | None = Field(
84-
max_length=IDENTIFIER_LENGTH, foreign_key=EventMode.__tablename__ + ".identifier"
85-
)
81+
mode_identifier: int | None = Field(foreign_key=EventMode.__tablename__ + ".identifier")
8682
mode: Optional[EventMode] = Relationship()
8783

8884
class RelationshipConfig(AIResource.RelationshipConfig):

src/database/model/knowledge_asset/knowledge_asset.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,29 @@
11
import copy
22

3+
from sqlalchemy import ForeignKey
34
from sqlmodel import Field, Relationship
45

56
from database.model.ai_asset.ai_asset import AIAssetBase, AIAsset
67
from database.model.ai_asset.ai_asset_table import AIAssetTable
78
from database.model.helper_functions import many_to_many_link_factory
89
from database.model.knowledge_asset.knowledge_asset_table import KnowledgeAssetTable
9-
from database.model.relationships import ManyToMany
10+
from database.model.relationships import ManyToMany, OneToOne
1011
from database.model.serializers import AttributeSerializer, FindByIdentifierDeserializerList
12+
from database.model.field_length import IDENTIFIER_LENGTH
1113

1214

1315
class KnowledgeAssetBase(AIAssetBase):
1416
pass
1517

1618

1719
class KnowledgeAsset(KnowledgeAssetBase, AIAsset):
18-
knowledge_asset_id: int | None = Field(
19-
foreign_key=KnowledgeAssetTable.__tablename__ + ".identifier", unique=True, index=True
20+
knowledge_asset_id: str | None = Field(
21+
max_length=IDENTIFIER_LENGTH,
22+
# Initializing `sa_column` instead doesn't work. Perhaps because it'd be used twice?
23+
sa_column_args=[
24+
ForeignKey(KnowledgeAssetTable.__tablename__ + ".identifier", onupdate="CASCADE")
25+
],
26+
sa_column_kwargs=dict(nullable=True, index=True, unique=True),
2027
)
2128
knowledge_asset_identifier: KnowledgeAssetTable | None = Relationship()
2229

@@ -50,3 +57,10 @@ class RelationshipConfig(AIAsset.RelationshipConfig):
5057
example=[],
5158
default_factory_pydantic=list,
5259
)
60+
knowledge_asset_identifier: str | None = OneToOne(
61+
identifier_name="knowledge_asset_id",
62+
_serializer=AttributeSerializer("identifier"),
63+
include_in_create=False,
64+
default_factory_orm=lambda type_: KnowledgeAssetTable(type=type_),
65+
on_delete_trigger_deletion_by="knowledge_asset_id",
66+
)
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
from sqlmodel import Field, SQLModel
22

33
from database.model.field_length import IDENTIFIER_LENGTH
4+
from database.identifiers import create_id_generator
45

56

67
class KnowledgeAssetTable(SQLModel, table=True): # type: ignore [call-arg]
78
__tablename__ = "knowledge_asset"
8-
identifier: str = Field(max_length=IDENTIFIER_LENGTH, default=None, primary_key=True)
9+
identifier: str = Field(
10+
max_length=IDENTIFIER_LENGTH,
11+
default_factory=create_id_generator(),
12+
primary_key=True,
13+
)
914
type: str = Field(
1015
description="The name of the table of the knowledge asset. E.g. 'publication'"
1116
)

src/database/model/models_and_experiments/runnable_distribution.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
from typing import Type
22

33
from pydantic import create_model
4-
from sqlalchemy import Column, Integer, ForeignKey
4+
from sqlalchemy import Column, Integer, ForeignKey, String
55
from sqlmodel import Field
66

77
from database.model.ai_asset.distribution import DistributionBase
8-
from database.model.field_length import NORMAL, LONG
8+
from database.model.field_length import NORMAL, LONG, IDENTIFIER_LENGTH
99

1010

1111
class RunnableDistributionBase(DistributionBase):
@@ -82,10 +82,11 @@ def runnable_distribution_factory(table_from: str, distribution_name="distributi
8282
__cls_kwargs__=dict(table=True),
8383
identifier=(int | None, Field(primary_key=True)),
8484
asset_identifier=(
85-
int | None,
85+
str | None,
8686
Field(
8787
sa_column=Column(
88-
Integer, ForeignKey(table_from + ".identifier", ondelete="CASCADE")
88+
String(IDENTIFIER_LENGTH),
89+
ForeignKey(table_from + ".identifier", ondelete="CASCADE"),
8990
)
9091
),
9192
),

0 commit comments

Comments
 (0)