271
|
1 package IMPL::SQL::Schema::MySQL::Formatter;
|
|
2 use strict;
|
|
3
|
|
4 use IMPL::lang qw(is);
|
|
5 use IMPL::require {
|
|
6 Exception => 'IMPL::Exception',
|
|
7 OpException => '-IMPL::InvalidOperationException',
|
|
8 ArgException => '-IMPL::InvalidArgumentException',
|
|
9 PrimaryKey => '-IMPL::SQL::Schema::Constraint::PrimaryKey',
|
|
10 UniqueIndex => '-IMPL::SQL::Schema::Constraint::Unique',
|
|
11 Index => '-IMPL::SQL::Schema::Constraint::Index',
|
|
12 ForeignKey => '-IMPL::SQL::Schema::Constraint::ForeignKey',
|
|
13 CharType => '-IMPL::SQL::Schema::MySQL::CharType',
|
|
14 EnumType => '-IMPL::SQL::Schema::MySQL::EnumType',
|
|
15 TraitsDropTable => '-IMPL::SQL::Schema::Traits::DropTable',
|
|
16 TraitsCreateTable => '-IMPL::SQL::Schema::Traits::CreateTable',
|
|
17 TraitsAlterTableDropConstraint => '-IMPL::SQL::Schema::Traits::AlterTableDropConstraint',
|
|
18 TraitsAlterTableAddConstraint => '-IMPL::SQL::Schema::Traits::AlterTableAddConstraint',
|
|
19 TraitsAlterTableDropColumn => '-IMPL::SQL::Schema::Traits::AlterTableDropColumn',
|
|
20 TraitsAlterTableAddColumn => '-IMPL::SQL::Schema::Traits::AlterTableAddColumn',
|
|
21 TraitsAlterTableChangeColumn => '-IMPL::SQL::Schema::Traits::AlterTableChangeColumn'
|
|
22 };
|
|
23
|
|
24 our %TRAITS_FORMATS = (
|
|
25 TraitsDropTable, 'FormatDropTable',
|
|
26 TraitsCreateTable, 'FormatCreateTable',
|
|
27 TraitsAlterTableDropConstraint, 'FormatAlterTableDropConstraint',
|
|
28 TraitsAlterTableAddConstraint, 'FormatAlterTableAddConstraint',
|
|
29 TraitsAlterTableDropColumn, 'FormatAlterTableDropColumn',
|
|
30 TraitsAlterTableAddColumn, 'FormatAlterTableAddColumn',
|
|
31 TraitsAlterTableChangeColumn, 'FormatAlterTableChangeColumn'
|
|
32 );
|
|
33
|
|
34 sub quote {
|
|
35 my $self = shift;
|
|
36
|
|
37 if (wantarray) {
|
|
38 return map { my $str = $_; $str =~ s/'/''/g; "'$str'"; } @_;
|
|
39 }
|
|
40 else {
|
|
41 return join '', map { my $str = $_; $str =~ s/'/''/g; "'$str'"; } @_;
|
|
42 }
|
|
43 }
|
|
44
|
|
45 sub quote_names {
|
|
46 my $self = shift;
|
|
47
|
|
48 if (wantarray) {
|
|
49 return map { my $str = $_; $str =~ s/`/``/g; "`$str`"; } @_;
|
|
50 }
|
|
51 else {
|
|
52 return join '', map { my $str = $_; $str =~ s/`/``/g; "`$str`"; } @_;
|
|
53 }
|
|
54 }
|
|
55
|
|
56 sub formatTypeNameInteger {
|
|
57 my ( $self, $type ) = @_;
|
|
58
|
|
59 return
|
|
60 $type->name
|
|
61 . ( $type->maxLength ? '(' . $type->maxLength . ')' : '' )
|
|
62 . ( $type->unsigned ? ' UNSIGNED' : '' )
|
|
63 . ( $type->zerofill ? ' ZEROFILL' : '' );
|
|
64 }
|
|
65
|
|
66 sub formatTypeNameReal {
|
|
67 my ( $self, $type ) = @_;
|
|
68
|
|
69 return $type->name
|
|
70 . ( $type->maxLength
|
|
71 ? '(' . $type->maxLength . ', ' . $type->scale . ')'
|
|
72 : '' )
|
|
73 . ( $type->unsigned ? ' UNSIGNED' : '' )
|
|
74 . ( $type->zerofill ? ' ZEROFILL' : '' );
|
|
75 }
|
|
76
|
|
77 sub formatTypeNameNumeric {
|
|
78 my ( $self, $type ) = @_;
|
|
79 $type->maxLength
|
|
80 or die ArgException->new(
|
|
81 type => 'The length and precission must be specified',
|
|
82 $type->name
|
|
83 );
|
|
84 return $type->name
|
|
85 . ( $type->maxLength
|
|
86 ? '(' . $type->maxLength . ', ' . $type->scale . ')'
|
|
87 : '' )
|
|
88 . ( $type->unsigned ? ' UNSIGNED' : '' )
|
|
89 . ( $type->zerofill ? ' ZEROFILL' : '' );
|
|
90 }
|
|
91
|
|
92 sub formatTypeName {
|
|
93 my ( $self, $type ) = @_;
|
|
94 return $type->name;
|
|
95 }
|
|
96
|
|
97 sub formatTypeNameChar {
|
|
98 my ( $self, $type ) = @_;
|
|
99
|
|
100 return ($type->name . '('
|
|
101 . $type->MaxLength . ')'
|
|
102 . ( is( $type, CharType ) ? $type->encoding : '' ) );
|
|
103 }
|
|
104
|
|
105 sub formatTypeNameVarChar {
|
|
106 my ( $self, $type ) = @_;
|
|
107
|
|
108 return ($type->name . '('
|
|
109 . $type->maxLength . ')'
|
|
110 . ( is( $type, CharType ) ? $type->encoding : '' ) );
|
|
111 }
|
|
112
|
|
113 sub formatTypeNameEnum {
|
|
114 my ( $self, $type ) = @_;
|
|
115
|
|
116 die ArgException->new( type => 'Invalid enum type' )
|
|
117 unless is( $type, EnumType );
|
|
118 return ($type->name . '('
|
|
119 . join( ',', map { $self->quote($_) } $type->enumValues )
|
|
120 . ')' );
|
|
121 }
|
|
122
|
|
123 sub formatStringValue {
|
|
124 my ( $self, $value ) = @_;
|
|
125
|
|
126 if ( ref $value eq 'SCALAR' ) {
|
|
127 return $$value;
|
|
128 }
|
|
129 else {
|
|
130 return $self->quote($value);
|
|
131 }
|
|
132 }
|
|
133
|
|
134 sub formatNumberValue {
|
|
135 my ( $self, $value ) = @_;
|
|
136
|
|
137 if ( ref $value eq 'SCALAR' ) {
|
|
138 return $$value;
|
|
139 }
|
|
140 else {
|
|
141 $value =~ /^((\+|-)\s*)?\d+(\.\d+)?(e(\+|-)?\d+)?$/
|
|
142 or die ArgException->new(
|
|
143 value => 'The specified value isn\'t a valid number',
|
|
144 $value
|
|
145 );
|
|
146 return $value;
|
|
147 }
|
|
148 }
|
|
149
|
|
150 our %TYPES_FORMATS = (
|
|
151 TINYINT => {
|
|
152 formatType => \&formatTypeNameInteger,
|
|
153 formatValue => \&formatNumberValue
|
|
154 },
|
|
155 SMALLINT => {
|
|
156 formatType => \&formatTypeNameInteger,
|
|
157 formatValue => \&formatNumberValue
|
|
158 },
|
|
159 MEDIUMINT => {
|
|
160 formatType => \&formatTypeNameInteger,
|
|
161 formatValue => \&formatNumberValue
|
|
162 },
|
|
163 INT => {
|
|
164 formatType => \&formatTypeNameInteger,
|
|
165 formatValue => \&formatNumberValue
|
|
166 },
|
|
167 INTEGER => {
|
|
168 formatType => \&formatTypeNameInteger,
|
|
169 formatValue => \&formatNumberValue
|
|
170 },
|
|
171 BIGINT => {
|
|
172 formatType => \&formatTypeNameInteger,
|
|
173 formatValue => \&formatNumberValue
|
|
174 },
|
|
175 REAL => {
|
|
176 formatType => \&formatTypeNameReal,
|
|
177 formatValue => \&formatNumberValue
|
|
178 },
|
|
179 DOUBLE => {
|
|
180 formatType => \&formatTypeNameReal,
|
|
181 formatValue => \&formatNumberValue
|
|
182 },
|
|
183 FLOAT => {
|
|
184 formatType => \&formatTypeNameReal,
|
|
185 formatValue => \&formatNumberValue
|
|
186 },
|
|
187 DECIMAL => {
|
|
188 formatType => \&formatTypeNameNumeric,
|
|
189 formatValue => \&formatNumberValue
|
|
190 },
|
|
191 NUMERIC => {
|
|
192 formatType => \&formatTypeNameNumeric,
|
|
193 formatValue => \&formatNumberValue
|
|
194 },
|
|
195 DATE => {
|
|
196 formatType => \&formatTypeName,
|
|
197 formatValue => \&formatStringValue
|
|
198 },
|
|
199 TIME => {
|
|
200 formatType => \&formatTypeName,
|
|
201 formatValue => \&formatStringValue
|
|
202 },
|
|
203 TIMESTAMP => {
|
|
204 formatType => \&formatTypeName,
|
|
205 formatValue => \&formatStringValue
|
|
206 },
|
|
207 DATETIME => {
|
|
208 formatType => \&formatTypeName,
|
|
209 formatValue => \&formatStringValue
|
|
210 },
|
|
211 CHAR => {
|
|
212 formatType => \&formatTypeNameChar,
|
|
213 formatValue => \&formatStringValue
|
|
214 },
|
|
215 VARCHAR => {
|
|
216 formatType => \&formatTypeNameVarChar,
|
|
217 formatValue => \&formatStringValue
|
|
218 },
|
|
219 TINYBLOB => {
|
|
220 formatType => \&formatTypeName,
|
|
221 formatValue => \&formatStringValue
|
|
222 },
|
|
223 BLOB => {
|
|
224 formatType => \&formatTypeName,
|
|
225 formatValue => \&formatStringValue
|
|
226 },
|
|
227 MEDIUMBLOB => {
|
|
228 formatType => \&formatTypeName,
|
|
229 formatValue => \&formatStringValue
|
|
230 },
|
|
231 LONGBLOB => {
|
|
232 formatType => \&formatTypeName,
|
|
233 formatValue => \&formatStringValue
|
|
234 },
|
|
235 TINYTEXT => {
|
|
236 formatType => \&formatTypeName,
|
|
237 formatValue => \&formatStringValue
|
|
238 },
|
|
239 TEXT => {
|
|
240 formatType => \&formatTypeName,
|
|
241 formatValue => \&formatStringValue
|
|
242 },
|
|
243 MEDIUMTEXT => {
|
|
244 formatType => \&formatTypeName,
|
|
245 formatValue => \&formatStringValue
|
|
246 },
|
|
247 LONGTEXT => {
|
|
248 formatType => \&formatTypeName,
|
|
249 formatValue => \&formatStringValue
|
|
250 },
|
|
251 ENUM => {
|
|
252 formatType => \&formatTypeNameEnum,
|
|
253 formatValue => \&formatStringValue
|
|
254 },
|
|
255 SET => {
|
|
256 formatType => \&formatTypeNameEnum,
|
|
257 formatValue => \&formatStringValue
|
|
258 }
|
|
259 );
|
|
260
|
|
261 sub FormatTypeName {
|
|
262 my ( $self, $type ) = @_;
|
|
263
|
|
264 my $fn = $TYPES_FORMATS{ $type->name }{formatType}
|
|
265 or die ArgException->new( type => "The specified type is unknown",
|
|
266 $type->name );
|
|
267
|
|
268 return $self->$fn($type);
|
|
269 }
|
|
270
|
|
271 sub FormatValue {
|
|
272 my ( $self, $value, $type ) = @_;
|
|
273
|
|
274 my $fn = $TYPES_FORMATS{ $type->name }{formatValue}
|
|
275 or die ArgException->new( type => "The specified type is unknown",
|
|
276 $type->name );
|
|
277
|
|
278 return $self->$fn( $value, $type );
|
|
279 }
|
|
280
|
|
281 sub FormatColumn {
|
|
282 my ( $self, $column ) = @_;
|
|
283
|
|
284 my @parts = (
|
|
285 $self->quote_names( $column->{name} ),
|
|
286 $self->FormatTypeName( $column->{type} ),
|
|
287 $column->{isNullable} ? 'NULL' : 'NOT NULL'
|
|
288 );
|
|
289
|
|
290 push @parts, $self->FormatValue( $column->{defaultValue}, $column->{type} )
|
|
291 if $column->{defaultValue};
|
|
292
|
|
293 push @parts, 'AUTO_INCREMENT'
|
|
294 if $column->{tag} and $column->{tag}->{auto_increment};
|
|
295
|
|
296 return join ' ', @parts;
|
|
297 }
|
|
298
|
|
299 sub FormatCreateTable {
|
|
300 my ( $self, $op ) = @_;
|
|
301
|
|
302 my $table = $op->table;
|
|
303
|
|
304 my @lines;
|
|
305 my @body;
|
|
306
|
|
307 push @lines, "CREATE TABLE " . $self->quote_names($table->{name}) . "(";
|
|
308
|
|
309 push @body, map { " " . $self->FormatColumn($_) } @{ $table->{columns} }
|
|
310 if $table->{columns};
|
|
311
|
|
312 push @body, map { " " . $self->FormatConstraint($_) } @{ $table->{constraints} }
|
|
313 if $table->{constraints};
|
|
314
|
|
315 push @lines, join(",\n", @body);
|
|
316
|
|
317 push @lines, ");";
|
|
318
|
|
319 return join "\n", @lines;
|
|
320 }
|
|
321
|
|
322 sub FormatDropTable {
|
|
323 my ( $self, $op ) = @_;
|
|
324
|
|
325 return join ' ', 'DROP TABLE', $self->quote_names( $op->tableName ), ';';
|
|
326 }
|
|
327
|
|
328 sub FormatRenameTable {
|
|
329 my ( $self, $op ) = @_;
|
|
330
|
|
331 return join ' ',
|
|
332 'ALTER TABLE',
|
|
333 $self->quote_names( $op->tableName ),
|
|
334 'RENAME TO',
|
|
335 $self->quote_names( $op->tableNewName ),
|
|
336 ';';
|
|
337 }
|
|
338
|
|
339 sub FormatAlterTableAddColumn {
|
|
340 my ( $self, $op, $schema ) = @_;
|
|
341
|
|
342 my @parts = (
|
|
343 'ALTER TABLE',$self->quote_names($op->tableName), 'ADD COLUMN',
|
|
344 $self->FormatColumn( $op->column )
|
|
345 );
|
|
346
|
|
347 if ( defined $op->position ) {
|
|
348
|
|
349 # mysql supports column reordering
|
|
350 # the new location is specified relative to the previous column
|
|
351 # to determine the name of the previous column we need to ask the schema
|
|
352
|
|
353 my $table = $schema->GetTable( $op->tableName );
|
|
354
|
|
355 if ( $op->position == 0 ) {
|
|
356 push @parts, 'FIRST';
|
|
357 }
|
|
358 else {
|
|
359 push @parts, 'AFTER';
|
|
360
|
|
361 my $prevColumn = $table->GetColumnAt( $op->position - 1 );
|
|
362 push @parts, $self->quote_names( $prevColumn->{name} );
|
|
363 }
|
|
364 }
|
|
365
|
|
366 push @parts, ';';
|
|
367
|
|
368 return join ' ', @parts;
|
|
369 }
|
|
370
|
|
371 sub FormatAlterTableDropColumn {
|
|
372 my ( $self, $op ) = @_;
|
|
373
|
|
374 return join ' ',
|
|
375 'ALTER TABLE',
|
|
376 $self->quote_names( $op->tableName ),
|
|
377 'DROP COLUMN',
|
|
378 $self->quote_names( $op->columnName ),
|
|
379 ';';
|
|
380 }
|
|
381
|
|
382 sub FormatAlterTableChangeColumn {
|
|
383 my ( $self, $op, $schema ) = @_;
|
|
384
|
|
385 my $table = $schema->GetTable( $op->tableName );
|
|
386 my $column = $table->GetColumn( $op->columnName );
|
|
387
|
|
388 my @parts = (
|
|
389 'ALTER TABLE',
|
|
390 $self->quote_names( $op->tableName ),
|
|
391 'MODIFY COLUMN',
|
|
392 $self->quote_names( $op->columnName ),
|
|
393 $self->FormatColumn( $self->_Column2Traits($column) )
|
|
394 );
|
|
395
|
|
396 if ( defined $op->position ) {
|
|
397
|
|
398 # mysql supports column reordering
|
|
399 # the new location is specified relative to the previous column
|
|
400 # to determine the name of the previous column we need to ask the schema
|
|
401
|
|
402 if ( $op->position == 0 ) {
|
|
403 push @parts, 'FIRST';
|
|
404 }
|
|
405 else {
|
|
406 push @parts, 'AFTER';
|
|
407
|
|
408 my $prevColumn = $table->GetColumnAt( $op->position - 1 );
|
|
409 push @parts, $self->quote_names( $prevColumn->{name} );
|
|
410 }
|
|
411 }
|
|
412
|
|
413 push @parts, ';';
|
|
414 return join ' ', @parts;
|
|
415 }
|
|
416
|
|
417 sub FormatConstraint {
|
|
418 my ($self,$constraint) = @_;
|
|
419
|
|
420 my @fkRules =
|
|
421 ( 'RESTRICT', 'CASCADE', 'SET NULL', 'SET DEFAULT', 'NO ACTION' );
|
|
422
|
|
423 my @parts;
|
|
424
|
|
425 if ( $constraint->constraintClass eq ForeignKey ) {
|
|
426 push @parts,
|
|
427 'CONSTRAINT',
|
|
428 $self->quote_names( $constraint->{name} ),
|
|
429 'FOREIGN KEY',
|
|
430 $self->quote_names( $constraint->{name} ),
|
|
431 '(',
|
|
432 join( ', ', $self->quote_names( @{ $constraint->{columns} || [] } ) ),
|
|
433 ')',
|
|
434 'REFERENCES', $self->quote_names( $constraint->{foreignTable} ), '(',
|
|
435 join( ', ',
|
|
436 $self->quote_names( @{ $constraint->{foreignColumns} || [] } ) ),
|
|
437 ')';
|
|
438
|
272
|
439 if ( my $rule = $constraint->{onDelete} ) {
|
|
440 $rule = uc($rule);
|
271
|
441 grep $_ eq $rule, @fkRules
|
|
442 or die Exception->new( "Invalid onDelete rule specified",
|
|
443 $constraint->{name}, $rule );
|
|
444
|
|
445 push @parts, 'ON DELETE', $rule;
|
|
446 }
|
|
447
|
272
|
448 if ( my $rule = $constraint->{onUpdate} ) {
|
|
449 $rule = uc($rule);
|
271
|
450 grep $_ eq $rule, @fkRules
|
|
451 or die Exception->new( "Invalid onUpdate rule specified",
|
|
452 $constraint->{name}, $rule );
|
|
453
|
|
454 push @parts, 'ON UPDATE', $rule;
|
|
455 }
|
|
456
|
|
457 }
|
|
458 else {
|
|
459 if ( $constraint->constraintClass eq PrimaryKey ) {
|
|
460 push @parts, 'PRIMARY KEY';
|
|
461
|
|
462 }
|
|
463 elsif ( $constraint->constraintClass eq UniqueIndex ) {
|
|
464 push @parts, 'UNIQUE', $self->quote_names( $constraint->{name} );
|
|
465 }
|
|
466 elsif ( $constraint->constraintClass eq Index ) {
|
|
467 push @parts, 'INDEX', $self->quote_names( $constraint->{name} );
|
|
468 }
|
|
469 else {
|
|
470 die Exception->new( 'Invalid constraint type',
|
|
471 $constraint->constraintClass );
|
|
472 }
|
|
473
|
|
474 push @parts,
|
|
475 '(',
|
|
476 join( ', ', $self->quote_names( @{ $constraint->{columns} || [] } ) ),
|
|
477 ')';
|
|
478 }
|
|
479
|
|
480
|
|
481 return join ' ', @parts;
|
|
482 }
|
|
483
|
|
484 sub FormatAlterTableAddConstraint {
|
|
485 my ( $self, $op ) = @_;
|
|
486
|
|
487 return join(' ',
|
|
488 'ALTER TABLE',
|
|
489 $self->quote_names( $op->tableName ),
|
|
490 'ADD',
|
|
491 $self->FormatConstraint($op->constraint),
|
|
492 ';'
|
|
493 );
|
|
494 }
|
|
495
|
|
496 sub FormatAlterTableDropConstraint {
|
|
497 my ( $self, $op, $constraintType ) = @_;
|
|
498
|
|
499 my @parts = ( 'ALTER TABLE', $self->quote_names( $op->tableName ), 'DROP' );
|
|
500
|
|
501 if ( $constraintType eq PrimaryKey ) {
|
|
502 push @parts, 'PRIMARY KEY';
|
|
503 }
|
|
504 elsif ( $constraintType eq ForeignKey ) {
|
|
505 push @parts, 'FOREIGN KEY', $self->quote_names( $op->constraintName );
|
|
506 }
|
|
507 elsif ( $constraintType eq UniqueIndex or $constraintType eq Index ) {
|
|
508 push @parts, 'INDEX', $self->quote_names( $op->constraintName );
|
|
509 }
|
|
510 else {
|
|
511 die Exception->new(
|
|
512 'Invalid constraint type', $op->tableName,
|
|
513 $op->constraintName, $constraintType
|
|
514 );
|
|
515 }
|
|
516
|
|
517 push @parts, ';';
|
|
518
|
|
519 return join ' ', @parts;
|
|
520 }
|
|
521
|
|
522 sub Format {
|
|
523 my $self = shift;
|
|
524 my ($op) = @_;
|
|
525
|
|
526 my $formatter = $TRAITS_FORMATS{ref $op}
|
|
527 or die OpException->new("Don't know how to format the specified operation", $op);
|
|
528
|
|
529 $self->$formatter(@_);
|
|
530 }
|
|
531
|
|
532 sub _Column2Traits {
|
|
533 my ( $self, $column, %options ) = @_;
|
|
534
|
|
535 return new IMPL::SQL::Schema::Traits::Column(
|
|
536 $column->name,
|
|
537 $column->type,
|
|
538 isNullable => $column->isNullable,
|
|
539 defaultValue => $column->defaultValue,
|
|
540 tag => $column->tag,
|
|
541 %options
|
|
542 );
|
|
543 }
|
|
544
|
|
545 1;
|
|
546
|
|
547 __END__
|
|
548
|
|
549 =pod
|
|
550
|
|
551 =head1 NAME
|
|
552
|
|
553 C<IMPL::SQL::Traits::MysqlFormatter> - преобразует операции над схемой в C<SQL>
|
|
554 выражения.
|
|
555
|
|
556 =head1 DESCRIPTION
|
|
557
|
|
558 Используется для форматирования операций изменения схемы БД. Осуществляет
|
|
559 правильное экранирование имен, форматирование значений, имен типов данных.
|
|
560
|
|
561 =cut
|