uzbek/laravel-partition-manager 问题修复 & 功能扩展

解决BUG、新增功能、兼容多环境部署,快速响应你的开发需求

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

uzbek/laravel-partition-manager

最新稳定版本:v0.3.4

Composer 安装命令:

composer require uzbek/laravel-partition-manager

包简介

A Laravel package for managing PostgreSQL partitioned tables

README 文档

README

A powerful Laravel package for managing PostgreSQL partitioned tables. Supports RANGE, LIST, and HASH partitioning strategies with multi-level sub-partitioning, automatic partition generation, and schema management.

Requirements

  • PHP 8.3 or higher
  • Laravel 12.0 or higher
  • PostgreSQL 14 or higher

Table of Contents

Installation

Install the package via Composer:

composer require uzbek/laravel-partition-manager

Optionally publish the configuration:

php artisan vendor:publish --tag=partition-manager-config

Quick Start

use Uzbek\LaravelPartitionManager\Partition;

// Create a partitioned table with 12 monthly partitions
Partition::create('logs', function($table) {
    $table->id();
    $table->string('level');
    $table->text('message');
    $table->timestamp('created_at');
})
->by('created_at')
->monthly();

// Add partitions to an existing table
Partition::for('events')
    ->by('created_at')
    ->daily(30);

Features

  • Three Partitioning Strategies: RANGE, LIST, and HASH partitioning
  • Automatic Partition Generation: Monthly, yearly, daily, weekly, quarterly partitions
  • Multi-Level Sub-Partitioning: Create hierarchical partition structures
  • Schema Management: Per-partition schema assignment with auto-creation
  • Transaction Safety: All operations wrapped in transactions with automatic rollback
  • Partition Lifecycle Management: Attach, detach, drop, analyze, vacuum operations
  • Advanced Options: Default partitions, check constraints, custom tablespaces, partition pruning
  • Multi-Column Partitioning: Partition by multiple columns or custom expressions
  • Artisan Commands: 8 CLI commands for partition management and maintenance
  • Scheduler Integration: Schedule partition rotation and creation via Laravel's scheduler
  • Partition Templates: Reusable partition configurations with placeholder support
  • Model Trait: Partition-aware Eloquent queries with scopes and helper methods
  • Laravel Octane Support: Proper cache management for long-running processes

Partition Types

Range Partitioning

Divide data based on ranges of values, ideal for date-based partitioning.

// Simple syntax with terminal method
Partition::create('orders', function($table) {
    $table->id();
    $table->decimal('amount');
    $table->date('order_date');
})
->by('order_date')
->monthly(24, '2024-01-01');  // 24 partitions from Jan 2024

// Manual partition definition
Partition::create('orders', function($table) {
    $table->id();
    $table->decimal('amount');
    $table->date('order_date');
})
->by('order_date')
->addRangePartition('orders_2024_q1', '2024-01-01', '2024-04-01')
->addRangePartition('orders_2024_q2', '2024-04-01', '2024-07-01')
->generate();  // Required when using addRangePartition()

List Partitioning

Partition data based on discrete values.

Partition::create('users', function($table) {
    $table->id();
    $table->string('country');
    $table->string('email');
})
->by('country')
->addListPartition('users_us', ['US', 'CA'])
->addListPartition('users_eu', ['DE', 'FR', 'IT', 'ES'])
->addListPartition('users_asia', ['JP', 'CN', 'KR'])
->generate();

// Or use addListPartitions for bulk creation
Partition::create('orders', function($table) {
    $table->id();
    $table->string('status');
})
->by('status')
->addListPartitions(['pending', 'processing', 'shipped', 'delivered'])
->generate();

Hash Partitioning

Distribute data evenly across partitions using a hash function.

Partition::create('events', function($table) {
    $table->id();
    $table->string('event_type');
    $table->jsonb('payload');
})
->by('id')
->addHashPartitions(4)
->generate();

Automatic Partition Generation

Terminal Methods

Terminal methods create the table and partitions immediately (no generate() call needed):

Partition::create('logs', function($table) { /* ... */ })
->by('created_at')
->monthly();           // 12 monthly partitions from current month
->monthly(24);         // 24 monthly partitions
->monthly(12, '2024-01-01');  // From specific date

->yearly();            // 5 yearly partitions from current year
->yearly(10, 2020);    // 10 partitions starting from 2020

->daily();             // 30 daily partitions from today
->daily(7, '2024-01-01');

->weekly();            // 12 weekly partitions
->quarterly();         // 8 quarterly partitions

Flexible Date Input

All date parameters accept multiple formats:

// Integer year
->monthly(12, 2024);           // Starts from 2024-01-01
->yearly(5, 2024);             // Starts from 2024-01-01

// Partial date strings
->monthly(12, '2024');         // Starts from 2024-01-01
->monthly(12, '2024-06');      // Starts from 2024-06-01

// Full date strings
->monthly(12, '2024-06-15');   // Starts from 2024-06-01 (aligned to month)

// DateTime/Carbon objects
->monthly(12, new DateTime('2024-06-15'));
->monthly(12, Carbon::now()->subMonth());

// Start from today explicitly
->monthly(12, fromToday: true);

Quick Generation for Existing Tables

// One-liner methods
Partition::monthly('logs', 'created_at', 12);
Partition::yearly('reports', 'year', 5);
Partition::daily('metrics', 'recorded_at', 30);
Partition::weekly('events', 'event_date', 12);
Partition::quarterly('sales', 'sale_date', 8);

// Using the builder for more control
Partition::for('logs')
    ->by('created_at')
    ->schema('log_partitions')
    ->monthly(24);

// List partitions
Partition::for('regions')
    ->byList('country', [
        'us' => ['US', 'CA'],
        'eu' => ['DE', 'FR', 'ES'],
        'asia' => ['JP', 'CN', 'KR']
    ]);

// Hash partitions
Partition::for('users')
    ->byHash('id', 8);

DateRangeBuilder

For advanced date range configurations:

use Uzbek\LaravelPartitionManager\Builders\DateRangeBuilder;

