SQL Endpoint
The SQL endpoint lets BI tools, notebooks and reverse-ETL jobs query a semantic model with ordinary SELECT statements. Each entity of the model is a table, and each dimension and metric is a column. Every statement runs through the federated query engine: predicates, projections and aggregations are pushed down to the data sources where possible, and your access policies (ROW filters, COLUMN masks, ENTITY denials) are applied exactly as they are for saved queries.
The endpoint is read-only. INSERT, UPDATE, DELETE, DDL and multiple statements in one request are rejected before anything reaches a data source.
Status
| Surface | Status |
|---|---|
HTTP SQL API (semExecuteSql, semSqlCatalog) | Available |
| SQL console in the SemanticFed app (Query Console page) | Available |
| Power BI / Excel (Power Query), Grafana, Python notebooks, scripted bridges | Available through the HTTP API (recipes below) |
| Native JDBC / ODBC driver or wire protocol (Arrow Flight SQL, PostgreSQL wire) | Roadmap. Until it ships, JDBC-only tools use the bridge described below |
Endpoint and authentication
The SQL API is served by the same authenticated workspace gateway as the GraphQL API:
BURDENOFF_ENV | URL |
|---|---|
prod (default) | https://graphqlworkspaces.burdenoff.com/workspaces/graphql |
alpha | https://alphagraphqlworkspaces.burdenoff.com/workspaces/graphql |
Send an HTTP POST with Content-Type: application/json, an Authorization: Bearer <token> header and an x-workspace-id: <workspace id> header. See Authentication for how to obtain a token; for unattended BI refreshes use a dedicated automation identity with only the read and execute permissions it needs.
Two operations make up the API:
| Operation | Permission | Purpose |
|---|---|---|
semSqlCatalog(modelId) | read semanticfed-model | Schema discovery: the model's tables, columns, data types and primary keys |
semExecuteSql(input) | execute semanticfed-query | Run one SELECT and return the governed rows synchronously |
modelId is optional when the workspace has exactly one semantic model.
Run a statement
curl -s -X POST https://graphqlworkspaces.burdenoff.com/workspaces/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $BURDENOFF_TOKEN" \
-H "x-workspace-id: $WORKSPACE_ID" \
-d @- <<'JSON'
{
"query": "mutation Sql($input: SemExecuteSqlInput!) { semExecuteSql(input: $input) { columns { name dataType masked } rows rowCount truncated durationMs } }",
"variables": {
"input": {
"modelId": "<model id>",
"sql": "SELECT c.region, SUM(o.amount) AS revenue FROM orders o JOIN customers c ON o.customer_id = c.id WHERE o.status = 'paid' GROUP BY c.region ORDER BY revenue DESC LIMIT 10",
"maxRows": 1000
}
}
}
JSON
The response carries:
columns: output column name, source entity and field, aggregation,dataType(STRING,INTEGER,DECIMAL,BOOLEAN,DATE,TIMESTAMP,JSON,GEOorUNKNOWN), and whether a COLUMN policy masked it (masked,mask).rows: one JSON object per row, keyed by column name, in result order.rowCount,truncated(true when more rows matched than were returned),durationMs,sourcesTouchedandplanSummary(the execution plan, push-down and policy decisions).
Discover the schema
query Catalog($modelId: ID) {
semSqlCatalog(modelId: $modelId) {
modelName
tables {
name
kind
primaryKey
columns { name kind dataType aggregation isPrimary description }
}
}
}
Supported SQL
| Feature | Notes |
|---|---|
SELECT [DISTINCT] ... | Columns, table.column, *, table.*, column aliases (AS optional) |
| Aggregates | SUM, AVG, MIN, MAX, COUNT(*), COUNT(col), COUNT(DISTINCT col) |
FROM / JOIN | One or more entities, [INNER] JOIN ... ON a.x = b.y, table aliases |
WHERE | column op literal comparisons (=, <>, !=, <, <=, >, >=) against string, number or boolean literals, combined with AND |
GROUP BY | Columns or select-list positions (GROUP BY 1) |
ORDER BY | Output columns, aliases or positions, ASC / DESC (NULLs last ascending, first descending; NULLS FIRST/LAST is not supported) |
LIMIT / OFFSET | Non-negative integer literals |
| Lexical | Double-quoted or backticked identifiers, schema.table names (the schema is ignored), single-quoted strings, -- and /* */ comments, one trailing ; |
Not supported yet: OR, IN, LIKE, IS [NOT] NULL, column-to-column comparisons, subqueries, CTEs (WITH), UNION, HAVING, window functions and other functions, outer joins, comma joins, self-joins, and expressions in the select list, GROUP BY or ORDER BY. These are rejected with a clear BAD_USER_INPUT error naming the unsupported feature, never silently ignored.
Limits
| Limit | Default | Bound |
|---|---|---|
maxRows (rows returned) | 1,000 | 1 to 10,000 |
timeoutMs (execution time) | 30,000 ms | 1,000 to 60,000 ms |
| Response size | 5 MiB | truncated is set when the cap is reached |
| Concurrent statements per workspace | 4 per service replica | Further statements get TOO_MANY_REQUESTS |
Page through large results with ORDER BY ... LIMIT n OFFSET m.
Errors
| Code | Meaning |
|---|---|
BAD_USER_INPUT | Syntax error (with the character position), a write or multiple statements, unknown table or column, unsupported SQL, or no model chosen |
FORBIDDEN | An access policy denies the statement (for example an ENTITY DENY) |
TIMEOUT | The statement exceeded timeoutMs |
TOO_MANY_REQUESTS | The workspace already has the maximum number of statements executing |
SOURCE_QUERY_FAILED | A data source rejected the pushed-down query; the message is the source's own |
Every execution is recorded in the workspace activity log with a SHA-256 hash of the statement (not its text), the row count, duration, entities touched and the policy decisions.
Connecting tools
Power BI and Excel (Power Query)
Power Query can call the HTTP SQL API directly. In Power BI choose Get Data → Blank Query, open the Advanced Editor and paste:
let
Url = "https://graphqlworkspaces.burdenoff.com/workspaces/graphql",
Sql = "SELECT region, SUM(amount) AS revenue FROM orders GROUP BY region",
Body = Json.FromValue([
query = "mutation Sql($input: SemExecuteSqlInput!) { semExecuteSql(input: $input) { rows truncated } }",
variables = [ input = [ modelId = "<model id>", sql = Sql, maxRows = 10000 ] ]
]),
Response = Json.Document(Web.Contents(Url, [
Headers = [
#"Content-Type" = "application/json",
Authorization = "Bearer " & Token,
#"x-workspace-id" = "<workspace id>"
],
Content = Body
])),
Rows = Response[data][semExecuteSql][rows],
Result = Table.FromRecords(Rows)
in
Result
Define Token as a Power Query parameter rather than typing it into the query, and set the data source privacy level to Organizational. The same query works in Excel under Data → Get Data → From Other Sources → Blank Query.
Grafana
Install the Infinity data source plugin, add a data source pointing at the gateway URL with the Authorization and x-workspace-id headers, and create a panel query of type JSON with method POST, the body shown in the curl example above, and root selector data.semExecuteSql.rows.
Python notebooks and pandas
import httpx
import pandas as pd
GATEWAY = "https://graphqlworkspaces.burdenoff.com/workspaces/graphql"
EXECUTE = "mutation Sql($input: SemExecuteSqlInput!) { semExecuteSql(input: $input) { rows truncated } }"
def sql(statement: str, model_id: str, max_rows: int = 10000) -> pd.DataFrame:
response = httpx.post(
GATEWAY,
json={"query": EXECUTE, "variables": {"input": {"sql": statement, "modelId": model_id, "maxRows": max_rows}}},
headers={"Authorization": f"Bearer {token}", "x-workspace-id": workspace_id},
timeout=90,
)
payload = response.json()
if payload.get("errors"):
raise RuntimeError(payload["errors"][0]["message"])
return pd.DataFrame(payload["data"]["semExecuteSql"]["rows"])
df = sql("SELECT region, SUM(amount) AS revenue FROM orders GROUP BY region ORDER BY revenue DESC", model_id)
Tableau, Metabase, Looker Studio and other JDBC/ODBC-only tools
A native JDBC/ODBC driver is on the roadmap. Until then, connect these tools through a small scheduled bridge that runs the governed SQL and lands the result where the tool already connects:
- Run the
sql()helper above on a schedule (cron, Airflow, a GitHub Actions workflow) with an automation identity whose access policies match what the dashboard's audience may see. - Write the DataFrame to a store the tool reads natively, for example
df.to_parquet(...)for Tableau's Parquet connector or a DuckDB file for Metabase's DuckDB driver, ordf.to_sql(...)into a reporting schema. - Point the tool at that store and set its refresh to follow the schedule.
Because the bridge runs as a fixed identity, the landed data reflects that identity's policies, not each viewer's. Use per-audience bridge identities (or the live Power Query / HTTP integrations above) when row- or column-level policies must follow the individual viewer.