# --
# Copyright (C) 2001-2021 OTRS AG, https://otrs.com/
# Copyright (C) 2021 Znuny GmbH, https://znuny.org/
# --
# This software comes with ABSOLUTELY NO WARRANTY. For details, see
# the enclosed file COPYING for license information (GPL). If you
# did not receive this file, see https://www.gnu.org/licenses/gpl-3.0.txt.
# --

package Kernel::System::DB::oracle;

use strict;
use warnings;

our @ObjectDependencies = (
    'Kernel::Config',
    'Kernel::System::DateTime',
    'Kernel::System::Log',
    'Kernel::System::Main',
);

sub new {
    my ( $Type, %Param ) = @_;

    # allocate new hash for object
    my $Self = {%Param};
    bless( $Self, $Type );

    return $Self;
}

sub LoadPreferences {
    my ( $Self, %Param ) = @_;

    # db settings
    $Self->{'DB::Limit'}                       = 0;
    $Self->{'DB::DirectBlob'}                  = 0;
    $Self->{'DB::QuoteSingle'}                 = '\'';
    $Self->{'DB::QuoteBack'}                   = 0;
    $Self->{'DB::QuoteSemicolon'}              = '';
    $Self->{'DB::QuoteUnderscoreStart'}        = '\\';
    $Self->{'DB::QuoteUnderscoreEnd'}          = '';
    $Self->{'DB::CaseSensitive'}               = 1;
    $Self->{'DB::LikeEscapeString'}            = q{ESCAPE '\\'};
    $Self->{'DB::MaxParamCountForInCondition'} = 1000;

    # how to determine server version
    $Self->{'DB::Version'}
        = "SELECT CONCAT('Oracle ', version) FROM product_component_version WHERE product LIKE 'Oracle Database%'";

    $Self->{'DB::ListTables'} = 'SELECT table_name FROM user_tables ORDER BY table_name';

    # dbi attributes
    $Self->{'DB::Attribute'} = {
        LongTruncOk => 1,
        LongReadLen => 40 * 1024 * 1024,
    };

    # set current time stamp if different to "current_timestamp"
    $Self->{'DB::CurrentTimestamp'} = '';

    # set encoding of selected data to utf8
    $Self->{'DB::Encode'} = 0;

    # shell setting
    $Self->{'DB::Comment'}      = '-- ';
    $Self->{'DB::ShellCommit'}  = ';';
    $Self->{'DB::ShellConnect'} = "SET DEFINE OFF;\nSET SQLBLANKLINES ON";    # must be on separate lines!

    # init sql setting on db connect
    $Self->{'DB::Connect'} = "ALTER SESSION SET NLS_DATE_FORMAT = 'YYYY-MM-DD HH24:MI:SS'";

    return 1;
}

sub Quote {
    my ( $Self, $Text, $Type ) = @_;

    if ( defined ${$Text} ) {
        if ( $Self->{'DB::QuoteBack'} ) {
            ${$Text} =~ s/\\/$Self->{'DB::QuoteBack'}\\/g;
        }
        if ( $Self->{'DB::QuoteSingle'} ) {
            ${$Text} =~ s/'/$Self->{'DB::QuoteSingle'}'/g;
        }
        if ( $Self->{'DB::QuoteSemicolon'} ) {
            ${$Text} =~ s/;/$Self->{'DB::QuoteSemicolon'};/g;
        }
        if ( $Type && $Type eq 'Like' ) {
            if ( $Self->{'DB::QuoteUnderscoreStart'} || $Self->{'DB::QuoteUnderscoreEnd'} ) {
                ${$Text}
                    =~ s/_/$Self->{'DB::QuoteUnderscoreStart'}_$Self->{'DB::QuoteUnderscoreEnd'}/g;
            }
        }
    }
    return $Text;
}

sub DatabaseCreate {
    my ( $Self, %Param ) = @_;

    # check needed stuff
    if ( !$Param{Name} ) {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'error',
            Message  => 'Need Name!'
        );
        return;
    }

    # return SQL
    return ("CREATE DATABASE $Param{Name}");
}

sub DatabaseDrop {
    my ( $Self, %Param ) = @_;

    # check needed stuff
    if ( !$Param{Name} ) {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'error',
            Message  => 'Need Name!'
        );
        return;
    }

    # return SQL
    return ("DROP DATABASE $Param{Name}");
}

