На SQL можно написать порой ужасные вещи, например:

SELECT id, jsonb_path_query_array(value, '$[*].entityType')
FROM demo WHERE value @? '$[*] ? (@.entityId == 1 &&
    (!exists (@.childrenIds) || exists (@.childrenIds ? (@[*] == 4))))';

Выглядит как совсем другой язык, потому что идет обращение к jsonb. Одно дело хранить неструктурированные данные в jsonb, но гонять по ним запросы — это уже звоночек. Для какой-нибудь отладки это еще куда ни шло, хотя там быстрее будет сделать большую часть операций вручную, чем написать полностью корректный запрос. Но в основном коде это делать — это признак того, что кто-то не смог нормально смоделировать бизнес сущности или использует данные по какому-то извращенному сценарию.

А в запросе выбираются элементы массива, в которых есть объект с entityId и либо пустым childrenIds, либо содержащим 4. В ответе выдаются только значения полей entityType.

Полный пример
CREATE TABLE demo(
  id int not NULL,
  value jsonb Not NULL
);
INSERT into demo VALUES
  (1, '[{"entityId": 1, "entityType": "type1", "childrenIds": []}, {"entityId": 10, "entityType": "type5", "childrenIds": [30, 20]}]'::jsonb),
  (2, '[{"entityId": 1, "entityType": "type2"}, {"entityId": 15, "entityType": "type1", "childrenIds": [4, 20]}]'::jsonb),
  (3, '[{"entityId": 1, "entityType": "type3", "childrenIds": [5, 4]}, {"entityId": 25, "entityType": "type4", "childrenIds": [30, 4]}]'::jsonb),
  (4, '[{"entityId": 35, "entityType": "type4", "childrenIds": [35, 4]}]'::jsonb);

SELECT id, jsonb_path_query_array(value, '$[*].entityType')
FROM demo WHERE VALUE @? '$[*] ? (@.entityId == 1 && (!exists (@.childrenIds) || exists (@.childrenIds ? (@[*] == 4))))';

Поиграться можно тут. В ответе будет 2 и 3 запись.