Partition::create('metrics', function($table) {
    $table->id();
    $table->float('value');
    $table->timestamp('recorded_at');
})
->by('recorded_at')
->dateRange(
    DateRangeBuilder::daily()
        ->from('2024-01-01')
        ->count(30)
        ->defaultSchema('daily_metrics')
)
->generate();

Schema Management

Per-Partition Schemas

// Default schema for all partitions
Partition::create('logs', function($table) { /* ... */ })
->by('logged_at')
->schema('log_partitions')
->monthly();

// Different schemas per partition
Partition::create('logs', function($table) { /* ... */ })
->by('logged_at')
->addRangePartition('logs_archive', '2023-01-01', '2024-01-01', 'archive_schema')
->addRangePartition('logs_current', '2024-01-01', '2025-01-01', 'current_schema')
->generate();

SchemaCreator Service

use Uzbek\LaravelPartitionManager\Services\SchemaCreator;

// Ensure schema exists
SchemaCreator::ensure('my_schema');
SchemaCreator::ensure('my_schema', $connection);

// Ensure schema and return prefixed table name
$fullName = SchemaCreator::ensureAndPrefix('my_table', 'my_schema');
// Returns: 'my_schema.my_table'

PartitionSchemaManager Service

use Uzbek\LaravelPartitionManager\Services\PartitionSchemaManager;

$schemaManager = new PartitionSchemaManager();

$schemaManager->setDefault('default_partitions');
$schemaManager->register('error', 'error_log_schema');
$schemaManager->registerMultiple([
    'active' => 'active_schema',
    'archived' => 'archive_schema'
]);

$schema = $schemaManager->getSchemaFor('error');
$hasSchema = $schemaManager->hasSchemaFor('info');

Sub-Partitioning

Create hierarchical partition structures with support for unlimited nesting levels.

Inline Sub-Partitions (Recommended)

The cleanest way to define sub-partitions - no need to repeat partition names:

use Uzbek\LaravelPartitionManager\Builders\SubPartitionBuilder;

Partition::create('events', function($table) {
    $table->id();
    $table->string('type');
    $table->integer('user_id');
    $table->timestamp('created_at');
})
->by('created_at')
->addRangePartition('events_2024_01', '2024-01-01', '2024-02-01',
    subPartitions: SubPartitionBuilder::list('type')
        ->addListPartition('events_2024_01_user', ['login', 'logout'])
        ->addListPartition('events_2024_01_system', ['error', 'warning'])
)
->generate();

Separate Declaration

You can also declare sub-partitions separately using withSubPartitions():

Partition::create('events', function($table) { /* ... */ })
->by('created_at')
->addRangePartition('events_2024_01', '2024-01-01', '2024-02-01')
->withSubPartitions('events_2024_01',
    SubPartitionBuilder::list('type')
        ->addListPartition('events_2024_01_user', ['login', 'logout'])
        ->addListPartition('events_2024_01_system', ['error', 'warning'])
)
->generate();

Apply Same Sub-Partition to All Partitions

Use addSubPartitionToAll() to apply the same sub-partition configuration to all existing partitions:

Partition::create('logs', function($table) {
    $table->id();
    $table->integer('user_id');
    $table->timestamp('created_at');
})
->by('created_at')
->addMonthlyPartitions(12, '2024-01-01')
->addSubPartitionToAll(
    SubPartitionBuilder::hash('user_id')
        ->addHashPartitions(4, '%_shard_')  // % = partition name
        // Creates: logs_m2024_01_shard_0, logs_m2024_01_shard_1, etc.
)
->generate();

The % placeholder in prefixes is replaced with the parent partition name, making it easy to create hierarchical naming.

Multi-Level Nesting

Sub-partitions can have their own sub-partitions, enabling complex hierarchical structures:

// Example: LIST → HASH → RANGE (3 levels deep)
Partition::create('orders', function($table) {
    $table->id();
    $table->string('status');
    $table->integer('user_id');
    $table->timestamp('created_at');
})
->list()
->by('status')
->addListPartition('orders_active', ['pending', 'processing'],
    subPartitions: SubPartitionBuilder::hash('user_id')
        ->addHashPartition('active_shard_0', 4, 0,
            subPartitions: SubPartitionBuilder::range('created_at')
                ->addMonthlyPartitions('m_', 12)
        )
        ->addHashPartition('active_shard_1', 4, 1,
            subPartitions: SubPartitionBuilder::range('created_at')
                ->addMonthlyPartitions('m_', 12)
        )
        ->addHashPartition('active_shard_2', 4, 2)
        ->addHashPartition('active_shard_3', 4, 3)
)
->addListPartition('orders_completed', ['shipped', 'delivered'])
->generate();

Using Value Objects

use Uzbek\LaravelPartitionManager\ValueObjects\RangePartition;
use Uzbek\LaravelPartitionManager\ValueObjects\ListSubPartition;

$partition = RangePartition::range('data_2024_01')
    ->withRange('2024-01-01', '2024-02-01')
    ->withSchema('monthly_data')
    ->withSubPartitions(
        SubPartitionBuilder::list('status')
            ->add(ListSubPartition::create('data_active')
                ->withValues(['active', 'pending'])
                ->withSchema('active_data'))
    );

$builder->addPartition($partition);

Add Sub-Partitions to Existing Partitions

Convert an existing non-partitioned partition into a partitioned one:

use Uzbek\LaravelPartitionManager\Services\PartitionMaintenance;

// Add sub-partitions to an existing partition (auto-detects partition expression)
PartitionMaintenance::addSubPartitions(
    'orders',                    // parent table
    'orders_active',             // existing partition to sub-partition
    SubPartitionBuilder::hash('user_id')->addHashPartitions('shard_', 8)
);

// Or manually specify the partition expression
PartitionMaintenance::addSubPartitions(
    'orders',
    'orders_active',
    SubPartitionBuilder::hash('user_id')->addHashPartitions('shard_', 8),
    "FOR VALUES IN ('active', 'pending')"  // optional, auto-detected if omitted
);

Partition Management

Runtime Operations

use Uzbek\LaravelPartitionManager\Services\PartitionManager;

$manager = app(PartitionManager::class);

// List all partitions
$partitions = $manager->getPartitions('logs');

// Get partition details
$info = $manager->getPartitionInfo('logs', 'logs_2024_01');

