From f005b04391257479d2bd967aa5d1543b925375a8 Mon Sep 17 00:00:00 2001 From: purplesmoke05 Date: Thu, 2 Apr 2026 21:32:38 +0900 Subject: [PATCH 1/3] Make listing fields required and remove orphan executable contracts --- app/api/routers/admin.py | 16 -- app/api/routers/company_info.py | 4 - app/model/db/base.py | 8 +- app/model/db/company.py | 4 +- app/model/db/listing.py | 11 +- app/model/db/notification.py | 4 +- .../4df7e2c1b8a3_v26_6_0_feature_1792.py | 215 ++++++++++++++++++ .../app/company_info_ListAllCompanies_test.py | 1 + .../app/eth_SendRawTransactionNowait_test.py | 1 + tests/app/eth_SendRawTransaction_test.py | 1 + .../app/eth_WaitForTransactionReceipt_test.py | 1 + ...tion_PositionCouponContractAddress_test.py | 1 + tests/app/position_PositionCoupon_test.py | 1 + ..._PositionMembershipContractAddress_test.py | 1 + tests/app/position_PositionMembership_test.py | 1 + ...ition_PositionShareContractAddress_test.py | 1 + ...ition_PositionShare_Lock_Event_GET_test.py | 1 + tests/app/position_PositionShare_test.py | 1 + ...ositionStraightBondContractAddress_test.py | 1 + ...ositionStraightBond_Lock_Event_GET_test.py | 1 + .../app/position_PositionStraightBond_test.py | 1 + tests/app/position_Position_test.py | 1 + ...en_bond_StraightBondTokenAddresses_test.py | 1 + ...oken_bond_StraightBondTokenDetails_test.py | 1 + .../app/token_bond_StraightBondTokens_test.py | 1 + .../token_coupon_CouponTokenAddresses_test.py | 1 + .../token_coupon_CouponTokenDetails_test.py | 1 + tests/app/token_coupon_CouponTokens_test.py | 1 + ...embership_MembershipTokenAddresses_test.py | 1 + ..._membership_MembershipTokenDetails_test.py | 1 + .../token_membership_MembershipTokens_test.py | 1 + .../token_share_ShareTokenAddresses_test.py | 1 + .../app/token_share_ShareTokenDetails_test.py | 1 + tests/app/token_share_ShareTokens_test.py | 1 + ...fer_history_ListAllTransferHistory_test.py | 1 + ...y_ListTokenTransferApprovalHistory_test.py | 1 + ...r_history_ListTokenTransferHistory_test.py | 1 + ...SearchTokenTransferApprovalHistory_test.py | 1 + ...history_SearchTokenTransferHistory_test.py | 1 + tests/migrations/upgrade_test.py | 147 +++++++++++- 40 files changed, 405 insertions(+), 36 deletions(-) create mode 100644 migrations/versions/4df7e2c1b8a3_v26_6_0_feature_1792.py diff --git a/app/api/routers/admin.py b/app/api/routers/admin.py index bb38328d1..12f5bf337 100644 --- a/app/api/routers/admin.py +++ b/app/api/routers/admin.py @@ -92,14 +92,6 @@ async def list_all_admin_tokens(async_session: DBAsyncSession): if TYPE_CHECKING: admin_tokens_for_type_check: list[RetrieveAdminTokenResponse] = [] for token in listed_tokens: - # TODO: Migrate listing.token_address to NOT NULL and update ORM typing. - assert token.token_address is not None - # TODO: Migrate listing.is_public to NOT NULL and update ORM typing. - assert token.is_public is not None - # TODO: Migrate listing.owner_address to NOT NULL and update ORM typing. - assert token.owner_address is not None - # TODO: Migrate listing.created to NOT NULL and update ORM typing. - assert token.created is not None admin_tokens_for_type_check.append( RetrieveAdminTokenResponse( id=token.id, @@ -325,14 +317,6 @@ async def retrieve_admin_token( if token is not None: listed_token_payload = token.json() if TYPE_CHECKING: - # TODO: Migrate listing.token_address to NOT NULL and update ORM typing. - assert token.token_address is not None - # TODO: Migrate listing.is_public to NOT NULL and update ORM typing. - assert token.is_public is not None - # TODO: Migrate listing.owner_address to NOT NULL and update ORM typing. - assert token.owner_address is not None - # TODO: Migrate listing.created to NOT NULL and update ORM typing. - assert token.created is not None _ = GenericSuccessResponse[RetrieveAdminTokenResponse]( meta=Success200MetaModel(code=200, message="OK"), data=RetrieveAdminTokenResponse( diff --git a/app/api/routers/company_info.py b/app/api/routers/company_info.py index 4b5136382..f4ef63068 100644 --- a/app/api/routers/company_info.py +++ b/app/api/routers/company_info.py @@ -179,8 +179,6 @@ async def list_all_companies( listing_owner_set.add(owner_address_is_cached[0]) continue - # TODO: Migrate listing.token_address to NOT NULL and update ORM typing - assert token[0].token_address is not None token_address = to_checksum_address(token[0].token_address) token_contract = AsyncContract.get_contract( contract_name="Ownable", address=token_address @@ -353,8 +351,6 @@ async def list_all_company_tokens( token_list: list[TokenDetailDict] = [] token_instance_list: list[TokenInstanceTypes] = [] for available_token in available_list: - # TODO: Migrate listing.token_address to NOT NULL and update ORM typing - assert available_token.token_address is not None token_address = to_checksum_address(available_token.token_address) token_info = await AsyncContract.call_function( contract=list_contract, diff --git a/app/model/db/base.py b/app/model/db/base.py index e8e9b588a..4e903d0b7 100644 --- a/app/model/db/base.py +++ b/app/model/db/base.py @@ -45,15 +45,15 @@ def naive_utcnow(): class Base(DeclarativeBase): if engine.name == "mysql": # NOTE:MySQLではDatetime型で小数秒桁を指定しない場合、整数秒しか保存されない - created: Mapped[datetime | None] = mapped_column( + created: Mapped[datetime] = mapped_column( MySQLDATETIME(fsp=6), default=naive_utcnow ) - modified: Mapped[datetime | None] = mapped_column( + modified: Mapped[datetime] = mapped_column( MySQLDATETIME(fsp=6), default=naive_utcnow, onupdate=naive_utcnow ) else: - created: Mapped[datetime | None] = mapped_column(DateTime, default=naive_utcnow) - modified: Mapped[datetime | None] = mapped_column( + created: Mapped[datetime] = mapped_column(DateTime, default=naive_utcnow) + modified: Mapped[datetime] = mapped_column( DateTime, default=naive_utcnow, onupdate=naive_utcnow ) diff --git a/app/model/db/company.py b/app/model/db/company.py index aaf2a65a9..9d2703539 100644 --- a/app/model/db/company.py +++ b/app/model/db/company.py @@ -49,11 +49,11 @@ class Company(Base): if engine.name == "mysql": # NOTE:MySQLではDatetime型で小数秒桁を指定しない場合、整数秒しか保存されない - created: Mapped[datetime | None] = mapped_column( + created: Mapped[datetime] = mapped_column( MySQLDATETIME(fsp=6), default=naive_utcnow, index=True ) else: - created: Mapped[datetime | None] = mapped_column( + created: Mapped[datetime] = mapped_column( DateTime, default=naive_utcnow, index=True ) __table_args__ = ( diff --git a/app/model/db/listing.py b/app/model/db/listing.py index 9cb3855a9..e85cbb892 100644 --- a/app/model/db/listing.py +++ b/app/model/db/listing.py @@ -39,21 +39,21 @@ class Listing(Base): __tablename__ = "listing" id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) - token_address: Mapped[str | None] = mapped_column( + token_address: Mapped[str] = mapped_column( String(256), index=True ) # トークンアドレス - is_public: Mapped[bool | None] = mapped_column(Boolean) + is_public: Mapped[bool] = mapped_column(Boolean) max_holding_quantity: Mapped[int | None] = mapped_column(BigInteger) # 最大保有数量 max_sell_amount: Mapped[int | None] = mapped_column(BigInteger) # 売却価格上限 - owner_address: Mapped[str | None] = mapped_column(String(256)) # 発行体アドレス + owner_address: Mapped[str] = mapped_column(String(256)) # 発行体アドレス if engine.name == "mysql": # NOTE:MySQLではDatetime型で小数秒桁を指定しない場合、整数秒しか保存されない - created: Mapped[datetime | None] = mapped_column( + created: Mapped[datetime] = mapped_column( MySQLDATETIME(fsp=6), default=naive_utcnow, index=True ) else: - created: Mapped[datetime | None] = mapped_column( + created: Mapped[datetime] = mapped_column( DateTime, default=naive_utcnow, index=True ) @@ -61,7 +61,6 @@ def __repr__(self): return "" % self.id def json(self): - assert self.created is not None return { "id": self.id, "token_address": self.token_address, diff --git a/app/model/db/notification.py b/app/model/db/notification.py index 622d5abc1..10b425d82 100644 --- a/app/model/db/notification.py +++ b/app/model/db/notification.py @@ -127,11 +127,11 @@ class Notification(Base): if engine.name == "mysql": # NOTE:MySQLではDatetime型で小数秒桁を指定しない場合、整数秒しか保存されない - created: Mapped[datetime | None] = mapped_column( + created: Mapped[datetime] = mapped_column( MySQLDATETIME(fsp=6), default=naive_utcnow, index=True ) else: - created: Mapped[datetime | None] = mapped_column( + created: Mapped[datetime] = mapped_column( DateTime, default=naive_utcnow, index=True ) diff --git a/migrations/versions/4df7e2c1b8a3_v26_6_0_feature_1792.py b/migrations/versions/4df7e2c1b8a3_v26_6_0_feature_1792.py new file mode 100644 index 000000000..eeaee3add --- /dev/null +++ b/migrations/versions/4df7e2c1b8a3_v26_6_0_feature_1792.py @@ -0,0 +1,215 @@ +"""v26_6_0_feature_1792 + +Revision ID: 4df7e2c1b8a3 +Revises: d7de2d20be69 +Create Date: 2026-04-02 20:04:29.343543 + +""" + +from datetime import UTC, datetime + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + + +from app.database import engine, get_db_schema + +# revision identifiers, used by Alembic. +revision = "4df7e2c1b8a3" +down_revision = "d7de2d20be69" +branch_labels = None +depends_on = None + + +AUDIT_TABLE_NAMES = [ + "account_tag", + "bond_token", + "chat_webhook", + "company", + "consume_coupon", + "coupon_token", + "executable_contract", + "idx_position_bond_block_number", + "idx_position_coupon_block_number", + "idx_position_membership_block_number", + "idx_position_share_block_number", + "idx_token_list_block_number", + "idx_transfer_approval_block_number", + "idx_transfer_block_number", + "listing", + "lock", + "locked_position", + "mail", + "membership_token", + "node", + "notification", + "notification_attribute_value", + "notification_block_number", + "position", + "public_account_list", + "share_token", + "token_holder", + "token_holders_list", + "token_list", + "token_list_register", + "transfer", + "transfer_approval", + "unlock", +] + + +def _get_audit_datetime_type(): + if engine.name == "mysql": + return mysql.DATETIME(fsp=6) + else: + return sa.DateTime() + + +def _get_audit_table(table_name: str): + datetime_type = _get_audit_datetime_type() + return sa.Table( + table_name, + sa.MetaData(), + sa.Column("created", datetime_type), + sa.Column("modified", datetime_type), + schema=get_db_schema(), + ) + + +def _backfill_audit_columns(table_name: str, current_dt: datetime): + table = _get_audit_table(table_name) + + op.get_bind().execute( + table.update().where(table.c.created.is_(None)).values(created=current_dt) + ) + op.get_bind().execute( + table.update().where(table.c.modified.is_(None)).values(modified=current_dt) + ) + + +def _alter_audit_columns(table_name: str, nullable: bool): + datetime_type = _get_audit_datetime_type() + op.alter_column( + table_name, + "created", + existing_type=datetime_type, + nullable=nullable, + schema=get_db_schema(), + ) + op.alter_column( + table_name, + "modified", + existing_type=datetime_type, + nullable=nullable, + schema=get_db_schema(), + ) + + +def upgrade(): + current_dt = datetime.now(UTC).replace(tzinfo=None) + datetime_type = _get_audit_datetime_type() + + ############################ + # Migration for listing + ############################ + listing = sa.Table( + "listing", + sa.MetaData(), + sa.Column("token_address", sa.String(length=256)), + sa.Column("is_public", sa.Boolean()), + sa.Column("owner_address", sa.String(length=256)), + sa.Column("created", datetime_type), + sa.Column("modified", datetime_type), + schema=get_db_schema(), + ) + executable_contract = sa.Table( + "executable_contract", + sa.MetaData(), + sa.Column("contract_address", sa.String(length=256)), + schema=get_db_schema(), + ) + listing_invalid_condition = sa.or_( + listing.c.token_address.is_(None), + listing.c.owner_address.is_(None), + ) + + op.get_bind().execute( + listing.update().where(listing.c.is_public.is_(None)).values(is_public=True) + ) + op.get_bind().execute(listing.delete().where(listing_invalid_condition)) + op.get_bind().execute( + executable_contract.delete().where( + sa.not_( + sa.exists( + sa.select(1) + .select_from(listing) + .where( + listing.c.token_address + == executable_contract.c.contract_address + ) + ) + ) + ) + ) + + ############################ + # Migration for created/modified columns + ############################ + for table_name in AUDIT_TABLE_NAMES: + _backfill_audit_columns(table_name, current_dt) + + ############################ + # Migration for listing not null columns + ############################ + op.alter_column( + "listing", + "token_address", + existing_type=sa.String(length=256), + nullable=False, + schema=get_db_schema(), + ) + op.alter_column( + "listing", + "is_public", + existing_type=sa.Boolean(), + nullable=False, + schema=get_db_schema(), + ) + op.alter_column( + "listing", + "owner_address", + existing_type=sa.String(length=256), + nullable=False, + schema=get_db_schema(), + ) + + for table_name in AUDIT_TABLE_NAMES: + _alter_audit_columns(table_name, nullable=False) + + +def downgrade(): + op.alter_column( + "listing", + "token_address", + existing_type=sa.String(length=256), + nullable=True, + schema=get_db_schema(), + ) + op.alter_column( + "listing", + "is_public", + existing_type=sa.Boolean(), + nullable=True, + schema=get_db_schema(), + ) + op.alter_column( + "listing", + "owner_address", + existing_type=sa.String(length=256), + nullable=True, + schema=get_db_schema(), + ) + + for table_name in AUDIT_TABLE_NAMES: + _alter_audit_columns(table_name, nullable=True) diff --git a/tests/app/company_info_ListAllCompanies_test.py b/tests/app/company_info_ListAllCompanies_test.py index 6f3eb4e05..ba7aa1b05 100644 --- a/tests/app/company_info_ListAllCompanies_test.py +++ b/tests/app/company_info_ListAllCompanies_test.py @@ -204,6 +204,7 @@ def list_token( listed_token.is_public = is_public listed_token.max_holding_quantity = 1 listed_token.max_sell_amount = 1000 + listed_token.owner_address = "0x0000000000000000000000000000000000000000" session.add(listed_token) @staticmethod diff --git a/tests/app/eth_SendRawTransactionNowait_test.py b/tests/app/eth_SendRawTransactionNowait_test.py index a60ebd2e6..06a7110f8 100644 --- a/tests/app/eth_SendRawTransactionNowait_test.py +++ b/tests/app/eth_SendRawTransactionNowait_test.py @@ -70,6 +70,7 @@ def listing_token(session: Session, token_address: str) -> None: listing.is_public = True listing.max_holding_quantity = 1 listing.max_sell_amount = 1000 + listing.owner_address = "0x0000000000000000000000000000000000000000" session.add(listing) diff --git a/tests/app/eth_SendRawTransaction_test.py b/tests/app/eth_SendRawTransaction_test.py index 4849409de..df588d525 100644 --- a/tests/app/eth_SendRawTransaction_test.py +++ b/tests/app/eth_SendRawTransaction_test.py @@ -71,6 +71,7 @@ def listing_token(session: Session, token_address: str) -> None: listing.is_public = True listing.max_holding_quantity = 1 listing.max_sell_amount = 1000 + listing.owner_address = "0x0000000000000000000000000000000000000000" session.add(listing) diff --git a/tests/app/eth_WaitForTransactionReceipt_test.py b/tests/app/eth_WaitForTransactionReceipt_test.py index 8d255cd2d..2aef7a07d 100644 --- a/tests/app/eth_WaitForTransactionReceipt_test.py +++ b/tests/app/eth_WaitForTransactionReceipt_test.py @@ -46,6 +46,7 @@ def listing_token(session: Session, token_address: str) -> None: listing.is_public = True listing.max_holding_quantity = 1 listing.max_sell_amount = 1000 + listing.owner_address = "0x0000000000000000000000000000000000000000" session.add(listing) diff --git a/tests/app/position_PositionCouponContractAddress_test.py b/tests/app/position_PositionCouponContractAddress_test.py index 1ce06aa9d..1aef6c38f 100644 --- a/tests/app/position_PositionCouponContractAddress_test.py +++ b/tests/app/position_PositionCouponContractAddress_test.py @@ -163,6 +163,7 @@ def list_token(token_address: str, session: Session) -> None: listed_token.is_public = True listed_token.max_holding_quantity = 1 listed_token.max_sell_amount = 1000 + listed_token.owner_address = "0x0000000000000000000000000000000000000000" session.add(listed_token) ########################################################################### diff --git a/tests/app/position_PositionCoupon_test.py b/tests/app/position_PositionCoupon_test.py index 5557cf7cf..07a3b71ed 100644 --- a/tests/app/position_PositionCoupon_test.py +++ b/tests/app/position_PositionCoupon_test.py @@ -245,6 +245,7 @@ def list_token(token_address: str, session: Session) -> None: listed_token.is_public = True listed_token.max_holding_quantity = 1 listed_token.max_sell_amount = 1000 + listed_token.owner_address = "0x0000000000000000000000000000000000000000" session.add(listed_token) ########################################################################### diff --git a/tests/app/position_PositionMembershipContractAddress_test.py b/tests/app/position_PositionMembershipContractAddress_test.py index 526903f01..7fc6682b2 100644 --- a/tests/app/position_PositionMembershipContractAddress_test.py +++ b/tests/app/position_PositionMembershipContractAddress_test.py @@ -145,6 +145,7 @@ def list_token(token_address: str, session: Session) -> None: listed_token.is_public = True listed_token.max_holding_quantity = 1 listed_token.max_sell_amount = 1000 + listed_token.owner_address = "0x0000000000000000000000000000000000000000" session.add(listed_token) ########################################################################### diff --git a/tests/app/position_PositionMembership_test.py b/tests/app/position_PositionMembership_test.py index 8fc98f842..d82b620de 100644 --- a/tests/app/position_PositionMembership_test.py +++ b/tests/app/position_PositionMembership_test.py @@ -205,6 +205,7 @@ def list_token(token_address: str, session: Session) -> None: listed_token.is_public = True listed_token.max_holding_quantity = 1 listed_token.max_sell_amount = 1000 + listed_token.owner_address = "0x0000000000000000000000000000000000000000" session.add(listed_token) ########################################################################### diff --git a/tests/app/position_PositionShareContractAddress_test.py b/tests/app/position_PositionShareContractAddress_test.py index 9d478a2bd..b5b3cd456 100644 --- a/tests/app/position_PositionShareContractAddress_test.py +++ b/tests/app/position_PositionShareContractAddress_test.py @@ -283,6 +283,7 @@ def list_token(token_address: str, session: Session) -> None: listed_token.is_public = True listed_token.max_holding_quantity = 1 listed_token.max_sell_amount = 1000 + listed_token.owner_address = "0x0000000000000000000000000000000000000000" session.add(listed_token) ########################################################################### diff --git a/tests/app/position_PositionShare_Lock_Event_GET_test.py b/tests/app/position_PositionShare_Lock_Event_GET_test.py index 29ecbdf94..8579f0d0e 100644 --- a/tests/app/position_PositionShare_Lock_Event_GET_test.py +++ b/tests/app/position_PositionShare_Lock_Event_GET_test.py @@ -98,6 +98,7 @@ def insert_listing(session: Session, token_address: str): _listing = Listing() _listing.token_address = token_address _listing.is_public = True + _listing.owner_address = "0x0000000000000000000000000000000000000000" session.add(_listing) @staticmethod diff --git a/tests/app/position_PositionShare_test.py b/tests/app/position_PositionShare_test.py index 501fc89d8..082b48536 100644 --- a/tests/app/position_PositionShare_test.py +++ b/tests/app/position_PositionShare_test.py @@ -289,6 +289,7 @@ def list_token(token_address: str, session: Session) -> None: listed_token.is_public = True listed_token.max_holding_quantity = 1 listed_token.max_sell_amount = 1000 + listed_token.owner_address = "0x0000000000000000000000000000000000000000" session.add(listed_token) ########################################################################### diff --git a/tests/app/position_PositionStraightBondContractAddress_test.py b/tests/app/position_PositionStraightBondContractAddress_test.py index c2191ccec..226bdee1a 100644 --- a/tests/app/position_PositionStraightBondContractAddress_test.py +++ b/tests/app/position_PositionStraightBondContractAddress_test.py @@ -296,6 +296,7 @@ def list_token(token_address: str, session: Session) -> None: listed_token.is_public = True listed_token.max_holding_quantity = 1 listed_token.max_sell_amount = 1000 + listed_token.owner_address = "0x0000000000000000000000000000000000000000" session.add(listed_token) ########################################################################### diff --git a/tests/app/position_PositionStraightBond_Lock_Event_GET_test.py b/tests/app/position_PositionStraightBond_Lock_Event_GET_test.py index 77f7df8fb..626a27d1a 100644 --- a/tests/app/position_PositionStraightBond_Lock_Event_GET_test.py +++ b/tests/app/position_PositionStraightBond_Lock_Event_GET_test.py @@ -113,6 +113,7 @@ def insert_listing(session: Session, token_address: str): _listing = Listing() _listing.token_address = token_address _listing.is_public = True + _listing.owner_address = "0x0000000000000000000000000000000000000000" session.add(_listing) @staticmethod diff --git a/tests/app/position_PositionStraightBond_test.py b/tests/app/position_PositionStraightBond_test.py index 7516702d4..65168aca8 100644 --- a/tests/app/position_PositionStraightBond_test.py +++ b/tests/app/position_PositionStraightBond_test.py @@ -302,6 +302,7 @@ def list_token(token_address: str, session: Session) -> None: listed_token.is_public = True listed_token.max_holding_quantity = 1 listed_token.max_sell_amount = 1000 + listed_token.owner_address = "0x0000000000000000000000000000000000000000" session.add(listed_token) ########################################################################### diff --git a/tests/app/position_Position_test.py b/tests/app/position_Position_test.py index 75bd0d40d..4dd35f14d 100644 --- a/tests/app/position_Position_test.py +++ b/tests/app/position_Position_test.py @@ -109,6 +109,7 @@ def list_token(token_address: str, session: Session) -> None: listed_token.is_public = True listed_token.max_holding_quantity = 1 listed_token.max_sell_amount = 1000 + listed_token.owner_address = "0x0000000000000000000000000000000000000000" session.add(listed_token) @staticmethod diff --git a/tests/app/token_bond_StraightBondTokenAddresses_test.py b/tests/app/token_bond_StraightBondTokenAddresses_test.py index a85c8ce4b..aa71a0b3d 100644 --- a/tests/app/token_bond_StraightBondTokenAddresses_test.py +++ b/tests/app/token_bond_StraightBondTokenAddresses_test.py @@ -99,6 +99,7 @@ def list_token(session: Session, token_address: str) -> None: listed_token.is_public = True listed_token.max_holding_quantity = 1 listed_token.max_sell_amount = 1000 + listed_token.owner_address = "0x0000000000000000000000000000000000000000" session.add(listed_token) token_list_item = IDXTokenListRegister() token_list_item.token_address = token_address diff --git a/tests/app/token_bond_StraightBondTokenDetails_test.py b/tests/app/token_bond_StraightBondTokenDetails_test.py index 2e1a4c30e..36e4e78b3 100644 --- a/tests/app/token_bond_StraightBondTokenDetails_test.py +++ b/tests/app/token_bond_StraightBondTokenDetails_test.py @@ -113,6 +113,7 @@ def list_token(session: Session, token_address: str) -> None: listed_token.is_public = True listed_token.max_holding_quantity = 1 listed_token.max_sell_amount = 1000 + listed_token.owner_address = "0x0000000000000000000000000000000000000000" session.add(listed_token) ########################################################################### diff --git a/tests/app/token_bond_StraightBondTokens_test.py b/tests/app/token_bond_StraightBondTokens_test.py index dec3c15d2..08c364f37 100644 --- a/tests/app/token_bond_StraightBondTokens_test.py +++ b/tests/app/token_bond_StraightBondTokens_test.py @@ -99,6 +99,7 @@ def list_token(session: Session, token_address: str) -> None: listed_token.is_public = True listed_token.max_holding_quantity = 1 listed_token.max_sell_amount = 1000 + listed_token.owner_address = "0x0000000000000000000000000000000000000000" session.add(listed_token) token_list_item = IDXTokenListRegister() token_list_item.token_address = token_address diff --git a/tests/app/token_coupon_CouponTokenAddresses_test.py b/tests/app/token_coupon_CouponTokenAddresses_test.py index 1c588d920..9adae395d 100644 --- a/tests/app/token_coupon_CouponTokenAddresses_test.py +++ b/tests/app/token_coupon_CouponTokenAddresses_test.py @@ -71,6 +71,7 @@ def list_token(session: Session, token_address: str) -> None: listed_token.is_public = True listed_token.max_holding_quantity = 1 listed_token.max_sell_amount = 1000 + listed_token.owner_address = "0x0000000000000000000000000000000000000000" session.add(listed_token) token_list_item = IDXTokenListRegister() token_list_item.token_address = token_address diff --git a/tests/app/token_coupon_CouponTokenDetails_test.py b/tests/app/token_coupon_CouponTokenDetails_test.py index 05cfb7551..b44f843ed 100644 --- a/tests/app/token_coupon_CouponTokenDetails_test.py +++ b/tests/app/token_coupon_CouponTokenDetails_test.py @@ -79,6 +79,7 @@ def list_token(session: Session, token_address: str) -> None: listed_token.is_public = True listed_token.max_holding_quantity = 1 listed_token.max_sell_amount = 1000 + listed_token.owner_address = "0x0000000000000000000000000000000000000000" session.add(listed_token) ########################################################################### diff --git a/tests/app/token_coupon_CouponTokens_test.py b/tests/app/token_coupon_CouponTokens_test.py index f0b8ffcd4..0bca4ce2f 100644 --- a/tests/app/token_coupon_CouponTokens_test.py +++ b/tests/app/token_coupon_CouponTokens_test.py @@ -71,6 +71,7 @@ def list_token(session: Session, token_address: str) -> None: listed_token.is_public = True listed_token.max_holding_quantity = 1 listed_token.max_sell_amount = 1000 + listed_token.owner_address = "0x0000000000000000000000000000000000000000" session.add(listed_token) token_list_item = IDXTokenListRegister() token_list_item.token_address = token_address diff --git a/tests/app/token_membership_MembershipTokenAddresses_test.py b/tests/app/token_membership_MembershipTokenAddresses_test.py index 0d3c2f6f6..3b86b2ecd 100644 --- a/tests/app/token_membership_MembershipTokenAddresses_test.py +++ b/tests/app/token_membership_MembershipTokenAddresses_test.py @@ -71,6 +71,7 @@ def list_token(session: Session, token_address: str) -> None: listed_token.is_public = True listed_token.max_holding_quantity = 1 listed_token.max_sell_amount = 1000 + listed_token.owner_address = "0x0000000000000000000000000000000000000000" session.add(listed_token) token_list_item = IDXTokenListRegister() token_list_item.token_address = token_address diff --git a/tests/app/token_membership_MembershipTokenDetails_test.py b/tests/app/token_membership_MembershipTokenDetails_test.py index e0eb5a592..cb4bf8573 100644 --- a/tests/app/token_membership_MembershipTokenDetails_test.py +++ b/tests/app/token_membership_MembershipTokenDetails_test.py @@ -80,6 +80,7 @@ def list_token(session: Session, token_address: str) -> None: listed_token.is_public = True listed_token.max_holding_quantity = 1 listed_token.max_sell_amount = 1000 + listed_token.owner_address = "0x0000000000000000000000000000000000000000" session.add(listed_token) ########################################################################### diff --git a/tests/app/token_membership_MembershipTokens_test.py b/tests/app/token_membership_MembershipTokens_test.py index c2f660750..34095a345 100644 --- a/tests/app/token_membership_MembershipTokens_test.py +++ b/tests/app/token_membership_MembershipTokens_test.py @@ -72,6 +72,7 @@ def list_token(session: Session, token_address: str) -> None: listed_token.is_public = True listed_token.max_holding_quantity = 1 listed_token.max_sell_amount = 1000 + listed_token.owner_address = "0x0000000000000000000000000000000000000000" session.add(listed_token) token_list_item = IDXTokenListRegister() token_list_item.token_address = token_address diff --git a/tests/app/token_share_ShareTokenAddresses_test.py b/tests/app/token_share_ShareTokenAddresses_test.py index 21ea23e25..a3fa3bfe5 100644 --- a/tests/app/token_share_ShareTokenAddresses_test.py +++ b/tests/app/token_share_ShareTokenAddresses_test.py @@ -77,6 +77,7 @@ def list_token(session: Session, token_address: str) -> None: listed_token.is_public = True listed_token.max_holding_quantity = 1 listed_token.max_sell_amount = 1000 + listed_token.owner_address = "0x0000000000000000000000000000000000000000" session.add(listed_token) token_list_item = IDXTokenListRegister() token_list_item.token_address = token_address diff --git a/tests/app/token_share_ShareTokenDetails_test.py b/tests/app/token_share_ShareTokenDetails_test.py index 3cc7ba544..502a5c737 100644 --- a/tests/app/token_share_ShareTokenDetails_test.py +++ b/tests/app/token_share_ShareTokenDetails_test.py @@ -109,6 +109,7 @@ def list_token(session: Session, token_address: str) -> None: listed_token.is_public = True listed_token.max_holding_quantity = 1 listed_token.max_sell_amount = 1000 + listed_token.owner_address = "0x0000000000000000000000000000000000000000" session.add(listed_token) ########################################################################### diff --git a/tests/app/token_share_ShareTokens_test.py b/tests/app/token_share_ShareTokens_test.py index e3b725291..3f742019c 100644 --- a/tests/app/token_share_ShareTokens_test.py +++ b/tests/app/token_share_ShareTokens_test.py @@ -78,6 +78,7 @@ def list_token(session: Session, token_address: str) -> None: listed_token.is_public = True listed_token.max_holding_quantity = 1 listed_token.max_sell_amount = 1000 + listed_token.owner_address = "0x0000000000000000000000000000000000000000" session.add(listed_token) token_list_item = IDXTokenListRegister() token_list_item.token_address = token_address diff --git a/tests/app/token_transfer_history_ListAllTransferHistory_test.py b/tests/app/token_transfer_history_ListAllTransferHistory_test.py index 59de04343..4e8cf535a 100644 --- a/tests/app/token_transfer_history_ListAllTransferHistory_test.py +++ b/tests/app/token_transfer_history_ListAllTransferHistory_test.py @@ -49,6 +49,7 @@ def insert_listing(session: Session, listing: dict[str, Any]): _listing = Listing() _listing.token_address = listing["token_address"] _listing.is_public = listing["is_public"] + _listing.owner_address = "0x0000000000000000000000000000000000000000" session.add(_listing) @staticmethod diff --git a/tests/app/token_transfer_history_ListTokenTransferApprovalHistory_test.py b/tests/app/token_transfer_history_ListTokenTransferApprovalHistory_test.py index 01cfa2cb1..03d9b4667 100644 --- a/tests/app/token_transfer_history_ListTokenTransferApprovalHistory_test.py +++ b/tests/app/token_transfer_history_ListTokenTransferApprovalHistory_test.py @@ -51,6 +51,7 @@ def insert_listing(session: Session, listing: dict[str, Any]): _listing = Listing() _listing.token_address = listing["token_address"] _listing.is_public = listing["is_public"] + _listing.owner_address = "0x0000000000000000000000000000000000000000" session.add(_listing) @staticmethod diff --git a/tests/app/token_transfer_history_ListTokenTransferHistory_test.py b/tests/app/token_transfer_history_ListTokenTransferHistory_test.py index 9b282bc6a..61348d110 100644 --- a/tests/app/token_transfer_history_ListTokenTransferHistory_test.py +++ b/tests/app/token_transfer_history_ListTokenTransferHistory_test.py @@ -47,6 +47,7 @@ def insert_listing(session: Session, listing: dict[str, Any]): _listing = Listing() _listing.token_address = listing["token_address"] _listing.is_public = listing["is_public"] + _listing.owner_address = "0x0000000000000000000000000000000000000000" session.add(_listing) @staticmethod diff --git a/tests/app/token_transfer_history_SearchTokenTransferApprovalHistory_test.py b/tests/app/token_transfer_history_SearchTokenTransferApprovalHistory_test.py index 566118828..d7c810153 100644 --- a/tests/app/token_transfer_history_SearchTokenTransferApprovalHistory_test.py +++ b/tests/app/token_transfer_history_SearchTokenTransferApprovalHistory_test.py @@ -52,6 +52,7 @@ def insert_listing(session: Session, listing: dict[str, Any]): _listing = Listing() _listing.token_address = listing["token_address"] _listing.is_public = listing["is_public"] + _listing.owner_address = "0x0000000000000000000000000000000000000000" session.add(_listing) @staticmethod diff --git a/tests/app/token_transfer_history_SearchTokenTransferHistory_test.py b/tests/app/token_transfer_history_SearchTokenTransferHistory_test.py index 3cb698f02..2235d2dc1 100644 --- a/tests/app/token_transfer_history_SearchTokenTransferHistory_test.py +++ b/tests/app/token_transfer_history_SearchTokenTransferHistory_test.py @@ -47,6 +47,7 @@ def insert_listing(session: Session, listing: dict[str, Any]): _listing = Listing() _listing.token_address = listing["token_address"] _listing.is_public = listing["is_public"] + _listing.owner_address = "0x0000000000000000000000000000000000000000" session.add(_listing) @staticmethod diff --git a/tests/migrations/upgrade_test.py b/tests/migrations/upgrade_test.py index 3fcc20fe7..b698728dc 100644 --- a/tests/migrations/upgrade_test.py +++ b/tests/migrations/upgrade_test.py @@ -26,10 +26,21 @@ from pytest import LogCaptureFixture, fixture, mark from pytest_alembic import MigrationContext -from sqlalchemy import Column, Integer, MetaData, String, Table, Text, insert, text +from sqlalchemy import ( + Column, + Integer, + MetaData, + String, + Table, + Text, + insert, + inspect, + select, + text, +) from sqlalchemy.engine import Engine -from app.database import engine +from app.database import engine, get_db_schema REVISION_22_3: Final = "a80595c53d52" REVISION_22_6: Final = "e8d970fdd886" @@ -42,6 +53,7 @@ REVISION_24_3: Final = "3d3b90fda898" REVISION_24_6: Final = "418af51b07b5" REVISION_25_6: Final = "9a28ed8d4afd" +REVISION_26_3: Final = "d7de2d20be69" REVISION_UP_TO_1_8 = [REVISION_22_3] REVISION_UP_TO_22_6 = REVISION_UP_TO_1_8 + [REVISION_22_6] @@ -1045,3 +1057,134 @@ def test_upgrade_v25_6( ) unlocks = list(unlocks) assert unlocks[0].data == {} + + def test_upgrade_v26_3_feature_1792( + self, alembic_runner: MigrationContext, caplog: LogCaptureFixture + ): + # 1. Migrate to v26.3 initial + alembic_runner.migrate_up_to(REVISION_26_3) + schema = get_db_schema() + listing_table_key = f"{schema}.listing" if schema else "listing" + executable_contract_table_key = ( + f"{schema}.executable_contract" if schema else "executable_contract" + ) + meta = MetaData() + meta.reflect(bind=engine) + + # 2. Insert test record + listing = meta.tables.get(listing_table_key) + assert listing is not None + stmt1 = insert(listing).values( + token_address="0x0000000000000000000000000000000000000011", + is_public=None, + max_holding_quantity=1, + max_sell_amount=1, + owner_address="0x0000000000000000000000000000000000000012", + created=None, + modified=None, + ) + stmt2 = insert(listing).values( + token_address=None, + is_public=True, + max_holding_quantity=1, + max_sell_amount=1, + owner_address="0x0000000000000000000000000000000000000013", + created=None, + modified=None, + ) + stmt3 = insert(listing).values( + token_address="0x0000000000000000000000000000000000000014", + is_public=False, + max_holding_quantity=1, + max_sell_amount=1, + owner_address=None, + created=None, + modified=None, + ) + + executable_contract = meta.tables.get(executable_contract_table_key) + assert executable_contract is not None + stmt4 = insert(executable_contract).values( + contract_address="0x0000000000000000000000000000000000000011", + created=None, + modified=None, + ) + stmt5 = insert(executable_contract).values( + contract_address="0x0000000000000000000000000000000000000014", + created=None, + modified=None, + ) + stmt6 = insert(executable_contract).values( + contract_address="0x0000000000000000000000000000000000000015", + created=None, + modified=None, + ) + + with engine.connect() as conn: + conn.execute(stmt1) + conn.execute(stmt2) + conn.execute(stmt3) + conn.execute(stmt4) + conn.execute(stmt5) + conn.execute(stmt6) + conn.commit() + + # 3. Run to head + alembic_runner.migrate_up_to("head") + + inspector = inspect(engine) + listing_columns = { + column["name"]: column + for column in inspector.get_columns("listing", schema=schema) + } + assert listing_columns["token_address"]["nullable"] is False + assert listing_columns["is_public"]["nullable"] is False + assert listing_columns["owner_address"]["nullable"] is False + assert listing_columns["created"]["nullable"] is False + assert listing_columns["modified"]["nullable"] is False + + executable_contract_columns = { + column["name"]: column + for column in inspector.get_columns("executable_contract", schema=schema) + } + assert executable_contract_columns["created"]["nullable"] is False + assert executable_contract_columns["modified"]["nullable"] is False + + post_meta = MetaData() + post_meta.reflect(bind=engine) + listing = post_meta.tables.get(listing_table_key) + executable_contract = post_meta.tables.get(executable_contract_table_key) + assert listing is not None + assert executable_contract is not None + + with engine.connect() as conn: + listing_rows = conn.execute( + select(listing).order_by(listing.c.id) + ).mappings() + listing_rows = list(listing_rows) + assert len(listing_rows) == 1 + assert ( + listing_rows[0]["token_address"] + == "0x0000000000000000000000000000000000000011" + ) + assert bool(listing_rows[0]["is_public"]) is True + assert ( + listing_rows[0]["owner_address"] + == "0x0000000000000000000000000000000000000012" + ) + assert listing_rows[0]["created"] is not None + assert listing_rows[0]["modified"] is not None + + executable_contract_rows = conn.execute( + select(executable_contract).order_by( + executable_contract.c.contract_address + ) + ).mappings() + executable_contract_rows = list(executable_contract_rows) + assert len(executable_contract_rows) == 1 + assert ( + executable_contract_rows[0]["contract_address"] + == "0x0000000000000000000000000000000000000011" + ) + assert executable_contract_rows[0]["created"] is not None + assert executable_contract_rows[0]["modified"] is not None From c30f1bbd8faf41821c2e13164f4770a91c550a0d Mon Sep 17 00:00:00 2001 From: purplesmoke05 Date: Fri, 3 Apr 2026 09:14:21 +0900 Subject: [PATCH 2/3] fix typecheck error --- app/api/routers/position.py | 6 ++---- app/model/blockchain/token.py | 1 - tests/app/token_TokenHolders_Search_test.py | 5 +++-- tests/app/token_TokenHolders_test.py | 5 +++-- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/app/api/routers/position.py b/app/api/routers/position.py index e570ff195..76183c2a3 100644 --- a/app/api/routers/position.py +++ b/app/api/routers/position.py @@ -850,10 +850,8 @@ async def get_list_from_index( if offset is not None: stmt = stmt.offset(offset) - # TODO: Migrate listing.token_address to NOT NULL and update ORM typing. - _token_position_list = cast( - Sequence[tuple[str, IDXPosition, IDXTokenInstance]], - (await async_session.execute(stmt)).tuples().all(), + _token_position_list: Sequence[tuple[str, IDXPosition, IDXTokenInstance]] = ( + (await async_session.execute(stmt)).tuples().all() ) position_list: list[PositionDataDict] = [] diff --git a/app/model/blockchain/token.py b/app/model/blockchain/token.py index f7fef74af..0e508e528 100644 --- a/app/model/blockchain/token.py +++ b/app/model/blockchain/token.py @@ -109,7 +109,6 @@ async def wrapper( now = datetime.now(UTC).replace(tzinfo=None) if ( cached_token - and cached_token.created is not None and cached_token.created + timedelta(seconds=TOKEN_CACHE_TTL) >= now ): # If cached data exists and doesn't expire, use cached data diff --git a/tests/app/token_TokenHolders_Search_test.py b/tests/app/token_TokenHolders_Search_test.py index c1ab9cab0..21d9859f6 100644 --- a/tests/app/token_TokenHolders_Search_test.py +++ b/tests/app/token_TokenHolders_Search_test.py @@ -64,8 +64,9 @@ def insert_position(session: Session, position: dict[str, Any]): _position.pending_transfer = position.get("pending_transfer") # nullable _position.exchange_balance = position.get("exchange_balance") # nullable _position.exchange_commitment = position.get("exchange_commitment") # nullable - if "created" in position: - _position.created = position.get("created") + created = position.get("created") + if created is not None: + _position.created = created session.add(_position) session.commit() diff --git a/tests/app/token_TokenHolders_test.py b/tests/app/token_TokenHolders_test.py index 66bafc54c..f6d696a91 100644 --- a/tests/app/token_TokenHolders_test.py +++ b/tests/app/token_TokenHolders_test.py @@ -64,8 +64,9 @@ def insert_position(session: Session, position: dict[str, Any]): _position.pending_transfer = position.get("pending_transfer") # nullable _position.exchange_balance = position.get("exchange_balance") # nullable _position.exchange_commitment = position.get("exchange_commitment") # nullable - if "created" in position: - _position.created = position.get("created") + created = position.get("created") + if created is not None: + _position.created = created session.add(_position) session.commit() From a04b066514d353218cf164fabd9ff8f7b8dd4b20 Mon Sep 17 00:00:00 2001 From: purplesmoke05 Date: Fri, 3 Apr 2026 13:44:28 +0900 Subject: [PATCH 3/3] fix migration test --- tests/migrations/upgrade_test.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/tests/migrations/upgrade_test.py b/tests/migrations/upgrade_test.py index b698728dc..1b789ebca 100644 --- a/tests/migrations/upgrade_test.py +++ b/tests/migrations/upgrade_test.py @@ -267,9 +267,9 @@ def test_upgrade_v22_9_to_v23_6_initial( meta = self.alembic_definition(REVISION_UP_TO_22_9, alembic_runner) if from_legacy_migration: self.reset_alembic_revision(engine) - self.create_migrate_version(engine, 47) + self.create_migrate_version(engine, 41) alembic_runner.config.alembic_config.set_main_option( - "sqlalchemy_migrate_version", str("47") + "sqlalchemy_migrate_version", str("41") ) assert "WARNING" not in caplog.text @@ -443,6 +443,25 @@ def test_upgrade_v23_6( conn.execute(stmt9) conn.commit() + # NOTE: listing data + listing = meta.tables.get("listing") + assert listing is not None + stmt1 = insert(listing).values( + token_address="token_address1", + is_public=True, + owner_address="owner_address1", + ) + stmt2 = insert(listing).values( + token_address="token_address2", + is_public=True, + owner_address="owner_address2", + ) + + with engine.connect() as conn: + conn.execute(stmt1) + conn.execute(stmt2) + conn.commit() + # NOTE: executable_contract data executable_contract = meta.tables.get("executable_contract") assert executable_contract is not None