Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ Series and episode data comes from [TMDB](https://www.themoviedb.org/).
5. **First user:** create the initial admin with
`php bin/create_user.php <username> <password> --admin`.

**Updating:** after pulling a new version, apply pending schema changes with
`php bin/migrate.php` (idempotent; see
[`sql/migrations/README.md`](sql/migrations/README.md)).

## Translations

UI translations are flat JSON files in `lang/` (English is the source
Expand Down
70 changes: 70 additions & 0 deletions bin/migrate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#!/usr/bin/php
<?php

declare(strict_types=1);

/**
* Applies pending database migrations from sql/migrations/ (see the README
* there). Idempotent and safe to re-run: already applied migrations are
* tracked in the schema_migrations table and skipped.
*
* Usage:
* php bin/migrate.php # apply all pending migrations
* php bin/migrate.php --status # read-only: show applied/pending state
*/

use Catenvis\Config;
use Catenvis\Database;
use Catenvis\Migrator;

if (PHP_SAPI !== 'cli') {
fwrite(STDERR, "Can only be run from the command line.\n");
exit(1);
}

$projectDir = dirname(__DIR__);
require $projectDir . '/vendor/autoload.php';

$status = false;
foreach (array_slice($argv, 1) as $arg) {
if ($arg === '--status') {
$status = true;
continue;
}
fwrite(STDERR, "Unknown argument: $arg\nUsage: php bin/migrate.php [--status]\n");
exit(1);
}

$config = Config::load($projectDir . '/config/config.php');
/** @var array<string, mixed> $dbConfig */
$dbConfig = $config->get('db', []);
$migrator = new Migrator(new Database($dbConfig), $projectDir . '/sql/migrations');

try {
// Applied-but-missing files hint at a renamed migration - warn, never fail.
foreach ($migrator->missingMigrations() as $name) {
fwrite(STDERR, "Warning: migration $name is recorded as applied but missing on disk.\n");
}

if ($status) {
printf("Applied: %d\n", count($migrator->appliedMigrations()));
$pending = $migrator->pendingMigrations();
if ($pending === []) {
echo "Nothing pending.\n";
} else {
echo "Pending:\n";
foreach ($pending as $name) {
echo " $name\n";
}
}
exit(0);
}

$applied = $migrator->migrate(static function (string $name): void {
printf("Applied %s\n", $name);
});
echo $applied === [] ? "Nothing to apply.\n" : sprintf("%d migration(s) applied.\n", count($applied));
} catch (Throwable $e) {
fwrite(STDERR, $e->getMessage() . "\n");
exit(1);
}
17 changes: 13 additions & 4 deletions deploy/SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ to the MySQL database configured in `config/config.php`.

Create a MySQL/MariaDB database and user and load the schema. The exact
statements and load command are documented in the header of
[`sql/schema.sql`](../sql/schema.sql). Catenvis has no migration tooling -
`sql/schema.sql` is the single source of truth for the schema.
[`sql/schema.sql`](../sql/schema.sql) - it always contains the full current
schema, so a fresh install needs nothing else. Later updates apply schema
changes as deltas via `php bin/migrate.php` (see "Ongoing operation").

## 2. Prepare the server (via `ssh <your-server>`)

Expand Down Expand Up @@ -98,5 +99,13 @@ deploy/deploy.sh
```

- `config/config.php` and `catenvis.log` on the server are left untouched.
- There are no automatic database migrations: `sql/schema.sql` is the single
source of truth for the schema, so apply schema changes from there.
- Database migrations are not applied automatically. After deploying a
version that ships new files in `sql/migrations/`, run on the server:

```bash
php /var/www/catenvis/bin/migrate.php --status # read-only preview
php /var/www/catenvis/bin/migrate.php # apply pending migrations
```

The command is idempotent and safe to re-run. If several instances share
one database, deploy the new code everywhere first, then migrate once.
41 changes: 41 additions & 0 deletions sql/migrations/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Database migrations

Delta files that bring an **existing** installation up to the current schema.
Fresh installs don't need them: [`sql/schema.sql`](../schema.sql) always
contains the full current schema (including markers for the migrations
already folded in).

## Running

```bash
php bin/migrate.php --status # read-only: show applied/pending state
php bin/migrate.php # apply all pending migrations
```

Applied migrations are tracked in the `schema_migrations` table, so the
command is idempotent and safe to re-run. Run it after every update that
ships new files in this directory. When one database is shared by several
instances, deploy the new code everywhere first, then migrate once.

## Naming

`NNN_short_description.sql` — zero-padded sequence number, next = highest + 1
(e.g. `001_add_episode_overview.sql`). Files are applied in byte-sorted
filename order.

## Rules

- One logical change per file.
- Plain SQL only: statements separated by `;`; `--`, `#` and `/* ... */`
comments are fine. No `DELIMITER` switching (so no stored routines or
triggers with compound bodies).
- **Never edit or rename a migration once it may have been applied
somewhere** — the filename is its identity in `schema_migrations`.
- MySQL DDL is not transactional: if a statement fails mid-file, earlier
statements of that file stay applied and the migration stays unrecorded.
Prefer re-runnable statements (e.g. `ADD COLUMN IF NOT EXISTS` on
MariaDB/MySQL 8+) so a fixed migration can simply be retried.
- **Every migration must also be folded into `sql/schema.sql`**: apply the
same change to the table definitions there and add the marker line
`INSERT INTO schema_migrations (migration) VALUES ('NNN_....sql');` so
fresh installs don't re-apply it.
18 changes: 18 additions & 0 deletions sql/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@
--
-- Step 2: load this schema into the catenvis database, e.g.:
-- mysql -h <db-host> -u catenvis -p catenvis < sql/schema.sql
--
-- This file always reflects the CURRENT full schema - a fresh install needs
-- nothing else. Schema changes additionally ship as delta files in
-- sql/migrations/ so existing installations can catch up with
-- "php bin/migrate.php" (see sql/migrations/README.md).

SET NAMES utf8mb4;

Expand Down Expand Up @@ -136,3 +141,16 @@ CREATE TABLE IF NOT EXISTS login_attempts (
KEY idx_login_attempts_ip (ip, attempted_at),
KEY idx_login_attempts_username (username, attempted_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- Applied schema migrations (see sql/migrations/README.md). bin/migrate.php
-- creates this table on existing installations if it is missing.
CREATE TABLE IF NOT EXISTS schema_migrations (
migration VARCHAR(255) NOT NULL,
applied_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (migration)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- Migrations already folded into this file count as applied on a fresh
-- install. When folding in a migration, also add its marker here:
-- INSERT INTO schema_migrations (migration) VALUES ('NNN_description.sql');
-- (none yet)
Loading
Loading