// Get partition strategy
$strategy = $manager->getPartitionStrategy('logs');
// Returns: PartitionType::RANGE, LIST, or HASH

// Check if table is partitioned
if ($manager->isPartitioned('logs')) { /* ... */ }

// Get statistics
$size = $manager->getTableSize('logs');
$count = $manager->getPartitionCount('logs');
$oldest = $manager->getOldestPartition('logs');
$newest = $manager->getNewestPartition('logs');

// Drop old partitions
$dropped = $manager->dropOldPartitions('logs', new DateTime('-6 months'));

// Maintenance
$manager->analyzePartition('logs_2024_01');
$manager->vacuumPartition('logs_2024_01', full: true);

Table Operations

use Uzbek\LaravelPartitionManager\Builders\PostgresPartitionBuilder;

$builder = new PostgresPartitionBuilder('orders');

// Attach existing table as partition
$builder->attachPartition('old_orders', 'orders_2023', '2023-01-01', '2024-01-01');

// Detach partition (optionally with CONCURRENTLY)
$builder->detachPartition('orders_2023', concurrently: true);

// Drop partition
$builder->dropPartition('orders_2023');

// Maintenance
$builder->analyze();
$builder->vacuum();
$builder->vacuum(full: true);

Using Facades

use Uzbek\LaravelPartitionManager\Facades\PartitionManager;

$partitions = PartitionManager::getPartitions('logs');
$isPartitioned = PartitionManager::isPartitioned('logs');
$strategy = PartitionManager::getPartitionStrategy('logs');

Artisan Commands

The package provides 8 Artisan commands for managing partitions from the command line:

partition:list

List all partitions for a table with size and row count:

php artisan partition:list orders

# Output:
# +------------------+------------+----------+
# | Partition        | Rows       | Size     |
# +------------------+------------+----------+
# | orders_m2024_01  | 15,234     | 2.1 MB   |
# | orders_m2024_02  | 18,456     | 2.5 MB   |
# | orders_m2024_03  | 12,890     | 1.8 MB   |
# +------------------+------------+----------+

partition:tree

Display partition hierarchy as a tree:

php artisan partition:tree orders --depth=2

# Output:
# orders (partitioned by RANGE)
# ├── orders_2024_01 [2024-01-01 → 2024-02-01]
# ├── orders_2024_02 [2024-02-01 → 2024-03-01]
# └── orders_2024_03 [2024-03-01 → 2024-04-01]

partition:health

Run a health check on partitions:

php artisan partition:health orders

# Checks for:
# - Gaps between partitions
# - Overlapping ranges
# - Missing indexes
# - Orphan data in default partition

partition:ensure-future

Create future partitions proactively:

# Ensure 3 monthly partitions exist
php artisan partition:ensure-future orders created_at --count=3 --interval=monthly

# With custom schema
php artisan partition:ensure-future orders created_at --count=6 --interval=monthly --schema=archive

partition:drop-old

Drop partitions older than a threshold:

# Drop partitions older than 6 months
php artisan partition:drop-old logs --keep=6

# Preview without dropping
php artisan partition:drop-old logs --keep=6 --dry-run

partition:vacuum

Run VACUUM on partitions:

# Vacuum specific partition
php artisan partition:vacuum orders_2024_01

# Vacuum all partitions of a table
php artisan partition:vacuum orders --all

# Full vacuum (reclaims more space, locks table)
php artisan partition:vacuum orders_2024_01 --full

# Vacuum with analyze
php artisan partition:vacuum orders_2024_01 --analyze

partition:reindex

Rebuild indexes on partitions:

# Reindex specific partition
php artisan partition:reindex orders_2024_01

# Reindex concurrently (non-blocking)
php artisan partition:reindex orders_2024_01 --concurrently

# Reindex all partitions
php artisan partition:reindex orders --all

partition:analyze

Update statistics for query planner:

# Analyze specific partition
php artisan partition:analyze orders_2024_01

# Analyze all partitions
php artisan partition:analyze orders --all

Scheduler Integration

Schedule partition maintenance tasks using Laravel's scheduler with a fluent API:

Basic Usage

In your app/Console/Kernel.php or routes/console.php:

use Illuminate\Console\Scheduling\Schedule;

$schedule->partition('orders', 'created_at')
    ->ensureFuture(3, 'monthly')
    ->daily();

$schedule->partition('logs', 'created_at')
    ->rotate(keep: 12)
    ->monthly();

Combined Operations

Run both creation and rotation in a single scheduled task:

$schedule->partition('events', 'created_at')
    ->ensureFuture(3, 'monthly')
    ->rotate(keep: 24)
    ->schema('event_partitions')
    ->dailyAt('02:00')
    ->withoutOverlapping()
    ->onOneServer();

Available Methods

$schedule->partition(string $table, string $column)
    // Partition operations
    ->ensureFuture(int $count, string $interval = 'monthly')  // Create future partitions
    ->rotate(int $keep, bool $dropSchemas = false)            // Drop old partitions
    ->schema(string $schema)                                   // Schema for new partitions

    // Scheduling options (all Laravel scheduler methods available)
    ->daily()
    ->dailyAt('02:00')
    ->weekly()
    ->weeklyOn(1, '03:00')  // Monday at 3 AM
    ->monthly()
    ->monthlyOn(1, '04:00')
    ->cron('0 2 * * *')
    ->timezone('America/New_York')

    // Constraints
    ->withoutOverlapping()
    ->onOneServer()
    ->evenInMaintenanceMode()
    ->runInBackground()

    // Callbacks
    ->onSuccess(fn($result) => Log::info($result->summary()))
    ->onFailure(fn($e) => Log::error($e->getMessage()))
    ->name('partition-maintenance:orders');

Result Object

The scheduled task returns a PartitionScheduleResult object:

$schedule->partition('orders', 'created_at')
    ->ensureFuture(3, 'monthly')
    ->rotate(keep: 12)
    ->daily()
    ->onSuccess(function (PartitionScheduleResult $result) {
        if ($result->hasChanges()) {
            Log::info($result->summary());
            // Output: "Partition maintenance: created 2 partition(s), dropped 1 partition(s)"
        }

        echo $result->partitionsCreated;      // Number created
        echo count($result->partitionsDropped); // Names of dropped partitions
    });

