# Database Skill ## Purpose Use this skill for PDO, repositories, SQL, migrations, transactions, database configuration, and persistence rules. --- ## Database Stack Preferred database access: - PDO - Repositories or data access classes - Optional SQLite, MySQL, or SQL Server through PDO - Prepared statements for all untrusted values --- ## Database Class API `Core\Database` wraps PDO and exposes these methods: | Method | Returns | Description | |--------|---------|-------------| | `query(string $sql, array $params = [])` | `array` | Runs a SELECT and returns all rows as associative arrays | | `first(string $sql, array $params = [])` | `?array` | Runs a SELECT and returns the first row, or `null` | | `execute(string $sql, array $params = [])` | `bool` | Runs INSERT / UPDATE / DELETE | | `lastInsertId()` | `string` | Returns the last auto-increment ID as a string — cast to `int` for integer PKs | | `transaction(callable $fn)` | `mixed` | Runs `$fn($db)` inside a transaction; commits on success, rolls back and rethrows on failure | | `pdo()` | `PDO` | Returns the raw PDO instance for advanced use | `lastInsertId()` is only meaningful immediately after an `execute()` INSERT on the same connection. Calling it at any other point returns `"0"`. Typical INSERT + ID retrieval in a repository: ```php public function create(Employee $employee): int { $this->database->execute( 'INSERT INTO employees (first_name, email) VALUES (:first_name, :email)', ['first_name' => $employee->firstName, 'email' => $employee->email] ); return (int) $this->database->lastInsertId(); } ``` --- ## Database Access Rules Use PDO or a well-maintained database abstraction layer/ORM. Never concatenate untrusted input into SQL. Bad: ```php $sql = "SELECT * FROM users WHERE id = " . $_GET['id']; ``` Good: ```php $stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id'); $stmt->bindValue(':id', $id, PDO::PARAM_INT); $stmt->execute(); $user = $stmt->fetch(PDO::FETCH_ASSOC); ``` Rules: - Use prepared statements and bound parameters. - Validate input before using it in writes. - Keep SQL out of templates. - Keep database access out of controllers where practical. - Use transactions when multiple writes must succeed or fail together. - Do not rely only on client-side validation. - Do not expose raw database errors to users. --- ## Transactions Use `Database::transaction()` when multiple writes must succeed or fail together. It begins a transaction, runs the callback, commits on success, and rolls back and rethrows on any `Throwable`. ```php $database->transaction(function (Database $db) use ($order): void { $orders->create($order); $auditLog->record('order.created', $order->id()); }); ``` Do not call `$database->pdo()->beginTransaction()` directly for transaction management — use `transaction()` instead. Reserve `pdo()` for driver-specific features that have no `Database` API equivalent. --- ## Repository Rules - Put persistence logic in repositories or data access classes. - Keep repositories focused around a table, aggregate, or use case. - Do not let repositories render HTML. - Do not let repositories read directly from `$_GET`, `$_POST`, or other superglobals. - Return domain objects, entities, DTOs, arrays, or ViewModels according to existing project convention. - Prefer explicit methods such as `findById`, `findAllActive`, and `save` over generic magic calls. --- ## Migration System The migration system is made up of three files: - `core/Migration.php` — abstract base class all migration files extend - `core/MigrationManager.php` — runs, rolls back, and tracks applied migrations - `core/helpers.php` — provides the `migration_manager()` helper that wires a `MigrationManager` to the app database and the `database/migrations/` path The CLI entry point is `scripts/migrate.php`. Run it from the project root: | Command | Description | |---------|-------------| | `php scripts/migrate.php up` | Run all pending migrations | | `php scripts/migrate.php down [steps]` | Roll back the last N migrations (default: 1) | | `php scripts/migrate.php status` | Show which migrations have run and when | | `php scripts/migrate.php make ` | Scaffold a new timestamped migration file | | `php scripts/migrate.php fresh` | Roll back everything and re-run from scratch | | `php scripts/migrate.php fresh --seed` | Same as fresh, then run the employee seed | Migration files live in `database/migrations/` and are named `YYYYMMDD_HHMMSS_.php`. Each file must return a `Migration` instance: ```php execute('CREATE TABLE ...'); } public function down(Database $database): void { $database->execute('DROP TABLE IF EXISTS ...'); } }; ``` --- ## Migration Rules - Keep migrations small and reversible when practical. - Document destructive migrations clearly. - Do not mix schema changes with unrelated feature logic. - Use project migration conventions before inventing new ones. - Support SQLite/MySQL/SQL Server differences explicitly when the project targets multiple engines. - Do not use database-specific SQL in framework or migration code. `INSERT OR IGNORE` (SQLite) and `INSERT IGNORE` (MySQL) are not portable — use a check-then-insert pattern instead. - Migration records in the `migrations` table are permanent. A row means "this migration ran against this database." Deleting a migration file does not remove the record and does not allow the migration to be re-run. To intentionally re-run a migration, delete its row from the `migrations` table manually — this makes the action explicit. - To reset completely, use `php scripts/migrate.php fresh`, which rolls back all migrations in reverse order and re-runs them from scratch. --- ## SQL Safety Checklist Before completing database work, verify: - [ ] SQL uses prepared statements or a safe query builder. - [ ] Untrusted values are never concatenated into SQL. - [ ] Writes validate input server-side. - [ ] Multi-step writes use transactions where needed. - [ ] Database errors are logged safely and not displayed raw to users. - [ ] Schema changes are documented. - [ ] Tests or verification steps cover the changed behavior. --- ## Database Configuration - Keep database credentials out of source control. - Prefer environment variables or ignored local config files for secrets. - Provide safe examples such as `.env.example`. - Do not commit production DSNs, passwords, tokens, or private keys. Example `.env.example`: ```text APP_ENV=local APP_DEBUG=true DATABASE_URL=mysql://user:password@localhost:3306/app ```