pluf2

pluf2 Commit Details


Date:2012-05-15 13:23:39 (12 years 7 months ago)
Author:Thomas Keller
Branch:master
Commit:a45dc195a7f1cc6943734434bc13cdd466c96800
Parents: 4121ca463fa4f6f8da878aba9a9288b52743087d
Message:When new tables are created (e.g. on initial installations or during upgrades), Pluf used to create foreign key relations as part of the table definitions. This however made problems when two tables, A and B, cross-referenced themselves via a foreign key relation, because strict DBMS like PostgreSQL or MySQL couldn't create such a relation without that both target tables existed prior. This patch changes this behaviour by separating table creation and foreign key setup into two phases, that have to be called after another, namely Pluf_DB_Schema::createTables() and Pluf_DB_Schema::createConstraints(). For "reverse" migrations the same logic is used, at first Pluf_DB_Schema::dropConstraints() has to be called, then Pluf_DB_Schema::dropTables() can be executed.

While being at it, I fixed the following things:
- Association tables (*_assoc) where missing foreign key relations to the base
tables for PostgreSQL; this has been fixed
- PostgreSQL's (and MySQL's as well) limit for name identifiers are 64 characters;
the schema code now ensures that we do not hit this boundary for foreign keys
- MySQL uses InnoDB tables now by default and also sets up foreign key relations
- no changes to SQLite, beside that constraints are still completly ignored, due
to the unability of SQLite to change table definitions after a table has been
created

All these changes are likely to break backwards compatibility, so if you are already
using Pluf for example to manage your database, you might be surprised that some
of your (old) tables are relation-less, while others (new ones) come with relations.
In cases like this it might be wise to backup your database (migrate.php -b) and
restore it from the dump, because this re-creates the table structure including
all the foreign key relations. Note however that the data dump might have become
inconsistent, so the restore might not go through without that your DBMS yells
at you...
Changes:

File differences

