88
99
1010class DSDatabase :
11- """A database utility class for connecting to a DesignSafe SQL database.
12-
13- This class provides functionality to connect to a MySQL database using
14- SQLAlchemy and PyMySQL. It supports executing SQL queries and returning
15- results in different formats.
16-
17- Attributes:
18- user (str): Database username, defaults to 'dspublic'.
19- password (str): Database password, defaults to 'R3ad0nlY'.
20- host (str): Database host address, defaults to '129.114.52.174'.
21- port (int): Database port, defaults to 3306.
22- db (str): Database name, can be 'sjbrande_ngl_db', 'sjbrande_vpdb', or 'post_earthquake_recovery'.
23- recycle_time (int): Time in seconds to recycle database connections.
24- engine (Engine): SQLAlchemy engine for database connection.
25- Session (sessionmaker): SQLAlchemy session maker bound to the engine.
11+ """
12+ Manages connection and querying for a specific DesignSafe database.
13+ Uses SQLAlchemy engine for connection pooling and session-per-query pattern.
2614 """
2715
2816 def __init__ (self , dbname = "ngl" ):
29- """Initializes the DSDatabase instance with environment variables and creates the database engine.
30-
31- Args:
32- dbname (str): Shorthand for the database name. Must be one of 'ngl', 'vp', or 'eq'.
33- """
34-
17+ """Initializes the DSDatabase instance and creates the engine."""
3518 if dbname not in db_config :
3619 raise ValueError (
37- f"Invalid database shorthand '{ dbname } '. Allowed shorthands are : { ', ' .join (db_config .keys ())} "
20+ f"Invalid db shorthand '{ dbname } '. Allowed: { ', ' .join (db_config .keys ())} "
3821 )
3922
4023 config = db_config [dbname ]
@@ -45,50 +28,71 @@ def __init__(self, dbname="ngl"):
4528 self .host = os .getenv (f"{ env_prefix } DB_HOST" , "129.114.52.174" )
4629 self .port = os .getenv (f"{ env_prefix } DB_PORT" , 3306 )
4730 self .db = config ["dbname" ]
31+ self .dbname_short = dbname # Store shorthand name for reference
4832
49- # Setup the database connection
33+ print (
34+ f"Creating SQLAlchemy engine for database '{ self .db } ' ({ self .dbname_short } )..."
35+ )
36+ # Setup the database connection engine with pooling
5037 self .engine = create_engine (
5138 f"mysql+pymysql://{ self .user } :{ self .password } @{ self .host } :{ self .port } /{ self .db } " ,
52- pool_recycle = 3600 , # 1 hour in seconds
39+ pool_recycle = 3600 , # Recycle connections older than 1 hour
40+ pool_pre_ping = True , # Check connection validity before use
5341 )
42+ # Create a configured "Session" class
5443 self .Session = sessionmaker (bind = self .engine )
44+ print (f"Engine for '{ self .dbname_short } ' created." )
5545
5646 def read_sql (self , sql , output_type = "DataFrame" ):
57- """Executes a SQL query and returns the results.
58-
59- Args:
60- sql (str): The SQL query string to be executed.
61- output_type (str, optional): The format for the query results. Defaults to 'DataFrame'.
62- Possible values are 'DataFrame' for a pandas DataFrame, or 'dict' for a list of dictionaries.
63-
64- Returns:
65- pandas.DataFrame or list of dict: The result of the SQL query.
47+ """
48+ Executes a SQL query using a dedicated session and returns the results.
6649
67- Raises:
68- ValueError: If the SQL query string is empty or if the output type is not valid.
69- SQLAlchemyError: If an error occurs during query execution .
50+ Each call obtains a session (and underlying connection from the pool),
51+ executes the query, and closes the session (returning the connection
52+ to the pool) .
7053 """
7154 if not sql :
7255 raise ValueError ("SQL query string is required" )
73-
7456 if output_type not in ["DataFrame" , "dict" ]:
7557 raise ValueError ('Output type must be either "DataFrame" or "dict"' )
7658
59+ # Obtain a new session for this query
7760 session = self .Session ()
78-
61+ print ( f"Executing query on ' { self . dbname_short } '..." )
7962 try :
8063 if output_type == "DataFrame" :
81- return pd .read_sql_query (sql , session .bind )
64+ # pandas read_sql_query handles connection/session management implicitly sometimes,
65+ # but using the session explicitly ensures consistency.
66+ # Pass the engine bound to the session.
67+ return pd .read_sql_query (
68+ sql , session .bind .connect ()
69+ ) # Get connection from engine
8270 else :
83- # Convert SQL string to a text object
8471 sql_text = text (sql )
72+ # Execute within the session context
8573 result = session .execute (sql_text )
86- return [dict (row ) for row in result ]
74+ # Fetch results before closing session
75+ data = [
76+ dict (row ._mapping ) for row in result
77+ ] # Use ._mapping for modern SQLAlchemy
78+ return data
8779 except exc .SQLAlchemyError as e :
88- raise Exception (f"SQLAlchemyError: { e } " )
80+ print (f"SQLAlchemyError executing query on '{ self .dbname_short } ': { e } " )
81+ raise # Re-raise the exception
82+ except Exception as e :
83+ print (f"Unexpected error executing query on '{ self .dbname_short } ': { e } " )
84+ raise
8985 finally :
86+ # Ensure the session is closed, returning the connection to the pool
9087 session .close ()
88+ # print(f"Session for '{self.dbname_short}' query closed.") # Can be noisy
9189
9290 def close (self ):
93- """Close the database connection."""
94- self .engine .dispose ()
91+ """Dispose of the engine and its connection pool for this database."""
92+ if self .engine :
93+ print (f"Disposing engine and closing pool for '{ self .dbname_short } '..." )
94+ self .engine .dispose ()
95+ self .engine = None # Mark as disposed
96+ print (f"Engine for '{ self .dbname_short } ' disposed." )
97+ else :
98+ print (f"Engine for '{ self .dbname_short } ' already disposed." )
0 commit comments