sub TableCreate {
    my ( $Self, @Param ) = @_;

    # get needed objects
    my $ConfigObject = $Kernel::OM->Get('Kernel::Config');
    my $MainObject   = $Kernel::OM->Get('Kernel::System::Main');

    my $SQLStart     = '';
    my $SQLEnd       = '';
    my $SQL          = '';
    my @Column       = ();
    my $TableName    = '';
    my $ForeignKey   = ();
    my %Foreign      = ();
    my $IndexCurrent = ();
    my %Index        = ();
    my $UniqCurrent  = ();
    my %Uniq         = ();
    my $PrimaryKey   = '';
    my @Return       = ();
    my @Return2      = ();

    for my $Tag (@Param) {

        if (
            ( $Tag->{Tag} eq 'Table' || $Tag->{Tag} eq 'TableCreate' )
            && $Tag->{TagType} eq 'Start'
            )
        {
            if ( $ConfigObject->Get('Database::ShellOutput') ) {
                $SQLStart .= $Self->{'DB::Comment'}
                    . "----------------------------------------------------------\n";
                $SQLStart .= $Self->{'DB::Comment'} . " create table $Tag->{Name}\n";
                $SQLStart .= $Self->{'DB::Comment'}
                    . "----------------------------------------------------------\n";
            }
        }
        if ( $Tag->{Tag} eq 'Table' || $Tag->{Tag} eq 'TableCreate' ) {
            if ( $Tag->{TagType} eq 'Start' ) {
                $SQLStart .= "CREATE TABLE $Tag->{Name} (\n";
                $TableName = $Tag->{Name};
            }
            elsif ( $Tag->{TagType} eq 'End' ) {
                $SQLEnd .= "\n)";
            }
        }
        elsif ( $Tag->{Tag} eq 'Column' && $Tag->{TagType} eq 'Start' ) {
            push @Column, $Tag;
        }
        elsif ( $Tag->{Tag} eq 'Index' && $Tag->{TagType} eq 'Start' ) {
            $IndexCurrent = $Tag->{Name};
        }
        elsif ( $Tag->{Tag} eq 'IndexColumn' && $Tag->{TagType} eq 'Start' ) {
            push @{ $Index{$IndexCurrent} }, $Tag;
        }
        elsif ( $Tag->{Tag} eq 'Unique' && $Tag->{TagType} eq 'Start' ) {
            $UniqCurrent = $Tag->{Name} || $TableName . '_U_' . int( rand(999) );
        }
        elsif ( $Tag->{Tag} eq 'UniqueColumn' && $Tag->{TagType} eq 'Start' ) {
            push @{ $Uniq{$UniqCurrent} }, $Tag;
        }
        elsif ( $Tag->{Tag} eq 'ForeignKey' && $Tag->{TagType} eq 'Start' ) {
            $ForeignKey = $Tag->{ForeignTable};
        }
        elsif ( $Tag->{Tag} eq 'Reference' && $Tag->{TagType} eq 'Start' ) {
            push @{ $Foreign{$ForeignKey} }, $Tag;
        }
    }
    for my $Tag (@Column) {

        # type translation
        $Tag = $Self->_TypeTranslation($Tag);

        # add new line
        if ($SQL) {
            $SQL .= ",\n";
        }

        # normal data type
        $SQL .= "    $Tag->{Name} $Tag->{Type}";

        # handle default
        my $DisableNotNull;
        if ( defined $Tag->{Default} ) {
            if ( $Tag->{Type} =~ m{ (int|number) }xmsi ) {
                $SQL .= " DEFAULT " . $Tag->{Default};
            }
            elsif ( $Tag->{Default} ) {
                $SQL .= " DEFAULT '" . $Tag->{Default} . "'";
            }
            else {
                $DisableNotNull = 1;
            }
        }

        # handle require
        if ( !$DisableNotNull && $Tag->{Required} && lc $Tag->{Required} eq 'true' ) {
            $SQL .= ' NOT NULL';
        }
        else {
            $SQL .= ' NULL';
        }

        # add primary key
        if ( $Tag->{PrimaryKey} && $Tag->{PrimaryKey} =~ /true/i ) {

            my $Constraint = $Self->_ConstraintName(
                TableName => $TableName,
            );

            push @Return2,
                "ALTER TABLE $TableName ADD CONSTRAINT $Constraint PRIMARY KEY ($Tag->{Name})";
        }

        # auto increment
        if ( $Tag->{AutoIncrement} && $Tag->{AutoIncrement} =~ /^true$/i ) {

            my $Sequence = $Self->_SequenceName(
                TableName => $TableName,
            );

            my $Trigger = $Sequence . '_t';

            my $Shell = '';
            if ( $ConfigObject->Get('Database::ShellOutput') ) {
                $Shell = "/\n--";
            }

            push @Return2, <<"EOF";
BEGIN
    EXECUTE IMMEDIATE 'DROP SEQUENCE $Sequence';
EXCEPTION
    WHEN OTHERS THEN NULL;
END;
$Shell
EOF

            push @Return2, <<"EOF";
CREATE SEQUENCE $Sequence
INCREMENT BY 1
START WITH 1
NOMAXVALUE
NOCYCLE
CACHE 20
ORDER
EOF

            push @Return2, <<"EOF";
BEGIN
    EXECUTE IMMEDIATE 'DROP TRIGGER $Trigger';
EXCEPTION
    WHEN OTHERS THEN NULL;
END;
$Shell
EOF

            push @Return2, <<"EOF";
CREATE OR REPLACE TRIGGER $Trigger
BEFORE INSERT ON $TableName
FOR EACH ROW
BEGIN
    IF :new.$Tag->{Name} IS NULL THEN
        SELECT $Sequence.nextval
        INTO :new.$Tag->{Name}
        FROM DUAL;
    END IF;
END;
$Shell
EOF

        }
    }

    # add unique
    for my $Name ( sort keys %Uniq ) {

        my $Unique = $Self->_UniqueName(
            Name => $Name,
        );

        if ($SQL) {
            $SQL .= ",\n";
        }
        $SQL .= "    CONSTRAINT $Unique UNIQUE (";
        my @Array = @{ $Uniq{$Name} };
        my $Name  = '';
        for my $Index ( 0 .. $#Array ) {
            if ( $Index > 0 ) {
                $SQL .= ', ';
            }
            $SQL  .= $Array[$Index]->{Name};
            $Name .= '_' . $Array[$Index]->{Name};
        }
        $SQL .= ')';
    }
    push @Return, $SQLStart . $SQL . $SQLEnd, @Return2;

    # add foreign keys
    for my $ForeignKey ( sort keys %Foreign ) {
        my @Array = @{ $Foreign{$ForeignKey} };
        for my $Index ( 0 .. $#Array ) {
            push @{ $Self->{Post} },
                $Self->ForeignKeyCreate(
                LocalTableName   => $TableName,
                Local            => $Array[$Index]->{Local},
                ForeignTableName => $ForeignKey,
                Foreign          => $Array[$Index]->{Foreign},
                );
        }
    }

    # add indexes
    for my $Name ( sort keys %Index ) {
        push @Return,
            $Self->IndexCreate(
            TableName => $TableName,
            Name      => $Name,
            Data      => $Index{$Name},
            );
    }

    return @Return;
}

