SET datestyle = 'ISO';
CREATE SERVER binary_json_loopback FOREIGN DATA WRAPPER clickhouse_fdw OPTIONS(dbname 'json_test', driver 'binary');
CREATE SERVER http_json_loopback FOREIGN DATA WRAPPER clickhouse_fdw OPTIONS(dbname 'json_test', driver 'http');
CREATE USER MAPPING FOR CURRENT_USER SERVER binary_json_loopback;
CREATE USER MAPPING FOR CURRENT_USER SERVER http_json_loopback;

SELECT clickhouse_raw_query('DROP DATABASE IF EXISTS json_test');
SELECT clickhouse_raw_query('CREATE DATABASE json_test');
SELECT clickhouse_raw_query($$
    CREATE TABLE json_test.things (
        id   Int32 NOT NULL,
        data JSON NOT NULL
    ) ENGINE = MergeTree PARTITION BY id ORDER BY (id);
$$);

CREATE SCHEMA json_bin;
CREATE SCHEMA json_http;
IMPORT FOREIGN SCHEMA "json_test" FROM SERVER binary_json_loopback INTO json_bin;
\d json_bin.things
IMPORT FOREIGN SCHEMA "json_test" FROM SERVER http_json_loopback INTO json_http;
\d json_http.things

-- Fails pending https://github.com/ClickHouse/clickhouse-cpp/issues/422
INSERT INTO json_bin.things VALUES
    (1, '{"id": 1, "name": "widget", "size": "large", "stocked": true}'),
    (2, '{"id": 2, "name": "sprocket", "size": "small", "stocked": true}')
;

INSERT INTO json_http.things VALUES
    (1, '{"id": 1, "name": "widget", "size": "large", "stocked": true}'),
    (2, '{"id": 2, "name": "sprocket", "size": "small", "stocked": true}'),
    (3, '{"id": 3, "name": "gizmo", "size": "medium", "stocked": true}'),
    (4, '{"id": 4, "name": "doodad", "size": "large", "stocked": false}')
;

SELECT * FROM json_bin.things ORDER BY id;
SELECT * FROM json_http.things ORDER BY id;

-- Subscript access on JSON columns must not be pushed down to ClickHouse.
-- ClickHouse JSON does not support the jsonb `column['key']` syntax (it
-- requires dot notation), so subscripts must be evaluated locally by
-- PostgreSQL.
EXPLAIN (VERBOSE, COSTS OFF)
SELECT data['name'] FROM json_http.things;
SELECT data['name'] FROM json_http.things ORDER BY id;

-- DISTINCT forces an ORDER BY or HashAgg; the subscript must stay local.
EXPLAIN (VERBOSE, COSTS OFF)
SELECT DISTINCT data['size'] FROM json_http.things;
SELECT DISTINCT data['size'] FROM json_http.things;

-- GROUP BY with a JSON subscript expression.
EXPLAIN (VERBOSE, COSTS OFF)
SELECT data['size'], count(*) FROM json_http.things GROUP BY data['size'];
SELECT data['size'], count(*) FROM json_http.things GROUP BY data['size'];

-- The jsonb ->> operator is pushed down in WHERE / ORDER BY clauses, but
-- target-list expressions are evaluated locally (PostgreSQL fetches the whole
-- column and applies the operator after). This query runs -> locally.
-- N.B.: Binary driver JSON data not yet supported.
EXPLAIN (VERBOSE, COSTS OFF)
SELECT data ->> 'name' FROM json_http.things;
SELECT data ->> 'name' FROM json_http.things ORDER BY id;

-- WHERE clause with ->> equality must be pushed down.
EXPLAIN (VERBOSE, COSTS OFF)
SELECT * FROM json_http.things WHERE data ->> 'name' = 'widget';
SELECT * FROM json_http.things WHERE data ->> 'name' = 'widget';

-- WHERE clause with ->> and LIKE.
EXPLAIN (VERBOSE, COSTS OFF)
SELECT * FROM json_http.things WHERE data ->> 'name' LIKE 'wid%';

