repro_postgres_ltree_report.txt

text/plain

Filename: repro_postgres_ltree_report.txt
Type: text/plain
Part: 0
Message: ltree gist index "incomplete split"
from uuid import UUID
import psycopg2


def create_ltree(cursor):
    # Activate the ltree extension
    cursor.execute("CREATE EXTENSION IF NOT EXISTS ltree;")
    # Create table with ltree field
    cursor.execute("""
            CREATE TABLE IF NOT EXISTS ltree_corruption (
            id SERIAL PRIMARY KEY,
            directory ltree
            );
        """)

    # Create index on the ltree field
    cursor.execute(
        """CREATE INDEX IF NOT EXISTS idx_directory ON ltree_corruption USING GIST (directory);"""
    )

    cursor.execute("TRUNCATE TABLE ltree_corruption;")


def insert_directory(cursor, directory: list[UUID]):
    _directory = ".".join(str(d) for d in directory).replace("-", "_")
    cursor.execute(
        f"""
        INSERT INTO ltree_corruption (directory)
        VALUES ('{_directory}');
        """
    )


def insert_corrupt_ltree(cursor):
    # These UUID's are somewhat arbitrary. The error does not repro with all UUIDs but it does with these.
    dir1 = UUID(int=4000)
    dir2 = UUID(int=50001)
    dir3 = UUID(int=6000002)

    # # Insert an intial path. Without this the incomplete split does not happen.
    insert_directory(cursor, [dir1, dir2])

    bad_directory = (
        [dir1] + list(map(lambda x: UUID(int=3000000 + x), range(50))) + [dir3]
    )
    try:
        # Insert until the index becomes corrupted. The exact number of insertions required doesn't seem to be
        # deterministic but is always less than 50.
        for i in range(0, 50):
            insert_directory(cursor, bad_directory + [UUID(int=10000000 + i)])
    except psycopg2.Error:
        # traceback.print_exc()
        pass

    try:
        # Run again until corruption happens again. The logs will show:
        # `fixing incomplete split in index "idx_directory"`
        for i in range(0, 50):
            insert_directory(cursor, bad_directory + [UUID(int=10000000 + 100 + i)])
    except psycopg2.Error:
        # traceback.print_exc()
        cursor.execute("""
            SELECT line
            FROM regexp_split_to_table(
            pg_read_file(
                (SELECT setting FROM pg_settings WHERE name = 'data_directory') || '/' ||
                (SELECT setting FROM pg_settings WHERE name = 'log_directory') || '/' ||
                (SELECT name FROM pg_ls_logdir() ORDER BY modification DESC LIMIT 1),
                0,
                10000000 -- adjust bytes if needed
            ),
            E'\n'
            ) AS line
            WHERE line LIKE '%fixing incomplete split in index%';
        """)
        print(cursor.fetchone())


def exec():
    # Define connection parameters
    connection = psycopg2.connect(
        dbname="postgres",
        user="user",
        password="password",
        host="localhost",
        port="5432",
    )
    connection.autocommit = True

    # Create a cursor object
    cursor = connection.cursor()

    cursor.execute("ALTER SYSTEM SET log_min_messages = 'INFO';")
    create_ltree(cursor)
    insert_corrupt_ltree(cursor)

    # Close the cursor and connection
    cursor.close()
    connection.close()


if __name__ == "__main__":
    exec()