sub TableDrop {
    my ( $Self, @Param ) = @_;

    # get config object
    my $ConfigObject = $Kernel::OM->Get('Kernel::Config');

    my $SQL = '';
    for my $Tag (@Param) {
        if ( $Tag->{Tag} eq 'Table' && $Tag->{TagType} eq 'Start' ) {
            if ( $ConfigObject->Get('Database::ShellOutput') ) {
                $SQL .= $Self->{'DB::Comment'}
                    . "----------------------------------------------------------\n";
                $SQL .= $Self->{'DB::Comment'} . " drop table $Tag->{Name}\n";
                $SQL .= $Self->{'DB::Comment'}
                    . "----------------------------------------------------------\n";
            }
        }

        $SQL .= "DROP TABLE $Tag->{Name} CASCADE CONSTRAINTS";

        # get sequence name
        my $Sequence = $Self->_SequenceName(
            TableName => $Tag->{Name},
        );
        my $Shell = '';
        if ( $ConfigObject->Get('Database::ShellOutput') ) {
            $Shell = "/\n--";
        }

        # build sql to drop sequence
        my $DropSequenceSQL = <<"EOF";
BEGIN
    EXECUTE IMMEDIATE 'DROP SEQUENCE $Sequence';
EXCEPTION
    WHEN OTHERS THEN NULL;
END;
$Shell
EOF

        return ( $SQL, $DropSequenceSQL );
    }
    return ();
}

