# credativ-pg-migrator
# Copyright (C) 2025 credativ GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import time
import psycopg2
import psycopg2.extras
from psycopg2 import sql
from credativ_pg_migrator.database_connector import DatabaseConnector
from credativ_pg_migrator.migrator_logging import MigratorLogger
import traceback
import re
import datetime

class PostgreSQLConnector(DatabaseConnector):
    def __init__(self, config_parser, source_or_target):
        self.connection = None
        self.config_parser = config_parser
        self.source_or_target = source_or_target
        self.logger = MigratorLogger(self.config_parser.get_log_file()).logger
        self.session_settings = self.prepare_session_settings()

    def connect(self):
        connection_string = self.config_parser.get_connect_string(self.source_or_target)
        self.connection = psycopg2.connect(connection_string)
        self.connection.autocommit = True

    def disconnect(self):
        try:
            if self.connection:
                self.connection.close()
        except Exception as e:
            pass

    def get_sql_functions_mapping(self, settings):
        """ Returns a dictionary of SQL functions mapping for the target database """
        target_db_type = settings['target_db_type']
        if target_db_type == 'postgresql':
            return {}
        else:
            self.config_parser.print_log_message('ERROR', f"postgresql_connector: get_sql_functions_mapping: Unsupported target database type: {target_db_type}")

    def fetch_table_names(self, schema: str = 'public'):
        query = f"""
            SELECT
                c.oid,
                c.relname,
                obj_description(c.oid, 'pg_class') as table_comment,
                c.relkind,
                c.relispartition,
                pg_get_partkeydef(c.oid) as partition_key_def,
                pg_get_expr(c.relpartbound, c.oid) as partition_bound,
                (SELECT relname FROM pg_class WHERE oid = i.inhparent) as parent_table
            FROM pg_class c
            LEFT JOIN pg_inherits i ON c.oid = i.inhrelid
            WHERE c.relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = '{schema}')
            AND c.relkind in ('r', 'p')
            ORDER BY c.relname
        """
        self.config_parser.print_log_message('DEBUG3', f"postgresql_connector: fetch_table_names: Reading table names for {schema}")
        self.config_parser.print_log_message('DEBUG3', f"postgresql_connector: fetch_table_names: Query: {query}")
        try:
            tables = {}
            order_num = 1
            self.connect()
            cursor = self.connection.cursor()
            cursor.execute(query)
            for row in cursor.fetchall():
                tables[order_num] = {
                    'id': row[0],
                    'schema_name': schema,
                    'table_name': row[1],
                    'comment': row[2],
                    'relkind': row[3],
                    'relispartition': row[4],
                    'partition_key_def': row[5],
                    'partition_bound': row[6],
                    'parent_table': row[7]
                }
                order_num += 1
            cursor.close()
            self.disconnect()
            return tables
        except Exception as e:
            self.config_parser.print_log_message('ERROR', f"postgresql_connector: fetch_table_names: Error executing query: {query}")
            self.config_parser.print_log_message('ERROR', e)
            raise

    def get_table_description(self, settings) -> dict:
        table_schema = settings['table_schema']
        table_name = settings['table_name']
        output = []
        output.append(f'Table "{table_schema}"."{table_name}"')
        self.config_parser.print_log_message('DEBUG3', f"postgresql_connector: get_table_description: PostgreSQL connector: Getting table description for {table_schema}.{table_name}")

        try:
            self.connect()
            cursor = self.connection.cursor()

            # 1. Attributes (Columns)
            # Fetch: Column, Type, Nullable, Default
            query_columns = f"""
                SELECT column_name, data_type, is_nullable, column_default
                FROM information_schema.columns
                WHERE table_schema = '{table_schema}' AND table_name = '{table_name}'
                ORDER BY ordinal_position
            """
            cursor.execute(query_columns)
            columns = cursor.fetchall()

            if columns:
                headers = ['Column', 'Type', 'Nullable', 'Default']
                rows = []
                for col in columns:
                    name = str(col[0]) if col[0] is not None else ''
                    dtype = str(col[1]) if col[1] is not None else ''
                    nullable = str(col[2]) if col[2] is not None else ''
                    default = str(col[3]) if col[3] is not None else ''
                    rows.append([name, dtype, nullable, default])

                # Calculate column widths
                widths = [len(h) for h in headers]
                for row in rows:
                    for i, val in enumerate(row):
                        if len(val) > widths[i]:
                            widths[i] = len(val)

                # Format Table
                header_line = " | ".join(h.ljust(widths[i]) for i, h in enumerate(headers))
                output.append(header_line)
                divider_line = "-+-".join("-" * widths[i] for i in range(len(widths)))
                output.append(divider_line)

                for row in rows:
                    line = " | ".join(row[i].ljust(widths[i]) for i in range(len(row)))
                    output.append(line)

            output.append("")

            # 2. Indexes
            # Use pg_indexes as information_schema standard does not fully cover indexes in PG
            query_indexes = f"""
                SELECT indexname, indexdef
                FROM pg_indexes
                WHERE schemaname = '{table_schema}' AND tablename = '{table_name}'
            """
            cursor.execute(query_indexes)
            indexes = cursor.fetchall()
            if indexes:
                output.append("Indexes:")
                for idx in indexes:
                    output.append(f"    {idx[1]}")

            # 3. Constraints
            # Use pg_constraint for robust definition
            query_constraints = f"""
                SELECT conname, pg_get_constraintdef(oid), contype
                FROM pg_constraint
                WHERE conrelid = '"{table_schema}"."{table_name}"'::regclass
            """
            cursor.execute(query_constraints)
            pg_cons = cursor.fetchall()

            check_constraints = []
            fk_constraints = []

            for con in pg_cons:
                name = con[0]
                definition = con[1]
                contype = con[2] # c=check, f=foreign key, p=primary key, u=unique

                # Primary keys and Unique constraints are typically listed in Indexes section (as matching indexes)
                # So we focus on Checks and Foreign Keys here similar to psql output
                if contype == 'c':
                    check_constraints.append(f"    \"{name}\" CHECK {definition}")
                elif contype == 'f':
                    fk_constraints.append(f"    \"{name}\" {definition}")

            if check_constraints:
                output.append("Check constraints:")
                output.extend(check_constraints)

            if fk_constraints:
                output.append("Foreign-key constraints:")
                output.extend(fk_constraints)

            # 4. Triggers (Optional but good for \d+)
            # Use simple query from information_schema
            query_triggers = f"""
                SELECT trigger_name, action_timing, event_manipulation
                FROM information_schema.triggers
                WHERE event_object_schema = '{table_schema}' AND event_object_table = '{table_name}'
            """
            cursor.execute(query_triggers)
            triggers = cursor.fetchall()
            if triggers:
                output.append("Triggers:")
                for trig in triggers:
                    output.append(f"    {trig[0]} {trig[1]} {trig[2]}")

            cursor.close()
            self.disconnect()

        except Exception as e:
            self.config_parser.print_log_message('ERROR', f"postgresql_connector: get_table_description: Error getting table description for {table_schema}.{table_name}: {e}")
            return {'table_description': f"Error: {str(e)}"}

        self.config_parser.print_log_message('DEBUG3', f"postgresql_connector: get_table_description: Table description for {table_schema}.{table_name}: {output}")
        return {'table_description': "\\n".join(output)}

    def fetch_table_columns(self, settings) -> dict:
        table_schema = settings['table_schema']
        table_name = settings['table_name']
        result = {}
        try:
            query =f"""
                    SELECT
                        c.ordinal_position,
                        c.column_name,
                        c.data_type,
                        c.character_maximum_length,
                        c.numeric_precision,
                        c.numeric_scale,
                        c.is_identity,
                        c.is_nullable,
                        c.column_default,
                        u.udt_schema,
                        u.udt_name,
                        col_description((c.table_schema||'.'||c.table_name)::regclass::oid, c.ordinal_position) as column_comment,
                        is_generated
                    FROM information_schema.columns c
                    LEFT JOIN information_schema.column_udt_usage u ON c.table_schema = u.table_schema
                        AND c.table_name = u.table_name
                        AND c.column_name = u.column_name
                        AND c.udt_name = u.udt_name
                    WHERE c.table_name = '{table_name}' AND c.table_schema = '{table_schema}'
                """
            self.connect()
            cursor = self.connection.cursor()
            self.config_parser.print_log_message('DEBUG2', f"postgresql_connector: fetch_table_columns: PostgreSQL: Reading columns for {table_schema}.{table_name}")
            cursor.execute(query)
            for row in cursor.fetchall():
                ordinal_position = row[0]
                column_name = row[1]
                data_type = row[2]
                character_maximum_length = row[3]
                numeric_precision = row[4]
                numeric_scale = row[5]
                is_identity = row[6]
                is_nullable = row[7]
                column_default = row[8]
                udt_schema = row[9]
                udt_name = row[10]
                column_comment = row[11]
                is_generated = row[12]
                column_type = data_type
                if self.is_string_type(data_type) and character_maximum_length:
                    column_type = f"{data_type}({character_maximum_length})"
                elif self.is_numeric_type(data_type) and numeric_precision and numeric_scale:
                    column_type = f"{data_type}({numeric_precision},{numeric_scale})"
                elif self.is_numeric_type(data_type) and numeric_precision and not numeric_scale:
                    column_type = f"{data_type}({numeric_precision})"
                result[ordinal_position] = {
                    'column_name': column_name,
                    'is_nullable': is_nullable,
                    'column_default_name': '',
                    'column_default_value': column_default,
                    'data_type': data_type,
                    'column_type': column_type,
                    'basic_data_type': data_type if data_type not in ('USER-DEFINED', 'DOMAIN') else '',
                    'is_identity': is_identity,
                    'character_maximum_length': character_maximum_length,
                    'numeric_precision': numeric_precision,
                    'numeric_scale': numeric_scale,
                    'udt_schema': udt_schema,
                    'udt_name': udt_name,
                    'column_comment': column_comment,
                    'is_generated_virtual': 'NO',
                    'is_generated_stored': is_generated,
                }
            cursor.close()
            self.disconnect()
            return result

        except Exception as e:
            self.config_parser.print_log_message('ERROR', f"postgresql_connector: fetch_table_columns: Error executing query: {query}")
            self.config_parser.print_log_message('ERROR', e)
            raise

    def get_types_mapping(self, settings):
        target_db_type = settings['target_db_type']
        types_mapping = {}
        if target_db_type != 'postgresql':
            raise ValueError(f"Unsupported target database type: {target_db_type}")
        return types_mapping

    def get_create_table_sql(self, settings):
        source_schema_name = settings['source_schema_name']
        source_table_name = settings['source_table_name']
        source_table_id = settings['source_table_id']
        # target_schema_name = self.config_parser.convert_names_case(settings['target_schema_name'])
        target_schema_name = settings['target_schema_name'] ## target schema is used as it is defined in config, not converted to upper/lower case
        target_table_name = self.config_parser.convert_names_case(settings['target_table_name'])
        # source_columns = settings['source_columns']
        converted = settings['target_columns']
        migrator_tables = settings['migrator_tables']
        create_table_sql = ""
        create_table_sql_parts = []

        if self.config_parser.get_source_db_type() == 'postgresql':
           table_info_list = self.fetch_table_names(source_schema_name)
           # Find key for current table
           current_table_info = None
           for key, val in table_info_list.items():
               if val['table_name'] == source_table_name:
                   current_table_info = val
                   break

           if current_table_info:
               if current_table_info.get('relispartition'):
                   # It is a partition. Generate CREATE TABLE ... PARTITION OF ...
                   parent_table = current_table_info.get('parent_table')
                   partition_bound = current_table_info.get('partition_bound')
                   # For partition, we don't list columns as they are inherited
                   create_table_sql = f"""CREATE TABLE "{target_schema_name}"."{target_table_name}" PARTITION OF "{target_schema_name}"."{parent_table}" {partition_bound}"""
                   return create_table_sql

        self.config_parser.print_log_message('DEBUG', f"postgresql_connector: get_create_table_sql: Creating DDL for table {target_schema_name}.{target_table_name}, case handling: {self.config_parser.get_names_case_handling()}")

        for _, column_info in converted.items():

            column_name = self.config_parser.convert_names_case(column_info['column_name'])

            self.config_parser.print_log_message('DEBUG3', f"postgresql_connector: get_create_table_sql: Creating DDL for table {target_schema_name}.{target_table_name}, column_info: {column_info}")

            if column_info['is_hidden_column'] == 'YES':
                self.config_parser.print_log_message('DEBUG', f"postgresql_connector: get_create_table_sql: Skipping hidden column {column_name}: {column_info}")
                continue

            create_column_sql = ""
            column_data_type = column_info['data_type'].upper()
            is_identity = column_info['is_identity']
            # if column_info['column_type_substitution'] != '':
            #     column_data_type = column_info['column_type_substitution'].upper()
            if column_info['data_type'] == 'USER-DEFINED' and column_info['udt_schema'] != '' and column_info['udt_name'] != '':
                mapped_schema = target_schema_name if column_info['udt_schema'] == source_schema_name else column_info['udt_schema']
                column_data_type = f'''"{mapped_schema}"."{column_info['udt_name']}"'''
            # elif column_info['basic_data_type'] != '':
            #     column_data_type = column_info['basic_data_type'].upper()

            character_maximum_length = ''
            if 'character_maximum_length' in column_info and column_info['character_maximum_length'] not in ('', None):
                character_maximum_length = column_info['character_maximum_length']
            if column_info['basic_character_maximum_length'] not in ('', None):
                character_maximum_length = column_info['basic_character_maximum_length']

            domain_name = column_info['domain_name']

            column_comment = column_info['column_comment']
            nullable_string = ''
            if column_info['is_nullable'] == 'NO':
                nullable_string = 'NOT NULL'

            numeric_precision = column_info.get('numeric_precision')
            numeric_scale = column_info.get('numeric_scale')
            basic_numeric_precision = column_info.get('basic_numeric_precision')
            basic_numeric_scale = column_info.get('basic_numeric_scale')
            altered_data_type = ''

            if is_identity == 'YES' and column_data_type not in ('BIGINT', 'INTEGER', 'SMALLINT'):
                altered_data_type = 'BIGINT'
                migrator_tables.insert_target_column_alteration({
                    'target_schema_name': settings['target_schema_name'],
                    'target_table_name': settings['target_table_name'],
                    'target_column': column_info['column_name'],
                    'reason': 'IDENTITY',
                    'original_data_type': column_data_type,
                    'altered_data_type': altered_data_type,
                })
                create_column_sql = f""""{column_name}" {altered_data_type}"""
                self.config_parser.print_log_message('DEBUG', f"postgresql_connector: get_create_table_sql: Column {column_name} is identity, altered data type to {altered_data_type}")
            elif column_data_type in ('NUMBER', 'NUMERIC') and (numeric_precision is None or numeric_precision == 10) and numeric_scale == 0:
                altered_data_type = 'INTEGER'
                migrator_tables.insert_target_column_alteration({
                    'target_schema_name': settings['target_schema_name'],
                    'target_table_name': settings['target_table_name'],
                    'target_column': column_info['column_name'],
                    'reason': 'NUMBER without precision, scale ' + str(numeric_scale),
                    'original_data_type': column_data_type,
                    'altered_data_type': altered_data_type,
                })
                create_column_sql = f""""{column_name}" {altered_data_type}"""
                self.config_parser.print_log_message('DEBUG', f"postgresql_connector: get_create_table_sql: Column {column_name} is NUMBER without precision, scale {numeric_scale}, altered data type to {altered_data_type}")
            elif column_data_type in ('NUMBER', 'NUMERIC') and numeric_precision is None and numeric_scale == 10:
                altered_data_type = 'DOUBLE PRECISION'
                migrator_tables.insert_target_column_alteration({
                    'target_schema_name': settings['target_schema_name'],
                    'target_table_name': settings['target_table_name'],
                    'target_column': column_info['column_name'],
                    'reason': 'NUMBER without precision, scale ' + str(numeric_scale),
                    'original_data_type': column_data_type,
                    'altered_data_type': altered_data_type,
                })
                create_column_sql = f""""{column_name}" {altered_data_type}"""
                self.config_parser.print_log_message('DEBUG', f"postgresql_connector: get_create_table_sql: Column {column_name} is NUMBER without precision, scale {numeric_scale}, altered data type to {altered_data_type}")
            elif column_data_type in ('NUMBER', 'NUMERIC') and numeric_precision == 1 and numeric_scale == 0:
                altered_data_type = 'BOOLEAN'
                migrator_tables.insert_target_column_alteration({
                    'target_schema_name': settings['target_schema_name'],
                    'target_table_name': settings['target_table_name'],
                    'target_column': column_info['column_name'],
                    'reason': 'NUMBER with precision 1, scale 0',
                    'original_data_type': column_data_type,
                    'altered_data_type': altered_data_type,
                })
                create_column_sql = f""""{column_name}" {altered_data_type}"""
                self.config_parser.print_log_message('DEBUG', f"postgresql_connector: get_create_table_sql: Column {column_name} is NUMBER with precision 1, scale 0, altered data type to {altered_data_type}")
            elif column_data_type in ('NUMBER', 'NUMERIC') and numeric_precision == 19 and numeric_scale == 0:
                altered_data_type = 'BIGINT'
                migrator_tables.insert_target_column_alteration({
                    'target_schema_name': settings['target_schema_name'],
                    'target_table_name': settings['target_table_name'],
                    'target_column': column_info['column_name'],
                    'reason': 'NUMBER with precision 19, scale 0',
                    'original_data_type': column_data_type,
                    'altered_data_type': altered_data_type,
                })
                create_column_sql = f""""{column_name}" {altered_data_type}"""
                self.config_parser.print_log_message('DEBUG', f"postgresql_connector: get_create_table_sql: Column {column_name} is NUMBER with precision 19, scale 0, altered data type to {altered_data_type}")
            else:
                if (character_maximum_length != '' and 'CHAR' in column_data_type):
                    create_column_sql = f""""{column_name}" {column_data_type}({character_maximum_length})"""
                elif self.is_numeric_type(column_data_type) and column_data_type in ('DECIMAL', 'NUMERIC'):
                    if numeric_precision not in (None, '') and numeric_scale not in (None, ''):
                        create_column_sql = f""""{column_name}" {column_data_type}({numeric_precision},{numeric_scale})"""
                    elif numeric_precision not in (None, ''):
                        create_column_sql = f""""{column_name}" {column_data_type}({numeric_precision})"""
                    elif basic_numeric_precision not in (None, '') and basic_numeric_scale not in (None, ''):
                        create_column_sql = f""""{column_name}" {column_data_type}({basic_numeric_precision},{basic_numeric_scale})"""
                    elif basic_numeric_precision not in (None, ''):
                        create_column_sql = f""""{column_name}" {column_data_type}({basic_numeric_precision})"""
                    else:
                        create_column_sql = f""""{column_name}" {column_data_type}"""
                else:
                    create_column_sql = f""""{column_name}" {column_data_type}"""

            if altered_data_type != '':
                column_data_type = altered_data_type

            if nullable_string != '':
                create_column_sql += f""" {nullable_string}"""

            if is_identity == 'YES':
                create_column_sql += " GENERATED BY DEFAULT AS IDENTITY"

            if column_info['is_generated_virtual'] == 'YES' or column_info['is_generated_stored'] == 'YES':
                generated_column_expression = column_info['stripped_generation_expression']
                # Quote column names in generated_column_expression if they match any column name in converted
                for other_col_info in converted.values():
                    other_col_name = other_col_info['column_name']
                    # Use word boundary for precise match, preserve case
                    pattern = r'\b{}\b'.format(re.escape(other_col_name))
                    # Only replace if not already quoted
                    generated_column_expression = re.sub(
                        pattern,
                        lambda m: f'"{m.group(0)}"' if not m.group(0).startswith('"') and not m.group(0).endswith('"') else m.group(0),
                        generated_column_expression
                    )
                if self.is_string_type(column_data_type):
                    generated_column_expression = generated_column_expression.replace("+", "||")
                create_column_sql += f" GENERATED ALWAYS AS {generated_column_expression} STORED"

            column_default = ''
            if column_info['column_default_name'] != '' and column_info['column_default_value'] == '' and column_info['replaced_column_default_value'] == '':
                default_value_info = migrator_tables.get_default_value_details({'default_value_name': column_info['column_default_name']})
                if default_value_info:
                    column_default = default_value_info['extracted_default_value']

            elif column_info['column_default_value'] or column_info['replaced_column_default_value']:
                column_default = column_info['column_default_value']
                if column_info['replaced_column_default_value']:
                    column_default = column_info['replaced_column_default_value'].strip()

            if column_default != '':
                # Remove SQL Server syntax artifacts like () around default values
                column_default = column_default.strip()

                # Remove outer parentheses if present (SQL Server specific)
                if column_default.startswith('(') and column_default.endswith(')'):
                    column_default = column_default[1:-1].strip()

                # Skip empty defaults
                if not column_default:
                    column_default = ''
                if (('CHAR' in column_data_type or column_data_type in ('TEXT'))
                    and ('||' in column_default or '(' in column_default or ')' in column_default or '::' in column_default)):
                    # default value is here NOT quoted
                    create_column_sql += f""" DEFAULT {column_default}""".replace("''", "'")
                elif 'CHAR' in column_data_type or column_data_type in ('TEXT'):
                    # here we must quote the default value
                    create_column_sql += f""" DEFAULT '{column_default}'""".replace("''", "'")
                elif column_data_type in ('BOOLEAN', 'BIT'):
                    if column_default.lower() in ('0', '(0)', 'false'):
                        create_column_sql += """ DEFAULT FALSE"""
                    elif column_default.lower() in ('1', '(1)', 'true'):
                        create_column_sql += """ DEFAULT TRUE"""
                    else:
                        create_column_sql += f""" DEFAULT {column_default}::BOOLEAN"""
                elif column_data_type in ('BYTEA'):
                    create_column_sql += f""" DEFAULT '{column_default}'::BYTEA"""
                else:
                    create_column_sql += f" DEFAULT {column_default}::{column_data_type}"

            if domain_name:
                domain_details = migrator_tables.get_domain_details({'source_domain_name': domain_name})
                if domain_details:
                    domain_row_id = domain_details['id']
                    domain_name = domain_details['source_domain_name']
                    migrated_as = domain_details['migrated_as']
                    source_domain_check_sql = domain_details['source_domain_check_sql']
                    if source_domain_check_sql:
                        # Replace exact word VALUE with the column name, case-sensitive, word boundary
                        pattern = r'\bVALUE\b'
                        source_domain_check_sql = re.sub(pattern, f'"{column_info["column_name"]}"', source_domain_check_sql)
                    if migrated_as == 'CHECK CONSTRAINT':
                        constraint_name = f"{domain_name}_tab_{target_table_name}"
                        create_constraint_sql = f"""ALTER TABLE "{target_schema_name}"."{target_table_name}" ADD CONSTRAINT "{constraint_name}" CHECK({source_domain_check_sql})"""
                        migrator_tables.insert_constraint({
                            'source_schema_name': source_schema_name,
                            'source_table_name': source_table_name,
                            'source_table_id': source_table_id,
                            'target_schema_name': target_schema_name,
                            'target_table_name': target_table_name,
                            'constraint_name': constraint_name,
                            'constraint_type': 'CHECK (from domain)',
                            'constraint_sql': create_constraint_sql,
                            'constraint_comment': ('added from domains ' + column_comment).strip(),
                        })

            self.config_parser.print_log_message('DEBUG3', f"postgresql_connector: get_create_table_sql: Creating DDL for table {target_schema_name}.{target_table_name}, create_column_sql: {create_column_sql}")
            create_table_sql_parts.append(create_column_sql)

        create_table_sql = ", ".join(create_table_sql_parts)

        if self.config_parser.get_source_db_type() == 'postgresql' and current_table_info and current_table_info.get('relkind') == 'p':
            partition_key_def = current_table_info.get('partition_key_def')
            create_table_sql = f"""CREATE TABLE "{target_schema_name}"."{target_table_name}" ({create_table_sql}) PARTITION BY {partition_key_def}"""
        else:
            create_table_sql = f"""CREATE TABLE "{target_schema_name}"."{target_table_name}" ({create_table_sql})"""
        return create_table_sql

    def is_string_type(self, column_type: str) -> bool:
        string_types = ['CHAR', 'VARCHAR', 'NCHAR', 'NVARCHAR', 'TEXT', 'LONG VARCHAR', 'LONG NVARCHAR', 'UNICHAR', 'UNIVARCHAR']
        return column_type.upper() in string_types

    def is_numeric_type(self, column_type: str) -> bool:
        numeric_types = ['BIGINT', 'INTEGER', 'INT', 'TINYINT', 'SMALLINT', 'FLOAT', 'DOUBLE PRECISION', 'DECIMAL', 'NUMERIC']
        return column_type.upper() in numeric_types

    def fetch_indexes(self, settings):
        source_table_id = settings['source_table_id']
        source_table_schema = settings['source_table_schema']
        source_table_name = settings['source_table_name']

        table_indexes = {}
        order_num = 1
        query = f"""
            SELECT
                i.indexname,
                i.indexdef,
                coalesce(c.constraint_type, 'INDEX') as type,
                obj_description(('"'||i.schemaname||'"."'||i.indexname||'"')::regclass::oid, 'pg_class') as index_comment
            FROM pg_indexes i
            JOIN pg_class t
            ON t.relnamespace::regnamespace::text = i.schemaname
            AND t.relname = i.tablename
            LEFT JOIN information_schema.table_constraints c
            ON i.schemaname = c.table_schema
                and i.tablename = c.table_name
                and i.indexname = c.constraint_name
            WHERE t.oid = {source_table_id}
        """
        try:
            self.connect()
            cursor = self.connection.cursor()
            cursor.execute(query)
            for row in cursor.fetchall():
                columns_match = re.search(r'\((.*?)\)', row[1])
                index_columns = columns_match.group(1) if columns_match else ''
                index_name = row[0]
                index_type = row[2]
                index_sql = row[1]

                table_indexes[order_num] = {
                    'index_name': index_name,
                    'index_type': index_type,
                    'index_owner': source_table_schema,
                    'index_columns': index_columns,
                    'index_sql': index_sql,
                    'index_comment': row[3]
                }
                order_num += 1
            cursor.close()
            self.disconnect()
            return table_indexes
        except Exception as e:
            self.config_parser.print_log_message('ERROR', f"postgresql_connector: fetch_indexes: Error executing query: {query}")
            self.config_parser.print_log_message('ERROR', e)
            raise

    def get_create_index_sql(self, settings):

        # source_schema_name = settings['source_schema_name']
        # source_table_name = settings['source_table_name']
        # source_table_id = settings['source_table_id']
        # index_owner = settings['index_owner']
        index_name = self.config_parser.convert_names_case(settings['index_name'])
        index_type = settings['index_type']
        # target_schema_name = self.config_parser.convert_names_case(settings['target_schema_name'])
        target_schema_name = settings['target_schema_name'] ## target schema is used as it is defined in config, not converted to upper/lower case
        target_table_name = self.config_parser.convert_names_case(settings['target_table_name'])
        index_columns = settings['index_columns']

        # Split index_columns by comma, clean up quotes, convert case, and re-quote
        column_names = []
        for col in index_columns.split(','):
            col = col.strip()
            # Remove backticks, single quotes, and double quotes
            col = col.strip('`').strip("'").strip('"')
            # Convert case using config parser function
            col = self.config_parser.convert_names_case(col)
            # Add to list with double quotes
            column_names.append(f'"{col}"')
        # Join back with comma
        index_columns = ', '.join(column_names)
        # index_comment = settings['index_comment']

        # index_columns = ', '.join(f'"{col}"' for col in index_columns)
        # index_columns_count = row[2]
        create_index_query = ''
        if index_type == 'PRIMARY KEY':
            create_index_query = f"""ALTER TABLE "{target_schema_name}"."{target_table_name}" ADD CONSTRAINT "{index_name}_tab_{target_table_name}" PRIMARY KEY ({index_columns});"""
        else:
            create_index_query = f"""CREATE {'UNIQUE' if index_type == 'UNIQUE' else ''} INDEX "{index_name}_tab_{target_table_name}" ON "{target_schema_name}"."{target_table_name}" ({index_columns});"""

        return create_index_query

        # index_columns_count = 0
        # index_columns_data_types = []
        # for column_name in index_columns.split(','):
        #     column_name = column_name.strip().strip('"')
        #     for col_order_num, column_info in target_columns.items():
        #         if column_name == column_info['column_name']:
        #             index_columns_count += 1
        #             column_data_type = column_info['data_type']
        #             self.config_parser.print_log_message('DEBUG', f"postgresql_connector: get_create_index_sql: Table: {target_schema_name}.{target_table_name}, index: {index_name}, column: {column_name} has data type {column_data_type}")
        #             index_columns_data_types.append(column_data_type)
        #             index_columns_data_types_str = ', '.join(index_columns_data_types)

        # columns = []
        # for col in index_columns.split(","):
        #     col = col.strip().replace(" ASC", "").replace(" DESC", "")
        #     if col not in columns:
        #         columns.append('"'+col+'"')
        # index_columns = ','.join(columns)

    def fetch_constraints(self, settings):
        source_table_id = settings['source_table_id']
        source_table_schema = settings['source_table_schema']
        source_table_name = settings['source_table_name']

        order_num = 1
        constraints = {}
        # c = check constraint, f = foreign key constraint, n = not-null constraint (domains only),
        # p = primary key constraint, u = unique constraint, t = constraint trigger,
        # x = exclusion constraint
        query = f"""
            SELECT
                oid,
                conname,
                CASE WHEN contype = 'c'
                    THEN 'CHECK'
                WHEN contype = 'f'
                    THEN 'FOREIGN KEY'
                WHEN contype = 'p'
                    THEN 'PRIMARY KEY'
                WHEN contype = 'u'
                    THEN 'UNIQUE'
                WHEN contype = 't'
                    THEN 'TRIGGER'
                WHEN contype = 'x'
                    THEN 'EXCLUSION'
                ELSE contype::text
                END as type,
                pg_get_constraintdef(oid) as condef,
                CASE WHEN contype = 'f'
                    THEN (SELECT n.nspname FROM pg_namespace n, pg_class c WHERE c.oid = confrelid AND n.oid = c.relnamespace)
                ELSE NULL
                END AS ref_schema,
                CASE WHEN contype = 'f'
                    THEN (SELECT c.relname FROM pg_class c WHERE oid = confrelid)
                ELSE ''
                END AS ref_table,
                obj_description(oid, 'pg_constraint') as constraint_comment
            FROM pg_constraint
            WHERE conrelid = '{source_table_id}'::regclass
            AND contype NOT IN ('n')
        """
        try:
            self.connect()
            cursor = self.connection.cursor()
            cursor.execute(query)
            for row in cursor.fetchall():
                constraint_name = row[1]
                constraint_type = row[2]
                constraint_sql = row[3]
                constraint_ref_schema = row[4]
                constraint_ref_table = row[5]
                constraint_comment = row[6]

                if constraint_type in ('PRIMARY KEY', 'p', 'P'):
                    continue # Primary key is handled in fetch_indexes

                if constraint_type in ('FOREIGN KEY', 'f', 'F'):
                     constraints[order_num] = {
                         'constraint_name': constraint_name,
                         'constraint_type': constraint_type,
                         'constraint_sql': constraint_sql,
                         'constraint_comment': constraint_comment,
                         'referenced_table_schema': constraint_ref_schema,
                         'referenced_table_name': constraint_ref_table,
                     }
                     order_num += 1
                else:
                     constraints[order_num] = {
                         'constraint_name': constraint_name,
                         'constraint_type': constraint_type,
                         'constraint_sql': constraint_sql,
                         'constraint_comment': constraint_comment,
                     }
                     order_num += 1

            cursor.close()
            self.disconnect()
            return constraints
        except Exception as e:
            self.config_parser.print_log_message('ERROR', f"postgresql_connector: fetch_constraints: Error executing query: {query}")
            self.config_parser.print_log_message('ERROR', e)
            raise

    def fetch_mapping_target_indexes(self, schema_name: str, table_name: str):
        query = f"""
            SELECT
                i.relname as indexname,
                pg_get_indexdef(i.oid) as indexdef,
                ix.indisprimary as is_primary_key,
                am.amname as indextype
            FROM pg_class t
            JOIN pg_index ix ON t.oid = ix.indrelid
            JOIN pg_class i ON i.oid = ix.indexrelid
            JOIN pg_namespace n ON t.relnamespace = n.oid
            JOIN pg_am am ON i.relam = am.oid
            WHERE n.nspname = '{schema_name}' AND t.relname = '{table_name}'
        """
        indexes = []
        try:
            self.connect()
            cursor = self.connection.cursor()
            cursor.execute(query)
            for row in cursor.fetchall():
                indexes.append({
                    'index_name': row[0],
                    'index_def': row[1],
                    'is_primary_key': row[2],
                    'index_type': row[3].upper() if row[3] else 'UNKNOWN'
                })
            cursor.close()
            self.disconnect()
            return indexes
        except Exception as e:
            self.config_parser.print_log_message('ERROR', f"postgresql_connector: fetch_mapping_target_indexes: Error executing query: {query}")
            self.config_parser.print_log_message('ERROR', e)
            return []

    def fetch_mapping_target_constraints(self, schema_name: str, table_name: str):
        query = f"""
            SELECT
                c.conname,
                pg_get_constraintdef(c.oid) as condef,
                CASE WHEN contype = 'c' THEN 'CHECK'
                     WHEN contype = 'f' THEN 'FOREIGN KEY'
                     WHEN contype = 'p' THEN 'PRIMARY KEY'
                     WHEN contype = 'u' THEN 'UNIQUE'
                     WHEN contype = 't' THEN 'TRIGGER'
                     WHEN contype = 'x' THEN 'EXCLUSION'
                     ELSE contype::text END as type
            FROM pg_constraint c
            JOIN pg_class t ON c.conrelid = t.oid
            JOIN pg_namespace n ON t.relnamespace = n.oid
            WHERE n.nspname = '{schema_name}' AND t.relname = '{table_name}'
            AND c.contype NOT IN ('n')
        """
        constraints = []
        try:
            self.connect()
            cursor = self.connection.cursor()
            cursor.execute(query)
            for row in cursor.fetchall():
                constraints.append({
                    'constraint_name': row[0],
                    'constraint_def': row[1],
                    'constraint_type': row[2]
                })
            cursor.close()
            self.disconnect()
            return constraints
        except Exception as e:
            self.config_parser.print_log_message('ERROR', f"postgresql_connector: fetch_mapping_target_constraints: Error executing query: {query}")
            self.config_parser.print_log_message('ERROR', e)
            return []

    def fetch_mapping_target_sequences(self, schema_name: str, table_name: str):
        self.config_parser.print_log_message('DEBUG', f"postgresql_connector: fetch_mapping_target_sequences: Fetching sequences for {schema_name}.{table_name}")
        query_general = f"""
            SELECT 
                a.attname as column_name,
                s.relname as sequence_name,
                n.nspname as sequence_schema_name,
                a.attidentity != '' as is_identity,
                pg_get_expr(ad.adbin, ad.adrelid) as default_expr
            FROM pg_class t
            JOIN pg_namespace tn ON t.relnamespace = tn.oid
            JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum > 0 AND NOT a.attisdropped
            LEFT JOIN pg_attrdef ad ON ad.adrelid = t.oid AND ad.adnum = a.attnum
            LEFT JOIN pg_depend d ON d.refobjid = t.oid AND d.refobjsubid = a.attnum AND d.classid = 'pg_class'::regclass AND d.refclassid = 'pg_class'::regclass
            LEFT JOIN pg_class s ON s.oid = d.objid AND s.relkind = 'S'
            LEFT JOIN pg_namespace n ON s.relnamespace = n.oid
            WHERE tn.nspname = '{schema_name}' AND t.relname = '{table_name}'
            AND (
                s.oid IS NOT NULL 
                OR (ad.adbin IS NOT NULL AND pg_get_expr(ad.adbin, ad.adrelid) LIKE '%nextval(%')
            )
        """

        sequences = []
        try:
            self.connect()
            cursor = self.connection.cursor()
            
            # Fetch default and identity sequences
            cursor.execute(query_general)
            for row in cursor.fetchall():
                col_name = row[0]
                seq_name = row[1]
                seq_schema = row[2]
                is_identity = row[3]
                default_expr = row[4]
                
                used_in_default = False
                if default_expr and 'nextval(' in default_expr:
                    used_in_default = True
                    if not seq_name:
                        import re
                        match = re.search(r"nextval\('([^']+)'", default_expr)
                        if match:
                            full_name = match.group(1).replace('"', '').replace("'::regclass", "")
                            if '.' in full_name:
                                seq_schema, seq_name = full_name.split('.', 1)
                            else:
                                seq_schema = schema_name
                                seq_name = full_name
                
                if seq_name:
                    sequences.append({
                        'sequence_schema_name': seq_schema,
                        'sequence_name': seq_name,
                        'used_in_default': used_in_default,
                        'used_in_identity': is_identity,
                        'used_in_trigger': False,
                        'trigger_name': None,
                        'column_name': col_name
                    })

            # Fetch triggered sequences
            query_triggers = f"""
                SELECT tg.tgname, pg_get_triggerdef(tg.oid) as trigger_def
                FROM pg_trigger tg
                JOIN pg_class t ON tg.tgrelid = t.oid
                JOIN pg_namespace n ON t.relnamespace = n.oid
                WHERE n.nspname = '{schema_name}' AND t.relname = '{table_name}' AND tg.tgname NOT LIKE 'RI_ConstraintTrigger%'
            """
            cursor.execute(query_triggers)
            for row in cursor.fetchall():
                tgname = row[0]
                tgdef = row[1]
                if tgdef and 'nextval(' in tgdef:
                    import re
                    matches = re.findall(r"nextval\('([^']+)'", tgdef)
                    for match in matches:
                        full_name = match.replace('"', '').replace("'::regclass", "")
                        if '.' in full_name:
                            seq_schema, seq_name = full_name.split('.', 1)
                        else:
                            seq_schema = schema_name
                            seq_name = full_name
                        sequences.append({
                            'sequence_schema_name': seq_schema,
                            'sequence_name': seq_name,
                            'used_in_default': False,
                            'used_in_identity': False,
                            'used_in_trigger': True,
                            'trigger_name': tgname,
                            'column_name': None
                        })

            cursor.close()
            self.disconnect()
            
            # Deduplicate
            unique_seqs = {}
            for s in sequences:
                key = (s['sequence_schema_name'], s['sequence_name'], s['column_name'], s['trigger_name'])
                unique_seqs[key] = s
            return list(unique_seqs.values())
        except Exception as e:
            self.config_parser.print_log_message('ERROR', f"postgresql_connector: fetch_mapping_target_sequences: Error fetching sequences for {schema_name}.{table_name}")
            self.config_parser.print_log_message('ERROR', e)
            return []

    def get_create_constraint_sql(self, settings):
        create_constraint_query = ''
        source_db_type = settings['source_db_type']
        # target_schema_name = self.config_parser.convert_names_case(settings['target_schema_name'])
        target_schema_name = settings['target_schema_name'] ## target schema is used as it is defined in config, not converted to upper/lower case
        target_table_name = self.config_parser.convert_names_case(settings['target_table_name'])
        target_columns = settings['target_columns']
        constraint_name = self.config_parser.convert_names_case(settings['constraint_name'])
        constraint_type = settings['constraint_type']
        constraint_owner = self.config_parser.convert_names_case(settings['constraint_owner'])
        constraint_columns = self.config_parser.convert_names_case(settings['constraint_columns'])
        #referenced_table_schema = target_schema_name
        # referenced_table_schema = self.config_parser.convert_names_case(settings['referenced_table_schema'])
        referenced_table_schema = settings['referenced_table_schema']
        referenced_table_name = self.config_parser.convert_names_case(settings['referenced_table_name'])
        referenced_columns = self.config_parser.convert_names_case(settings['referenced_columns'])
        delete_rule = settings['delete_rule'] if 'delete_rule' in settings else 'NO ACTION'
        update_rule = settings['update_rule'] if 'update_rule' in settings else 'NO ACTION'
        constraint_comment = settings['constraint_comment']
        constraint_sql = self.config_parser.convert_names_case(settings['constraint_sql']) if settings.get('constraint_sql') else ''
        constraint_status = settings['constraint_status'] if 'constraint_status' in settings else 'ENABLED'

        # Split constraint_columns by comma, clean up quotes, convert case, and re-quote
        if constraint_columns:
            column_names = []
            for col in constraint_columns.split(','):
                col = col.strip()
                # Remove backticks, single quotes, and double quotes
                col = col.strip('`').strip("'").strip('"')
                # Convert case using config parser function
                col = self.config_parser.convert_names_case(col)
                # Add to list with double quotes
                column_names.append(f'"{col}"')
            # Join back with comma
            constraint_columns = ', '.join(column_names)

        # Split referenced_columns by comma, clean up quotes, convert case, and re-quote
        if referenced_columns:
            ref_column_names = []
            for col in referenced_columns.split(','):
                col = col.strip()
                # Remove backticks, single quotes, and double quotes
                col = col.strip('`').strip("'").strip('"')
                # Convert case using config parser function
                col = self.config_parser.convert_names_case(col)
                # Add to list with double quotes
                ref_column_names.append(f'"{col}"')
            # Join back with comma
            referenced_columns = ', '.join(ref_column_names)

        if source_db_type != 'postgresql':
            if constraint_type == 'FOREIGN KEY':
                create_constraint_query = (
                    f'ALTER TABLE "{target_schema_name}"."{target_table_name}" ADD CONSTRAINT "{constraint_name}_tab_{target_table_name}" '
                    f'FOREIGN KEY ({constraint_columns}) REFERENCES "{target_schema_name}"."{referenced_table_name}" ({referenced_columns})'
                )
                if delete_rule == 'CASCADE':
                    create_constraint_query += " ON DELETE CASCADE"
                if update_rule == 'CASCADE':
                    create_constraint_query += " ON UPDATE CASCADE"
                if constraint_comment:
                    create_constraint_query += f" COMMENT '{constraint_comment}'"
            elif constraint_type == 'CHECK':
                # Replace column names in constraint_sql with double-quoted names using precise match

                if constraint_sql and target_columns:
                    for col_info in target_columns.values():
                        col_name = col_info['column_name']
                        # Use word boundary for precise match, preserve case
                        pattern = r'\b{}\b'.format(re.escape(col_name))
                        constraint_sql = re.sub(pattern, f'"{col_name}"', constraint_sql)
                create_constraint_query = f"""ALTER TABLE "{target_schema_name}"."{target_table_name}" ADD CONSTRAINT "{constraint_name}_tab_{target_table_name}" CHECK ({constraint_sql})"""
        else:
            create_constraint_query = f"""ALTER TABLE "{target_schema_name}"."{target_table_name}" ADD CONSTRAINT "{constraint_name}" {constraint_sql}"""
        return create_constraint_query

    def get_aliases(self, settings):
        return {}

    def fetch_triggers(self, table_id: int, table_schema: str, table_name: str):
        triggers = {}
        order_num = 1
        query = f"""
            SELECT
                t.oid,
                t.tgname,
                pg_get_triggerdef(t.oid) as definition,
                t.tgtype::text,
                t.tgisinternal,
                t.tgenabled,
                obj_description(t.oid, 'pg_trigger') as comment,
                t.tgtype
            FROM pg_trigger t
            JOIN pg_class c ON t.tgrelid = c.oid
            JOIN pg_namespace n ON c.relnamespace = n.oid
            WHERE c.oid = {table_id}
            AND NOT t.tgisinternal
        """
        try:
            self.connect()
            cursor = self.connection.cursor()
            cursor.execute(query)
            for row in cursor.fetchall():
                # Detect event and timing from tgtype (bitmask) or just rely on definition
                # tgtype definition:
                # 2 = BEFORE
                # 0 = AFTER (default?) -> actually checking bits
                # It is easier to rely on pg_get_triggerdef for the full SQL.
                # But planner expects decomposed values: event, new, old, etc?
                # looking at planner.py:
                # trigger_details['event']
                # trigger_details['new']
                # trigger_details['old']
                # trigger_details['sql']

                # For PG, we simply put the full definition in 'sql'.
                # The other fields might be purely informational for logging or other connectors.

                triggers[order_num] = {
                    'id': row[0],
                    'name': row[1],
                    'sql': row[2],
                    'event': 'See SQL', # simplified
                    'new': '',
                    'old': '',
                    'comment': row[6]
                }
                order_num += 1
            cursor.close()
            self.disconnect()
            return triggers
        except Exception as e:
            self.config_parser.print_log_message('ERROR', f"postgresql_connector: fetch_triggers: Error executing query: {query}")
            self.config_parser.print_log_message('ERROR', e)
            raise

    def execute_query(self, query: str, params=None):
        with self.connection.cursor() as cursor:
            cursor.execute(query, params)

    def copy_from_file(self, sql: str, file_path: str):
        self.config_parser.print_log_message('DEBUG3', f"postgresql_connector: copy_from_file: sql: {sql}, file_path: {file_path}")
        with open(file_path, 'r') as file:
            with self.connection.cursor() as cursor:
                try:
                    cursor.copy_expert(sql, file)
                    for notice in cursor.connection.notices:
                        self.config_parser.print_log_message('INFO', notice)
                    cursor.connection.commit()
                except Exception as e:
                    for notice in cursor.connection.notices:
                        self.config_parser.print_log_message('INFO', notice)
                    self.config_parser.print_log_message('ERROR', f"postgresql_connector: copy_from_file: file_path: {file_path}: Error executing copy_expert: {e}")
                    raise

    def execute_sql_script(self, script_path: str):
        with open(script_path, 'r') as file:
            script = file.read()

        with self.connection.cursor() as cursor:
            try:
                cursor.execute(script)
                for notice in cursor.connection.notices:
                    self.config_parser.print_log_message('INFO', notice)
            except Exception as e:
                for notice in cursor.connection.notices:
                    self.config_parser.print_log_message('INFO', notice)
                self.config_parser.print_log_message('ERROR', f"postgresql_connector: execute_sql_script: Error executing script: {e}")
                raise

    def begin_transaction(self):
        self.connection.autocommit = False

    def commit_transaction(self):
        self.connection.commit()
        self.connection.autocommit = True

    def rollback_transaction(self):
        self.connection.rollback()

    def migrate_table(self, migrate_target_connection, settings):
        part_name = 'initialize'
        source_table_rows = 0
        target_table_rows = 0
        total_inserted_rows = 0
        migration_stats = {}
        batch_number = 0
        shortest_batch_seconds = 0
        longest_batch_seconds = 0
        average_batch_seconds = 0
        chunk_start_row_number = 0
        chunk_end_row_number = 0
        processing_start_time = time.time()
        order_by_clause = ''
        try:
            worker_id = settings['worker_id']
            source_schema_name = settings['source_schema_name']
            source_table_name = settings['source_table_name']
            source_table_id = settings['source_table_id']
            source_columns = settings['source_columns']
            # target_schema_name = self.config_parser.convert_names_case(settings['target_schema_name'])
            target_schema_name = settings['target_schema_name'] ## target schema is used as it is defined in config, not converted to upper/lower case
            target_table_name = self.config_parser.convert_names_case(settings['target_table_name'])
            target_columns = settings['target_columns']
            # primary_key_columns = settings['primary_key_columns']
            batch_size = settings['batch_size']
            migrator_tables = settings['migrator_tables']
            migration_limitation = settings['migration_limitation']
            chunk_size = settings['chunk_size']
            chunk_number = settings['chunk_number']
            resume_after_crash = settings['resume_after_crash']
            drop_unfinished_tables = settings['drop_unfinished_tables']

            source_table_rows = self.get_rows_count(source_schema_name, source_table_name, migration_limitation)
            target_table_rows = migrate_target_connection.get_rows_count(target_schema_name, target_table_name)

            total_chunks = self.config_parser.get_total_chunks(source_table_rows, chunk_size)
            if chunk_size == -1:
                chunk_size = source_table_rows + 1

            migration_stats = {
                'rows_migrated': target_table_rows,
                'chunk_number': chunk_number,
                'total_chunks': total_chunks,
                'source_table_rows': source_table_rows,
                'target_table_rows': target_table_rows,
                'finished': True if source_table_rows == 0 else False,
            }
            ## source_schema_name, source_table_name, source_table_id, source_table_rows, worker_id, target_schema_name, target_table_name, target_table_rows
            protocol_id = migrator_tables.insert_data_migration({
                'worker_id': worker_id,
                'source_table_id': source_table_id,
                'source_schema_name': source_schema_name,
                'source_table_name': source_table_name,
                'target_schema_name': target_schema_name,
                'target_table_name': target_table_name,
                'source_table_rows': source_table_rows,
                'target_table_rows': target_table_rows,
            })

            if source_table_rows == 0:
                self.config_parser.print_log_message('INFO', f"postgresql_connector: migrate_table: Worker {worker_id}: Table {source_table_name} is empty - skipping data migration.")
                migrator_tables.update_data_migration_status({
                        'row_id': protocol_id,
                        'success': True,
                        'message': 'Skipped',
                        'target_table_rows': 0,
                        'batch_count': 0,
                        'shortest_batch_seconds': 0,
                        'longest_batch_seconds': 0,
                        'average_batch_seconds': 0,
                    })

                return migration_stats

            else:

                data_conflict_action = settings.get('data_conflict_action')
                if source_table_rows > target_table_rows or data_conflict_action in ('merge_keep_target', 'merge_keep_source', 'replace'):
                    migrator_tables.update_data_migration_started(protocol_id)

                    part_name = 'migrate_table in batches using cursor'
                    self.config_parser.print_log_message('INFO', f"postgresql_connector: migrate_table: Worker {worker_id}: Source table {source_table_name}: {source_table_rows} rows / Target table {target_table_name}: {target_table_rows} rows - starting data migration.")

                    select_columns_list = []
                    orderby_columns_list = []
                    insert_columns_list = []
                    for order_num, col in source_columns.items():
                        self.config_parser.print_log_message('DEBUG2',
                                                            f"Worker {worker_id}: Table {source_schema_name}.{source_table_name}: Processing column {col['column_name']} ({order_num}) with data type {col['data_type']}")

                        if col['data_type'].lower() == 'datetime':
                            select_columns_list.append(f"TO_CHAR({col['column_name']}, '%Y-%m-%d %H:%M:%S') as {col['column_name']}")
                        #     select_columns_list.append(f"ST_asText(`{col['column_name']}`) as `{col['column_name']}`")
                        # elif col['data_type'].lower() == 'set':
                        #     select_columns_list.append(f"cast(`{col['column_name']}` as char(4000)) as `{col['column_name']}`")
                        else:
                            select_columns_list.append(f'''"{col['column_name']}"''')

                        insert_columns_list.append(f'''"{self.config_parser.convert_names_case(col['column_name'])}"''')
                        orderby_columns_list.append(f'''"{col['column_name']}"''')

                    select_columns = ', '.join(select_columns_list)
                    orderby_columns = ', '.join(orderby_columns_list)
                    insert_columns = ', '.join(insert_columns_list)

                    if resume_after_crash and not drop_unfinished_tables:
                        chunk_number = self.config_parser.get_total_chunks(target_table_rows, chunk_size)
                        self.config_parser.print_log_message('DEBUG', f"postgresql_connector: migrate_table: Worker {worker_id}: Resuming migration for table {source_schema_name}.{source_table_name} from chunk {chunk_number} with data chunk size {chunk_size}.")
                        chunk_offset = target_table_rows
                    else:
                        chunk_offset = (chunk_number - 1) * chunk_size

                    chunk_start_row_number = chunk_offset + 1
                    chunk_end_row_number = chunk_offset + chunk_size

                    self.config_parser.print_log_message('DEBUG', f"postgresql_connector: migrate_table: Worker {worker_id}: Migrating table {source_schema_name}.{source_table_name}: chunk {chunk_number}, data chunk size {chunk_size}, batch size {batch_size}, chunk offset {chunk_offset}, chunk end row number {chunk_end_row_number}, source table rows {source_table_rows}")
                    order_by_clause = ''

                    query = f'''SELECT {select_columns} FROM "{source_schema_name}"."{source_table_name}" '''
                    if migration_limitation:
                        query += f" WHERE {migration_limitation}"
                    primary_key_columns = migrator_tables.select_primary_key({'source_schema_name': source_schema_name, 'source_table_name': source_table_name})
                    self.config_parser.print_log_message('DEBUG2', f"postgresql_connector: migrate_table: Worker {worker_id}: Primary key columns for {source_schema_name}.{source_table_name}: {primary_key_columns}")
                    if primary_key_columns:
                        orderby_columns = primary_key_columns
                    order_by_clause = f""" ORDER BY {orderby_columns}"""
                    query += order_by_clause + f" LIMIT {chunk_size} OFFSET {chunk_offset}"

                    self.config_parser.print_log_message('DEBUG', f"postgresql_connector: migrate_table: Worker {worker_id}: Fetching data with cursor using query: {query}")

                    part_name = 'execute query'
                    cursor = self.connection.cursor()
                    cursor.arraysize = batch_size

                    batch_start_time = time.time()
                    reading_start_time = batch_start_time
                    processing_start_time = batch_start_time
                    batch_end_time = None
                    batch_number = 0
                    batch_durations = []

                    cursor.execute(query)
                    total_inserted_rows = 0
                    while True:
                        records = cursor.fetchmany(batch_size)
                        if not records:
                            break
                        batch_number += 1
                        reading_end_time = time.time()
                        reading_duration = reading_end_time - reading_start_time
                        self.config_parser.print_log_message('DEBUG', f"postgresql_connector: migrate_table: Worker {worker_id}: Fetched {len(records)} rows (batch {batch_number}) from source table {source_table_name}.")

                        transforming_start_time = time.time()
                        records = [
                            {column['column_name']: value for column, value in zip(source_columns.values(), record)}
                            for record in records
                        ]
                        for record in records:
                            for order_num, column in source_columns.items():
                                column_name = column['column_name']
                                column_type = column['data_type']
                                if column_type in ['bytea'] and record[column_name] is not None:
                                    record[column_name] = record[column_name].tobytes()

                        # Insert batch into target table
                        self.config_parser.print_log_message('DEBUG', f"postgresql_connector: migrate_table: Worker {worker_id}: Starting insert of {len(records)} rows from source table {source_table_name}")
                        transforming_end_time = time.time()
                        transforming_duration = transforming_end_time - transforming_start_time
                        inserting_start_time = time.time()
                        inserted_rows = migrate_target_connection.insert_batch({
                            'target_schema_name': target_schema_name,
                            'target_table_name': target_table_name,
                            'target_columns': target_columns,
                            'data': records,
                            'worker_id': worker_id,
                            'migrator_tables': migrator_tables,
                            'insert_columns': insert_columns,
                            'insert_values': settings.get('insert_values'),
                        })
                        total_inserted_rows += inserted_rows
                        inserting_end_time = time.time()
                        inserting_duration = inserting_end_time - inserting_start_time

                        batch_end_time = time.time()
                        batch_duration = batch_end_time - batch_start_time
                        batch_durations.append(batch_duration)
                        percent_done = round(total_inserted_rows / source_table_rows * 100, 2)

                        batch_start_dt = datetime.datetime.fromtimestamp(batch_start_time)
                        batch_end_dt = datetime.datetime.fromtimestamp(batch_end_time)
                        batch_start_str = batch_start_dt.strftime('%Y-%m-%d %H:%M:%S.%f')
                        batch_end_str = batch_end_dt.strftime('%Y-%m-%d %H:%M:%S.%f')
                        migrator_tables.insert_batches_stats({
                            'source_schema_name': source_schema_name,
                            'source_table_name': source_table_name,
                            'source_table_id': source_table_id,
                            'chunk_number': chunk_number,
                            'batch_number': batch_number,
                            'batch_start': batch_start_str,
                            'batch_end': batch_end_str,
                            'batch_rows': inserted_rows,
                            'batch_seconds': batch_duration,
                            'worker_id': worker_id,
                            'reading_seconds': reading_duration,
                            'transforming_seconds': transforming_duration,
                            'writing_seconds': inserting_duration,
                        })

                        msg = (
                            f"Worker {worker_id}: Inserted {inserted_rows} "
                            f"(total: {total_inserted_rows} from: {source_table_rows} "
                            f"({percent_done}%)) rows into target table '{target_table_name}': "
                            f"Batch {batch_number} duration: {batch_duration:.2f} seconds "
                            f"(r: {reading_duration:.2f}, t: {transforming_duration:.2f}, w: {inserting_duration:.2f})"
                        )
                        self.config_parser.print_log_message('INFO', msg)

                        batch_start_time = time.time()
                        reading_start_time = batch_start_time

                    target_table_rows = migrate_target_connection.get_rows_count(target_schema_name, target_table_name)
                    self.config_parser.print_log_message('INFO', f"postgresql_connector: migrate_table: Worker {worker_id}: Target table {target_schema_name}.{target_table_name} has {target_table_rows} rows")

                    shortest_batch_seconds = min(batch_durations) if batch_durations else 0
                    longest_batch_seconds = max(batch_durations) if batch_durations else 0
                    average_batch_seconds = sum(batch_durations) / len(batch_durations) if batch_durations else 0
                    self.config_parser.print_log_message('INFO', f"postgresql_connector: migrate_table: Worker {worker_id}: Migrated {total_inserted_rows} rows from {source_table_name} to {target_schema_name}.{target_table_name} in {batch_number} batches: "
                                                            f"Shortest batch: {shortest_batch_seconds:.2f} seconds, "
                                                            f"Longest batch: {longest_batch_seconds:.2f} seconds, "
                                                            f"Average batch: {average_batch_seconds:.2f} seconds")

                    cursor.close()

                elif source_table_rows <= target_table_rows and data_conflict_action not in ('merge_keep_target', 'merge_keep_source', 'replace'):
                    self.config_parser.print_log_message('INFO', f"postgresql_connector: migrate_table: Worker {worker_id}: Source table {source_table_name} has {source_table_rows} rows, which is less than or equal to target table {target_table_name} with {target_table_rows} rows. No data migration needed.")

                migration_stats = {
                    'rows_migrated': total_inserted_rows,
                    'chunk_number': chunk_number,
                    'total_chunks': total_chunks,
                    'source_table_rows': source_table_rows,
                    'target_table_rows': target_table_rows,
                    'finished': False,
                }

                self.config_parser.print_log_message('DEBUG', f"postgresql_connector: migrate_table: Worker {worker_id}: Migration stats: {migration_stats}")
                if source_table_rows <= target_table_rows or chunk_number >= total_chunks:
                    self.config_parser.print_log_message('DEBUG3', f"postgresql_connector: migrate_table: Worker {worker_id}: Setting migration status to finished for table {source_table_name} (chunk {chunk_number}/{total_chunks})")
                    migration_stats['finished'] = True
                    migrator_tables.update_data_migration_status({
                        'row_id': protocol_id,
                        'success': True,
                        'message': 'OK',
                        'target_table_rows': target_table_rows,
                        'batch_count': batch_number,
                        'shortest_batch_seconds': shortest_batch_seconds,
                        'longest_batch_seconds': longest_batch_seconds,
                        'average_batch_seconds': average_batch_seconds,
                    })

                migrator_tables.insert_data_chunk({
                    'worker_id': worker_id,
                    'source_table_id': source_table_id,
                    'source_schema_name': source_schema_name,
                    'source_table_name': source_table_name,
                    'target_schema_name': target_schema_name,
                    'target_table_name': target_table_name,
                    'source_table_rows': source_table_rows,
                    'target_table_rows': target_table_rows,
                    'chunk_number': chunk_number,
                    'chunk_size': chunk_size,
                    'migration_limitation': migration_limitation,
                    'chunk_start': chunk_start_row_number,
                    'chunk_end': chunk_end_row_number,
                    'inserted_rows': total_inserted_rows,
                    'batch_size': batch_size,
                    'total_batches': batch_number,
                    'task_started': datetime.datetime.fromtimestamp(processing_start_time).strftime('%Y-%m-%d %H:%M:%S.%f'),
                    'task_completed': datetime.datetime.fromtimestamp(time.time()).strftime('%Y-%m-%d %H:%M:%S.%f'),
                    'order_by_clause': order_by_clause,
                })

                return migration_stats

        except Exception as e:
            self.config_parser.print_log_message('ERROR', f"postgresql_connector: migrate_table: Worker {worker_id}: Error during {part_name} -> {e}")
            self.config_parser.print_log_message('ERROR', f"postgresql_connector: migrate_table: Worker {worker_id}: Full stack trace: {traceback.format_exc()}")
            raise e

    def insert_batch(self, settings):
        target_schema_name = settings['target_schema_name']
        target_table_name = settings['target_table_name']
        columns = settings['target_columns']
        data = settings['data']
        worker_id = settings['worker_id']
        insert_columns = settings.get('insert_columns', None)

        insert_columns_provided = settings.get('insert_columns', None)
        insert_values = settings.get('insert_values', None)

        if not insert_columns:
            insert_columns = [f'"{columns[col]["column_name"]}"' for col in sorted(columns.keys(), key=lambda x: int(x))]

        if not insert_values:
            insert_values = ', '.join(['%s' for _ in columns.keys()])

        if isinstance(insert_columns, list):
            insert_columns = ', '.join(insert_columns)

        inserted_rows = 0
        self.config_parser.print_log_message('DEBUG2', f"postgresql_connector: insert_batch: Worker {worker_id}: insert_batch into {target_schema_name}.{target_table_name} with {len(data)} rows, columns: {insert_columns}, data type: {type(data)}")
        try:
            # Ensure data is a list of tuples
            self.config_parser.print_log_message('DEBUG2', f"postgresql_connector: insert_batch: Worker {worker_id}: Started insert batch into {target_schema_name}.{target_table_name} with {len(data)} rows")
            if isinstance(data, list) and all(isinstance(item, dict) for item in data):

                # Symmetrically unpack the payload values
                if insert_columns_provided and len(data) > 0:
                    extract_keys = list(data[0].keys())
                else:
                    extract_keys = [columns[col]['column_name'] for col in sorted(columns.keys(), key=lambda x: int(x))]

                formatted_data = []
                for item in data:
                    row = []
                    for col_name in extract_keys:
                        row.append(item.get(col_name))
                    formatted_data.append(tuple(row))
                self.config_parser.print_log_message('DEBUG3', f"postgresql_connector: insert_batch: INSERT COLUMNS: {insert_columns}")
                self.config_parser.print_log_message('DEBUG3', f"postgresql_connector: insert_batch: EXTRACT KEYS: {extract_keys}")
                self.config_parser.print_log_message('DEBUG3', f"postgresql_connector: insert_batch: FORMATTED DATA[0]: {formatted_data[0]}")
                data = formatted_data
            else:
                self.config_parser.print_log_message('ERROR', f"postgresql_connector: insert_batch: Worker {worker_id}: Data for insert_batch must be a list of dictionaries, got {type(data)}")
                return 0

            self.config_parser.print_log_message('DEBUG2', f"postgresql_connector: insert_batch: Worker {worker_id}: insert_batch [2] into {target_schema_name}.{target_table_name} with {len(data)} rows, columns: |{insert_columns}| data type: {type(data)}")

            conflict_action = settings.get('data_conflict_action')
            conflict_clause = ""
            if conflict_action == 'merge_keep_target':
                conflict_clause = " ON CONFLICT DO NOTHING"
            elif conflict_action == 'merge_keep_source':
                primary_key_columns = settings.get('primary_key_columns')
                if primary_key_columns:
                    col_names = [c.strip().strip('"') for c in insert_columns.split(',')]
                    pk_names = [c.strip().strip('"') for c in primary_key_columns.split(',')]
                    update_parts = []
                    for col in col_names:
                        if col not in pk_names:
                            update_parts.append(f'"{col}" = EXCLUDED."{col}"')
                    
                    if update_parts:
                        where_parts = []
                        for col in col_names:
                            if col not in pk_names:
                                where_parts.append(f'"{target_table_name}"."{col}" IS DISTINCT FROM EXCLUDED."{col}"')
                        
                        where_clause = ""
                        if where_parts:
                            where_clause = " WHERE " + " OR ".join(where_parts)
                        conflict_clause = f" ON CONFLICT ({primary_key_columns}) DO UPDATE SET " + ", ".join(update_parts) + where_clause
                    else:
                        conflict_clause = f" ON CONFLICT ({primary_key_columns}) DO NOTHING"
                else:
                    self.config_parser.print_log_message('ERROR', f"postgresql_connector: insert_batch: Worker {worker_id}: Cannot use 'merge_keep_source' for table {target_table_name} without a primary key.")
                    raise ValueError(f"Table {target_table_name} lacks a primary key required for 'merge_keep_source'.")

            insert_query_str = f"""INSERT INTO "{target_schema_name}"."{target_table_name}" ({insert_columns}) VALUES ({insert_values}){conflict_clause}"""
            
            with self.connection.cursor() as cursor:
                insert_query = sql.SQL(insert_query_str)
                self.config_parser.print_log_message('DEBUG3', f"postgresql_connector: insert_batch: Worker {worker_id}: Insert query: {insert_query_str}")
                self.connection.autocommit = False
                try:
                    if self.session_settings:
                        cursor.execute(self.session_settings)
                    self.config_parser.print_log_message('DEBUG3', f"postgresql_connector: insert_batch: Worker {worker_id}: Starting psycopg2.extras.execute_batch into {target_table_name} with {len(data)} rows")

                    psycopg2.extras.execute_batch(cursor, insert_query, data)
                    inserted_rows = len(data)
                    self.connection.commit()
                except Exception as e:
                    self.config_parser.print_log_message('ERROR', f"postgresql_connector: insert_batch: Worker {worker_id}: Error inserting batch data into {target_table_name}: {e}")
                    self.config_parser.print_log_message('ERROR', f"postgresql_connector: insert_batch: Worker {worker_id}: Trying to insert row by row.")
                    self.connection.rollback()
                    for row in data:
                        try:
                            cursor.execute(insert_query, row)
                            inserted_rows += 1
                            self.connection.commit()
                        except Exception as e:
                            self.connection.rollback()
                            self.config_parser.print_log_message('ERROR', f"postgresql_connector: insert_batch: Worker {worker_id}: Error inserting row into {target_table_name}: {row}")
                            self.config_parser.print_log_message('ERROR', e)

                self.connection.autocommit = True

        except Exception as e:
            self.config_parser.print_log_message('ERROR', f"postgresql_connector: insert_batch: Worker {worker_id}: Error before inserting batch data: {e}")
            raise

        return inserted_rows

    def fetch_funcproc_names(self, schema: str):
        funcprocs = {}
        order_num = 1
        query = f"""
            SELECT
                p.oid,
                p.proname,
                pg_get_function_identity_arguments(p.oid) as arguments,
                obj_description(p.oid, 'pg_proc') as comment,
                p.prokind
            FROM pg_proc p
            JOIN pg_namespace n ON p.pronamespace = n.oid
            WHERE n.nspname = '{schema}'
              AND p.prokind IN ('f', 'p')
            ORDER BY p.proname
        """
        try:
            self.connect()
            cursor = self.connection.cursor()
            cursor.execute(query)
            for row in cursor.fetchall():
                func_type = 'PROCEDURE' if row[4] == 'p' else 'FUNCTION'
                funcprocs[order_num] = {
                    'id': row[0],
                    'name': row[1],
                    'header': f"{row[1]}({row[2]})",
                    'comment': row[3],
                    'type': func_type
                }
                order_num += 1
            cursor.close()
            self.disconnect()
            return funcprocs
        except Exception as e:
            self.config_parser.print_log_message('ERROR', f"postgresql_connector: fetch_funcproc_names: Error executing query: {query}")
            self.config_parser.print_log_message('ERROR', e)
            raise

    def fetch_funcproc_code(self, funcproc_id: int):
        query = f"SELECT pg_get_functiondef({funcproc_id})"
        try:
            self.connect()
            cursor = self.connection.cursor()
            cursor.execute(query)
            code = cursor.fetchone()[0]
            cursor.close()
            self.disconnect()
            return code
        except Exception as e:
            self.config_parser.print_log_message('ERROR', f"postgresql_connector: fetch_funcproc_code: Error executing query: {query}")
            self.config_parser.print_log_message('ERROR', e)
            raise

    def convert_funcproc_code(self, settings):
        funcproc_code = settings['funcproc_code']
        # target_db_type = settings['target_db_type']
        source_schema_name = settings['source_schema_name']
        target_schema_name = settings['target_schema_name']

        # Simple schema replacement if they differ
        converted_code = funcproc_code
        if source_schema_name != target_schema_name:
            # Replace schema references
            # Use loose matching or generic replace for source_schema_name
            # This is risky if schema name is common word, but standard practice in this simple migration
            # Better: Replace "source_schema_name". with "target_schema_name".
            converted_code = converted_code.replace(f'"{source_schema_name}".', f'"{target_schema_name}".')
            # Also without quotes?
            converted_code = converted_code.replace(f'{source_schema_name}.', f'{target_schema_name}.')

        return converted_code

    def handle_error(self, e, description=None):
        self.config_parser.print_log_message('ERROR', f"postgresql_connector: handle_error: An error in {self.__class__.__name__} ({description}): {e}")
        self.config_parser.print_log_message('ERROR', traceback.format_exc())
        if self.on_error_action == 'stop':
            self.config_parser.print_log_message('ERROR', "postgresql_connector: handle_error: Stopping due to error.")
            exit(1)

    def fetch_sequences(self, schema_name: str):
        return {}

    def fetch_table_sequences(self, table_schema: str, table_name: str):
        sequence_data = {}
        order_num = 1
        try:
            query = f"""
                SELECT
                    c.relname::text AS sequence_name,
                    c.oid AS sequence_id,
                    a.attname AS column_name,
                    'SELECT SETVAL( (SELECT oid from pg_class s where s.relname = ''' || c.relname ||
                    ''' and s.relkind = ''S'' AND s.relnamespace::regnamespace::text = ''' ||
                    c.relnamespace::regnamespace::text || '''), (SELECT MAX(' || quote_ident(a.attname) || ') /*+ 1*/ FROM ' ||
                    quote_ident(t.relnamespace::regnamespace::text)||'.'|| quote_ident(t.relname) || '));' as sequence_sql
                FROM
                    pg_depend d
                    JOIN pg_class c ON d.objid = c.oid
                    JOIN pg_attribute a ON d.refobjid = a.attrelid AND a.attnum = d.refobjsubid
                    JOIN pg_class t ON t.oid = d.refobjid
                WHERE
                    c.relkind = 'S'  /* sequence */
                    AND t.relname = '{table_name}'
                    AND t.relkind = 'r' /* regular local table */
                    AND d.refobjsubid > 0
                    AND c.relnamespace = '{table_schema}'::regnamespace
                ORDER BY 2,3
                """
            # self.connect()
            cursor = self.connection.cursor()
            cursor.execute(query)
            for row in cursor.fetchall():
                sequence_details = self.get_sequence_details(table_schema, row[0])

                sequence_data[order_num] = {
                    'name': row[0],
                    'id': row[1],
                    'column_name': row[2],
                    'set_sequence_sql': row[3],
                    'details': sequence_details # Embed details
                }
            cursor.close()
            # self.disconnect()
            return sequence_data
        except Exception as e:
            self.config_parser.print_log_message('ERROR', f"postgresql_connector: fetch_sequences: Error executing sequence query: {query}")
            self.config_parser.print_log_message('ERROR', e)

    def get_sequence_details(self, sequence_owner, sequence_name):
        query = f"""
            SELECT
                s.seqmin,
                s.seqmax,
                s.seqincrement,
                s.seqcycle,
                s.seqcache,
                s.seqstart
            FROM pg_sequence s
            JOIN pg_class c ON s.seqrelid = c.oid
            JOIN pg_namespace n ON c.relnamespace = n.oid
            WHERE n.nspname = '{sequence_owner}'
              AND c.relname = '{sequence_name}'
        """
        try:
            cursor = self.connection.cursor()
            cursor.execute(query)
            result = cursor.fetchone()
            cursor.close()

            if result:
                return {
                    'name': sequence_name,
                    'min_value': result[0],
                    'max_value': result[1],
                    'increment_by': result[2],
                    'cycle': result[3],
                    'cache_size': result[4],
                    'last_value': result[5], # seqstart is 'start value', current value needs get_sequence_current_value
                    'start_value': result[5],
                    'comment': ''
                }
            else:
                return None

        except Exception as e:
            self.config_parser.print_log_message('ERROR', f"postgresql_connector: get_sequence_details: Error executing sequence query: {query}")
            self.config_parser.print_log_message('ERROR', e)
            raise

    def get_sequence_current_value(self, sequence_id: int):
        try:
            query = f"""select '"'||relnamespace::regnamespace::text||'"."'||relname||'"' as seqname from pg_class where oid = {sequence_id}"""
            cursor = self.connection.cursor()
            cursor.execute(query)
            sequence_data = cursor.fetchone()
            sequence_name = f"{sequence_data[0]}"

            query = f"""SELECT last_value FROM {sequence_name}"""
            self.connect()
            cursor = self.connection.cursor()
            cursor.execute(query)
            cur_value = cursor.fetchone()[0]
            cursor.close()
            self.disconnect()
            return cur_value
        except Exception as e:
            self.config_parser.print_log_message('ERROR', f"postgresql_connector: get_sequence_current_value: Error executing query: {query}")
            self.config_parser.print_log_message('ERROR', e)
            raise

    def get_rows_count(self, table_schema: str, table_name: str, migration_limitation: str = None):
        query = f"""SELECT count(*) FROM "{table_schema}"."{table_name}" """
        if migration_limitation:
            query += f" WHERE {migration_limitation}"
        self.config_parser.print_log_message('DEBUG3', f"postgresql_connector: get_rows_count: postgresql: get_rows_count query: {query}")
        cursor = self.connection.cursor()
        cursor.execute(query)
        count = cursor.fetchone()[0]
        cursor.close()
        return count

    def get_table_size(self, table_schema: str, table_name: str):
        query = f"""SELECT pg_total_relation_size('{table_schema}.{table_name}')"""
        cursor = self.connection.cursor()
        cursor.execute(query)
        size = cursor.fetchone()[0]
        cursor.close()
        return size

    def convert_trigger(self, settings: dict):
        trigger_sql = settings['trigger_sql']
        source_schema_name = settings['source_schema_name']
        target_schema_name = settings['target_schema_name']

        # Simple schema replacement
        converted_code = trigger_sql
        if source_schema_name != target_schema_name:
             converted_code = converted_code.replace(f'"{source_schema_name}".', f'"{target_schema_name}".')
             converted_code = converted_code.replace(f'{source_schema_name}.', f'{target_schema_name}.')

        return converted_code

    def fetch_views_names(self, source_schema_name: str):
        views = {}
        order_num = 1
        query = f"""
            SELECT
                oid,
                relname as viewname,
                obj_description(oid, 'pg_class') as view_comment,
                relkind
            FROM pg_class
            WHERE relkind IN ('v', 'm')
            AND relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = '{source_schema_name}')
            AND relname NOT LIKE 'pg_%'
            ORDER BY viewname
        """
        try:
            self.connect()
            cursor = self.connection.cursor()
            cursor.execute(query)
            for row in cursor.fetchall():
                view_type = 'MATERIALIZED VIEW' if row[3] == 'm' else 'VIEW'
                views[order_num] = {
                    'id': row[0],
                    'schema_name': source_schema_name,
                    'view_name': row[1],
                    'comment': row[2],
                    'view_type': view_type
                }
                order_num += 1
            cursor.close()
            self.disconnect()
            return views
        except Exception as e:
            self.config_parser.print_log_message('ERROR', f"postgresql_connector: fetch_views_names: Error executing query: {query}")
            self.config_parser.print_log_message('ERROR', e)
            raise

    def migrate_sequences(self, target_connector, settings):
        source_schema_name = settings['source_schema_name']
        target_schema_name = settings['target_schema_name']
        migrator_tables = settings.get('migrator_tables')

        self.config_parser.print_log_message('INFO', f"postgresql_connector: migrate_sequences: Migrating sequences from {source_schema_name} to {target_schema_name}...")

        query = f"""
            SELECT c.relname, c.oid
            FROM pg_class c
            JOIN pg_namespace n ON c.relnamespace = n.oid
            WHERE n.nspname = '{source_schema_name}'
              AND c.relkind = 'S'
        """

        try:
            self.connect()
            target_connector.connect() # Ensure target is connected
            cursor = self.connection.cursor()
            cursor.execute(query)
            sequences = cursor.fetchall() # list of (name, oid) matches
            cursor.close()

            for seq_row in sequences:
                seq_name = seq_row[0]
                seq_oid = seq_row[1]

                self.config_parser.print_log_message('DEBUG', f"postgresql_connector: migrate_sequences: Processing sequence: {seq_name}")

                # Insert into protocol table if migrator_tables is available
                # We don't have table/column info here as we are migrating all sequences in schema
                if migrator_tables:
                    try:
                        # set_sequence_sql will be populated later, but we need to insert first to get ID/track start?
                        # insert_sequence(self, sequence_id, schema_name, table_name, column_name, sequence_name, set_sequence_sql)
                        # We'll update it later or insert it now with placeholders?
                        # Usually insert happens before work starts to track 'started', but insert_sequence seems to just log existence?
                        # Looking at other methods, insert_* usually logs the item and then update_* sets status.
                        migrator_tables.insert_sequence({
                            'sequence_id': seq_oid,
                            'source_schema_name': source_schema_name,
                            'source_table_name': '',
                            'source_column_name': '',
                            'source_sequence_name': seq_name,
                            'source_sequence_sql': ''
                        })
                    except Exception as e:
                        self.config_parser.print_log_message('ERROR', f"postgresql_connector: migrate_sequences: Failed to insert sequence {seq_name} into protocol: {e}")
                details = self.get_sequence_details(source_schema_name, seq_name)

                # Fetch current value separately as it's not in pg_sequence catalog
                curr_val_query = f"SELECT last_value, is_called FROM {source_schema_name}.{seq_name}"
                cursor = self.connection.cursor()
                cursor.execute(curr_val_query)
                curr_val_row = cursor.fetchone()
                last_value = curr_val_row[0]
                is_called = curr_val_row[1]
                cursor.close()

                # Generate CREATE SEQUENCE
                # Details: min_value, max_value, increment_by, cycle, cache_size, start_value
                # We use START WITH = last_value to ensure it picks up where it left off,
                # OR we use START WITH = min_value and then setval?
                # Postgres dump usually does CREATE SEQUENCE ...; SELECT setval(...);

                # If we use setval, CREATE SEQUENCE can just use defaults or original properties.
                # However, if we want `START WITH` to be correct for a fresh init, we might want original start_value (which we have in details['start_value'])
                # But `last_value` is the critical runtime state.

                cycle_str = "CYCLE" if details['cycle'] else "NO CYCLE"
                create_sql = f"""CREATE SEQUENCE IF NOT EXISTS "{target_schema_name}"."{seq_name}"
                    INCREMENT BY {details['increment_by']}
                    MINVALUE {details['min_value']}
                    MAXVALUE {details['max_value']}
                    START WITH {details['start_value']}
                    CACHE {details['cache_size']}
                    {cycle_str};
                """

                setval_sql = f"SELECT setval('\"{target_schema_name}\".\"{seq_name}\"', {last_value}, {'true' if is_called else 'false'});"

                self.config_parser.print_log_message('DEBUG', f"postgresql_connector: migrate_sequences: Sequence {seq_name} SQL: {create_sql}")
                self.config_parser.print_log_message('DEBUG', f"postgresql_connector: migrate_sequences: Sequence {seq_name} SETVAL: {setval_sql}")

                try:
                    target_connector.execute_query(create_sql)
                    target_connector.execute_query(setval_sql)

                    if migrator_tables:
                        # Update protocol with success and the setval SQL used
                        # Note: update_sequence_status doesn't update SQL. insert check above put empty SQL.
                        # Ideally we should have inserted SQL there. But we didn't have it yet.
                        # Maybe we should DELETE and re-INSERT or just update status?
                        # insert_sequence puts it in 'sequences' table.
                        # update_sequence_status updates 'sequences' table.
                        # If I want to save the SQL, I might need to update it.
                        # But migrator_tables implementation of update_sequence_status only updates success/message/time.
                        # So I should probably insert with the SQL if I can generate it before?
                        # No, I generate it later.
                        # I'll just leave SQL empty or put "See logs" if I can't update it.
                        # Or I accept that the protocol table won't show the SQL.
                        migrator_tables.update_sequence_status({'sequence_id': seq_oid, 'success': True, 'message': 'migrated OK'})

                except Exception as ex:
                    self.config_parser.print_log_message('ERROR', f"postgresql_connector: migrate_sequences: Failed to migrate sequence {seq_name}: {ex}")
                    if migrator_tables:
                        migrator_tables.update_sequence_status({'sequence_id': seq_oid, 'success': False, 'message': str(ex)})

            self.disconnect()
            target_connector.disconnect()
            return True
        except Exception as e:
            self.config_parser.print_log_message('ERROR', f"postgresql_connector: migrate_sequences: Error migrating sequences: {e}")
            self.disconnect()
            # Try to disconnect target if possible, though it might be closed/failed
            try:
                target_connector.disconnect()
            except:
                pass
            raise

    def fetch_view_code(self, settings):
        view_id = settings['view_id']
        query = f"SELECT pg_get_viewdef({view_id}, true)"
        try:
            self.connect()
            cursor = self.connection.cursor()
            cursor.execute(query)
            view_code = cursor.fetchone()[0]
            cursor.close()
            self.disconnect()
            return view_code
        except Exception as e:
            self.config_parser.print_log_message('ERROR', f"postgresql_connector: fetch_view_code: Error executing query: {query}")
            self.config_parser.print_log_message('ERROR', e)
            raise

    def convert_view_code(self, settings: dict):
        view_code = settings['view_code']
        view_name = settings['target_view_name']
        target_schema_name = settings['target_schema_name']
        view_type = settings.get('view_type', 'VIEW')

        ddl = f'CREATE {view_type} "{target_schema_name}"."{view_name}" AS {view_code}'
        if not ddl.strip().endswith(';'):
             ddl += ';'

        return ddl

    def fetch_user_defined_types(self, schema: str):
        user_defined_types = {}
        order_num = 1
        self.connect()
        cursor = self.connection.cursor()

        try:
            # 1. ENUMS
            query_enum = f"""
                SELECT t.typnamespace::regnamespace::text as schemaname, typname as type_name,
                    'CREATE TYPE "'||t.typnamespace::regnamespace||'"."'||typname||'" AS ENUM ('||string_agg(''''||e.enumlabel||'''', ',' ORDER BY e.enumsortorder)::text||');' AS elements,
                    obj_description(t.oid, 'pg_type') as type_comment
                FROM pg_type AS t
                JOIN pg_enum AS e ON e.enumtypid = t.oid
                JOIN pg_namespace n ON t.typnamespace = n.oid
                WHERE n.nspname = '{schema}'
                AND t.typtype = 'e'
                GROUP BY t.oid, t.typnamespace, t.typname
                ORDER BY t.typname;
            """
            cursor.execute(query_enum)
            for row in cursor.fetchall():
                user_defined_types[order_num] = {
                    'schema_name': row[0],
                    'type_name': row[1],
                    'sql': row[2],
                    'comment': row[3]
                }
                order_num += 1

            # 2. Composite Types
            query_composite = f"""
                SELECT
                    n.nspname,
                    t.typname,
                    pg_catalog.obj_description(t.oid, 'pg_type'),
                    (
                        SELECT string_agg('"'||a.attname||'" '||pg_catalog.format_type(a.atttypid, a.atttypmod), ', ' ORDER BY a.attnum)
                        FROM pg_attribute a
                        WHERE a.attrelid = t.typrelid AND a.attnum > 0 AND NOT a.attisdropped
                    ) as attributes
                FROM pg_type t
                JOIN pg_namespace n ON t.typnamespace = n.oid
                JOIN pg_class c ON t.typrelid = c.oid
                WHERE t.typtype = 'c'
                  AND c.relkind = 'c'
                  AND n.nspname = '{schema}'
                ORDER BY t.typname
            """
            cursor.execute(query_composite)
            for row in cursor.fetchall():
                schema_name = row[0]
                type_name = row[1]
                comment = row[2]
                attributes = row[3]
                sql = f'CREATE TYPE "{schema_name}"."{type_name}" AS ({attributes});'

                user_defined_types[order_num] = {
                    'schema_name': schema_name,
                    'type_name': type_name,
                    'sql': sql,
                    'comment': comment
                }
                order_num += 1

            # 3. Range Types
            query_range = f"""
                SELECT
                    n.nspname,
                    t.typname,
                    pg_catalog.obj_description(t.oid, 'pg_type'),
                    r.rngsubtype::regtype::text,
                    (SELECT c.collname FROM pg_collation c WHERE c.oid = r.rngcollation AND c.collname != 'default') as collation,
                    r.rngcanonical::regproc::text,
                    r.rngsubdiff::regproc::text
                FROM pg_type t
                JOIN pg_range r ON r.rngtypid = t.oid
                JOIN pg_namespace n ON t.typnamespace = n.oid
                WHERE n.nspname = '{schema}'
                ORDER BY t.typname
            """
            cursor.execute(query_range)
            for row in cursor.fetchall():
                schema_name = row[0]
                type_name = row[1]
                comment = row[2]
                subtype = row[3]
                collation = row[4]
                canonical = row[5]
                subdiff = row[6]

                parts = [f"SUBTYPE = {subtype}"]
                if collation:
                    parts.append(f"COLLATION = {collation}")
                if canonical and canonical != '-':
                    parts.append(f"CANONICAL = {canonical}")
                if subdiff and subdiff != '-':
                    parts.append(f"SUBDIFF = {subdiff}")

                sql = f'CREATE TYPE "{schema_name}"."{type_name}" AS RANGE ({", ".join(parts)});'

                user_defined_types[order_num] = {
                    'schema_name': schema_name,
                    'type_name': type_name,
                    'sql': sql,
                    'comment': comment
                }
                order_num += 1

            cursor.close()
            self.disconnect()
            return user_defined_types
        except Exception as e:
            self.config_parser.print_log_message('ERROR', f"postgresql_connector: fetch_user_defined_types: Error fetching UDTs: {e}")
            raise

    def prepare_session_settings(self):
        """
        Prepare session settings for the database connection.
        """
        filtered_settings = ""
        try:
            settings = self.config_parser.get_target_db_session_settings()
            if not settings:
                self.config_parser.print_log_message('INFO', "postgresql_connector: prepare_session_settings: No session settings found in config file.")
                return filtered_settings
            # self.config_parser.print_log_message('INFO', f"postgresql_connector: prepare_session_settings: Preparing session settings: {settings} / {settings.keys()} / {tuple(settings.keys())}")
            self.connect()
            cursor = self.connection.cursor()
            lower_keys = tuple(k.lower() for k in settings.keys())
            cursor.execute("SELECT name FROM (SELECT name FROM pg_settings UNION ALL SELECT name FROM (VALUES('role')) as t(name) ) a WHERE lower(a.name) IN %s", (lower_keys,))
            matching_settings = cursor.fetchall()
            cursor.close()
            self.disconnect()
            if not matching_settings:
                self.config_parser.print_log_message('INFO', "postgresql_connector: prepare_session_settings: No settings found to prepare.")
                return filtered_settings

            for setting in matching_settings:
                setting_name = setting[0]
                if setting_name in ['search_path']:
                    filtered_settings += f"SET {setting_name} = {settings[setting_name]};"
                else:
                    filtered_settings += f"SET {setting_name} = '{settings[setting_name]}';"
            self.config_parser.print_log_message('INFO', f"postgresql_connector: prepare_session_settings: Session settings: {filtered_settings}")
            return filtered_settings
        except Exception as e:
            self.config_parser.print_log_message('ERROR', f"postgresql_connector: prepare_session_settings: Error preparing session settings: {e}")
            raise

    def fetch_domains(self, schema: str):
        domains = {}
        order_num = 1
        # Fetch domains (Base type, Default, Not Null, Constraints)
        query = f"""
            SELECT
                n.nspname as domain_schema,
                t.typname as domain_name,
                pg_catalog.format_type(t.typbasetype, t.typtypmod) as domain_data_type,
                t.typnotnull,
                t.typdefault,
                pg_catalog.obj_description(t.oid, 'pg_type') as comment,
                (SELECT string_agg(pg_get_constraintdef(r.oid), ' ') FROM pg_constraint r WHERE r.contypid = t.oid) as constraints
            FROM pg_type t
            JOIN pg_namespace n ON t.typnamespace = n.oid
            LEFT JOIN pg_class c ON c.oid = t.typrelid
            WHERE t.typtype = 'd'
              AND n.nspname = '{schema}'
            ORDER BY t.typname
        """
        try:
            self.connect()
            cursor = self.connection.cursor()
            cursor.execute(query)
            for row in cursor.fetchall():
                domains[order_num] = {
                    'domain_schema': row[0],
                    'domain_name': row[1],
                    'domain_data_type': row[2],
                    'domain_not_null': row[3],
                    'domain_default': row[4],
                    'domain_comment': row[5],
                    'source_domain_check_sql': row[6], # Can be None
                    'source_domain_sql': f"CREATE DOMAIN \"{row[0]}\".\"{row[1]}\" AS {row[2]}", # Simplified for now, real construction happen in get_create_domain_sql
                }
                order_num += 1
            cursor.close()
            self.disconnect()
            return domains
        except Exception as e:
            self.config_parser.print_log_message('ERROR', f"postgresql_connector: fetch_domains: Error executing query: {query}")
            self.config_parser.print_log_message('ERROR', e)
            raise

    def get_create_domain_sql(self, settings):
        create_domain_sql = ""
        domain_name = settings['domain_name']
        target_schema_name = settings['target_schema_name']
        domain_check_sql = settings.get('source_domain_check_sql')
        domain_data_type = settings['domain_data_type']
        domain_default = settings.get('domain_default')
        domain_not_null = settings.get('domain_not_null')

        migrated_as = settings.get('migrated_as', 'CHECK CONSTRAINT')

        if migrated_as == 'CHECK CONSTRAINT':
             # Fallback logic if needed, but primarily used for sybase patterns
             if domain_check_sql:
                create_domain_sql = f"""CHECK({domain_check_sql})"""
             else:
                create_domain_sql = ""
        else:
            # Construct standard CREATE DOMAIN
            sql_parts = [f'CREATE DOMAIN "{target_schema_name}"."{domain_name}" AS {domain_data_type}']

            if domain_default is not None:
                sql_parts.append(f"DEFAULT {domain_default}")

            if domain_not_null:
                sql_parts.append("NOT NULL")

            if domain_check_sql:
                # pg_get_constraintdef already allows CHECK (...).
                # If multiple constraints were aggregated, they might look like CHECK (...) CHECK (...)
                # We just append them.
                sql_parts.append(domain_check_sql)

            create_domain_sql = " ".join(sql_parts) + ";"

        return create_domain_sql

    def fetch_default_values(self, settings) -> dict:
        # Placeholder for fetching default values
        return {}

    def testing_select(self):
        return "SELECT 1"

    def get_database_version(self):
        query = "SELECT version()"
        self.connect()
        cursor = self.connection.cursor()
        cursor.execute(query)
        version = cursor.fetchone()[0]
        cursor.close()
        self.disconnect()
        return version

    def get_database_size(self):
        query = "SELECT pg_database_size(current_database())"
        self.connect()
        cursor = self.connection.cursor()
        cursor.execute(query)
        size = cursor.fetchone()[0]
        cursor.close()
        self.disconnect()
        return size

    def get_top_n_tables(self, settings):
        top_tables = {}
        top_tables['by_rows'] = {}
        top_tables['by_size'] = {}
        top_tables['by_columns'] = {}
        top_tables['by_indexes'] = {}
        top_tables['by_constraints'] = {}
        # return top_tables

        source_schema_name = settings.get('source_schema_name', 'public')
        try:
            order_num = 1
            top_n = self.config_parser.get_top_n_tables_by_rows()
            if top_n > 0:
                query = f"""
                    SELECT
                    n.nspname AS owner,
                    c.relname AS table_name,
                    c.reltuples::bigint AS row_count,
                    pg_total_relation_size(c.oid) AS row_size
                    FROM
                    pg_class c
                    JOIN
                    pg_namespace n ON n.oid = c.relnamespace
                    WHERE
                    c.relkind = 'r' AND n.nspname = '{source_schema_name}'
                    ORDER BY
                    c.reltuples DESC
                    LIMIT {top_n};
                """
                self.connect()
                cursor = self.connection.cursor()
                cursor.execute(query)
                results = cursor.fetchall()
                cursor.close()
                self.disconnect()

                order_num = 1
                for row in results:
                    top_tables['by_rows'][order_num] = {
                    'owner': row[0].strip(),
                    'table_name': row[1].strip(),
                    'row_count': row[2],
                    'table_size': row[3],
                    }
                    order_num += 1

                self.config_parser.print_log_message('DEBUG2', f"postgresql_connector: get_top_n_tables: Top {top_n} tables by rows: {top_tables['by_rows']}")
            else:
                self.config_parser.print_log_message('DEBUG', "postgresql_connector: get_top_n_tables: Top N tables by rows is not configured or set to 0, skipping this part.")

        except Exception as e:
            self.config_parser.print_log_message('ERROR', f"postgresql_connector: get_top_n_tables: Error fetching top tables by rows: {e}")

        return top_tables
    
    def get_table_checksum(self, schema_name: str, table_name: str, columns: list):
        if not columns:
            return None
        # Use hashtext of the concatenated text representations
        cols_concat = " || ".join([f"COALESCE(\"{col['column_name']}\"::text, '')" for col in columns])
        query = f'SELECT sum(hashtext({cols_concat})) FROM "{schema_name}"."{table_name}"'
        try:
            self.connect()
            cursor = self.connection.cursor()
            cursor.execute(query)
            result = cursor.fetchone()[0]
            cursor.close()
            self.disconnect()
            return result
        except Exception as e:
            self.config_parser.print_log_message('ERROR', f"postgresql_connector: get_table_checksum: Error executing query: {query}")
            self.config_parser.print_log_message('ERROR', e)
            return None

    def get_random_pks(self, schema_name: str, table_name: str, pk_columns: list, sample_size: int):
        if not pk_columns:
            return []
        cols = ", ".join([f'"{c}"' for c in pk_columns])
        query = f'SELECT {cols} FROM "{schema_name}"."{table_name}" ORDER BY random() LIMIT {sample_size}'
        pks = []
        try:
            self.connect()
            cursor = self.connection.cursor()
            cursor.execute(query)
            for row in cursor.fetchall():
                pks.append(dict(zip(pk_columns, row)))
            cursor.close()
            self.disconnect()
            return pks
        except Exception as e:
            self.config_parser.print_log_message('ERROR', f"postgresql_connector: get_random_pks: Error executing query: {query}")
            self.config_parser.print_log_message('ERROR', e)
            return []

    def get_row_checksums(self, schema_name: str, table_name: str, pk_columns: list, pk_values_list: list, columns: list):
        if not columns or not pk_columns or not pk_values_list:
            return {}
        cols_concat = " || ".join([f"COALESCE(\"{col['column_name']}\"::text, '')" for col in columns])
        pk_cols_str = ", ".join([f'"{c}"' for c in pk_columns])
        
        # Build IN clause properly
        in_values = []
        for pk_dict in pk_values_list:
            vals = []
            for c in pk_columns:
                val = pk_dict[c]
                if val is None:
                    vals.append("NULL")
                elif isinstance(val, str):
                    escaped_val = val.replace("'", "''")
                    vals.append(f"'{escaped_val}'")
                else:
                    vals.append(str(val))
            in_values.append(f"({', '.join(vals)})")
        
        where_clause = f"({pk_cols_str}) IN ({', '.join(in_values)})"
        if len(pk_columns) == 1:
            where_clause = f"{pk_cols_str} IN ({', '.join([v.strip('()') for v in in_values])})"
            
        query = f'SELECT {pk_cols_str}, hashtext({cols_concat}) FROM "{schema_name}"."{table_name}" WHERE {where_clause}'
        
        checksums = {}
        try:
            self.connect()
            cursor = self.connection.cursor()
            cursor.execute(query)
            for row in cursor.fetchall():
                pk_tuple = tuple(row[:len(pk_columns)])
                pk_key = pk_tuple[0] if len(pk_tuple) == 1 else pk_tuple
                checksums[pk_key] = row[-1]
            cursor.close()
            self.disconnect()
            return checksums
        except Exception as e:
            self.config_parser.print_log_message('ERROR', f"postgresql_connector: get_row_checksums: Error executing query: {query}")
            self.config_parser.print_log_message('ERROR', e)
            return {}

    def get_lob_sizes(self, schema_name: str, table_name: str, pk_columns: list, pk_values_list: list, lob_columns: list):
        if not lob_columns or not pk_columns or not pk_values_list:
            return {}
            
        size_cols = []
        for col in lob_columns:
            if 'bytea' in col['data_type'].lower():
                size_cols.append(f"octet_length(\"{col['column_name']}\")")
            else:
                size_cols.append(f"length(\"{col['column_name']}\")")
                
        size_selects = ", ".join(size_cols)
        pk_cols_str = ", ".join([f'"{c}"' for c in pk_columns])
        
        in_values = []
        for pk_dict in pk_values_list:
            vals = []
            for c in pk_columns:
                val = pk_dict[c]
                if val is None:
                    vals.append("NULL")
                elif isinstance(val, str):
                    escaped_val = val.replace("'", "''")
                    vals.append(f"'{escaped_val}'")
                else:
                    vals.append(str(val))
            in_values.append(f"({', '.join(vals)})")
        
        where_clause = f"({pk_cols_str}) IN ({', '.join(in_values)})"
        if len(pk_columns) == 1:
            where_clause = f"{pk_cols_str} IN ({', '.join([v.strip('()') for v in in_values])})"
            
        query = f'SELECT {pk_cols_str}, {size_selects} FROM "{schema_name}"."{table_name}" WHERE {where_clause}'
        
        sizes = {}
        try:
            cursor = self.connection.cursor()
            cursor.execute(query)
            for row in cursor.fetchall():
                pk_tuple = tuple(row[:len(pk_columns)])
                pk_key = pk_tuple[0] if len(pk_tuple) == 1 else pk_tuple
                sizes[pk_key] = row[len(pk_columns):]
            cursor.close()
            return sizes
        except Exception as e:
            self.config_parser.print_log_message('ERROR', f"postgresql_connector: get_lob_sizes: Error executing query: {query}")
            self.config_parser.print_log_message('ERROR', e)
            return {}

    def get_top_fk_dependencies(self, settings):
        top_fk_dependencies = {}
        return top_fk_dependencies

    def target_table_exists(self, target_schema_name, target_table_name):
        query = f"""
            SELECT EXISTS (
                SELECT 1
                FROM information_schema.tables
                WHERE lower(table_schema) = lower('{target_schema_name}')
                AND lower(table_name) = lower('{target_table_name}')
            )
        """
        cursor = self.connection.cursor()
        cursor.execute(query)
        exists = cursor.fetchone()[0]
        cursor.close()
        return exists

    def fetch_all_rows(self, query):
        cursor = self.connection.cursor()
        cursor.execute(query)
        rows = cursor.fetchall()
        cursor.close()
        return rows

    def convert_default_value(self, settings) -> dict:
        extracted_default_value = settings['extracted_default_value']
        return extracted_default_value

if __name__ == "__main__":
    print("This script is not meant to be run directly")
