Database Skill
Manage PostgreSQL databases and execute SQL queries safely in your development and production environments.
When to Use
Use this skill when:
- Creating a new PostgreSQL database for your project
- Checking if a database is provisioned and accessible
- Running SQL queries against the development or production database
- Querying data warehouses (BigQuery, Databricks, Snowflake). For Databricks, use the
databricks-m2m connector (not the plain databricks connector).
When NOT to Use
- Schema migrations in production environments — see "Production schema changes" below
- Direct modifications to Stripe tables (use Stripe API instead)
- Converting a pre-existing database over to Replit, unless a user explicitly asks you to.
Production schema changes
For projects that use Replit's managed PostgreSQL, the production database schema is managed automatically by Replit's Publish flow. When the user clicks Publish, Replit diffs the development schema against production, asks the user to resolve any renames in the Publish UI, and applies the diff to production. This is the supported path for changing the production schema; the agent must not write any code or scripts to migrate the production database.
For the full set of rules on what the agent must and must not do here — including specific patterns to avoid (custom migration scripts, deploy-build hooks, startup-time DDL) and the correct dev-side flow — read .local/skills/database/references/database-migrations-on-publish.md.
Reference Documents
.local/skills/database/references/database-migrations-on-publish.md — Full detail on the publish-time schema migration flow, including the two automatic application points (post-merge → dev, publish → prod), the SQL diff and rename-confirmation behavior, and the canonical failure mode this guidance prevents. Read this when the user reports production is missing a column or table, asks to "push dev to prod" / "migrate the production database" / "sync the schema", or reports the deployed app failing with "column does not exist" / "relation does not exist" errors.
Available Functions
checkDatabase()
Check if the PostgreSQL database is provisioned and accessible.
Parameters: None
Returns: Dict with provisioned (bool) and message (str)
Example:
const status = await checkDatabase();
if (status.provisioned) {
console.log("Database is ready!");
} else {
console.log(status.message);
// Consider calling createDatabase()
}
createDatabase()
Create or verify a PostgreSQL database exists for the project.
Parameters: None
Returns: Dict with:
success (bool): Whether operation succeeded
message (str): Status message
alreadyExisted (bool): True if database already existed
secretKeys (list): Environment variables set (DATABASE_URL, PGHOST, etc.)
Example:
const result = await createDatabase();
if (result.success) {
console.log(`Database ready! Environment variables: ${result.secretKeys}`);
// Now you can use DATABASE_URL in your application
}
executeSql()
Execute a SQL query with safety checks.
Parameters:
sqlQuery (str, required): The SQL query to execute. Use $1, $2, etc. placeholders when using parameterized queries.
params (array, optional): Parameter values for parameterized queries. When provided, values are bound separately from the SQL string, preventing SQL injection. Each $N placeholder in sqlQuery corresponds to the Nth element in this array (1-indexed). Supported types: string, number, boolean, null. Two restrictions:
- Single-statement only. When
params is provided (including params: []), sqlQuery MUST be a single SQL statement. Semicolon-delimited multi-statement scripts (BEGIN; ...; COMMIT;, migration batches, etc.) are rejected on the parameterized path. Send them as a single executeSql call WITHOUT params — separate calls do NOT share a transaction or session, so splitting would silently lose atomicity. Validate any interpolated values against a strict allowlist before embedding them.
replit_database target only. Data warehouse targets (bigquery, databricks, snowflake) do not support parameter binding — passing params with those targets raises an error.
target (str, default "replit_database"): Target database: "replit_database", "bigquery", "databricks", or "snowflake"
environment (str, default "development"): "development" runs against the development database (all SQL operations supported). "production" runs READ-ONLY queries against a replica of the production database (only SELECT queries allowed). Production is only supported for the "replit_database" target. "production" database, depending on when the user last deployed, may have outdated schemas.
sampleSize (int, optional): Sample size for warehouse queries (only for bigquery/databricks/snowflake)
Returns: Dict with:
success (bool): Whether query succeeded
output (str): Query output/results
exitCode (int): Exit code (0 = success)
exitReason (str | None): Reason for exit if failed
CRITICAL: When params is supported, ALWAYS use it for any user input or variable.
This applies when all of these are true:
target is replit_database (the default).
sqlQuery is a single SQL statement (no semicolons separating multiple statements).
In that case, never use string interpolation or concatenation to build SQL with dynamic literal values — pass them through params. This prevents SQL injection.
// BAD - vulnerable to SQL injection:
const result = await executeSql({ sqlQuery: `SELECT * FROM users WHERE id = ${userId}` });
// GOOD - use parameterized queries:
const result = await executeSql({
sqlQuery: "SELECT * FROM users WHERE id = $1",
params: [userId]
});
Note: params only binds literal values (strings, numbers, booleans, null). PostgreSQL placeholders cannot substitute identifiers (table or column names), SQL keywords (sort directions like ASC/DESC, operators), or other syntactic elements. For dynamic identifiers or keywords, validate the value against a strict allowlist (e.g. a fixed set of allowed table names, or a regex like /^[a-z_][a-z0-9_]*$/), then interpolate the validated value into the SQL string. Never interpolate free-form user text into identifier positions.
// BAD - $1 cannot bind a table name; this query fails to parse:
await executeSql({ sqlQuery: "SELECT * FROM $1 WHERE id = $2", params: [tableName, userId] });
// GOOD - allowlist + interpolate identifiers, $N for values:
const ALLOWED_TABLES = ["users", "orders", "products"];
if (!ALLOWED_TABLES.includes(tableName)) throw new Error("invalid table");
await executeSql({
sqlQuery: `SELECT * FROM ${tableName} WHERE id = $1`,
params: [userId]
});
When params is NOT supported on replit_database (multi-statement scripts only — BEGIN; ...; COMMIT;, migrations, batched DDL):
- For independent statements that don't need shared transaction or session state, you can split them into separate
executeSql calls each with its own params (e.g., a sequence of unrelated INSERTs into different tables). Note that separate executeSql calls do NOT share a connection, transaction, or session — temp tables, SET LOCAL GUCs, and BEGIN/COMMIT will not carry across calls.
- For scripts that DO need transactional atomicity or session state (most multi-statement writes, migrations, scripts that depend on temp tables), send the whole script as a single
executeSql call without params. Validate any interpolated values against a strict allowlist (e.g. integers only, identifier whitelist) before embedding them in the SQL string. Treat any interpolated value as a potential injection vector — never interpolate free-form user text.
For warehouse targets (bigquery, databricks, snowflake), params is NEVER supported. The runtime rejects any warehouse query that includes params, whether single-statement or multi-statement. You MUST either: (a) reject the user input if it cannot be validated, or (b) validate the value against a strict allowlist (e.g. integers only, fixed identifier set) before interpolating it into the SQL string. Never interpolate free-form user text into warehouse SQL.
Example:
// Simple SELECT with parameters
const result = await executeSql({
sqlQuery: "SELECT * FROM users WHERE id = $1",
params: [userId]
});
if (result.success) {
console.log(result.output);
}
// Static query without user input (no params needed)
const result1b = await executeSql({ sqlQuery: "SELECT * FROM users LIMIT 5" });
// CREATE TABLE (static DDL, no params needed)
const result2 = await executeSql({
sqlQuery: `
CREATE TABLE IF NOT EXISTS products (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
price DECIMAL(10, 2)
)
`
});
// INSERT data with parameters
const result3 = await executeSql({
sqlQuery: "INSERT INTO products (name, price) VALUES ($1, $2)",
params: [productName, productPrice]
});
// UPDATE with parameters
const result3b = await executeSql({
sqlQuery: "UPDATE products SET price = $1 WHERE name = $2",
params: [newPrice, productName]
});
// DELETE with parameters
const result3c = await executeSql({
sqlQuery: "DELETE FROM products WHERE id = $1",
params: [productId]
});
// Read-only production query with parameters
const result4 = await executeSql({
sqlQuery: "SELECT * FROM users WHERE active = $1",
params: [true],
environment: "production"
});
// Data warehouse query with sampling
// (Note: `params` is NOT supported for warehouse targets; if `year` were
// dynamic, validate it as an integer literal before interpolating.)
const result5 = await executeSql({
sqlQuery: "SELECT * FROM sales_data WHERE year = 2024",
target: "bigquery",
sampleSize: 100
});
// Multi-statement transactional scripts (BEGIN ... COMMIT, temp tables,
// SET LOCAL, etc.) MUST run as a single executeSql call WITHOUT `params`
// — separate calls do NOT share a connection or transaction, so
// splitting a transaction across multiple calls would silently lose
// atomicity. If the script needs user-controlled values, validate them
// against a strict allowlist (integers only, identifier whitelist)
// before interpolating; never interpolate free-form user text.
const result = await executeSql({
sqlQuery: `
BEGIN;
UPDATE users SET active = false WHERE last_login < '2024-01-01';
UPDATE audit_log SET archived = true WHERE created_at < '2024-01-01';
COMMIT;
`,
});
Safety Features
- Environment Isolation: Development queries run against the development database; production queries are READ-ONLY against a read replica
- Stripe Protection: Mutations to Stripe schema tables (stripe.*) are blocked
- Discussion Mode: Mutating queries are blocked in Planning/Discussion mode
- Destructive Query Protection: DROP, TRUNCATE, etc. are blocked via the skill callback path (use the tool interface directly for destructive operations that require user confirmation)
Best Practices
- Prefer the built-in database: Replit's built-in PostgreSQL database is always preferred over external services like Supabase. It supports rollback and integrates directly with the Replit product. Only use external database services if the user has specific requirements. The
pg package should be installed already.
- Check before creating: Call
checkDatabase() before createDatabase() to avoid unnecessary operations
- ALWAYS use parameterized queries with user input on the
replit_database single-statement path: When target is replit_database (the default) and sqlQuery is a single statement, you MUST use the params argument with $1, $2, etc. placeholders for any user-provided literal value. NEVER use string interpolation, template literals, or concatenation for values in that case. For dynamic identifiers (table/column names) or SQL keywords (sort directions, operators), validate against a strict allowlist before interpolating since params cannot bind those. For warehouse targets and multi-statement transactional scripts where params is not supported, send the whole script as a single executeSql call and validate any interpolated values against a strict allowlist; only split into separate calls when the statements are truly independent and don't share transaction or session state.
- Test queries first: Run SELECT queries before INSERT/UPDATE/DELETE
- Keep backups: Important data should be backed up before destructive operations
Environment Variables
After creating a database, these environment variables are available:
DATABASE_URL: Full connection string
PGHOST: Database host
PGPORT: Database port (5432)
PGUSER: Database username
PGPASSWORD: Database password
PGDATABASE: Database name
Example Workflow
// 1. Check if database exists
const status = await checkDatabase();
if (!status.provisioned) {
// 2. Create database
const createResult = await createDatabase();
if (!createResult.success) {
console.log(`Failed: ${createResult.message}`);
}
}
// 3. Create schema
await executeSql({
sqlQuery: `
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
)
`
});
// 4. Insert data (using parameterized query)
await executeSql({
sqlQuery: "INSERT INTO users (email) VALUES ($1)",
params: ['user@example.com']
});
// 5. Query data
const result = await executeSql({ sqlQuery: "SELECT * FROM users" });
console.log(result.output);
Limitations
- Production queries are READ-ONLY (SELECT only) — INSERT, UPDATE, DELETE, and DDL statements will fail
- Production environment is only supported for the "replit_database" target (not data warehouses)
- Cannot modify Stripe schema tables (read-only)
- Destructive queries (DROP, TRUNCATE, etc.) are blocked via the skill callback path
- Mutating queries blocked in Planning mode
Rollbacks
As stated in the diagnostic skills, the development database support rollbacks. Open that skill for more information.
1---2name: database3description: Create and manage Replit's built-in PostgreSQL databases, check status, execute SQL queries with safety checks, and run read-only queries against the production database. Use when the user wants to check prod data, debug database issues in production, or asks to "check the prod db", "query production", "look at live data", or "see what's in the database on the deployed app". Also use when the user asks how to apply development schema changes to the production database, e.g. "push dev to prod", "migrate the production database", "sync the schema", "production is missing a column", or reports a deployed app failing with "column does not exist" / "relation does not exist".4---56# Database Skill78Manage PostgreSQL databases and execute SQL queries safely in your development and production environments.910## When to Use1112Use this skill when:1314- Creating a new PostgreSQL database for your project15- Checking if a database is provisioned and accessible16- Running SQL queries against the development or production database17- Querying data warehouses (BigQuery, Databricks, Snowflake). For Databricks, use the `databricks-m2m` connector (not the plain `databricks` connector).1819## When NOT to Use2021- Schema migrations in production environments — see "Production schema changes" below22- Direct modifications to Stripe tables (use Stripe API instead)23- Converting a pre-existing database over to Replit, unless a user explicitly asks you to.2425## Production schema changes2627For projects that use Replit's managed PostgreSQL, the production database schema is managed automatically by Replit's Publish flow. When the user clicks Publish, Replit diffs the development schema against production, asks the user to resolve any renames in the Publish UI, and applies the diff to production. This is the supported path for changing the production schema; the agent must not write any code or scripts to migrate the production database.2829For the full set of rules on what the agent must and must not do here — including specific patterns to avoid (custom migration scripts, deploy-build hooks, startup-time DDL) and the correct dev-side flow — read `.local/skills/database/references/database-migrations-on-publish.md`.3031## Reference Documents3233- `.local/skills/database/references/database-migrations-on-publish.md` — Full detail on the publish-time schema migration flow, including the two automatic application points (post-merge → dev, publish → prod), the SQL diff and rename-confirmation behavior, and the canonical failure mode this guidance prevents. Read this when the user reports production is missing a column or table, asks to "push dev to prod" / "migrate the production database" / "sync the schema", or reports the deployed app failing with "column does not exist" / "relation does not exist" errors.3435## Available Functions3637### checkDatabase()3839Check if the PostgreSQL database is provisioned and accessible.4041**Parameters:** None4243**Returns:** Dict with `provisioned` (bool) and `message` (str)4445**Example:**4647```javascript48const status = await checkDatabase();49if (status.provisioned) {50 console.log("Database is ready!");51} else {52 console.log(status.message);53 // Consider calling createDatabase()54}55```5657### createDatabase()5859Create or verify a PostgreSQL database exists for the project.6061**Parameters:** None6263**Returns:** Dict with:6465- `success` (bool): Whether operation succeeded66- `message` (str): Status message67- `alreadyExisted` (bool): True if database already existed68- `secretKeys` (list): Environment variables set (DATABASE_URL, PGHOST, etc.)6970**Example:**7172```javascript73const result = await createDatabase();74if (result.success) {75 console.log(`Database ready! Environment variables: ${result.secretKeys}`);76 // Now you can use DATABASE_URL in your application77}78```7980### executeSql()8182Execute a SQL query with safety checks.8384**Parameters:**8586- `sqlQuery` (str, required): The SQL query to execute. Use `$1`, `$2`, etc. placeholders when using parameterized queries.87- `params` (array, optional): Parameter values for parameterized queries. When provided, values are bound separately from the SQL string, **preventing SQL injection**. Each `$N` placeholder in `sqlQuery` corresponds to the Nth element in this array (1-indexed). Supported types: string, number, boolean, null. **Two restrictions:**88 - **Single-statement only.** When `params` is provided (including `params: []`), `sqlQuery` MUST be a single SQL statement. Semicolon-delimited multi-statement scripts (`BEGIN; ...; COMMIT;`, migration batches, etc.) are rejected on the parameterized path. Send them as a single `executeSql` call WITHOUT `params` — separate calls do NOT share a transaction or session, so splitting would silently lose atomicity. Validate any interpolated values against a strict allowlist before embedding them.89 - **`replit_database` target only.** Data warehouse targets (bigquery, databricks, snowflake) do not support parameter binding — passing `params` with those targets raises an error.90- `target` (str, default "replit_database"): Target database: "replit_database", "bigquery", "databricks", or "snowflake"91- `environment` (str, default "development"): "development" runs against the development database (all SQL operations supported). "production" runs READ-ONLY queries against a replica of the production database (only SELECT queries allowed). Production is only supported for the "replit_database" target. "production" database, depending on when the user last deployed, may have outdated schemas.92- `sampleSize` (int, optional): Sample size for warehouse queries (only for bigquery/databricks/snowflake)9394**Returns:** Dict with:9596- `success` (bool): Whether query succeeded97- `output` (str): Query output/results98- `exitCode` (int): Exit code (0 = success)99- `exitReason` (str | None): Reason for exit if failed100101**CRITICAL: When `params` is supported, ALWAYS use it for any user input or variable.**102This applies when **all** of these are true:103104- `target` is `replit_database` (the default).105- `sqlQuery` is a single SQL statement (no semicolons separating multiple statements).106107In that case, never use string interpolation or concatenation to build SQL with dynamic literal values — pass them through `params`. This prevents SQL injection.108109```javascript110// BAD - vulnerable to SQL injection:111const result = await executeSql({ sqlQuery: `SELECT * FROM users WHERE id = ${userId}` });112113// GOOD - use parameterized queries:114const result = await executeSql({115 sqlQuery: "SELECT * FROM users WHERE id = $1",116 params: [userId]117});118```119120**Note: `params` only binds literal values** (strings, numbers, booleans, null). PostgreSQL placeholders cannot substitute identifiers (table or column names), SQL keywords (sort directions like `ASC`/`DESC`, operators), or other syntactic elements. For dynamic identifiers or keywords, validate the value against a strict allowlist (e.g. a fixed set of allowed table names, or a regex like `/^[a-z_][a-z0-9_]*$/`), then interpolate the validated value into the SQL string. Never interpolate free-form user text into identifier positions.121122```javascript123// BAD - $1 cannot bind a table name; this query fails to parse:124await executeSql({ sqlQuery: "SELECT * FROM $1 WHERE id = $2", params: [tableName, userId] });125126// GOOD - allowlist + interpolate identifiers, $N for values:127const ALLOWED_TABLES = ["users", "orders", "products"];128if (!ALLOWED_TABLES.includes(tableName)) throw new Error("invalid table");129await executeSql({130 sqlQuery: `SELECT * FROM ${tableName} WHERE id = $1`,131 params: [userId]132});133```134135**When `params` is NOT supported on `replit_database`** (multi-statement scripts only — `BEGIN; ...; COMMIT;`, migrations, batched DDL):1361371. **For independent statements that don't need shared transaction or session state**, you can split them into separate `executeSql` calls each with its own `params` (e.g., a sequence of unrelated INSERTs into different tables). Note that separate `executeSql` calls do NOT share a connection, transaction, or session — temp tables, `SET LOCAL` GUCs, and `BEGIN`/`COMMIT` will not carry across calls.1382. **For scripts that DO need transactional atomicity or session state** (most multi-statement writes, migrations, scripts that depend on temp tables), send the whole script as a single `executeSql` call without `params`. Validate any interpolated values against a strict allowlist (e.g. integers only, identifier whitelist) before embedding them in the SQL string. Treat any interpolated value as a potential injection vector — never interpolate free-form user text.139140**For warehouse targets** (`bigquery`, `databricks`, `snowflake`), `params` is NEVER supported. The runtime rejects any warehouse query that includes `params`, whether single-statement or multi-statement. You MUST either: (a) reject the user input if it cannot be validated, or (b) validate the value against a strict allowlist (e.g. integers only, fixed identifier set) before interpolating it into the SQL string. Never interpolate free-form user text into warehouse SQL.141142**Example:**143144```javascript145// Simple SELECT with parameters146const result = await executeSql({147 sqlQuery: "SELECT * FROM users WHERE id = $1",148 params: [userId]149});150if (result.success) {151 console.log(result.output);152}153154// Static query without user input (no params needed)155const result1b = await executeSql({ sqlQuery: "SELECT * FROM users LIMIT 5" });156157// CREATE TABLE (static DDL, no params needed)158const result2 = await executeSql({159 sqlQuery: `160 CREATE TABLE IF NOT EXISTS products (161 id SERIAL PRIMARY KEY,162 name VARCHAR(255) NOT NULL,163 price DECIMAL(10, 2)164 )165 `166});167168// INSERT data with parameters169const result3 = await executeSql({170 sqlQuery: "INSERT INTO products (name, price) VALUES ($1, $2)",171 params: [productName, productPrice]172});173174// UPDATE with parameters175const result3b = await executeSql({176 sqlQuery: "UPDATE products SET price = $1 WHERE name = $2",177 params: [newPrice, productName]178});179180// DELETE with parameters181const result3c = await executeSql({182 sqlQuery: "DELETE FROM products WHERE id = $1",183 params: [productId]184});185186// Read-only production query with parameters187const result4 = await executeSql({188 sqlQuery: "SELECT * FROM users WHERE active = $1",189 params: [true],190 environment: "production"191});192193// Data warehouse query with sampling194// (Note: `params` is NOT supported for warehouse targets; if `year` were195// dynamic, validate it as an integer literal before interpolating.)196const result5 = await executeSql({197 sqlQuery: "SELECT * FROM sales_data WHERE year = 2024",198 target: "bigquery",199 sampleSize: 100200});201202// Multi-statement transactional scripts (BEGIN ... COMMIT, temp tables,203// SET LOCAL, etc.) MUST run as a single executeSql call WITHOUT `params`204// — separate calls do NOT share a connection or transaction, so205// splitting a transaction across multiple calls would silently lose206// atomicity. If the script needs user-controlled values, validate them207// against a strict allowlist (integers only, identifier whitelist)208// before interpolating; never interpolate free-form user text.209const result = await executeSql({210 sqlQuery: `211 BEGIN;212 UPDATE users SET active = false WHERE last_login < '2024-01-01';213 UPDATE audit_log SET archived = true WHERE created_at < '2024-01-01';214 COMMIT;215 `,216});217```218219## Safety Features2202211. **Environment Isolation**: Development queries run against the development database; production queries are READ-ONLY against a read replica2222. **Stripe Protection**: Mutations to Stripe schema tables (stripe.*) are blocked2233. **Discussion Mode**: Mutating queries are blocked in Planning/Discussion mode2244. **Destructive Query Protection**: DROP, TRUNCATE, etc. are blocked via the skill callback path (use the tool interface directly for destructive operations that require user confirmation)225226## Best Practices2272281. **Prefer the built-in database**: Replit's built-in PostgreSQL database is always preferred over external services like Supabase. It supports rollback and integrates directly with the Replit product. Only use external database services if the user has specific requirements. The `pg` package should be installed already.2292. **Check before creating**: Call `checkDatabase()` before `createDatabase()` to avoid unnecessary operations2303. **ALWAYS use parameterized queries with user input on the `replit_database` single-statement path**: When `target` is `replit_database` (the default) and `sqlQuery` is a single statement, you MUST use the `params` argument with `$1`, `$2`, etc. placeholders for any user-provided literal value. NEVER use string interpolation, template literals, or concatenation for values in that case. For dynamic identifiers (table/column names) or SQL keywords (sort directions, operators), validate against a strict allowlist before interpolating since `params` cannot bind those. For warehouse targets and multi-statement transactional scripts where `params` is not supported, send the whole script as a single `executeSql` call and validate any interpolated values against a strict allowlist; only split into separate calls when the statements are truly independent and don't share transaction or session state.2314. **Test queries first**: Run SELECT queries before INSERT/UPDATE/DELETE2325. **Keep backups**: Important data should be backed up before destructive operations233234## Environment Variables235236After creating a database, these environment variables are available:237238- `DATABASE_URL`: Full connection string239- `PGHOST`: Database host240- `PGPORT`: Database port (5432)241- `PGUSER`: Database username242- `PGPASSWORD`: Database password243- `PGDATABASE`: Database name244245## Example Workflow246247```javascript248// 1. Check if database exists249const status = await checkDatabase();250251if (!status.provisioned) {252 // 2. Create database253 const createResult = await createDatabase();254 if (!createResult.success) {255 console.log(`Failed: ${createResult.message}`);256 }257}258259// 3. Create schema260await executeSql({261 sqlQuery: `262 CREATE TABLE IF NOT EXISTS users (263 id SERIAL PRIMARY KEY,264 email VARCHAR(255) UNIQUE NOT NULL,265 created_at TIMESTAMP DEFAULT NOW()266 )267 `268});269270// 4. Insert data (using parameterized query)271await executeSql({272 sqlQuery: "INSERT INTO users (email) VALUES ($1)",273 params: ['user@example.com']274});275276// 5. Query data277const result = await executeSql({ sqlQuery: "SELECT * FROM users" });278console.log(result.output);279```280281## Limitations282283- Production queries are READ-ONLY (SELECT only) — INSERT, UPDATE, DELETE, and DDL statements will fail284- Production environment is only supported for the "replit_database" target (not data warehouses)285- Cannot modify Stripe schema tables (read-only)286- Destructive queries (DROP, TRUNCATE, etc.) are blocked via the skill callback path287- Mutating queries blocked in Planning mode288289## Rollbacks290291As stated in the diagnostic skills, the development database support rollbacks. Open that skill for more information.