diff --git a/lib/SQL/Translator/Parser/MySQL.pm b/lib/SQL/Translator/Parser/MySQL.pm index 071f8727..65a24c62 100644 --- a/lib/SQL/Translator/Parser/MySQL.pm +++ b/lib/SQL/Translator/Parser/MySQL.pm @@ -17,7 +17,7 @@ SQL::Translator::Parser::MySQL - parser for MySQL The grammar is influenced heavily by Tim Bunce's "mysql2ora" grammar. Here's the word from the MySQL site -(http://www.mysql.com/doc/en/CREATE_TABLE.html): +(https://dev.mysql.com/doc/refman/en/create-table.html): CREATE [TEMPORARY] TABLE [IF NOT EXISTS] tbl_name [(create_definition,...)] [table_options] [select_statement] @@ -27,7 +27,7 @@ Here's the word from the MySQL site CREATE [TEMPORARY] TABLE [IF NOT EXISTS] tbl_name LIKE old_table_name; create_definition: - col_name type [NOT NULL | NULL] [DEFAULT default_value] [AUTO_INCREMENT] + col_name type [GENERATED ALWAYS] AS (expr) [NOT NULL | NULL] [DEFAULT default_value] [AUTO_INCREMENT] [PRIMARY KEY] [reference_definition] or PRIMARY KEY (index_col_name,...) or KEY [index_name] (index_col_name,...) @@ -478,7 +478,7 @@ field_comment : /^\s*(?:#|-{2}).*\n/ blank : /\s*/ -field : field_comment(s?) field_name data_type field_qualifier(s?) reference_definition(?) on_update(?) field_comment(s?) +field : field_comment(s?) field_name data_type field_generated(s?) field_qualifier(s?) reference_definition(?) on_update(?) field_comment(s?) { my %qualifiers = map { %$_ } @{ $item{'field_qualifier(s?)'} || [] }; if ( my @type_quals = @{ $item{'data_type'}{'qualifiers'} || [] } ) { @@ -497,6 +497,7 @@ field : field_comment(s?) field_name data_type field_qualifier(s?) reference_def data_type => $item{'data_type'}{'type'}, size => $item{'data_type'}{'size'}, list => $item{'data_type'}{'list'}, + generated => $item{'field_generated(s?)'}[0], null => $null, constraints => $item{'reference_definition(?)'}, comments => [ @comments ], @@ -505,6 +506,14 @@ field : field_comment(s?) field_name data_type field_qualifier(s?) reference_def } | +field_generated : generated_always(s?) /as/i '(' expr ')' generated_type(s?) + { + $return = { + expr => $item{'expr'}, + type => $item{'generated_type(s?)'}[0] || 'VIRTUAL', + } + } + field_qualifier : not_null { $return = { @@ -708,7 +717,12 @@ constraint : primary_key_def | check_def | -expr : /[^)]* \( [^)]+ \) [^)]*/x # parens, balanced one deep +generated_always : /generated/i /always/i + +generated_type : /stored/i + | /virtual/i + +expr : /( [^()]* \( (?: (?> [^()]+ ) | (?1) )* \) ) [^)]*/x # parens, recursive balanced | /[^)]+/ check_def : check_def_begin '(' expr ')' @@ -972,7 +986,7 @@ sub parse { $table->primary_key($field->name) if $fdata->{'is_primary_key'}; - for my $qual (qw[ binary unsigned zerofill list collate ], 'character set', 'on update') { + for my $qual (qw[ binary unsigned zerofill list collate generated ], 'character set', 'on update') { if (my $val = $fdata->{$qual} || $fdata->{ uc $qual }) { next if ref $val eq 'ARRAY' && !@$val; $field->extra($qual, $val); diff --git a/lib/SQL/Translator/Producer/MySQL.pm b/lib/SQL/Translator/Producer/MySQL.pm index 50926e50..ab03a81a 100644 --- a/lib/SQL/Translator/Producer/MySQL.pm +++ b/lib/SQL/Translator/Producer/MySQL.pm @@ -568,6 +568,7 @@ sub create_field { my $data_type = $field->data_type; my @size = $field->size; my %extra = $field->extra; + my $generated = $extra{'generated'} || {}; my $list = $extra{'list'} || []; my $commalist = join(', ', map { __PACKAGE__->_quote_string($_) } @$list); my $charset = $extra{'mysql_charset'}; @@ -624,6 +625,8 @@ sub create_field { $field_def .= '(' . join(', ', @size) . ')'; } + $field_def .= " GENERATED ALWAYS AS (" . $generated->{expr} . ") " . $generated->{type} if %$generated; + # char sets $field_def .= " CHARACTER SET $charset" if $charset; $field_def .= " COLLATE $collate" if $collate; diff --git a/t/02mysql-parser.t b/t/02mysql-parser.t index 397161d6..89dad72f 100644 --- a/t/02mysql-parser.t +++ b/t/02mysql-parser.t @@ -87,6 +87,9 @@ BEGIN { foo_enabled bit(1) default b'0', bar_enabled bit(1) default b"1", long_foo_enabled bit(10) default b'1010101', + g1 int(11) AS (if((1 + 1 = 2),1,NULL)), + g2 int(11) AS (if((1 + 1 = 2),1,(1 + 1))) STORED, + g3 int(11) GENERATED ALWAYS AS (1 + 1) VIRTUAL, KEY (i1), UNIQUE (date, i1) USING BTREE, KEY date_idx (date), @@ -103,7 +106,7 @@ BEGIN { is($table->name, 'check', 'Found "check" table'); my @fields = $table->get_fields; - is(scalar @fields, 13, 'Right number of fields (13)'); + is(scalar @fields, 16, 'Right number of fields (16)'); my $f1 = shift @fields; is($f1->name, 'check_id', 'First field name is "check_id"'); is($f1->data_type, 'int', 'Type is "int"'); @@ -218,6 +221,39 @@ BEGIN { is($f13->default_value, '1010101', 'Default value is 1010101'); is($f13->is_primary_key, 0, 'Field is not PK'); + my $f14 = shift @fields; + is($f14->name, 'g1', 'Fifth field name is "g1"'); + is($f14->data_type, 'int', 'Type is "int"'); + is($f14->size, 11, 'Size is "11"'); + is($f14->is_nullable, 1, 'Field can be null'); + is($f14->default_value, undef, 'Default value is undefined'); + is($f14->is_primary_key, 0, 'Field is not PK'); + my %f14extra = $f14->extra; + is($f14extra{'generated'}->{expr}, "if((1 + 1 = 2),1,NULL)", 'Generated expr is "if((1 + 1 = 2),1,NULL)"'); + is($f14extra{'generated'}->{type}, 'VIRTUAL', 'Generated type is "VIRTUAL"'); + + my $f15 = shift @fields; + is($f15->name, 'g2', 'Fifth field name is "g2"'); + is($f15->data_type, 'int', 'Type is "int"'); + is($f15->size, 11, 'Size is "11"'); + is($f15->is_nullable, 1, 'Field can be null'); + is($f15->default_value, undef, 'Default value is undefined'); + is($f15->is_primary_key, 0, 'Field is not PK'); + my %f15extra = $f15->extra; + is($f15extra{'generated'}->{expr}, 'if((1 + 1 = 2),1,(1 + 1))', 'Generated expr is "if((1 + 1 = 2),1,(1 + 1))"'); + is($f15extra{'generated'}->{type}, 'STORED', 'Generated type is "STORED"'); + + my $f16 = shift @fields; + is($f16->name, 'g3', 'Fifth field name is "g3"'); + is($f16->data_type, 'int', 'Type is "int"'); + is($f16->size, 11, 'Size is "11"'); + is($f16->is_nullable, 1, 'Field can be null'); + is($f16->default_value, undef, 'Default value is undefined'); + is($f16->is_primary_key, 0, 'Field is not PK'); + my %f16extra = $f16->extra; + is($f16extra{'generated'}->{expr}, '1 + 1', 'Generated expr is "1 + 1"'); + is($f16extra{'generated'}->{type}, 'VIRTUAL', 'Generated type is "VIRTUAL"'); + my @indices = $table->get_indices; is(scalar @indices, 3, 'Right number of indices (3)'); diff --git a/t/38-mysql-producer.t b/t/38-mysql-producer.t index 9b0c3498..80154c49 100644 --- a/t/38-mysql-producer.t +++ b/t/38-mysql-producer.t @@ -19,7 +19,7 @@ use FindBin qw/$Bin/; #============================================================================= BEGIN { - maybe_plan(79, 'YAML', 'SQL::Translator::Producer::MySQL', 'Test::Differences',); + maybe_plan(81, 'YAML', 'SQL::Translator::Producer::MySQL', 'Test::Differences',); } use Test::Differences; use SQL::Translator; @@ -168,6 +168,26 @@ schema: - foo - bar - ba'z + g1: + name: g1 + data_type: int + size: 11 + order: 5 + is_not_null: 0 + extra: + generated: + expr: if((1 + 1 = 2),1,NULL) + type: STORED + g2: + name: g2 + data_type: int + size: 11 + order: 6 + is_not_null: 0 + extra: + generated: + expr: 1 + 1 + type: VIRTUAL indices: - type: NORMAL fields: @@ -227,6 +247,8 @@ EOSCHEMA `foo` integer NOT NULL, `foo2` integer NULL, `bar_set` set('foo', 'bar', 'ba''z') NULL, + `g1` integer(11) GENERATED ALWAYS AS (if((1 + 1 = 2),1,NULL)) STORED NULL, + `g2` integer(11) GENERATED ALWAYS AS (1 + 1) VIRTUAL NULL, INDEX `index_1` (`id`), INDEX `really_long_name_bigger_than_64_chars_aaaaaaaaaaaaaaaaa_aed44c47` (`id`), INDEX (`foo`), @@ -847,6 +869,35 @@ EOV ); } +{ + my $table = SQL::Translator::Schema::Table->new(name => 'table'); + my $options = { quote_table_names => '`' }; + + for my $type (qw/STORED VIRTUAL/) { + my $field = SQL::Translator::Schema::Field->new( + name => 'myfield', + table => $table, + data_type => 'int', + size => 11, + extra => { + generated => { + expr => 'if((1 + 1 = 2),1,NULL)', + type => $type + } + }, + is_nullable => 1, + is_foreign_key => 0, + is_unique => 0 + ); + + is( + SQL::Translator::Producer::MySQL::create_field($field, $options), + "`myfield` integer(11) GENERATED ALWAYS AS (if((1 + 1 = 2),1,NULL)) $type NULL", + 'valid generated field' + ); + } +} + { my $schema = SQL::Translator::Schema->new(); my $table = $schema->add_table(name => 'foo', fields => ['bar']);