sub TableAlter {
    my ( $Self, @Param ) = @_;

    # get config object
    my $ConfigObject = $Kernel::OM->Get('Kernel::Config');

    my $SQLStart      = '';
    my @SQL           = ();
    my @Index         = ();
    my $IndexName     = ();
    my $ForeignTable  = '';
    my $ReferenceName = '';
    my @Reference     = ();
    my $Table         = '';

    for my $Tag (@Param) {

        if ( $Tag->{Tag} eq 'TableAlter' && $Tag->{TagType} eq 'Start' ) {
            $Table = $Tag->{Name} || $Tag->{NameNew};
            if ( $ConfigObject->Get('Database::ShellOutput') ) {
                $SQLStart .= $Self->{'DB::Comment'}
                    . "----------------------------------------------------------\n";
                $SQLStart .= $Self->{'DB::Comment'} . " alter table $Table\n";
                $SQLStart .= $Self->{'DB::Comment'}
                    . "----------------------------------------------------------\n";
            }

            # rename table
            if ( $Tag->{NameOld} && $Tag->{NameNew} ) {
                push @SQL, $SQLStart . "ALTER TABLE $Tag->{NameOld} RENAME TO $Tag->{NameNew}";

                my $Shell = '';
                if ( $ConfigObject->Get('Database::ShellOutput') ) {
                    $Shell = "/\n--";
                }

                # get old primary key name constraint
                my $OldConstraint = $Self->_ConstraintName(
                    TableName => $Tag->{NameOld},
                );

                # get new primary key name constraint
                my $NewConstraint = $Self->_ConstraintName(
                    TableName => $Tag->{NameNew},
                );

                # build SQL to rename primary key constraint
                my $RenameConstraintSQL = <<"EOF";
BEGIN
    EXECUTE IMMEDIATE 'ALTER TABLE $Tag->{NameNew} RENAME CONSTRAINT $OldConstraint TO $NewConstraint';
EXCEPTION
    WHEN OTHERS THEN NULL;
END;
$Shell
EOF

                push @SQL, $SQLStart . $RenameConstraintSQL;

                # build SQL to rename index of primary key
                my $RenameIndexSQL = <<"EOF";
BEGIN
    EXECUTE IMMEDIATE 'ALTER INDEX $OldConstraint RENAME TO $NewConstraint';
EXCEPTION
    WHEN OTHERS THEN NULL;
END;
$Shell
EOF

                push @SQL, $SQLStart . $RenameIndexSQL;

                # get old sequence name
                my $OldSequence = $Self->_SequenceName(
                    TableName => $Tag->{NameOld},
                );

                # get new sequence name
                my $NewSequence = $Self->_SequenceName(
                    TableName => $Tag->{NameNew},
                );

                # define old and new trigger names
                my $OldTrigger = $OldSequence . '_t';
                my $NewTrigger = $NewSequence . '_t';

                # build SQL to rename sequence (only if a sequence exists)
                my $RenameSequenceSQL = <<"EOF";
DECLARE
    sequence_count NUMBER;
BEGIN
    SELECT COUNT(*)
    INTO sequence_count
    FROM user_sequences
    WHERE UPPER(sequence_name) = UPPER('$OldSequence');

    IF sequence_count > 0 THEN
        EXECUTE IMMEDIATE 'RENAME $OldSequence TO $NewSequence';
    END IF;

EXCEPTION
    WHEN OTHERS THEN NULL;
END;
$Shell
EOF

                push @SQL, $SQLStart . $RenameSequenceSQL;

                # build SQL to drop old trigger (only if a trigger exists)
                # and to create a new trigger
                # (the name of the autoincrement column needed for the trigger is investigated automatically)
                my $DropOldAndCreateNewTriggerSQL = <<"EOF";
DECLARE
    trigger_count NUMBER;
    pk_column_name VARCHAR(50);
BEGIN
    SELECT COUNT(*)
    INTO trigger_count
    FROM user_triggers
    WHERE UPPER(trigger_name) = UPPER('$OldTrigger');

    SELECT column_name
    INTO pk_column_name
    FROM user_ind_columns
    WHERE UPPER(table_name) = UPPER('$Tag->{NameNew}')
    AND UPPER(index_name) = UPPER('$NewConstraint');

    IF trigger_count > 0 THEN
        EXECUTE IMMEDIATE 'DROP TRIGGER $OldTrigger';

        EXECUTE IMMEDIATE 'CREATE OR REPLACE TRIGGER $NewTrigger
            BEFORE INSERT ON $Tag->{NameNew}
            FOR EACH ROW
            BEGIN
                IF :new.' || pk_column_name || ' IS NULL THEN
                    SELECT $NewSequence.nextval
                    INTO :new.' || pk_column_name || '
                    FROM DUAL;
                END IF;
            END;';
    END IF;

EXCEPTION
    WHEN OTHERS THEN NULL;
END;
$Shell
EOF

                push @SQL, $SQLStart . $DropOldAndCreateNewTriggerSQL;
            }
            $SQLStart .= "ALTER TABLE $Table";
        }
        elsif ( $Tag->{Tag} eq 'ColumnAdd' && $Tag->{TagType} eq 'Start' ) {

            # Type translation
            $Tag = $Self->_TypeTranslation($Tag);

            # normal data type
            push @SQL, $SQLStart . " ADD $Tag->{Name} $Tag->{Type} NULL";

            # investigate the require
            my $Required = ( $Tag->{Required} && lc $Tag->{Required} eq 'true' ) ? 1 : 0;

            # investigate the default value
            my $Default = '';
            if ( $Tag->{Type} =~ m{ (int|number) }xmsi ) {
                $Default = defined $Tag->{Default} ? $Tag->{Default} : 0;
            }
            else {
                $Default = defined $Tag->{Default} ? "'$Tag->{Default}'" : "''";
            }

            # handle default and require
            if ( $Default ne "''" && ( $Required || defined $Tag->{Default} ) ) {

                # fill up empty rows
                push @SQL, "UPDATE $Table SET $Tag->{Name} = $Default WHERE $Tag->{Name} IS NULL";

                my $SQLAlter = "ALTER TABLE $Table MODIFY $Tag->{Name} $Tag->{Type}";

                # add default
                if ( defined $Tag->{Default} ) {
                    $SQLAlter .= " DEFAULT $Default";
                }

                # add require
                if ($Required) {
                    $SQLAlter .= ' NOT NULL';
                }

                push @SQL, $SQLAlter;
            }
        }
        elsif ( $Tag->{Tag} eq 'ColumnChange' && $Tag->{TagType} eq 'Start' ) {

            # Type translation
            $Tag = $Self->_TypeTranslation($Tag);

            # rename oldname to newname
            if ( $Tag->{NameOld} ne $Tag->{NameNew} ) {
                push @SQL, $SQLStart . " RENAME COLUMN $Tag->{NameOld} TO $Tag->{NameNew}";
            }

            # alter table name modify
            if ( !$Tag->{Name} && $Tag->{NameNew} ) {
                $Tag->{Name} = $Tag->{NameNew};
            }
            if ( !$Tag->{Name} && $Tag->{NameOld} ) {
                $Tag->{Name} = $Tag->{NameOld};
            }

            # investigate the require
            my $Required = ( $Tag->{Required} && lc $Tag->{Required} eq 'true' ) ? 1 : 0;

            # investigate the default value
            my $Default = '';
            if ( $Tag->{Type} =~ m{ (int|number) }xmsi ) {
                $Default = defined $Tag->{Default} ? $Tag->{Default} : 0;
            }
            else {
                $Default = defined $Tag->{Default} ? "'$Tag->{Default}'" : "''";
            }

            # if the column is a CLOB we create a temporary column with the CLOB type,
            # then copy the data from the old column to the temporary column
            # and then remove the temporary column
            # Fix/Workaround for ORA-22858: invalid alteration of datatype
            if ( $Tag->{Type} eq 'CLOB' ) {

                # create temp column name
                my $ColumnTemp = $Tag->{Name} . '_TEMP';

                # create temp column
                push @SQL, "ALTER TABLE $Table ADD $ColumnTemp $Tag->{Type} NULL";

                # copy data from old column into temp column
                push @SQL, "UPDATE $Table SET $ColumnTemp = $Tag->{Name}";

                # delete old column
                push @SQL, "ALTER TABLE $Table DROP COLUMN $Tag->{Name}";

                # rename temp column to old column name
                push @SQL, "ALTER TABLE $Table RENAME COLUMN $ColumnTemp TO $Tag->{Name}";
            }

            # the column type is no CLOB
            else {
                my $SQLEnd = "ALTER TABLE $Table MODIFY $Tag->{Name} $Tag->{Type} DEFAULT NULL";
                push @SQL, $SQLEnd;
            }

            # handle default and require
            if ( $Default ne "''" && ( $Required || defined $Tag->{Default} ) ) {

                # fill up empty rows
                push @SQL, "UPDATE $Table SET $Tag->{Name} = $Default WHERE $Tag->{Name} IS NULL";

                my $SQLAlter = "ALTER TABLE $Table MODIFY $Tag->{Name} $Tag->{Type}";

                # add default
                if ( defined $Tag->{Default} ) {
                    $SQLAlter .= " DEFAULT $Default";
                }

                # add require
                if ($Required) {
                    $SQLAlter .= ' NOT NULL';
                }

                push @SQL, $SQLAlter;
            }
        }
        elsif ( $Tag->{Tag} eq 'ColumnDrop' && $Tag->{TagType} eq 'Start' ) {
            my $SQLEnd = $SQLStart . " DROP COLUMN $Tag->{Name}";
            push @SQL, $SQLEnd;
        }
        elsif ( $Tag->{Tag} =~ /^((Index|Unique)(Create|Drop))/ ) {
            my $Method = $Tag->{Tag};
            if ( $Tag->{Name} ) {
                $IndexName = $Tag->{Name};
            }
            if ( $Tag->{TagType} eq 'End' ) {
                push @SQL, $Self->$Method(
                    TableName => $Table,
                    Name      => $IndexName,
                    Data      => \@Index,
                );
                $IndexName = '';
                @Index     = ();
            }
        }
        elsif ( $Tag->{Tag} =~ /^(IndexColumn|UniqueColumn)/ && $Tag->{TagType} eq 'Start' ) {
            push @Index, $Tag;
        }
        elsif ( $Tag->{Tag} =~ /^((ForeignKey)(Create|Drop))/ ) {
            my $Method = $Tag->{Tag};
            if ( $Tag->{ForeignTable} ) {
                $ForeignTable = $Tag->{ForeignTable};
            }
            if ( $Tag->{TagType} eq 'End' ) {
                for my $Reference (@Reference) {
                    push @SQL, $Self->$Method(
                        LocalTableName   => $Table,
                        Local            => $Reference->{Local},
                        ForeignTableName => $ForeignTable,
                        Foreign          => $Reference->{Foreign},
                    );
                }
                $ReferenceName = '';
                @Reference     = ();
            }
        }
        elsif ( $Tag->{Tag} =~ /^(Reference)/ && $Tag->{TagType} eq 'Start' ) {
            push @Reference, $Tag;
        }
    }

    return @SQL;
}

