Version: v0.2
Applies to: Funl Data Translator
Maintained by: VaultKit Engineering Team
AQL (Abstract Query Language) is a structured, JSON-based query specification format used by the VaultKit + Funl stack.
It allows secure, policy-aware data access by expressing what data to retrieve rather than writing raw SQL.
AQL queries are translated into SQL dynamically by Funl, applying masking, approval, and compliance policies before execution.
Every AQL query describes a single data retrieval request.
{
"source_table": "orders",
"columns": ["id", "region", "amount"],
"joins": [],
"aggregates": [],
"filters": [],
"group_by": [],
"having": [],
"order_by": null,
"limit": 0,
"offset": 0
}
| Field | Type | Description | Example |
|---|---|---|---|
source_table |
string | The base table or dataset to query. | "users" |
columns |
array[string] | List of fields to select (supports table.field). |
["email", "username"] |
joins |
array[Join] | Describes table joins. | [{"type": "LEFT", "table": "orders", "left_field": "users.id", "right_field": "orders.user_id"}] |
aggregates |
array[Aggregation] | Aggregate functions. | [{"func": "sum", "field": "orders.amount", "alias": "total"}] |
filters |
array[Filter] | WHERE clause filters. | [{"operator": "eq", "field": "users.active", "value": true}] |
group_by |
array[string] | Columns to group results by. | ["region"] |
having |
array[Having] | Post-aggregation filters. | [{"operator": "gt", "field": "SUM(orders.amount)", "value": 10000}] |
order_by |
object | Sorting configuration. | {"column": "orders.amount", "direction": "DESC"} |
limit |
integer | Max number of rows to return. | 10 |
offset |
integer | Pagination offset. | 20 |
Determines which fields or aggregates to return.
{
"source_table": "users",
"columns": ["email", "username"]
}
➡️ SQL Output:
SELECT users.email, users.username FROM users;
With aggregates:
{
"source_table": "orders",
"columns": ["region"],
"aggregates": [{ "func": "sum", "field": "orders.amount", "alias": "total_amount" }],
"group_by": ["region"]
}
➡️ SQL Output:
SELECT region, SUM(orders.amount) AS total_amount FROM orders GROUP BY region;
💡 If group_by is missing but aggregates exist, Funl automatically groups by columns.
"joins": [
{
"type": "LEFT",
"table": "orders",
"alias": "o",
"left_field": "users.id",
"right_field": "o.user_id"
}
]
➡️ SQL Output:
LEFT JOIN orders o ON users.id = o.user_id
Supported join types: INNER, LEFT, RIGHT, FULL
Used for filtering before aggregation.
Supports both simple conditions and nested logical groups using AND and OR.
"filters": [
{ "operator": "eq", "field": "users.active", "value": true },
{ "operator": "gt", "field": "orders.amount", "value": 100 }
]
➡️ SQL Output:
WHERE users.active = $1 AND orders.amount > $2
"filters": [
{
"logic": "OR",
"conditions": [
{ "operator": "eq", "field": "users.role", "value": "admin" },
{ "operator": "eq", "field": "users.role", "value": "manager" }
]
},
{ "operator": "gt", "field": "orders.amount", "value": 100 }
]
➡️ SQL Output:
WHERE (users.role = $1 OR users.role = $2) AND orders.amount > $3;
You can nest multiple logical groups, and the translator will automatically wrap OR blocks in parentheses to preserve precedence.
"filters": [
{
"logic": "AND",
"conditions": [
{
"logic": "OR",
"conditions": [
{ "operator": "eq", "field": "users.country", "value": "CA" },
{ "operator": "eq", "field": "users.country", "value": "US" }
]
},
{
"logic": "OR",
"conditions": [
{ "operator": "gt", "field": "orders.amount", "value": 5000 },
{ "operator": "eq", "field": "users.vip", "value": true }
]
}
]
}
]
➡️ SQL Output:
WHERE ((users.country = $1 OR users.country = $2)
AND (orders.amount > $3 OR users.vip = $4));
| Operator | SQL Equivalent | Example |
|---|---|---|
eq |
= |
users.role = $1 |
neq |
!= |
users.role != $1 |
gt |
> |
orders.amount > $1 |
lt |
< |
orders.amount < $1 |
gte |
>= |
orders.amount >= $1 |
lte |
<= |
orders.amount <= $1 |
like |
LIKE |
users.email LIKE '%@example.com%' |
in |
IN (...) |
users.id IN ($1, $2, $3) |
is_null |
IS NULL |
users.deleted_at IS NULL |
is_not_null |
IS NOT NULL |
users.deleted_at IS NOT NULL |
If masking policies are defined for a field, filters on that field automatically use the masked version in SQL.
Example:
{ "operator": "eq", "field": "users.email", "value": "test@example.com" }
➡️ SQL Output (with hash masking):
WHERE ENCODE(DIGEST(users.email::text, 'sha256'), 'hex') = $1
Post-aggregation filters.
"having": [
{ "operator": "gt", "field": "SUM(orders.amount)", "value": 10000 }
]
➡️ SQL Output:
HAVING SUM(orders.amount) > $1
"order_by": { "column": "orders.amount", "direction": "DESC" }
➡️ SQL Output:
ORDER BY orders.amount DESC
"limit": 10,
"offset": 20
➡️ SQL Output:
LIMIT 10 OFFSET 20
Funl can apply masking at translation time based on registered policies.
| Type | Description | SQL Output |
|---|---|---|
partial |
Show first few characters | CONCAT(LEFT(email, 3), '****') |
hash |
Replace with irreversible hash | ENCODE(DIGEST(email::text, 'sha256'), 'hex') |
full |
Fully masked | '*****' |
Example:
{
"source_table": "users",
"columns": ["email", "username"]
}
Mask registry:
masking.Register("users.email", masking.Policy{Type: "partial"})
➡️ SQL Output:
SELECT CONCAT(LEFT(users.email, 3), '****') AS email, users.username FROM users;
{
"source_table": "users",
"columns": ["id", "email"]
}
➡️ SQL Output:
SELECT users.id, users.email FROM users;
{
"source_table": "orders",
"columns": ["region"],
"aggregates": [{ "func": "sum", "field": "orders.amount", "alias": "total" }],
"group_by": ["region"],
"having": [{ "operator": "gt", "field": "SUM(orders.amount)", "value": 5000 }]
}
➡️ SQL Output:
SELECT region, SUM(orders.amount) AS total
FROM orders
GROUP BY region
HAVING SUM(orders.amount) > $1;
{
"source_table": "users",
"columns": ["users.id", "orders.amount"],
"joins": [
{"type": "LEFT", "table": "orders", "left_field": "users.id", "right_field": "orders.user_id"}
],
"filters": [
{"operator": "eq", "field": "users.active", "value": true}
]
}
➡️ SQL Output:
SELECT users.id, orders.amount
FROM users
LEFT JOIN orders ON users.id = orders.user_id
WHERE users.active = $1;
| Feature | Description |
|---|---|
| Nested subqueries | SELECT ... FROM (SELECT ...) support |
| CASE expressions | Conditional field derivation |
| Window functions | ROW_NUMBER() OVER (...), RANK() |
| UNION / INTERSECT / EXCEPT | Multi-query composition |
| JSON operators | Semi-structured data support (e.g., ->, ->>) |
AQL:
{
"source_table": "users",
"joins": [
{"type": "LEFT", "table": "orders", "left_field": "users.id", "right_field": "orders.user_id"}
],
"columns": ["users.email", "users.username"],
"aggregates": [{"func": "sum", "field": "orders.amount", "alias": "total_spent"}],
"group_by": ["users.email", "users.username"],
"having": [{"operator": "gt", "field": "SUM(orders.amount)", "value": 500}],
"order_by": {"column": "total_spent", "direction": "DESC"},
"limit": 10
}
SQL Output:
SELECT users.email, users.username, SUM(orders.amount) AS total_spent
FROM users
LEFT JOIN orders ON users.id = orders.user_id
GROUP BY users.email, users.username
HAVING SUM(orders.amount) > $1
ORDER BY total_spent DESC
LIMIT 10;
| Concept | SQL Equivalent | AQL Field | Supported |
|---|---|---|---|
| SELECT | columns, aggregates | ✅ | ✅ |
| FROM | source_table | ✅ | ✅ |
| JOIN | joins | ✅ | ✅ |
| WHERE | filters | ✅ | ✅ |
| GROUP BY | group_by / inferred | ✅ | ✅ |
| HAVING | having | ✅ | ✅ |
| ORDER BY | order_by | ✅ | ✅ |
| LIMIT / OFFSET | limit, offset | ✅ | ✅ |