Files
Code_of_Conquest/api/app/services/database_init.py

1258 lines
41 KiB
Python

"""
Database Initialization Service.
This service handles programmatic creation of Appwrite database tables,
including schema definition, column creation, and index setup.
"""
import os
import time
from typing import List, Dict, Any, Optional
from appwrite.client import Client
from appwrite.services.tables_db import TablesDB
from appwrite.exception import AppwriteException
from app.utils.logging import get_logger
logger = get_logger(__file__)
class DatabaseInitService:
"""
Service for initializing Appwrite database tables.
This service provides methods to:
- Create tables if they don't exist
- Define table schemas (columns/attributes)
- Create indexes for efficient querying
- Validate existing table structures
"""
def __init__(self):
"""
Initialize the database initialization service.
Reads configuration from environment variables:
- APPWRITE_ENDPOINT: Appwrite API endpoint
- APPWRITE_PROJECT_ID: Appwrite project ID
- APPWRITE_API_KEY: Appwrite API key
- APPWRITE_DATABASE_ID: Appwrite database ID
"""
self.endpoint = os.getenv('APPWRITE_ENDPOINT')
self.project_id = os.getenv('APPWRITE_PROJECT_ID')
self.api_key = os.getenv('APPWRITE_API_KEY')
self.database_id = os.getenv('APPWRITE_DATABASE_ID', 'main')
if not all([self.endpoint, self.project_id, self.api_key]):
logger.error("Missing Appwrite configuration in environment variables")
raise ValueError("Appwrite configuration incomplete. Check APPWRITE_* environment variables.")
# Initialize Appwrite client
self.client = Client()
self.client.set_endpoint(self.endpoint)
self.client.set_project(self.project_id)
self.client.set_key(self.api_key)
# Initialize TablesDB service
self.tables_db = TablesDB(self.client)
logger.info("DatabaseInitService initialized", database_id=self.database_id)
def init_all_tables(self) -> Dict[str, bool]:
"""
Initialize all application tables.
Returns:
Dictionary mapping table names to success status
"""
results = {}
logger.info("Initializing all database tables")
# Initialize characters table
try:
self.init_characters_table()
results['characters'] = True
logger.info("Characters table initialized successfully")
except Exception as e:
logger.error("Failed to initialize characters table", error=str(e))
results['characters'] = False
# Initialize game_sessions table
try:
self.init_game_sessions_table()
results['game_sessions'] = True
logger.info("Game sessions table initialized successfully")
except Exception as e:
logger.error("Failed to initialize game_sessions table", error=str(e))
results['game_sessions'] = False
# Initialize ai_usage_logs table
try:
self.init_ai_usage_logs_table()
results['ai_usage_logs'] = True
logger.info("AI usage logs table initialized successfully")
except Exception as e:
logger.error("Failed to initialize ai_usage_logs table", error=str(e))
results['ai_usage_logs'] = False
# Initialize chat_messages table
try:
self.init_chat_messages_table()
results['chat_messages'] = True
logger.info("Chat messages table initialized successfully")
except Exception as e:
logger.error("Failed to initialize chat_messages table", error=str(e))
results['chat_messages'] = False
# Initialize combat_encounters table
try:
self.init_combat_encounters_table()
results['combat_encounters'] = True
logger.info("Combat encounters table initialized successfully")
except Exception as e:
logger.error("Failed to initialize combat_encounters table", error=str(e))
results['combat_encounters'] = False
# Initialize combat_rounds table
try:
self.init_combat_rounds_table()
results['combat_rounds'] = True
logger.info("Combat rounds table initialized successfully")
except Exception as e:
logger.error("Failed to initialize combat_rounds table", error=str(e))
results['combat_rounds'] = False
success_count = sum(1 for v in results.values() if v)
total_count = len(results)
logger.info("Table initialization complete",
success=success_count,
total=total_count,
results=results)
return results
def init_characters_table(self) -> bool:
"""
Initialize the characters table.
Table schema:
- userId (string, required): Owner's user ID
- characterData (string, required): JSON-serialized character data
- is_active (boolean, default=True): Soft delete flag
- created_at (datetime): Auto-managed creation timestamp
- updated_at (datetime): Auto-managed update timestamp
Indexes:
- userId: For general user queries
- userId + is_active: Composite index for efficiently fetching active characters
Returns:
True if successful
Raises:
AppwriteException: If table creation fails
"""
table_id = 'characters'
logger.info("Initializing characters table", table_id=table_id)
try:
# Check if table already exists
try:
self.tables_db.get_table(
database_id=self.database_id,
table_id=table_id
)
logger.info("Characters table already exists", table_id=table_id)
return True
except AppwriteException as e:
if e.code != 404:
raise
logger.info("Characters table does not exist, creating...")
# Create table
logger.info("Creating characters table")
table = self.tables_db.create_table(
database_id=self.database_id,
table_id=table_id,
name='Characters'
)
logger.info("Characters table created", table_id=table['$id'])
# Create columns
self._create_column(
table_id=table_id,
column_id='userId',
column_type='string',
size=255,
required=True
)
self._create_column(
table_id=table_id,
column_id='characterData',
column_type='string',
size=65535, # Large text field for JSON data
required=True
)
self._create_column(
table_id=table_id,
column_id='is_active',
column_type='boolean',
required=False, # Cannot be required if we want a default value
default=True
)
# Note: created_at and updated_at are auto-managed by DatabaseService
# through the _parse_row method and timestamp updates
# Wait for columns to fully propagate in Appwrite before creating indexes
logger.info("Waiting for columns to propagate before creating indexes...")
time.sleep(2)
# Create indexes for efficient querying
# Note: Individual userId index for general user queries
self._create_index(
table_id=table_id,
index_id='idx_userId',
index_type='key',
attributes=['userId']
)
# Composite index for the most common query pattern:
# Query.equal('userId', user_id) + Query.equal('is_active', True)
# This single composite index covers both conditions efficiently
self._create_index(
table_id=table_id,
index_id='idx_userId_is_active',
index_type='key',
attributes=['userId', 'is_active']
)
logger.info("Characters table initialized successfully", table_id=table_id)
return True
except AppwriteException as e:
logger.error("Failed to initialize characters table",
table_id=table_id,
error=str(e),
code=e.code)
raise
def init_game_sessions_table(self) -> bool:
"""
Initialize the game_sessions table.
Table schema:
- userId (string, required): Owner's user ID
- characterId (string, required): Character ID for this session
- sessionData (string, required): JSON-serialized session data
- status (string, required): Session status (active, completed, abandoned)
- sessionType (string, required): Session type (solo, multiplayer)
Indexes:
- userId: For user session queries
- userId + status: For active session queries
- characterId: For character session lookups
Returns:
True if successful
Raises:
AppwriteException: If table creation fails
"""
table_id = 'game_sessions'
logger.info("Initializing game_sessions table", table_id=table_id)
try:
# Check if table already exists
try:
self.tables_db.get_table(
database_id=self.database_id,
table_id=table_id
)
logger.info("Game sessions table already exists", table_id=table_id)
return True
except AppwriteException as e:
if e.code != 404:
raise
logger.info("Game sessions table does not exist, creating...")
# Create table
logger.info("Creating game_sessions table")
table = self.tables_db.create_table(
database_id=self.database_id,
table_id=table_id,
name='Game Sessions'
)
logger.info("Game sessions table created", table_id=table['$id'])
# Create columns
self._create_column(
table_id=table_id,
column_id='userId',
column_type='string',
size=255,
required=True
)
self._create_column(
table_id=table_id,
column_id='characterId',
column_type='string',
size=255,
required=True
)
self._create_column(
table_id=table_id,
column_id='sessionData',
column_type='string',
size=65535, # Large text field for JSON data
required=True
)
self._create_column(
table_id=table_id,
column_id='status',
column_type='string',
size=50,
required=True
)
self._create_column(
table_id=table_id,
column_id='sessionType',
column_type='string',
size=50,
required=True
)
# Wait for columns to fully propagate
logger.info("Waiting for columns to propagate before creating indexes...")
time.sleep(2)
# Create indexes
self._create_index(
table_id=table_id,
index_id='idx_userId',
index_type='key',
attributes=['userId']
)
self._create_index(
table_id=table_id,
index_id='idx_userId_status',
index_type='key',
attributes=['userId', 'status']
)
self._create_index(
table_id=table_id,
index_id='idx_characterId',
index_type='key',
attributes=['characterId']
)
logger.info("Game sessions table initialized successfully", table_id=table_id)
return True
except AppwriteException as e:
logger.error("Failed to initialize game_sessions table",
table_id=table_id,
error=str(e),
code=e.code)
raise
def init_ai_usage_logs_table(self) -> bool:
"""
Initialize the ai_usage_logs table for tracking AI API usage and costs.
Table schema:
- user_id (string, required): User who made the request
- timestamp (string, required): ISO timestamp of the request
- model (string, required): Model identifier
- tokens_input (integer, required): Input token count
- tokens_output (integer, required): Output token count
- tokens_total (integer, required): Total token count
- estimated_cost (float, required): Estimated cost in USD
- task_type (string, required): Type of task
- session_id (string, optional): Game session ID
- character_id (string, optional): Character ID
- request_duration_ms (integer): Request duration in milliseconds
- success (boolean): Whether request succeeded
- error_message (string, optional): Error message if failed
Indexes:
- user_id: For user usage queries
- timestamp: For date range queries
- user_id + timestamp: Composite for user date range queries
Returns:
True if successful
Raises:
AppwriteException: If table creation fails
"""
table_id = 'ai_usage_logs'
logger.info("Initializing ai_usage_logs table", table_id=table_id)
try:
# Check if table already exists
try:
self.tables_db.get_table(
database_id=self.database_id,
table_id=table_id
)
logger.info("AI usage logs table already exists", table_id=table_id)
return True
except AppwriteException as e:
if e.code != 404:
raise
logger.info("AI usage logs table does not exist, creating...")
# Create table
logger.info("Creating ai_usage_logs table")
table = self.tables_db.create_table(
database_id=self.database_id,
table_id=table_id,
name='AI Usage Logs'
)
logger.info("AI usage logs table created", table_id=table['$id'])
# Create columns
self._create_column(
table_id=table_id,
column_id='user_id',
column_type='string',
size=255,
required=True
)
self._create_column(
table_id=table_id,
column_id='timestamp',
column_type='string',
size=50, # ISO timestamp format
required=True
)
self._create_column(
table_id=table_id,
column_id='model',
column_type='string',
size=255,
required=True
)
self._create_column(
table_id=table_id,
column_id='tokens_input',
column_type='integer',
required=True
)
self._create_column(
table_id=table_id,
column_id='tokens_output',
column_type='integer',
required=True
)
self._create_column(
table_id=table_id,
column_id='tokens_total',
column_type='integer',
required=True
)
self._create_column(
table_id=table_id,
column_id='estimated_cost',
column_type='float',
required=True
)
self._create_column(
table_id=table_id,
column_id='task_type',
column_type='string',
size=50,
required=True
)
self._create_column(
table_id=table_id,
column_id='session_id',
column_type='string',
size=255,
required=False
)
self._create_column(
table_id=table_id,
column_id='character_id',
column_type='string',
size=255,
required=False
)
self._create_column(
table_id=table_id,
column_id='request_duration_ms',
column_type='integer',
required=False,
default=0
)
self._create_column(
table_id=table_id,
column_id='success',
column_type='boolean',
required=False,
default=True
)
self._create_column(
table_id=table_id,
column_id='error_message',
column_type='string',
size=1000,
required=False
)
# Wait for columns to fully propagate
logger.info("Waiting for columns to propagate before creating indexes...")
time.sleep(2)
# Create indexes
self._create_index(
table_id=table_id,
index_id='idx_user_id',
index_type='key',
attributes=['user_id']
)
self._create_index(
table_id=table_id,
index_id='idx_timestamp',
index_type='key',
attributes=['timestamp']
)
self._create_index(
table_id=table_id,
index_id='idx_user_id_timestamp',
index_type='key',
attributes=['user_id', 'timestamp']
)
logger.info("AI usage logs table initialized successfully", table_id=table_id)
return True
except AppwriteException as e:
logger.error("Failed to initialize ai_usage_logs table",
table_id=table_id,
error=str(e),
code=e.code)
raise
def init_chat_messages_table(self) -> bool:
"""
Initialize the chat_messages table for storing player-NPC conversation history.
Table schema:
- message_id (string, required): Unique message identifier (UUID)
- character_id (string, required): Player's character ID
- npc_id (string, required): NPC identifier
- player_message (string, required): What the player said
- npc_response (string, required): NPC's reply
- timestamp (string, required): ISO timestamp when message was created
- session_id (string, optional): Game session reference
- location_id (string, optional): Where conversation happened
- context (string, required): Message context type (dialogue, quest, shop, etc.)
- metadata (string, optional): JSON metadata for quest_id, faction_id, etc.
- is_deleted (boolean, default=False): Soft delete flag
Indexes:
- character_id + npc_id + timestamp: Primary query pattern (conversation history)
- character_id + timestamp: All character messages
- session_id + timestamp: Session-based queries
- context: Filter by interaction type
- timestamp: Date range queries
Returns:
True if successful
Raises:
AppwriteException: If table creation fails
"""
table_id = 'chat_messages'
logger.info("Initializing chat_messages table", table_id=table_id)
try:
# Check if table already exists
try:
self.tables_db.get_table(
database_id=self.database_id,
table_id=table_id
)
logger.info("Chat messages table already exists", table_id=table_id)
return True
except AppwriteException as e:
if e.code != 404:
raise
logger.info("Chat messages table does not exist, creating...")
# Create table
logger.info("Creating chat_messages table")
table = self.tables_db.create_table(
database_id=self.database_id,
table_id=table_id,
name='Chat Messages'
)
logger.info("Chat messages table created", table_id=table['$id'])
# Create columns
self._create_column(
table_id=table_id,
column_id='message_id',
column_type='string',
size=36, # UUID length
required=True
)
self._create_column(
table_id=table_id,
column_id='character_id',
column_type='string',
size=100,
required=True
)
self._create_column(
table_id=table_id,
column_id='npc_id',
column_type='string',
size=100,
required=True
)
self._create_column(
table_id=table_id,
column_id='player_message',
column_type='string',
size=2000, # Player input limit
required=True
)
self._create_column(
table_id=table_id,
column_id='npc_response',
column_type='string',
size=5000, # AI-generated response
required=True
)
self._create_column(
table_id=table_id,
column_id='timestamp',
column_type='string',
size=50, # ISO timestamp format
required=True
)
self._create_column(
table_id=table_id,
column_id='session_id',
column_type='string',
size=100,
required=False
)
self._create_column(
table_id=table_id,
column_id='location_id',
column_type='string',
size=100,
required=False
)
self._create_column(
table_id=table_id,
column_id='context',
column_type='string',
size=50, # MessageContext enum values
required=True
)
self._create_column(
table_id=table_id,
column_id='metadata',
column_type='string',
size=1000, # JSON metadata
required=False
)
self._create_column(
table_id=table_id,
column_id='is_deleted',
column_type='boolean',
required=False,
default=False
)
# Wait for columns to fully propagate
logger.info("Waiting for columns to propagate before creating indexes...")
time.sleep(2)
# Create indexes for efficient querying
# Most common query: get conversation between character and specific NPC
self._create_index(
table_id=table_id,
index_id='idx_character_npc_time',
index_type='key',
attributes=['character_id', 'npc_id', 'timestamp']
)
# Get all messages for a character (across all NPCs)
self._create_index(
table_id=table_id,
index_id='idx_character_time',
index_type='key',
attributes=['character_id', 'timestamp']
)
# Session-based queries
self._create_index(
table_id=table_id,
index_id='idx_session_time',
index_type='key',
attributes=['session_id', 'timestamp']
)
# Filter by context (quest, shop, lore, etc.)
self._create_index(
table_id=table_id,
index_id='idx_context',
index_type='key',
attributes=['context']
)
# Date range queries
self._create_index(
table_id=table_id,
index_id='idx_timestamp',
index_type='key',
attributes=['timestamp']
)
logger.info("Chat messages table initialized successfully", table_id=table_id)
return True
except AppwriteException as e:
logger.error("Failed to initialize chat_messages table",
table_id=table_id,
error=str(e),
code=e.code)
raise
def init_combat_encounters_table(self) -> bool:
"""
Initialize the combat_encounters table for storing combat encounter state.
Table schema:
- sessionId (string, required): Game session ID (FK to game_sessions)
- userId (string, required): Owner user ID for authorization
- status (string, required): Combat status (active, victory, defeat, fled)
- roundNumber (integer, required): Current round number
- currentTurnIndex (integer, required): Index in turn_order for current turn
- turnOrder (string, required): JSON array of combatant IDs in initiative order
- combatantsData (string, required): JSON array of Combatant objects (full state)
- combatLog (string, optional): JSON array of all combat log entries
- created_at (string, required): ISO timestamp of combat start
- ended_at (string, optional): ISO timestamp when combat ended
Indexes:
- idx_sessionId: Session-based lookups
- idx_userId_status: User's active combats query
- idx_status_created_at: Time-based cleanup queries
Returns:
True if successful
Raises:
AppwriteException: If table creation fails
"""
table_id = 'combat_encounters'
logger.info("Initializing combat_encounters table", table_id=table_id)
try:
# Check if table already exists
try:
self.tables_db.get_table(
database_id=self.database_id,
table_id=table_id
)
logger.info("Combat encounters table already exists", table_id=table_id)
return True
except AppwriteException as e:
if e.code != 404:
raise
logger.info("Combat encounters table does not exist, creating...")
# Create table
logger.info("Creating combat_encounters table")
table = self.tables_db.create_table(
database_id=self.database_id,
table_id=table_id,
name='Combat Encounters'
)
logger.info("Combat encounters table created", table_id=table['$id'])
# Create columns
self._create_column(
table_id=table_id,
column_id='sessionId',
column_type='string',
size=255,
required=True
)
self._create_column(
table_id=table_id,
column_id='userId',
column_type='string',
size=255,
required=True
)
self._create_column(
table_id=table_id,
column_id='status',
column_type='string',
size=20,
required=True
)
self._create_column(
table_id=table_id,
column_id='roundNumber',
column_type='integer',
required=True
)
self._create_column(
table_id=table_id,
column_id='currentTurnIndex',
column_type='integer',
required=True
)
self._create_column(
table_id=table_id,
column_id='turnOrder',
column_type='string',
size=2000, # JSON array of combatant IDs
required=True
)
self._create_column(
table_id=table_id,
column_id='combatantsData',
column_type='string',
size=65535, # Large text field for JSON combatant array
required=True
)
self._create_column(
table_id=table_id,
column_id='combatLog',
column_type='string',
size=65535, # Large text field for combat log
required=False
)
self._create_column(
table_id=table_id,
column_id='created_at',
column_type='string',
size=50, # ISO timestamp format
required=True
)
self._create_column(
table_id=table_id,
column_id='ended_at',
column_type='string',
size=50, # ISO timestamp format
required=False
)
# Wait for columns to fully propagate
logger.info("Waiting for columns to propagate before creating indexes...")
time.sleep(2)
# Create indexes
self._create_index(
table_id=table_id,
index_id='idx_sessionId',
index_type='key',
attributes=['sessionId']
)
self._create_index(
table_id=table_id,
index_id='idx_userId_status',
index_type='key',
attributes=['userId', 'status']
)
self._create_index(
table_id=table_id,
index_id='idx_status_created_at',
index_type='key',
attributes=['status', 'created_at']
)
logger.info("Combat encounters table initialized successfully", table_id=table_id)
return True
except AppwriteException as e:
logger.error("Failed to initialize combat_encounters table",
table_id=table_id,
error=str(e),
code=e.code)
raise
def init_combat_rounds_table(self) -> bool:
"""
Initialize the combat_rounds table for storing per-round action history.
Table schema:
- encounterId (string, required): FK to combat_encounters
- sessionId (string, required): Denormalized for efficient queries
- roundNumber (integer, required): Round number (1-indexed)
- actionsData (string, required): JSON array of all actions in this round
- combatantStatesStart (string, required): JSON snapshot of combatant states at round start
- combatantStatesEnd (string, required): JSON snapshot of combatant states at round end
- created_at (string, required): ISO timestamp when round completed
Indexes:
- idx_encounterId: Encounter-based lookups
- idx_encounterId_roundNumber: Ordered retrieval of rounds
- idx_sessionId: Session-based queries
- idx_created_at: Time-based cleanup
Returns:
True if successful
Raises:
AppwriteException: If table creation fails
"""
table_id = 'combat_rounds'
logger.info("Initializing combat_rounds table", table_id=table_id)
try:
# Check if table already exists
try:
self.tables_db.get_table(
database_id=self.database_id,
table_id=table_id
)
logger.info("Combat rounds table already exists", table_id=table_id)
return True
except AppwriteException as e:
if e.code != 404:
raise
logger.info("Combat rounds table does not exist, creating...")
# Create table
logger.info("Creating combat_rounds table")
table = self.tables_db.create_table(
database_id=self.database_id,
table_id=table_id,
name='Combat Rounds'
)
logger.info("Combat rounds table created", table_id=table['$id'])
# Create columns
self._create_column(
table_id=table_id,
column_id='encounterId',
column_type='string',
size=36, # UUID format: enc_xxxxxxxxxxxx
required=True
)
self._create_column(
table_id=table_id,
column_id='sessionId',
column_type='string',
size=255,
required=True
)
self._create_column(
table_id=table_id,
column_id='roundNumber',
column_type='integer',
required=True
)
self._create_column(
table_id=table_id,
column_id='actionsData',
column_type='string',
size=65535, # JSON array of action objects
required=True
)
self._create_column(
table_id=table_id,
column_id='combatantStatesStart',
column_type='string',
size=65535, # JSON snapshot of combatant states
required=True
)
self._create_column(
table_id=table_id,
column_id='combatantStatesEnd',
column_type='string',
size=65535, # JSON snapshot of combatant states
required=True
)
self._create_column(
table_id=table_id,
column_id='created_at',
column_type='string',
size=50, # ISO timestamp format
required=True
)
# Wait for columns to fully propagate
logger.info("Waiting for columns to propagate before creating indexes...")
time.sleep(2)
# Create indexes
self._create_index(
table_id=table_id,
index_id='idx_encounterId',
index_type='key',
attributes=['encounterId']
)
self._create_index(
table_id=table_id,
index_id='idx_encounterId_roundNumber',
index_type='key',
attributes=['encounterId', 'roundNumber']
)
self._create_index(
table_id=table_id,
index_id='idx_sessionId',
index_type='key',
attributes=['sessionId']
)
self._create_index(
table_id=table_id,
index_id='idx_created_at',
index_type='key',
attributes=['created_at']
)
logger.info("Combat rounds table initialized successfully", table_id=table_id)
return True
except AppwriteException as e:
logger.error("Failed to initialize combat_rounds table",
table_id=table_id,
error=str(e),
code=e.code)
raise
def _create_column(
self,
table_id: str,
column_id: str,
column_type: str,
size: Optional[int] = None,
required: bool = False,
default: Optional[Any] = None,
array: bool = False
) -> Dict[str, Any]:
"""
Create a column in a table.
Args:
table_id: Table ID
column_id: Column ID
column_type: Column type (string, integer, float, boolean, datetime, email, ip, url)
size: Column size (for string types)
required: Whether column is required
default: Default value
array: Whether column is an array
Returns:
Column creation response
Raises:
AppwriteException: If column creation fails
"""
try:
logger.info("Creating column",
table_id=table_id,
column_id=column_id,
column_type=column_type)
# Build column parameters (Appwrite SDK uses 'key' not 'column_id')
params = {
'database_id': self.database_id,
'table_id': table_id,
'key': column_id,
'required': required,
'array': array
}
if size is not None:
params['size'] = size
if default is not None:
params['default'] = default
# Create column using the appropriate method based on type
if column_type == 'string':
result = self.tables_db.create_string_column(**params)
elif column_type == 'integer':
result = self.tables_db.create_integer_column(**params)
elif column_type == 'float':
result = self.tables_db.create_float_column(**params)
elif column_type == 'boolean':
result = self.tables_db.create_boolean_column(**params)
elif column_type == 'datetime':
result = self.tables_db.create_datetime_column(**params)
elif column_type == 'email':
result = self.tables_db.create_email_column(**params)
else:
raise ValueError(f"Unsupported column type: {column_type}")
logger.info("Column created successfully",
table_id=table_id,
column_id=column_id)
return result
except AppwriteException as e:
# If column already exists, log warning but don't fail
if e.code == 409: # Conflict - column already exists
logger.warning("Column already exists",
table_id=table_id,
column_id=column_id)
return {}
logger.error("Failed to create column",
table_id=table_id,
column_id=column_id,
error=str(e),
code=e.code)
raise
def _create_index(
self,
table_id: str,
index_id: str,
index_type: str,
attributes: List[str],
orders: Optional[List[str]] = None
) -> Dict[str, Any]:
"""
Create an index on a table.
Args:
table_id: Table ID
index_id: Index ID
index_type: Index type (key, fulltext, unique)
attributes: List of column IDs to index
orders: List of sort orders (ASC, DESC) for each attribute
Returns:
Index creation response
Raises:
AppwriteException: If index creation fails
"""
try:
logger.info("Creating index",
table_id=table_id,
index_id=index_id,
attributes=attributes)
result = self.tables_db.create_index(
database_id=self.database_id,
table_id=table_id,
key=index_id,
type=index_type,
columns=attributes, # SDK uses 'columns', not 'attributes'
orders=orders or ['ASC'] * len(attributes)
)
logger.info("Index created successfully",
table_id=table_id,
index_id=index_id)
return result
except AppwriteException as e:
# If index already exists, log warning but don't fail
if e.code == 409: # Conflict - index already exists
logger.warning("Index already exists",
table_id=table_id,
index_id=index_id)
return {}
logger.error("Failed to create index",
table_id=table_id,
index_id=index_id,
error=str(e),
code=e.code)
raise
# Global instance for convenience
_init_service_instance: Optional[DatabaseInitService] = None
def get_database_init_service() -> DatabaseInitService:
"""
Get the global DatabaseInitService instance.
Returns:
Singleton DatabaseInitService instance
"""
global _init_service_instance
if _init_service_instance is None:
_init_service_instance = DatabaseInitService()
return _init_service_instance
def init_database() -> Dict[str, bool]:
"""
Convenience function to initialize all database tables.
Returns:
Dictionary mapping table names to success status
"""
service = get_database_init_service()
return service.init_all_tables()