sub IndexCreate {
    my ( $Self, %Param ) = @_;

    # check needed stuff
    for my $Needed (qw(TableName Name Data)) {
        if ( !$Param{$Needed} ) {
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                Priority => 'error',
                Message  => "Need $Needed!"
            );
            return;
        }
    }
    my $Index = $Self->_IndexName(
        Name => $Param{Name},
    );
    my $CreateIndexSQL = "CREATE INDEX $Index ON $Param{TableName} (";
    my @Array          = @{ $Param{Data} };
    for my $Index ( 0 .. $#Array ) {
        if ( $Index > 0 ) {
            $CreateIndexSQL .= ', ';
        }
        $CreateIndexSQL .= $Array[$Index]->{Name};
    }
    $CreateIndexSQL .= ')';

    my $Shell = '';
    if ( $Kernel::OM->Get('Kernel::Config')->Get('Database::ShellOutput') ) {
        $Shell = "/\n--";
    }

    # build sql to create index within a "try/catch block"
    # to prevent errors if index exists already
    $CreateIndexSQL = <<"EOF";
BEGIN
    EXECUTE IMMEDIATE '$CreateIndexSQL';
EXCEPTION
  WHEN OTHERS THEN NULL;
END;
$Shell
EOF

    return ($CreateIndexSQL);
}

