Skip to content

Commit de3f3b2

Browse files
author
Sergii Kovalenko
committed
MAGETWO-87928: Implement infrastructure for safe-rollback feature
1 parent 15d1a96 commit de3f3b2

File tree

12 files changed

+756
-21
lines changed

12 files changed

+756
-21
lines changed

app/etc/di.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@
177177
<preference for="Magento\Setup\Model\Declaration\Schema\Db\DbSchemaReaderInterface" type="Magento\Setup\Model\Declaration\Schema\Db\MySQL\DbSchemaReader" />
178178
<preference for="Magento\Setup\Model\Declaration\Schema\Db\DbSchemaWriterInterface" type="Magento\Setup\Model\Declaration\Schema\Db\MySQL\DbSchemaWriter" />
179179
<preference for="Magento\Setup\Model\Declaration\Schema\SchemaConfigInterface" type="Magento\Setup\Model\Declaration\Schema\SchemaConfig" />
180+
<preference for="Magento\Setup\Model\Declaration\Schema\DataSavior\DumpAccessorInterface" type="Magento\Setup\Model\Declaration\Schema\FileSystem\Csv" />
180181
<type name="Magento\Framework\Model\ResourceModel\Db\TransactionManager" shared="false" />
181182
<type name="Magento\Framework\Acl\Data\Cache">
182183
<arguments>
@@ -1447,6 +1448,10 @@
14471448
<item name="add_complex_element" xsi:type="object">Magento\Setup\Model\Declaration\Schema\Operations\AddComplexElement</item>
14481449
<item name="modify_table" xsi:type="object">Magento\Setup\Model\Declaration\Schema\Operations\ModifyTable</item>
14491450
</argument>
1451+
<argument name="dataSaviorsCollection" xsi:type="array">
1452+
<item name="table_savior" xsi:type="object">Magento\Setup\Model\Declaration\Schema\DataSavior\TableSavior</item>
1453+
<item name="column_savior" xsi:type="object">Magento\Setup\Model\Declaration\Schema\DataSavior\ColumnSavior</item>
1454+
</argument>
14501455
</arguments>
14511456
</type>
14521457
<type name="Magento\Setup\Model\Declaration\Schema\Sharding">
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
namespace Magento\Setup\Model\Declaration\Schema\DataSavior;
8+
9+
use Magento\Framework\App\ResourceConnection;
10+
use Magento\Setup\Model\Declaration\Schema\Dto\Column;
11+
use Magento\Setup\Model\Declaration\Schema\Dto\ElementInterface;
12+
use Magento\Setup\Model\Declaration\Schema\Dto\Table;
13+
14+
/**
15+
* Allows to dump and restore data for one specific field
16+
*/
17+
class ColumnSavior implements DataSaviorInterface
18+
{
19+
/**
20+
* @var SelectGeneratorFactory
21+
*/
22+
private $selectGeneratorFactory;
23+
24+
/**
25+
* @var ResourceConnection
26+
*/
27+
private $resourceConnection;
28+
29+
/**
30+
* @var UniqueConstraintsResolver
31+
*/
32+
private $uniqueConstraintsResolver;
33+
34+
/**
35+
* @var DumpAccessorInterface
36+
*/
37+
private $dumpAccessor;
38+
39+
/**
40+
* TableDump constructor.
41+
* @param ResourceConnection $resourceConnection
42+
* @param SelectGeneratorFactory $selectGeneratorFactory
43+
* @param DumpAccessorInterface $dumpAccessor
44+
* @param UniqueConstraintsResolver $uniqueConstraintsResolver
45+
*/
46+
public function __construct(
47+
ResourceConnection $resourceConnection,
48+
SelectGeneratorFactory $selectGeneratorFactory,
49+
DumpAccessorInterface $dumpAccessor,
50+
UniqueConstraintsResolver $uniqueConstraintsResolver
51+
) {
52+
$this->selectGeneratorFactory = $selectGeneratorFactory;
53+
$this->resourceConnection = $resourceConnection;
54+
$this->uniqueConstraintsResolver = $uniqueConstraintsResolver;
55+
$this->dumpAccessor = $dumpAccessor;
56+
}
57+
58+
/**
59+
* Prepare select to database
60+
*
61+
* @param Column $column
62+
* @param array $fieldsToDump
63+
* @return \Magento\Framework\DB\Select
64+
*/
65+
private function prepareColumnSelect(Column $column, array $fieldsToDump)
66+
{
67+
$adapter = $this->resourceConnection->getConnection($column->getTable()->getResource());
68+
$select = $adapter
69+
->select()
70+
->from($column->getTable()->getName(), $fieldsToDump);
71+
72+
return $select;
73+
}
74+
75+
/**
76+
* @inheritdoc
77+
* @param Column | ElementInterface $column
78+
* @return void
79+
*/
80+
public function dump(ElementInterface $column)
81+
{
82+
$columns = $this->uniqueConstraintsResolver->resolve($column->getTable());
83+
84+
/**
85+
* Only if table have unique keys or primary key
86+
*/
87+
if ($columns) {
88+
$connectionName = $column->getTable()->getResource();
89+
$columns[] = $column->getName();
90+
$select = $this->prepareColumnSelect($column, $columns);
91+
$selectGenerator = $this->selectGeneratorFactory->create();
92+
$resourceSignature = $this->generateDumpFileSignature($column);
93+
94+
foreach ($selectGenerator->generator($select, $connectionName) as $data) {
95+
$this->dumpAccessor->save($resourceSignature, $data);
96+
}
97+
}
98+
}
99+
100+
/**
101+
* Do Insert on duplicate to table, where field should be restored
102+
*
103+
* @param Table $table
104+
* @param array $data
105+
*/
106+
private function applyDumpChunk(Table $table, $data)
107+
{
108+
$columns = [];
109+
$adapter = $this->resourceConnection->getConnection($table->getResource());
110+
$firstRow = reset($data);
111+
112+
/**
113+
* Prepare all table fields
114+
*/
115+
foreach ($table->getColumns() as $column) {
116+
$columns[$column->getName()] = $column->getName();
117+
}
118+
119+
$adapter->insertOnDuplicate($table->getName(), $data, array_keys($firstRow));
120+
}
121+
122+
/**
123+
* @param Column | ElementInterface $column
124+
* @return string
125+
*/
126+
private function generateDumpFileSignature(Column $column)
127+
{
128+
$dimensions = [
129+
$column->getTable()->getName(),
130+
$column->getElementType(),
131+
$column->getName()
132+
];
133+
134+
return implode("_", $dimensions);
135+
}
136+
137+
/**
138+
* @param Column | ElementInterface $column
139+
* @inheritdoc
140+
*/
141+
public function restore(ElementInterface $column)
142+
{
143+
$file = $this->generateDumpFileSignature($column);
144+
$generator = $this->dumpAccessor->read($file);
145+
146+
while ($generator->valid()) {
147+
$data = $generator->current();
148+
$this->applyDumpChunk(
149+
$column->getTable(),
150+
$data
151+
);
152+
$generator->next();
153+
}
154+
155+
$this->dumpAccessor->destruct($file);
156+
}
157+
158+
/**
159+
* @param ElementInterface $element
160+
* @return bool
161+
*/
162+
public function isAcceptable(ElementInterface $element)
163+
{
164+
return $element instanceof Column;
165+
}
166+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
namespace Magento\Setup\Model\Declaration\Schema\DataSavior;
8+
9+
use Magento\Setup\Model\Declaration\Schema\Dto\ElementInterface;
10+
11+
/**
12+
* This interface allows to dump data during declarative installation process
13+
* and revert changes with applying previously saved data, if something goes wrong
14+
*/
15+
interface DataSaviorInterface
16+
{
17+
/**
18+
* Generate dump file by element
19+
*
20+
* For example, it can generate file for removed column is_allowed, in this case
21+
* this file will consists of data in is_allowed column and additional data, that allows
22+
* to identify each `is_allowed` value
23+
*
24+
* @param ElementInterface $element
25+
* @return void
26+
*/
27+
public function dump(ElementInterface $element);
28+
29+
/**
30+
* Find the field, that was backed up by file name, and tries to restore data
31+
* that is in this file
32+
*
33+
* @param ElementInterface $element
34+
* @return mixed
35+
*/
36+
public function restore(ElementInterface $element);
37+
38+
/**
39+
* Check whether this element is acceptable by current implementation of data savior
40+
*
41+
* @param ElementInterface $element
42+
* @return bool
43+
*/
44+
public function isAcceptable(ElementInterface $element);
45+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
namespace Magento\Setup\Model\Declaration\Schema\DataSavior;
8+
9+
/**
10+
* Allows to access dump, that can be persisted in any file format or in database
11+
*/
12+
interface DumpAccessorInterface
13+
{
14+
/**
15+
* Allows to persist data to different sources: file, database, etc
16+
*
17+
* @param string $resource - can be for example absolute path to file
18+
* @param array $data - data format, in which data should be stored
19+
* @return void
20+
*/
21+
public function save($resource, array $data);
22+
23+
/**
24+
* Allows to read data by batches from different resources
25+
*
26+
* By resource means connection to database to absolute path to file, depends to implementation
27+
*
28+
* @param string $resource
29+
* @return \Generator
30+
*/
31+
public function read($resource);
32+
33+
/**
34+
* Destruct resource
35+
*
36+
* @param string $resource
37+
* @return void
38+
*/
39+
public function destruct($resource);
40+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
/**
3+
* Copyright © 2016 Magento. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
namespace Magento\Setup\Model\Declaration\Schema\DataSavior;
8+
9+
use Magento\Framework\App\ResourceConnection;
10+
use Magento\Framework\Db\Select;
11+
12+
/**
13+
* Yields data from database by select objects
14+
*/
15+
class SelectGenerator
16+
{
17+
/**
18+
* @var int
19+
*/
20+
private $batchSize = 12000;
21+
22+
/**
23+
* @var int
24+
*/
25+
private $baseBatchSize;
26+
27+
/**
28+
* @var ResourceConnection
29+
*/
30+
private $resourceConnection;
31+
32+
/**
33+
* TableDump constructor.
34+
* @param ResourceConnection $resourceConnection
35+
* @param int $baseBatchSize
36+
*/
37+
public function __construct(
38+
ResourceConnection $resourceConnection,
39+
$baseBatchSize = 15000
40+
) {
41+
$this->baseBatchSize = $baseBatchSize;
42+
$this->resourceConnection = $resourceConnection;
43+
}
44+
45+
/**
46+
* It retrieves data by batches
47+
*
48+
* Select generator do not know what data he will fetch, so you need to pass builded Select statement in it
49+
*
50+
* @param Select $select
51+
* @param string $connectionName
52+
* @return \Generator
53+
*/
54+
public function generator(Select $select, $connectionName)
55+
{
56+
$page = 0;
57+
$select->limit($this->batchSize, $page * $this->batchSize);
58+
$adapter = $this->resourceConnection->getConnection($connectionName);
59+
$data = $adapter->fetchAll($select);
60+
yield $data;
61+
62+
while (count($data)) {
63+
++$page;
64+
$select->limit($this->batchSize, $page * $this->batchSize + 1);
65+
$data = $adapter->fetchAll($select);
66+
yield $data;
67+
}
68+
}
69+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
namespace Magento\Setup\Model\Declaration\Schema\DataSavior;
8+
9+
use Magento\Framework\ObjectManagerInterface;
10+
11+
/**
12+
* Factory which allows to create SQL select generator
13+
*/
14+
class SelectGeneratorFactory
15+
{
16+
/**
17+
* @var string
18+
*/
19+
private $instanceName;
20+
21+
/**
22+
* @var ObjectManagerInterface
23+
*/
24+
private $objectManager;
25+
26+
/**
27+
* SelectGeneratorFactory constructor.
28+
* @param ObjectManagerInterface $objectManager
29+
* @param string $instanceName
30+
*/
31+
public function __construct(
32+
ObjectManagerInterface $objectManager,
33+
$instanceName = SelectGenerator::class
34+
) {
35+
$this->objectManager = $objectManager;
36+
$this->instanceName = $instanceName;
37+
}
38+
39+
/**
40+
* Create class instance with specified parameters
41+
*
42+
* @return SelectGenerator
43+
*/
44+
public function create()
45+
{
46+
return $this->objectManager->create($this->instanceName);
47+
}
48+
}

0 commit comments

Comments
 (0)