-- WHERE with multiple ->> conditions (AND).
EXPLAIN (VERBOSE, COSTS OFF)
SELECT * FROM json_http.things
  WHERE data ->> 'size' = 'large' AND data ->> 'stocked' = 'true';

-- WHERE with ->> in an OR condition.
EXPLAIN (VERBOSE, COSTS OFF)
SELECT * FROM json_http.things
  WHERE data ->> 'name' = 'widget' OR data ->> 'name' = 'gizmo';

-- ORDER BY with ->> pushdown.
EXPLAIN (VERBOSE, COSTS OFF)
SELECT * FROM json_http.things ORDER BY data ->> 'size';

-- The jsonb -> operator: target-list expressions are evaluated locally
-- (same as ->>). This query evaluates `->` locally.
EXPLAIN (VERBOSE, COSTS OFF)
SELECT data -> 'name' FROM json_http.things;
SELECT data -> 'name' FROM json_http.things ORDER BY id;

-- WHERE clause with -> equality must be pushed down.
EXPLAIN (VERBOSE, COSTS OFF)
SELECT * FROM json_http.things WHERE data -> 'name' = '"widget"';
SELECT * FROM json_http.things WHERE data -> 'name' = '"widget"';

-- WHERE clause with -> JSON boolean literal must push down.
EXPLAIN (VERBOSE, COSTS OFF)
SELECT * FROM json_http.things WHERE data -> 'stocked' = 'true'::jsonb;
SELECT * FROM json_http.things WHERE data -> 'stocked' = 'true'::jsonb ORDER BY id;

-- WHERE clause with -> wraps the dot notation in toJSONString() so that the
-- result is a proper JSON value (-> returns jsonb, not text like ->>).
EXPLAIN (VERBOSE, COSTS OFF)
SELECT * FROM json_http.things WHERE data -> 'name' = '"widget"'::jsonb;

-- Edge cases: JSON keys that require identifier quoting.
SELECT clickhouse_raw_query($$
    CREATE TABLE json_test.special_keys (
        id   Int32 NOT NULL,
        data JSON NOT NULL
    ) ENGINE = MergeTree ORDER BY (id);
$$);

CREATE FOREIGN TABLE json_http.special_keys (id integer NOT NULL, data jsonb NOT NULL)
  SERVER http_json_loopback OPTIONS (database 'json_test', table_name 'special_keys');

INSERT INTO json_http.special_keys VALUES
    (1, '{"my field": "hello", "CamelCase": "world", "select": "reserved"}'),
    (2, E'{"The \\"meaning\\" of life": 42, "back\\\\slash": "bs", "dotted.key": "dot", "it''s": "apos", "key/with!special@chars#": "special", "123numeric": "num"}');

-- Key with a space: must be quoted in the remote SQL.
EXPLAIN (VERBOSE, COSTS OFF)
SELECT * FROM json_http.special_keys WHERE data ->> 'my field' = 'hello';
SELECT data ->> 'my field' FROM json_http.special_keys;

-- Key with mixed case.
EXPLAIN (VERBOSE, COSTS OFF)
SELECT * FROM json_http.special_keys WHERE data ->> 'CamelCase' = 'world';
SELECT data ->> 'CamelCase' FROM json_http.special_keys;

-- Key that is a SQL reserved word.
EXPLAIN (VERBOSE, COSTS OFF)
SELECT * FROM json_http.special_keys WHERE data ->> 'select' = 'reserved';
SELECT data ->> 'select' FROM json_http.special_keys;

-- Key containing embedded double quotes.
EXPLAIN (VERBOSE, COSTS OFF)
SELECT * FROM json_http.special_keys WHERE data ->> 'The "meaning" of life' = '42';
SELECT data ->> 'The "meaning" of life' FROM json_http.special_keys WHERE id = 2;

-- Key containing a backslash.
EXPLAIN (VERBOSE, COSTS OFF)
SELECT * FROM json_http.special_keys WHERE data ->> 'back\slash' = 'bs';
SELECT data ->> 'back\slash' FROM json_http.special_keys WHERE id = 2;

