Skip to main content

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​

SurfaceStatus
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 bridgesAvailable 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_ENVURL
prod (default)https://graphqlworkspaces.burdenoff.com/workspaces/graphql
alphahttps://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:

OperationPermissionPurpose
semSqlCatalog(modelId)read semanticfed-modelSchema discovery: the model's tables, columns, data types and primary keys
semExecuteSql(input)execute semanticfed-queryRun 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, GEO or UNKNOWN), 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, sourcesTouched and planSummary (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​

FeatureNotes
SELECT [DISTINCT] ...Columns, table.column, *, table.*, column aliases (AS optional)
AggregatesSUM, AVG, MIN, MAX, COUNT(*), COUNT(col), COUNT(DISTINCT col)
FROM / JOINOne or more entities, [INNER] JOIN ... ON a.x = b.y, table aliases
WHEREcolumn op literal comparisons (=, <>, !=, <, <=, >, >=) against string, number or boolean literals, combined with AND
GROUP BYColumns or select-list positions (GROUP BY 1)
ORDER BYOutput columns, aliases or positions, ASC / DESC (NULLs last ascending, first descending; NULLS FIRST/LAST is not supported)
LIMIT / OFFSETNon-negative integer literals
LexicalDouble-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​

LimitDefaultBound
maxRows (rows returned)1,0001 to 10,000
timeoutMs (execution time)30,000 ms1,000 to 60,000 ms
Response size5 MiBtruncated is set when the cap is reached
Concurrent statements per workspace4 per service replicaFurther statements get TOO_MANY_REQUESTS

Page through large results with ORDER BY ... LIMIT n OFFSET m.

Errors​

CodeMeaning
BAD_USER_INPUTSyntax error (with the character position), a write or multiple statements, unknown table or column, unsupported SQL, or no model chosen
FORBIDDENAn access policy denies the statement (for example an ENTITY DENY)
TIMEOUTThe statement exceeded timeoutMs
TOO_MANY_REQUESTSThe workspace already has the maximum number of statements executing
SOURCE_QUERY_FAILEDA 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:

  1. 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.
  2. 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, or df.to_sql(...) into a reporting schema.
  3. 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.

Next steps​