This commit is contained in:
Satyam 2026-06-15 14:23:10 +00:00 committed by GitHub
commit 0d302d148a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 651 additions and 2 deletions

View File

@ -326,6 +326,10 @@ return [
'class' => Database\Structure\ShowCreateController::class,
'arguments' => [ResponseRenderer::class, Template::class, DatabaseInterface::class],
],
Database\Structure\CopyStructureController::class => [
'class' => Database\Structure\CopyStructureController::class,
'arguments' => [ResponseRenderer::class, DatabaseInterface::class, DbTableExists::class],
],
Database\StructureController::class => [
'class' => Database\StructureController::class,
'arguments' => [
@ -1131,6 +1135,10 @@ return [
DbTableExists::class,
],
],
Table\Structure\CopyStructureController::class => [
'class' => Table\Structure\CopyStructureController::class,
'arguments' => [ResponseRenderer::class, DatabaseInterface::class, DbTableExists::class],
],
Table\Structure\ReservedWordCheckController::class => [
'class' => Table\Structure\ReservedWordCheckController::class,
'arguments' => [ResponseRenderer::class, Config::class],

View File

@ -948,6 +948,11 @@
<code><![CDATA[DatabaseInterface::getInstance()]]></code>
</DeprecatedMethod>
</file>
<file src="src/Controllers/Database/Structure/CopyStructureController.php">
<PossiblyUnusedReturnValue>
<code><![CDATA[Response]]></code>
</PossiblyUnusedReturnValue>
</file>
<file src="src/Controllers/Database/Structure/CopyTableController.php">
<PossiblyUnusedMethod>
<code><![CDATA[__construct]]></code>
@ -2475,6 +2480,11 @@
<code><![CDATA[[$request->getParam('field')]]]></code>
</MixedArgumentTypeCoercion>
</file>
<file src="src/Controllers/Table/Structure/CopyStructureController.php">
<PossiblyUnusedReturnValue>
<code><![CDATA[Response]]></code>
</PossiblyUnusedReturnValue>
</file>
<file src="src/Controllers/Table/Structure/MoveColumnsController.php">
<MixedArgumentTypeCoercion>
<code><![CDATA[$moveColumns]]></code>

View File

@ -1,7 +1,7 @@
import $ from 'jquery';
import * as bootstrap from 'bootstrap';
import { AJAX } from '../modules/ajax.ts';
import { getForeignKeyCheckboxLoader, loadForeignKeyCheckbox } from '../modules/functions.ts';
import { copyToClipboard, displayCopyNotification, getForeignKeyCheckboxLoader, loadForeignKeyCheckbox } from '../modules/functions.ts';
import { Navigation } from '../modules/navigation.ts';
import { CommonParams } from '../modules/common.ts';
import { ajaxRemoveMessage, ajaxShowMessage } from '../modules/ajax-message.ts';
@ -33,6 +33,7 @@ AJAX.registerTeardown('database/structure.js', function () {
$(document).off('click', 'a.truncate_table_anchor.ajax');
$(document).off('click', 'a.drop_table_anchor.ajax');
$(document).off('click', 'a.favorite_table_anchor.ajax');
$(document).off('click', '#copyStructureBtn');
$('a.real_row_count').off('click');
$('a.row_count_sum').off('click');
$('select[name=submit_mult]').off('change');
@ -323,4 +324,40 @@ AJAX.registerOnload('database/structure.js', function () {
event.preventDefault();
fetchRealRowCount($(this));
});
function copyStructureSql (sql: string): void {
if (typeof navigator.clipboard !== 'undefined' && typeof navigator.clipboard.writeText === 'function') {
navigator.clipboard.writeText(sql).then(() => {
displayCopyNotification(true);
}).catch(() => {
displayCopyNotification(copyToClipboard(sql, '<textarea>'));
});
} else {
displayCopyNotification(copyToClipboard(sql, '<textarea>'));
}
}
$(document).on('click', '#copyStructureBtn', function (event) {
event.preventDefault();
const argsep = CommonParams.get('arg_separator');
const db = CommonParams.get('db');
const data =
'ajax_request=true' + argsep +
'ajax_page_request=true' + argsep +
'token=' + encodeURIComponent(CommonParams.get('token')) + argsep +
'db=' + encodeURIComponent(db);
const $msg = ajaxShowMessage(window.Messages.strProcessingRequest);
$.post('index.php?route=/database/structure/copy-structure', data, function (response) {
ajaxRemoveMessage($msg);
if (typeof response !== 'undefined' && response.success === true && typeof response.sql === 'string') {
copyStructureSql(response.sql);
} else {
const err = typeof response !== 'undefined' && typeof response.error === 'string' ? response.error : '';
ajaxShowMessage(window.Messages.strErrorProcessingRequest + ' : ' + err, false);
}
}, 'json').fail(function () {
ajaxRemoveMessage($msg);
ajaxShowMessage(window.Messages.strErrorProcessingRequest, false);
});
});
});

View File

@ -1,6 +1,6 @@
import $ from 'jquery';
import { AJAX } from '../modules/ajax.ts';
import { checkReservedWordColumns, checkTableEditForm, prepareForAjaxRequest } from '../modules/functions.ts';
import { checkReservedWordColumns, checkTableEditForm, copyToClipboard, displayCopyNotification, prepareForAjaxRequest } from '../modules/functions.ts';
import { Navigation } from '../modules/navigation.ts';
import { CommonParams } from '../modules/common.ts';
import highlightSql from '../modules/sql-highlight.ts';
@ -60,6 +60,7 @@ AJAX.registerTeardown('table/structure.js', function () {
$('body').off('click', '#fieldsForm button.mult_submit');
$(document).off('click', 'a[id^=partition_action].ajax');
$(document).off('click', '#remove_partitioning.ajax');
$(document).off('click', '#copyTableStructureBtn');
});
AJAX.registerOnload('table/structure.js', function () {
@ -486,4 +487,42 @@ AJAX.registerOnload('table/structure.js', function () {
$(document).on('change', 'select[name=after_field]', function () {
checkFirst();
});
function copyTableStructureSql (sql: string): void {
if (typeof navigator.clipboard !== 'undefined' && typeof navigator.clipboard.writeText === 'function') {
navigator.clipboard.writeText(sql).then(() => {
displayCopyNotification(true);
}).catch(() => {
displayCopyNotification(copyToClipboard(sql, '<textarea>'));
});
} else {
displayCopyNotification(copyToClipboard(sql, '<textarea>'));
}
}
$(document).on('click', '#copyTableStructureBtn', function (event) {
event.preventDefault();
const argsep = CommonParams.get('arg_separator');
const db = CommonParams.get('db');
const table = CommonParams.get('table');
const data =
'ajax_request=true' + argsep +
'ajax_page_request=true' + argsep +
'token=' + encodeURIComponent(CommonParams.get('token')) + argsep +
'db=' + encodeURIComponent(db) + argsep +
'table=' + encodeURIComponent(table);
const $msg = ajaxShowMessage(window.Messages.strProcessingRequest);
$.post('index.php?route=/table/structure/copy-structure', data, function (response) {
ajaxRemoveMessage($msg);
if (typeof response !== 'undefined' && response.success === true && typeof response.sql === 'string') {
copyTableStructureSql(response.sql);
} else {
const err = typeof response !== 'undefined' && typeof response.error === 'string' ? response.error : '';
ajaxShowMessage(window.Messages.strErrorProcessingRequest + ' : ' + err, false);
}
}, 'json').fail(function () {
ajaxRemoveMessage($msg);
ajaxShowMessage(window.Messages.strErrorProcessingRequest, false);
});
});
});

View File

@ -11,6 +11,9 @@
<hr>
<p class="d-print-none">
<button type="button" class="btn btn-link p-0 jsPrintButton">{{ get_icon('b_print', t('Print'), true) }}</button>
<button type="button" id="copyStructureBtn" class="btn btn-link p-0">
{{ get_icon('b_export', t('Copy DB Structure'), true) }}
</button>
<a href="{{ url('/database/data-dictionary', {'db': database, 'goto': url('/database/structure')}) }}">
{{ get_icon('b_tblanalyse', t('Data dictionary'), true) }}
</a>

View File

@ -424,6 +424,11 @@
</a>
{% endif %}
{% endif %}
{% if not db_is_system_schema %}
<button type="button" id="copyTableStructureBtn" class="btn btn-link p-0">
{{ get_icon('b_export', t('Copy Table Structure'), true) }}
</button>
{% endif %}
</div>
{% if not tbl_is_view and not db_is_system_schema %}
<form method="post" action="{{ url('/table/add-field') }}" id="addColumns" name="addColumns" class="d-print-none">

View File

@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Database\Structure;
use PhpMyAdmin\Controllers\InvocableController;
use PhpMyAdmin\Current;
use PhpMyAdmin\Dbal\DatabaseInterface;
use PhpMyAdmin\DbTableExists;
use PhpMyAdmin\Http\Response;
use PhpMyAdmin\Http\ServerRequest;
use PhpMyAdmin\Identifiers\DatabaseName;
use PhpMyAdmin\Message;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Routing\Route;
use function __;
use function implode;
use function sprintf;
#[Route('/database/structure/copy-structure', ['POST'])]
final readonly class CopyStructureController implements InvocableController
{
public function __construct(
private ResponseRenderer $response,
private DatabaseInterface $dbi,
private DbTableExists $dbTableExists,
) {
}
public function __invoke(ServerRequest $request): Response
{
if (Current::$database === '') {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', Message::error(__('No databases selected.')));
return $this->response->response();
}
$databaseName = DatabaseName::tryFrom($request->getParam('db'));
if ($databaseName === null || ! $this->dbTableExists->selectDatabase($databaseName)) {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', Message::error(__('No databases selected.')));
return $this->response->response();
}
$dbName = $databaseName->getName();
/** @var string[] $tableNames */
$tableNames = $this->dbi->getTables($dbName);
$baseTables = [];
$views = [];
foreach ($tableNames as $table) {
$object = $this->dbi->getTable($dbName, $table);
if ($object->isView()) {
$views[] = $table;
} else {
$baseTables[] = $table;
}
}
$segments = [
sprintf('-- Database: %s', $dbName),
];
foreach ($baseTables as $table) {
$object = $this->dbi->getTable($dbName, $table);
$segments[] = $object->showCreate();
}
if ($views !== []) {
$segments[] = '-- Views';
foreach ($views as $table) {
$object = $this->dbi->getTable($dbName, $table);
$segments[] = $object->showCreate();
}
}
$this->response->addJSON('sql', implode("\n\n", $segments));
return $this->response->response();
}
}

View File

@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Table\Structure;
use PhpMyAdmin\Controllers\InvocableController;
use PhpMyAdmin\Current;
use PhpMyAdmin\Dbal\DatabaseInterface;
use PhpMyAdmin\DbTableExists;
use PhpMyAdmin\Http\Response;
use PhpMyAdmin\Http\ServerRequest;
use PhpMyAdmin\Identifiers\DatabaseName;
use PhpMyAdmin\Identifiers\TableName;
use PhpMyAdmin\Message;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Routing\Route;
use function __;
#[Route('/table/structure/copy-structure', ['POST'])]
final readonly class CopyStructureController implements InvocableController
{
public function __construct(
private ResponseRenderer $response,
private DatabaseInterface $dbi,
private DbTableExists $dbTableExists,
) {
}
public function __invoke(ServerRequest $request): Response
{
if (Current::$database === '') {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', Message::error(__('No databases selected.')));
return $this->response->response();
}
if (Current::$table === '') {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', Message::error(__('No table selected.')));
return $this->response->response();
}
$databaseName = DatabaseName::tryFrom($request->getParam('db'));
if ($databaseName === null || ! $this->dbTableExists->selectDatabase($databaseName)) {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', Message::error(__('No databases selected.')));
return $this->response->response();
}
$tableName = TableName::tryFrom($request->getParam('table'));
if ($tableName === null || ! $this->dbTableExists->hasTable($databaseName, $tableName)) {
$this->response->setRequestStatus(false);
$this->response->addJSON('message', Message::error(__('No table selected.')));
return $this->response->response();
}
$object = $this->dbi->getTable($databaseName->getName(), $tableName->getName());
$this->response->addJSON('sql', $object->showCreate());
return $this->response->response();
}
}

View File

@ -0,0 +1,196 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Tests\Controllers\Database\Structure;
use PhpMyAdmin\Controllers\Database\Structure\CopyStructureController;
use PhpMyAdmin\Current;
use PhpMyAdmin\Dbal\DatabaseInterface;
use PhpMyAdmin\DbTableExists;
use PhpMyAdmin\Http\Factory\ServerRequestFactory;
use PhpMyAdmin\Tests\AbstractTestCase;
use PhpMyAdmin\Tests\Stubs\ResponseRenderer as ResponseStub;
use PHPUnit\Framework\Attributes\CoversClass;
use function strpos;
#[CoversClass(CopyStructureController::class)]
final class CopyStructureControllerTest extends AbstractTestCase
{
public function testReturnErrorWhenNoDatabaseSet(): void
{
Current::$database = '';
$dbi = $this->createDatabaseInterface();
DatabaseInterface::$instance = $dbi;
$responseRenderer = new ResponseStub();
$request = ServerRequestFactory::create()->createServerRequest('POST', 'http://example.com/')
->withQueryParams(['db' => '']);
(new CopyStructureController($responseRenderer, $dbi, new DbTableExists($dbi)))($request);
self::assertFalse($responseRenderer->hasSuccessState());
$json = $responseRenderer->getJSONResult();
self::assertArrayHasKey('message', $json);
$message = $json['message'];
self::assertIsString($message);
self::assertStringContainsString('No databases selected', $message);
}
public function testReturnErrorWhenDatabaseNameInvalid(): void
{
Current::$database = 'test_db';
$dbi = $this->createDatabaseInterface();
DatabaseInterface::$instance = $dbi;
$responseRenderer = new ResponseStub();
// empty 'db' param → DatabaseName::tryFrom returns null
$request = ServerRequestFactory::create()->createServerRequest('POST', 'http://example.com/')
->withQueryParams(['db' => '']);
(new CopyStructureController($responseRenderer, $dbi, new DbTableExists($dbi)))($request);
self::assertFalse($responseRenderer->hasSuccessState());
$json = $responseRenderer->getJSONResult();
self::assertArrayHasKey('message', $json);
$message = $json['message'];
self::assertIsString($message);
self::assertStringContainsString('No databases selected', $message);
}
public function testReturnsSqlForTablesOnly(): void
{
Current::$database = 'test_db';
$createSql = "CREATE TABLE `orders` (\n `id` int(11) NOT NULL\n) ENGINE=InnoDB";
$dbiDummy = $this->createDbiDummy();
$dbiDummy->addSelectDb('test_db');
// getTables() call
$dbiDummy->addResult(
'SHOW TABLES FROM `test_db`;',
[['orders']],
);
// showCreate() for orders
$dbiDummy->addResult(
'SHOW CREATE TABLE `test_db`.`orders`',
[['orders', $createSql]],
['Table', 'Create Table'],
);
$dbi = $this->createDatabaseInterface($dbiDummy);
DatabaseInterface::$instance = $dbi;
// Pre-seed the TABLE_TYPE cache so isView() returns false without an extra query
$dbi->getCache()->cacheTableValue('test_db', 'orders', 'TABLE_TYPE', 'BASE TABLE');
$responseRenderer = new ResponseStub();
$request = ServerRequestFactory::create()->createServerRequest('POST', 'http://example.com/')
->withQueryParams(['db' => 'test_db'])
->withParsedBody(['db' => 'test_db']);
(new CopyStructureController($responseRenderer, $dbi, new DbTableExists($dbi)))($request);
self::assertTrue($responseRenderer->hasSuccessState());
$json = $responseRenderer->getJSONResult();
self::assertArrayHasKey('sql', $json);
$sql = $json['sql'];
self::assertIsString($sql);
self::assertStringContainsString('-- Database: test_db', $sql);
self::assertStringContainsString($createSql, $sql);
self::assertStringNotContainsString('-- Views', $sql);
$dbiDummy->assertAllSelectsConsumed();
$dbiDummy->assertAllQueriesConsumed();
}
public function testReturnsSqlWithViewsSeparated(): void
{
Current::$database = 'test_db';
$tableSql = "CREATE TABLE `products` (\n `id` int(11) NOT NULL\n) ENGINE=InnoDB";
$viewSql = 'CREATE VIEW `v_products` AS SELECT * FROM `products`';
$dbiDummy = $this->createDbiDummy();
$dbiDummy->addSelectDb('test_db');
$dbiDummy->addResult(
'SHOW TABLES FROM `test_db`;',
[['products'], ['v_products']],
);
$dbiDummy->addResult(
'SHOW CREATE TABLE `test_db`.`products`',
[['products', $tableSql]],
['Table', 'Create Table'],
);
$dbiDummy->addResult(
'SHOW CREATE TABLE `test_db`.`v_products`',
[['v_products', $viewSql]],
['Table', 'Create Table'],
);
$dbi = $this->createDatabaseInterface($dbiDummy);
DatabaseInterface::$instance = $dbi;
$dbi->getCache()->cacheTableValue('test_db', 'products', 'TABLE_TYPE', 'BASE TABLE');
$dbi->getCache()->cacheTableValue('test_db', 'v_products', 'TABLE_TYPE', 'VIEW');
$responseRenderer = new ResponseStub();
$request = ServerRequestFactory::create()->createServerRequest('POST', 'http://example.com/')
->withQueryParams(['db' => 'test_db'])
->withParsedBody(['db' => 'test_db']);
(new CopyStructureController($responseRenderer, $dbi, new DbTableExists($dbi)))($request);
self::assertTrue($responseRenderer->hasSuccessState());
$json = $responseRenderer->getJSONResult();
self::assertArrayHasKey('sql', $json);
$sql = $json['sql'];
self::assertIsString($sql);
self::assertStringContainsString('-- Database: test_db', $sql);
self::assertStringContainsString($tableSql, $sql);
self::assertStringContainsString('-- Views', $sql);
self::assertStringContainsString($viewSql, $sql);
// Views section must come after tables
self::assertGreaterThan(
strpos($sql, $tableSql),
strpos($sql, '-- Views'),
);
$dbiDummy->assertAllSelectsConsumed();
$dbiDummy->assertAllQueriesConsumed();
}
public function testReturnsSqlForEmptyDatabase(): void
{
Current::$database = 'empty_db';
$dbiDummy = $this->createDbiDummy();
$dbiDummy->addSelectDb('empty_db');
$dbiDummy->addResult('SHOW TABLES FROM `empty_db`;', []);
$dbi = $this->createDatabaseInterface($dbiDummy);
DatabaseInterface::$instance = $dbi;
$responseRenderer = new ResponseStub();
$request = ServerRequestFactory::create()->createServerRequest('POST', 'http://example.com/')
->withQueryParams(['db' => 'empty_db'])
->withParsedBody(['db' => 'empty_db']);
(new CopyStructureController($responseRenderer, $dbi, new DbTableExists($dbi)))($request);
self::assertTrue($responseRenderer->hasSuccessState());
$json = $responseRenderer->getJSONResult();
self::assertArrayHasKey('sql', $json);
$sql = $json['sql'];
self::assertIsString($sql);
self::assertStringContainsString('-- Database: empty_db', $sql);
self::assertStringNotContainsString('CREATE TABLE', $sql);
self::assertStringNotContainsString('-- Views', $sql);
$dbiDummy->assertAllSelectsConsumed();
$dbiDummy->assertAllQueriesConsumed();
}
}

View File

@ -363,6 +363,9 @@
<hr>
<p class="d-print-none">
<button type="button" class="btn btn-link p-0 jsPrintButton"><span class="text-nowrap"><img src="themes/dot.gif" title="Print" alt="Print" class="icon ic_b_print">&nbsp;Print</span></button>
<button type="button" id="copyStructureBtn" class="btn btn-link p-0">
<span class="text-nowrap"><img src="themes/dot.gif" title="Copy DB Structure" alt="Copy DB Structure" class="icon ic_b_export">&nbsp;Copy DB Structure</span>
</button>
<a href="index.php?route=/database/data-dictionary&db=test_db&goto=index.php%3Froute%3D%2Fdatabase%2Fstructure%26lang%3Den&lang=en">
<span class="text-nowrap"><img src="themes/dot.gif" title="Data dictionary" alt="Data dictionary" class="icon ic_b_tblanalyse">&nbsp;Data dictionary</span>
</a>

View File

@ -0,0 +1,192 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Tests\Controllers\Table\Structure;
use PhpMyAdmin\Controllers\Table\Structure\CopyStructureController;
use PhpMyAdmin\Current;
use PhpMyAdmin\Dbal\DatabaseInterface;
use PhpMyAdmin\DbTableExists;
use PhpMyAdmin\Http\Factory\ServerRequestFactory;
use PhpMyAdmin\Tests\AbstractTestCase;
use PhpMyAdmin\Tests\Stubs\ResponseRenderer as ResponseStub;
use PHPUnit\Framework\Attributes\CoversClass;
#[CoversClass(CopyStructureController::class)]
final class CopyStructureControllerTest extends AbstractTestCase
{
public function testReturnErrorWhenNoDatabaseSet(): void
{
Current::$database = '';
Current::$table = 'orders';
$dbi = $this->createDatabaseInterface();
DatabaseInterface::$instance = $dbi;
$responseRenderer = new ResponseStub();
$request = ServerRequestFactory::create()->createServerRequest('POST', 'http://example.com/')
->withQueryParams(['db' => '', 'table' => 'orders']);
(new CopyStructureController($responseRenderer, $dbi, new DbTableExists($dbi)))($request);
self::assertFalse($responseRenderer->hasSuccessState());
$json = $responseRenderer->getJSONResult();
self::assertArrayHasKey('message', $json);
$message = $json['message'];
self::assertIsString($message);
self::assertStringContainsString('No databases selected', $message);
}
public function testReturnErrorWhenNoTableSet(): void
{
Current::$database = 'test_db';
Current::$table = '';
$dbi = $this->createDatabaseInterface();
DatabaseInterface::$instance = $dbi;
$responseRenderer = new ResponseStub();
$request = ServerRequestFactory::create()->createServerRequest('POST', 'http://example.com/')
->withQueryParams(['db' => 'test_db', 'table' => '']);
(new CopyStructureController($responseRenderer, $dbi, new DbTableExists($dbi)))($request);
self::assertFalse($responseRenderer->hasSuccessState());
$json = $responseRenderer->getJSONResult();
self::assertArrayHasKey('message', $json);
$message = $json['message'];
self::assertIsString($message);
self::assertStringContainsString('No table selected', $message);
}
public function testReturnErrorWhenDatabaseNameInvalid(): void
{
Current::$database = 'test_db';
Current::$table = 'orders';
$dbi = $this->createDatabaseInterface();
DatabaseInterface::$instance = $dbi;
$responseRenderer = new ResponseStub();
// 'db' param is empty → DatabaseName::tryFrom returns null, no DB call made
$request = ServerRequestFactory::create()->createServerRequest('POST', 'http://example.com/')
->withQueryParams(['db' => '', 'table' => 'orders']);
(new CopyStructureController($responseRenderer, $dbi, new DbTableExists($dbi)))($request);
self::assertFalse($responseRenderer->hasSuccessState());
$json = $responseRenderer->getJSONResult();
self::assertArrayHasKey('message', $json);
$message = $json['message'];
self::assertIsString($message);
self::assertStringContainsString('No databases selected', $message);
}
public function testReturnsSqlForTable(): void
{
Current::$database = 'test_db';
Current::$table = 'orders';
$createSql = "CREATE TABLE `orders` (\n `id` int(11) NOT NULL\n) ENGINE=InnoDB";
$dbiDummy = $this->createDbiDummy();
$dbiDummy->addSelectDb('test_db');
// DbTableExists::hasTable issues SELECT 1 FROM `db`.`table` LIMIT 1
$dbiDummy->addResult('SELECT 1 FROM `test_db`.`orders` LIMIT 1;', [['1']]);
// showCreate()
$dbiDummy->addResult(
'SHOW CREATE TABLE `test_db`.`orders`',
[['orders', $createSql]],
['Table', 'Create Table'],
);
$dbi = $this->createDatabaseInterface($dbiDummy);
DatabaseInterface::$instance = $dbi;
$responseRenderer = new ResponseStub();
$request = ServerRequestFactory::create()->createServerRequest('POST', 'http://example.com/')
->withQueryParams(['db' => 'test_db', 'table' => 'orders'])
->withParsedBody(['db' => 'test_db', 'table' => 'orders']);
(new CopyStructureController($responseRenderer, $dbi, new DbTableExists($dbi)))($request);
self::assertTrue($responseRenderer->hasSuccessState());
$json = $responseRenderer->getJSONResult();
self::assertArrayHasKey('sql', $json);
$sql = $json['sql'];
self::assertIsString($sql);
self::assertSame($createSql, $sql);
$dbiDummy->assertAllSelectsConsumed();
$dbiDummy->assertAllQueriesConsumed();
}
public function testReturnsSqlForView(): void
{
Current::$database = 'test_db';
Current::$table = 'v_orders';
$viewSql = 'CREATE VIEW `v_orders` AS SELECT * FROM `orders`';
$dbiDummy = $this->createDbiDummy();
$dbiDummy->addSelectDb('test_db');
$dbiDummy->addResult('SELECT 1 FROM `test_db`.`v_orders` LIMIT 1;', [['1']]);
$dbiDummy->addResult(
'SHOW CREATE TABLE `test_db`.`v_orders`',
[['v_orders', $viewSql]],
['Table', 'Create Table'],
);
$dbi = $this->createDatabaseInterface($dbiDummy);
DatabaseInterface::$instance = $dbi;
$responseRenderer = new ResponseStub();
$request = ServerRequestFactory::create()->createServerRequest('POST', 'http://example.com/')
->withQueryParams(['db' => 'test_db', 'table' => 'v_orders'])
->withParsedBody(['db' => 'test_db', 'table' => 'v_orders']);
(new CopyStructureController($responseRenderer, $dbi, new DbTableExists($dbi)))($request);
self::assertTrue($responseRenderer->hasSuccessState());
$json = $responseRenderer->getJSONResult();
self::assertArrayHasKey('sql', $json);
$sql = $json['sql'];
self::assertIsString($sql);
self::assertSame($viewSql, $sql);
$dbiDummy->assertAllSelectsConsumed();
$dbiDummy->assertAllQueriesConsumed();
}
public function testReturnErrorWhenTableDoesNotExist(): void
{
Current::$database = 'test_db';
Current::$table = 'ghost_table';
$dbiDummy = $this->createDbiDummy();
$dbiDummy->addSelectDb('test_db');
// hasTable SELECT fails (table not found)
$dbiDummy->addResult('SELECT 1 FROM `test_db`.`ghost_table` LIMIT 1;', false);
$dbi = $this->createDatabaseInterface($dbiDummy);
DatabaseInterface::$instance = $dbi;
$responseRenderer = new ResponseStub();
$request = ServerRequestFactory::create()->createServerRequest('POST', 'http://example.com/')
->withQueryParams(['db' => 'test_db', 'table' => 'ghost_table'])
->withParsedBody(['db' => 'test_db', 'table' => 'ghost_table']);
(new CopyStructureController($responseRenderer, $dbi, new DbTableExists($dbi)))($request);
self::assertFalse($responseRenderer->hasSuccessState());
$json = $responseRenderer->getJSONResult();
self::assertArrayHasKey('message', $json);
$message = $json['message'];
self::assertIsString($message);
self::assertStringContainsString('No table selected', $message);
$dbiDummy->assertAllSelectsConsumed();
$dbiDummy->assertAllQueriesConsumed();
}
}

View File

@ -239,6 +239,7 @@ final class RoutesTest extends TestCase
'/database/structure/central-columns/remove' => CentralColumns\RemoveController::class,
'/database/structure/change-prefix-form' => Database\Structure\ChangePrefixFormController::class,
'/database/structure/copy-form' => Database\Structure\CopyFormController::class,
'/database/structure/copy-structure' => Database\Structure\CopyStructureController::class,
'/database/structure/copy-table' => Database\Structure\CopyTableController::class,
'/database/structure/copy-table-with-prefix' => Database\Structure\CopyTableWithPrefixController::class,
'/database/structure/drop-form' => Database\Structure\DropFormController::class,
@ -357,6 +358,7 @@ final class RoutesTest extends TestCase
'/table/structure/central-columns-add' => Table\Structure\CentralColumnsAddController::class,
'/table/structure/central-columns-remove' => Table\Structure\CentralColumnsRemoveController::class,
'/table/structure/change' => Table\Structure\ChangeController::class,
'/table/structure/copy-structure' => Table\Structure\CopyStructureController::class,
'/table/structure/drop' => Table\DropColumnController::class,
'/table/structure/drop-confirm' => Table\DropColumnConfirmationController::class,
'/table/structure/fulltext' => Table\Structure\FulltextController::class,