MCPcopy Create free account
hub / github.com/PostHog/duckgres / createJSONConcatExpr

Method createJSONConcatExpr

transpiler/transform/operators.go:756–812  ·  view source on GitHub ↗

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)

Source from the content-addressed store, hash-verified

754// duckgres_jsonb_concat(l, r)) at session/worker init and emit one call per
755// ||, binding each operand exactly once.
756func (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

Callers 1

transformExpressionMethod · 0.95

Calls 8

transformExpressionMethod · 0.95
jsonFuncCallFunction · 0.85
cloneNodeFunction · 0.85
strConstFunction · 0.85
jsonTypeEqualsFunction · 0.85
castToJSONFunction · 0.85
isNullTestFunction · 0.85
nullConstNodeFunction · 0.85

Tested by

no test coverage detected