Automated relational database schema management
This example demonstrates how to use prt.db
module to manage database schema migrations and static data, specifically for your Practicus AI applications.
Core Concepts:
- Isolated Schemas: Practicus AI encourages creating
separate restricted database schemas
(and corresponding users/roles) for each application to ensureisolation and security
. This is handled byprt.apps.prepare_db
. - Migrations: Schema changes (creating tables, adding columns, etc.) are automatically managed by Practicus AI. This provides
version control
andGitOps
capabilities for your database structure. - Models: Database tables are defined using
SQLModel
, a popular python library that combines Pydantic and SQLAlchemy. - Workflow:
- Prepare the dedicated app schema (
prt.apps.prepare_db
). - Initialize the local migration environment (
prt.db.init
). - Define your data models (
db/models.py
). - Generate a migration script based on model changes (
prt.db.revision
). - Apply the migration to the database (
prt.db.upgrade
). - Optionally, define and load static/lookup data (
db/static_data.py
,prt.db.upsert_static_data
). - Use the database in your application code.
- Deploy the application, ensuring it has the database connection string.
- Prepare the dedicated app schema (
Prerequisites:
* A system admin must configure a database connection for your Practicus AI app deployment setting
, effectively making it db-enabled
.
* Please see below Preparing a Database for Your Applications
section to learn how.
* The app_deployment_key
below refers to the key of this db-enabled app deployment setting.
1. Setup and Preparation
First, we define some parameters for our application.
# Parameters
app_deployment_key = None # must be db-enabled
app_prefix = "apps"
app_name = "my-db-app"
# Sanity check
assert app_deployment_key, "app_deployment_key not defined"
assert app_prefix, "app_prefix not defined"
assert app_name, "app_name not defined"
If you don't know which app app deployment settings are db-enabled
, you can list the app deployment settings you have acess to and check the db_enabled
column running the below.
import practicuscore as prt
my_app_settings = prt.apps.get_deployment_setting_list()
print("Application deployment settings I have access to:")
display(my_app_settings.to_pandas())
# Let's verify our app deployment setting is db-enabled
verified = False
for app_setting in my_app_settings:
if app_setting.key == app_deployment_key:
if not app_setting.db_enabled:
raise SystemError(f"Setting '{app_deployment_key}' is not db-enabled.")
verified = True
break
if not verified:
raise ValueError(f"Setting '{app_deployment_key}' could not be located.")
print(f"Located setting '{app_deployment_key}' and it is db-enabled. Good to go!")
1.1. Prepare the Application Database Schema
We use prt.apps.prepare_db
to create a dedicated schema and user/role for our application in the configured database. This function returns the schema name and a unique connection string.
Important: The connection string grants access only to the newly created schema. Save it securely, as it won't be shown again.
print(f"Preparing database schema for app: {app_prefix}/{app_name} using deployment setting: {app_deployment_key}")
try:
db_schema, db_conn_str = prt.apps.prepare_db(
prefix=app_prefix,
app_name=app_name,
deployment_setting_key=app_deployment_key,
)
print(f"Successfully prepared database schema: {db_schema}")
# print(f"Database connection string: {db_conn_str}") # Uncomment locally to see if needed, but avoid printing secrets.
print("*** Connection string obtained. Please handle it securely. ***")
except Exception as e:
print(f"Error preparing database: {e}")
print("Please ensure the deployment setting key is correct and the database is accessible.")
raise
1.2. Securely Store and Use the Connection String
It's highly recommended to store sensitive information like database connection strings in Practicus AI's Vault.
The prt.db
commands (revision
, upgrade
, upsert_static_data
) expect the connection string to be available in the environment variable PRT_DB_CONN_STR
. We'll store it in Vault and then set the environment variable for the current notebook session.
import os
# Define a unique name for the secret where we'll store the DB connection string.
db_conn_secret_name = f"{app_prefix.replace('/', '_')}__{app_name.replace('-', '_')}__CONN_STR".upper()
print(f"Database connection string will be stored in Vault secret: {db_conn_secret_name}")
# Store the connection string in Vault
print(f"Storing connection string in Vault secret: {db_conn_secret_name}")
prt.vault.create_or_update_secret(db_conn_secret_name, db_conn_str)
# Set the OS environment variable for prt.db commands in this session
# IMPORTANT: Restart the notebook kernel if you change this variable elsewhere!
os.environ["PRT_DB_CONN_STR"] = db_conn_str
print("Environment variable PRT_DB_CONN_STR set for this session.")
1.3. (Optional) Reload Connection String from Vault
If you restart the notebook kernel or run this notebook later, you can retrieve the connection string from Vault instead of running prepare_db
again (which would try to recreate the schema).
print(f"Attempting to reload connection string from Vault secret: {db_conn_secret_name}")
try:
db_conn_str, age = prt.vault.get_secret(db_conn_secret_name)
os.environ["PRT_DB_CONN_STR"] = db_conn_str
print(f"Successfully reloaded connection string from Vault (Age: {age}).")
print("Environment variable PRT_DB_CONN_STR set for this session.")
except Exception as e:
print(f"Error reloading secret {db_conn_secret_name}: {e}")
print("Ensure the secret exists and you have permissions.")
1.4. (Optional/Development Only) Remove Database Schema
If you need to start over during development, you can remove the application's database schema and associated user/role using prt.apps.remove_db
.
⚠️ WARNING: This operation is destructive and will delete all data within the application's schema. Use with extreme caution, primarily in development environments.
# --- Development Only: Reset Database Schema ---
# Set this to True only if you understand the consequences and want to delete the schema
run_db_removal = False
if run_db_removal:
print("*** WARNING: Proceeding with database schema removal! ***")
confirm = input(f"Type 'delete {app_prefix}/{app_name}' to confirm DELETION of schema and data: ")
if confirm == f"delete {app_prefix}/{app_name}":
print(f"Removing database schema for app: {app_prefix}/{app_name}")
try:
prt.apps.remove_db(
prefix=app_prefix,
app_name=app_name,
deployment_setting_key=app_deployment_key,
)
print("Database schema removed successfully.")
# Unset the environment variable as the connection is now invalid
if "PRT_DB_CONN_STR" in os.environ:
del os.environ["PRT_DB_CONN_STR"]
print("PRT_DB_CONN_STR environment variable unset.")
# Clear the variable in the notebook context
if "db_conn_str" in locals():
del db_conn_str
except Exception as e:
print(f"Error removing database schema: {e}")
else:
print("Confirmation failed. Database schema not removed.")
else:
print("Database removal step skipped (run_db_removal is False).")
2. Core prt.db
Workflow
Now that we have a database schema and connection string ready, we can use the prt.db
functions to manage the schema structure.
2.1. Initialize Migration Environment (prt.db.init
)
This command scaffolds the necessary directory structure (db/
, db/migrations/
) and configuration files (db/migrations.ini
, db/migrations/env.py
, etc.) for Alembic in your current working directory.
It also creates template files:
* db/models.py
: Where you'll define your database tables using SQLModel
.
* db/static_data.py
: Where you can define static lookup data.
* db/__init__.py
: Makes the db
directory a Python package.
Run this once per project. Use overwrite=True
if you need to regenerate the configuration files (this won't overwrite your models.py
or static_data.py
unless they don't exist).
print("Initializing database migration environment in the current directory...")
# Use overwrite=True cautiously if you have existing custom configurations
prt.db.init(overwrite=False)
print("Initialization complete. Check the 'db' folder.")
print("Next step: Edit db/models.py to define your database tables.")
2.2. Define Data Models
Now, you need to edit the db/models.py
file.
Define your database tables as classes inheriting from SQLModel
. Here's an example based on the template created by prt.db.init
(uncomment and modify the Hero
class):
# Example content for db/models.py
from typing import Optional
from sqlmodel import Field, SQLModel
# Define your table models here
# Example:
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str = Field(index=True)
secret_name: str
age: Optional[int] = Field(default=None, index=True)
# You can add more models/tables below
# class Item(SQLModel, table=True):
# ...
Action Required: Open db/models.py
in the file browser, uncomment or add your SQLModel
classes, and save the file.
2.3. Create a Database Revision (prt.db.revision
)
After defining or modifying your models in db/models.py
, run prt.db.revision
. This command inspects your models and compares them to the database state recorded in previous migrations. It then automatically generates a new migration script in the db/migrations/versions/
directory.
Provide a short, descriptive message (max 75 chars) for the revision.
Important: Always review the generated Python script in db/migrations/versions/
before proceeding. Auto-detection isn't perfect, especially for complex changes like column renames or constraints. You may need to edit the script manually.
# Make sure you have edited and saved db/models.py before running this!
revision_message = "Create initial tables" # Change this message for subsequent revisions
print(f"Generating database revision with message: '{revision_message}'")
assert os.environ.get("PRT_DB_CONN_STR"), "PRT_DB_CONN_STR environment variable not set. Cannot run revision."
try:
prt.db.revision(revision_message)
print("Revision script generated in db/migrations/versions/")
print("*** IMPORTANT: Please review the generated script before running upgrade. ***")
except Exception as e:
print(f"Error generating revision: {e}")
print("Check db/models.py for syntax errors and ensure the database is accessible.")
# raise # Optional: Stop if revision fails
2.4. Apply Migrations to Database (prt.db.upgrade
)
Once you have reviewed (and if necessary, edited) the migration script(s), use prt.db.upgrade
to apply the changes to the actual database.
- Online Mode (default):
prt.db.upgrade()
connects to the database (usingPRT_DB_CONN_STR
) and executes the migration scripts to bring the schema to the latest version. - Offline Mode:
prt.db.upgrade(offline=True)
does not connect to the database. Instead, it generates a single SQL script containing all the necessary commands to upgrade the schema. You can review this SQL and execute it manually against your database (useful for production environments or when direct access from the notebook is restricted).
print("Applying migrations to the database (Online Mode)...")
assert os.environ.get("PRT_DB_CONN_STR"), "PRT_DB_CONN_STR environment variable not set. Cannot run upgrade."
# --- Online Upgrade (Applies changes directly) ---
try:
prt.db.upgrade()
print("Database upgrade complete.")
except Exception as e:
print(f"Error during online upgrade: {e}")
# raise # Optional: Stop if upgrade fails
# --- Offline Upgrade (Generates SQL script) ---
# print("Generating SQL script for offline upgrade...")
# try:
# prt.db.upgrade(offline=True)
# print("SQL script generated. Check the console output.")
# print("Review the SQL script and apply it manually to your database.")
# except Exception as e:
# print(f"Error during offline upgrade generation: {e}")
3. Managing Static Data (prt.db.upsert_static_data
)
Some applications require lookup tables with predefined data (e.g., status types, categories). You can manage this using db/static_data.py
and prt.db.upsert_static_data
.
3.1. Define Static Data
Edit the db/static_data.py
file. It should contain a function get_static_data()
that returns a list of SQLModel
instances representing the rows you want to insert or update.
Important: For the upsert logic to work correctly (updating existing rows instead of always inserting new ones), your static data models must have static primary keys defined in the data itself, not auto-incrementing keys.
# Example content for db/static_data.py
# Import your models that hold static data
from .models import EnumLikeModel
def get_static_data():
"""Returns a list of SQLModel instances for static data."""
static_data = [
# Example using EnumLikeModel (replace with your actual models and data)
EnumLikeModel(key="pending", name="Pending"),
EnumLikeModel(key="in_progress", name="In Progress"),
EnumLikeModel(key="completed", name="Completed", description="Task is finished"),
# SomeOtherModel(...
]
return static_data
Action Required:
1. Ensure the models for your static data (e.g., EnumLikeModel
) are defined in db/models.py
.
2. Ensure these models have primary keys that you can set manually (not auto-incrementing).
3. Edit db/static_data.py
, import your models, and populate the list returned by get_static_data()
.
4. Make sure you have run prt.db.revision
and prt.db.upgrade
to create the necessary tables (EnumLikeModel
in this example) before trying to upsert data.
3.2. Upsert Static Data to Database
Run prt.db.upsert_static_data()
to load the data defined in db/static_data.py
into the database. It connects using PRT_DB_CONN_STR
and performs an insert-or-update operation based on the primary key.
# Make sure you have edited db/static_data.py and the corresponding tables exist in the DB.
print("Upserting static data from db/static_data.py...")
assert os.environ.get("PRT_DB_CONN_STR"), "PRT_DB_CONN_STR environment variable not set. Cannot upsert static data."
try:
upsert_summary = prt.db.upsert_static_data()
print("Upsert operation summary:")
if upsert_summary:
for model_name, count in upsert_summary.items():
print(f"- Upserted {count} rows for model: {model_name}")
else:
print("- No static data found or processed. Check db/static_data.py.")
except ModuleNotFoundError:
print(
"ERROR: Could not import models or static data. Ensure 'db/models.py' and 'db/static_data.py' exist and are correct."
)
except Exception as e:
print(f"Error upserting static data: {e}")
print("Ensure the tables exist (run upgrade) and have static primary keys.")
# raise # Optional: Stop if upsert fails
4. Schema Evolution (Iteration)
As your application requirements change, you'll need to update your database schema:
- Modify
db/models.py
: Add new models (tables), add/remove/modify fields (columns) in existing models. - Run
prt.db.revision("Your descriptive message")
: Generate a new migration script reflecting the changes. - Review the script in
db/migrations/versions/
. - Run
prt.db.upgrade()
: Apply the new migration to the database.
You can repeat this cycle as many times as needed.
# Example: Suppose you added a 'last_updated' column to the Hero model in db/models.py
print("--- Simulating Schema Evolution ---")
print("1. Models modified (manually edit db/models.py)")
print("2. Generating new revision...")
try:
prt.db.revision("Add last_updated to Hero")
print("Review the new script in db/migrations/versions/")
except Exception as e:
print(f"Error: {e}")
print("3. Applying upgrade...")
try:
prt.db.upgrade()
print("Upgrade applied.")
except Exception as e:
print(f"Error: {e}")
5. Using the Database at Runtime
Once your schema is set up, you can interact with the database using standard SQLModel
(or SQLAlchemy) patterns in your application code or notebooks. You'll need the connection string, which is typically retrieved from the PRT_DB_CONN_STR
environment variable (set directly or via Vault secrets during deployment).
from sqlmodel import Session, create_engine
# Ensure models are importable (because we ran init)
# If you get ModuleNotFoundError, restart kernel after running prt.db.init
try:
from db.models import Hero
except ModuleNotFoundError:
print("Could not import from db.models. Did you run prt.db.init and define models?")
print("You might need to restart the notebook kernel.")
raise
def add_hero_data():
# Retrieve connection string from environment
conn_str = os.environ.get("PRT_DB_CONN_STR")
if not conn_str:
print("Error: PRT_DB_CONN_STR not set. Cannot connect to database.")
return
print("Connecting to database using PRT_DB_CONN_STR OS env variable...")
# Set echo=True to see SQL statements
engine = create_engine(conn_str, echo=False)
# Create sample data
hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson")
hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")
hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48)
print("Adding sample Hero data...")
try:
with Session(engine) as session:
session.add(hero_1)
session.add(hero_2)
session.add(hero_3)
session.commit()
print("Data committed successfully.")
# Example of retrieving data
session.refresh(hero_1)
print("Refreshed hero_1:", hero_1)
except Exception as e:
print(f"Database error during runtime usage: {e}")
print("Ensure the table exists and the connection string is valid.")
add_hero_data()
6. Integration with Application Deployment
When deploying your application using prt.apps.deploy
, you need to provide the database connection string to the running application container. This is typically done by:
- Storing the connection string as a secret in Vault (as done in step 1.2).
- Mapping this Vault secret to the
PRT_DB_CONN_STR
environment variable within the application container using thepersonal_secrets
argument inprt.apps.deploy
.
Automatic Database Migration on App Deployment
By default, Practicus AI automatically applies database migrations
(effectively running prt.db.upgrade
and prt.db.upsert_static_data
) whenever your application starts or restarts in a deployed environment, including production settings. This convenient feature helps ensure your database schema is synchronized with your application code. If you need to disable this behavior, for example, to manage migrations manually or through a separate CI/CD pipeline, simply set the environment variable PRT_DB_SKIP_MIGRATION
to true in your application's deployment configuration.
visible_name = "My Database App"
description = "An example app using prt.db for schema management."
icon = "fa-database"
print(f"Deploying app: {app_prefix}/{app_name}")
print(f"Mapping Vault secret '{db_conn_secret_name}' to env var 'PRT_DB_CONN_STR'")
try:
app_url, api_url = prt.apps.deploy(
deployment_setting_key=app_deployment_key,
prefix=app_prefix,
app_name=app_name,
visible_name=visible_name,
description=description,
icon=icon,
# --- Database Connection Mapping ---
# Format: "VAULT_SECRET_NAME:TARGET_ENV_VAR_NAME"
personal_secrets=[f"{db_conn_secret_name}:PRT_DB_CONN_STR"],
# To disable automated db migration:
# env_variables={"PRT_DB_SKIP_MIGRATION": "true"}
)
print("Deployment submitted.")
print(f"App UI URL (once ready): {app_url}")
print(f"App API URL (once ready): {api_url}")
except Exception as e:
print(f"Error during deployment submission: {e}")
7. A Guide for Admins: Preparing a Database for Your Applications
Use the following SQL scripts as a guide to create the necessary database structure for your applications.
1. Create an Administrative Database Role (User)
First, create a dedicated administrative role. This role is primarily intended for control plane operations (e.g., automated setup scripts or administrative tools) to manage database artifacts for your applications.
- Purpose: This admin role (
prt_apps_admin
in the example) will be used to create more restricted, application-specific database roles and schemas. It is not intended for direct use by the applications themselves. CREATEROLE
Permission: This permission is required because the admin role needs the ability to create separate, isolated database roles for each application, enhancing security and separation.LOGIN
Permission: Allows this role to connect to the database server.
-- Create the administrative role
CREATE ROLE prt_apps_admin WITH
LOGIN
CREATEROLE
PASSWORD '_your_password_'; -- Replace with a strong, secure password
2. Create a Dedicated Database (Recommended)
It's best practice to create a separate database specifically for your applications. Hosting this database on a dedicated server instance is also recommended for performance and isolation, if feasible.
OWNER prt_apps_admin
: Assigning the administrative role as the owner grants it initial full permissions within this new database, simplifying subsequent setup.
3. Grant Necessary Permissions to the Admin Role
Ensure the administrative role has the essential permissions on the newly created database.
CONNECT
Permission: Allows theprt_apps_admin
role to connect specifically to theprt_apps
database.CREATE
Permission: Allows theprt_apps_admin
role to create new schemas within theprt_apps
database. This is necessary for creating isolated schemas for different applications or services.
-- Grant connection access to the database
GRANT CONNECT ON DATABASE prt_apps TO prt_apps_admin;
-- Grant permission to create schemas within the database
GRANT CREATE ON DATABASE prt_apps TO prt_apps_admin;
4. Configure Application Deployment Settings
After setting up the administrative database role and database, you need to configure your app deployment setting
to use them.
a. Construct the Connection String
You will need a standard PostgreSQL connection string to connect Practicus AI to the database you created.
- Standard Format:
postgresql://user:password@host/database
- Example (using the admin role):
postgresql://prt_apps_admin:your_secure_password@your_database_host/prt_apps
- Using Practicus AI Password Management (Recommended): To avoid hardcoding the password directly in the connection string, Practicus AI allows you to use a placeholder (
__PASSWORD__
) and store the password securely and separately within the platform.- Format:
postgresql://user:__PASSWORD__@host/database
- Example:
postgresql://prt_apps_admin:__PASSWORD__@prt-db-pg-1-rw.prt-ns.svc.cluster.local/prt_apps
(Replace the hostprt-db-pg-1-rw.prt-ns.svc.cluster.local
with your actual database host address)
- Format:
b. Create a System Database Connection in Practicus AI
Next, register this connection within the Practicus AI Admin Console.
- Navigate to Data Catalog > System DB Connections.
- Click to add a new database connection (e.g., name it
my-db-for-apps
). - Enter the connection string constructed in the previous step.
- Recommendation: Use the connection string format with
__PASSWORD__
and enter the actual password in the dedicated password field provided by Practicus AI. Save the connection.
c. Update Application Deployment Settings
Finally, link this database connection to your app deployment settings. This tells Practicus AI which database connection to use when apps need to create their own isolated database resources.
- Navigate to App Hosting > Deployment Settings.
- Either add a new deployment setting or edit an existing one.
- Scroll down to the Database Connection section.
- Select the System DB Connection you created earlier (e.g.,
my-db-for-apps
) from the dropdown list. - Save the deployment settings.
With this configuration complete, users creating new applications or updating existing ones through Practicus AI will now have the capability to automatically provision isolated database schemas and roles using the administrative database connection you've set up.
8. Advanced Notes and Troubleshooting
Migration Script Review
- Column Renames: Alembic's auto-generation often detects a column rename as a
drop_column
followed by anadd_column
, which causes data loss. Review the generated scripts indb/migrations/versions/
and manually change these to useop.alter_column(..., new_column_name='...')
if you need to preserve data during a rename. - Default Values & Constraints: Auto-generation might miss default values, specific constraints, or index types. Check the generated scripts and add or modify Alembic operations (
op.
) as needed. - SQLModel vs. Alembic Types: Ensure the data types in your
SQLModel
definitions translate correctly to Alembic/SQLAlchemy types in the migration scripts.
Static Data Notes
- As emphasized before, use static primary keys in
db/static_data.py
for reliable upserts. prt.db.upsert_static_data()
runs within a transaction. If any part fails, the entire operation for that run is rolled back.
Environment Variables
- Changes to environment variables like
PRT_DB_CONN_STR
within a notebook session might not be picked up by subprocesses (like Alembic calls withinprt.db
) unless the kernel is restarted.
File Locations
prt.db.init
,revision
,upgrade
,upsert_static_data
all expect thedb
directory (containingmodels.py
,static_data.py
,migrations.ini
,migrations/
) to be in the current working directory where the Python command/notebook cell is executed.
Previous: Sample Vector Db | Next: Message Queues > Build