createJSONConcatExpr rewrites `a || b` (with a JSON-looking operand) to a CASE expression reproducing Postgres jsonb || semantics in DuckDB: object || object -> shallow merge, right side wins, explicit nulls are KEPT (unlike json_merge_patch, which deep-merges and
(left, right *pg_query.Node)
| 754 | // duckgres_jsonb_concat(l, r)) at session/worker init and emit one call per |
| 755 | // ||, binding each operand exactly once. |
| 756 | func (t *OperatorTransform) createJSONConcatExpr(left, right *pg_query.Node) *pg_query.Node { |
| 757 | if newLeft := t.transformExpression(left); newLeft != nil { |
| 758 | left = newLeft |
| 759 | } |
| 760 | if newRight := t.transformExpression(right); newRight != nil { |
| 761 | right = newRight |
| 762 | } |
| 763 | |
| 764 | asObjectMap := func(operand *pg_query.Node) *pg_query.Node { |
| 765 | return jsonFuncCall("json_transform", cloneNode(operand), strConst(`"MAP(VARCHAR, JSON)"`)) |
| 766 | } |
| 767 | // CASE WHEN json_type(x) = 'ARRAY' THEN json_transform(x, '["JSON"]') |
| 768 | // ELSE list_value(CAST(x AS JSON)) END — Postgres treats every non-array |
| 769 | // operand of a non-object||object concat as a one-element array. |
| 770 | asElementList := func(operand *pg_query.Node) *pg_query.Node { |
| 771 | return &pg_query.Node{Node: &pg_query.Node_CaseExpr{CaseExpr: &pg_query.CaseExpr{ |
| 772 | Args: []*pg_query.Node{{Node: &pg_query.Node_CaseWhen{CaseWhen: &pg_query.CaseWhen{ |
| 773 | Expr: jsonTypeEquals(operand, "ARRAY"), |
| 774 | Result: jsonFuncCall("json_transform", cloneNode(operand), strConst(`["JSON"]`)), |
| 775 | }}}}, |
| 776 | Defresult: jsonFuncCall("list_value", castToJSON(cloneNode(operand))), |
| 777 | }}} |
| 778 | } |
| 779 | |
| 780 | // The whole CASE is wrapped in an explicit CAST(... AS JSON). DuckDB's |
| 781 | // to_json()/map_concat()/list_concat() result type is not reported as JSON |
| 782 | // by every bundled DuckDB version — on some versions the wire layer then |
| 783 | // sees the column as an untyped list and renders it with Go's %v |
| 784 | // ("[1 2 3 4]") instead of routing it through the JSON re-serializer |
| 785 | // ("[1,2,3,4]"). Forcing the column to JSON pins OID 114 so server's |
| 786 | // encodeJSON path always produces valid JSON text. The cast is idempotent |
| 787 | // on versions that already type it as JSON. (#716 regression.) |
| 788 | return castToJSON(&pg_query.Node{Node: &pg_query.Node_CaseExpr{CaseExpr: &pg_query.CaseExpr{ |
| 789 | Args: []*pg_query.Node{ |
| 790 | // WHEN L IS NULL OR R IS NULL THEN NULL — Postgres jsonb || is |
| 791 | // strict; without this guard the ELSE branch would wrap a SQL |
| 792 | // NULL operand into [null, ...]. |
| 793 | {Node: &pg_query.Node_CaseWhen{CaseWhen: &pg_query.CaseWhen{ |
| 794 | Expr: &pg_query.Node{Node: &pg_query.Node_BoolExpr{BoolExpr: &pg_query.BoolExpr{ |
| 795 | Boolop: pg_query.BoolExprType_OR_EXPR, |
| 796 | Args: []*pg_query.Node{isNullTest(left), isNullTest(right)}, |
| 797 | }}}, |
| 798 | Result: nullConstNode(0), |
| 799 | }}}, |
| 800 | // WHEN both objects THEN shallow merge, right side wins. |
| 801 | {Node: &pg_query.Node_CaseWhen{CaseWhen: &pg_query.CaseWhen{ |
| 802 | Expr: &pg_query.Node{Node: &pg_query.Node_BoolExpr{BoolExpr: &pg_query.BoolExpr{ |
| 803 | Boolop: pg_query.BoolExprType_AND_EXPR, |
| 804 | Args: []*pg_query.Node{jsonTypeEquals(left, "OBJECT"), jsonTypeEquals(right, "OBJECT")}, |
| 805 | }}}, |
| 806 | Result: jsonFuncCall("to_json", jsonFuncCall("map_concat", asObjectMap(left), asObjectMap(right))), |
| 807 | }}}, |
| 808 | }, |
| 809 | // ELSE array concatenation (with non-arrays wrapped as one element). |
| 810 | Defresult: jsonFuncCall("to_json", jsonFuncCall("list_concat", asElementList(left), asElementList(right))), |
| 811 | }}}) |
| 812 | } |
| 813 |
no test coverage detected