feat: M2 database migration runner with Phinx and initial schema
This commit is contained in:
1 parent
d3e8cf8fae
commit
93d991703c
20 files changed
+2000
-14
No files matched your search
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Initial schema: folders mirroring the managed library tree.
|
||||
*
|
||||
* Folders form a recursive tree structure where each folder has an optional
|
||||
* parent folder. The root folder has parent_id = NULL. The path is stored
|
||||
* as a materialized absolute path from the library root for efficient queries.
|
||||
*/
|
||||
final class CreateFolders extends AbstractMigration
|
||||
{
|
||||
public function change(): void
|
||||
{
|
||||
$this->table('folders', ['id' => 'id', 'primary_key' => 'id'])
|
||||
->addColumn('parent_id', 'integer', ['null' => true, 'signed' => false])
|
||||
->addColumn('name', 'string', ['limit' => 255])
|
||||
->addColumn('path', 'text', ['null' => false])
|
||||
->addColumn('added_at', 'timestamp', ['default' => 'CURRENT_TIMESTAMP', 'timezone' => true])
|
||||
->addForeignKey('parent_id', 'folders', 'id', ['delete' => 'CASCADE', 'update' => 'NO_ACTION'])
|
||||
->addIndex(['parent_id', 'name'], ['unique' => true, 'name' => 'idx_folders_parent_name'])
|
||||
->addIndex(['path'], ['unique' => true, 'name' => 'idx_folders_path'])
|
||||
->create();
|
||||
|
||||
// Root folder entry (will be created by application on first run)
|
||||
// Application ensures root folder exists with path = '/' and parent_id = NULL
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Schema: models as logical catalog items.
|
||||
*
|
||||
* Each model belongs to a folder and represents a logical 3D model consisting
|
||||
* of one or more files (STL parts, images, documents). Models have metadata
|
||||
* including categories, tags, notes, and optional source URL.
|
||||
*/
|
||||
final class CreateModels extends AbstractMigration
|
||||
{
|
||||
public function change(): void
|
||||
{
|
||||
$this->table('models', ['id' => 'id', 'primary_key' => 'id'])
|
||||
->addColumn('folder_id', 'integer', ['null' => false, 'signed' => false])
|
||||
->addColumn('name', 'string', ['limit' => 255])
|
||||
->addColumn('notes', 'text', ['null' => true])
|
||||
->addColumn('source_url', 'string', ['limit' => 2048, 'null' => true])
|
||||
->addColumn('added_at', 'timestamp', ['default' => 'CURRENT_TIMESTAMP', 'timezone' => true])
|
||||
->addColumn('updated_at', 'timestamp', ['default' => 'CURRENT_TIMESTAMP', 'timezone' => true])
|
||||
->addForeignKey('folder_id', 'folders', 'id', ['delete' => 'RESTRICT', 'update' => 'NO_ACTION'])
|
||||
->addIndex(['folder_id', 'name'], ['unique' => true, 'name' => 'idx_models_folder_name'])
|
||||
->addIndex(['added_at'], ['name' => 'idx_models_added_at'])
|
||||
->create();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Schema: files with hash, size, and lifecycle status.
|
||||
*
|
||||
* Files belong to a model and represent physical files in the managed library.
|
||||
* Each file has a SHA-256 hash for change detection, size for space tracking,
|
||||
* and a lifecycle status (Present, Missing, Updated, Duplicate).
|
||||
* The managed path is relative to the library root.
|
||||
*/
|
||||
final class CreateFiles extends AbstractMigration
|
||||
{
|
||||
public function change(): void
|
||||
{
|
||||
$this->table('files', ['id' => 'id', 'primary_key' => 'id'])
|
||||
->addColumn('model_id', 'integer', ['null' => false, 'signed' => false])
|
||||
->addColumn('folder_id', 'integer', ['null' => false, 'signed' => false])
|
||||
->addColumn('name', 'string', ['limit' => 255])
|
||||
->addColumn('managed_path', 'text', ['null' => false])
|
||||
->addColumn('hash', 'string', ['limit' => 64, 'null' => true]) // SHA-256 hex
|
||||
->addColumn('size', 'biginteger', ['null' => true, 'signed' => false])
|
||||
->addColumn('mime_type', 'string', ['limit' => 128, 'null' => true])
|
||||
->addColumn('status', 'string', ['limit' => 32, 'default' => 'Present'])
|
||||
->addColumn('added_at', 'timestamp', ['default' => 'CURRENT_TIMESTAMP', 'timezone' => true])
|
||||
->addColumn('updated_at', 'timestamp', ['default' => 'CURRENT_TIMESTAMP', 'timezone' => true])
|
||||
->addForeignKey('model_id', 'models', 'id', ['delete' => 'CASCADE', 'update' => 'NO_ACTION'])
|
||||
->addForeignKey('folder_id', 'folders', 'id', ['delete' => 'RESTRICT', 'update' => 'NO_ACTION'])
|
||||
->addIndex(['model_id', 'managed_path'], ['unique' => true, 'name' => 'idx_files_model_path'])
|
||||
->addIndex(['hash'], ['name' => 'idx_files_hash'])
|
||||
->addIndex(['status'], ['name' => 'idx_files_status'])
|
||||
->addIndex(['folder_id'], ['name' => 'idx_files_folder_id'])
|
||||
->create();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Schema: categories, tags, and model-tag associations.
|
||||
*
|
||||
* Categories are hierarchical (optional parent) for broad classification.
|
||||
* Tags are flat, free-form labels. Models can have multiple tags via a
|
||||
* many-to-many association table. Each model has exactly one category (nullable).
|
||||
*/
|
||||
final class CreateCategoriesTags extends AbstractMigration
|
||||
{
|
||||
public function change(): void
|
||||
{
|
||||
// Categories - hierarchical
|
||||
$this->table('categories', ['id' => 'id', 'primary_key' => 'id'])
|
||||
->addColumn('parent_id', 'integer', ['null' => true, 'signed' => false])
|
||||
->addColumn('name', 'string', ['limit' => 128])
|
||||
->addColumn('description', 'text', ['null' => true])
|
||||
->addForeignKey('parent_id', 'categories', 'id', ['delete' => 'SET NULL', 'update' => 'NO_ACTION'])
|
||||
->addIndex(['parent_id', 'name'], ['unique' => true, 'name' => 'idx_categories_parent_name'])
|
||||
->create();
|
||||
|
||||
// Tags - flat
|
||||
$this->table('tags', ['id' => 'id', 'primary_key' => 'id'])
|
||||
->addColumn('name', 'string', ['limit' => 128])
|
||||
->addIndex(['name'], ['unique' => true, 'name' => 'idx_tags_name'])
|
||||
->create();
|
||||
|
||||
// Model-Tag many-to-many
|
||||
$this->table('model_tags', ['id' => false, 'primary_key' => ['model_id', 'tag_id']])
|
||||
->addColumn('model_id', 'integer', ['null' => false, 'signed' => false])
|
||||
->addColumn('tag_id', 'integer', ['null' => false, 'signed' => false])
|
||||
->addForeignKey('model_id', 'models', 'id', ['delete' => 'CASCADE', 'update' => 'NO_ACTION'])
|
||||
->addForeignKey('tag_id', 'tags', 'id', ['delete' => 'CASCADE', 'update' => 'NO_ACTION'])
|
||||
->create();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Schema: uploaded thumbnails.
|
||||
*
|
||||
* Thumbnails are user-uploaded images associated with models.
|
||||
* They are stored in the managed library under a thumbnails/ subdirectory
|
||||
* and tracked separately from model files.
|
||||
*/
|
||||
final class CreateThumbnails extends AbstractMigration
|
||||
{
|
||||
public function change(): void
|
||||
{
|
||||
$this->table('thumbnails', ['id' => 'id', 'primary_key' => 'id'])
|
||||
->addColumn('model_id', 'integer', ['null' => false, 'signed' => false])
|
||||
->addColumn('managed_path', 'text', ['null' => false])
|
||||
->addColumn('hash', 'string', ['limit' => 64, 'null' => false]) // SHA-256 hex
|
||||
->addColumn('size', 'biginteger', ['null' => false, 'signed' => false])
|
||||
->addColumn('mime_type', 'string', ['limit' => 128, 'null' => false])
|
||||
->addColumn('width', 'integer', ['null' => true, 'signed' => false])
|
||||
->addColumn('height', 'integer', ['null' => true, 'signed' => false])
|
||||
->addColumn('uploaded_at', 'timestamp', ['default' => 'CURRENT_TIMESTAMP', 'timezone' => true])
|
||||
->addForeignKey('model_id', 'models', 'id', ['delete' => 'CASCADE', 'update' => 'NO_ACTION'])
|
||||
->addIndex(['model_id'], ['name' => 'idx_thumbnails_model_id'])
|
||||
->addIndex(['hash'], ['unique' => true, 'name' => 'idx_thumbnails_hash'])
|
||||
->create();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Schema: import jobs and per-file import items.
|
||||
*
|
||||
* Import jobs track the overall import operation with progress, totals,
|
||||
* and status. Import items represent individual files being imported,
|
||||
* with their source path, destination, status, and any failure reason.
|
||||
*/
|
||||
final class CreateImportJobs extends AbstractMigration
|
||||
{
|
||||
public function change(): void
|
||||
{
|
||||
// Import jobs - overall operation tracking
|
||||
$this->table('import_jobs', ['id' => 'id', 'primary_key' => 'id'])
|
||||
->addColumn('source_path', 'text', ['null' => false])
|
||||
// Status: Pending, Scanning, Copying, Completed, Failed, Cancelled
|
||||
->addColumn('status', 'string', ['limit' => 32, 'default' => 'Pending'])
|
||||
->addColumn('total_files', 'integer', ['null' => false, 'default' => 0, 'signed' => false])
|
||||
->addColumn('processed_files', 'integer', ['null' => false, 'default' => 0, 'signed' => false])
|
||||
->addColumn('total_bytes', 'biginteger', ['null' => false, 'default' => 0, 'signed' => false])
|
||||
->addColumn('copied_bytes', 'biginteger', ['null' => false, 'default' => 0, 'signed' => false])
|
||||
->addColumn('started_at', 'timestamp', ['null' => true, 'timezone' => true])
|
||||
->addColumn('completed_at', 'timestamp', ['null' => true, 'timezone' => true])
|
||||
->addColumn('created_at', 'timestamp', ['default' => 'CURRENT_TIMESTAMP', 'timezone' => true])
|
||||
->addIndex(['status'], ['name' => 'idx_import_jobs_status'])
|
||||
->addIndex(['created_at'], ['name' => 'idx_import_jobs_created_at'])
|
||||
->create();
|
||||
|
||||
// Import items - per-file tracking
|
||||
$this->table('import_items', ['id' => 'id', 'primary_key' => 'id'])
|
||||
->addColumn('job_id', 'integer', ['null' => false, 'signed' => false])
|
||||
->addColumn('source_path', 'text', ['null' => false])
|
||||
->addColumn('managed_path', 'text', ['null' => true])
|
||||
->addColumn('model_id', 'integer', ['null' => true, 'signed' => false])
|
||||
->addColumn('hash', 'string', ['limit' => 64, 'null' => true]) // SHA-256 hex
|
||||
->addColumn('size', 'biginteger', ['null' => true, 'signed' => false])
|
||||
// Status: Pending, Copying, Completed, Failed, Skipped
|
||||
->addColumn('status', 'string', ['limit' => 32, 'default' => 'Pending'])
|
||||
->addColumn('failure_reason', 'text', ['null' => true])
|
||||
->addColumn('started_at', 'timestamp', ['null' => true, 'timezone' => true])
|
||||
->addColumn('completed_at', 'timestamp', ['null' => true, 'timezone' => true])
|
||||
->addForeignKey('job_id', 'import_jobs', 'id', ['delete' => 'CASCADE', 'update' => 'NO_ACTION'])
|
||||
->addForeignKey('model_id', 'models', 'id', ['delete' => 'SET NULL', 'update' => 'NO_ACTION'])
|
||||
->addIndex(['job_id', 'status'], ['name' => 'idx_import_items_job_status'])
|
||||
->addIndex(['job_id', 'managed_path'], ['name' => 'idx_import_items_job_path'])
|
||||
->addIndex(['hash'], ['name' => 'idx_import_items_hash'])
|
||||
->create();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Seed\AbstractSeed;
|
||||
|
||||
/**
|
||||
* Seed script for default reference data.
|
||||
*
|
||||
* Populates initial categories and tags. Safe to re-run (idempotent).
|
||||
*/
|
||||
final class DefaultDataSeed extends AbstractSeed
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
// Seed default categories
|
||||
$categories = [
|
||||
[
|
||||
'name' => 'Aircraft',
|
||||
'description' => 'Planes, helicopters, and drones',
|
||||
],
|
||||
[
|
||||
'name' => 'Automotive',
|
||||
'description' => 'Cars, trucks, and vehicles',
|
||||
],
|
||||
[
|
||||
'name' => 'Characters',
|
||||
'description' => 'Humanoid and creature models',
|
||||
],
|
||||
[
|
||||
'name' => 'Props',
|
||||
'description' => 'Objects and accessories',
|
||||
],
|
||||
[
|
||||
'name' => 'Terrain',
|
||||
'description' => 'Landscapes, buildings, and scenery',
|
||||
],
|
||||
[
|
||||
'name' => 'Tools',
|
||||
'description' => 'Functional items and gadgets',
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($categories as $category) {
|
||||
$exists = $this->getAdapter()->fetchRow(
|
||||
"SELECT id FROM categories WHERE name = '"
|
||||
. $category['name'] . "'"
|
||||
);
|
||||
if (!$exists) {
|
||||
$this->getAdapter()->execute(
|
||||
"INSERT INTO categories (name, description) VALUES ('"
|
||||
. $category['name'] . "', '"
|
||||
. $category['description'] . "')"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Seed default tags
|
||||
$tags = [
|
||||
['name' => 'wip'],
|
||||
['name' => 'completed'],
|
||||
['name' => 'high-poly'],
|
||||
['name' => 'low-poly'],
|
||||
['name' => 'print-ready'],
|
||||
['name' => 'needs-supports'],
|
||||
['name' => 'articulated'],
|
||||
['name' => 'modular'],
|
||||
['name' => 'multi-part'],
|
||||
['name' => 'single-piece'],
|
||||
];
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
$exists = $this->getAdapter()->fetchRow(
|
||||
"SELECT id FROM tags WHERE name = '" . $tag['name'] . "'"
|
||||
);
|
||||
if (!$exists) {
|
||||
$this->getAdapter()->execute(
|
||||
"INSERT INTO tags (name) VALUES ('" . $tag['name'] . "')"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user