1d4738dfa2
Закрыт пункт «pg_partman replacement» из project_phase1_strategy.md
(расширение pg_partman недоступно на native Windows-стеке без сборки
из исходников).
Реализация:
- app/app/Console/Commands/PartitionsCreateMonths.php
- signature: partitions:create-months {--ahead=2}
- создаёт партиции для deals + supplier_lead_costs (обе по received_at)
- идемпотентна (проверка через pg_class WHERE relkind='r' перед CREATE)
- запускать ежесуточно через Windows Task Scheduler / cron
Smoke-test на dev: --ahead=8 создал 6 партиций (Nov 2026 - Jan 2027) +
12 skipped. После migrate:fresh партиции возвращаются к initial 6.
4 новых Pest-теста в PartitionsCreateMonthsTest:
- создание партиций на 8 месяцев вперёд для обеих таблиц
- идемпотентность (повторный --ahead=5 → 0 created, 12 skipped)
- --ahead=0 создаёт только текущий месяц
- INSERT в deals с received_at в новой партиции корректно роутится
Тесты используют beforeEach/afterEach для cleanup'а через
DROP TABLE ... CASCADE (FK webhook_dedup_keys на партицию propagates).
Pest 45/45 зелёные за 4.9 сек. Pint + Larastan чисто (phpstan-baseline
регенерирован для динамических свойств $this в Pest closure'ах).
CLAUDE.md v1.14 → v1.15. Реестр Открытые_вопросы v1.23 → v1.24.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
93 lines
3.4 KiB
PHP
93 lines
3.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Carbon;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
/**
|
|
* Создаёт ежемесячные партиции для `deals` и `supplier_lead_costs`
|
|
* на N месяцев вперёд от текущей даты.
|
|
*
|
|
* Замена `pg_partman` на native Windows-стеке (расширение недоступно
|
|
* без сборки из исходников). Запускается ежесуточно через Windows Task
|
|
* Scheduler / cron — идемпотентна (CREATE TABLE IF NOT EXISTS).
|
|
*
|
|
* По дефолту 2 месяца вперёд (паритет с инициализацией schema.sql:
|
|
* 6 партиций при `migrate:fresh`, последующие месяцы — этим cron'ом).
|
|
*
|
|
* Источник: db/schema.sql §5 (deals partition), §8.5 (supplier_lead_costs);
|
|
* project_phase1_strategy.md (pg_partman заменён ручным cron'ом).
|
|
*/
|
|
class PartitionsCreateMonths extends Command
|
|
{
|
|
/** @var string */
|
|
protected $signature = 'partitions:create-months {--ahead=2 : Сколько месяцев вперёд создать партиций}';
|
|
|
|
/** @var string */
|
|
protected $description = 'Создаёт ежемесячные партиции deals и supplier_lead_costs на N месяцев вперёд (idempotent)';
|
|
|
|
/**
|
|
* Список таблиц, которые партиционируются по received_at помесячно.
|
|
*
|
|
* @var array<int, string>
|
|
*/
|
|
private const PARTITIONED_TABLES = ['deals', 'supplier_lead_costs'];
|
|
|
|
public function handle(): int
|
|
{
|
|
$ahead = max(1, (int) $this->option('ahead'));
|
|
$now = Carbon::now()->startOfMonth();
|
|
|
|
$created = 0;
|
|
$skipped = 0;
|
|
|
|
for ($i = 0; $i <= $ahead; $i++) {
|
|
$monthStart = $now->copy()->addMonths($i);
|
|
$monthEnd = $monthStart->copy()->addMonth();
|
|
|
|
foreach (self::PARTITIONED_TABLES as $table) {
|
|
$partitionName = sprintf('%s_%s', $table, $monthStart->format('Y_m'));
|
|
|
|
if ($this->partitionExists($partitionName)) {
|
|
$skipped++;
|
|
$this->line(" skip <fg=gray>{$partitionName}</> (already exists)");
|
|
|
|
continue;
|
|
}
|
|
|
|
DB::statement(sprintf(
|
|
"CREATE TABLE %s PARTITION OF %s FOR VALUES FROM ('%s') TO ('%s')",
|
|
$partitionName,
|
|
$table,
|
|
$monthStart->format('Y-m-d'),
|
|
$monthEnd->format('Y-m-d'),
|
|
));
|
|
$created++;
|
|
$this->info(" create <fg=green>{$partitionName}</> [{$monthStart->format('Y-m-d')} → {$monthEnd->format('Y-m-d')})");
|
|
}
|
|
}
|
|
|
|
$this->newLine();
|
|
$this->info("Done: {$created} created, {$skipped} skipped (ahead={$ahead}).");
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
/**
|
|
* Проверка существования партиции через pg_class (быстрее information_schema).
|
|
*/
|
|
private function partitionExists(string $name): bool
|
|
{
|
|
$row = DB::selectOne(
|
|
"SELECT 1 AS exists FROM pg_class WHERE relname = ? AND relkind = 'r'",
|
|
[$name],
|
|
);
|
|
|
|
return $row !== null;
|
|
}
|
|
}
|