Merge a5f73eb951 into 54b67bdbf1
This commit is contained in:
commit
0d302d148a
@ -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],
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
68
src/Controllers/Table/Structure/CopyStructureController.php
Normal file
68
src/Controllers/Table/Structure/CopyStructureController.php
Normal 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();
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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"> 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"> 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"> Data dictionary</span>
|
||||
</a>
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user