Add master seed data export and auto-apply during setup install - idempotent REPLACE mode prevents duplicates
This commit is contained in:
parent
67fbc68c1c
commit
ee3193ce43
|
|
@ -313,6 +313,7 @@ $routes->get('Ipd_discharge/show_file3/(:num)', 'Ipd_discharge::show_file3/$1');
|
||||||
$routes->get('setup/db-tools', 'Setup::dbTools');
|
$routes->get('setup/db-tools', 'Setup::dbTools');
|
||||||
$routes->post('setup/db-tools/generate', 'Setup::generateMigrations');
|
$routes->post('setup/db-tools/generate', 'Setup::generateMigrations');
|
||||||
$routes->post('setup/db-tools/export-master-schema', 'Setup::exportMasterSchema');
|
$routes->post('setup/db-tools/export-master-schema', 'Setup::exportMasterSchema');
|
||||||
|
$routes->post('setup/db-tools/export-master-seed', 'Setup::exportMasterSeedData');
|
||||||
$routes->post('setup/db-tools/schema-sync', 'Setup::schemaSync');
|
$routes->post('setup/db-tools/schema-sync', 'Setup::schemaSync');
|
||||||
$routes->post('setup/db-tools/schema-sync-step', 'Setup::schemaSyncStep');
|
$routes->post('setup/db-tools/schema-sync-step', 'Setup::schemaSyncStep');
|
||||||
$routes->post('setup/db-tools/repair-auth-schema', 'Setup::repairAuthSchema');
|
$routes->post('setup/db-tools/repair-auth-schema', 'Setup::repairAuthSchema');
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,20 @@ class Setup extends BaseController
|
||||||
return APPPATH . 'Database/master_schema.json';
|
return APPPATH . 'Database/master_schema.json';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function masterSeedFilePath(): string
|
||||||
|
{
|
||||||
|
$fromEnv = trim((string) env('setup.seed.file', ''));
|
||||||
|
if ($fromEnv !== '') {
|
||||||
|
if (str_starts_with($fromEnv, '/') || preg_match('/^[A-Za-z]:[\\\/]/', $fromEnv) === 1) {
|
||||||
|
return $fromEnv;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ROOTPATH . ltrim(str_replace(['\\'], '/', $fromEnv), '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
return APPPATH . 'Database/master_seed_data.json';
|
||||||
|
}
|
||||||
|
|
||||||
private function isSetupLocked(): bool
|
private function isSetupLocked(): bool
|
||||||
{
|
{
|
||||||
return is_file($this->lockFilePath());
|
return is_file($this->lockFilePath());
|
||||||
|
|
@ -181,6 +195,16 @@ class Setup extends BaseController
|
||||||
$syncResult['apply_truncated'] = $applyResult['truncated'];
|
$syncResult['apply_truncated'] = $applyResult['truncated'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($action === 'apply') {
|
||||||
|
$canRunSeed = empty($syncResult['errors'])
|
||||||
|
&& empty($syncResult['apply_errors'] ?? [])
|
||||||
|
&& empty($syncResult['apply_truncated'] ?? false);
|
||||||
|
|
||||||
|
if ($canRunSeed) {
|
||||||
|
$syncResult['seed'] = $this->applySeedDataToClientDb();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return $syncResult;
|
return $syncResult;
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
log_message('error', 'Setup schemaSync failed: {message}', ['message' => $e->getMessage()]);
|
log_message('error', 'Setup schemaSync failed: {message}', ['message' => $e->getMessage()]);
|
||||||
|
|
@ -223,6 +247,56 @@ class Setup extends BaseController
|
||||||
return redirect()->to($this->dbToolsUrlWithKey());
|
return redirect()->to($this->dbToolsUrlWithKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function exportMasterSeedData()
|
||||||
|
{
|
||||||
|
if ($deny = $this->ensureSetupAccess()) {
|
||||||
|
return $deny;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tables = $this->parseSeedTableList();
|
||||||
|
if (empty($tables)) {
|
||||||
|
session()->setFlashdata('setup_msg', 'No seed tables configured. Set setup.seed.export_tables in .env (comma-separated table names).');
|
||||||
|
return redirect()->to($this->dbToolsUrlWithKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
$seedPayload = [
|
||||||
|
'generated_at' => date('Y-m-d H:i:s'),
|
||||||
|
'source' => 'local-current-db',
|
||||||
|
'tables' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($tables as $tableName) {
|
||||||
|
if (!$this->tableExists($tableName)) {
|
||||||
|
$seedPayload['tables'][$tableName] = [];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$rows = $this->db->table($tableName)->get()->getResultArray();
|
||||||
|
$seedPayload['tables'][$tableName] = is_array($rows) ? $rows : [];
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
log_message('error', 'Seed export table read failed for {table}: {message}', ['table' => $tableName, 'message' => $e->getMessage()]);
|
||||||
|
$seedPayload['tables'][$tableName] = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$file = $this->masterSeedFilePath();
|
||||||
|
$dir = dirname($file);
|
||||||
|
if (!is_dir($dir) && !mkdir($dir, 0777, true) && !is_dir($dir)) {
|
||||||
|
session()->setFlashdata('setup_msg', 'Unable to create seed file directory.');
|
||||||
|
return redirect()->to($this->dbToolsUrlWithKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
$json = json_encode($seedPayload, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||||
|
if (!is_string($json) || @file_put_contents($file, $json) === false) {
|
||||||
|
session()->setFlashdata('setup_msg', 'Unable to write seed data file.');
|
||||||
|
return redirect()->to($this->dbToolsUrlWithKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
session()->setFlashdata('setup_msg', 'Master seed data exported: ' . $file . ' (' . count($tables) . ' table(s)).');
|
||||||
|
return redirect()->to($this->dbToolsUrlWithKey());
|
||||||
|
}
|
||||||
|
|
||||||
public function generateMigrations()
|
public function generateMigrations()
|
||||||
{
|
{
|
||||||
if ($deny = $this->ensureSetupAccess()) {
|
if ($deny = $this->ensureSetupAccess()) {
|
||||||
|
|
@ -515,6 +589,8 @@ class Setup extends BaseController
|
||||||
'sync_client_database' => (string) env('setup.sync.client.database', ''),
|
'sync_client_database' => (string) env('setup.sync.client.database', ''),
|
||||||
'master_schema_file' => $this->masterSchemaFilePath(),
|
'master_schema_file' => $this->masterSchemaFilePath(),
|
||||||
'master_schema_exists' => is_file($this->masterSchemaFilePath()),
|
'master_schema_exists' => is_file($this->masterSchemaFilePath()),
|
||||||
|
'master_seed_file' => $this->masterSeedFilePath(),
|
||||||
|
'master_seed_exists' => is_file($this->masterSeedFilePath()),
|
||||||
'diagnostics' => $this->collectSetupDiagnostics($dbError),
|
'diagnostics' => $this->collectSetupDiagnostics($dbError),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
@ -950,6 +1026,143 @@ class Setup extends BaseController
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function loadMasterSeedDefinition(): array
|
||||||
|
{
|
||||||
|
$file = $this->masterSeedFilePath();
|
||||||
|
if (!is_file($file)) {
|
||||||
|
return [
|
||||||
|
'errors' => ['Master seed data file not found: ' . $file],
|
||||||
|
'tables' => [],
|
||||||
|
'source' => 'master-seed-file',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$json = @file_get_contents($file);
|
||||||
|
$decoded = is_string($json) ? json_decode($json, true) : null;
|
||||||
|
if (!is_array($decoded) || !is_array($decoded['tables'] ?? null)) {
|
||||||
|
return [
|
||||||
|
'errors' => ['Invalid master seed data file format: ' . $file],
|
||||||
|
'tables' => [],
|
||||||
|
'source' => 'master-seed-file',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'errors' => [],
|
||||||
|
'tables' => $decoded['tables'],
|
||||||
|
'source' => 'master-seed-file',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
private function parseSeedTableList(): array
|
||||||
|
{
|
||||||
|
$raw = trim((string) env('setup.seed.export_tables', ''));
|
||||||
|
if ($raw === '') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$parts = preg_split('/[\s,]+/', $raw) ?: [];
|
||||||
|
$out = [];
|
||||||
|
foreach ($parts as $part) {
|
||||||
|
$name = strtolower(trim((string) $part));
|
||||||
|
$name = preg_replace('/[^a-z0-9_]/', '', $name) ?? '';
|
||||||
|
if ($name !== '') {
|
||||||
|
$out[$name] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_keys($out);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function applySeedDataToClientDb(): array
|
||||||
|
{
|
||||||
|
$definition = $this->loadMasterSeedDefinition();
|
||||||
|
if (!empty($definition['errors'])) {
|
||||||
|
return [
|
||||||
|
'applied' => 0,
|
||||||
|
'skipped' => 0,
|
||||||
|
'errors' => (array) $definition['errors'],
|
||||||
|
'tables' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$tables = is_array($definition['tables'] ?? null) ? $definition['tables'] : [];
|
||||||
|
if (empty($tables)) {
|
||||||
|
return [
|
||||||
|
'applied' => 0,
|
||||||
|
'skipped' => 0,
|
||||||
|
'errors' => [],
|
||||||
|
'tables' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$client = $this->resolveClientConnection();
|
||||||
|
$applied = 0;
|
||||||
|
$skipped = 0;
|
||||||
|
$errors = [];
|
||||||
|
$tableStats = [];
|
||||||
|
|
||||||
|
foreach ($tables as $tableName => $rows) {
|
||||||
|
$table = strtolower(trim((string) $tableName));
|
||||||
|
if ($table === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$safeTable = preg_replace('/[^a-z0-9_]/', '', $table) ?? '';
|
||||||
|
if ($safeTable === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->tableExistsInConnection($client, $safeTable)) {
|
||||||
|
$skipped++;
|
||||||
|
$tableStats[] = $safeTable . ' (skipped: table missing)';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_array($rows)) {
|
||||||
|
$skipped++;
|
||||||
|
$tableStats[] = $safeTable . ' (skipped: invalid row list)';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tableApplied = 0;
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
if (!is_array($row) || empty($row)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// REPLACE provides idempotent seed behavior: on re-run, rows with existing
|
||||||
|
// PRIMARY or UNIQUE keys are updated instead of duplicated.
|
||||||
|
// This ensures seed data can be safely applied multiple times.
|
||||||
|
$client->table($safeTable)->replace($row);
|
||||||
|
$applied++;
|
||||||
|
$tableApplied++;
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$errors[] = $safeTable . ': ' . $e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$tableStats[] = $safeTable . ' (rows: ' . $tableApplied . ')';
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'applied' => $applied,
|
||||||
|
'skipped' => $skipped,
|
||||||
|
'errors' => $errors,
|
||||||
|
'tables' => $tableStats,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
private function hasSyncRoleConfig(string $role): bool
|
private function hasSyncRoleConfig(string $role): bool
|
||||||
{
|
{
|
||||||
$role = strtolower(trim($role));
|
$role = strtolower(trim($role));
|
||||||
|
|
@ -2269,6 +2482,21 @@ class Setup extends BaseController
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function tableExistsInConnection($db, string $tableName): bool
|
||||||
|
{
|
||||||
|
$tableName = trim($tableName);
|
||||||
|
if ($tableName === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$tables = $db->listTables();
|
||||||
|
return in_array($tableName, $tables, true);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private function columnExists(string $tableName, string $columnName): bool
|
private function columnExists(string $tableName, string $columnName): bool
|
||||||
{
|
{
|
||||||
$tableName = trim($tableName);
|
$tableName = trim($tableName);
|
||||||
|
|
|
||||||
5
app/Database/master_seed_data.json
Normal file
5
app/Database/master_seed_data.json
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"generated_at": "",
|
||||||
|
"source": "manual",
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
|
@ -62,6 +62,21 @@
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<form method="post" action="<?= base_url('setup/db-tools/export-master-seed') ?>" style="margin-bottom:12px;">
|
||||||
|
<?= csrf_field() ?>
|
||||||
|
<?php if (!empty($setup_key ?? '')) : ?>
|
||||||
|
<input type="hidden" name="key" value="<?= esc($setup_key) ?>">
|
||||||
|
<?php endif; ?>
|
||||||
|
<button type="submit" class="btn btn-success">Export Master Seed Data (from current DB)</button>
|
||||||
|
<span class="text-muted" style="margin-left:10px;">File: <?= esc($master_seed_file ?? '') ?></span>
|
||||||
|
<?php if (!empty($master_seed_exists ?? false)) : ?>
|
||||||
|
<span class="label label-success" style="margin-left:6px;">Found</span>
|
||||||
|
<?php else : ?>
|
||||||
|
<span class="label label-warning" style="margin-left:6px;">Not Found</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
<div class="text-muted" style="margin-top:4px;">Uses .env key <code>setup.seed.export_tables</code> (comma-separated table names).</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
<form method="post" action="<?= base_url('setup/db-tools/schema-sync') ?>">
|
<form method="post" action="<?= base_url('setup/db-tools/schema-sync') ?>">
|
||||||
<?= csrf_field() ?>
|
<?= csrf_field() ?>
|
||||||
<?php if (!empty($setup_key ?? '')) : ?>
|
<?php if (!empty($setup_key ?? '')) : ?>
|
||||||
|
|
@ -145,6 +160,24 @@
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (array_key_exists('seed', (array) $sync_result)) : ?>
|
||||||
|
<?php $seedResult = is_array($sync_result['seed'] ?? null) ? $sync_result['seed'] : []; ?>
|
||||||
|
<?php if (!empty($seedResult['errors'] ?? [])) : ?>
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<strong>Seed Apply Errors:</strong>
|
||||||
|
<?php foreach (($seedResult['errors'] ?? []) as $seedErr) : ?>
|
||||||
|
<div><?= esc($seedErr) ?></div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php else : ?>
|
||||||
|
<div class="alert alert-success">
|
||||||
|
Seed data applied successfully.
|
||||||
|
Rows upserted: <?= esc((string) ($seedResult['applied'] ?? 0)) ?>,
|
||||||
|
tables skipped: <?= esc((string) ($seedResult['skipped'] ?? 0)) ?>.
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Generated SQL</label>
|
<label>Generated SQL</label>
|
||||||
<textarea class="form-control" rows="14" readonly><?php
|
<textarea class="form-control" rows="14" readonly><?php
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user