See More

#!/usr/bin/perl -w # This script takes a sql server SQL schema dump, and creates a postgresql dump # Optionnaly, if asked, generate a kettle job to transfer all data. This is done via -k dir # Details in the README file # This program is made to die on all error conditions: if there is something not understood in # a dump, it has to be added, or manually ignored (in order to improve this program rapidly) # Licence: GPLv3 # Copyright Marc Cousin, Dalibo use Getopt::Long; use Data::Dumper; use Cwd; use Encode::Guess; use Carp; use strict; # Global objects definition structure: we need it to store all seen tables, detect which ones have LOBS # and if their PK is a simple integer (so we can parallelize readings on these tables in a special # kettle transformation) # $objects will contain the parsed structure of the SQL Server dump # If you have to hack and want to understand its structure, just uncomment the call to Dumper() in the code # There is just too many things in it, and it evolves all the time my $objects; # These are global variables, from configuration file or command line arguments our ($sd, $sh, $si, $sp, $su, $sw, $pd, $ph, $pp, $pu, $pw); # Connection args our $conf_file; our $filename; # Filename passed as arg our $case_insensitive; # Passed as arg: was SQL Server installation case insensitive ? PostgreSQL can't ignore accents anyway # If yes, we will generate citext with CHECK constraints, that's the best we can do our $norelabel_dbo; # Passed as arg: should we convert DBO to public ? our $relabel_schemas; our $convert_numeric_to_int; # Should we convert numerics to int when possible ? (numeric (4,0) could be converted an int, for instance) our $kettle; our $before_file; our $after_file; our $unsure_file; our $case_treatment=1; # 1=convert to lowercase, 2=convert to snake_case, 0 do nothing our $ignore_errors; our $keep_identifier_case; our $camel_to_snake; our $validate_constraints; our $parallelism_in; our $parallelism_out; our $sort_size; our $use_pk_if_possible; our $pforce_ssl; # Will be set if we detect GIS objects our $requires_postgis=0; # These three variables are loaded in the BEGIN block at the end of this file (they are very big my $template; my $template_lob; my $incremental_template; my $incremental_template_sortable_pk; my ($job_header, $job_middle, $job_footer) ; # These are used to create the static parts of the job my ($job_entry, $job_hop) ; # These are used to create the dynamic parts of the job (XML file) my @view_list; # array to keep view ordering from sql server's dump (a view may depend on another view) # Opens the configuration file # Sets $sd $sh $si $sp $su $sw $pd $ph $pp $pu $pw when they are not set in the command line already # Also gets kettle parameters... sub parse_conf_file { # Correspondance between conf_file parameter and program variable # This is also used as the list of accepted parameters in the configuration file my %parameters = ('sql server database' => 'sd', 'sql server host' => 'sh', 'sql server host instance' => 'si', 'sql server port' => 'sp', 'sql server username' => 'su', 'sql server password' => 'sw', 'postgresql database' => 'pd', 'postgresql host' => 'ph', 'postgresql port' => 'pp', 'postgresql username' => 'pu', 'postgresql password' => 'pw', 'kettle directory' => 'kettle', 'parallelism_in' => 'parallelism_in', 'parallelism_out' => 'parallelism_out', 'before file' => 'before_file', 'after file' => 'after_file', 'unsure file' => 'unsure_file', 'sql server dump filename' => 'filename', 'case insensitive' => 'case_insensitive', 'no relabel dbo' => 'norelabel_dbo', 'convert numeric to int' => 'convert_numeric_to_int', 'relabel schemas' => 'relabel_schemas', 'keep identifier case' => 'keep_identifier_case', 'camelcasetosnake' => 'camel_to_snake', 'validate constraints' => 'validate_constraints', 'sort size' => 'sort_size', 'use pk if possible' => 'use_pk_if_possible', 'ignore errors' => 'ignore_errors', 'postgresql force ssl' => 'pforce_ssl', ); # Open the conf file or die open CONF, $conf_file or die "Cannot open $conf_file"; while (my $line = ) { $line =~ s/#.*//; # Remove comments $line =~ s/\s+=\s+/=/; # Remove whitespaces around the = $line =~ s/\s+$//; # Remove trailing whitespaces next if ($line =~ /^$/); # Empty line after comments have been removed $line =~ /^(.*?)=(.*)$/ or die "Cannot parse $line from $conf_file"; my ($param, $value) = ($1, $2); no strict 'refs'; # Using references by name, temporarily unless (defined $parameters{$param}) { die "Cannot understand parameter $param in $conf_file"; } my $param_name = $parameters{$param}; if (defined $$param_name) { next; # Parameter overriden in command line } $$param_name = $value; use strict 'refs'; } close CONF; } sub set_default_conf_values { # Hard coded default values, set only if not passed or found in configuration $case_insensitive=0 unless (defined ($case_insensitive)); $norelabel_dbo=0 unless (defined ($norelabel_dbo)); $convert_numeric_to_int=0 unless (defined ($convert_numeric_to_int)); $case_treatment=0 if (defined ($keep_identifier_case)); $case_treatment=2 if (defined ($camel_to_snake)); $parallelism_in=1 unless (defined ($parallelism_in));# the jdbc driver often errors when there are several sessions to sql server $parallelism_out=8 unless (defined ($parallelism_out)); $sort_size=10000 unless (defined ($sort_size)); $use_pk_if_possible=0 unless (defined ($use_pk_if_possible)); $validate_constraints='yes' unless (defined ($validate_constraints)); $ignore_errors=0 unless (defined ($ignore_errors)); # Default ports for PostgreSQL and SQL Server $pp=5432 unless (defined ($pp)); $sp=1433 unless (defined ($sp)); $pforce_ssl=0 unless (defined ($pforce_ssl)); } # Converts numeric(4,0) and similar to int, bigint, smallint sub convert_numeric_to_int { my ($qual) = @_; croak "not a good qualifier $qual" unless ($qual =~ /^(\d+),\s*(\d+)$/); my $precision = $1; my $scale = $2; croak "scale should be 0\n" unless ($scale eq '0'); return 'smallint' if ($precision <= 4); return 'integer' if ($precision <= 9); return 'bigint' if ($precision <= 18); return "numeric($qual)"; } # This is a list of the types that require a cast to be imported in kettle # C = using CREATE CAST # S = updating system catalog my %types_to_cast = ('uuid' => 'C','date' => 'C','timestamp with time zone' => 'C','xml' => 'S'); # This sub adds a cast (if not defined already) if # - we generate for kettle # - the passed type is in %types_to_cast sub add_cast { my ($type)=@_; if (defined $types_to_cast{$type}) { $objects->{CASTS}->{$type}=$types_to_cast{$type}; } } # These are the no-brainer conversions # There is still a special case for text types and case insensitivity (see convert_type) though my %types = ('int' => 'int', 'nvarchar' => 'varchar', 'nchar' => 'char', 'char' => 'char', 'varchar' => 'varchar', 'text' => 'text', 'char' => 'char', 'smallint' => 'smallint', 'tinyint' => 'smallint', 'bigint' => 'bigint', 'decimal' => 'numeric', 'float' => 'double precision', 'real' => 'real', 'date' => 'date', 'datetime' => 'timestamp', 'datetime2' => 'timestamp', 'smalldatetime' => 'timestamp', 'time' => 'time', 'timestamp' => 'bytea', 'rowversion' => 'bytea', 'datetimeoffset' => 'timestamp with time zone', 'image' => 'bytea', 'binary' => 'bytea', 'varbinary' => 'bytea', 'money' => 'numeric', 'smallmoney' => 'numeric(6,4)', 'uniqueidentifier' => 'uuid', 'xml' => 'xml',); # Types with no qualifier, and no point in putting one my %unqual = ('bytea' => 1, 'timestamp with time zone' => 1); # This function uses the two static lists above, plus domains and citext types that # may have been created during parsing, to convert mssql's types to pgsql's sub convert_type { my ($sqlstype, $sqlqual, $colname, $tablename, $typname, $schemaname) = @_; my $rettype; if (defined $types{$sqlstype}) { if ((defined $sqlqual and defined($unqual{$types{$sqlstype}})) or not defined $sqlqual) { # This is one of the few types that have to be unqualified (binary type) $rettype = $types{$sqlstype}; } elsif (defined $sqlqual) { $rettype = ($types{$sqlstype} . "($sqlqual)"); } } # A few special cases elsif ($sqlstype eq 'bit' and not defined $sqlqual) { $rettype = "boolean"; } elsif ($sqlstype eq 'ntext' and not defined $sqlqual) { $rettype = "text"; } elsif ($sqlstype eq 'numeric') { # Numeric is a special case: # No qualifier. We have to use numeric if (not $sqlqual) { $rettype='numeric'; } elsif ($sqlqual !~ /\d+,\s*0/) { $rettype="numeric($sqlqual)"; } elsif ( my $tmprettype=convert_numeric_to_int($sqlqual)) { $rettype=$tmprettype; } else { $rettype="numeric($sqlqual)"; } } elsif ($sqlstype eq 'sysname') { # Special case. This is an internal type, and should seldom be used in production. Converted to varchar(128) $rettype='varchar(128)'; } # We special case also the geometry and geography data types elsif ( $sqlstype =~ /^geography$|^geometry$/i ) { # These require that the destination database contains PostGIS unless ($requires_postgis) { print STDERR "WARNING: $sqlstype detected (in $schemaname.$tablename.$colname).\n You will need PostGIS (http://postgis.net/).\n The generated script will perform the CREATE EXTENSION, but please install PostGIS on this server\n"; $requires_postgis=1; } $rettype=lc($sqlstype); } elsif ($sqlstype eq 'sql_variant') { # There is no equivalent in PostgreSQL (and I think that's a good thing :) ) print STDERR "WARNING: $sqlstype detected (in $schemaname.$tablename.$colname).\n This is a 'not typed' field in SQL Server. There is no equivalent in PostgreSQL.\n This is converted to text, but you'll have rework to do on your client code\n"; $rettype='text'; } else { print "Types: ", Dumper(\%types); croak "Cannot determine the PostgreSQL's datatype corresponding to $sqlstype. This is a bug\n"; } # We special case when type is varchar, to be case insensitive if ($sqlstype =~ /text|varchar/ and $case_insensitive) { $rettype = "citext"; # Do we have a SQL qualifier ? (we'll have to do check constraints then) if ($sqlqual) { # Check we have a table name and a colname, or a typname if ( defined $colname and defined $tablename ) # We are called from a CREATE TABLE, we have to add a check constraint { my $constraint; $constraint->{TYPE} = 'CHECK_CITEXT'; $constraint->{TABLE} = $tablename; $constraint->{TEXT} = "char_length(" . format_identifier($colname) . ") <= $sqlqual"; push @{$objects->{SCHEMAS}->{$schemaname}->{TABLES}->{$tablename} ->{CONSTRAINTS}}, ($constraint); } elsif (defined $typname ) # We are called from a CREATE TYPE, which will be converted to a CREATE DOMAIN { $rettype = "citext CHECK(char_length(value)<=$sqlqual)"; } else { die "Called in a case sensitive, trying to generate a check constraint, failed. This is a bug!"; } } } # We special case SQL Server's TABLE types: they should be converted into an array if ( $sqlstype =~ /^(\S+)\.(\S+)$/) { # This is a namespaced type. So we check if this is a special array my ($schema,$type)=($1,$2); if (defined($objects->{SCHEMAS}->{$schema}->{TABLE_TYPES}->{$type})) { $rettype=$rettype.'[]'; } } # Add this type to casts to perform if necessary add_cast($rettype); return $rettype; } # This function is used for selects from SQL Server, in kettle. It adds a function call # if there is a conversion to be done. # uniqueidentifier is upper case in SQL Server, whereas uuid is lower case in PG # date is converted to varchar in the YYYY-MM-DD format # timestamp with time zone is converted to varchar in the YYYY-MM-DD HH:MI:SS.MMM (24h) with time zone format # xml columns with empty values will be converted to null since the empty values won't be accepted in PG (datalength of an empty xml column is 5) sub sql_convert_column { my ($colname,$coltype)=@_; my %functions = ( 'uuid' => 'lower({colname})', 'date' => 'convert(varchar(50), {colname}, 120)', 'timestamp with time zone(7)' => 'convert(varchar(50), {colname}, 121)', 'xml' => 'case when datalength({colname}) > 5 then {colname} else null end'); if (defined ($functions{$coltype})) { my $tmpcol=$functions{$coltype}; $tmpcol =~ s/\{colname\}/[$colname]/g; return $tmpcol; } else { return "[$colname]"; } } # This function is used for selects from PostgreSQL, in kettle. It adds a function call # if there is a conversion to be done. # uuid is converted to varchar and forced to lower case # date is converted to varchar in the YYYY-MM-DD format # timestamp with time zone is converted to varchar in the YYYY-MM-DD HH:MI:SS.US+00 format (UTC) sub postgres_convert_column { my ($colname,$coltype)=@_; my %functions = ( 'uuid' => 'lower(cast({colname} as varchar))', 'date' => 'to_char({colname}, \'YYYY-MM-DD\')', 'timestamp with time zone' => 'to_char({colname} AT TIME ZONE \'UTC\', \'YYYY-MM-DD HH:MI:SS.US+00\')'); if (defined ($functions{$coltype})) { my $tmpcol = $functions{$coltype}; $tmpcol =~ s/\{colname\}/"$colname"/; return $tmpcol; } else { return "\"$colname\""; } } # This is used to convert camelCase to snake_case. The latter is more usual with PostgreSQL sub camel_to_snake { my ($string)=@_; $string =~ s/([[:lower:]])([[:upper:]]+)/$1_\l$2/g; $string=lc($string); return $string; } # This function is used to determine if a PK will be sorted the same in SQL Server and PG # It means that it doesn't depend on collation orders or other internals. # For now, only numeric and date data types are considered OK # Used for incremental jobs, to know if we can ask the databases to send us pre-sorted data # We also filter on $use_pk_if_possible sub is_pk_sort_order_safe { my ($schema,$table)=@_; my %safe_types = ('numeric' => 1, 'int' => 1, 'bigint' => 1, 'smallint' => 1, 'real' => 1, 'double precision' => 1, 'date' => 1, 'timestamp' => 1, ); return 0 unless (defined $objects->{SCHEMAS}->{$schema}->{TABLES}->{$table}->{PK}); # There is no PK return 0 unless ($use_pk_if_possible =~ /\b${schema}\.${table}\b/i or $use_pk_if_possible eq '1'); my $pk=$objects->{SCHEMAS}->{$schema}->{TABLES}->{$table}->{PK}; my $isok=1; # It is OK as long as all types are in %safe_type foreach my $col (@{$pk->{COLS}}) { $isok=0 unless defined ($safe_types{$objects->{SCHEMAS}->{$schema}->{TABLES}->{$table}->{COLS}->{$col}->{TYPE}}); } return $isok; } # This function renames the identifiers sub rename_identifier { my ($identifier)=@_; if ($case_treatment==1) { $identifier=lc($identifier); } elsif ($case_treatment==2) { $identifier=camel_to_snake($identifier); } return $identifier; } # This function formats the identifiers (object name), putting double quotes around it # It also converts case if asked sub format_identifier { my ($identifier)=@_; croak "identifier not defined in format_identifier" unless (defined $identifier); $identifier=rename_identifier($identifier); # Now, we protect the identifier (similar to quote_ident in PG) $identifier=~ s/"/""/g; $identifier='"'.$identifier.'"'; return $identifier; } # This is a bit of a ugly hack: for indexes, in the column definition, there may be ASC/DESC at the end # Instead of changing the whole structure of the code, just detect this asc/desc and split it before calling format_identifier sub format_identifier_cols_index { my ($idx_identifier)=@_; $idx_identifier =~ /^(.*?)(?: (ASC|DESC))?$/; my ($col,$order)=($1,$2); my $formatted=format_identifier($col); return $formatted unless defined ($order); return $formatted . ' ' . $order; } # This one will try to convert what can obviously be converted from transact to (or embedded WHERE in indexes for instance) PG # Things such as getdate() which can become CURRENT_TIMESTAMP sub convert_transactsql_code { my ($code)=@_; if ($case_treatment==0) { $code =~ s/[\[\]]/"/gi; # Bit brutal probably } else { $code =~ s/\[(.*)\]/rename_identifier($1)/gie; # Bit brutal probably } $code =~ s/getdate\s*\(\)/CURRENT_TIMESTAMP/gi; $code =~ s/user_name\s*\(\)/CURRENT_USER/gi; $code =~ s/datepart\s*\(\s*(.*?)\s*\,\s*(.*?)\s*\)/date_part('$1', $2)/gi; return $code; } # This function does its best to convert MS's weird default values syntax into something logical sub store_default_value { my ($schema,$table,$col,$value,$line)=@_; if ($value =~ /^\(?(\d+(\.\d+)?)\)?$/) # Value is numeric { $value = $1; # Get rid of parenthesis if ($objects->{SCHEMAS}->{relabel_schemas($schema)}->{TABLES}->{$table}->{COLS}->{$col}->{TYPE} eq 'boolean') { # Ok, it IS a boolean, and we have received a number if ($value eq '0') { $objects->{SCHEMAS}->{$schema}->{TABLES}->{$table}->{COLS}->{$col}->{DEFAULT}->{VALUE} = 'false'; } elsif ($value eq '1') { $objects->{SCHEMAS}->{$schema}->{TABLES}->{$table}->{COLS}->{$col}->{DEFAULT}->{VALUE} = 'true'; } else { # We should not get here: we have a numeric which isn't 0 or 1, and is supposed to be a boolean die "Got an unexpected boolean : $value, for line $line\n"; } } else { $objects->{SCHEMAS}->{$schema}->{TABLES}->{$table}->{COLS}->{$col}->{DEFAULT}->{VALUE} = $value; } $objects->{SCHEMAS}->{$schema}->{TABLES}->{$table}->{COLS}->{$col}->{DEFAULT}->{UNSURE} = 0; } elsif ($value =~ /^NULL$/) # A NULL value { # NULL WITHOUT quotes around it ! $objects->{SCHEMAS}->{$schema}->{TABLES}->{$table}->{COLS}->{$col}->{DEFAULT}->{VALUE} = 'NULL'; $objects->{SCHEMAS}->{$schema}->{TABLES}->{$table}->{COLS}->{$col}->{DEFAULT}->{UNSURE} = 0; } elsif ($value =~ /^N?'(.*)'$/) # There is sometimes an N before a string. { $value = $1; # Get rid of junk # Default text value, text, between commas $objects->{SCHEMAS}->{$schema}->{TABLES}->{$table}->{COLS}->{$col}->{DEFAULT}->{VALUE} = "'$1'"; $objects->{SCHEMAS}->{$schema}->{TABLES}->{$table}->{COLS}->{$col}->{DEFAULT}->{UNSURE} = 0; } else { #This must be a function call... $objects->{SCHEMAS}->{$schema}->{TABLES}->{$table}->{COLS}->{$col}->{DEFAULT}->{VALUE} = convert_transactsql_code($value); $objects->{SCHEMAS}->{$schema}->{TABLES}->{$table}->{COLS}->{$col}->{DEFAULT}->{UNSURE} = 1; } } # This gives the next column position for a table # It is used when we add a new column # These tables are added at the end of the table, in %objects sub next_col_pos { my ($schema, $table) = @_; if (defined $objects->{SCHEMAS}->{$schema}->{TABLES}->{$table}->{COLS}) { my $max = 0; foreach my $col ( values(%{$objects->{SCHEMAS}->{$schema}->{TABLES}->{$table}->{COLS}})) { if ($col->{POS} > $max) { $max = $col->{POS}; } } return $max + 1; } elsif (defined $objects->{SCHEMAS}->{$schema}->{TABLES}->{$table}) { # First column return 1; } else { die "We tried to add a column to an unknown table"; } } { # This builds %relabel_schemas for use in the next function. Both are scoped so that %relabel_schemas is not visible from outside my %relabel_schemas; sub build_relabel_schemas { # Don't forget dbo -> public if it was asked (default) unless ($norelabel_dbo) { $relabel_schemas{'dbo'}='public'; } # dbo can be overwritten in relabel_schema (the user will probably forget to deactivate the relabel). # so we do the real relabeling after the norelabel_dbo, to overwrite if (defined $relabel_schemas) { foreach my $pair (split (';',$relabel_schemas)) { my @pair=split('=>',$pair); unless (scalar(@pair)==2) { die "Cannot parse the schema list given as argument: <$relabel_schemas>\n"; } $relabel_schemas{$pair[0]}=$pair[1]; } } } # This relabels the schemas sub relabel_schemas { my ($schema) = @_; return $schema unless (defined $relabel_schemas{$schema}); return $relabel_schemas{$schema}; } } # Test if we are on windows. We will have to convert / to \ in the XML files sub is_windows { if ($^O =~ /MSWin32/) { return 1; } return 0; } # Die if kettle is not set up correctly sub kettle_warn { my ($file) = @_; print STDERR "Important WARNING: You have to set up KETTLE_EMPTY_STRING_DIFFERS_FROM_NULL=Y in $file.\nIf this file doesn't exist yet, start spoon from the kettle directory once."; } # This sub checks ~/.kettle/kettle.properties to be sure # KETTLE_EMPTY_STRING_DIFFERS_FROM_NULL=Y is in place # We die if not sub check_kettle_properties { my $ok = 0; my $file; if (!is_windows()) { $file = $ENV{'HOME'} . '/.kettle/kettle.properties'; } else { $file = $ENV{'USERPROFILE'} . '/.kettle/kettle.properties'; } unless (open FILE, $file ) { kettle_warn($file); return 1; } while () { next unless (/^KETTLE_EMPTY_STRING_DIFFERS_FROM_NULL=Y$/); $ok = 1; } close FILE; if (not $ok) { kettle_warn($file); return 1; } return 0; } # Usage, obviously. Has to be kept in sync with new command line options sub usage { print "$0 [-k kettle_output_directory] -b before_file -a after_file -u unsure_file -f sql_server_schema_file[-h] [-i]\n"; print "\nExpects a SQL Server SQL structure dump as -f (preferably unicode)\n"; print "-conf uses a conf file. All options below can also be set there. Command line options will overwrite conf options\n"; print "-i tells $0 to create a case-insensitive PostgreSQL schema\n"; print "-nr tells $0 not to convert the dbo schema to public. dbo will stay dbo\n"; print "-num tells $0 to convert numeric xxx,0 to int, bigint, etc. Will not keep numeric scale and precision for the converted\n"; print "-relabel_schemas gives a list of schemas to rename. For instance -relabel_schemas 'source1=>dest1;source2=>dest2'\n"; print " -nr simply cancels the default dbo=>public remapping. Don't forget to put the remapping between quotes\n"; print "-keep_identifier_case tells $0 to keep the case of sql server database objects (not advised). Default is to lowercase everything.\n"; print "-camel_to_snake tells $0 to convert the object names from camelCase to camel_case, which is more often used in PostgreSQL.\nDon't use this unless you are ready to do changes in the client.\n"; print "before_file contains the structure\n"; print "after_file contains index, constraints\n"; print "validate_constraints validates the constraints that have been created\n"; print "sort_size will change size of sort batch for the incremental job. Too small and it will be slow, too big and you will get Java Out of Heap Memory errors.\n"; print "sort_size is 10000, which is very low, to try to avoid problems. First, raise java heap memory (in the kitchen script), then try higher values if you need more speed\n"; print "use_pk_if_possible is false (0) by default. You can put it to 1 (true), or give a comma separated list of tables (with schema). Compared case insensitively\n"; print "unsure_file contains things we cannot guarantee will work, such as views\n"; print "\n"; print "If you are generating for kettle, you'll need to provide connection information\n"; print "for connecting to both databases:\n"; print "-sd: sqlserver database\n"; print "-sh: sqlserver host\n"; print "-si: sqlserver host instance\n"; print "-sp: sqlserver port\n"; print "-su: sqlserver username\n"; print "-sw: sqlserver password\n"; print "-pd: postgresql database\n"; print "-ph: postgresql host\n"; print "-pp: postgresql port\n"; print "-pu: postgresql username\n"; print "-pw: postgresql password\n"; print "-pi: parallelism level for the kettle job (input, sql server)\n"; print "-po: parallelism level for the kettle job (output, postgresql)\n"; print "-pforce_ssl: force a SSL session to PostgreSQL\n"; print "You may also choose to ignore insert errors (inserting will be much slower)\n"; print "-ignore_errors\n"; } # This function generates kettle transformations, and a kettle job running all these # transformations sequentially, for all the tables, in all the schemas, in sql server's dump sub generate_kettle { my ($dir) = @_; # first, create the kettle directory unless (-d $dir) { mkdir($dir) or die "Cannot create $dir"; } # For each table in each schema in $objects, we generate a kettle file in the directory # We also create an incremental transformation foreach my $schema (sort keys %{$objects->{SCHEMAS}}) { my $refschema = $objects->{SCHEMAS}->{$schema}; my $targetschema = $schema; foreach my $table (sort keys %{$refschema->{TABLES}}) { my $origschema=$refschema->{TABLES}->{$table}->{origschema}; # First, does this table have LOBs ? The template depends on this and is this # table having an int PK ? my $newtemplate; if ( $refschema->{TABLES}->{$table}->{haslobs} and defined($refschema->{TABLES}->{$table}->{PK}->{COLS}) and scalar(@{$refschema->{TABLES}->{$table}->{PK}->{COLS}}) == 1 and ($refschema->{TABLES}->{$table}->{COLS}->{($refschema->{TABLES}->{$table}->{PK}->{COLS}->[0])}->{TYPE} =~ /int$/) ) { my $wherefilter; $newtemplate = $template_lob; $wherefilter = 'WHERE ' . $refschema->{TABLES}->{$table}->{PK}->{COLS}->[0] . '% ${Internal.Step.Unique.Count} = ${Internal.Step.Unique.Number}'; $newtemplate =~ s/__sqlserver_where_filter__/$wherefilter/; } else { $newtemplate = $template; } # We have a similar question for incremental jobs: can we use the primary key ? # We obviously need a primary key, and we need the sort order to be the same in both databases my $newincrementaltemplate; if (is_pk_sort_order_safe($schema,$table)) { $newincrementaltemplate=$incremental_template_sortable_pk; my $collist=join(',',@{$refschema->{TABLES}->{$table}->{PK}->{COLS}}); $newincrementaltemplate =~ s/__sqlserver_pk_condition__/$collist/g; $newincrementaltemplate =~ s/__pg_pk_condition__/$collist/g; } else { $newincrementaltemplate=$incremental_template; } # Build the column list of the table to put into the SQL Server query my @colsdef; my @pgcolsdef; foreach my $col ( sort { $refschema->{TABLES}->{$table}->{COLS}->{$a}->{POS} <=> $refschema->{TABLES}->{$table}->{COLS}->{$b} ->{POS} } (keys %{$refschema->{TABLES}->{$table}->{COLS}})) { my $coldef = sql_convert_column($col,$refschema->{TABLES}->{$table}->{COLS}->{$col}->{TYPE}) . " AS " . format_identifier($col); my $pgcoldef = postgres_convert_column($col,$refschema->{TABLES}->{$table}->{COLS}->{$col}->{TYPE}) . " AS " . format_identifier($col); push @colsdef,($coldef); push @pgcolsdef,($pgcoldef); } my $colsdef=join(',',@colsdef); my $pgcolsdef=join(',',@pgcolsdef); my $pgtable=format_identifier($table); my $pgschema=format_identifier($targetschema); my $sqlinstancename = ''; if (defined $si) { $sqlinstancename = $si; } # Substitute every connection placeholder with the real value. We do this for both templates $newtemplate =~ s/__sqlserver_database__/$sd/g; $newtemplate =~ s/__sqlserver_database__/$sd/g; $newtemplate =~ s/__sqlserver_host__/$sh/g; $newtemplate =~ s/__sqlserver_port__/$sp/g; $newtemplate =~ s/__sqlserver_instance__/$sqlinstancename/g; $newtemplate =~ s/__sqlserver_username__/$su/g; $newtemplate =~ s/__sqlserver_password__/$sw/g; $newtemplate =~ s/__postgres_database__/$pd/g; $newtemplate =~ s/__postgres_host__/$ph/g; $newtemplate =~ s/__postgres_port__/$pp/g; $newtemplate =~ s/__postgres_username__/$pu/g; $newtemplate =~ s/__postgres_password__/$pw/g; $newtemplate =~ s/__sqlserver_table_name__/[$origschema].[$table]/g; $newtemplate =~ s/__sqlserver_table_cols__/$colsdef/g; $newtemplate =~ s/__postgres_table_name__/$pgtable/g; $newtemplate =~ s/__postgres_schema_name__/$pgschema/g; $newtemplate =~ s/__PARALLELISM_IN__/$parallelism_in/g; $newtemplate =~ s/__PARALLELISM_OUT__/$parallelism_out/g; if ($ignore_errors) { $newtemplate =~ s/N<\/ignore_errors>/Y<\/ignore_errors>/g; $newtemplate =~ s/Y<\/use_batch>/N<\/use_batch>/g; # Cannot use batch mode with ignore errors } if ($pforce_ssl) { $newtemplate =~ s/__pforce_ssl__/EXTRA_OPTION_POSTGRESQL.ssl<\/code>true<\/attribute><\/attribute>\nEXTRA_OPTION_POSTGRESQL.sslfactory<\/code>org.postgresql.ssl.NonValidatingFactory<\/attribute><\/attribute>/g; } else { $newtemplate =~ s/__pforce_ssl__//g; } $newincrementaltemplate =~ s/__sqlserver_database__/$sd/g; $newincrementaltemplate =~ s/__sqlserver_database__/$sd/g; $newincrementaltemplate =~ s/__sqlserver_host__/$sh/g; $newincrementaltemplate =~ s/__sqlserver_port__/$sp/g; $newincrementaltemplate =~ s/__sqlserver_username__/$su/g; $newincrementaltemplate =~ s/__sqlserver_password__/$sw/g; $newincrementaltemplate =~ s/__postgres_database__/$pd/g; $newincrementaltemplate =~ s/__sqlserver_instance__/$sqlinstancename/g; $newincrementaltemplate =~ s/__postgres_host__/$ph/g; $newincrementaltemplate =~ s/__postgres_port__/$pp/g; $newincrementaltemplate =~ s/__postgres_username__/$pu/g; $newincrementaltemplate =~ s/__postgres_password__/$pw/g; $newincrementaltemplate =~ s/__sqlserver_table_name__/[$origschema].[$table]/g; $newincrementaltemplate =~ s/__sqlserver_table_cols__/$colsdef/g; $newincrementaltemplate =~ s/__postgres_table_name__/$pgtable/g; $newincrementaltemplate =~ s/__postgres_schema_name__/$pgschema/g; $newincrementaltemplate =~ s/__postgres_table_cols__/$pgcolsdef/g; $newincrementaltemplate =~ s/__PARALLELISM_IN__/$parallelism_in/g; $newincrementaltemplate =~ s/__PARALLELISM_OUT__/$parallelism_out/g; $newincrementaltemplate =~ s/__sort_size__/$sort_size/g; if ($pforce_ssl) { $newincrementaltemplate =~ s/__pforce_ssl__/EXTRA_OPTION_POSTGRESQL.ssl<\/code>true<\/attribute><\/attribute>\nEXTRA_OPTION_POSTGRESQL.sslfactory<\/code>org.postgresql.ssl.NonValidatingFactory<\/attribute><\/attribute>/g; } else { $newincrementaltemplate =~ s/__pforce_ssl__//g; } # We have a bit of work to do on primary keys for the incremental template: we need them # to compare the tables… if (defined($refschema->{TABLES}->{$table}->{PK}->{COLS})) { my @pk=@{$refschema->{TABLES}->{$table}->{PK}->{COLS}}; my $keys; foreach my $pk(@pk) { $keys.="$pk\n"; } $newincrementaltemplate =~ s/__KEYS_MERGE__/$keys/g; my $sortkeys=''; my $synckeys=''; foreach my $pk(@pk) { $sortkeys.="\n$pk\nY\nY\n\n"; my $outcol=$pk; $outcol=rename_identifier($outcol); $synckeys.="\n$pk\n$outcol\n=\n\n\n"; } $newincrementaltemplate =~ s/__SORT_KEYS_SQLSERVER__/$sortkeys/g; $newincrementaltemplate =~ s/__SORT_KEYS_PG__/$sortkeys/g; $newincrementaltemplate =~ s/__KEYS_SYNC__/$synckeys/g; # We also need to tell the merge step to compare all columns my $valuesmerge=''; my $valuessync=''; foreach my $colname (keys(%{$refschema->{TABLES}->{$table}->{COLS}})) { # Is it a member of the PK ? If yes, no need to use it for comparison #FIXME: unless (scalar(grep(/^${colname}$/,@pk))) # Does grep find an element in the array matching colname ? # { $valuesmerge.="$colname\n"; # we need to use the correct case for postgresql output my $outcol=$colname; if ($case_treatment==1) { $outcol=lc($outcol); } elsif ($case_treatment==2) { $outcol=camel_to_snake($outcol); } $valuessync.="\n$outcol\n$colname\nY\n\n"; # } } $newincrementaltemplate =~ s/__VALUES_MERGE__/$valuesmerge/g; $newincrementaltemplate =~ s/__VALUES_SYNC__/$valuessync/g; # Produce the incremental transformation open FILE, ">$dir/incremental-$schema-$table.ktr" or die "Cannot write to $dir/incremental-$schema-$table.ktr"; binmode(FILE,":utf8"); print FILE $newincrementaltemplate; close FILE; } else { print STDERR "$schema/$table has no PK. Cannot create an incremental transformation\n"; } # Store this new transformation into its file open FILE, ">$dir/$schema-$table.ktr" or die "Cannot write to $dir/$schema-$table.ktr"; binmode(FILE,":utf8"); print FILE $newtemplate; close FILE; } } # All transformations are done # We have to create a job to launch everything in one go # We also create an incremental job. This incremental job # first deactivates all constraints (with triggers), then # runs all incremental jobs, and "standard" jobs for all those # tables where we cannot do incremental (no PK...) open JOBFILE, ">$dir/migration.kjb" or die "Cannot write to $dir/migration.kjb"; open INCFILE, ">$dir/incremental.kjb" or die "Cannot write to $dir/incremental.kjb"; my $real_dir = getcwd; my $entries = ''; my $incentries = ''; my $hops = ''; my $prev_node = 'SQL SCRIPT START'; # $cur_vert_pso is not that useful, it's just not to be ugly if someone wanted to open # the job with spoon (kettle's gui) and work on it graphically my $cur_vert_pos = 100; # First : the hop from START to SQL SCRIPT START my $tmp_hop = $job_hop; $tmp_hop =~ s/__table_1__/START/; $tmp_hop =~ s/__table_2__/SQL SCRIPT START/; # This is the first hop, so it is unconditionnal $tmp_hop =~ s/N<\/unconditional>/Y<\/unconditional>/; $hops.=$tmp_hop; # We sort only so that it will be easier to find a transformation in the job if one needed to # edit it. It's also easier to track progress if tables are sorted alphabetically foreach my $schema (sort keys %{$objects->{SCHEMAS}}) { my $refschema = $objects->{SCHEMAS}->{$schema}; foreach my $table (sort { lc($a) cmp lc($b) } keys %{$refschema->{TABLES}}) { my $tmp_entry = $job_entry; # We build the entries with regexp substitutions. The tablename contains the schema $tmp_entry =~ s/__table_name__/${schema}_${table}/; $tmp_entry =~ s/__y_loc__/$cur_vert_pos/; # JOBFILEname to use. We need the full path to the transformations # The only difference between normal and incremental job is the filename of the transformation my $JOBFILEname; my $INCJOBFILEname; if ($dir =~ /^([A-Za-z]:\\|\/)/) # Absolute path { $JOBFILEname = $dir . '/' . $schema . '-' . $table . '.ktr'; $INCJOBFILEname = $dir . '/' . 'incremental-' . $schema . '-' . $table . '.ktr'; } else { $JOBFILEname = $real_dir . '/' . $dir . '/' . $schema . '-' . $table . '.ktr'; $INCJOBFILEname = $real_dir . '/' . $dir . '/' . 'incremental-' . $schema . '-' . $table . '.ktr'; } # Does the incremental transformation exist ? unless (-e $INCJOBFILEname) { $INCJOBFILEname=$JOBFILEname; } # Different for windows and linux, obviously: we change / to \ for windows unless (is_windows()) { $JOBFILEname =~ s/\////g; $INCJOBFILEname =~ s/\////g; } else { $JOBFILEname =~ s/\//\\/g; $INCJOBFILEname =~ s/\//\\/g; } my $inctmp_entry=$tmp_entry; $tmp_entry =~ s/__file_name__/$JOBFILEname/; $inctmp_entry =~ s/__file_name__/$INCJOBFILEname/; $entries .= $tmp_entry; $incentries.=$inctmp_entry; # We build the hop with the regexp too my $tmp_hop = $job_hop; $tmp_hop =~ s/__table_1__/$prev_node/; $tmp_hop =~ s/__table_2__/${schema}_${table}/; $hops .= $tmp_hop; # We increment everything for next loop $prev_node = "${schema}_${table}"; # For the next hop $cur_vert_pos += 80; # To be pretty in spoon } } # Put the final hop $tmp_hop = $job_hop; $tmp_hop =~ s/__table_1__/$prev_node/; $tmp_hop =~ s/__table_2__/SQL SCRIPT END/; $hops .= $tmp_hop; # Build the casts in the start/stop job entries my $beforescript; my $afterscript; if (defined ($objects->{CASTS})) { foreach my $cast (keys %{$objects->{CASTS}}) { if ($objects->{CASTS}->{$cast} eq "C") { $beforescript.= "DROP CAST IF EXISTS (varchar as $cast);\n"; $beforescript.= "CREATE CAST (varchar as $cast) with inout as implicit;\n"; $afterscript.= "DROP CAST (varchar as $cast);\n"; } elsif ($objects->{CASTS}->{$cast} eq "S") { $beforescript.= "UPDATE pg_cast SET castcontext='i' WHERE castsource='character varying'::regtype AND casttarget='$cast'::regtype;\n"; $afterscript.= "UPDATE pg_cast SET castcontext='e' WHERE castsource='character varying'::regtype AND casttarget='$cast'::regtype;\n"; } } } # Remove/restore triggers to be able to insert without FK checks foreach my $schema (sort keys %{$objects->{SCHEMAS}}) { my $refschema = $objects->{SCHEMAS}->{$schema}; foreach my $table (sort { lc($a) cmp lc($b) } keys %{$refschema->{TABLES}}) { $beforescript.= "ALTER TABLE " . format_identifier($schema) . '.' . format_identifier($table) . " DISABLE TRIGGER ALL;\n"; $afterscript.= "ALTER TABLE " . format_identifier($schema) . '.' . format_identifier($table) . " ENABLE TRIGGER ALL;\n"; } } # This is for the SQL Scripts. We also need to specify the PG connection $job_header =~ s/__SQL_SCRIPT_INIT__/$beforescript/g; $job_header =~ s/__SQL_SCRIPT_END__/$afterscript/g; $job_header =~ s/__postgres_database__/$pd/g; $job_header =~ s/__postgres_host__/$ph/g; $job_header =~ s/__postgres_port__/$pp/g; $job_header =~ s/__postgres_username__/$pu/g; $job_header =~ s/__postgres_password__/$pw/g; print JOBFILE $job_header; print JOBFILE $entries; print JOBFILE $job_middle; print JOBFILE $hops; print JOBFILE $job_footer; close JOBFILE; print INCFILE $job_header; print INCFILE $incentries; print INCFILE $job_middle; print INCFILE $hops; print INCFILE $job_footer; close INCFILE; } # sql server's dump may contain multiline C style comments (/* */) # This sub reads a line and cleans it up, removing comments, \r, exec sp_executesql,... # It takes into account the status (in or out of comment) of the previous line, hence # the scoped $in_comment { my $in_comment = 0; sub read_and_clean { my ($fd) = @_; my $line = <$fd>; return undef if (not defined $line); $line =~ s/\r//g; # Remove \r from windows output $line =~ s/EXEC(ute)?\s*(dbo|sys)\.sp_executesql( \@statement =)? N'//i ; # Remove executesql… it's a bit weird in the SQL Server's dump # If we are not in comment, we look for /* # If we are in comment, we look for */, and we remove everything until */ if ($in_comment) { if ($line =~ /\*\//) { $in_comment = 0; $line =~ s/.*\*\///; # Remove everything before the uncomment } else { $line = "\n"; } } if (not $in_comment) { # We first remove all one-line only comments (there may be several on this line) $line =~ s/\/\*.*?\*\///g; # Is there a comment left ? if ($line =~ /\/\*/) { $in_comment = 1; $line =~ s/\/\*.*//; # Remove everything after the comment } } return $line; } } # This adds a column we just read to a table sub add_column_to_table { my ($schemaname,$tablename,$colname,$coltypeschema,$coltype,$colqual,$isidentity,$colisnull)=@_; my $colnumber=next_col_pos($schemaname,$tablename); if (defined $coltypeschema) { # The datatype is a user defined datatype # It has already been declared before. We just need to find it $coltype = relabel_schemas($coltypeschema) . '.' . $coltype; } if ($colqual) { if ($coltype eq 'xml') { $colqual = undef ; # ignoring sql server xml schema since its not supported in pg } elsif ($colqual eq '(max)') { $colqual = undef ; # max in sql server is the same as putting no colqual in pg } else { # We need the number (or 2 numbers) in this qual $colqual =~ /\((\d+(?:,\s*\d+)?)\)/ or die "Cannot parse colqual <$colqual>"; $colqual = "$1"; } } my $newtype = convert_type($coltype, $colqual, $colname, $tablename, undef, $schemaname); # If it is an identity, we'll map to serial/bigserial (create a sequence, then link it # to the column) if ($isidentity) { # We have an identity field. We remember the default value and # initialize the sequence correctly in the after script $isidentity =~ /IDENTITY\s*\((\d+),\s*(\d+)\)/ or die "Cannot understand <$isidentity>"; my $startseq = $1; my $stepseq = $2; my $seqname = lc("${tablename}_${colname}_seq"); # We get a sure default value. $objects->{SCHEMAS}->{$schemaname}->{TABLES}->{$tablename}->{COLS} ->{$colname}->{DEFAULT}->{VALUE} = "nextval('" . format_identifier(relabel_schemas(${schemaname})) . '.' . format_identifier(${seqname}) . "')"; $objects->{SCHEMAS}->{$schemaname}->{TABLES}->{$tablename}->{COLS} ->{$colname}->{DEFAULT}->{UNSURE} = 0; $objects->{SCHEMAS}->{$schemaname}->{SEQUENCES}->{$seqname}->{START} = $startseq; $objects->{SCHEMAS}->{$schemaname}->{SEQUENCES}->{$seqname}->{MIN} = $startseq; $objects->{SCHEMAS}->{$schemaname}->{SEQUENCES}->{$seqname}->{STEP} = $stepseq; $objects->{SCHEMAS}->{$schemaname}->{SEQUENCES}->{$seqname} ->{OWNERTABLE} = $tablename; $objects->{SCHEMAS}->{$schemaname}->{SEQUENCES}->{$seqname} ->{OWNERCOL} = $colname; $objects->{SCHEMAS}->{$schemaname}->{SEQUENCES}->{$seqname} ->{OWNERSCHEMA} = $schemaname; } # If there is a bytea generated, this table will contain a blob: # use a special kettle transformation for it if generating kettle # (see generate_kettle() ) if ( $newtype eq 'bytea' or $coltype eq 'ntext') # Ntext is very slow, stored out of page { $objects->{SCHEMAS}->{$schemaname}->{'TABLES'}->{$tablename} ->{haslobs} = 1; } $objects->{SCHEMAS}->{$schemaname}->{'TABLES'}->{$tablename}->{COLS} ->{$colname}->{POS} = $colnumber; $objects->{SCHEMAS}->{$schemaname}->{'TABLES'}->{$tablename}->{COLS} ->{$colname}->{TYPE} = $newtype; if ($colisnull eq 'NOT NULL') { $objects->{SCHEMAS}->{$schemaname}->{'TABLES'}->{$tablename}->{COLS} ->{$colname}->{NOT_NULL} = 1; } else { $objects->{SCHEMAS}->{$schemaname}->{'TABLES'}->{$tablename}->{COLS} ->{$colname}->{NOT_NULL} = 0; } } # Reads the dump passed as -f # Generates the $object structure # That's THE MAIN FUNCTION sub parse_dump { # Open the input file or die. This first pass is to detect encoding, and open it correctly afterwards my $data; my $file; open $file, "<$filename" or die "Cannot open $filename"; while (my $line = <$file>) { $data .= $line; } close $file; # We now ask guess... my $decoder = guess_encoding($data, qw/iso8859-15/); die $decoder unless ref($decoder); # If we got to here, it means we have found the right decoder # or at least, perl thinks it has :) open $file, "<:encoding(" . $decoder->name . ")", $filename or die "Cannot open $filename"; # Tagged because sql statements are often multi-line, so there are inner loops in some conditions MAIN: while (my $line = read_and_clean($file)) { # Create table, obviously. There will be other lines below for the rest of the table definition if ($line =~ /^CREATE TABLE \[(.*)\]\.\[(.*)\]\s*\(/) { my $schemaname = relabel_schemas($1); my $orig_schema = $1; my $tablename = $2; $objects->{SCHEMAS}->{$schemaname}->{TABLES}->{$tablename}->{haslobs} = 0; $objects->{SCHEMAS}->{$schemaname}->{TABLES}->{$tablename}->{origschema} = $orig_schema; # We are in a create table. Read everything until its end... TABLE: while (my $line = read_and_clean($file)) { # Here is a col definition. # We ignore ROWGUIDCOL as it has no meaning in PostgreSQL and cannot be emulated # (it makes it possible to do a select xxx WHERE $ROWGUID, without knowing the column name, typical microsoft stuff :( ) # To make matters even worse, they seem to systematically add a space after it :) if ($line =~ /^\t\[(.*)\] (?:\[(.*)\]\.)?\[(.*)\]\s*(\(.+?\))?(?: COLLATE (\S+))?( IDENTITY\s*\(\d+,\s*\d+\))?(?: ROWGUIDCOL ?)? (?:NOT FOR REPLICATION )?(?:SPARSE )?(NOT NULL|NULL)(?:\s+CONSTRAINT \[.*\])?(?:\s+DEFAULT \((.*)\))?(?:,|$)?/ ) { #Deported into a function because we can also meet alter table add columns on their own my $colname = $1; my $coltypeschema = $2; my $coltype = $3; my $colqual =$4; my $colcollate =$5; # Ignore for now my $isidentity =$6; my $colisnull =$7; my $default =$8; add_column_to_table($schemaname,$tablename,$colname,$coltypeschema,$coltype,$colqual,$isidentity,$colisnull); if (defined $default) { store_default_value($schemaname,$tablename,$colname,$default,$line); } } # This is a calculated column. It doesn't exist in PG, it is not typed (I guess its type is the type of the returning function) # So just put it as a varchar, and issue a warning is STDOUT elsif ($line =~ /^\t\[(.*)\]\s+AS\s+\((.*)\)/) { # We just get the column name my $colnumber=next_col_pos($schemaname,$tablename); my $colname = $1; my $code = $2; my $coltype = 'varchar'; $objects->{SCHEMAS}->{$schemaname}->{'TABLES'}->{$tablename}->{COLS} ->{$colname}->{POS} = $colnumber; $objects->{SCHEMAS}->{$schemaname}->{'TABLES'}->{$tablename}->{COLS} ->{$colname}->{TYPE} = $coltype; $objects->{SCHEMAS}->{$schemaname}->{'TABLES'}->{$tablename}->{COLS} ->{$colname}->{NOT_NULL} = 0; # Big fat warning print STDERR "Warning: There is a calculated column: $schemaname.$tablename.$colname. This isn't done the same way in PG at all\n"; print STDERR "\tFor now it has been declared as a varchar in PG, so that the values can be copied\n"; print STDERR "\tYou should change its type manually in the dump (sorry for that),\n"; print STDERR "\tA trigger has been written in the unsure file. It probably won't work as is.\n"; print STDERR "\tPlease review it.\n"; # Try to correct what can be corrected from the AS : replace [COL] with NEW.COL # It is obviously not going to work for anything a bit complicated $code =~ s/\[(.*?)\]/NEW.$1/g; my $triggerfunc = <{SCHEMAS}->{$schemaname}->{'TRIG_FUNCTIONS'} ->{'trig_func_ins_or_upd' || $tablename}->{DEF} = $triggerfunc; $objects->{SCHEMAS}->{$schemaname}->{'TRIG_FUNCTIONS'} ->{'trig_func_ins_or_upd' || $tablename}->{LANG} = 'plpgsql'; my %trigger; $trigger{EVENTS} = 'before insert or update'; $trigger{WHEN} = 'for each row'; $trigger{FUNCTION} = 'trig_func_ins_or_upd' || $tablename; # In the same schema $trigger{NAME} = 'trig_ins_or_upd' || $tablename; push @{$objects->{SCHEMAS}->{$schemaname}->{'TABLES'}->{$tablename} ->{TRIGGERS}}, (\%trigger); } elsif ($line =~ /^(?: CONSTRAINT \[(.*)\] )?PRIMARY KEY (?:NON)?CLUSTERED/) { my $constraint ; # We put everything inside this hashref, we'll push it into the constraint list later $constraint->{TYPE} = 'PK'; if (defined $1) { $constraint->{NAME} = $1; } # Here is the PK. We read the following lines until the end of the constraint while (my $pk = read_and_clean($file)) { # Exit when read a line beginning with ). The constraint is complete. We store it and go back to main loop if ($pk =~ /^\)/) { push @{$objects->{SCHEMAS}->{$schemaname}->{TABLES}->{$tablename} ->{CONSTRAINTS}}, ($constraint); # We also directly put the constraint reference in a direct path (for ease of use in generate_kettle) $objects->{SCHEMAS}->{$schemaname}->{TABLES}->{$tablename}->{PK} = $constraint; next TABLE; } if ($pk =~ /^\t\[(.*)\] (ASC|DESC)(,?)/) { push @{$constraint->{COLS}}, ($1); } } } elsif ($line =~ /^\s*(?:CONSTRAINT \[(.*)\] )?UNIQUE/) { my $constraint ; # We put everything inside this hashref, we'll push it into the constraint list later $constraint->{TYPE} = 'UNIQUE'; if (defined $1) { $constraint->{NAME} = $1; } # Unique key definition. We read following lines until the end of the constraint while (my $uk = read_and_clean($file)) { # Exit when read a line beginning with ). The constraint is complete if ($uk =~ /^\)/) { push @{$objects->{SCHEMAS}->{$schemaname}->{'TABLES'}->{$tablename} ->{CONSTRAINTS}}, ($constraint); next TABLE; } if ($uk =~ /^\t\[(.*)\] (ASC|DESC)(,?)/) { push @{$constraint->{COLS}}, ($1); } } } elsif ($line =~ /^\) ON \[PRIMARY\]/) { # End of the table next MAIN; } else { croak "Cannot understand $line\n"; } } } ################################################################ # From HERE, these SQL commands are not linked to a create table ################################################################ elsif ($line =~ /^CREATE SEQUENCE \[(.*)\]\.\[(.*)\]/) { my $schemaname = relabel_schemas($1); my $orig_schema = $1; my $seqname = $2; while (my $contline = read_and_clean($file)) { if ($contline =~ /^\s*AS \[.*\]\s*$/) { next; # We don't care, sequences are always bigint in PostgreSQL } elsif ($contline =~ /^\s*START WITH (\d+)\s*$/) { $objects->{SCHEMAS}->{$schemaname}->{SEQUENCES}->{$seqname}->{START}=$1; } elsif ($contline =~ /^\s*INCREMENT BY (\d+)\s*$/) { $objects->{SCHEMAS}->{$schemaname}->{SEQUENCES}->{$seqname}->{STEP}=$1; } elsif ($contline =~ /^\s*MINVALUE (-?\d+)\s*$/) { $objects->{SCHEMAS}->{$schemaname}->{SEQUENCES}->{$seqname}->{MIN}=$1; } elsif ($contline =~ /^\s*MAXVALUE (-?\d+)\s*$/) { $objects->{SCHEMAS}->{$schemaname}->{SEQUENCES}->{$seqname}->{MAX}=$1; } elsif ($contline =~ /^\s*(NO)?CACHE( \d+)?\s*$/) { if (defined $1) { # It's a no cache. Equivalent to CACHE = 1 in PostgreSQL $objects->{SCHEMAS}->{$schemaname}->{SEQUENCES}->{$seqname}->{CACHE}=1; } elsif (defined $2) { # We have a specified value $objects->{SCHEMAS}->{$schemaname}->{SEQUENCES}->{$seqname}->{CACHE}=$2; } else { # Cache, but not specified. SQL Server isn't very clear on the size of the cache. Let's say 100 $objects->{SCHEMAS}->{$schemaname}->{SEQUENCES}->{$seqname}->{CACHE}=100; } } elsif ($contline =~ /^GO$/) { next MAIN; } } } elsif ($line =~ /^CREATE SCHEMA \[(.*)\]/) { $objects->{SCHEMAS}->{relabel_schemas($1)} = undef ; # Nothing to add here, we create the schema, and put undef in it for now } elsif ($line =~ /CREATE\s+PROC(?:EDURE)?\s+\[.*\]\.\[(.*)\]/i) { print STDERR "Warning: Procedure $1 ignored\n"; # We have to find next GO to know we are out of the procedure while (my $contline = read_and_clean($file)) { next MAIN if ($contline =~ /^GO$/); } } elsif ($line =~ /CREATE\s+FUNCTION\s+\[.*\]\.\[(.*)\]/i) { print STDERR "Warning: Function $1 ignored\n"; # We have to find next GO to know we are out of the procedure while (my $contline = read_and_clean($file)) { next MAIN if ($contline =~ /^GO$/); } } elsif ($line =~ /CREATE\s+TRIGGER\s+\[(.*)\]/i) { print STDERR "Warning: Trigger $1 ignored\n"; # We have to find next GO to know we are out of the procedure while (my $contline = read_and_clean($file)) { next MAIN if ($contline =~ /^GO$/); } } elsif ($line =~ /SET\s+IDENTITY_INSERT\s+\[(.*)\]/i) { print STDERR "Warning: SET IDENTITY_INSERT ignored\n"; # We have to find next GO to know we are out of the procedure while (my $contline = read_and_clean($file)) { next MAIN if ($contline eq ''); } } # Now we parse the create view. It is multi-line, so the code looks like like create table: we parse everything until a line # containing only a single quote (end of the dbo.sp_executesql) # The problem is that SQL Server seems to be spitting the original query used to create the view, not a normalized version # of it, as PostgreSQL does. So we capture the query, and hope it works for now. elsif ($line =~/^\s*(create\s*view)/i) { my $viewname; my $schemaname; my $supplement; # Either we have the create view and the object name with it, or it is on next non-empty line if ( $line =~ /^\s*(create\s*view)\s*(?:\[(\S+)\]\.)?\[(.*?)\]\s*(.*)$/i) { $viewname = $3; if (defined $2) { $schemaname = $2; } else { $schemaname = 'dbo'; } $supplement=$4; } else { # Find the line containing the view name. For now, it's the next line. If not, well, correct your dump by yourself, sorry :) my $line_name = read_and_clean($file); $line_name=~ /^\s*(?:\[(\S+)\]\.)?\[(.*?)\]\s*(.*)$/ or croak "This line $line_name doesn't contain a view name"; $viewname = $2; if (defined $1) { $schemaname = $1; } else { $schemaname = 'dbo'; } $supplement=$3; } $schemaname = relabel_schemas($schemaname); my $sql = 'CREATE VIEW ' . $schemaname . '.' . $viewname . ' ' . $supplement . "\n"; while (my $line_cont = read_and_clean($file)) { if ($line_cont =~ /^\s*'\s*$|^GO$/ ) # We may have a quote if the view is 'quoted', or a real sql query { # The view definition is complete. # We get rid of dbo. schemas $sql =~ s/(dbo)\./relabel_schemas($1) . '.'/eg ; # We put this in the replacement schema # Views will be stored without the full schema in them. We will # have to generate the schema in the output file $objects->{SCHEMAS}->{$schemaname}->{'VIEWS'}->{$viewname}->{SQL} = $sql; my @view_array=($schemaname,$viewname); push @view_list,(\@view_array); # adds another schema/view to the list next MAIN; } $sql .= $line_cont; } } # These are domains with PostgreSQL elsif ($line =~ /^CREATE TYPE \[(.*?)\]\.\[(.*?)\] FROM \[(.*?)](?:\((\d+(?:,\s*\d+)?)?\))?/ ) { # Dependency between types is not done for now. If the problem arises, it may be added my ($schema, $type, $origtype, $quals) = ($1, $2, $3, $4); $schema=relabel_schemas($schema); my $newtype = convert_type($origtype, $quals, undef, undef, $type, $schema); $objects->{SCHEMAS}->{$schema}->{DOMAINS}->{$type} = $newtype; # We add them to known data types, as they probably will be used in table definitions # but they point to themselves, with the schema corrected: we want them substituted by themselves $types{$schema . '.' . $type} = format_identifier($schema) . '.' . format_identifier($type); # We store the schema with it. And we do the case conversion, the quoting, etc right now } # These are like arrays of composite (with added functionnality, but we'll skip these # This will look like a table, but we'll ignore anything that isn't a column definition # If any of the type isn't a base type, this will die. But anyway, we wouldn't be able to convert properly elsif ($line =~ /^CREATE TYPE \[(.*)\]\.\[(.*)\] AS TABLE\(/) { my $schema=relabel_schemas($1); my $typename=$2; my $newbasetype=''; my @cols_newbasetype; my $colname; my $type; my $typequal; my $newtype; TYPE: while (my $typeline= read_and_clean($file)) { if ($typeline =~ /^\t\[(.*)\] \[(.*)\](?:\s*?\((\d+(?:,\d+)?)\))?(?:\s+?(?:NOT\s+?)?NULL),?$/) { # This is another column for this type $colname=$1; $type=$2; $typequal=$3; $newtype = convert_type($type, $typequal, undef, undef, undef, undef); push @cols_newbasetype,(format_identifier($colname) . ' ' . $newtype); } elsif ( $typeline =~ /PRIMARY KEY/) { print STDERR "Warning: TABLE type in SQL Server, input line $., ignored a primary key constraint\n"; # Let's skip everything till next parenthesis (probably the end) while (my $to_skip= read_and_clean($file)) { next TYPE if ($to_skip =~ /\)/); } } elsif ($typeline =~ /^\)$/) # We reached the end of the type def { next; } elsif ($typeline =~ /^GO$/) { # We reached the end. We add this new type # create the new type declaration $newbasetype=join(",\n",@cols_newbasetype); $objects->{SCHEMAS}->{$schema}->{TABLE_TYPES}->{$typename}=$newbasetype; # We add this to known data types, it will be used in table definitions $types{$schema . '.' . $typename} = format_identifier($schema) . '.' . format_identifier($typename); # We store the schema with it. And we do the case conversion, the quoting, etc right now next MAIN; } else { croak "Cannot understand $typeline\n"; } } } elsif ($line =~ /^\s*CREATE\s*(UNIQUE )?\s*(NONCLUSTERED|CLUSTERED)?\s*INDEX \[(.*?)\] ON \[(.*?)\]\.\[(.*?)\](\(\[.*?\]\))?/ ) { # Index creation. Index are namespaced per table in SQL Server, not in PostgreSQL # In PostgreSQL they are in the same namespace as the tables, and in the same # schema as the table they are attached to # So we store them in $objects, attached to the table # Conflicts will be sorted by resolve_name_conflicts() later my $isunique = $1; my $isclustered = $2; my $idxname = $3; my $schemaname = relabel_schemas($4); my $tablename = $5; my $maybecols = $6; # Some versions of sql server put column definitions on the first line if ($isunique) { $objects->{SCHEMAS}->{$schemaname}->{TABLES}->{$tablename}->{INDEXES} ->{$idxname}->{UNIQUE} = 1; } else { $objects->{SCHEMAS}->{$schemaname}->{TABLES}->{$tablename}->{INDEXES} ->{$idxname}->{UNIQUE} = 0; } if (defined $maybecols) { my @maybecols = split (',',$maybecols); foreach my $coldef(@maybecols) { $coldef=~/\[(.*)\](?: (ASC|DESC))?/ or die "Cannot understand coldef $coldef in index"; if (defined $2) { push @{$objects->{SCHEMAS}->{$schemaname}->{TABLES}->{$tablename} ->{INDEXES}->{$idxname}->{COLS}}, ("$1 $2"); } else { push @{$objects->{SCHEMAS}->{$schemaname}->{TABLES}->{$tablename} ->{INDEXES}->{$idxname}->{COLS}}, ("$1"); } } } while (my $idx = read_and_clean($file)) { # Exit when read a line with a GO. The index is complete if ($idx =~ /^GO/) { next MAIN; } next if ($idx =~ /^\(|^\)/) ; # Begin/end of the columns declaration if ($idx =~ /\t\[(.*)\] (ASC|DESC)(,)?/) { if (defined $2) { push @{$objects->{SCHEMAS}->{$schemaname}->{TABLES}->{$tablename} ->{INDEXES}->{$idxname}->{COLS}}, ("$1 $2"); } else { push @{$objects->{SCHEMAS}->{$schemaname}->{TABLES}->{$tablename} ->{INDEXES}->{$idxname}->{COLS}}, ("$1"); } } if ($idx =~ /^INCLUDE \(/) { print STDERR "Warning: This index ($schemaname.$tablename.$idxname) has some include columns. This isn't supported in PostgreSQL.\n"; print STDERR "\tThe columns in the INCLUDE clause have been ignored.\n"; next ; # Nothing equivalent in PG. Maybe if the index isn't unique, these columns should be added? } if ($idx =~ /^WHERE\s*\((.*)\)$/) { # This is a where clause. PostgreSQL has them too. But we cannot be sure this will be exactly the same. So if an index as a WHERE clause, it has to go to unsure my $filter=$1; $objects->{SCHEMAS}->{$schemaname}->{TABLES}->{$tablename} ->{INDEXES}->{$idxname}->{WHERE}=$filter; } } } elsif ($line =~ /^CREATE SPATIAL INDEX/) { my $def=$line; while (my $idx = read_and_clean($file)) { $def.=$idx; } print STDERR "This spatial index won't be migrated:\n$def\n"; } # Added table columns… this seems to appear in SQL Server when some columns have ANSI padding, and some not. # PG follows ANSI, that is not an option. The end of the regexp is pasted from the create table elsif ($line =~ /^ALTER TABLE \[(.*)\]\.\[(.*)\] ADD \[(.*)\] (?:\[(.*)\]\.)?\[(.*)\](\(.+?\))?( IDENTITY\(\d+,\s*\d+\))? (NOT NULL|NULL)(?: CONSTRAINT \[.*\] )?(?: DEFAULT \(.*\))?$/ ) { my $schemaname=relabel_schemas($1); my $tablename=$2; my $colname=$3; my $coltypeschema=$4; my $coltype=$5; my $colqual=$6; my $isidentity=$7; my $colisnull=$8; my $default=$9; add_column_to_table($schemaname,$tablename,$colname,$coltypeschema,$coltype,$colqual,$isidentity,$colisnull); if (defined $default) { store_default_value($schemaname,$tablename,$colname,$default,$line); } } # Table constraints # Primary key. Multiline elsif ($line =~ /^ALTER TABLE \[(.*)\]\.\[(.*)\] ADD\s*(?:CONSTRAINT \[(.*)\])? PRIMARY KEY (?:CLUSTERED|NONCLUSTERED)?/ ) { my $schemaname=relabel_schemas($1); my $tablename=$2; my $constraint; $constraint->{TYPE}='PK'; if (defined $3) { $constraint->{NAME} = $3; } CONS: while (my $consline= read_and_clean($file)) { next if ($consline =~ /^\($/); if ($consline =~ /^\t\[(.*)\] ASC,?$/) { push @{$constraint->{COLS}}, ($1); } elsif ($consline =~ /^\).*$/) { push @{$objects->{SCHEMAS}->{$schemaname}->{TABLES}->{$tablename} ->{CONSTRAINTS}}, ($constraint); # We also directly put the constraint reference in a direct path (for ease of use in generate_kettle) $objects->{SCHEMAS}->{$schemaname}->{TABLES}->{$tablename}->{PK} = $constraint; # We are done here next MAIN; } else { croak "Cannot understand $consline."; } } } elsif ($line =~ /^ALTER TABLE \[(.*)\]\.\[(.*)\] ADD\s*(?:CONSTRAINT \[(.*)\])? UNIQUE (?:CLUSTERED|NONCLUSTERED)?/ ) { my $schemaname=relabel_schemas($1); my $tablename=$2; my $constraint; $constraint->{TYPE}='UNIQUE'; if (defined $3) { $constraint->{NAME}=$3; } while (my $uk = read_and_clean($file)) { # Exit when read a line beginning with ). The constraint is complete if ($uk =~ /^\)/) { push @{$objects->{SCHEMAS}->{$schemaname}->{'TABLES'}->{$tablename} ->{CONSTRAINTS}}, ($constraint); next MAIN; } if ($uk =~ /^\t\[(.*)\] (ASC|DESC)(,?)/) { push @{$constraint->{COLS}}, ($1); } } } # Default values. numeric, then text. These are 100% sure, they will parse in PG # Sometimes there is a second pair of parenthesis. I don't even want to know why... # Bit just need a little bit of work to be converted to 'true'/'false' elsif ($line =~ /^ALTER TABLE \[(.*)\]\.\[(.*)\] ADD\s*(?:CONSTRAINT \[.*\])?\s*DEFAULT \((\(?(?:-)?\d+(?:\.\d+)?\))?\) FOR \[(.*)\]/ ) { store_default_value(relabel_schemas($1),$2,$4,$3,$line); # schema,table,col,value } elsif ($line =~ /^ALTER TABLE \[(.*)\]\.\[(.*)\] ADD\s*(?:CONSTRAINT \[.*\])?\s*DEFAULT \(('.*')\) FOR \[(.*)\]/ ) { store_default_value(relabel_schemas($1),$2,$4,$3,$line); # schema,table,col,value } # Yes, we also get default NULL (what for ? :) ), and sometimes with a different case elsif ($line =~ /^ALTER TABLE \[(.*)\]\.\[(.*)\] ADD\s*(?:CONSTRAINT \[.*\])?\s*DEFAULT \(((?i)NULL)\) FOR \[(.*)\]/ ) { store_default_value(relabel_schemas($1),$2,$4,$3,$line); # schema,table,col,value } # And there are also constraints with functions and other strange code in them. Put them as unsure elsif ($line =~ /^ALTER TABLE \[(.*)\]\.\[(.*)\] ADD\s*(?:CONSTRAINT \[.*\])?\s*DEFAULT \(\(?(.*)\)?\) FOR \[(.*)\]/ ) { store_default_value(relabel_schemas($1),$2,$4,$3,$line); # schema,table,col,value } # FK constraint. It's multi line, we have to look for references, and what to do on update, delete, etc (I have only seen delete cascade for now) # Constraint name is optionnal elsif ($line =~ /^ALTER TABLE \[(.*)\]\.\[(.*)\]\s+WITH (?:NO)?CHECK ADD(?:\s+CONSTRAINT \[(.*)\])? FOREIGN KEY\((.*?)\)/ ) { # This is a FK definition. We have the foreign table definition in next line. my $constraint; my $table = $2; my $schema = relabel_schemas($1); my $consname= $3; $constraint->{TYPE} = 'FK'; my @local_cols = split (/\s*,\s*/,$4); # Split around the comma. There may be whitespaces @local_cols=map{s/^\[//;s/]$//;$_;} @local_cols; # Remove the brackets around the columns $constraint->{LOCAL_COLS}=\@local_cols; $constraint->{LOCAL_TABLE} = $2; if (defined $consname) { $constraint->{NAME}=$consname; } while (my $fk = read_and_clean($file)) { if ($fk =~ /^GO/) { push @{$objects->{SCHEMAS}->{$schema}->{'TABLES'}->{$table} ->{CONSTRAINTS}}, ($constraint); next MAIN; } elsif ($fk =~ /^REFERENCES \[(.*)\]\.\[(.*)\] \((.*?)\)/) { my @remote_cols = split (/\s*,\s*/,$3); # Split around the comma. There may be whitespaces @remote_cols=map{s/^\[//;s/]$//;$_;} @remote_cols; # Remove the brackets around the columns $constraint->{REMOTE_COLS}=\@remote_cols; $constraint->{REMOTE_TABLE} = $2; $constraint->{REMOTE_SCHEMA} = relabel_schemas($1); $constraint->{REMOTE_COLS} =~ s/\[|\]//g; # Get rid of square brackets } elsif ($fk =~ /^ON DELETE CASCADE\s*$/) { $constraint->{ON_DEL_CASC} = 1; } elsif ($fk =~ /^ON DELETE SET NULL\s*$/) { $constraint->{ON_DEL_SET_NULL} = 1; } elsif ($fk =~ /^ON UPDATE CASCADE\s*$/) { $constraint->{ON_UPD_CASC} = 1; } elsif ($fk =~ /^NOT FOR REPLICATION$/) { next; # We don't care for this, it has no meaning for PostgreSQL } else { croak "Cannot parse $fk $., in a FK. This is a bug"; } } } # Check constraint. As it can be arbitrary code, we just get this code, and hope it will work on PG (it will be stored in a special script file) elsif ($line =~ /ALTER TABLE \[(.*)\]\.\[(.*)\] WITH (?:NO)?CHECK ADD(?:\s+CONSTRAINT \[(.*)\])? CHECK \(\((.*)\)\)/ ) { # Check constraint. We'll do what we can, syntax may be different. my $constraint; my $table = $2; my $constxt = $4; my $schema = relabel_schemas($1); $constraint->{TABLE} = $table; if (defined $3) { $constraint->{NAME} = $3; } $constraint->{TYPE} = 'CHECK'; $constraint->{TEXT} = $constxt; push @{$objects->{SCHEMAS}->{$schema}->{'TABLES'}->{$table}->{CONSTRAINTS}}, ($constraint); } # These are comments or extended attributes on objets. They can be multiline, so aggregate everything # Until a line that ends with a quote (but not two of them). We remove pair of quotes to make it simpler # If fact in can be a lot of things. So we have to ignore things like MS_DiagramPaneCount elsif ($line =~ /^EXEC sys.sp_addextendedproperty/) { $line =~ s/''//g; my $sqlproperty = $line; # If it ends with a single quote, and it is not the start (some people start their comments with a linefeed) unless ($line =~ /'$/ and $line !~ /=N'$/) { while (my $inline = read_and_clean($file)) { $inline =~ s/''//g; $sqlproperty .= $inline; # If it ends with a single quote, and it is not the start (some people start their comments with a linefeed) last if ($inline =~ /'$/ and $inline !~ /=N'$/); } } # We have all the extended property. Let's parse it. # First step: what kind is it ? we are only interested in comments for now $sqlproperty =~ /\@name=N'(.*?)'/ or croak "Cannot find a name for this extended property: $sqlproperty"; my $propertyname = $1; if ($propertyname =~ /^(MS_DiagramPaneCount|MS_DiagramPane1|Display Name|Description|Example Values|Source System|Table Description|Table Type|ETL Rules|Display Folder|SCD Type|Source Datatype)$/) { # We don't dump these. They are graphical descriptions of the GUI next; } elsif ($propertyname eq 'MS_Description') { # This is a comment. We parse it. # Spaces are mostly random it seems, in SQL Server's dump code. So \s* everywhere :( # There can be quotes inside a string. So (?<!')' matches only a ' not preceded by a '. # I hope it will be sufficient (won't be if someone decides to end a comment with a quote) unless ($sqlproperty =~ /^EXEC sys.sp_addextendedproperty \@name=N'(.*?)'\s*,\s*\@value=N'(.*)'\s*,\s*\@level0type=N'(.*?)'\s*,\s*\@level0name=N'(.*?)'\s*(?:,\s*\@level1type=N'(.*?)'\s*,\s*\@level1name=N'(.*?)')\s*?(?:,\s*\@level2type=N'(.*?)'\s*,\s*\@level2name=N'(.*?)')?/s) { # Not parsing a comment should not stop print STDERR "Could not parse <$sqlproperty>. Ignored.\n"; next MAIN; } my ($comment, $schema, $obj, $objname, $subobj, $subobjname) = ($2, $4, $5, $6, $7, $8); $schema=relabel_schemas($schema); if ($obj eq 'TABLE' and not defined $subobj) { $objects->{SCHEMAS}->{$schema}->{TABLES}->{$objname}->{COMMENT} = $comment; } elsif ($obj eq 'VIEW' and not defined $subobj) { $objects->{SCHEMAS}->{$schema}->{VIEWS}->{$objname}->{COMMENT} = $comment; } elsif ($obj eq 'TABLE' and $subobj eq 'COLUMN') { $objects->{SCHEMAS}->{$schema}->{TABLES}->{$objname}->{COLS} ->{$subobjname}->{COMMENT} = $comment; } elsif ($obj eq 'TABLE' and $subobj eq 'CONSTRAINT') { # It's a bit tedious: constraints are stored in an array for each table # Find the one foreach my $constraint (@{$objects->{SCHEMAS}->{$schema}->{TABLES}->{$objname}->{CONSTRAINTS}}) { if (defined ($constraint->{NAME}) and $constraint->{NAME} eq $subobjname) { $constraint->{COMMENT}=$comment; } } } else { croak "Cannot understand this comment: $sqlproperty"; } } elsif ($propertyname eq 'Dictionary') { # It seems to be another way to declare table comments. I hope this is right $sqlproperty =~ /^EXEC sys.sp_addextendedproperty \@name=N'(.*?)'\s*,\s*\@value=N'(.*?)(?<!')'\s*,\s*\@level0type=N'(.*?)'\s*,\s*\@level0name=N'(.*?)'\s*(?:,\s*\@level1type=N'(.*?)'\s*,\s*\@level1name=N'(.*?)')/s or croak "Could not parse $sqlproperty. This is a bug."; my ($comment, $schema, $obj, $objname) = ($2, $4, $5, $6); $schema=relabel_schemas($schema); if ($obj eq 'TABLE') { $objects->{SCHEMAS}->{$schema}->{TABLES}->{$objname}->{COMMENT} = $comment; } elsif ($obj eq 'SCHEMA') { # Never met one for now. Die and ask to send me an example croak "Schema comment : <$comment> not understood. Please send a bug report\n"; } } else { croak "Don't know what to do with this extendedproperty: $sqlproperty"; } } # Ignore USE, GO, and things that have no meaning for postgresql elsif ($line =~ /^USE\s|^GO\s*$|\/\*\*\*\*|^SET ANSI_NULLS (ON|OFF)|^SET QUOTED_IDENTIFIER|^SET ANSI_PADDING|CHECK CONSTRAINT|^BEGIN|^END/ ) { next; } elsif ($line =~ /^--/) # Comment { next; } # Don't know what it is. If you know, and it is worth converting, tell me :) elsif ($line =~ /^EXEC .*bindrule/) { next; } # Ignore users and roles. Security models will probably be very different between the two databases elsif ($line =~ /^CREATE (ROLE|USER)/) { next; } elsif ($line =~ /^ALTER (ROLE|USER)/) { next; } # Ignore xml schema collections since they are not supported in pg elsif ($line =~ /^CREATE XML SCHEMA COLLECTION/) { next; } elsif ($line =~ /^ALTER XML SCHEMA COLLECTION/) { next; } # Ignore existence tests… how could the object already exist anyway ? For now, only seen for views # Also ignore version tests elsif ($line =~ /^IF EXISTS|^IF \(\@\@microsoftversion/i) { # just read until next go (or EOF) while (defined $line and $line !~ /^GO$/) { $line =read_and_clean($file); } } elsif ($line =~ /^IF NOT EXISTS/i) { # Just ignore the line next; } # Ignore CREATE DATABASE: we hope that we are given a single database as an option. It is multiline. # Ignore everything until next GO # Ignore ALTER DATABASE for the same reason. The given parameters have no meaning in PG anyway # Except for SET ARITHABORT OFF, for which we print a warning because it probably means the database contents are weird (10/0 = null) elsif ($line =~ /^ALTER DATABASE.* SET ARITHABORT OFF/) { print STDERR "WARNING: the source database is set as ARITHABORT OFF.\n"; print STDERR " It means that for SQL Server, 10/0 = NULL.\n"; print STDERR " You'll probably have problems porting that to PostgreSQL.\n"; while ($line !~ /^GO$/) { $line =read_and_clean($file); } # We read everything in the CREATE DATABASE. Back to work ! next; } # Sometimes, when there is a ALTER DATABASE SET ARITHABORT OFF, there are SET ARITHABORT ON. Just ignore them elsif ($line =~ /^SET ARITHABORT ON/) { next; } # Sometimes we meet this: SET CONCAT_NULL_YIELDS_NULL ON. That's the normal behaviour for a SQL database. Just ignore elsif ($line =~ /^SET CONCAT_NULL_YIELDS_NULL ON/) { next; } # Same more or less elsif ($line =~ /^SET ANSI_WARNINGS ON/) { next; } # What the hell does this do in a dump ??? elsif ($line =~ /^SET NUMERIC_ROUNDABORT OFF/) { next; } # Same for tests about full text search. elsif ($line =~ /^(CREATE|ALTER) DATABASE|^IF \(1 = FULLTEXTSERVICEPROPERTY/) { while ($line !~ /^GO$/) { $line = read_and_clean($file); } # We read everything in the CREATE DATABASE. Back to work ! next; } # Ignore CREATE and ALTER statements for full text search objects, such as CATALOG, INDEX or STOPLIST. elsif ($line =~ /^(CREATE|ALTER) FULLTEXT/) { while ($line !~ /^GO$/) { $line =read_and_clean($file); } next; } # Ignore EXEC dbo.sp_executesql, for now only seen for a create view. Views sql command aren't executed directly, don't know why elsif ($line =~ /^EXEC dbo.sp_executesql/) { next; } # Ignore EXEC sys.sp_db_vardecimal_storage_format, enabling for vardecimal storage format was only needed in SQL Server 2005 and means nothing for PG. elsif ($line =~ /^EXEC sys\.sp_db_vardecimal_storage_format/) { next; } # Still on views: there are empty lines, and C-style comments elsif ($line =~ /^\s*$/) { next; } else { die "Line <$line> ($.) not understood. This is a bug"; } } close $file; } # Creates the SQL scripts from $object # We generate alphabetically, to make things less random (this data comes from a hash) sub generate_schema { my ($before_file, $after_file, $unsure_file) = @_; # Open the output files (except kettle, we'll do that at the end) open BEFORE, ">:utf8", $before_file or die "Cannot open $before_file, $!"; open AFTER, ">:utf8", $after_file or die "Cannot open $after_file, $!"; open UNSURE, ">:utf8", $unsure_file or die "Cannot open $unsure_file, $!"; print BEFORE "\\set ON_ERROR_STOP\n"; print BEFORE "\\set ECHO all\n"; print BEFORE "BEGIN;\n"; print AFTER "\\set ON_ERROR_STOP\n"; print AFTER "\\set ECHO all\n"; print AFTER "BEGIN;\n"; print UNSURE "\\set ON_ERROR_STOP\n"; print AFTER "\\set ECHO all\n"; print UNSURE "BEGIN;\n"; # Are we case insensitive ? We have to install citext then # Won't work on pre-9.1 database. But as this is a migration tool # if someone wants to start with an older version, it's their problem :) if ($case_insensitive) { print BEFORE "CREATE EXTENSION IF NOT EXISTS citext;\n"; } # Do we require PostGIS ? if ($requires_postgis) { print BEFORE "CREATE EXTENSION IF NOT EXISTS postgis;\n"; print BEFORE "CREATE EXTENSION IF NOT EXISTS postgis_topology;\n"; } # Ok, we have parsed everything, and definitions are in $objects # We will put in the BEFORE file only table and columns definitions. # The rest will go in the AFTER script (check constraints, put default values, etc...) # The schemas. don't create empty schema, sql server creates a schema per user, even if it ends empty foreach my $schema (sort keys %{$objects->{SCHEMAS}}) { unless ($schema eq 'public' or not defined $objects->{SCHEMAS}->{$schema}) { # Not compatible before 9.3. This is the logical target for this tool anyway print BEFORE "CREATE SCHEMA IF NOT EXISTS ",format_identifier($schema),";\n"; } } # For the rest, we iterate over schemas, except for array types (no point in complicating this) # The tables, columns, etc... will be created in the before script, so there is no dependancy # problem with constraints, that will be in the after script, except foreign keys which depend on unique indexes # We have to do all domains and types before all tables # Don't care for dependancy while (my ($schema, $refschema) = each %{$objects->{SCHEMAS}}) { # The user-defined types (domains, etc) foreach my $tabletype (sort keys %{$refschema->{TABLE_TYPES}}) { print BEFORE "CREATE TYPE " . format_identifier($schema) . '.' . format_identifier($tabletype) . " AS (\n" . $refschema->{TABLE_TYPES}->{$tabletype} . "\n);\n"; } print BEFORE "\n"; # We change sections in the dump file } while (my ($schema, $refschema) = each %{$objects->{SCHEMAS}}) { # The user-defined types (domains, etc) foreach my $domain (sort keys %{$refschema->{DOMAINS}}) { print BEFORE "CREATE DOMAIN " . format_identifier($schema) . '.' . format_identifier($domain) . ' ' . $refschema->{DOMAINS}->{$domain} . ";\n"; } print BEFORE "\n"; # We change sections in the dump file } # Tables and columns while (my ($schema, $refschema) = each %{$objects->{SCHEMAS}}) { # The tables foreach my $table (sort keys %{$refschema->{TABLES}}) { my @colsdef; foreach my $col ( sort { $refschema->{TABLES}->{$table}->{COLS}->{$a}->{POS} <=> $refschema->{TABLES}->{$table}->{COLS}->{$b} ->{POS} } (keys %{$refschema->{TABLES}->{$table}->{COLS}})) { my $colref = $refschema->{TABLES}->{$table}->{COLS}->{$col}; my $coldef = format_identifier($col) . " " . $colref->{TYPE}; if ($colref->{NOT_NULL}) { $coldef .= ' NOT NULL'; } push @colsdef, ($coldef); } print BEFORE "CREATE TABLE " . format_identifier($schema) . '.' . format_identifier($table) . "( \n\t" . join(",\n\t", @colsdef) . ");\n\n"; } } # Sequences, PKs, Indexes while (my ($schema, $refschema) = each %{$objects->{SCHEMAS}}) { # We now add all "AFTER" objects # We start with SEQUENCES, PKs and INDEXES (will be needed for FK) foreach my $sequence (sort keys %{$refschema->{SEQUENCES}}) { my $seqref = $refschema->{SEQUENCES}->{$sequence}; print AFTER "CREATE SEQUENCE " . format_identifier($schema) . '.' . format_identifier($sequence); if (defined $seqref->{STEP}) { print AFTER " INCREMENT BY ",$seqref->{STEP}; } if (defined $seqref->{MIN}) { print AFTER " MINVALUE ",$seqref->{MIN}; } if (defined $seqref->{MAX}) { print AFTER " MAXVALUE ",$seqref->{MAX}; } if (defined $seqref->{START}) { print AFTER " START WITH ",$seqref->{START}; } if (defined $seqref->{CACHE}) { print AFTER " CACHE ",$seqref->{CACHE}; } if (defined $seqref->{OWNERTABLE}) { print AFTER " OWNED BY ",format_identifier($seqref->{OWNERSCHEMA}), '.',format_identifier($seqref->{OWNERTABLE}), '.',format_identifier($seqref->{OWNERCOL}); } print AFTER ";\n"; } # Now PK. We have to go through all tables foreach my $table (sort keys %{$refschema->{TABLES}}) { my $refpk = $refschema->{TABLES}->{$table}->{PK}; # Warn if no PK! if (not defined $refpk) { # Don't know if it should be displayed #print STDERR "Warning: $table has no primary key.\n"; next; } my $pkdef = "ALTER TABLE " . format_identifier($schema) . '.' . format_identifier($table) . " ADD"; if (defined $refpk->{NAME}) { $pkdef .= " CONSTRAINT " . format_identifier($refpk->{NAME}); } # Create a list of formatted columns my @collist=map{format_identifier($_)} @{$refpk->{COLS}}; $pkdef .= " PRIMARY KEY (" . join(',', @collist) . ");\n"; print AFTER $pkdef; } } # Unique while (my ($schema, $refschema) = each %{$objects->{SCHEMAS}}) { # Now The UNIQUE constraints. They may be used for FK (if columns are not null) foreach my $table (sort keys %{$refschema->{TABLES}}) { foreach my $constraint ( @{$refschema->{TABLES}->{$table}->{CONSTRAINTS}}) { next unless ($constraint->{TYPE} eq 'UNIQUE'); my $consdef = "ALTER TABLE " . format_identifier($schema) . '.' . format_identifier($table) . " ADD"; if (defined $constraint->{NAME}) { $consdef .= " CONSTRAINT " . format_identifier($constraint->{NAME}); } my @collist=map{format_identifier($_)} @{$constraint->{COLS}}; $consdef .= " UNIQUE (" . join(",", @collist) . ");\n"; print AFTER $consdef; } } } # Indexes. Unique indexes are needed before foreign key constraints while (my ($schema, $refschema) = each %{$objects->{SCHEMAS}}) { # Indexes # They don't have a schema qualifier. But their table has, and they are in the same schema as their table foreach my $table (sort keys %{$refschema->{TABLES}}) { foreach my $index ( sort keys %{$refschema->{TABLES}->{$table}->{INDEXES}}) { my $idxref = $refschema->{TABLES}->{$table}->{INDEXES}->{$index}; my $idxdef = "CREATE"; if ($idxref->{UNIQUE}) { $idxdef .= " UNIQUE"; } $idxdef .= " INDEX " . format_identifier($index) . " ON " . format_identifier($schema) . '.' . format_identifier($table) . " (" . join(",", map{format_identifier_cols_index($_)} @{$idxref->{COLS}}) . ")"; if (not defined $idxref->{WHERE}) { $idxdef .= ";\n"; print AFTER $idxdef; } else { print STDERR "Warning: index $schema.$index contains a where clause. It goes to unsure file\n"; $idxdef .= "\nWHERE (" . convert_transactsql_code($idxref->{WHERE}) . ");\n"; print UNSURE $idxdef; } } } } # Other constraints while (my ($schema, $refschema) = each %{$objects->{SCHEMAS}}) { # We have all we need for FKs now. We can put all other constraints (except PK of course) foreach my $table (sort keys %{$refschema->{TABLES}}) { foreach my $constraint ( @{$refschema->{TABLES}->{$table}->{CONSTRAINTS}}) { next if ($constraint->{TYPE} =~ /^UNIQUE|PK$/); my $consdef = "ALTER TABLE " . format_identifier($schema) . '.' . format_identifier($table) . " ADD"; if (defined $constraint->{NAME}) { $consdef .= " CONSTRAINT " . format_identifier($constraint->{NAME}); } if ($constraint->{TYPE} eq 'FK') # COLS are already a comma separated list { # We need to convert the column list to protected names my @localcollist=map{format_identifier($_)} @{$constraint->{LOCAL_COLS}}; my @remotecollist=map{format_identifier($_)} @{$constraint->{REMOTE_COLS}}; $consdef .= " FOREIGN KEY (" . join(',',@localcollist) . ")" . " REFERENCES " . format_identifier($constraint->{REMOTE_SCHEMA}) . '.' . format_identifier($constraint->{REMOTE_TABLE}) . " ( " . join(',',@remotecollist) . ")"; if (defined $constraint->{ON_DEL_CASC} and $constraint->{ON_DEL_CASC}) { $consdef .= " ON DELETE CASCADE"; } if (defined $constraint->{ON_DEL_SET_NULL} and $constraint->{ON_DEL_SET_NULL}) { $consdef .= " ON DELETE SET NULL"; } if (defined $constraint->{ON_UPD_CASC} and $constraint->{ON_UPD_CASC}) { $consdef .= " ON UPDATE CASCADE"; } # We need a name on the constraint to be able to validate it later. Maybe it would be better to generate one # FIXME: we'll see later if a generator is needed (probably) if ($constraint->{TYPE} eq 'FK' and ($validate_constraints =~ /^after|no$/) and defined($constraint->{NAME})) { $consdef .= " NOT VALID"; } $consdef .= ";\n"; print AFTER $consdef; if ($constraint->{TYPE} eq 'FK' and $validate_constraints eq 'after' and defined $constraint->{NAME}) { print UNSURE "ALTER TABLE " . format_identifier($schema) . '.' . format_identifier($table) . " VALIDATE CONSTRAINT " . format_identifier($constraint->{NAME}) . ";\n"; } } elsif ($constraint->{TYPE} eq 'CHECK') { $consdef .= " CHECK (" . convert_transactsql_code($constraint->{TEXT}) . ");\n"; print UNSURE $consdef ; # Check constraints are SQL, so cannot be sure } elsif ($constraint->{TYPE} eq 'CHECK_CITEXT') { # These have been generated here, for citext mostly. So we know their syntax is ok $consdef .= " CHECK (" . $constraint->{TEXT} . ");\n"; print BEFORE $consdef ; # These are for citext. So they should be checked asap } else { # Shouldn't get there. it would mean I have forgotten a type of constraint croak "I couldn't translate a constraint. This is a bug"; } } } } # Another pass at constraints. This time we want to produce the comments while (my ($schema, $refschema) = each %{$objects->{SCHEMAS}}) { # We have all we need for FKs now. We can put all other constraints (except PK of course) foreach my $table (sort keys %{$refschema->{TABLES}}) { foreach my $constraint ( @{$refschema->{TABLES}->{$table}->{CONSTRAINTS}}) { next unless defined ($constraint->{COMMENT}); print UNSURE "COMMENT ON CONSTRAINT " . format_identifier($constraint->{NAME}) . " ON " . format_identifier($schema) . '.' . format_identifier($table) . " IS '" . $constraint->{COMMENT} . "';\n"; } } } # Default values while (my ($schema, $refschema) = each %{$objects->{SCHEMAS}}) { # Default values foreach my $table (sort keys %{$refschema->{TABLES}}) { foreach my $col (sort keys %{$refschema->{TABLES}->{$table}->{COLS}}) { my $colref = $refschema->{TABLES}->{$table}->{COLS}->{$col}; next unless (defined $colref->{DEFAULT}); my $definition = "ALTER TABLE " . format_identifier($schema) . '.' . format_identifier($table) . " ALTER COLUMN " . format_identifier($col) . " SET DEFAULT " . $colref->{DEFAULT}->{VALUE} . ";\n"; if ($colref->{DEFAULT}->{UNSURE}) { print UNSURE $definition; } else { print AFTER $definition; } } } } # Current values for sequences: autodetect the current max in the table, now that we probably have the indexes while (my ($schema, $refschema) = each %{$objects->{SCHEMAS}}) { foreach my $sequence (sort keys %{$refschema->{SEQUENCES}}) { my $seqref = $refschema->{SEQUENCES}->{$sequence}; # This may not be an identity. Skip it then next unless defined ($seqref->{OWNERCOL}); print AFTER "select setval('" . format_identifier($schema) . '.' . format_identifier($sequence) . "',(select max(". format_identifier($seqref->{OWNERCOL}) .") from " . format_identifier($seqref->{OWNERSCHEMA}) . '.'. format_identifier($seqref->{OWNERTABLE}) . ")::bigint);\n"; } } # Comments on tables and columns while (my ($schema, $refschema) = each %{$objects->{SCHEMAS}}) { # Comments on tables foreach my $table (sort keys %{$refschema->{TABLES}}) { if (defined($refschema->{TABLES}->{$table}->{COMMENT})) { print AFTER "COMMENT ON TABLE " . format_identifier($schema) . '.' . format_identifier($table) . " IS '" . $refschema->{TABLES}->{$table}->{COMMENT} . "';\n"; } foreach my $col (sort keys %{$refschema->{TABLES}->{$table}->{COLS}}) { my $colref = $refschema->{TABLES}->{$table}->{COLS}->{$col}; if (defined($colref->{COMMENT})) { print AFTER "COMMENT ON COLUMN " . format_identifier($schema) . '.' . format_identifier($table) . '.' . format_identifier($col) . " IS '" . $colref->{COMMENT} . "';\n"; } } } } # Views, and their comments # This is different from other objets: we keep the views ordering foreach my $viewref(@view_list) { my ($schema,$view)=@$viewref; my $refschema=$objects->{SCHEMAS}->{$schema}; print UNSURE $refschema->{VIEWS}->{$view}->{SQL}, ";\n"; if (defined $refschema->{VIEWS}->{$view}->{COMMENT}) { print UNSURE "COMMENT ON VIEW $schema.$view IS '" . $refschema->{VIEWS}->{$view}->{COMMENT} . "';\n"; } } # Trigger functions while (my ($schema, $refschema) = each %{$objects->{SCHEMAS}}) { # The trigger functions foreach my $triggerfunc (sort keys %{$refschema->{TRIG_FUNCTIONS}}) { my $code = $refschema->{TRIG_FUNCTIONS}->{$triggerfunc}->{DEF}; my $language = $refschema->{TRIG_FUNCTIONS}->{$triggerfunc}->{LANG}; print UNSURE "CREATE FUNCTION " . format_identifier($schema) . '.' . $triggerfunc . "() RETURNS trigger LANGUAGE $language AS \$def\$\n"; print UNSURE $code; print UNSURE "\$def\$;\n"; } } # Triggers while (my ($schema, $refschema) = each %{$objects->{SCHEMAS}}) { # triggers on tables, as these functions are declared now foreach my $table (sort keys %{$refschema->{TABLES}}) { foreach my $reftrigger (@{$refschema->{TABLES}->{$table}->{TRIGGERS}}) { print UNSURE "CREATE TRIGGER "; print UNSURE $reftrigger->{NAME}; print UNSURE ' '; print UNSURE $reftrigger->{EVENTS}; print UNSURE ' ON '; print UNSURE$schema; print UNSURE '.'; print UNSURE $table; print UNSURE ' '; print UNSURE $reftrigger->{WHEN}; print UNSURE ' execute procedure '; print UNSURE $schema; print UNSURE '.'; print UNSURE $reftrigger->{FUNCTION}; print UNSURE "();\n"; } } } print BEFORE "COMMIT;\n"; print AFTER "COMMIT;\n"; print UNSURE "COMMIT;\n"; close BEFORE; close AFTER; close UNSURE; } # This sub tries to avoid naming conflicts: # Under PostgreSQL, types, tables and indexes share the same namespace # As the table name is the one that will be used directly, types and indexes will be renamed # Print a warning for each renamed type # We do this schema per schema sub resolve_name_conflicts { while (my ($schema, $refschema) = each %{$objects->{SCHEMAS}}) { my %known_names; # Store all known names foreach my $table (keys %{$refschema->{TABLES}}) { $known_names{format_identifier($table)} = 1; } # We scan all types. For now, this tool only generates domains, so we scan domains foreach my $domain (keys %{$refschema->{DOMAINS}}) { if (not defined($known_names{format_identifier($domain)})) { # Great. Just skip to the next and remember this name $known_names{format_identifier($domain)} = 1; } else { # We rename $refschema->{DOMAINS}->{$domain . "2pgd"} = $refschema->{DOMAINS}->{$domain}; delete $refschema->{DOMAINS}->{$domain}; print STDERR "Warning: I had to rename domain $domain to ${domain}2pgd because of naming conflicts between a table and a domain, in source schema $schema\n"; # I also have to check all cols type to rename this while ( my ($tablename,$table) = each %{$refschema->{TABLES}}) { while (my ($colname,$col) =each %{$table->{COLS}}) { # If a column has a custom type, it will be prefixed by schema # The schema will be the destination schema: dbo may have been replaced by public # Be careful that they are stored formatted through format_identifier if ($col->{TYPE} eq (format_identifier($schema) . '.' . format_identifier($domain))) { $col->{TYPE} = format_identifier($schema) . '.' . format_identifier($domain . "2pgd"); } } } $known_names{format_identifier($domain."2pgd")}=1; } } # Then we scan all indexes foreach my $table (keys %{$refschema->{TABLES}}) { foreach my $idx (keys %{$refschema->{TABLES}->{$table}->{INDEXES}}) { if (not defined($known_names{format_identifier($idx)})) { # Great. Just skip to the next and remember this name $known_names{format_identifier($idx)} = 1; } else { my $i = 0; my $postfix = "2pgi"; while (defined($known_names{format_identifier("${idx}2pgi${i}")})) { $i++; } $postfix .= $i; # We have to rename :/ # Postfix with a 2pgi followed by number # We have to update the name in the $refschema hash $refschema->{TABLES}->{$table}->{INDEXES} ->{"${idx}${postfix}"} = $refschema->{TABLES}->{$table}->{INDEXES}->{$idx}; delete $refschema->{TABLES}->{$table}->{INDEXES}->{$idx}; print STDERR "Warning: I had to rename index $table.$idx to ${idx}${postfix} because of naming conflicts in source schema $schema\n"; $known_names{format_identifier("${idx}${postfix}")} = 1; } } } } } # Main # Parse command line my $help = 0; my $options = GetOptions("k=s" => \$kettle, "pi=i" => \$parallelism_in, "po=i" => \$parallelism_out, "b=s" => \$before_file, "a=s" => \$after_file, "u=s" => \$unsure_file, "h" => \$help, "conf=s" => \$conf_file, "sd=s" => \$sd, "sh=s" => \$sh, "si=s" => \$si, "sp=s" => \$sp, "su=s" => \$su, "sw=s" => \$sw, "pd=s" => \$pd, "ph=s" => \$ph, "pp=s" => \$pp, "pu=s" => \$pu, "pw=s" => \$pw, "f=s" => \$filename, "i" => \$case_insensitive, "nr" => \$norelabel_dbo, "num" => \$convert_numeric_to_int, "relabel_schemas=s" => \$relabel_schemas, "keep_identifier_case" =>\$keep_identifier_case, "camel_to_snake" => \$camel_to_snake, "validate_constraints=s" =>\$validate_constraints, "sort_size=i" =>\$sort_size, "use_pk_if_possible=s" =>\$use_pk_if_possible, "ignore_errors" => \$ignore_errors, "pforce_ssl" => \$pforce_ssl); # We don't understand command line or have been asked for usage if (not $options or $help) { usage(); exit 1; } # We have a configuration file. We load it, and set # all we can find in it if ($conf_file) { parse_conf_file(); } # Set default values for anything not set yet set_default_conf_values(); # We have no before, after, or unsure if ( not $before_file or not $after_file or not $unsure_file or not $filename) { usage(); exit 1; } if ($validate_constraints !~ '^(yes|after|no)$') { croak "validate_constraints should be yes, after or no (default yes)\n"; } # We have been asked for kettle, but the compulsory parameters are not there if ($kettle and ( not $sd or not $sh or not $sp or not $su or not defined($sw) # password can be empty, it just has to be defined or not $pd or not $ph or not $pp or not $pu or not defined($pw) # password can be empty, it just has to be defined ) ) { usage(); print "You have to provide all connection information, if using -k or kettle directory set in configuration file\n"; exit 1; } # We need to build %relabel_schemas from $relabel_schemas build_relabel_schemas(); # Read SQL Server's dump file parse_dump(); # Debug, uncomment: #print Dumper($objects); # Rename indexes if they conflict resolve_name_conflicts(); # Create the 3 schema files for PostgreSQL generate_schema($before_file, $after_file, $unsure_file); # If asked, create the kettle job if ($kettle and (defined $ENV{'HOME'} or defined $ENV{'USERPROFILE'})) { check_kettle_properties(); } generate_kettle($kettle) if ($kettle); ##################################################################################################################################### # Begin block to load ugly template variables BEGIN { $template = < __sqlserver_table_name__ Normal 0 /
ID_BATCHYID_BATCHCHANNEL_IDYCHANNEL_IDTRANSNAMEYTRANSNAMESTATUSYSTATUSLINES_READYLINES_READLINES_WRITTENYLINES_WRITTENLINES_UPDATEDYLINES_UPDATEDLINES_INPUTYLINES_INPUTLINES_OUTPUTYLINES_OUTPUTLINES_REJECTEDYLINES_REJECTEDERRORSYERRORSSTARTDATEYSTARTDATEENDDATEYENDDATELOGDATEYLOGDATEDEPDATEYDEPDATEREPLAYDATEYREPLAYDATELOG_FIELDYLOG_FIELD
ID_BATCHYID_BATCHSEQ_NRYSEQ_NRLOGDATEYLOGDATETRANSNAMEYTRANSNAMESTEPNAMEYSTEPNAMESTEP_COPYYSTEP_COPYLINES_READYLINES_READLINES_WRITTENYLINES_WRITTENLINES_UPDATEDYLINES_UPDATEDLINES_INPUTYLINES_INPUTLINES_OUTPUTYLINES_OUTPUTLINES_REJECTEDYLINES_REJECTEDERRORSYERRORSINPUT_BUFFER_ROWSYINPUT_BUFFER_ROWSOUTPUT_BUFFER_ROWSYOUTPUT_BUFFER_ROWS
ID_BATCHYID_BATCHCHANNEL_IDYCHANNEL_IDLOG_DATEYLOG_DATELOGGING_OBJECT_TYPEYLOGGING_OBJECT_TYPEOBJECT_NAMEYOBJECT_NAMEOBJECT_COPYYOBJECT_COPYREPOSITORY_DIRECTORYYREPOSITORY_DIRECTORYFILENAMEYFILENAMEOBJECT_IDYOBJECT_IDOBJECT_REVISIONYOBJECT_REVISIONPARENT_CHANNEL_IDYPARENT_CHANNEL_IDROOT_CHANNEL_IDYROOT_CHANNEL_ID
ID_BATCHYID_BATCHCHANNEL_IDYCHANNEL_IDLOG_DATEYLOG_DATETRANSNAMEYTRANSNAMESTEPNAMEYSTEPNAMESTEP_COPYYSTEP_COPYLINES_READYLINES_READLINES_WRITTENYLINES_WRITTENLINES_UPDATEDYLINES_UPDATEDLINES_INPUTYLINES_INPUTLINES_OUTPUTYLINES_OUTPUTLINES_REJECTEDYLINES_REJECTEDERRORSYERRORSLOG_FIELDNLOG_FIELD
0.0 0.0
200 50 50 N Y 50000 Y N 1000 100 - 2013/02/28 14:04:49.560 - 2013/04/08 11:49:18.185
__sqlserver_db__ __sqlserver_host__ MSSQL Native __sqlserver_database__ __sqlserver_port__ __sqlserver_username__ __sqlserver_password__ FORCE_IDENTIFIERS_TO_LOWERCASEN FORCE_IDENTIFIERS_TO_UPPERCASEN IS_CLUSTEREDN MSSQL_DOUBLE_DECIMAL_SEPARATORN PORT_NUMBER__sqlserver_port__ QUOTE_ALL_FIELDSY SUPPORTS_BOOLEAN_DATA_TYPEN USE_POOLINGN __postgres_db__ __postgres_host__ POSTGRESQL Native __postgres_database__ __postgres_port__ __postgres_username__ __postgres_password__ EXTRA_OPTION_POSTGRESQL.reWriteBatchedInsertstrue __pforce_ssl__ FORCE_IDENTIFIERS_TO_LOWERCASEN FORCE_IDENTIFIERS_TO_UPPERCASEN IS_CLUSTEREDN PORT_NUMBER__postgres_port__ QUOTE_ALL_FIELDSY SUPPORTS_BOOLEAN_DATA_TYPEY USE_POOLINGN SQL_CONNECTset synchronous_commit to off; PRESERVE_RESERVED_WORD_CASEY User Defined Java Class Table output Y Table input Block this step until steps finish Y Block this step until steps finish User Defined Java Class Y Execute SQL script ExecSQL Y 1 none __postgres_db__ N N N N truncate table __postgres_schema_name__.__postgres_table_name__ N 64 160 Y Block this step until steps finish BlockUntilStepsFinish Y 1 none Execute SQL script 0 240 240 Y User Defined Java Class UserDefinedJavaClass Y __PARALLELISM_OUT__ none TRANSFORM_CLASS Processor <![CDATA[import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.Arrays; String[] fieldNames; long numFields; Pattern pattern = Pattern.compile("\\00"); RowMetaInterface inputRowMeta; public boolean processRow(StepMetaInterface smi, StepDataInterface sdi) throws KettleException { // First, get a row from the default input hop // Object[] r = getRow(); // If the row object is null, we are done processing. // if (r == null) { setOutputDone(); return false; } // Let's look up parameters only once for performance reason. // Let's also get field types and names if (first) { inputRowMeta = getInputRowMeta(); fieldNames = inputRowMeta.getFieldNames(); numFields = fieldNames.length; int fieldnum; for (fieldnum = 0; fieldnum < numFields; fieldnum++) { if(inputRowMeta.getValueMeta(fieldnum).getType()!= ValueMetaInterface.TYPE_STRING) { fieldNames[fieldnum]="-1"; } } first=false; } Object[] outputRow = createOutputRow(r, data.outputRowMeta.size()); // Object[] outputRow = RowDataUtil.createResizedCopy(r, data.outputRowMeta.size()); int fieldnum; for (fieldnum = 0; fieldnum < numFields; fieldnum++) { if (!(fieldNames[fieldnum].equals("-1"))){ String inputStr = get(Fields.In,fieldNames[fieldnum]).getString(r); if (inputStr != null) { // else null pointer execption in regexp Matcher matcher = pattern.matcher(inputStr); String newfield = matcher.replaceAll(""); get(Fields.Out,fieldNames[fieldnum]).setValue(outputRow,newfield); } } } // putRow will send the row on to the default output hop. // putRow(data.outputRowMeta, outputRow); return true; }]]> N 280 332 Y Table input TableInput Y 1 none __sqlserver_db__ SELECT __sqlserver_table_cols__ FROM __sqlserver_table_name__ WITH(NOLOCK) 0 N Y N 182 160 Y Table output TableOutput Y __PARALLELISM_OUT__ none __postgres_db__ __postgres_schema_name__ __postgres_table_name__
100 N N Y N N N Y N Y N 371 158 Y
N EOF #################################################################################### $template_lob = < __sqlserver_table_name__ Normal 0 /
ID_BATCHYID_BATCHCHANNEL_IDYCHANNEL_IDTRANSNAMEYTRANSNAMESTATUSYSTATUSLINES_READYLINES_READLINES_WRITTENYLINES_WRITTENLINES_UPDATEDYLINES_UPDATEDLINES_INPUTYLINES_INPUTLINES_OUTPUTYLINES_OUTPUTLINES_REJECTEDYLINES_REJECTEDERRORSYERRORSSTARTDATEYSTARTDATEENDDATEYENDDATELOGDATEYLOGDATEDEPDATEYDEPDATEREPLAYDATEYREPLAYDATELOG_FIELDYLOG_FIELD
ID_BATCHYID_BATCHSEQ_NRYSEQ_NRLOGDATEYLOGDATETRANSNAMEYTRANSNAMESTEPNAMEYSTEPNAMESTEP_COPYYSTEP_COPYLINES_READYLINES_READLINES_WRITTENYLINES_WRITTENLINES_UPDATEDYLINES_UPDATEDLINES_INPUTYLINES_INPUTLINES_OUTPUTYLINES_OUTPUTLINES_REJECTEDYLINES_REJECTEDERRORSYERRORSINPUT_BUFFER_ROWSYINPUT_BUFFER_ROWSOUTPUT_BUFFER_ROWSYOUTPUT_BUFFER_ROWS
ID_BATCHYID_BATCHCHANNEL_IDYCHANNEL_IDLOG_DATEYLOG_DATELOGGING_OBJECT_TYPEYLOGGING_OBJECT_TYPEOBJECT_NAMEYOBJECT_NAMEOBJECT_COPYYOBJECT_COPYREPOSITORY_DIRECTORYYREPOSITORY_DIRECTORYFILENAMEYFILENAMEOBJECT_IDYOBJECT_IDOBJECT_REVISIONYOBJECT_REVISIONPARENT_CHANNEL_IDYPARENT_CHANNEL_IDROOT_CHANNEL_IDYROOT_CHANNEL_ID
ID_BATCHYID_BATCHCHANNEL_IDYCHANNEL_IDLOG_DATEYLOG_DATETRANSNAMEYTRANSNAMESTEPNAMEYSTEPNAMESTEP_COPYYSTEP_COPYLINES_READYLINES_READLINES_WRITTENYLINES_WRITTENLINES_UPDATEDYLINES_UPDATEDLINES_INPUTYLINES_INPUTLINES_OUTPUTYLINES_OUTPUTLINES_REJECTEDYLINES_REJECTEDERRORSYERRORSLOG_FIELDNLOG_FIELD
0.0 0.0
10 50 50 N Y 1000 Y N 1000 100 - 2013/02/28 14:04:49.560 - 2013/04/08 11:49:18.185
__sqlserver_db__ __sqlserver_host__ MSSQL Native __sqlserver_database__ __sqlserver_port__ __sqlserver_username__ __sqlserver_password__ FORCE_IDENTIFIERS_TO_LOWERCASEN FORCE_IDENTIFIERS_TO_UPPERCASEN IS_CLUSTEREDN MSSQL_DOUBLE_DECIMAL_SEPARATORN PORT_NUMBER__sqlserver_port__ QUOTE_ALL_FIELDSY SUPPORTS_BOOLEAN_DATA_TYPEN USE_POOLINGN __postgres_db__ __postgres_host__ POSTGRESQL Native __postgres_database__ __postgres_port__ __postgres_username__ __postgres_password__ EXTRA_OPTION_POSTGRESQL.reWriteBatchedInsertstrue __pforce_ssl__ FORCE_IDENTIFIERS_TO_LOWERCASEN FORCE_IDENTIFIERS_TO_UPPERCASEN IS_CLUSTEREDN PORT_NUMBER__postgres_port__ QUOTE_ALL_FIELDSY SUPPORTS_BOOLEAN_DATA_TYPEY USE_POOLINGN SQL_CONNECTset synchronous_commit to off; PRESERVE_RESERVED_WORD_CASEY User Defined Java Class Table output Y Table input Block this step until steps finish Y Block this step until steps finish User Defined Java Class Y Execute SQL script ExecSQL Y 1 none __postgres_db__ N N N N truncate table __postgres_schema_name__.__postgres_table_name__ N 64 160 Y Block this step until steps finish BlockUntilStepsFinish Y 1 none Execute SQL script 0 240 240 Y User Defined Java Class UserDefinedJavaClass Y __PARALLELISM_OUT__ none TRANSFORM_CLASS Processor <![CDATA[import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.Arrays; String[] fieldNames; long numFields; Pattern pattern = Pattern.compile("\\00"); RowMetaInterface inputRowMeta; public boolean processRow(StepMetaInterface smi, StepDataInterface sdi) throws KettleException { // First, get a row from the default input hop // Object[] r = getRow(); // If the row object is null, we are done processing. // if (r == null) { setOutputDone(); return false; } // Let's look up parameters only once for performance reason. // Let's also get field types and names if (first) { inputRowMeta = getInputRowMeta(); fieldNames = inputRowMeta.getFieldNames(); numFields = fieldNames.length; int fieldnum; for (fieldnum = 0; fieldnum < numFields; fieldnum++) { if(inputRowMeta.getValueMeta(fieldnum).getType()!= ValueMetaInterface.TYPE_STRING) { fieldNames[fieldnum]="-1"; } } first=false; } Object[] outputRow = createOutputRow(r, data.outputRowMeta.size()); // Object[] outputRow = RowDataUtil.createResizedCopy(r, data.outputRowMeta.size()); int fieldnum; for (fieldnum = 0; fieldnum < numFields; fieldnum++) { if (!(fieldNames[fieldnum].equals("-1"))){ String inputStr = get(Fields.In,fieldNames[fieldnum]).getString(r); if (inputStr != null) { // else null pointer execption in regexp Matcher matcher = pattern.matcher(inputStr); String newfield = matcher.replaceAll(""); get(Fields.Out,fieldNames[fieldnum]).setValue(outputRow,newfield); } } } // putRow will send the row on to the default output hop. // putRow(data.outputRowMeta, outputRow); return true; }]]> N 280 332 Y Table input TableInput Y __PARALLELISM_IN__ none __sqlserver_db__ SELECT __sqlserver_table_cols__ FROM __sqlserver_table_name__ WITH(NOLOCK) __sqlserver_where_filter__ 0 N Y N 182 160 Y Table output TableOutput Y __PARALLELISM_OUT__ none __postgres_db__ __postgres_schema_name__ __postgres_table_name__
100 N N Y N N N Y N Y N 371 158 Y
N EOF #################################################################################### $job_header = < Migration 0 / - 2013/04/08 14:05:05.685 - 2013/04/08 14:08:27.625 __postgres_db__ __postgres_host__ POSTGRESQL Native __postgres_database__ __postgres_port__ __postgres_username__ __postgres_password__ EXTRA_OPTION_POSTGRESQL.reWriteBatchedInsertstrue __pforce_ssl__ FORCE_IDENTIFIERS_TO_LOWERCASEN FORCE_IDENTIFIERS_TO_UPPERCASEN IS_CLUSTEREDN PORT_NUMBER__postgres_port__ QUOTE_ALL_FIELDSY SUPPORTS_BOOLEAN_DATA_TYPEY USE_POOLINGN SQL_CONNECTset synchronous_commit to off; PRESERVE_RESERVED_WORD_CASEY
ID_JOBYID_JOBCHANNEL_IDYCHANNEL_IDJOBNAMEYJOBNAMESTATUSYSTATUSLINES_READYLINES_READLINES_WRITTENYLINES_WRITTENLINES_UPDATEDYLINES_UPDATEDLINES_INPUTYLINES_INPUTLINES_OUTPUTYLINES_OUTPUTLINES_REJECTEDYLINES_REJECTEDERRORSYERRORSSTARTDATEYSTARTDATEENDDATEYENDDATELOGDATEYLOGDATEDEPDATEYDEPDATEREPLAYDATEYREPLAYDATELOG_FIELDYLOG_FIELD
ID_BATCHYID_BATCHCHANNEL_IDYCHANNEL_IDLOG_DATEYLOG_DATEJOBNAMEYTRANSNAMEJOBENTRYNAMEYSTEPNAMELINES_READYLINES_READLINES_WRITTENYLINES_WRITTENLINES_UPDATEDYLINES_UPDATEDLINES_INPUTYLINES_INPUTLINES_OUTPUTYLINES_OUTPUTLINES_REJECTEDYLINES_REJECTEDERRORSYERRORSRESULTYRESULTNR_RESULT_ROWSYNR_RESULT_ROWSNR_RESULT_FILESYNR_RESULT_FILESLOG_FIELDNLOG_FIELDCOPY_NRNCOPY_NR
ID_BATCHYID_BATCHCHANNEL_IDYCHANNEL_IDLOG_DATEYLOG_DATELOGGING_OBJECT_TYPEYLOGGING_OBJECT_TYPEOBJECT_NAMEYOBJECT_NAMEOBJECT_COPYYOBJECT_COPYREPOSITORY_DIRECTORYYREPOSITORY_DIRECTORYFILENAMEYFILENAMEOBJECT_IDYOBJECT_IDOBJECT_REVISIONYOBJECT_REVISIONPARENT_CHANNEL_IDYPARENT_CHANNEL_IDROOT_CHANNEL_IDYROOT_CHANNEL_ID
N START SPECIAL Y N N 0 0 60 12 0 1 1 N Y 0 38 73 SQL SCRIPT START SQL __SQL_SCRIPT_INIT__ F F F __postgres_db__ N Y 0 38 140 SQL SCRIPT END SQL __SQL_SCRIPT_END__ F F F __postgres_db__ N Y 0 38 200 EOF #################################################################################### $job_middle = < EOF #################################################################################### $job_footer = < EOF #################################################################################### $job_entry = < __table_name__ TRANS filename __file_name__ N N N N N N N N Basic N N Y N N Y N Y 0 197 __y_loc__ EOF #################################################################################### $job_hop = < __table_1__ __table_2__ 0 0 Y Y N EOF #################################################################################### $incremental_template= < migration__sqlserver_table_name__ Normal 0 /
ID_BATCHYID_BATCHCHANNEL_IDYCHANNEL_IDTRANSNAMEYTRANSNAMESTATUSYSTATUSLINES_READYLINES_READLINES_WRITTENYLINES_WRITTENLINES_UPDATEDYLINES_UPDATEDLINES_INPUTYLINES_INPUTLINES_OUTPUTYLINES_OUTPUTLINES_REJECTEDYLINES_REJECTEDERRORSYERRORSSTARTDATEYSTARTDATEENDDATEYENDDATELOGDATEYLOGDATEDEPDATEYDEPDATEREPLAYDATEYREPLAYDATELOG_FIELDYLOG_FIELD
ID_BATCHYID_BATCHSEQ_NRYSEQ_NRLOGDATEYLOGDATETRANSNAMEYTRANSNAMESTEPNAMEYSTEPNAMESTEP_COPYYSTEP_COPYLINES_READYLINES_READLINES_WRITTENYLINES_WRITTENLINES_UPDATEDYLINES_UPDATEDLINES_INPUTYLINES_INPUTLINES_OUTPUTYLINES_OUTPUTLINES_REJECTEDYLINES_REJECTEDERRORSYERRORSINPUT_BUFFER_ROWSYINPUT_BUFFER_ROWSOUTPUT_BUFFER_ROWSYOUTPUT_BUFFER_ROWS
ID_BATCHYID_BATCHCHANNEL_IDYCHANNEL_IDLOG_DATEYLOG_DATELOGGING_OBJECT_TYPEYLOGGING_OBJECT_TYPEOBJECT_NAMEYOBJECT_NAMEOBJECT_COPYYOBJECT_COPYREPOSITORY_DIRECTORYYREPOSITORY_DIRECTORYFILENAMEYFILENAMEOBJECT_IDYOBJECT_IDOBJECT_REVISIONYOBJECT_REVISIONPARENT_CHANNEL_IDYPARENT_CHANNEL_IDROOT_CHANNEL_IDYROOT_CHANNEL_ID
ID_BATCHYID_BATCHCHANNEL_IDYCHANNEL_IDLOG_DATEYLOG_DATETRANSNAMEYTRANSNAMESTEPNAMEYSTEPNAMESTEP_COPYYSTEP_COPYLINES_READYLINES_READLINES_WRITTENYLINES_WRITTENLINES_UPDATEDYLINES_UPDATEDLINES_INPUTYLINES_INPUTLINES_OUTPUTYLINES_OUTPUTLINES_REJECTEDYLINES_REJECTEDERRORSYERRORSLOG_FIELDNLOG_FIELD
0.0 0.0
200 50 50 N Y 50000 Y N 1000 100 - 2013/02/28 14:04:49.560 - 2014/08/26 15:16:59.019
__postgres_db__ __postgres_host__ POSTGRESQL Native __postgres_database__ __postgres_port__ __postgres_username__ __postgres_password__ EXTRA_OPTION_POSTGRESQL.reWriteBatchedInsertstrue __pforce_ssl__ FORCE_IDENTIFIERS_TO_LOWERCASEN FORCE_IDENTIFIERS_TO_UPPERCASEN IS_CLUSTEREDN PORT_NUMBER__postgres_port__ QUOTE_ALL_FIELDSY SQL_CONNECTset synchronous_commit to off; SUPPORTS_BOOLEAN_DATA_TYPEY USE_POOLINGN __sqlserver_db__ __sqlserver_host__ MSSQL Native __sqlserver_database__ __sqlserver_port__ __sqlserver_username__ __sqlserver_password__ FORCE_IDENTIFIERS_TO_LOWERCASEN FORCE_IDENTIFIERS_TO_UPPERCASEN IS_CLUSTEREDN MSSQL_DOUBLE_DECIMAL_SEPARATORN PORT_NUMBER__sqlserver_port__ QUOTE_ALL_FIELDSY SUPPORTS_BOOLEAN_DATA_TYPEN USE_POOLINGN Table input 2User Defined Java ClassY User Defined Java ClassSort rows 2Y Sort rows 2Sorted Merge 2Y Table inputSort rowsY Sort rowsSorted MergeY Sorted MergeMerge Rows (diff)Y Sorted Merge 2Merge Rows (diff)Y Merge Rows (diff)Synchronize after mergeY Table input 2 TableInput Y 1 none __sqlserver_db__ SELECT __sqlserver_table_cols__ FROM __sqlserver_table_name__ WITH(NOLOCK) 0 N Y N 122 250 Y User Defined Java Class UserDefinedJavaClass Y __PARALLELISM_OUT__ none TRANSFORM_CLASS Processor <![CDATA[import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.Arrays; String[] fieldNames; long numFields; Pattern pattern = Pattern.compile("\\00"); RowMetaInterface inputRowMeta; public boolean processRow(StepMetaInterface smi, StepDataInterface sdi) throws KettleException { // First, get a row from the default input hop // Object[] r = getRow(); // If the row object is null, we are done processing. // if (r == null) { setOutputDone(); return false; } // Let's look up parameters only once for performance reason. // Let's also get field types and names if (first) { inputRowMeta = getInputRowMeta(); fieldNames = inputRowMeta.getFieldNames(); numFields = fieldNames.length; int fieldnum; for (fieldnum = 0; fieldnum < numFields; fieldnum++) { if(inputRowMeta.getValueMeta(fieldnum).getType()!= ValueMetaInterface.TYPE_STRING) { fieldNames[fieldnum]="-1"; } } first=false; } Object[] outputRow = createOutputRow(r, data.outputRowMeta.size()); // Object[] outputRow = RowDataUtil.createResizedCopy(r, data.outputRowMeta.size()); int fieldnum; for (fieldnum = 0; fieldnum < numFields; fieldnum++) { if (!(fieldNames[fieldnum].equals("-1"))){ String inputStr = get(Fields.In,fieldNames[fieldnum]).getString(r); if (inputStr != null) { // else null pointer execption in regexp Matcher matcher = pattern.matcher(inputStr); String newfield = matcher.replaceAll(""); get(Fields.Out,fieldNames[fieldnum]).setValue(outputRow,newfield); } } } // putRow will send the row on to the default output hop. // putRow(data.outputRowMeta, outputRow); return true; }]]> N 280 332 Y Table input TableInput Y 1 none __postgres_db__ SELECT __postgres_table_cols__ FROM __postgres_schema_name__.__postgres_table_name__ 0 N N N 122 150 Y Sort rows SortRows Y __PARALLELISM_OUT__ none %%java.io.tmpdir%% out __sort_size__ N N __SORT_KEYS_PG__ 351 161 Y Sort rows 2 SortRows Y __PARALLELISM_OUT__ none %%java.io.tmpdir%% out __sort_size__ N N __SORT_KEYS_SQLSERVER__ 352 253 Y Sorted Merge SortedMerge Y 1 none __SORT_KEYS_PG__ 428 161 Y Sorted Merge 2 SortedMerge Y 1 none __SORT_KEYS_SQLSERVER__ 429 245 Y Synchronize after merge SynchronizeAfterMerge Y __PARALLELISM_OUT__ none __postgres_db__ 100 N N N __changed__ new changed deleted __postgres_schema_name__ __postgres_table_name__
__KEYS_SYNC__ __VALUES_SYNC__
700 212 Y
Merge Rows (diff) MergeRows Y 1 none __KEYS_MERGE__ __VALUES_MERGE__ __changed__ Sorted Merge Sorted Merge 2 506 212 Y N EOF #################################################################################### $incremental_template_sortable_pk= < migration__sqlserver_table_name__ Normal 0 /
ID_BATCHYID_BATCHCHANNEL_IDYCHANNEL_IDTRANSNAMEYTRANSNAMESTATUSYSTATUSLINES_READYLINES_READLINES_WRITTENYLINES_WRITTENLINES_UPDATEDYLINES_UPDATEDLINES_INPUTYLINES_INPUTLINES_OUTPUTYLINES_OUTPUTLINES_REJECTEDYLINES_REJECTEDERRORSYERRORSSTARTDATEYSTARTDATEENDDATEYENDDATELOGDATEYLOGDATEDEPDATEYDEPDATEREPLAYDATEYREPLAYDATELOG_FIELDYLOG_FIELD
ID_BATCHYID_BATCHSEQ_NRYSEQ_NRLOGDATEYLOGDATETRANSNAMEYTRANSNAMESTEPNAMEYSTEPNAMESTEP_COPYYSTEP_COPYLINES_READYLINES_READLINES_WRITTENYLINES_WRITTENLINES_UPDATEDYLINES_UPDATEDLINES_INPUTYLINES_INPUTLINES_OUTPUTYLINES_OUTPUTLINES_REJECTEDYLINES_REJECTEDERRORSYERRORSINPUT_BUFFER_ROWSYINPUT_BUFFER_ROWSOUTPUT_BUFFER_ROWSYOUTPUT_BUFFER_ROWS
ID_BATCHYID_BATCHCHANNEL_IDYCHANNEL_IDLOG_DATEYLOG_DATELOGGING_OBJECT_TYPEYLOGGING_OBJECT_TYPEOBJECT_NAMEYOBJECT_NAMEOBJECT_COPYYOBJECT_COPYREPOSITORY_DIRECTORYYREPOSITORY_DIRECTORYFILENAMEYFILENAMEOBJECT_IDYOBJECT_IDOBJECT_REVISIONYOBJECT_REVISIONPARENT_CHANNEL_IDYPARENT_CHANNEL_IDROOT_CHANNEL_IDYROOT_CHANNEL_ID
ID_BATCHYID_BATCHCHANNEL_IDYCHANNEL_IDLOG_DATEYLOG_DATETRANSNAMEYTRANSNAMESTEPNAMEYSTEPNAMESTEP_COPYYSTEP_COPYLINES_READYLINES_READLINES_WRITTENYLINES_WRITTENLINES_UPDATEDYLINES_UPDATEDLINES_INPUTYLINES_INPUTLINES_OUTPUTYLINES_OUTPUTLINES_REJECTEDYLINES_REJECTEDERRORSYERRORSLOG_FIELDNLOG_FIELD
0.0 0.0
200 50 50 N Y 50000 Y N 1000 100 - 2013/02/28 14:04:49.560 - 2014/08/26 15:16:59.019
__postgres_db__ __postgres_host__ POSTGRESQL Native __postgres_database__ __postgres_port__ __postgres_username__ __postgres_password__ EXTRA_OPTION_POSTGRESQL.reWriteBatchedInsertstrue __pforce_ssl__ FORCE_IDENTIFIERS_TO_LOWERCASEN FORCE_IDENTIFIERS_TO_UPPERCASEN IS_CLUSTEREDN PORT_NUMBER__postgres_port__ QUOTE_ALL_FIELDSY SQL_CONNECTset synchronous_commit to off; SUPPORTS_BOOLEAN_DATA_TYPEY USE_POOLINGN __sqlserver_db__ __sqlserver_host__ MSSQL Native __sqlserver_database__ __sqlserver_port__ __sqlserver_username__ __sqlserver_password__ FORCE_IDENTIFIERS_TO_LOWERCASEN FORCE_IDENTIFIERS_TO_UPPERCASEN IS_CLUSTEREDN MSSQL_DOUBLE_DECIMAL_SEPARATORN PORT_NUMBER__sqlserver_port__ QUOTE_ALL_FIELDSY SUPPORTS_BOOLEAN_DATA_TYPEN USE_POOLINGN Table input 2User Defined Java ClassY User Defined Java ClassMerge Rows (diff)Y Table inputMerge Rows (diff)Y Merge Rows (diff)Synchronize after mergeY Table input 2 TableInput Y 1 none __sqlserver_db__ SELECT __sqlserver_table_cols__ FROM __sqlserver_table_name__ WITH(NOLOCK) ORDER BY __sqlserver_pk_condition__ 0 N Y N 122 250 Y User Defined Java Class UserDefinedJavaClass Y 1 none TRANSFORM_CLASS Processor <![CDATA[import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.Arrays; String[] fieldNames; long numFields; Pattern pattern = Pattern.compile("\\00"); RowMetaInterface inputRowMeta; public boolean processRow(StepMetaInterface smi, StepDataInterface sdi) throws KettleException { // First, get a row from the default input hop // Object[] r = getRow(); // If the row object is null, we are done processing. // if (r == null) { setOutputDone(); return false; } // Let's look up parameters only once for performance reason. // Let's also get field types and names if (first) { inputRowMeta = getInputRowMeta(); fieldNames = inputRowMeta.getFieldNames(); numFields = fieldNames.length; int fieldnum; for (fieldnum = 0; fieldnum < numFields; fieldnum++) { if(inputRowMeta.getValueMeta(fieldnum).getType()!= ValueMetaInterface.TYPE_STRING) { fieldNames[fieldnum]="-1"; } } first=false; } Object[] outputRow = createOutputRow(r, data.outputRowMeta.size()); // Object[] outputRow = RowDataUtil.createResizedCopy(r, data.outputRowMeta.size()); int fieldnum; for (fieldnum = 0; fieldnum < numFields; fieldnum++) { if (!(fieldNames[fieldnum].equals("-1"))){ String inputStr = get(Fields.In,fieldNames[fieldnum]).getString(r); if (inputStr != null) { // else null pointer execption in regexp Matcher matcher = pattern.matcher(inputStr); String newfield = matcher.replaceAll(""); get(Fields.Out,fieldNames[fieldnum]).setValue(outputRow,newfield); } } } // putRow will send the row on to the default output hop. // putRow(data.outputRowMeta, outputRow); return true; }]]> N 280 332 Y Table input TableInput Y 1 none __postgres_db__ SELECT __postgres_table_cols__ FROM __postgres_schema_name__.__postgres_table_name__ ORDER BY __pg_pk_condition__ 0 N N N 122 150 Y Synchronize after merge SynchronizeAfterMerge Y __PARALLELISM_OUT__ none __postgres_db__ 100 N N N __changed__ new changed deleted __postgres_schema_name__ __postgres_table_name__
__KEYS_SYNC__ __VALUES_SYNC__
700 212 Y
Merge Rows (diff) MergeRows Y 1 none __KEYS_MERGE__ __VALUES_MERGE__ __changed__ Table input User Defined Java Class 506 212 Y N EOF #################################################################################### }