Partition Templates

Templates provide reusable partition configurations that can be applied to multiple tables. The % placeholder is replaced with the table name.

Configuration

Define templates in config/partition-manager.php:

'templates' => [
    'monthly_archive' => [
        'type' => 'range',
        'column' => 'created_at',
        'interval' => 'monthly',
        'count' => 12,
        'schema' => '%_archive',        // orders → orders_archive
        'default_partition' => true,
        'future_partitions' => 3,
    ],

    'tenant_hash' => [
        'type' => 'hash',
        'column' => 'tenant_id',
        'modulus' => 16,
        'schema' => 'tenants',
    ],

    'status_list' => [
        'type' => 'list',
        'column' => 'status',
        'values' => ['pending', 'active', 'completed', 'cancelled'],
        'default_partition' => true,
    ],
],

Using Templates in Migrations

use Uzbek\LaravelPartitionManager\Partition;

// Apply template from config
Partition::create('orders', function($table) {
    $table->id();
    $table->decimal('amount');
    $table->timestamp('created_at');
})
->fromTemplate('monthly_archive')
->generate();

// Override template settings
Partition::create('events', function($table) {
    $table->id();
    $table->timestamp('created_at');
})
->fromTemplate('monthly_archive', [
    'count' => 24,
    'schema' => 'event_archive',
])
->generate();

Programmatic Templates

Create templates programmatically:

use Uzbek\LaravelPartitionManager\Templates\PartitionTemplate;

$template = PartitionTemplate::define('custom')
    ->range('created_at')
    ->monthly(12)
    ->withSchema('%_partitions')
    ->withDefaultPartition()
    ->withFuturePartitions(3);

Partition::create('logs', fn($t) => $t->id()->timestamps())
    ->fromTemplate($template)
    ->generate();

Template Methods

PartitionTemplate::define(string $name)
    // Partition type
    ->range(string|array $columns)
    ->list(string $column)
    ->hash(string $column, int $modulus)

    // Intervals (for RANGE)
    ->daily(int $count = 30)
    ->weekly(int $count = 12)
    ->monthly(int $count = 12)
    ->yearly(int $count = 5)

    // LIST values
    ->withValues(array $values)

    // Options
    ->withSchema(string $schema)           // Supports % placeholder
    ->withTablespace(string $tablespace)
    ->withPrefix(string $prefix)           // Supports % placeholder
    ->withDefaultPartition(bool $enabled = true)
    ->withFuturePartitions(int $count);

Model Trait

The HasPartitions trait adds partition-aware methods to your Eloquent models:

Basic Setup

use Illuminate\Database\Eloquent\Model;
use Uzbek\LaravelPartitionManager\Traits\HasPartitions;

class Order extends Model
{
    use HasPartitions;

    // Optional: specify the partition column (default: 'created_at')
    protected static string $partitionColumn = 'created_at';
}

Query Scopes

Query specific partitions directly:

// Query a specific partition by name
Order::inPartition('orders_m2024_01')->where('status', 'pending')->get();

// Query by date range (enables partition pruning)
Order::inPartitionRange('2024-01-01', '2024-03-31')->get();

// Query by value (for LIST partitions)
Order::inPartitionValue('active')->get();
Order::inPartitionValues(['pending', 'processing'])->get();

Partition Information

// Check if table is partitioned
if (Order::isPartitioned()) {
    // Get all partitions
    $partitions = Order::getPartitions();

    // Get partition strategy (RANGE, LIST, HASH)
    $strategy = Order::getPartitionStrategy();

    // Get statistics
    $stats = Order::getPartitionStats();

    // Get boundaries
    $boundaries = Order::getPartitionBoundaries();
}

Finding Partitions

// Find partition for a specific date (RANGE)
$partitionName = Order::getPartitionForDate('2024-06-15');
// Returns: 'orders_m2024_06' or null

// Check if partition exists for date
if (Order::hasPartitionForDate('2024-12-01')) {
    // Safe to insert
}

// Find partition for a value (LIST)
$partitionName = Order::getPartitionForValue('active');

Health and Statistics

// Run health check
$health = Order::partitionHealthCheck();
// Returns: ['gaps' => [...], 'overlaps' => [...], 'missing_indexes' => [...], 'orphan_data' => bool]

// Estimate total rows
$totalRows = Order::estimateTotalRows();

// Count rows in specific partition
$count = Order::countInPartition('orders_m2024_01');

// Get oldest/newest partition
$oldest = Order::getOldestPartition();
$newest = Order::getNewestPartition();

Query Analysis

// Explain partition pruning for a query
$query = Order::where('created_at', '>=', '2024-01-01')
    ->where('created_at', '<', '2024-04-01');

$analysis = Order::explainPartitionPruning($query);
// Returns:
// [
//     'partitions_scanned' => ['orders_m2024_01', 'orders_m2024_02', 'orders_m2024_03'],
//     'total_partitions' => 12,
//     'pruning_effective' => true,
//     'plan' => '...'
// ]

// Print partition tree
echo Order::printPartitionTree();

All Trait Methods

// Static methods
Order::getPartitionColumn(): string
Order::isPartitioned(): bool
Order::getPartitions(): array
Order::getPartitionStrategy(): ?string
Order::getPartitionStats(): array
Order::getPartitionBoundaries(): array
Order::partitionHealthCheck(): array
Order::estimateTotalRows(): int
Order::getPartitionForDate(string $date): ?string
Order::getPartitionForValue(mixed $value): ?string
Order::hasPartitionForDate(string $date): bool
Order::explainPartitionPruning(?Builder $query = null): array
Order::printPartitionTree(): string
Order::countInPartition(string $partitionName): int
Order::getOldestPartition(): ?object
Order::getNewestPartition(): ?object

// Query scopes
Order::inPartition(string $partitionName)
Order::inPartitionRange(string $from, string $to)
Order::inPartitionValue(mixed $value)
Order::inPartitionValues(array $values)

Advanced Features

PostgreSQL Enum Types

The package provides a pgEnum Blueprint macro for creating PostgreSQL native enum types:

use Uzbek\LaravelPartitionManager\Partition;