sub IndexDrop {
    my ( $Self, %Param ) = @_;

    # check needed stuff
    for my $Needed (qw(TableName Name)) {
        if ( !$Param{$Needed} ) {
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                Priority => 'error',
                Message  => "Need $Needed!",
            );
            return;
        }
    }

    my $Index = $Self->_IndexName(
        Name => $Param{Name},
    );

    my $DropIndexSQL = 'DROP INDEX ' . $Index;

    my $Shell = '';
    if ( $Kernel::OM->Get('Kernel::Config')->Get('Database::ShellOutput') ) {
        $Shell = "/\n--";
    }

    # build sql to drop index within a "try/catch block"
    # to prevent errors if index does not exist
    $DropIndexSQL = <<"EOF";
BEGIN
    EXECUTE IMMEDIATE '$DropIndexSQL';
EXCEPTION
  WHEN OTHERS THEN NULL;
END;
$Shell
EOF

    return ($DropIndexSQL);
}

sub ForeignKeyCreate {
    my ( $Self, %Param ) = @_;

    # check needed stuff
    for my $Needed (qw(LocalTableName Local ForeignTableName Foreign)) {
        if ( !$Param{$Needed} ) {
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                Priority => 'error',
                Message  => "Need $Needed!"
            );
            return;
        }
    }

    # create foreign key name
    my $ForeignKey = $Self->_ForeignKeyName(
        Name => "FK_$Param{LocalTableName}_$Param{Local}_$Param{Foreign}",
    );

    # add foreign key
    my $CreateForeignKeySQL = "ALTER TABLE $Param{LocalTableName} ADD CONSTRAINT $ForeignKey FOREIGN KEY "
        . "($Param{Local}) REFERENCES $Param{ForeignTableName} ($Param{Foreign})";

    my $Shell = '';
    if ( $Kernel::OM->Get('Kernel::Config')->Get('Database::ShellOutput') ) {
        $Shell = "/\n--";
    }

    # build sql to create foreign key within a "try/catch block"
    # to prevent errors if foreign key exists already
    $CreateForeignKeySQL = <<"EOF";
BEGIN
    EXECUTE IMMEDIATE '$CreateForeignKeySQL';
EXCEPTION
  WHEN OTHERS THEN NULL;
END;
$Shell
EOF

    # build index name
    my $IndexName = $Self->_IndexName(
        Name => 'FK_' . $Param{LocalTableName} . '_' . $Param{Local},
    );

    # generate forced index for every FK to do row locking (not table locking)
    my @CreateIndexSQL = $Self->IndexCreate(
        TableName => $Param{LocalTableName},
        Name      => $IndexName,
        Data      => [ { Name => $Param{Local} } ],
    );

    return ( $CreateForeignKeySQL, @CreateIndexSQL );
}

sub ForeignKeyDrop {
    my ( $Self, %Param ) = @_;

    # check needed stuff
    for my $Needed (qw(LocalTableName Local ForeignTableName Foreign)) {
        if ( !$Param{$Needed} ) {
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                Priority => 'error',
                Message  => "Need $Needed!"
            );
            return;
        }
    }

    # create foreign key name
    my $ForeignKey = $Self->_ForeignKeyName(
        Name => "FK_$Param{LocalTableName}_$Param{Local}_$Param{Foreign}",
    );

    my $Shell = '';
    if ( $Kernel::OM->Get('Kernel::Config')->Get('Database::ShellOutput') ) {
        $Shell = "/\n--";
    }

    # drop foreign key
    my $DropForeignKeySQL = <<"EOF";
BEGIN
    EXECUTE IMMEDIATE 'ALTER TABLE $Param{LocalTableName} DROP CONSTRAINT $ForeignKey';
EXCEPTION
  WHEN OTHERS THEN NULL;
END;
$Shell
EOF

    # build index name
    my $IndexName = $Self->_IndexName(
        Name => 'FK_' . $Param{LocalTableName} . '_' . $Param{Local},
    );

    # build sql to drop index
    my $DropIndexSQL = <<"EOF";
BEGIN
    EXECUTE IMMEDIATE 'DROP INDEX $IndexName';
EXCEPTION
  WHEN OTHERS THEN NULL;
