def setup_fts():
    cursor = connection.cursor()

    # Create the FTS table with date as timestamptz
    cursor.execute('''
    CREATE TABLE IF NOT EXISTS messages_fts (
        id SERIAL PRIMARY KEY,
        id1 BIGINT,
        id2 BIGINT,
        board TEXT,
        message TEXT,
        subject TEXT,
        files TEXT,
        date TIMESTAMPTZ,
        tsv tsvector
    );
    ''')
    connection.commit()

    # Clear existing data from the FTS table
    cursor.execute('DELETE FROM messages_fts;')
    connection.commit()

    # Create trigger function for updating tsvector
    cursor.execute('''
    CREATE OR REPLACE FUNCTION messages_fts_trigger() RETURNS trigger AS $$
    BEGIN
      NEW.tsv :=
         to_tsvector('english', coalesce(NEW.message, '') || ' ' || coalesce(NEW.subject, ''));
      RETURN NEW;
    END
    $$ LANGUAGE plpgsql;
    ''')
    connection.commit()

    # Create trigger on messages_fts table
    cursor.execute('''
    DROP TRIGGER IF EXISTS tsvectorupdate ON messages_fts;
    CREATE TRIGGER tsvectorupdate
    BEFORE INSERT OR UPDATE ON messages_fts
    FOR EACH ROW EXECUTE PROCEDURE messages_fts_trigger();
    ''')
    connection.commit()

    # Create GIN index on tsvector column
    cursor.execute('''
    CREATE INDEX IF NOT EXISTS idx_messages_fts_tsv ON messages_fts USING GIN(tsv);
    ''')
    connection.commit()

    # Fetch all board tables excluding 'fadenwache' and 'messages_fts'
    cursor.execute("""
    SELECT table_name
    FROM information_schema.tables
    WHERE table_schema = 'public' 
      AND table_type = 'BASE TABLE' 
      AND table_name NOT LIKE 'messages_fts%'
      AND table_name NOT ILIKE 'fadenwache';
    """)
    rows = cursor.fetchall()

    boards = [row[0] for row in rows]

    # Populate the FTS table
    for board in boards:
        # Insert top-level messages and subjects only if 'voll' starts with '{' and is valid JSON
        cursor.execute(f'''
        INSERT INTO messages_fts (id1, board, message, subject, files, date)
        SELECT 
            {board}.id AS id1,
            %s AS board,
            {board}.voll::jsonb->>'message' AS message,
            {board}.voll::jsonb->>'subject' AS subject,
            {board}.voll::jsonb->>'files' AS files,
            to_timestamp(split_part({board}.zeit, 'Z', 1), 'YYYY-MM-DD"T"HH24:MI:SS.MS') AS date
        FROM {board}
        WHERE {board}.voll ~ '^\\s*\\{{'    -- Ensure 'voll' starts with '{{'
          AND {board}.voll::jsonb IS NOT NULL
          AND ({board}.voll::jsonb->>'message' IS NOT NULL 
               OR {board}.voll::jsonb->>'subject' IS NOT NULL);
        ''', (board,))
        
        # Insert messages and subjects from nested posts only if 'voll' starts with '{' and is valid JSON
        cursor.execute(f'''
        INSERT INTO messages_fts (id1, id2, board, message, subject, files, date)
        SELECT 
            {board}.id AS id1,
            (p.value::jsonb->>'postId')::BIGINT AS id2,
            %s AS board,
            p.value::jsonb->>'message' AS message,
            p.value::jsonb->>'subject' AS subject,
            p.value::jsonb->>'files' AS files,
            to_timestamp(split_part({board}.zeit, 'Z', 1), 'YYYY-MM-DD"T"HH24:MI:SS.MS') AS date
        FROM {board}
        CROSS JOIN LATERAL jsonb_each({board}.voll::jsonb->'posts') AS p(key, value)
        WHERE {board}.voll ~ '^\\s*\\{{'   -- Ensure 'voll' starts with '{{'
          AND {board}.voll::jsonb IS NOT NULL
          AND (p.value::jsonb->>'message' IS NOT NULL 
               OR p.value::jsonb->>'subject' IS NOT NULL);
        ''', (board,))
    
    connection.commit()

    # Set up triggers for each board
    for board in boards:
        # Drop existing triggers if they exist
        cursor.execute(f'''
        DROP TRIGGER IF EXISTS {board}_insert_trigger ON {board};
        DROP TRIGGER IF EXISTS {board}_update_trigger ON {board};
        DROP TRIGGER IF EXISTS {board}_delete_trigger ON {board};
        ''')
        connection.commit()

        # Create INSERT trigger
        cursor.execute(f'''
        CREATE OR REPLACE FUNCTION {board}_insert_trigger_fn() RETURNS trigger AS %%%LATEX_BLOCK_0%%% LANGUAGE plpgsql;

        CREATE TRIGGER {board}_insert_trigger
        AFTER INSERT ON {board}
        FOR EACH ROW EXECUTE PROCEDURE {board}_insert_trigger_fn();
        ''', (board,))
        connection.commit()

        # Create UPDATE trigger
        cursor.execute(f'''
        CREATE OR REPLACE FUNCTION {board}_update_trigger_fn() RETURNS trigger AS %%%LATEX_BLOCK_1%%% LANGUAGE plpgsql;

        CREATE TRIGGER {board}_update_trigger
        AFTER UPDATE ON {board}
        FOR EACH ROW EXECUTE PROCEDURE {board}_update_trigger_fn();
        ''', (board,))
        connection.commit()

        # Create DELETE trigger
        cursor.execute(f'''
        CREATE OR REPLACE FUNCTION {board}_delete_trigger_fn() RETURNS trigger AS %%%LATEX_BLOCK_2%%% LANGUAGE plpgsql;

        CREATE TRIGGER {board}_delete_trigger
        AFTER DELETE ON {board}
        FOR EACH ROW EXECUTE PROCEDURE {board}_delete_trigger_fn();
        ''', (board,))
        connection.commit()

    cursor.close()
    print('completed setup_fts')


line 125, in setup_fts
    cursor.execute(f'''
psycopg2.errors.InvalidParameterValue: cannot call jsonb_each on a non-object

Please consider that 'voll' (type text) sometimes only contains an excerpt (like "Mag...", "Sample message..."), instead of JSON data.