-- Key containing a dot (must not be confused with nested access).
EXPLAIN (VERBOSE, COSTS OFF)
SELECT * FROM json_http.special_keys WHERE data ->> 'dotted.key' = 'dot';
SELECT data ->> 'dotted.key' FROM json_http.special_keys WHERE id = 2;

-- Key containing an apostrophe / single quote.
EXPLAIN (VERBOSE, COSTS OFF)
SELECT * FROM json_http.special_keys WHERE data ->> 'it''s' = 'apos';
SELECT data ->> 'it''s' FROM json_http.special_keys WHERE id = 2;

-- Key with slashes, bangs, at-signs, etc.
EXPLAIN (VERBOSE, COSTS OFF)
SELECT * FROM json_http.special_keys WHERE data ->> 'key/with!special@chars#' = 'special';

-- Key that starts with a digit.
EXPLAIN (VERBOSE, COSTS OFF)
SELECT * FROM json_http.special_keys WHERE data ->> '123numeric' = 'num';

-- =======================================================================
-- jsonb_extract_path_text / jsonb_extract_path pushdown
-- =======================================================================

-- Create a table with nested JSON for multi-level path tests.
SELECT clickhouse_raw_query($$
    CREATE TABLE json_test.events (
        id         UInt32,
        event_name String,
        props      JSON
    ) ENGINE = MergeTree ORDER BY (event_name, id);
$$);
SELECT clickhouse_raw_query($$
    INSERT INTO json_test.events VALUES
        (1, 'order', '{"customerId": "C100", "address": {"city": "Paris", "zip": "75001"}}'),
        (2, 'order', '{"customerId": "C200", "address": {"city": "London", "zip": "SW1A"}}');
$$);

CREATE FOREIGN TABLE json_http_events (
    id         integer,
    event_name text,
    props      jsonb
) SERVER http_json_loopback OPTIONS (table_name 'events');

-- Target-list: jsonb_extract_path_text is evaluated locally (like -> / ->>).
EXPLAIN (VERBOSE, COSTS OFF)
SELECT jsonb_extract_path_text(props, 'customerId') FROM json_http_events;
SELECT jsonb_extract_path_text(props, 'customerId') FROM json_http_events ORDER BY id;

-- Target-list: multi-level path, still evaluated locally.
EXPLAIN (VERBOSE, COSTS OFF)
SELECT jsonb_extract_path_text(props, 'address', 'city') FROM json_http_events;
SELECT jsonb_extract_path_text(props, 'address', 'city') FROM json_http_events ORDER BY id;

-- Target-list: jsonb_extract_path (returns jsonb, not text), evaluated locally.
EXPLAIN (VERBOSE, COSTS OFF)
SELECT jsonb_extract_path(props, 'address') FROM json_http_events;

-- WHERE: single-level jsonb_extract_path_text pushes down as dot notation.
EXPLAIN (VERBOSE, COSTS OFF)
SELECT id FROM json_http_events
WHERE jsonb_extract_path_text(props, 'customerId') = 'C100';
SELECT id FROM json_http_events
WHERE jsonb_extract_path_text(props, 'customerId') = 'C100';

-- WHERE: multi-level jsonb_extract_path_text pushes down as dot notation.
EXPLAIN (VERBOSE, COSTS OFF)
SELECT id FROM json_http_events
WHERE jsonb_extract_path_text(props, 'address', 'city') = 'Paris';
SELECT id FROM json_http_events
WHERE jsonb_extract_path_text(props, 'address', 'city') = 'Paris';

-- WHERE: jsonb_extract_path pushes down with toJSONString wrapping.
EXPLAIN (VERBOSE, COSTS OFF)
SELECT id FROM json_http_events
WHERE jsonb_extract_path(props, 'address', 'city') = '"Paris"'::jsonb;

DROP FOREIGN TABLE json_http_events;

SELECT clickhouse_raw_query('DROP DATABASE json_test');
DROP USER MAPPING FOR CURRENT_USER SERVER binary_json_loopback;
DROP USER MAPPING FOR CURRENT_USER SERVER http_json_loopback;
DROP SERVER binary_json_loopback CASCADE;
DROP SERVER http_json_loopback CASCADE;
