Issues (45)

src/IO/Database/SQL.php (2 issues)

1
<?php
2
/**
3
 * This file is part of the Divergence package.
4
 *
5
 * (c) Henry Paradiz <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
namespace Divergence\IO\Database;
12
13
use Exception;
14
15
/**
16
 * SQL.
17
 * @package Divergence
18
 * @author  Henry Paradiz <[email protected]>
19
 * @author  Chris Alfano <[email protected]>
20
 *
21
 */
22
class SQL
23
{
24
    protected static $aggregateFieldConfigs;
25
26
    /**
27
     * This is how MySQL escapes it's string under the hood.
28
     * Keep it. We don't need a database connection to escape strings.
29
     *
30
     * @param string $str String to escape.
31
     * @return string Escaped string.
32
     */
33 5
    public static function escape($str)
34
    {
35 5
        return str_replace(
36 5
            ["\\",  "\x00", "\n",  "\r",  "'",  '"', "\x1a"],
37 5
            ["\\\\","\\0","\\n", "\\r", "\'", '\"', "\\Z"],
38 5
            $str
39 5
        );
40
    }
41
42 4
    public static function compileFields($recordClass, $historyVariant = false)
43
    {
44 4
        $queryString = [];
45 4
        $fields = static::getAggregateFieldOptions($recordClass);
46
47 4
        foreach ($fields as $fieldId => $field) {
48 4
            if ($field['columnName'] == 'RevisionID') {
49 3
                continue;
50
            }
51
52 4
            $queryString[] = static::getFieldDefinition($recordClass, $fieldId, $historyVariant);
53
54 4
            if (!empty($field['primary'])) {
55 4
                if ($historyVariant) {
56 2
                    $queryString[] = 'KEY `'.$field['columnName'].'` (`'.$field['columnName'].'`)';
57
                } else {
58 3
                    $queryString[] = 'PRIMARY KEY (`'.$field['columnName'].'`)';
59
                }
60
            }
61
62 4
            if (!empty($field['unique']) && !$historyVariant) {
63
                $queryString[] = 'UNIQUE KEY `'.$field['columnName'].'` (`'.$field['columnName'].'`)';
64
            }
65
66 4
            if (!empty($field['index']) && !$historyVariant) {
67
                $queryString[] = 'KEY `'.$field['columnName'].'` (`'.$field['columnName'].'`)';
68
            }
69
        }
70
71 4
        return $queryString;
72
    }
73
74 3
    public static function getFullTextColumns($recordClass)
75
    {
76 3
        $fulltextColumns = [];
77 3
        $fields = static::getAggregateFieldOptions($recordClass);
78
79 3
        foreach ($fields as $fieldId => $field) {
80 3
            if ($field['columnName'] == 'RevisionID') {
81 2
                continue;
82
            }
83
84 3
            if (!empty($field['fulltext'])) {
85
                $fulltextColumns[] = $field['columnName'];
86
            }
87
        }
88
89 3
        return $fulltextColumns;
90
    }
91
92 3
    public static function getContextIndex($recordClass)
93
    {
94 3
        return 'KEY `CONTEXT` (`'.$recordClass::getColumnName('ContextClass').'`,`'.$recordClass::getColumnName('ContextID').'`)';
95
    }
96
97
    /**
98
     * Generates a MySQL create table query from a Divergence\Models\ActiveRecord class.
99
     *
100
     * @param string $recordClass Class name
101
     * @param boolean $historyVariant
102
     * @return string
103
     */
104 4
    public static function getCreateTable($recordClass, $historyVariant = false)
105
    {
106 4
        $indexes = $historyVariant ? [] : $recordClass::$indexes;
0 ignored issues
show
The property indexes does not exist on string.
Loading history...
107 4
        $fulltextColumns = [];
108 4
        $queryString = [];
109
110
111
        // history table revisionID field
112 4
        if ($historyVariant) {
113 2
            $queryString[] = '`RevisionID` int(10) unsigned NOT NULL auto_increment';
114 2
            $queryString[] = 'PRIMARY KEY (`RevisionID`)';
115
        }
116
117 4
        $queryString = array_merge($queryString, static::compileFields($recordClass, $historyVariant));
118
119 4
        if (!$historyVariant) {
120
            // If ContextClass && ContextID are members of this model let's index them
121 3
            if ($recordClass::fieldExists('ContextClass') && $recordClass::fieldExists('ContextID')) {
122 3
                $queryString[] = static::getContextIndex($recordClass);
123
            }
124
125 3
            $fulltextColumns = static::getFullTextColumns($recordClass);
126
        }
127
128
        // compile indexes
129 4
        foreach ($indexes as $indexName => $index) {
130
131
            // translate field names
132 2
            foreach ($index['fields'] as &$indexField) {
133 2
                $indexField = $recordClass::getColumnName($indexField);
134
            }
135
136 2
            if (!empty($index['fulltext'])) {
137
                $fulltextColumns = array_unique(array_merge($fulltextColumns, $index['fields']));
138
                continue;
139
            }
140
141 2
            $queryString[] = sprintf(
142 2
                '%s KEY `%s` (`%s`)',
143 2
                !empty($index['unique']) ? 'UNIQUE' : '',
144 2
                $indexName,
145 2
                join('`,`', $index['fields'])
146 2
            );
147
        }
148
149 4
        if (!empty($fulltextColumns)) {
150
            $queryString[] = 'FULLTEXT KEY `FULLTEXT` (`'.join('`,`', $fulltextColumns).'`)';
151
        }
152
153
154 4
        $createSQL = sprintf(
155 4
            "CREATE TABLE IF NOT EXISTS `%s` (\n\t%s\n) ENGINE=MyISAM DEFAULT CHARSET=utf8;",
156 4
            $historyVariant ? $recordClass::getHistoryTable() : $recordClass::$tableName,
0 ignored issues
show
The property tableName does not exist on string.
Loading history...
157 4
            join("\n\t,", $queryString)
158 4
        );
159
160
        // append history table SQL
161 4
        if (!$historyVariant && is_subclass_of($recordClass, 'VersionedRecord')) {
162
            $createSQL .= PHP_EOL.PHP_EOL.PHP_EOL.static::getCreateTable($recordClass, true);
163
        }
164 4
        return $createSQL;
165
    }
166
167 4
    public static function getSQLType($field)
168
    {
169 4
        switch ($field['type']) {
170 4
            case 'boolean':
171 3
                return 'boolean';
172 4
            case 'tinyint':
173 4
            case 'smallint':
174 4
            case 'mediumint':
175 4
            case 'bigint':
176
                return $field['type'].($field['unsigned'] ? ' unsigned' : '').($field['zerofill'] ? ' zerofill' : '');
177 4
            case 'uint':
178 3
                $field['unsigned'] = true;
179
                // no break
180 4
            case 'int':
181 4
            case 'integer':
182 4
                return 'int'.($field['unsigned'] ? ' unsigned' : '').(!empty($field['zerofill']) ? ' zerofill' : '');
183 4
            case 'decimal':
184 4
                return sprintf('decimal(%s,%s)', $field['precision'], $field['scale']);
185 4
            case 'float':
186 3
                return 'float';
187 4
            case 'double':
188
                return 'double';
189
190 4
            case 'password':
191 4
            case 'string':
192 4
            case 'varchar':
193 4
            case 'list':
194 4
                return sprintf(!$field['length'] || $field['type'] == 'varchar' ? 'varchar(%u)' : 'char(%u)', $field['length'] ? $field['length'] : 255);
195 4
            case 'clob':
196 4
            case 'serialized':
197 4
            case 'json':
198 3
                return 'text';
199 4
            case 'blob':
200
                return 'blob';
201 4
            case 'binary':
202
                return sprintf('binary(%s)', isset($field['length']) ? $field['length'] : 1);
203
204 4
            case 'timestamp':
205 4
                return 'timestamp';
206 4
            case 'datetime':
207
                return 'datetime';
208 4
            case 'time':
209
                return 'time';
210 4
            case 'date':
211 3
                return 'date';
212 4
            case 'year':
213
                return 'year';
214
215 4
            case 'enum':
216 4
                return sprintf('enum("%s")', join('","', array_map([static::class,'escape'], $field['values'])));
217
218 3
            case 'set':
219 3
                return sprintf('set("%s")', join('","', array_map([static::class,'escape'], $field['values'])));
220
221
            default:
222
                throw new Exception("getSQLType: unhandled type $field[type]");
223
        }
224
    }
225
226 4
    public static function getFieldDefinition($recordClass, $fieldName, $historyVariant = false)
227
    {
228 4
        $field = static::getAggregateFieldOptions($recordClass, $fieldName);
229 4
        $rootClass = $recordClass::$rootClass;
230
231
        // force notnull=false on non-rootclass fields
232 4
        if ($rootClass && !$rootClass::fieldExists($fieldName)) {
233
            $field['notnull'] = false;
234
        }
235
236
        // auto-prepend class type
237 4
        if ($field['columnName'] == 'Class' && $field['type'] == 'enum' && !in_array($rootClass, $field['values']) && !count($rootClass::getStaticSubClasses())) {
238
            array_unshift($field['values'], $rootClass);
239
        }
240
241 4
        $fieldDef = '`'.$field['columnName'].'`';
242 4
        $fieldDef .= ' '.static::getSQLType($field);
243
244 4
        if (!empty($field['charset'])) {
245
            $fieldDef .= " CHARACTER SET $field[charset]";
246
        }
247
248 4
        if (!empty($field['collate'])) {
249
            $fieldDef .= " COLLATE $field[collate]";
250
        }
251
252 4
        $fieldDef .= ' '.($field['notnull'] ? 'NOT NULL' : 'NULL');
253
254 4
        if ($field['autoincrement'] && !$historyVariant) {
255 3
            $fieldDef .= ' auto_increment';
256 4
        } elseif (($field['type'] == 'timestamp') && ($field['default'] == 'CURRENT_TIMESTAMP')) {
257 4
            $fieldDef .= ' default CURRENT_TIMESTAMP';
258 4
        } elseif (empty($field['notnull']) && ($field['default'] == null)) {
259 4
            $fieldDef .= ' default NULL';
260 4
        } elseif (isset($field['default'])) {
261 3
            if ($field['type'] == 'boolean') {
262 3
                $fieldDef .= ' default '.($field['default'] ? 1 : 0);
263
            } else {
264 3
                $fieldDef .= ' default "'.static::escape($field['default']).'"';
265
            }
266
        }
267
268 4
        return $fieldDef;
269
    }
270
271 4
    protected static function getAggregateFieldOptions($recordClass, $field = null)
272
    {
273 4
        if (!isset(static::$aggregateFieldConfigs[$recordClass])) {
274 2
            static::$aggregateFieldConfigs[$recordClass] = $recordClass::getClassFields();
275
        }
276
277 4
        if ($field) {
278 4
            return static::$aggregateFieldConfigs[$recordClass][$field];
279
        } else {
280 4
            return static::$aggregateFieldConfigs[$recordClass];
281
        }
282
    }
283
}
284