Issues (48)

src/Tasks/MigrateDataTaskBase.php (3 issues)

1
<?php
2
3
namespace Sunnysideup\MigrateData\Tasks;
4
5
use Exception;
6
use SilverStripe\Core\Config\Config;
7
use SilverStripe\Core\Environment;
8
use SilverStripe\Dev\BuildTask;
9
use SilverStripe\ORM\DataObject;
10
use SilverStripe\ORM\DB;
11
use Sunnysideup\Flush\FlushNow;
12
use Sunnysideup\MigrateData\Traits\HelperMethods;
13
14
abstract class MigrateDataTaskBase extends BuildTask
15
{
16
    use FlushNow;
17
    use HelperMethods;
0 ignored issues
show
The trait Sunnysideup\MigrateData\Traits\HelperMethods requires some properties which are not provided by Sunnysideup\MigrateData\Tasks\MigrateDataTaskBase: $ID, $ClassName
Loading history...
18
19
    protected $title = 'Abstract Migration Class';
20
21
    protected $description = 'Please extend this class';
22
23
    protected $enabled = false;
24
25
    protected $_schema;
26
27
    protected $_schemaForDataObject;
28
29
    private $_cacheTableExists = [];
30
31
    private $_cacheFieldExists = [];
32
33
    public function run($request)
34
    {
35
        $this->flushNowLine();
36
        $this->flushNow('THE START - look out for THE END ...');
37
        $this->flushNowLine();
38
        $this->flushNow(
39
            '
40
            <link href="/resources/vendor/silverstripe/framework/client/styles/debug.css" rel="stylesheet">
41
            <ul class="build">
42
            ',
43
            '',
44
            false
45
        );
46
        DataObject::Config()->set('validation_enabled', false);
47
        ini_set('memory_limit', '1024M');
48
        Environment::increaseMemoryLimitTo();
49
        Environment::increaseTimeLimitTo(7200);
50
51
        $this->performMigration();
52
53
        $this->flushNow('</ul>', '', false);
54
        $this->flushNow('');
55
        $this->flushNow('');
56
        $this->flushNow('');
57
        $this->flushNowLine();
58
        $this->flushNow('THE END');
59
        $this->flushNowLine();
60
        $this->flushNow('');
61
    }
62
63
    /**
64
     * Queries the config for Migrate definitions, and runs migrations
65
     * if you extend this task then overwrite it this method.
66
     */
67
    abstract protected function performMigration();
68
69
    /**
70
     * data needs to be in this format:
71
     *      [
72
     *          'include_inserts' => true|false, #assumed true if not provided
73
     *          'old_table' => 'foo',
74
     *          'new_table' => 'bar' (can be the same!).
75
     *
76
     *          'simple_move_fields' => ['A', 'B', 'C']
77
     *          OR
78
     *          'complex_move_fields' => ['A' => 'Anew', 'B' => 'BBew', 'C2' => 'Cnew']
79
     *      ]
80
     * list of data that is going to be moved
81
     */
82
    protected function runMoveData(array $data)
83
    {
84
        if ([] !== $data) {
85
            $this->flushNow('<h3>Migrating data - Core Migration</h3>');
86
            foreach ($data as $dataItem) {
87
                if (! isset($dataItem['include_inserts'])) {
88
                    $dataItem['include_inserts'] = true;
89
                }
90
                if (! isset($dataItem['leftJoin'])) {
91
                    $dataItem['leftJoin'] = [];
92
                }
93
                if (! isset($dataItem['where'])) {
94
                    $dataItem['where'] = '';
95
                }
96
                if (! isset($dataItem['include_inserts'])) {
97
                    $dataItem['include_inserts'] = true;
98
                }
99
                if (! isset($dataItem['new_table'])) {
100
                    $dataItem['new_table'] = $dataItem['old_table'];
101
                }
102
                $dataItem['old_fields'] = [];
103
                $dataItem['new_fields'] = [];
104
105
                if (isset($dataItem['simple_move_fields'])) {
106
                    $dataItem['old_fields'] = $dataItem['simple_move_fields'];
107
                    $dataItem['new_fields'] = $dataItem['simple_move_fields'];
108
                } elseif (isset($dataItem['complex_move_fields'])) {
109
                    $dataItem['old_fields'] = array_keys($dataItem['complex_move_fields']);
110
                    $dataItem['new_fields'] = array_values($dataItem['complex_move_fields']);
111
                } else {
112
                    $this->flushNow('Could not find simple_move_fields or complex_move_fields.');
113
                }
114
115
                if (count($dataItem['new_fields']) !== count($dataItem['old_fields'])) {
116
                    user_error('Count of new fields does not match old fields');
117
                    foreach ($dataItem['old_fields'] as $value) {
118
                        if ((int) $value === $value) {
119
                            $this->flushNow('Potential error in fields: ' . print_r($dataItem['old_fields'], 1), 'error');
0 ignored issues
show
Are you sure print_r($dataItem['old_fields'], 1) of type string|true can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

119
                            $this->flushNow('Potential error in fields: ' . /** @scrutinizer ignore-type */ print_r($dataItem['old_fields'], 1), 'error');
Loading history...
120
                        }
121
                    }
122
                    foreach ($dataItem['new_fields'] as $value) {
123
                        if ((int) $value === $value) {
124
                            $this->flushNow('Potential error in fields: ' . print_r($dataItem['new_fields'], 1), 'error');
0 ignored issues
show
Are you sure print_r($dataItem['new_fields'], 1) of type string|true can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

124
                            $this->flushNow('Potential error in fields: ' . /** @scrutinizer ignore-type */ print_r($dataItem['new_fields'], 1), 'error');
Loading history...
125
                        }
126
                    }
127
                }
128
                $this->flushNow('<h6>Migrating data ' . $dataItem['old_table'] . ' to ' . $dataItem['new_table'] . '</h6>');
129
                $this->migrateSimple(
130
                    $dataItem['include_inserts'],
131
                    $dataItem['old_table'],
132
                    $dataItem['new_table'],
133
                    $dataItem['old_fields'],
134
                    $dataItem['new_fields'],
135
                    $dataItem['leftJoin'],
136
                    $dataItem['where']
137
                );
138
            }
139
        }
140
    }
141
142
    /**
143
     * Migrates data from one table to another.
144
     *
145
     * @param bool   $includeInserts - the db table where we are moving fields from
146
     * @param string $tableOld       - the db table where we are moving fields from
147
     * @param string $tableNew       - the db table where we are moving fields to
148
     * @param array  $fieldNamesOld  - The current field names
149
     * @param array  $fieldNamesNew  - The new field names (this may be the same as $fieldNameOld)
150
     * @param array  $leftJoin       -
151
     * @param string $where          -
152
     */
153
    protected function migrateSimple(
154
        bool $includeInserts,
155
        string $tableOld,
156
        string $tableNew,
157
        array $fieldNamesOld,
158
        array $fieldNamesNew,
159
        array $leftJoin = [],
160
        string $where = ''
161
    ) {
162
        if (! $this->tableExists($tableOld)) {
163
            $this->flushNow("{$tableOld} (old table) does not exist", 'error');
164
        }
165
166
        if (! $this->tableExists($tableNew)) {
167
            $this->flushNow("{$tableNew} (new table) does not exist", 'error');
168
        }
169
170
        try {
171
            $this->flushNow('getting new table IDs.');
172
            $newEntryIDs = $this->getListOfIDs($tableNew);
173
174
            $this->flushNow('getting old IDs.');
175
            $oldEntries = $this->getListAsIterableQuery($tableOld, $leftJoin, $where);
176
            $oldEntryIDs = [];
177
178
            //add a new line using the ID as identifier
179
            foreach ($oldEntries as $oldEntry) {
180
                if ($includeInserts) {
181
                    if (! in_array($oldEntry['ID'], $newEntryIDs, true)) {
182
                        $this->flushNow('Added row ' . $oldEntry['ID'] . ' to ' . $tableNew . '.');
183
                        $this->runUpdateQuery('INSERT INTO "' . $tableNew . '" ("ID") VALUES (' . $oldEntry['ID'] . ');');
184
                    }
185
                }
186
187
                $oldEntryIDs[] = $oldEntry['ID'];
188
            }
189
190
            //update fields
191
            if (count($oldEntryIDs) > 0) {
192
                //work out what option is shorter in terms of ID count:
193
                $this->flushNow('working out update SQL.');
194
                $allIDs = $this->getListOfIDs($tableNew);
195
                $allIDCount = count($allIDs);
196
                $oldIDCount = count($oldEntryIDs);
197
                if ($oldIDCount > ($allIDCount - $oldIDCount)) {
198
                    $excludeIDs = array_diff($allIDs, $oldEntryIDs);
199
                    if (0 === count($excludeIDs)) {
200
                        $excludeIDs = [0];
201
                    }
202
                    $wherePhrase = ' NOT IN (' . implode(', ', $excludeIDs) . ')';
203
                } else {
204
                    if (0 === count($oldEntryIDs)) {
205
                        $oldEntryIDs = [0];
206
                    }
207
                    $wherePhrase = ' IN (' . implode(', ', $oldEntryIDs) . ')';
208
                }
209
210
                //update the new table with the old values
211
                //for the rows that join with the ID and match the list of OLD ids.
212
                if (count($fieldNamesNew) > 0) {
213
                    $updateQuery = 'UPDATE "' . $tableNew . '" AS "tablenew" ';
214
                    $updateQuery .= 'INNER JOIN "' . $tableOld . '" AS "tableold" ON "tablenew"."ID" = "tableold"."ID" ';
215
                    if ('_versions' === substr((string) $tableNew, -9)) {
216
                        $updateQuery .= ' AND "tablenew"."RecordID" = "tableold"."RecordID" ';
217
                        // also link to RecordID ...
218
                    }
219
                    $updateQuery .= 'SET ';
220
                    $fieldNamesOldCount = count($fieldNamesOld);
221
                    $fieldNamesNewCount = count($fieldNamesNew);
222
                    for ($i = 0; $i < $fieldNamesNewCount && $i < $fieldNamesOldCount; ++$i) {
223
                        if ($i > 0) {
224
                            $updateQuery .= ', ';
225
                        }
226
                        $updateQuery .= '"tablenew"."' . $fieldNamesNew[$i] . '" = "tableold"."' . $fieldNamesOld[$i] . '" ';
227
                    }
228
                    $updateQuery .= 'WHERE "tablenew"."ID" ' . $wherePhrase . ';';
229
                    $this->flushNow(str_replace($wherePhrase, '........', $updateQuery));
230
                    $this->runUpdateQuery($updateQuery);
231
                }
232
            }
233
        } catch (Exception $exception) {
234
            $this->flushNow("Unable to migrate {$tableOld} to {$tableNew}.", 'error');
235
            $this->flushNow($exception->getMessage(), 'error');
236
        }
237
    }
238
}
239