Skip to content

Commit 06f18c2

Browse files
committed
Add emulation function for Snowflake's OBJECT_AGG() (GoogleCloudPlatform#539)
1 parent 2b48e80 commit 06f18c2

3 files changed

Lines changed: 131 additions & 1 deletion

File tree

udfs/migration/snowflake/README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,32 @@ Row|j
5656
---|-
5757
1|{"frame":2,"xray":1}
5858

59+
### [object_agg(key STRING, value JSON)](object_agg.sqlx)
60+
Emulates the `OBJECT_AGG` function in Snowflake. [Snowflake docs](https://docs.snowflake.com/en/sql-reference/functions/object_agg)
61+
```sql
62+
SELECT object_agg(k, v)
63+
FROM
64+
(
65+
SELECT 'a' AS k, json '1' AS v
66+
UNION ALL
67+
SELECT 'b' as k, json '2' as v);
68+
```
69+
70+
Row|f0_
71+
---|---
72+
1|{"b":2,"a":1}
73+
74+
```sql
75+
SELECT object_agg(k, v)
76+
FROM
77+
(
78+
SELECT 'a' AS k, json '1' AS v
79+
UNION ALL
80+
SELECT 'a' AS k, json '3' AS b
81+
UNION ALL
82+
SELECT 'b' as k, json '2' as v);
83+
```
84+
85+
Error: Duplicate field key 'a' at object_agg(STRING, JSON) line 7, columns 6-7
86+
87+
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
config { hasOutput: true }
2+
/*
3+
* Copyright 2026 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
CREATE OR REPLACE AGGREGATE FUNCTION ${self()}(
19+
key STRING,
20+
value JSON,
21+
allow_duplicate_pairs BOOL NOT AGGREGATE)
22+
RETURNS JSON
23+
LANGUAGE js
24+
OPTIONS (
25+
description = "Emulation of Snowflake's OBJECT_AGG function."
26+
) AS r"""
27+
export function initialState() {
28+
return {obj: {}, allow_duplicate_pairs: false};
29+
}
30+
31+
export function aggregate(state, key, value, allow_duplicate_pairs) {
32+
state.allow_duplicate_pairs = allow_duplicate_pairs;
33+
if (key === null || value === null) {
34+
return;
35+
}
36+
if (Object.hasOwn(state.obj, key) && (!allow_duplicate_pairs || state.obj[key] !== value)) {
37+
throw new Error("Duplicate field key '" + key + "'");
38+
}
39+
state.obj[key] = value;
40+
}
41+
42+
export function merge(state, partialState) {
43+
const stateKeys = Object.keys(state.obj);
44+
const partialStateKeys = Object.keys(partialState.obj);
45+
const [smallerKeys, largerObj, smallerObj] = stateKeys.length < partialStateKeys.length
46+
? [stateKeys, partialState.obj, state.obj]
47+
: [partialStateKeys, state.obj, partialState.obj];
48+
for (let i = 0; i < smallerKeys.length; i++) {
49+
const key = smallerKeys[i];
50+
if (Object.hasOwn(largerObj, key) &&
51+
(!state.allow_duplicate_pairs || largerObj[key] === smallerObj[key])) {
52+
throw new Error("Duplicate field key '" + key + "'");
53+
}
54+
}
55+
Object.assign(state.obj, partialState.obj);
56+
}
57+
58+
export function finalize(state) {
59+
return state.obj;
60+
}
61+
""";

udfs/migration/snowflake/test_cases.js

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
const {generate_udf_test} = unit_test_utils;
15+
const {generate_udf_test, generate_udaf_test} = unit_test_utils;
1616

1717
generate_udf_test("factorial", [
1818
{
@@ -108,3 +108,43 @@ generate_udf_test("json_ilike", [
108108
}
109109
]);
110110

111+
generate_udaf_test("object_agg",
112+
{
113+
input_columns: [`key`, `value`, `false NOT AGGREGATE`],
114+
input_rows: `
115+
select 'a' as key, JSON '1' as value
116+
union all
117+
select 'b' as key, JSON '2' as value
118+
`,
119+
expected_output: `JSON '{"a": 1, "b": 2}'`
120+
}
121+
);
122+
123+
generate_udaf_test("object_agg",
124+
{
125+
input_columns: [`key`, `value`, `true NOT AGGREGATE`],
126+
input_rows: `
127+
select 'a' as key, JSON '1' as value
128+
union all
129+
select null as key, JSON '3' as value
130+
union all
131+
select 'b' as key, JSON '2' as value
132+
union all
133+
select 'b' as key, null as value
134+
`,
135+
expected_output: `JSON '{"a": 1, "b": 2}'`
136+
}
137+
);
138+
139+
generate_udaf_test("object_agg",
140+
{
141+
input_columns: [`key`, `value`, `true NOT AGGREGATE`],
142+
input_rows: `
143+
select 'a' as key, JSON '1' as value
144+
union all
145+
select 'a' as key, JSON '1' as value
146+
`,
147+
expected_output: `JSON '{"a": 1}'`
148+
}
149+
);
150+

0 commit comments

Comments
 (0)