Давайте создадим таблицу со столбцом jsonb, которая представляет древовидную структуру узлов. Создать простой список всех узлов дерева, по одному на строку, легко в случае простой древовидной структуры с использованием удивительных рекурсивных CTE:
CREATE TEMPORARY TABLE api_schema (id INT, content JSONB);
INSERT INTO api_schema VALUES (1, '[{"name": "A", "category": "tuple", "children": [{"name": "B", "category": "datapoint"}, {"name": "C", "category": "datapoint"}]}]');
INSERT INTO api_schema VALUES (2, '[{"name": "D", "category": "tuple", "children": [{"name": "E", "category": "tuple", "children": [{"name": "F", "category": "datapoint"}]}]}]');
WITH RECURSIVE schema_objects (id, object) AS (
SELECT id, jsonb_array_elements(content) FROM api_schema
UNION
SELECT id, jsonb_array_elements(object->'children') FROM schema_objects
WHERE object->>'category' != 'datapoint'
) SELECT * FROM schema_objects;
Самое сложное - это когда требуется больше логики в формуле рекурсии. В моем случае, кроме категорий datapoint
(без детей) и tuple
(потомки - это список), существует категория multivalue
(потомки - это один узел). Как заставить CTE обращаться с этим делом?
Наивное переписывание CTE таково:
INSERT INTO api_schema VALUES (3, '[{"name": "D", "category": "multivalue", "children": {"name": "E", "category": "tuple", "children": [{"name": "F", "category": "datapoint"}]}}]');
WITH RECURSIVE schema_objects (id, object) AS (
SELECT id, jsonb_array_elements(content) FROM api_schema
UNION
SELECT id, CASE WHEN jsonb_typeof(object->'children') = 'array'
THEN jsonb_array_elements(object->'children')
ELSE object->'children'
END AS object
FROM schema_objects
WHERE object->>'category' != 'datapoint'
) SELECT * FROM schema_objects;
Однако проблема в том, что это не работает в Postgres 10:
ERROR: set-returning functions are not allowed in CASE
Можем ли мы сделать два SELECT, каждый из которых охватывает отдельную категорию? Это не разрешено:
WITH RECURSIVE schema_objects (id, object) AS (
SELECT id, jsonb_array_elements(content) FROM api_schema
UNION
(SELECT id, jsonb_array_elements(object->'children') FROM schema_objects WHERE object->>'category' = 'tuple'
UNION
SELECT id, object->'children' FROM schema_objects WHERE object->>'category' = 'multivalue')
) SELECT * FROM schema_objects WHERE id=1;
ERROR: recursive reference to query "schema_objects" must not appear more than once
Идея, распространяющаяся по Интернету, заключается в использовании CTE для выделения CASE, но мы уже внутри CTE, так что это даже не компилируется:
WITH RECURSIVE schema_objects (id, object) AS (
SELECT id, jsonb_array_elements(content) FROM api_schema
UNION
WITH schema_children (id, children) AS (
SELECT CASE jsonb_typeof(object->'children') WHEN 'array' THEN object->'children' ELSE jsonb_build_array(object->'children') END AS children
FROM schema_objects
WHERE object->>'category' != 'datapoint'
)
SELECT id, jsonb_array_elements(children)
FROM schema_children
) SELECT * FROM schema_objects WHERE id=1;
Postgres также предлагает использовать боковое FROM, но не ясно, как составить его в ситуации с одной «таблицей».