END;
$Shell
EOF

    return ( $DropForeignKeySQL, $DropIndexSQL );
}

sub UniqueCreate {
    my ( $Self, %Param ) = @_;

    # check needed stuff
    for my $Needed (qw(TableName Name Data)) {
        if ( !$Param{$Needed} ) {
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                Priority => 'error',
                Message  => "Need $Needed!"
            );
            return;
        }
    }

    my $Unique = $Self->_UniqueName(
        Name => $Param{Name},
    );

    my $CreateUniqueSQL = "ALTER TABLE $Param{TableName} ADD CONSTRAINT $Unique UNIQUE (";
    my @Array           = @{ $Param{Data} };
    my $Name            = '';
    for my $Index ( 0 .. $#Array ) {
        if ( $Index > 0 ) {
            $CreateUniqueSQL .= ', ';
        }
        $CreateUniqueSQL .= $Array[$Index]->{Name};
        $Name            .= '_' . $Array[$Index]->{Name};
    }
    $CreateUniqueSQL .= ')';

    my $Shell = '';
    if ( $Kernel::OM->Get('Kernel::Config')->Get('Database::ShellOutput') ) {
        $Shell = "/\n--";
    }

    # build SQL to create unique constraint within a "try/catch block"
    # to prevent errors if unique constraint does already exist
    $CreateUniqueSQL = <<"EOF";
BEGIN
    EXECUTE IMMEDIATE '$CreateUniqueSQL';
EXCEPTION
  WHEN OTHERS THEN NULL;
END;
$Shell
EOF

    return ($CreateUniqueSQL);
}

sub UniqueDrop {
    my ( $Self, %Param ) = @_;

    # check needed stuff
    for my $Needed (qw(TableName Name)) {
        if ( !$Param{$Needed} ) {
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                Priority => 'error',
                Message  => "Need $Needed!"
            );
            return;
        }
    }

    my $Unique = $Self->_UniqueName(
        Name => $Param{Name},
    );

    my $DropUniqueSQL = "ALTER TABLE $Param{TableName} DROP CONSTRAINT $Unique";

    my $Shell = '';
    if ( $Kernel::OM->Get('Kernel::Config')->Get('Database::ShellOutput') ) {
        $Shell = "/\n--";
    }

    # build SQL to drop unique constraint within a "try/catch block"
    # to prevent errors if unique constraint does not exist
    $DropUniqueSQL = <<"EOF";
BEGIN
    EXECUTE IMMEDIATE '$DropUniqueSQL';
EXCEPTION
  WHEN OTHERS THEN NULL;
END;
$Shell
EOF

    return ($DropUniqueSQL);
}

sub Insert {
    my ( $Self, @Param ) = @_;

    # get needed objects
    my $ConfigObject   = $Kernel::OM->Get('Kernel::Config');
    my $DateTimeObject = $Kernel::OM->Create('Kernel::System::DateTime');

    my $SQL    = '';
    my @Keys   = ();
    my @Values = ();
    TAG:
    for my $Tag (@Param) {
        if ( $Tag->{Tag} eq 'Insert' && $Tag->{TagType} eq 'Start' ) {
            if ( $ConfigObject->Get('Database::ShellOutput') ) {
                $SQL .= $Self->{'DB::Comment'}
                    . "----------------------------------------------------------\n";
                $SQL .= $Self->{'DB::Comment'} . " insert into table $Tag->{Table}\n";
                $SQL .= $Self->{'DB::Comment'}
                    . "----------------------------------------------------------\n";
            }
            $SQL .= "INSERT INTO $Tag->{Table} ";
        }
        if ( $Tag->{Tag} eq 'Data' && $Tag->{TagType} eq 'Start' ) {

            # do not use auto increment values
            next TAG if $Tag->{Type} && $Tag->{Type} =~ m{ ^AutoIncrement$ }xmsi;

            $Tag->{Key} = ${ $Self->Quote( \$Tag->{Key} ) };
            push @Keys, $Tag->{Key};
            my $Value;
            if ( defined $Tag->{Value} ) {
                $Value = $Tag->{Value};
                $Kernel::OM->Get('Kernel::System::Log')->Log(
                    Priority => 'error',
                    Message  => 'The content for inserts is not longer appreciated '
                        . 'attribut Value, use Content from now on! Reason: You can\'t '
                        . 'use new lines in attributes.',
                );
            }
            elsif ( defined $Tag->{Content} ) {
                $Value = $Tag->{Content};
            }
            else {
                $Value = '';
            }
            if ( $Tag->{Type} && $Tag->{Type} eq 'Quote' ) {
                $Value = "'" . ${ $Self->Quote( \$Value ) } . "'";
            }
            else {
                $Value = ${ $Self->Quote( \$Value ) };
            }
            push @Values, $Value;
        }
    }
    my $Key = '';
    for my $CurrentKey (@Keys) {
        if ( $Key ne '' ) {
            $Key .= ', ';
        }
        $Key .= $CurrentKey;
    }
    my $Value = '';
    for my $Tmp (@Values) {
        if ( $Value ne '' ) {
            $Value .= ', ';
        }
        if ( $Tmp eq 'current_timestamp' ) {
            if ( $ConfigObject->Get('Database::ShellOutput') ) {
                $Value .= $Tmp;
            }
            else {
                my $Timestamp = $DateTimeObject->ToString();
                $Value .= '\'' . $Timestamp . '\'';
            }
        }
        else {
            $Value .= $Tmp;
        }
    }
    $SQL .= "($Key)\n    VALUES\n    ($Value)";
    return ($SQL);
}

