diff Lib/IMPL/SQL/Schema/MySQL/Formatter.pm @ 271:56364d0c4b4f

+IMPL::SQL::Schema::MySQL: added basic support for MySQL
author cin
date Mon, 28 Jan 2013 02:43:14 +0400
parents
children 47db27ed5b43
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Lib/IMPL/SQL/Schema/MySQL/Formatter.pm	Mon Jan 28 02:43:14 2013 +0400
@@ -0,0 +1,559 @@
+package IMPL::SQL::Schema::MySQL::Formatter;
+use strict;
+
+use IMPL::lang qw(is);
+use IMPL::require {
+    Exception         => 'IMPL::Exception',
+    OpException       => '-IMPL::InvalidOperationException',
+    ArgException      => '-IMPL::InvalidArgumentException',
+    PrimaryKey        => '-IMPL::SQL::Schema::Constraint::PrimaryKey',
+    UniqueIndex       => '-IMPL::SQL::Schema::Constraint::Unique',
+    Index             => '-IMPL::SQL::Schema::Constraint::Index',
+    ForeignKey        => '-IMPL::SQL::Schema::Constraint::ForeignKey',
+    CharType          => '-IMPL::SQL::Schema::MySQL::CharType',
+    EnumType          => '-IMPL::SQL::Schema::MySQL::EnumType',
+    TraitsDropTable   => '-IMPL::SQL::Schema::Traits::DropTable',
+    TraitsCreateTable => '-IMPL::SQL::Schema::Traits::CreateTable',
+    TraitsAlterTableDropConstraint => '-IMPL::SQL::Schema::Traits::AlterTableDropConstraint',
+    TraitsAlterTableAddConstraint  => '-IMPL::SQL::Schema::Traits::AlterTableAddConstraint',
+    TraitsAlterTableDropColumn     => '-IMPL::SQL::Schema::Traits::AlterTableDropColumn',
+    TraitsAlterTableAddColumn      => '-IMPL::SQL::Schema::Traits::AlterTableAddColumn',
+    TraitsAlterTableChangeColumn   => '-IMPL::SQL::Schema::Traits::AlterTableChangeColumn'
+};
+
+our %TRAITS_FORMATS = (
+    TraitsDropTable,                'FormatDropTable',
+    TraitsCreateTable,              'FormatCreateTable',
+    TraitsAlterTableDropConstraint, 'FormatAlterTableDropConstraint',
+    TraitsAlterTableAddConstraint,  'FormatAlterTableAddConstraint',
+    TraitsAlterTableDropColumn,     'FormatAlterTableDropColumn',
+    TraitsAlterTableAddColumn,      'FormatAlterTableAddColumn',
+    TraitsAlterTableChangeColumn,   'FormatAlterTableChangeColumn'
+);
+
+sub quote {
+    my $self = shift;
+
+    if (wantarray) {
+        return map { my $str = $_; $str =~ s/'/''/g; "'$str'"; } @_;
+    }
+    else {
+        return join '', map { my $str = $_; $str =~ s/'/''/g; "'$str'"; } @_;
+    }
+}
+
+sub quote_names {
+    my $self = shift;
+
+    if (wantarray) {
+        return map { my $str = $_; $str =~ s/`/``/g; "`$str`"; } @_;
+    }
+    else {
+        return join '', map { my $str = $_; $str =~ s/`/``/g; "`$str`"; } @_;
+    }
+}
+
+sub formatTypeNameInteger {
+    my ( $self, $type ) = @_;
+
+    return
+        $type->name
+      . ( $type->maxLength ? '(' . $type->maxLength . ')' : '' )
+      . ( $type->unsigned  ? ' UNSIGNED'                  : '' )
+      . ( $type->zerofill  ? ' ZEROFILL'                  : '' );
+}
+
+sub formatTypeNameReal {
+    my ( $self, $type ) = @_;
+
+    return $type->name
+      . ( $type->maxLength
+        ? '(' . $type->maxLength . ', ' . $type->scale . ')'
+        : '' )
+      . ( $type->unsigned ? ' UNSIGNED' : '' )
+      . ( $type->zerofill ? ' ZEROFILL' : '' );
+}
+
+sub formatTypeNameNumeric {
+    my ( $self, $type ) = @_;
+    $type->maxLength
+      or die ArgException->new(
+        type => 'The length and precission must be specified',
+        $type->name
+      );
+    return $type->name
+      . ( $type->maxLength
+        ? '(' . $type->maxLength . ', ' . $type->scale . ')'
+        : '' )
+      . ( $type->unsigned ? ' UNSIGNED' : '' )
+      . ( $type->zerofill ? ' ZEROFILL' : '' );
+}
+
+sub formatTypeName {
+    my ( $self, $type ) = @_;
+    return $type->name;
+}
+
+sub formatTypeNameChar {
+    my ( $self, $type ) = @_;
+
+    return ($type->name . '('
+          . $type->MaxLength . ')'
+          . ( is( $type, CharType ) ? $type->encoding : '' ) );
+}
+
+sub formatTypeNameVarChar {
+    my ( $self, $type ) = @_;
+
+    return ($type->name . '('
+          . $type->maxLength . ')'
+          . ( is( $type, CharType ) ? $type->encoding : '' ) );
+}
+
+sub formatTypeNameEnum {
+    my ( $self, $type ) = @_;
+
+    die ArgException->new( type => 'Invalid enum type' )
+      unless is( $type, EnumType );
+    return ($type->name . '('
+          . join( ',', map { $self->quote($_) } $type->enumValues )
+          . ')' );
+}
+
+sub formatStringValue {
+    my ( $self, $value ) = @_;
+
+    if ( ref $value eq 'SCALAR' ) {
+        return $$value;
+    }
+    else {
+        return $self->quote($value);
+    }
+}
+
+sub formatNumberValue {
+    my ( $self, $value ) = @_;
+
+    if ( ref $value eq 'SCALAR' ) {
+        return $$value;
+    }
+    else {
+        $value =~ /^((\+|-)\s*)?\d+(\.\d+)?(e(\+|-)?\d+)?$/
+          or die ArgException->new(
+            value => 'The specified value isn\'t a valid number',
+            $value
+          );
+        return $value;
+    }
+}
+
+our %TYPES_FORMATS = (
+    TINYINT => {
+        formatType  => \&formatTypeNameInteger,
+        formatValue => \&formatNumberValue
+    },
+    SMALLINT => {
+        formatType  => \&formatTypeNameInteger,
+        formatValue => \&formatNumberValue
+    },
+    MEDIUMINT => {
+        formatType  => \&formatTypeNameInteger,
+        formatValue => \&formatNumberValue
+    },
+    INT => {
+        formatType  => \&formatTypeNameInteger,
+        formatValue => \&formatNumberValue
+    },
+    INTEGER => {
+        formatType  => \&formatTypeNameInteger,
+        formatValue => \&formatNumberValue
+    },
+    BIGINT => {
+        formatType  => \&formatTypeNameInteger,
+        formatValue => \&formatNumberValue
+    },
+    REAL => {
+        formatType  => \&formatTypeNameReal,
+        formatValue => \&formatNumberValue
+    },
+    DOUBLE => {
+        formatType  => \&formatTypeNameReal,
+        formatValue => \&formatNumberValue
+    },
+    FLOAT => {
+        formatType  => \&formatTypeNameReal,
+        formatValue => \&formatNumberValue
+    },
+    DECIMAL => {
+        formatType  => \&formatTypeNameNumeric,
+        formatValue => \&formatNumberValue
+    },
+    NUMERIC => {
+        formatType  => \&formatTypeNameNumeric,
+        formatValue => \&formatNumberValue
+    },
+    DATE => {
+        formatType  => \&formatTypeName,
+        formatValue => \&formatStringValue
+    },
+    TIME => {
+        formatType  => \&formatTypeName,
+        formatValue => \&formatStringValue
+    },
+    TIMESTAMP => {
+        formatType  => \&formatTypeName,
+        formatValue => \&formatStringValue
+    },
+    DATETIME => {
+        formatType  => \&formatTypeName,
+        formatValue => \&formatStringValue
+    },
+    CHAR => {
+        formatType  => \&formatTypeNameChar,
+        formatValue => \&formatStringValue
+    },
+    VARCHAR => {
+        formatType  => \&formatTypeNameVarChar,
+        formatValue => \&formatStringValue
+    },
+    TINYBLOB => {
+        formatType  => \&formatTypeName,
+        formatValue => \&formatStringValue
+    },
+    BLOB => {
+        formatType  => \&formatTypeName,
+        formatValue => \&formatStringValue
+    },
+    MEDIUMBLOB => {
+        formatType  => \&formatTypeName,
+        formatValue => \&formatStringValue
+    },
+    LONGBLOB => {
+        formatType  => \&formatTypeName,
+        formatValue => \&formatStringValue
+    },
+    TINYTEXT => {
+        formatType  => \&formatTypeName,
+        formatValue => \&formatStringValue
+    },
+    TEXT => {
+        formatType  => \&formatTypeName,
+        formatValue => \&formatStringValue
+    },
+    MEDIUMTEXT => {
+        formatType  => \&formatTypeName,
+        formatValue => \&formatStringValue
+    },
+    LONGTEXT => {
+        formatType  => \&formatTypeName,
+        formatValue => \&formatStringValue
+    },
+    ENUM => {
+        formatType  => \&formatTypeNameEnum,
+        formatValue => \&formatStringValue
+    },
+    SET => {
+        formatType  => \&formatTypeNameEnum,
+        formatValue => \&formatStringValue
+    }
+);
+
+sub FormatTypeName {
+    my ( $self, $type ) = @_;
+
+    my $fn = $TYPES_FORMATS{ $type->name }{formatType}
+      or die ArgException->new( type => "The specified type is unknown",
+        $type->name );
+
+    return $self->$fn($type);
+}
+
+sub FormatValue {
+    my ( $self, $value, $type ) = @_;
+
+    my $fn = $TYPES_FORMATS{ $type->name }{formatValue}
+      or die ArgException->new( type => "The specified type is unknown",
+        $type->name );
+
+    return $self->$fn( $value, $type );
+}
+
+sub FormatColumn {
+    my ( $self, $column ) = @_;
+
+    my @parts = (
+        $self->quote_names( $column->{name} ),
+        $self->FormatTypeName( $column->{type} ),
+        $column->{isNullable} ? 'NULL' : 'NOT NULL'
+    );
+
+    push @parts, $self->FormatValue( $column->{defaultValue}, $column->{type} )
+      if $column->{defaultValue};
+
+    push @parts, 'AUTO_INCREMENT'
+      if $column->{tag} and $column->{tag}->{auto_increment};
+
+    return join ' ', @parts;
+}
+
+sub FormatCreateTable {
+    my ( $self, $op ) = @_;
+
+    my $table = $op->table;
+
+    my @lines;
+    my @body;
+
+    push @lines, "CREATE TABLE " . $self->quote_names($table->{name}) . "(";
+
+    push @body, map { "  " . $self->FormatColumn($_) } @{ $table->{columns} }
+        if $table->{columns};
+      
+    push @body, map { "  " . $self->FormatConstraint($_) } @{ $table->{constraints} }
+        if $table->{constraints};
+        
+    push @lines, join(",\n", @body);
+
+    push @lines, ");";
+
+    return join "\n", @lines;
+}
+
+sub FormatDropTable {
+    my ( $self, $op ) = @_;
+
+    return join ' ', 'DROP TABLE', $self->quote_names( $op->tableName ), ';';
+}
+
+sub FormatRenameTable {
+    my ( $self, $op ) = @_;
+
+    return join ' ',
+      'ALTER TABLE',
+      $self->quote_names( $op->tableName ),
+      'RENAME TO',
+      $self->quote_names( $op->tableNewName ),
+      ';';
+}
+
+sub FormatAlterTableAddColumn {
+    my ( $self, $op, $schema ) = @_;
+
+    my @parts = (
+        'ALTER TABLE',$self->quote_names($op->tableName), 'ADD COLUMN',
+        $self->FormatColumn( $op->column )
+    );
+
+    if ( defined $op->position ) {
+
+        # mysql supports column reordering
+        # the new location is specified relative to the previous column
+        # to determine the name of the previous column we need to ask the schema
+
+        my $table = $schema->GetTable( $op->tableName );
+
+        if ( $op->position == 0 ) {
+            push @parts, 'FIRST';
+        }
+        else {
+            push @parts, 'AFTER';
+
+            my $prevColumn = $table->GetColumnAt( $op->position - 1 );
+            push @parts, $self->quote_names( $prevColumn->{name} );
+        }
+    }
+
+    push @parts, ';';
+
+    return join ' ', @parts;
+}
+
+sub FormatAlterTableDropColumn {
+    my ( $self, $op ) = @_;
+
+    return join ' ',
+      'ALTER TABLE',
+      $self->quote_names( $op->tableName ),
+      'DROP COLUMN',
+      $self->quote_names( $op->columnName ),
+      ';';
+}
+
+sub FormatAlterTableChangeColumn {
+    my ( $self, $op, $schema ) = @_;
+
+    my $table  = $schema->GetTable( $op->tableName );
+    my $column = $table->GetColumn( $op->columnName );
+
+    my @parts = (
+        'ALTER TABLE',
+        $self->quote_names( $op->tableName ),
+        'MODIFY COLUMN',
+        $self->quote_names( $op->columnName ),
+        $self->FormatColumn( $self->_Column2Traits($column) )
+    );
+
+    if ( defined $op->position ) {
+
+        # mysql supports column reordering
+        # the new location is specified relative to the previous column
+        # to determine the name of the previous column we need to ask the schema
+
+        if ( $op->position == 0 ) {
+            push @parts, 'FIRST';
+        }
+        else {
+            push @parts, 'AFTER';
+
+            my $prevColumn = $table->GetColumnAt( $op->position - 1 );
+            push @parts, $self->quote_names( $prevColumn->{name} );
+        }
+    }
+
+    push @parts, ';';
+    return join ' ', @parts;
+}
+
+sub FormatConstraint {
+    my ($self,$constraint) = @_;
+    
+    my @fkRules =
+      ( 'RESTRICT', 'CASCADE', 'SET NULL', 'SET DEFAULT', 'NO ACTION' );
+      
+    my @parts;
+
+    if ( $constraint->constraintClass eq ForeignKey ) {
+        push @parts,
+          'CONSTRAINT',
+          $self->quote_names( $constraint->{name} ),
+          'FOREIGN KEY',
+          $self->quote_names( $constraint->{name} ),
+          '(',
+          join( ', ', $self->quote_names( @{ $constraint->{columns} || [] } ) ),
+          ')',
+          'REFERENCES', $self->quote_names( $constraint->{foreignTable} ), '(',
+          join( ', ',
+            $self->quote_names( @{ $constraint->{foreignColumns} || [] } ) ),
+          ')';
+
+        if ( my $rule = uc( $constraint->{onDelete} ) ) {
+            grep $_ eq $rule, @fkRules
+              or die Exception->new( "Invalid onDelete rule specified",
+                $constraint->{name}, $rule );
+
+            push @parts, 'ON DELETE', $rule;
+        }
+
+        if ( my $rule = uc( $constraint->{onUpdate} ) ) {
+            grep $_ eq $rule, @fkRules
+              or die Exception->new( "Invalid onUpdate rule specified",
+                $constraint->{name}, $rule );
+
+            push @parts, 'ON UPDATE', $rule;
+        }
+
+    }
+    else {
+        if ( $constraint->constraintClass eq PrimaryKey ) {
+            push @parts, 'PRIMARY KEY';
+
+        }
+        elsif ( $constraint->constraintClass eq UniqueIndex ) {
+            push @parts, 'UNIQUE', $self->quote_names( $constraint->{name} );
+        }
+        elsif ( $constraint->constraintClass eq Index ) {
+            push @parts, 'INDEX', $self->quote_names( $constraint->{name} );
+        }
+        else {
+            die Exception->new( 'Invalid constraint type',
+                $constraint->constraintClass );
+        }
+
+        push @parts,
+          '(',
+          join( ', ', $self->quote_names( @{ $constraint->{columns} || [] } ) ),
+          ')';
+    }
+    
+    
+    return join ' ', @parts;
+}
+
+sub FormatAlterTableAddConstraint {
+    my ( $self, $op ) = @_;
+
+    return join(' ', 
+        'ALTER TABLE',
+        $self->quote_names( $op->tableName ),
+        'ADD',
+        $self->FormatConstraint($op->constraint),
+        ';'
+    );
+}
+
+sub FormatAlterTableDropConstraint {
+    my ( $self, $op, $constraintType ) = @_;
+
+    my @parts = ( 'ALTER TABLE', $self->quote_names( $op->tableName ), 'DROP' );
+
+    if ( $constraintType eq PrimaryKey ) {
+        push @parts, 'PRIMARY KEY';
+    }
+    elsif ( $constraintType eq ForeignKey ) {
+        push @parts, 'FOREIGN KEY', $self->quote_names( $op->constraintName );
+    }
+    elsif ( $constraintType eq UniqueIndex or $constraintType eq Index ) {
+        push @parts, 'INDEX', $self->quote_names( $op->constraintName );
+    }
+    else {
+        die Exception->new(
+            'Invalid constraint type', $op->tableName,
+            $op->constraintName,       $constraintType
+        );
+    }
+    
+    push @parts, ';';
+    
+    return join ' ', @parts;
+}
+
+sub Format {
+    my $self = shift;
+    my ($op) = @_;
+    
+    my $formatter = $TRAITS_FORMATS{ref $op}
+        or die OpException->new("Don't know how to format the specified operation", $op);
+        
+    $self->$formatter(@_);
+}
+
+sub _Column2Traits {
+    my ( $self, $column, %options ) = @_;
+
+    return new IMPL::SQL::Schema::Traits::Column(
+        $column->name,
+        $column->type,
+        isNullable   => $column->isNullable,
+        defaultValue => $column->defaultValue,
+        tag          => $column->tag,
+        %options
+    );
+}
+
+1;
+
+__END__
+
+=pod
+
+=head1 NAME
+
+C<IMPL::SQL::Traits::MysqlFormatter> - преобразует операции над схемой в C<SQL>
+выражения.
+
+=head1 DESCRIPTION
+
+Используется для форматирования операций изменения схемы БД. Осуществляет
+правильное экранирование имен, форматирование значений, имен типов данных.
+
+=cut