Files
rspade_system/app/RSpade/man/model_normalization.txt
root f6fac6c4bc Fix bin/publish: copy docs.dist from project root
Fix bin/publish: use correct .env path for rspade_system
Fix bin/publish script: prevent grep exit code 1 from terminating script

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-21 02:08:33 +00:00

475 lines
15 KiB
Plaintext
Executable File

DATABASE NORMALIZATION & MIGRATION SYSTEM
==========================================
RSpade Framework Database Normalization Documentation
OVERVIEW
--------
RSpade enforces an opinionated database schema through a two-layer system:
1. **SqlQueryTransformer** - Rewrites DDL statements during migrations to enforce
consistent column types and character sets at CREATE/ALTER time
2. **migrate:normalize_schema** - Adds framework-required columns and fixes drift
in existing tables
This eliminates foreign key type mismatches and ensures consistent schema across
all environments without manual ALTER statements.
ENFORCED COLUMN TYPE STANDARDS
-------------------------------
### Integer Types
✓ BIGINT - All integer columns (IDs, counts, foreign keys)
✓ TINYINT(1) - Boolean values only (true/false)
✗ INT, INTEGER - Automatically converted to BIGINT
✗ MEDIUMINT - Automatically converted to BIGINT
✗ SMALLINT - Automatically converted to BIGINT
✗ UNSIGNED - Removed (framework uses signed integers)
### Floating Point Types
✓ DOUBLE - All floating point numbers
✓ DECIMAL(p,s) - Exact precision (money, percentages)
✗ FLOAT, REAL - Automatically converted to DOUBLE
### String Types
✓ VARCHAR(n) - Variable length strings (with utf8mb4)
✓ LONGTEXT - Large text content (with utf8mb4)
✓ JSON - Structured data (when appropriate)
✗ CHAR(n) - Automatically converted to VARCHAR(n)
✗ TEXT, MEDIUMTEXT - Automatically converted to LONGTEXT
### Temporal Types
✓ DATE - Date only (YYYY-MM-DD)
✓ DATETIME(3) - Date and time with millisecond precision
✓ TIMESTAMP(3) - Unix timestamp with millisecond precision
✗ YEAR - FORBIDDEN (use INT or DATE)
✗ TIME - FORBIDDEN (use DATETIME)
### Binary Types
✓ BLOB, LONGBLOB - Binary data (images, files)
### FORBIDDEN Types (Throw Exception)
✗ ENUM - Use VARCHAR with validation instead
✗ SET - Use JSON or separate table
✗ YEAR - Use INT or DATE
✗ TIME - Use DATETIME
RATIONALE: ENUM and SET cannot be modified without table locks, causing deployment
issues in production. YEAR and TIME types are too limited and cause conversion issues.
CHARACTER SET ENFORCEMENT
-------------------------
All string columns (VARCHAR, TEXT) are automatically set to:
• CHARACTER SET: utf8mb4
• COLLATION: utf8mb4_unicode_ci
This ensures full Unicode support including emojis and prevents character set
mismatches in foreign keys.
MIGRATION FLOW
--------------
When you run `php artisan migrate`:
1. **Pre-Migration Normalization**
- Fixes existing tables created before transformer implementation
- Converts tables to utf8mb4 character set
- Ensures consistent schema before new migrations
2. **SqlQueryTransformer Enabled**
- Intercepts all DB::statement() calls
- Rewrites CREATE TABLE and ALTER TABLE queries
- Transforms types (INT→BIGINT, FLOAT→DOUBLE, etc.)
- Adds CHARACTER SET utf8mb4 to all string columns
- Validates forbidden types (throws exception for ENUM, SET, etc.)
3. **Run Migrations**
- Migrations execute with transformed SQL
- Tables created with correct types from the start
- Foreign keys work immediately (no type mismatches)
4. **Post-Migration Normalization**
- Adds framework-required columns to new tables:
• created_at, updated_at (with millisecond precision)
• created_by, updated_by (audit columns)
• deleted_by (for soft deletes)
• Trait-specific columns (site_id, version, etc.)
- Creates indexes on timestamp columns
- Upgrades DATETIME/TIMESTAMP precision to milliseconds
- Syncs Query/Join attribute indexes (see AUTOMATIC INDEX MANAGEMENT below)
5. **Regenerate Constants** (Dev Mode Only)
- Updates model constants from database schema
- Exports enum values to JavaScript
- Generates IDE type hints
6. **SqlQueryTransformer Disabled**
- Transformer only active during migration runs
- Normal queries unaffected
TRANSFORMATION EXAMPLES
-----------------------
### Example 1: Integer Type Transformation
User writes:
```sql
CREATE TABLE posts (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT,
views INT UNSIGNED,
is_published TINYINT(1),
FOREIGN KEY (user_id) REFERENCES users(id)
);
```
Transformer rewrites to:
```sql
CREATE TABLE posts (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT,
views BIGINT,
is_published TINYINT(1),
FOREIGN KEY (user_id) REFERENCES users(id)
);
```
Result: Foreign key works immediately (both columns BIGINT).
### Example 2: String Type Transformation
User writes:
```sql
CREATE TABLE users (
name VARCHAR(255),
bio TEXT,
code CHAR(10)
);
```
Transformer rewrites to:
```sql
CREATE TABLE users (
name VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
bio LONGTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
code VARCHAR(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
);
```
Result: Consistent character sets, no FK issues on string columns.
### Example 3: Forbidden Type Rejection
User writes:
```sql
CREATE TABLE products (
status ENUM('draft', 'published', 'archived')
);
```
Result: Migration fails with exception:
```
ENUM column type is forbidden in RSpade. Use VARCHAR with validation instead.
ENUMs cannot be modified without table locks and cause deployment issues.
```
Developer fixes by using VARCHAR:
```sql
CREATE TABLE products (
status VARCHAR(20) -- Add validation in model
);
```
FRAMEWORK-REQUIRED COLUMNS
--------------------------
normalize_schema ensures these columns exist on ALL tables (except excluded tables):
### Audit Columns
• created_at TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3)
• updated_at TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)
• created_by BIGINT NULL
• updated_by BIGINT NULL
• deleted_by BIGINT NULL (if deleted_at exists)
### Indexes
• INDEX created_at(created_at)
• INDEX updated_at(updated_at)
### Trait-Specific Columns
• site_id BIGINT (for Siteable trait)
• version BIGINT (for Versionable/Ajaxable traits)
Excluded Tables: migrations, api_clients, sessions
AUTOMATIC INDEX MANAGEMENT
---------------------------
RSX automatically creates and maintains database indexes based on #[Query] and
#[Join] attributes declared in model methods. This eliminates manual index
management and ensures all queries have appropriate indexes.
### How It Works
During database normalization (and manifest rebuild in dev mode), the framework:
1. **Scans Models** - Finds all methods with #[Query] or #[Join] attributes
2. **Collects Required Indexes** - Extracts index column lists from attributes
3. **Detects Covering Indexes** - Checks if manual indexes already cover requirements
4. **Removes Redundancy** - Filters overlapping index requirements
5. **Creates Missing Indexes** - Adds only indexes not covered by existing ones
6. **Removes Obsolete Indexes** - Drops auto-generated indexes no longer needed
### Index Naming Convention
Auto-generated indexes are prefixed with `__rsx_query_autoindex_` followed by
a sequential number. These indexes are managed automatically by the framework.
Manual indexes (without this prefix) are never modified. If a manual index
covers the columns required by a Query attribute, no auto-index is created.
### Covering Index Detection
The framework understands that index (a,b,c) can efficiently support queries on:
- (a) alone
- (a,b) together
- (a,b,c) together
But NOT efficiently on:
- (b) alone
- (c) alone
- (b,c) together
If you have #[Query(index: ['user_id', 'status'])] and a manual index already
exists on (user_id, status, created_at), the framework recognizes the manual
index covers your requirement and does not create an auto-index.
### Redundant Index Removal
If multiple Query attributes would create overlapping indexes, the framework
keeps only the more specific ones:
- #[Query(index: ['user_id'])]
- #[Query(index: ['user_id', 'status'])]
Result: Only create index on (user_id, status) because it covers both queries.
### Query Attribute Syntax
Declare required indexes directly in model methods:
```php
class User_Model extends Rsx_Model_Abstract
{
// Simple single-column index
#[Query(index: ['status'])]
public static function find_by_status(int $status)
{
return static::where('status', $status)->get();
}
// Composite index (order matters!)
#[Query(index: ['category_id', 'status'])]
public static function find_by_category(int $category_id)
{
return static::where('category_id', $category_id)
->where('status', static::STATUS_ACTIVE)
->get();
}
}
```
### Join Attribute Syntax
Joins specify the target table and index to create on that table:
```php
class User_Model extends Rsx_Model_Abstract
{
#[Join(
target_table: 'user_logins',
index: ['user_id', 'logged_in_at']
)]
public static function join_user_logins_recent($query, array $conditions = [])
{
return $query->leftJoin('user_logins', function($join) {
$join->on('user_logins.user_id', '=', 'users.id')
->where('user_logins.logged_in_at', '>',
DB::raw('NOW() - INTERVAL 30 DAY'));
});
}
}
```
The index is created on the TARGET table (user_logins), not the source table.
### When Indexes Are Created
**Development Mode**: Indexes sync automatically during manifest rebuild
```bash
php artisan rsx:manifest:build # Auto-syncs indexes
```
**Production Mode**: Indexes sync during database normalization
```bash
php artisan migrate:normalize_schema
```
### Viewing Required Indexes
```bash
php artisan rsx:db:check_indexes
```
Shows all Query and Join attributes with their required indexes and whether
those indexes exist in the database.
For complete documentation of the Query/Join system, see:
```bash
php artisan rsx:man database_queries
```
BENEFITS OF THIS SYSTEM
------------------------
1. **No Foreign Key Mismatches**
- Types match from the start (both BIGINT)
- No need to drop/recreate foreign keys
- Migrations run faster in production
2. **Write Natural SQL**
- Developer writes INT, framework uses BIGINT
- Developer writes TEXT, framework uses LONGTEXT
- No need to remember framework conventions
3. **Consistent Schema**
- All environments have identical schema
- No drift between dev and production
- Database migrations are deterministic
4. **Fast Production Migrations**
- No column type modifications (no row rewrites)
- No foreign key drop/recreate (metadata only)
- Tables created correctly on first try
5. **Fail Fast on Bad Types**
- ENUM throws exception immediately
- Developer gets clear error message
- No silent failures or runtime issues
COMMANDS
--------
### php artisan migrate
Main migration command. Runs the full cycle:
pre-normalize → migrations → post-normalize → regenerate constants (dev)
### php artisan migrate:begin
Creates MySQL snapshot before migrations (dev mode only).
Required for safety in development.
### php artisan migrate:commit
Deletes snapshot and exits migration mode.
Runs schema quality checks before committing.
### php artisan migrate:rollback
Restores database to snapshot state.
Stays in migration mode for retry.
### php artisan migrate:normalize_schema
Manually run normalization (rarely needed).
Add --production flag to skip snapshot requirement.
### php artisan migrate:check
Check database schema for standards compliance.
Reports violations without making changes.
SNAPSHOT-BASED DEVELOPMENT
--------------------------
Development migrations require snapshots for safety:
1. `php artisan migrate:begin` - Create snapshot
2. `php artisan migrate` - Run migrations
3. `php artisan migrate:commit` - Keep changes, delete snapshot
OR
`php artisan migrate:rollback` - Discard changes, restore snapshot
Production migrations skip snapshots and run directly.
WHY NO FOREIGN KEY DROP/RECREATE?
---------------------------------
Previous system:
1. User creates table with INT columns
2. normalize_schema converts INT→BIGINT (slow, locks table)
3. Must drop ALL FKs first, recreate after (complex, error-prone)
New system:
1. Transformer rewrites INT→BIGINT before execution
2. Table created with BIGINT from the start
3. normalize_schema finds nothing to change (fast)
4. No FK drop/recreate needed
The only remaining use case for column modifications is converting pre-existing
tables (created before transformer) to utf8mb4 character set. This is rare and
only affects legacy tables.
TROUBLESHOOTING
---------------
### Migration fails with "ENUM column type is forbidden"
Solution: Replace ENUM with VARCHAR and add validation in your model.
### Migration fails with "SET column type is forbidden"
Solution: Use JSON column or create a separate table.
### Foreign key fails with type mismatch
Check: Are you running migrations outside the framework? Use `php artisan migrate`.
The transformer only runs during framework migration commands.
### Table has INT instead of BIGINT
This means the table was created before the transformer was implemented or
created outside the migration system. Run `php artisan migrate:normalize_schema`
to fix it, or drop and recreate the table.
### Character set mismatch on foreign keys
Run `php artisan migrate:normalize_schema` to convert tables to utf8mb4.
ADVANCED: HOW THE TRANSFORMER WORKS
------------------------------------
SqlQueryTransformer uses regex-based pattern matching on SQL statements:
1. Whitespace Normalization
- Collapses whitespace while preserving quoted strings
- Handles single quotes, double quotes, and backticks
2. DDL Detection
- Only transforms CREATE TABLE and ALTER TABLE
- Other queries (SELECT, INSERT, etc.) pass through unchanged
3. Type Validation
- Checks for forbidden types first (ENUM, SET, YEAR, TIME)
- Throws exception immediately if found
4. Type Transformation (in order)
- Integer types: INT/INTEGER/MEDIUMINT/SMALLINT → BIGINT
- Float types: FLOAT/REAL → DOUBLE
- Text types: TEXT/MEDIUMTEXT/CHAR → LONGTEXT/VARCHAR
- Charset enforcement: Add CHARACTER SET utf8mb4 to string columns
5. Query Execution
- Transformed query executes via DB::statement()
- Database never sees the original types
The transformer is enabled only during `php artisan migrate` and disabled after
completion. Normal application queries are not affected.
SEE ALSO
--------
- php artisan rsx:man database_queries - Query/Join attribute system
- php artisan rsx:man migrations - Migration guidelines
- php artisan rsx:man rsx_architecture - Framework architecture
- php artisan rsx:man coding_standards - Code standards
- /app/RSpade/Core/Database/CLAUDE.md - Database system docs
AUTHOR
------
RSpade Framework - October 2025