Add master seed data export and auto-apply during setup install - idempotent REPLACE mode prevents duplicates

This commit is contained in:
DevSoftTech Server 2026-03-18 04:41:51 +05:30
parent 67fbc68c1c
commit ee3193ce43
4 changed files with 267 additions and 0 deletions

View File

@ -313,6 +313,7 @@ $routes->get('Ipd_discharge/show_file3/(:num)', 'Ipd_discharge::show_file3/$1');
$routes->get('setup/db-tools', 'Setup::dbTools');
$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-seed', 'Setup::exportMasterSeedData');
$routes->post('setup/db-tools/schema-sync', 'Setup::schemaSync');
$routes->post('setup/db-tools/schema-sync-step', 'Setup::schemaSyncStep');
$routes->post('setup/db-tools/repair-auth-schema', 'Setup::repairAuthSchema');

View File

@ -30,6 +30,20 @@ class Setup extends BaseController
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
{
return is_file($this->lockFilePath());
@ -181,6 +195,16 @@ class Setup extends BaseController
$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;
} catch (\Throwable $e) {
log_message('error', 'Setup schemaSync failed: {message}', ['message' => $e->getMessage()]);
@ -223,6 +247,56 @@ class Setup extends BaseController
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()
{
if ($deny = $this->ensureSetupAccess()) {
@ -515,6 +589,8 @@ class Setup extends BaseController
'sync_client_database' => (string) env('setup.sync.client.database', ''),
'master_schema_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),
];
}
@ -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
{
$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
{
$tableName = trim($tableName);

View File

@ -0,0 +1,5 @@
{
"generated_at": "",
"source": "manual",
"tables": {}
}

View File

@ -62,6 +62,21 @@
<?php endif; ?>
</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') ?>">
<?= csrf_field() ?>
<?php if (!empty($setup_key ?? '')) : ?>
@ -145,6 +160,24 @@
</div>
<?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">
<label>Generated SQL</label>
<textarea class="form-control" rows="14" readonly><?php