src/Pluf/DB/Schema.php
8282
8383
8484
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
85101
86102
87103
......
98114
99115
100116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
101135
102136
103137
}
/**
* Creates the constraints for the current model.
* This should be done _after_ all tables of all models have been created.
*
* @throws Exception
*/
function createConstraints()
{
$sql = $this->schema->getSqlCreateConstraints($this->model);
foreach ($sql as $k => $query) {
if (false === $this->con->execute($query)) {
throw new Exception($this->con->getError());
}
}
}
/**
* Drop the tables and indexes for the current model.
*
* @return mixed True if success or database error.
}
/**
* Drops the constraints for the current model.
* This should be done _before_ all tables of all models are dropped.
*
* @throws Exception
* @return boolean
*/
function dropConstraints()
{
$sql = $this->schema->getSqlDeleteConstraints($this->model);
foreach ($sql as $k => $query) {
if (false === $this->con->execute($query)) {
throw new Exception($this->con->getError());
}
}
return true;
}
/**
* Given a column name or a string with column names in the format
* "column1, column2, column3", returns the escaped correctly
* quoted column names. This is good for index creation.
src/Pluf/DB/Schema/MySQL.php
7676
7777
7878
79
80
8179
8280
8381
......
127125
128126
129127
130
131
132
128
129
133130
134
135
131
132
136133
137134
138135
......
141138
142139
143140
144
145
141
142
146143
147144
148145
......
162159
163160
164161
165
162
166163
167
164
168165
169166
170167
171168
172169
173
170
174171
175172
176173
177174
178
175
179176
180
181
177
178
182179
183180
184181
......
187184
188185
189186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
190251
191252
192253
......
204265
205266
206267
207
268
208269
209270
210271
......
214275
215276
216277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
217293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
218318
219319
$this->con = $con;
}
/**
* Get the SQL to generate the tables of the given model.
*
$manytomany[] = $col;
}
}
$sql .= "\n".'primary key (`id`)';
$sql .= "\n".') ENGINE=MyISAM';
$sql .=' DEFAULT CHARSET=utf8;';
$sql .= "\n".'PRIMARY KEY (`id`))';
$sql .= 'ENGINE=InnoDB DEFAULT CHARSET=utf8;';
$tables[$this->con->pfx.$model->_a['table']] = $sql;
//Now for the many to many
// Now for the many to many
foreach ($manytomany as $many) {
$omodel = new $cols[$many]['model']();
$hay = array(strtolower($model->_a['model']), strtolower($omodel->_a['model']));
$sql = 'CREATE TABLE `'.$this->con->pfx.$table.'` (';
$sql .= "\n".'`'.strtolower($model->_a['model']).'_id` '.$this->mappings['foreignkey'].' default 0,';
$sql .= "\n".'`'.strtolower($omodel->_a['model']).'_id` '.$this->mappings['foreignkey'].' default 0,';
$sql .= "\n".'primary key (`'.strtolower($model->_a['model']).'_id`, `'.strtolower($omodel->_a['model']).'_id`)';
$sql .= "\n".') ENGINE=MyISAM';
$sql .= "\n".'PRIMARY KEY ('.strtolower($model->_a['model']).'_id, '.strtolower($omodel->_a['model']).'_id)';
$sql .= "\n".') ENGINE=InnoDB';
$sql .=' DEFAULT CHARSET=utf8;';
$tables[$this->con->pfx.$table] = $sql;
}
if (!isset($val['col'])) {
$val['col'] = $idx;
}
$index[$this->con->pfx.$model->_a['table'].'_'.$idx] =
$index[$this->con->pfx.$model->_a['table'].'_'.$idx] =
sprintf('CREATE INDEX `%s` ON `%s` (%s);',
$idx, $this->con->pfx.$model->_a['table'],
$idx, $this->con->pfx.$model->_a['table'],
Pluf_DB_Schema::quoteColumn($val['col'], $this->con));
}
foreach ($model->_a['cols'] as $col => $val) {
$field = new $val['type']();
if ($field->type == 'foreignkey') {
$index[$this->con->pfx.$model->_a['table'].'_'.$col.'_foreignkey'] =
$index[$this->con->pfx.$model->_a['table'].'_'.$col.'_foreignkey'] =
sprintf('CREATE INDEX `%s` ON `%s` (`%s`);',
$col.'_foreignkey_idx', $this->con->pfx.$model->_a['table'], $col);
}
if (isset($val['unique']) and $val['unique'] == true) {
$index[$this->con->pfx.$model->_a['table'].'_'.$col.'_unique'] =
$index[$this->con->pfx.$model->_a['table'].'_'.$col.'_unique'] =
sprintf('CREATE UNIQUE INDEX `%s` ON `%s` (%s);',
$col.'_unique_idx',
$this->con->pfx.$model->_a['table'],
$col.'_unique_idx',
$this->con->pfx.$model->_a['table'],
Pluf_DB_Schema::quoteColumn($col, $this->con)
);
}
}
/**
* Workaround for <http://bugs.mysql.com/bug.php?id=13942> which limits the
* length of foreign key identifiers to 64 characters.
*
* @param string
* @return string
*/
function getShortenedFKeyName($name)
{
if (strlen($name) <= 64) {
return $name;
}
return substr($name, 0, 55).'_'.substr(md5($name), 0, 8);
}
/**
* Get the SQL to create the constraints for the given model
*
* @param Object Model
* @return array Array of SQL strings ready to execute.
*/
function getSqlCreateConstraints($model)
{
$table = $this->con->pfx.$model->_a['table'];
$constraints = array();
$alter_tbl = 'ALTER TABLE '.$table;
$cols = $model->_a['cols'];
$manytomany = array();
foreach ($cols as $col => $val) {
$field = new $val['type']();
// remember these for later
if ($field->type == 'manytomany') {
$manytomany[] = $col;
}
if ($field->type == 'foreignkey') {
// Add the foreignkey constraints
$referto = new $val['model']();
$constraints[] = $alter_tbl.' ADD CONSTRAINT '.$this->getShortenedFKeyName($table.'_'.$col.'_fkey').'
FOREIGN KEY ('.$this->con->qn($col).')
REFERENCES '.$this->con->pfx.$referto->_a['table'].' (id)
ON DELETE NO ACTION ON UPDATE NO ACTION';
}
}
// Now for the many to many
foreach ($manytomany as $many) {
$omodel = new $cols[$many]['model']();
$hay = array(strtolower($model->_a['model']), strtolower($omodel->_a['model']));
sort($hay);
$table = $this->con->pfx.$hay[0].'_'.$hay[1].'_assoc';
$alter_tbl = 'ALTER TABLE '.$table;
$constraints[] = $alter_tbl.' ADD CONSTRAINT '.$this->getShortenedFKeyName($table.'_fkey1').'
FOREIGN KEY ('.strtolower($model->_a['model']).'_id)
REFERENCES '.$this->con->pfx.$model->_a['table'].' (id)
ON DELETE NO ACTION ON UPDATE NO ACTION';
$constraints[] = $alter_tbl.' ADD CONSTRAINT '.$this->getShortenedFKeyName($table.'_fkey2').'
FOREIGN KEY ('.strtolower($omodel->_a['model']).'_id)
REFERENCES '.$this->con->pfx.$omodel->_a['table'].' (id)
ON DELETE NO ACTION ON UPDATE NO ACTION';
}
return $constraints;
}
/**
* Get the SQL to drop the tables corresponding to the model.
*
* @param Object Model
$manytomany[] = $col;
}
}
//Now for the many to many
foreach ($manytomany as $many) {
$omodel = new $cols[$many]['model']();
$sql .= ', `'.$this->con->pfx.$table.'`';
}
return array($sql);
}
/**
* Get the SQL to drop the constraints for the given model
*
* @param Object Model
* @return array Array of SQL strings ready to execute.
*/
function getSqlDeleteConstraints($model)
{
$table = $this->con->pfx.$model->_a['table'];
$constraints = array();
$alter_tbl = 'ALTER TABLE '.$table;
$cols = $model->_a['cols'];
$manytomany = array();
foreach ($cols as $col => $val) {
$field = new $val['type']();
// remember these for later
if ($field->type == 'manytomany') {
$manytomany[] = $col;
}
if ($field->type == 'foreignkey') {
// Add the foreignkey constraints
$referto = new $val['model']();
$constraints[] = $alter_tbl.' DROP CONSTRAINT '.$this->getShortenedFKeyName($table.'_'.$col.'_fkey');
}
}
// Now for the many to many
foreach ($manytomany as $many) {
$omodel = new $cols[$many]['model']();
$hay = array(strtolower($model->_a['model']), strtolower($omodel->_a['model']));
sort($hay);
$table = $this->con->pfx.$hay[0].'_'.$hay[1].'_assoc';
$alter_tbl = 'ALTER TABLE '.$table;
$constraints[] = $alter_tbl.' DROP CONSTRAINT '.$this->getShortenedFKeyName($table.'_fkey1');
$constraints[] = $alter_tbl.' DROP CONSTRAINT '.$this->getShortenedFKeyName($table.'_fkey2');
}
return $constraints;
}
}
src/Pluf/DB/Schema/PostgreSQL.php
9393
9494
9595
96
9796
9897
9998
......
112111
113112
114113
115
116
117
118
119
120
121
122
123114
124115
125
126116
127117
128
129
118
119
130120
131121
132122
......
135125
136126
137127
138
128
139129
140130
141131
......
161151
162152
163153
164
154
165155
166
167
156
157
168158
169159
170160
171161
172162
173163
174
164
175165
176
177
166
167
178168
179169
180170
......
183173
184174
185175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
186239
187240
188241
......
200253
201254
202255
203
256
204257
205258
206259
......
211264
212265
213266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
214307
215308
$manytomany = array();
$query = 'CREATE TABLE '.$this->con->pfx.$model->_a['table'].' (';
$sql_col = array();
$constraints = array();
foreach ($cols as $col => $val) {
$field = new $val['type']();
if ($field->type != 'manytomany') {
} else {
$manytomany[] = $col;
}
if ($field->type == 'foreignkey') {
// Add the foreignkey constraints
$referto = new $val['model']();
$_c = 'CONSTRAINT '.$this->con->pfx.$model->_a['table'].'_'.$col.'_fkey FOREIGN KEY ('.$this->con->qn($col).')
REFERENCES '.$this->con->pfx.$referto->_a['table'].' (id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION';
$constraints[] = $_c;
}
}
$sql_col[] = 'CONSTRAINT '.$this->con->pfx.$model->_a['table'].'_pkey PRIMARY KEY (id)';
$sql_col = array_merge($sql_col, $constraints);
$query = $query."\n".implode(",\n", $sql_col)."\n".');';
$tables[$this->con->pfx.$model->_a['table']] = $query;
//Now for the many to many
//FIXME add index on the second column
// Now for the many to many
// FIXME add index on the second column
foreach ($manytomany as $many) {
$omodel = new $cols[$many]['model']();
$hay = array(strtolower($model->_a['model']), strtolower($omodel->_a['model']));
$sql = 'CREATE TABLE '.$this->con->pfx.$table.' (';
$sql .= "\n".strtolower($model->_a['model']).'_id '.$this->mappings['foreignkey'].' default 0,';
$sql .= "\n".strtolower($omodel->_a['model']).'_id '.$this->mappings['foreignkey'].' default 0,';
$sql .= "\n".'CONSTRAINT '.$this->con->pfx.$table.'_pkey PRIMARY KEY ('.strtolower($model->_a['model']).'_id, '.strtolower($omodel->_a['model']).'_id)';
$sql .= "\n".'CONSTRAINT '.$this->getShortenedIdentifierName($this->con->pfx.$table.'_pkey').' PRIMARY KEY ('.strtolower($model->_a['model']).'_id, '.strtolower($omodel->_a['model']).'_id)';
$sql .= "\n".');';
$tables[$this->con->pfx.$table] = $sql;
}
$unique = '';
}
$index[$this->con->pfx.$model->_a['table'].'_'.$idx] =
$index[$this->con->pfx.$model->_a['table'].'_'.$idx] =
sprintf('CREATE '.$unique.'INDEX %s ON %s (%s);',
$this->con->pfx.$model->_a['table'].'_'.$idx,
$this->con->pfx.$model->_a['table'],
$this->con->pfx.$model->_a['table'].'_'.$idx,
$this->con->pfx.$model->_a['table'],
Pluf_DB_Schema::quoteColumn($val['col'], $this->con)
);
}
foreach ($model->_a['cols'] as $col => $val) {
$field = new $val['type']();
if (isset($val['unique']) and $val['unique'] == true) {
$index[$this->con->pfx.$model->_a['table'].'_'.$col.'_unique'] =
$index[$this->con->pfx.$model->_a['table'].'_'.$col.'_unique'] =
sprintf('CREATE UNIQUE INDEX %s ON %s (%s);',
$this->con->pfx.$model->_a['table'].'_'.$col.'_unique_idx',
$this->con->pfx.$model->_a['table'],
$this->con->pfx.$model->_a['table'].'_'.$col.'_unique_idx',
$this->con->pfx.$model->_a['table'],
Pluf_DB_Schema::quoteColumn($col, $this->con)
);
}
}
/**
* All identifiers in Postgres must not exceed 64 characters in length.
*
* @param string
* @return string
*/
function getShortenedIdentifierName($name)
{
if (strlen($name) <= 64) {
return $name;
}
return substr($name, 0, 55).'_'.substr(md5($name), 0, 8);
}
/**
* Get the SQL to create the constraints for the given model
*
* @param Object Model
* @return array Array of SQL strings ready to execute.
*/
function getSqlCreateConstraints($model)
{
$table = $this->con->pfx.$model->_a['table'];
$constraints = array();
$alter_tbl = 'ALTER TABLE '.$table;
$cols = $model->_a['cols'];
$manytomany = array();
foreach ($cols as $col => $val) {
$field = new $val['type']();
// remember these for later
if ($field->type == 'manytomany') {
$manytomany[] = $col;
}
if ($field->type == 'foreignkey') {
// Add the foreignkey constraints
$referto = new $val['model']();
$constraints[] = $alter_tbl.' ADD CONSTRAINT '.$this->getShortenedIdentifierName($table.'_'.$col.'_fkey').'
FOREIGN KEY ('.$this->con->qn($col).')
REFERENCES '.$this->con->pfx.$referto->_a['table'].' (id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION';
}
}
// Now for the many to many
foreach ($manytomany as $many) {
$omodel = new $cols[$many]['model']();
$hay = array(strtolower($model->_a['model']), strtolower($omodel->_a['model']));
sort($hay);
$table = $this->con->pfx.$hay[0].'_'.$hay[1].'_assoc';
$alter_tbl = 'ALTER TABLE '.$table;
$constraints[] = $alter_tbl.' ADD CONSTRAINT '.$this->getShortenedIdentifierName($table.'_fkey1').'
FOREIGN KEY ('.strtolower($model->_a['model']).'_id)
REFERENCES '.$this->con->pfx.$model->_a['table'].' (id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION';
$constraints[] = $alter_tbl.' ADD CONSTRAINT '.$this->getShortenedIdentifierName($table.'_fkey2').'
FOREIGN KEY ('.strtolower($omodel->_a['model']).'_id)
REFERENCES '.$this->con->pfx.$omodel->_a['table'].' (id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION';
}
return $constraints;
}
/**
* Get the SQL to drop the tables corresponding to the model.
*
* @param Object Model
$manytomany[] = $col;
}
}
//Now for the many to many
foreach ($manytomany as $many) {
$omodel = new $cols[$many]['model']();
}
return $sql;
}
/**
* Get the SQL to drop the constraints for the given model
*
* @param Object Model
* @return array Array of SQL strings ready to execute.
*/
function getSqlDeleteConstraints($model)
{
$table = $this->con->pfx.$model->_a['table'];
$constraints = array();
$alter_tbl = 'ALTER TABLE '.$table;
$cols = $model->_a['cols'];
$manytomany = array();
foreach ($cols as $col => $val) {
$field = new $val['type']();
// remember these for later
if ($field->type == 'manytomany') {
$manytomany[] = $col;
}
if ($field->type == 'foreignkey') {
// Add the foreignkey constraints
$referto = new $val['model']();
$constraints[] = $alter_tbl.' DROP CONSTRAINT '.$this->getShortenedIdentifierName($table.'_'.$col.'_fkey');
}
}
// Now for the many to many
foreach ($manytomany as $many) {
$omodel = new $cols[$many]['model']();
$hay = array(strtolower($model->_a['model']), strtolower($omodel->_a['model']));
sort($hay);
$table = $this->con->pfx.$hay[0].'_'.$hay[1].'_assoc';
$alter_tbl = 'ALTER TABLE '.$table;
$constraints[] = $alter_tbl.' DROP CONSTRAINT '.$this->getShortenedIdentifierName($table.'_fkey1');
$constraints[] = $alter_tbl.' DROP CONSTRAINT '.$this->getShortenedIdentifierName($table.'_fkey2');
}
return $constraints;
}
}
src/Pluf/DB/Schema/SQLite.php
129129
130130
131131
132
132
133133
134134
135135
......
147147
148148
149149
150
151
152
153
154
155
156
157
158
159
160
161
150162
151163
152164
......
160172
161173
162174
163
175
164176
165177
166
167
178
179
168180
169181
170182
171183
172184
173185
174
186
175187
176
177
188
189
178190
179191
180192
181
193
182194
183
184
195
196
185197
186198
187199
......
207219
208220
209221
210
222
211223
212224
213225
......
217229
218230
219231
232
220233
234
235
236
237
238
239
240
241
242
243
221244
222245
223246
}
$query = $query."\n".implode(",\n", $sql_col)."\n".');';
$tables[$this->con->pfx.$model->_a['table']] = $query;
//Now for the many to many
foreach ($manytomany as $many) {
$omodel = new $cols[$many]['model']();
}
/**
* SQLite cannot add foreign key constraints to already existing tables,
* so we skip their creation completely.
*
* @param Object Model
* @return array
*/
function getSqlCreateConstraints($model)
{
return array();
}
/**
* Get the SQL to generate the indexes of the given model.
*
* @param Object Model
$val['col'] = $idx;
}
$unique = (isset($val['type']) && ($val['type'] == 'unique')) ? 'UNIQUE ' : '';
$index[$this->con->pfx.$model->_a['table'].'_'.$idx] =
$index[$this->con->pfx.$model->_a['table'].'_'.$idx] =
sprintf('CREATE %sINDEX %s ON %s (%s);',
$unique,
$this->con->pfx.$model->_a['table'].'_'.$idx,
$this->con->pfx.$model->_a['table'],
$this->con->pfx.$model->_a['table'].'_'.$idx,
$this->con->pfx.$model->_a['table'],
Pluf_DB_Schema::quoteColumn($val['col'], $this->con)
);
}
foreach ($model->_a['cols'] as $col => $val) {
$field = new $val['type']();
if ($field->type == 'foreignkey') {
$index[$this->con->pfx.$model->_a['table'].'_'.$col.'_foreignkey'] =
$index[$this->con->pfx.$model->_a['table'].'_'.$col.'_foreignkey'] =
sprintf('CREATE INDEX %s ON %s (%s);',
$this->con->pfx.$model->_a['table'].'_'.$col.'_foreignkey_idx',
$this->con->pfx.$model->_a['table'],
$this->con->pfx.$model->_a['table'].'_'.$col.'_foreignkey_idx',
$this->con->pfx.$model->_a['table'],
Pluf_DB_Schema::quoteColumn($col, $this->con));
}
if (isset($val['unique']) and $val['unique'] == true) {
$index[$this->con->pfx.$model->_a['table'].'_'.$col.'_unique'] =
$index[$this->con->pfx.$model->_a['table'].'_'.$col.'_unique'] =
sprintf('CREATE UNIQUE INDEX %s ON %s (%s);',
$this->con->pfx.$model->_a['table'].'_'.$col.'_unique_idx',
$this->con->pfx.$model->_a['table'],
$this->con->pfx.$model->_a['table'].'_'.$col.'_unique_idx',
$this->con->pfx.$model->_a['table'],
Pluf_DB_Schema::quoteColumn($col, $this->con)
);
}
$manytomany[] = $col;
}
}
//Now for the many to many
foreach ($manytomany as $many) {
$omodel = new $cols[$many]['model']();
$sql[] = 'DROP TABLE IF EXISTS '.$this->con->pfx.$table;
}
return $sql;
}
/**
* SQLite cannot drop foreign keys from existing tables,
* so we skip their deletion completely.
*
* @param Object Model
* @return array
*/
function getSqlDeleteConstraints($model)
{
return array();
}
}

Archive Download the corresponding diff file

Branches

Number of commits:
Page rendered in 0.07469s using 13 queries.