Partition::create('orders', function($table) {
    $table->id();
    $table->pgEnum('status', ['pending', 'processing', 'shipped', 'delivered']);
    $table->pgEnum('priority', ['low', 'medium', 'high'], 'order_priority'); // custom type name
    $table->timestamp('created_at');
})
->list()
->by('status')
->addListPartition('orders_pending', ['pending', 'processing'])
->addListPartition('orders_shipped', ['shipped', 'delivered'])
->generate();

The macro:

  • Creates a PostgreSQL ENUM type with the specified values
  • Registers the type with Laravel's grammar for proper SQL compilation
  • Auto-generates type name as {singular_table}_{column}_enum if not specified
  • Handles duplicate type creation gracefully (won't error if type already exists)

Conditional (Partial) Indexes

The conditionalIndex macro creates PostgreSQL partial indexes with WHERE clauses and optional column ordering. Follows Laravel's convention with columns first and optional auto-generated name:

Schema::table('promo_codes', function (Blueprint $table) {
    // Simple partial index with raw WHERE string (auto-generated name)
    $table->conditionalIndex(['code'], "status = 'void'");
    // Creates: promo_codes_code_idx

    // With custom name
    $table->conditionalIndex(['code'], "status = 'void'", 'idx_pc_void');

    // Laravel-style closure for WHERE clause
    $table->conditionalIndex(['source_type', 'source_id'], function ($query) {
        $query->whereIn('status', ['new', 'dispensed'])
              ->whereNotNull('source_type');
    }, 'idx_pc_source');
});

Schema::table('user_daily_points', function (Blueprint $table) {
    // Index with column ordering (no WHERE clause)
    $table->conditionalIndex(['date', 'points' => 'DESC']);

    // Alternative syntax: ordering in column string
    $table->conditionalIndex(['date ASC', 'points DESC'], name: 'idx_udp_sorted');

    // With closure and ordering
    $table->conditionalIndex(
        ['created_at' => 'DESC', 'score' => 'DESC'],
        fn($q) => $q->where('status', 'active'),
        'idx_active_users'
    );

    // Custom index method (e.g., GIN for JSONB columns)
    $table->conditionalIndex(['metadata'], method: 'gin');
});

Parameters:

  • $columns: Array of columns with optional ordering
    • String value: column name (e.g., 'code' or 'points DESC')
    • Key-value: 'column' => 'ASC' or 'column' => 'DESC'
  • $where: Optional WHERE clause - raw string or Laravel-style closure
  • $name: Optional index name (auto-generated as {table}_{columns}_idx if null)
  • $method: Index method - btree (default), hash, gist, gin, brin

Default Partitions

Catch rows that don't match any defined partition:

->withDefaultPartition('others')

Check Constraints

->check('positive_amount', 'amount > 0')
->check('valid_status', "status IN ('pending', 'completed')")

Partition Pruning

->enablePartitionPruning()      // Enable query optimization
->detachConcurrently()          // Non-blocking detach operations

Custom Tablespaces

->tablespace('fast_ssd')

Custom Partition Expressions

->partitionByExpression("DATE_TRUNC('week', event_time AT TIME ZONE timezone)")
->partitionByYear('order_date')
->partitionByMonth('created_at')
->partitionByDay('logged_at')

Multi-Column Partitioning

Partition::create('sales', function($table) {
    $table->id();
    $table->string('region');
    $table->integer('year');
    $table->decimal('amount');
})
->by(['region', 'year'])
->addRangePartition('sales_us_2024', ['US', 2024], ['US', 2025])
->generate();

Configuration

// config/partition-manager.php
return [
    'default_connection' => env('DB_CONNECTION', 'pgsql'),

    'defaults' => [
        'enable_partition_pruning' => true,
        'detach_concurrently' => true,
        'analyze_after_create' => true,
        'vacuum_after_drop' => true,
    ],

    'naming' => [
        'prefix' => '',
        'suffix' => '',
        'separator' => '_',
        'date_format' => 'Y_m',
        'day_format' => 'Y_m_d',
    ],

    'schemas' => [
        'auto_create' => [],
        'default' => null,
        'mappings' => [],
    ],

    'logging' => [
        'enabled' => env('PARTITION_LOGGING', true),
        'channel' => env('PARTITION_LOG_CHANNEL', 'daily'),
    ],
];

Service Classes

The package provides standalone service classes for specific partition operations:

PartitionStats

Get statistics and health information about partitions:

use Uzbek\LaravelPartitionManager\Services\PartitionStats;

// Get partition statistics (row count, size)
$stats = PartitionStats::get('orders');
// Returns: partition_name, row_count, size_bytes, size_pretty

// Get partition boundaries
$boundaries = PartitionStats::boundaries('orders');
// Returns: partition_name, partition_type, from_value, to_value

// Estimate row counts
$rowCount = PartitionStats::estimateRowCount('orders');
$totalRows = PartitionStats::estimateTotalRowCount('orders');

// Health check
$health = PartitionStats::healthCheck('orders');
// Returns: gaps, overlaps, missing_indexes, orphan_data

// Find specific issues
$gaps = PartitionStats::findGaps('orders');
$overlaps = PartitionStats::findOverlaps('orders');
$missingIndexes = PartitionStats::findMissingIndexes('orders');

// Analyze query pruning
$pruning = PartitionStats::explainPruning(
    "SELECT * FROM orders WHERE created_at >= '2024-01-01'"
);
// Returns: partitions_scanned, total_partitions, pruning_effective, plan

// Tree visualization
$tree = PartitionStats::getTree('orders');
echo PartitionStats::printTree('orders');
// Output:
// orders
// ├── orders_2024_01 [2024-01-01 → 2024-02-01]
// ├── orders_2024_02 [2024-02-01 → 2024-03-01]
// └── orders_2024_03 [2024-03-01 → 2024-04-01]

PartitionMaintenance

Perform maintenance operations on partitions:

use Uzbek\LaravelPartitionManager\Services\PartitionMaintenance;
use Uzbek\LaravelPartitionManager\Builders\SubPartitionBuilder;

// Vacuum partition (reclaim storage)
PartitionMaintenance::vacuum('orders_2024_01');
PartitionMaintenance::vacuum('orders_2024_01', full: true);
PartitionMaintenance::vacuum('orders_2024_01', analyze: true);

// Vacuum all partitions of a table
PartitionMaintenance::vacuumAll('orders');
PartitionMaintenance::vacuumAll('orders', full: true);

// Analyze partition (update statistics)
PartitionMaintenance::analyze('orders_2024_01');
PartitionMaintenance::analyzeAll('orders');

// Reindex partition
PartitionMaintenance::reindex('orders_2024_01');
PartitionMaintenance::reindex('orders_2024_01', concurrently: true);
PartitionMaintenance::reindexAll('orders');

// Rebalance hash partitions (change modulus)
PartitionMaintenance::rebalanceHash('events', newModulus: 16);
PartitionMaintenance::rebalanceHash('events', newModulus: 16, schema: 'partitions');

// Add sub-partitions to an existing partition
PartitionMaintenance::addSubPartitions(
    'orders',
    'orders_active',
    SubPartitionBuilder::hash('user_id')->addHashPartitions('shard_', 8)
);

// Get partition expression for an existing partition
$expr = PartitionMaintenance::getPartitionExpression('orders', 'orders_active');
// Returns: "FOR VALUES IN ('active', 'pending')"

// Get commands for parallel execution via jobs/CLI
$commands = PartitionMaintenance::getParallelVacuumCommands('orders');
$commands = PartitionMaintenance::getParallelReindexCommands('orders', concurrently: true);
$commands = PartitionMaintenance::getParallelAnalyzeCommands('orders');

// Dry run mode - returns SQL without executing
$queries = PartitionMaintenance::dryRun(function() {
    PartitionMaintenance::vacuum('orders_2024_01');
    PartitionMaintenance::analyze('orders_2024_01');
});

PartitionConsolidator

Merge multiple partitions into larger ones:

use Uzbek\LaravelPartitionManager\Services\PartitionConsolidator;

// Merge specific partitions
PartitionConsolidator::merge(
    'orders',
    ['orders_2024_01', 'orders_2024_02', 'orders_2024_03'],
    'orders_2024_q1',
    '2024-01-01',
    '2024-04-01'
);

// Consolidate monthly partitions into yearly
PartitionConsolidator::monthlyToYearly('orders', 2024, 'orders_m');

// Consolidate daily partitions into weekly
PartitionConsolidator::dailyToWeekly('logs', '2024-01-01', 'logs_d');

// Consolidate daily partitions into monthly
PartitionConsolidator::dailyToMonthly('logs', 2024, 1, 'logs_d');

// Consolidate weekly partitions into monthly
PartitionConsolidator::weeklyToMonthly('events', 2024, 1, 'events_w');

// Merge all partitions within a date range
PartitionConsolidator::range(
    'orders',
    '2024-01-01',
    '2024-07-01',
    'orders_2024_h1'
);

PartitionSplitter

Split partitions into smaller granularity:

use Uzbek\LaravelPartitionManager\Services\PartitionSplitter;

// Split yearly partition into monthly
PartitionSplitter::yearlyToMonthly('orders', 'orders_y2024', 2024);

// Split yearly partition into weekly
PartitionSplitter::yearlyToWeekly('events', 'events_y2024', 2024);

// Split monthly partition into daily
PartitionSplitter::monthlyToDaily('logs', 'logs_m2024_01', 2024, 1);

// Split monthly partition into weekly
PartitionSplitter::monthlyToWeekly('events', 'events_m2024_01', 2024, 1);

// Custom split with specific ranges
PartitionSplitter::custom('orders', 'orders_q1', [
    'orders_jan' => ['from' => '2024-01-01', 'to' => '2024-02-01'],
    'orders_feb' => ['from' => '2024-02-01', 'to' => '2024-03-01'],
    'orders_mar' => ['from' => '2024-03-01', 'to' => '2024-04-01'],
]);

PartitionRotation

Manage partition lifecycle with rotation policies:

use Uzbek\LaravelPartitionManager\Services\PartitionRotation;

// Ensure future partitions exist (column and interval are auto-detected if omitted)
$created = PartitionRotation::ensureFuture('orders', count: 3);
$created = PartitionRotation::ensureFuture('orders', 3, 'created_at', 'monthly');

// Rotate old partitions (keep only the most recent N partitions)
$dropped = PartitionRotation::rotate('logs', keep: 12);

// Add monthly partitions for an entire year
// Year accepts: int, string ("2025"), or DateTimeInterface
PartitionRotation::addMonthlyForYear('orders', 'created_at', 2025);
PartitionRotation::addMonthlyForYear('orders', 'created_at', '2025');

PartitionIndex

Manage indexes on partitions:

use Uzbek\LaravelPartitionManager\Services\PartitionIndex;

// Create index on partition
PartitionIndex::create('orders_2024_01', 'idx_orders_customer', ['customer_id']);

// Create unique index
PartitionIndex::create('orders_2024_01', 'idx_orders_ref', ['reference'], unique: true);

// Create index with specific method (btree, hash, gist, gin, brin)
PartitionIndex::create('logs_2024_01', 'idx_logs_message', ['message'], method: 'gin');

// Create index concurrently (non-blocking)
PartitionIndex::createConcurrently('orders_2024_01', 'idx_customer', ['customer_id']);

// Drop index
PartitionIndex::drop('idx_orders_customer');
PartitionIndex::drop('idx_orders_customer', cascade: true);
PartitionIndex::dropConcurrently('idx_orders_customer');

// List indexes on partition
$indexes = PartitionIndex::list('orders_2024_01');

PartitionExport

Export partition data to files:

use Uzbek\LaravelPartitionManager\Services\PartitionExport;

// Export to SQL file
PartitionExport::toSql('orders_2024_01', '/backup/orders_2024_01.sql');

// Export to compressed SQL
PartitionExport::toCompressedSql('orders_2024_01', '/backup/orders_2024_01.sql.gz');

// Export to CSV
PartitionExport::toCsv('orders_2024_01', '/backup/orders_2024_01.csv');

// Get export command (without executing)
$command = PartitionExport::getExportCommand('orders_2024_01', '/backup/orders.sql');

API Reference

Partition (Static Helper)

Partition::create(string $table, Closure $callback): PostgresPartitionBuilder
Partition::for(string $table): QuickPartitionBuilder

// Quick generation
Partition::monthly(string $table, string $column, int $count = 12): void
Partition::yearly(string $table, string $column, int $count = 5): void
Partition::daily(string $table, string $column, int $count = 30): void
Partition::weekly(string $table, string $column, int $count = 12): void
Partition::quarterly(string $table, string $column, int $count = 8): void

// Queries
Partition::isPartitioned(string $table): bool
Partition::getPartitions(string $table): array
Partition::partitionExists(string $table, string $partitionName): bool
Partition::dropIfExists(string $table, bool $withSchema = false): void
Partition::dropSchemaIfEmpty(string $schema): void
Partition::dropSchemaIfExists(string $schema): void

// Attach partitions (for all partition types)
Partition::attachPartition(string $table, string $partitionName, mixed $from, mixed $to): void  // RANGE
Partition::attachListPartition(string $table, string $partitionName, array $values): void       // LIST
Partition::attachHashPartition(string $table, string $partitionName, int $modulus, int $remainder): void  // HASH

// Detach partition
Partition::detachPartition(string $table, string $partitionName, bool $concurrently = false): void

PartitionType Enum

use Uzbek\LaravelPartitionManager\Enums\PartitionType;

PartitionType::RANGE    // 'RANGE'
PartitionType::LIST     // 'LIST'
PartitionType::HASH     // 'HASH'

// Helper methods
$type->isRange(): bool
$type->isList(): bool
$type->isHash(): bool

// PostgreSQL strategy codes
PartitionType::PG_STRATEGY_RANGE  // 'r'
PartitionType::PG_STRATEGY_LIST   // 'l'
PartitionType::PG_STRATEGY_HASH   // 'h'

// Create from PostgreSQL strategy code
PartitionType::fromPgStrategy('r')  // Returns PartitionType::RANGE

PostgresPartitionBuilder

// Configuration
->setBlueprint(Blueprint $blueprint): self
->connection(string $connection): self
->partition(PartitionType|string $type): self  // Optional: auto-detected from add*Partition calls
->range(): self   // Optional: auto-detected
->list(): self    // Optional: auto-detected
->hash(): self    // Optional: auto-detected
->by(string|array $columns): self
->partitionByExpression(string $expression): self

// Adding single partitions (all support optional subPartitions parameter)
->addPartition(PartitionDefinition $partition): self
->addRangePartition(string $name, mixed $from, mixed $to, ?string $schema = null, ?AbstractSubPartitionBuilder $subPartitions = null): self
->addListPartition(string $name, array $values, ?string $schema = null, ?AbstractSubPartitionBuilder $subPartitions = null): self
->addHashPartition(string $name, int $modulus, int $remainder, ?string $schema = null, ?AbstractSubPartitionBuilder $subPartitions = null): self

// Adding multiple partitions at once (non-terminal)
// All prefix parameters support % as placeholder for table name
// All date parameters accept: int (year), string ("2024", "2024-01", "2024-01-15"), or DateTimeInterface
->addListPartitions(array $values, ?string $schema = null): self
// Examples: ['new', 'void', 'used'] or [true => '%_active', false => '%_inactive']
->addHashPartitions(int $modulus, ?string $prefix = null, ?string $schema = null): self
// Example: addHashPartitions(4, '%_shard_') creates: orders_shard_0, orders_shard_1, etc.
->addMonthlyPartitions(int $count, int|string|DateTimeInterface|null $start = null, ?string $prefix = null, ?string $schema = null, bool $fromToday = false): self
->addYearlyPartitions(int $count, int|string|DateTimeInterface|null $start = null, ?string $prefix = null, ?string $schema = null, bool $fromToday = false): self
->addWeeklyPartitions(int $count, int|string|DateTimeInterface|null $start = null, ?string $prefix = null, ?string $schema = null, bool $fromToday = false): self
->addDailyPartitions(int $count, int|string|DateTimeInterface|null $start = null, ?string $prefix = null, ?string $schema = null, bool $fromToday = false): self
->dateRange(DateRangeBuilder $builder): self

// Terminal methods (execute immediately, includes generate())
// All accept: int (year), string ("2024", "2024-01", "2024-01-15"), or DateTimeInterface
->monthly(int $count = 12, int|string|DateTimeInterface|null $start = null, bool $fromToday = false): void
->yearly(int $count = 5, int|string|DateTimeInterface|null $start = null, bool $fromToday = false): void
->daily(int $count = 30, int|string|DateTimeInterface|null $start = null, bool $fromToday = false): void
->weekly(int $count = 12, int|string|DateTimeInterface|null $start = null, bool $fromToday = false): void
->quarterly(int $count = 8, int|string|DateTimeInterface|null $start = null, bool $fromToday = false): void

// Schema management
->schema(string $schema): self
->schemaFor(string $partitionType, string $schema): self
->schemasFor(array $schemas): self

// Sub-partitions
->withSubPartitions(string $partitionName, SubPartitionBuilder $builder): self
->addSubPartitionToAll(AbstractSubPartitionBuilder $builder): self  // Apply same sub-partition to all partitions

// Advanced options
->withDefaultPartition(string $name = 'default'): self
->tablespace(string $tablespace): self
->check(string $name, string $expression): self
->enablePartitionPruning(bool $enable = true): self
->detachConcurrently(bool $enable = true): self

// Execution
->generate(): void

// Runtime operations
->attachPartition(string $tableName, string $partitionName, mixed $from, mixed $to): self
->detachPartition(string $partitionName, ?bool $concurrently = null): self
->dropPartition(string $partitionName): self
->analyze(): self
->vacuum(bool $full = false): self

QuickPartitionBuilder

QuickPartitionBuilder::table(string $table): self

->by(string $column): self
->schema(string $schema): self
->connection(string $connection): self

// Range partitions
// All accept: int (year), string ("2024", "2024-01", "2024-01-15"), or DateTimeInterface
->monthly(int $count = 12, int|string|DateTimeInterface|null $start = null, bool $fromToday = false): void
->yearly(int $count = 5, int|string|DateTimeInterface|null $start = null, bool $fromToday = false): void
->daily(int $count = 30, int|string|DateTimeInterface|null $start = null, bool $fromToday = false): void
->weekly(int $count = 12, int|string|DateTimeInterface|null $start = null, bool $fromToday = false): void
->quarterly(int $count = 8, int|string|DateTimeInterface|null $start = null, bool $fromToday = false): void

// List and hash
->byList(string $column, array $partitions): void
->byHash(string $column, int $count = 4): void

DateRangeBuilder

DateRangeBuilder::monthly(): self
DateRangeBuilder::yearly(): self
DateRangeBuilder::daily(): self
DateRangeBuilder::weekly(): self
DateRangeBuilder::quarterly(): self

// Date parameters accept: int (year), string ("2024", "2024-01", "2024-01-15"), or DateTimeInterface
->from(int|string|DateTimeInterface $date): self
->to(int|string|DateTimeInterface $date): self
->count(int $count): self
->interval(string $interval): self
->nameFormat(string $format): self
->schema(string $schema): self
->build(string $prefix = ''): array

SubPartitionBuilder

SubPartitionBuilder::list(string $column): ListSubPartitionBuilder
SubPartitionBuilder::range(string $column): RangeSubPartitionBuilder
SubPartitionBuilder::hash(string $column): HashSubPartitionBuilder

// Common methods
->for(string $baseName): self  // Set base name for auto-generating partition names
->table(string $tableName): self  // Set table name for column type lookups (auto-set when used via PostgresPartitionBuilder)
->schema(string $schema): self
->add(SubPartition $partition): self
->getBaseName(): ?string
->getTableName(): ?string
->getPartitionType(): PartitionType
->getPartitionColumn(): string
->toArray(?string $defaultSchema = null): array

// ListSubPartitionBuilder
->addListPartition(string $name, array $values, ?string $schema = null, ?AbstractSubPartitionBuilder $subPartitions = null): self
->addListPartitions(array $values, ?string $schema = null): self
// Examples:
//   ->addListPartitions(['new', 'void', 'used'])  // auto-generated names using baseName
//   ->addListPartitions(['new' => '%_new', 'void' => '%_void'])  // % = baseName placeholder
//   ->addListPartitions(['false' => 'inactive', 'true' => 'active'])  // casts based on column type (auto-detected)

// RangeSubPartitionBuilder
// All date parameters accept: int (year), string ("2024", "2024-01", "2024-01-15"), or DateTimeInterface
->addRangePartition(string $name, mixed $from, mixed $to, ?string $schema = null, ?AbstractSubPartitionBuilder $subPartitions = null): self
->addYearlyPartitions(int $count, int|string|DateTimeInterface|null $start = null, ?string $prefix = null, ?string $schema = null, bool $fromToday = false): self
->addMonthlyPartitions(int $count, int|string|DateTimeInterface|null $start = null, ?string $prefix = null, ?string $schema = null, bool $fromToday = false): self
->addWeeklyPartitions(int $count, int|string|DateTimeInterface|null $start = null, ?string $prefix = null, ?string $schema = null, bool $fromToday = false): self
->addDailyPartitions(int $count, int|string|DateTimeInterface|null $start = null, ?string $prefix = null, ?string $schema = null, bool $fromToday = false): self
// If prefix is null, auto-generates using baseName: {baseName}_y, {baseName}_m, {baseName}_w, {baseName}_d

// HashSubPartitionBuilder
->addHashPartition(string $name, int $modulus, int $remainder, ?string $schema = null, ?AbstractSubPartitionBuilder $subPartitions = null): self
->addHashPartitions(int $modulus, ?string $prefix = null, ?string $schema = null): self
// If prefix is null, auto-generates using baseName: {baseName}_p

Value Objects

// RangePartition
RangePartition::range(string $name)
    ->withRange(mixed $from, mixed $to): self
    ->withSchema(string $schema): self
    ->withSubPartitions(SubPartitionBuilder $builder): self

// ListPartition
ListPartition::list(string $name)
    ->withValues(array $values): self
    ->withValue(mixed $value): self
    ->withSchema(string $schema): self

// HashPartition
HashPartition::hash(string $name)
    ->withHash(int $modulus, int $remainder): self
    ->withSchema(string $schema): self

// Sub-partition value objects (all support nested sub-partitions)
RangeSubPartition::create(string $name)
    ->withRange(mixed $from, mixed $to): self
    ->withSchema(string $schema): self
    ->withTablespace(string $tablespace): self
    ->withSubPartitions(AbstractSubPartitionBuilder $builder): self

ListSubPartition::create(string $name)
    ->withValues(array $values): self
    ->withSchema(string $schema): self
    ->withTablespace(string $tablespace): self
    ->withSubPartitions(AbstractSubPartitionBuilder $builder): self

HashSubPartition::create(string $name)
    ->withHash(int $modulus, int $remainder): self
    ->withSchema(string $schema): self
    ->withTablespace(string $tablespace): self
    ->withSubPartitions(AbstractSubPartitionBuilder $builder): self

// Common SubPartition methods
->getName(): string
->getSchema(): ?string
->getTablespace(): ?string
->hasSubPartitions(): bool
->getSubPartitions(): ?AbstractSubPartitionBuilder
->toArray(): array

PartitionManager Service

// Query
->getPartitions(string $table, ?string $connection = null): array
->getPartitionInfo(string $table, string $partitionName, ?string $connection = null): ?object
->isPartitioned(string $table, ?string $connection = null): bool
->getPartitionStrategy(string $table, ?string $connection = null): ?PartitionType
->getPartitionColumns(string $table, ?string $connection = null): array
->getTableSize(string $table, ?string $connection = null): string
->getPartitionCount(string $table, ?string $connection = null): int
->getOldestPartition(string $table, ?string $connection = null): ?object
->getNewestPartition(string $table, ?string $connection = null): ?object

// Maintenance
->analyzePartition(string $partitionName, ?string $connection = null): void
->vacuumPartition(string $partitionName, bool $full = false, ?string $connection = null): void
->dropOldPartitions(string $table, DateTime $before, ?string $connection = null): array

License

MIT

统计信息

  • 总下载量: 26
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 0
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2025-12-03