#!/usr/bin/perl -w #convert sybase table DDL files to oracle #work in progress use strict; use Data::Dumper; use Text::Balanced qw(extract_bracketed); main(); sub main { for my $file (qw( cust_event cust cust_login cust_login_failed )) { convertTableFile("_test/syb/$file.sql", "_test/ora"); } return(1); } sub convertTableFile { my ($file, $dirDest) = @_; my $sql = loadFile($file) or die("Could not load SQL file ($file)\n"); my ($description, $nameTable, $defTable, $remainder) = aExtractTableDescNameDef($sql) or die("Could not extract table definition from sql ((($sql))) in file ($file)\n"); my @aField = aExtractFields($defTable) or die("Could not extract any fields from sql ((($sql))) in file ($file)\n"); my @aConstraint = aExtractConstraints($defTable); my @aIndex = aExtractIndexes($remainder); warnOnUnimplemented($sql, $file); #print "($nameTable)\n"; #print "(|||$description|||$nameTable|||$defTable|||$remainder|||)\n"; print $file, Dumper(\@aIndex); return(1); } =head2 aExtractTableDescNameDef($sql) Extract the table definition in $sql. Return array with (description, table name, table definition, reminder), or an empty array on errors. =cut sub aExtractTableDescNameDef { my ($sql) = @_; my ($description, $nameTable, $sqlReminder) = ($sql =~ m{( /\* .*? \*/ )? \s* create \s+ table \s+ ([\w.]+) \s* (.*) }xsi) or return(); $description ||= ""; $nameTable = normalizeName($nameTable); my ($def, $remainder) = extract_bracketed($sqlReminder, "()") or return(); return($description, $nameTable, $def, $remainder); } =head2 aExtractFields($sqlTableDef) Extract the fields in the $sqlTableDef. Return array with hash refs (keys: name, type, null, default, identity, comment), or an empty array on errors. Hash ref values may be undef for null, default, comment, identity. =cut sub aExtractFields { my ($sql) = @_; my @aField; for my $line(split(/\n/, $sql)) { #country varchar(20) null, #is_sessions_enabled bit default 0, #cust_id numeric(10, 0) not null, if($line =~ / \s* (\w+) \s+ #name ( #type \w+ #word ( #optional type specifier \( \d+ \s* #paren digits and ( #optional comma digits , \s* \d+ )? \) #closing paren )? ) \s* ( #option [^,]+ #everything up until the final comma )? , #comma \s* ( #comment -- ( .* #comment token and the rest of the line ) )? /x) { my $option = $5; my $rhField = { name => $1, type => $2, comment => $7, }; my $null = $1 if($option =~ /((not)? \s* null)/xi); $rhField->{null} = $null; my $default = $1 if($option =~ /(default \s+ (\w+))/xi); ##todo: works for numbers and NULL, not for strings yet. Implement when needed. $rhField->{default} = $default; my $identity = $1 if($option =~ /( \b identity \b )/xi); $rhField->{identity} = $identity; next if($rhField->{name} =~ /^constraint$/i); push(@aField, $rhField); } } return(@aField); } =head2 aExtractConstraints($sqlTableDef) Extract the constraints in the $sqlTableDef. Return array with hash refs (keys: name, type, index, raField, referencesTable, raReferencesField), or an empty array on errors. The key type is "index" or "foreign key". =cut sub aExtractConstraints { my ($sql) = @_; my @aConstraint; for my $line(split(/\n/, $sql)) { if($line =~ / constraint \s* ( \w+ ) \s+ #name ( #index ( primary \s+ key ) #PK | #or ( unique ) #unique | #or ( foreign \s+ key ) #FK ) \s* ( #clustered (optional) ( clustered | nonclustered ) \s* )? \( #fields ( [^)]+ ) #Everything within the paren \) \s* ( #references section, optional references \s+ ( #table name \w+ \s* #word ) ( #field list, optional \( #fields ( [^)]+ ) #Everything within the paren \) )? \s* )? /xi) { my $fields = $8; my $referencesFields = $12; my $rhConstraint = { type => "index", name => $1, index => $2, clustered => $6, raField => [], referencesTable => $10, raReferencesField => [], }; $rhConstraint->{raField} = raExtractFields($fields); $rhConstraint->{raReferencesField} = raExtractFields($referencesFields); $rhConstraint->{index} =~ s/\s+/ /gs; #Reduce whitespace $rhConstraint->{type} = "foreign key" if($rhConstraint->{index} =~ /foreign/i); push(@aConstraint, $rhConstraint) } } return(@aConstraint); } =head2 aExtractIndexes($sql) Extract the indexes in the $sql. Return array with hash refs (keys: name, index, raField, referencesTable, raReferencesField), or an empty array on errors. =cut sub aExtractIndexes { my ($sql) = @_; my @aIndex; while($sql =~ m/ create \s+ ( #unique, optionsl unique )? \s* ( #clustered, optional clustered | nonclustered )? \s* index \s+ ( #index name \w+ ) \s+ on \s+ ( #table name [\w.]+ ) \s* ( #field list, optional \( #fields ( [^)]+ ) #Everything within the paren \) \s* )? ( #with clause, optional, (only supports one option as implemented here) with \s+ ( \w+ ) \s* )? /gsxi ) { my $fields = $6; my $rhIndex = { name => $3, clustered => $2, unique => $1, table => $4, raField => [], with => $8, }; $rhIndex->{name} = normalizeName($rhIndex->{name}); $rhIndex->{table} = normalizeName($rhIndex->{table}); $rhIndex->{raField} = raExtractFields($fields); push(@aIndex, $rhIndex); } return(@aIndex); } =head2 raExtractFields($fields) Extract the field names in $fields and return array ref with the field names, or [] if none were found or if $fields is undef. Any words in $fields are the field names. =cut sub raExtractFields { my ($fields) = @_; $fields or return([]); return( [ $fields =~ /(\w+)/g ] ); } =head2 warnOnUnimplemented($sql, $file) warn for any occurrences of not yet supported SQL features in $sql. Return 1. =cut sub warnOnUnimplemented { my ($sql, $file) = @_; warn("WARNING: Unsupported ALTER TABLE found in file ($file)\n") if($sql =~ /alter \s+ table/xi); return(1); } =head2 normalizeName($name) Return normalized version of $name, e.g. with the user stripped ("dbo.cust" becomes "cust"). =cut sub normalizeName { my ($name) = @_; $name =~ s/^.*\.(\w+)$/$1/; #Remove any owner from the beginning of the name return($name); } sub loadFile { open(my $fh, "< $_[0]") or return(undef); local $/; return(<$fh>); } __END__