Skip to content

Commit 4f087e8

Browse files
committed
fix: add bindings support
1 parent a5d5e44 commit 4f087e8

File tree

3 files changed

+427
-52
lines changed

3 files changed

+427
-52
lines changed

docs/README.md

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,18 +85,73 @@ SELECT jsonata('{
8585
└────────────────────────────────────────────────┘
8686
```
8787

88-
## Function
88+
## Functions
8989

9090
### `jsonata(expression, json_data)`
9191

92-
The JSONata extension provides a single scalar function that evaluates a JSONata expression against JSON data.
92+
Evaluates a JSONata expression against JSON data.
9393

9494
**Parameters:**
9595
- `expression` (VARCHAR): The JSONata expression to evaluate
9696
- `json_data` (JSON): The JSON data to query/transform
9797

9898
**Returns:** JSON
9999

100+
### `jsonata(expression, json_data, bindings)`
101+
102+
Evaluates a JSONata expression against JSON data with external variable bindings.
103+
104+
**Parameters:**
105+
- `expression` (VARCHAR): The JSONata expression to evaluate
106+
- `json_data` (JSON): The JSON data to query/transform
107+
- `bindings` (JSON): A JSON object where keys become variable names accessible in the expression using `$variable_name` syntax
108+
109+
**Returns:** JSON
110+
111+
**Bindings Example:**
112+
113+
```sql
114+
-- Simple variable binding
115+
SELECT jsonata('$x + $y', '{}', '{"x": 10, "y": 20}') as result;
116+
┌────────┐
117+
│ result │
118+
│ json │
119+
├────────┤
120+
30
121+
└────────┘
122+
123+
-- Use bindings for dynamic filtering
124+
SELECT jsonata(
125+
'items[price > $threshold]',
126+
'{"items": [{"price": 50}, {"price": 150}, {"price": 200}]}',
127+
'{"threshold": 100}'
128+
) as expensive_items;
129+
┌─────────────────────────────────┐
130+
│ expensive_items │
131+
│ json │
132+
├─────────────────────────────────┤
133+
│ [{"price":150},{"price":200}] │
134+
└─────────────────────────────────┘
135+
136+
-- Combine data access with bindings
137+
SELECT jsonata(
138+
'name & " - " & $status',
139+
'{"name": "Order #123"}',
140+
'{"status": "shipped"}'
141+
) as message;
142+
┌─────────────────────────┐
143+
│ message │
144+
│ json │
145+
├─────────────────────────┤
146+
"Order #123 - shipped"
147+
└─────────────────────────┘
148+
```
149+
150+
Bindings are useful for:
151+
- Passing parameters into JSONata expressions without modifying the expression string
152+
- Dynamic filtering with thresholds or values from other columns
153+
- Separating configuration from data transformation logic
154+
100155
**Examples:**
101156

102157

@@ -235,6 +290,31 @@ Use the descendant wildcard operator (`**`) to traverse all levels of a nested s
235290

236291
## Practical Examples
237292

293+
### Using Bindings with Table Data
294+
295+
```sql
296+
-- Use column values as bindings for dynamic filtering
297+
CREATE TABLE products (
298+
category VARCHAR,
299+
inventory JSON,
300+
min_quantity INTEGER
301+
);
302+
303+
INSERT INTO products VALUES
304+
('electronics', '{"items": [{"name": "Phone", "qty": 5}, {"name": "Laptop", "qty": 12}]}', 10),
305+
('clothing', '{"items": [{"name": "Shirt", "qty": 25}, {"name": "Pants", "qty": 8}]}', 15);
306+
307+
-- Filter items based on per-row threshold from min_quantity column
308+
SELECT
309+
category,
310+
jsonata(
311+
'items[qty >= $min].name',
312+
inventory,
313+
json_object('min', min_quantity)
314+
) as items_in_stock
315+
FROM products;
316+
```
317+
238318
### Extract Data from API Responses
239319

240320
```sql

src/jsonata_extension.cpp

Lines changed: 112 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include "jsonata_extension.hpp"
44
#include "duckdb.hpp"
55
#include "duckdb/common/exception.hpp"
6+
#include "duckdb/common/vector_operations/ternary_executor.hpp"
67
#include "duckdb/function/scalar_function.hpp"
78
#include <duckdb/parser/parsed_data/create_scalar_function_info.hpp>
89

@@ -12,79 +13,126 @@
1213

1314
namespace duckdb {
1415

16+
static std::unique_ptr<jsonata::Jsonata> ParseJsonataExpression(const string &expr_str) {
17+
try {
18+
return make_uniq<jsonata::Jsonata>(expr_str);
19+
} catch (const std::exception &e) {
20+
throw InvalidInputException("Invalid JSONata expression: %s", e.what());
21+
}
22+
}
23+
24+
static nlohmann::json ParseJsonData(string_t data) {
25+
try {
26+
return nlohmann::json::parse(data.GetData(), data.GetData() + data.GetSize());
27+
} catch (const nlohmann::json::parse_error &e) {
28+
throw InvalidInputException("Invalid JSON data: %s", e.what());
29+
}
30+
}
31+
32+
static void BindJsonToFrame(std::shared_ptr<jsonata::Frame> frame, const nlohmann::json &bindings) {
33+
if (!bindings.is_object()) {
34+
throw InvalidInputException("Bindings must be a JSON object");
35+
}
36+
for (auto &[key, value] : bindings.items()) {
37+
frame->bind(key, jsonata::Jsonata::jsonToAny(value));
38+
}
39+
}
40+
41+
static string_t EvaluateJsonata(jsonata::Jsonata &expr, const nlohmann::json &data, Vector &result) {
42+
nlohmann::json output;
43+
try {
44+
output = expr.evaluate(data);
45+
} catch (const std::exception &e) {
46+
throw InvalidInputException("JSONata evaluation error: %s", e.what());
47+
}
48+
return StringVector::AddString(result, output.dump());
49+
}
50+
51+
static string_t EvaluateJsonataWithBindings(jsonata::Jsonata &expr, const nlohmann::json &data,
52+
const nlohmann::json &bindings, Vector &result) {
53+
nlohmann::json output;
54+
try {
55+
auto frame = expr.createFrame();
56+
BindJsonToFrame(frame, bindings);
57+
output = expr.evaluate(data, frame);
58+
} catch (const std::exception &e) {
59+
throw InvalidInputException("JSONata evaluation error: %s", e.what());
60+
}
61+
return StringVector::AddString(result, output.dump());
62+
}
63+
1564
inline void JsonataScalarFun(DataChunk &args, ExpressionState &state, Vector &result) {
1665
auto &jsonata_vector = args.data[0];
1766
auto &data_vector = args.data[1];
1867

1968
if (jsonata_vector.GetVectorType() == VectorType::CONSTANT_VECTOR) {
2069
if (ConstantVector::IsNull(jsonata_vector)) {
21-
// If the JSONata expression is NULL, the result is NULL
2270
result.SetVectorType(VectorType::CONSTANT_VECTOR);
2371
ConstantVector::SetNull(result, true);
2472
return;
2573
}
2674
auto jsonata_str = ConstantVector::GetData<string_t>(jsonata_vector)[0];
27-
28-
// Parse the JSONata expression once for all rows (constant optimization)
29-
std::unique_ptr<jsonata::Jsonata> jsonata_expr;
30-
try {
31-
jsonata_expr = make_uniq<jsonata::Jsonata>(jsonata_str.GetString());
32-
} catch (const std::exception &e) {
33-
throw InvalidInputException("Invalid JSONata expression: %s", e.what());
34-
}
75+
auto jsonata_expr = ParseJsonataExpression(jsonata_str.GetString());
3576

3677
UnaryExecutor::Execute<string_t, string_t>(data_vector, result, args.size(), [&](string_t data) {
37-
nlohmann::json parsed;
38-
try {
39-
parsed = nlohmann::json::parse(string(data.GetData(), data.GetSize()));
40-
} catch (const nlohmann::json::parse_error &e) {
41-
throw InvalidInputException("Invalid JSON data: %s", e.what());
42-
}
43-
44-
nlohmann::json output;
45-
try {
46-
output = jsonata_expr->evaluate(parsed);
47-
} catch (const std::exception &e) {
48-
throw InvalidInputException("JSONata evaluation error: %s", e.what());
49-
}
50-
51-
return StringVector::AddString(result, output.dump());
78+
auto parsed = ParseJsonData(data);
79+
return EvaluateJsonata(*jsonata_expr, parsed, result);
5280
});
5381
} else {
5482
BinaryExecutor::Execute<string_t, string_t, string_t>(
5583
jsonata_vector, data_vector, result, args.size(), [&](string_t jsonata_str, string_t data) {
56-
nlohmann::json parsed;
57-
try {
58-
parsed = nlohmann::json::parse(string(data.GetData(), data.GetSize()));
59-
} catch (const nlohmann::json::parse_error &e) {
60-
throw InvalidInputException("Invalid JSON data: %s", e.what());
61-
}
62-
63-
std::unique_ptr<jsonata::Jsonata> jsonata_expr;
64-
try {
65-
jsonata_expr = make_uniq<jsonata::Jsonata>(string(jsonata_str.GetData(), jsonata_str.GetSize()));
66-
} catch (const std::exception &e) {
67-
throw InvalidInputException("Invalid JSONata expression: %s", e.what());
68-
}
69-
70-
nlohmann::json output;
71-
try {
72-
output = jsonata_expr->evaluate(parsed);
73-
} catch (const std::exception &e) {
74-
throw InvalidInputException("JSONata evaluation error: %s", e.what());
75-
}
76-
77-
return StringVector::AddString(result, output.dump());
84+
auto jsonata_expr = ParseJsonataExpression(string(jsonata_str.GetData(), jsonata_str.GetSize()));
85+
auto parsed = ParseJsonData(data);
86+
return EvaluateJsonata(*jsonata_expr, parsed, result);
87+
});
88+
}
89+
}
90+
91+
inline void JsonataScalarFunWithBindings(DataChunk &args, ExpressionState &state, Vector &result) {
92+
auto &jsonata_vector = args.data[0];
93+
auto &data_vector = args.data[1];
94+
auto &bindings_vector = args.data[2];
95+
96+
if (jsonata_vector.GetVectorType() == VectorType::CONSTANT_VECTOR) {
97+
if (ConstantVector::IsNull(jsonata_vector)) {
98+
result.SetVectorType(VectorType::CONSTANT_VECTOR);
99+
ConstantVector::SetNull(result, true);
100+
return;
101+
}
102+
auto jsonata_str = ConstantVector::GetData<string_t>(jsonata_vector)[0];
103+
auto jsonata_expr = ParseJsonataExpression(jsonata_str.GetString());
104+
105+
BinaryExecutor::Execute<string_t, string_t, string_t>(
106+
data_vector, bindings_vector, result, args.size(), [&](string_t data, string_t bindings) {
107+
auto parsed = ParseJsonData(data);
108+
auto parsed_bindings = ParseJsonData(bindings);
109+
return EvaluateJsonataWithBindings(*jsonata_expr, parsed, parsed_bindings, result);
110+
});
111+
} else {
112+
TernaryExecutor::Execute<string_t, string_t, string_t, string_t>(
113+
jsonata_vector, data_vector, bindings_vector, result, args.size(),
114+
[&](string_t jsonata_str, string_t data, string_t bindings) {
115+
auto jsonata_expr = ParseJsonataExpression(string(jsonata_str.GetData(), jsonata_str.GetSize()));
116+
auto parsed = ParseJsonData(data);
117+
auto parsed_bindings = ParseJsonData(bindings);
118+
return EvaluateJsonataWithBindings(*jsonata_expr, parsed, parsed_bindings, result);
78119
});
79120
}
80121
}
81122

82123
static void LoadInternal(ExtensionLoader &loader) {
83124
ScalarFunctionSet jsonata_function_set("jsonata");
125+
126+
// 2-argument version: jsonata(expression, json_data)
84127
auto jsonata_scalar_function =
85128
ScalarFunction({LogicalType::VARCHAR, LogicalType::JSON()}, LogicalType::JSON(), JsonataScalarFun);
86129
jsonata_function_set.AddFunction(jsonata_scalar_function);
87130

131+
// 3-argument version: jsonata(expression, json_data, bindings)
132+
auto jsonata_with_bindings = ScalarFunction({LogicalType::VARCHAR, LogicalType::JSON(), LogicalType::JSON()},
133+
LogicalType::JSON(), JsonataScalarFunWithBindings);
134+
jsonata_function_set.AddFunction(jsonata_with_bindings);
135+
88136
CreateScalarFunctionInfo info(jsonata_function_set);
89137
info.descriptions.push_back({
90138
// parameter_types
@@ -96,15 +144,29 @@ static void LoadInternal(ExtensionLoader &loader) {
96144
"language for JSON data. See https://jsonata.org for the full language reference.",
97145
// examples
98146
{"jsonata('Account.Name', '{\"Account\": {\"Name\": \"Firefly\"}}')",
99-
"jsonata('$.prices[price > 100]', my_json_column)",
100-
"jsonata('$sum(Order.Product.Price)', orders)"},
147+
"jsonata('$.prices[price > 100]', my_json_column)", "jsonata('$sum(Order.Product.Price)', orders)"},
148+
// categories
149+
{"json"},
150+
});
151+
info.descriptions.push_back({
152+
// parameter_types
153+
{LogicalType::VARCHAR, LogicalType::JSON(), LogicalType::JSON()},
154+
// parameter_names
155+
{"expression", "json_data", "bindings"},
156+
// description
157+
"Evaluates a JSONata expression against JSON data with external variable bindings. "
158+
"The bindings parameter is a JSON object where keys become variable names accessible in the expression "
159+
"using $variable_name syntax.",
160+
// examples
161+
{"jsonata('$name', '{}', '{\"name\": \"Alice\"}')", "jsonata('$x + $y', '{}', '{\"x\": 10, \"y\": 20}')",
162+
"jsonata('items[price > $threshold]', my_json, '{\"threshold\": 100}')"},
101163
// categories
102164
{"json"},
103165
});
104166

105167
loader.RegisterFunction(info);
106168

107-
QueryFarmSendTelemetry(loader, "jsonata", "2025110901");
169+
QueryFarmSendTelemetry(loader, "jsonata", "2025121201");
108170
}
109171

110172
void JsonataExtension::Load(ExtensionLoader &loader) {
@@ -115,7 +177,7 @@ std::string JsonataExtension::Name() {
115177
}
116178

117179
std::string JsonataExtension::Version() const {
118-
return "2025110901";
180+
return "2025121201";
119181
}
120182

121183
} // namespace duckdb

0 commit comments

Comments
 (0)