sub _TypeTranslation {
    my ( $Self, $Tag ) = @_;

    # Type translation
    if ( $Tag->{Type} =~ /^DATE$/i ) {
        $Tag->{Type} = 'DATE';
    }
    elsif ( $Tag->{Type} =~ /^smallint$/i ) {
        $Tag->{Type} = 'NUMBER (5, 0)';
    }
    elsif ( $Tag->{Type} =~ /^integer$/i ) {
        $Tag->{Type} = 'NUMBER (12, 0)';
    }
    elsif ( $Tag->{Type} =~ /^bigint$/i ) {
        $Tag->{Type} = 'NUMBER (20, 0)';
    }
    elsif ( $Tag->{Type} =~ /^longblob$/i ) {
        $Tag->{Type} = 'CLOB';
    }
    elsif ( $Tag->{Type} =~ /^VARCHAR$/i ) {
        $Tag->{Type} = 'VARCHAR2 (' . $Tag->{Size} . ')';
        if ( $Tag->{Size} > 4000 ) {
            $Tag->{Type} = 'CLOB';
        }
    }
    elsif ( $Tag->{Type} =~ /^DECIMAL$/i ) {
        $Tag->{Type} = 'DECIMAL (' . $Tag->{Size} . ')';
    }
    return $Tag;
}

sub _SequenceName {
    my ( $Self, %Param ) = @_;

    # check needed stuff
    if ( !$Param{TableName} ) {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'error',
            Message  => "Need TableName!",
        );
        return;
    }

    my $Sequence = 'SE_' . $Param{TableName};
    if ( length $Sequence > 28 ) {
        my $MD5 = $Kernel::OM->Get('Kernel::System::Main')->MD5sum(
            String => $Sequence,
        );
        $Sequence = substr $Sequence, 0, 26;
        $Sequence .= substr $MD5, 0,  1;
        $Sequence .= substr $MD5, 31, 1;
    }

    return $Sequence;
}

sub _ConstraintName {
    my ( $Self, %Param ) = @_;

    # check needed stuff
    if ( !$Param{TableName} ) {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'error',
            Message  => "Need TableName!",
        );
        return;
    }

    my $Constraint = 'PK_' . $Param{TableName};
    if ( length $Constraint > 30 ) {
        my $MD5 = $Kernel::OM->Get('Kernel::System::Main')->MD5sum(
            String => $Constraint,
        );
        $Constraint = substr $Constraint, 0, 28;
        $Constraint .= substr $MD5, 0,  1;
        $Constraint .= substr $MD5, 31, 1;
    }

    return $Constraint;
}

sub _UniqueName {
    my ( $Self, %Param ) = @_;

    # check needed stuff
    if ( !$Param{Name} ) {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'error',
            Message  => "Need Name!",
        );
        return;
    }

    my $Unique = $Param{Name};
    if ( length $Unique > 30 ) {
        my $MD5 = $Kernel::OM->Get('Kernel::System::Main')->MD5sum(
            String => $Unique,
        );
        $Unique = substr $Unique, 0, 28;
        $Unique .= substr $MD5, 0,  1;
        $Unique .= substr $MD5, 31, 1;
    }

    return $Unique;
}

sub _IndexName {
    my ( $Self, %Param ) = @_;

    # check needed stuff
    if ( !$Param{Name} ) {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'error',
            Message  => "Need Name!",
        );
        return;
    }

    my $Index = $Param{Name};
    if ( length $Index > 30 ) {
        my $MD5 = $Kernel::OM->Get('Kernel::System::Main')->MD5sum(
            String => $Index,
        );
        $Index = substr $Index, 0, 28;
        $Index .= substr $MD5, 0,  1;
        $Index .= substr $MD5, 31, 1;
    }

    return $Index;
}

sub _ForeignKeyName {
    my ( $Self, %Param ) = @_;

    # check needed stuff
    if ( !$Param{Name} ) {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'error',
            Message  => "Need Name!",
        );
        return;
    }

    my $ForeignKey = $Param{Name};
    if ( length($ForeignKey) > 30 ) {
        my $MD5 = $Kernel::OM->Get('Kernel::System::Main')->MD5sum(
            String => $ForeignKey,
        );
        $ForeignKey = substr $ForeignKey, 0, 28;
        $ForeignKey .= substr $MD5, 0,  1;
        $ForeignKey .= substr $MD5, 31, 1;
    }

    return $ForeignKey;
}

1;
