The query language built for AI agents - deterministic queries across any database with built-in privacy
Project description
NexaQL
The query language built for AI agents. One unified syntax to deterministically query, join, and aggregate data across any database - with built-in privacy enforcement.
NexaQL is not a GraphQL wrapper. It's a standalone query engine that translates natural language questions into a single, deterministic query representation, then compiles that into the native dialect of whatever database holds the data - PostgreSQL, MySQL, DuckDB, Snowflake, BigQuery, or any SQL engine. Data living across different databases? NexaQL joins it in memory. Sensitive fields? The policy engine strips or masks them before results leave the server.
"Which customers spent the most last quarter?"
↓ Agent (deterministic)
NexaQL query
↓ Translator
Native SQL dialect (PostgreSQL, MySQL, DuckDB, ...)
↓ Adapter
Execute → Results
↓ Policy enforcer
Stripped / masked per user role
Why NexaQL?
| Problem | How NexaQL solves it |
|---|---|
| Agents generate unreliable SQL | NexaQL is a constrained, deterministic intermediate language - the agent generates NexaQL (not raw SQL), and the engine compiles it to correct, optimized SQL |
| Data scattered across databases | One query can traverse and join data from PostgreSQL, MySQL, DuckDB in a single request |
| No access control on agent queries | Built-in RBAC, field-level security, row-level security, and PII masking - enforced at the engine level, invisible to the agent |
| Schema drift breaks queries | Ontology-driven validation catches errors before execution, not after |
| Every database has different SQL | NexaQL compiles to 8 SQL dialects from one syntax |
Quick Start
pip install nexaql
nexaql install
nexaql serve
Open http://localhost:3717 — a playground with sample e-commerce data loads instantly. No external database needed.
Connect Your Own Database
# Add a PostgreSQL connector via the Admin panel or API
curl -X POST localhost:3717/api/connectors \
-H 'Content-Type: application/json' \
-d '{"name":"mydb","type":"postgresql","config":{"url":"postgresql://user:pass@localhost:5432/mydb"}}'
# Generate an ontology from it
curl -X POST localhost:3717/api/connectors/generate-ontology \
-d '{"connector_name":"mydb","domain":"my_domain","output_schema_name":"main"}'
NexaQL introspects your database schema, detects relationships, enums, and PII fields, then generates a full ontology with default roles and access policies - ready to query.
How It Works
1. Write a query (or let an agent generate one)
{
order(status: "DELIVERED") @orderby(ordered_at, DESC) @limit(10) {
id
ordered_at
total_amount
customer {
name
email
}
items {
quantity
unit_price
product {
name
}
}
}
}
This single query traverses 4 tables (orders → customers, orders → order_items → products) with automatic join resolution. No manual JOINs, no subqueries, no dialect-specific syntax.
2. The engine translates it
NexaQL compiles the query into the native SQL dialect of your database:
SELECT o0.id, o0.ordered_at, o0.total_amount,
c1.name, c1.email,
oi2.quantity, oi2.unit_price,
p3.name
FROM orders o0
JOIN customers c1 ON c1.id = o0.customer_id
LEFT JOIN order_items oi2 ON oi2.order_id = o0.id
JOIN products p3 ON p3.id = oi2.product_id
WHERE o0.status = 'DELIVERED'
ORDER BY o0.ordered_at DESC
LIMIT 10
3. Privacy policies are enforced automatically
If the user is an analyst, the engine silently strips sensitive fields before translation:
# In the ontology:
customer:
visible_to: [analyst, manager, admin]
fields:
email:
visible_to: [manager, admin] # analysts can't see emails
pii: true
mask_with: email # managers see "a***@example.com"
lifetime_value:
visible_to: [manager, admin] # analysts can't see spend data
The analyst's query returns name but not email or lifetime_value. The manager sees all fields but with masked emails. The admin sees everything. The agent doesn't need to know about access control - the engine handles it.
Features
Full grammar reference with 20+ examples: docs/grammar.md
Query Language
- Filters - equality, comparison (
gt,lt,gte,lte),like,in,not_in, null checks - Edge traversals - traverse relationships across tables with automatic join resolution
- Aggregations -
sum(),avg(),min(),max(),count()with automatic GROUP BY - Computed fields -
calc(quantity * unit_price)with cross-entity references - Directives -
@limit,@offset,@orderby,@distinct - Special filters - named, reusable WHERE clauses defined in the ontology
Privacy & Access Control
- Node-level RBAC - control which roles can access which tables
- Field-level security - strip sensitive columns based on user role
- Row-level security (RLS) - auto-inject WHERE clauses based on user attributes (region, department, etc.)
- PII masking - mask emails, phones, names in query results (5 strategies)
- Policy-in-ontology - access rules live alongside schema definitions
Agent Integration
- MCP server - Model Context Protocol connector for Claude Desktop, Cursor, and any MCP-compatible AI agent
- 11 tools + 3 resources - ask, query, validate, describe ontology, SQL preview, auth configuration, and more
- Deterministic translation - natural language → NexaQL → SQL, no hallucinated queries
- Auto-retry - if the generated query fails validation, the agent corrects it
- Result summarization - agent generates a natural language summary of query results
- Ontology-aware prompts - the agent knows your schema, field types, and available filters
- JWT authentication - tamper-proof user identity for production deployments
Multi-Database
- 8 SQL dialects - PostgreSQL, MySQL, DuckDB, Snowflake, BigQuery, Presto, Spark, MSSQL
- Cross-source joins - data from different databases joined in memory via DuckDB
- Pluggable adapters - add new databases by implementing a simple interface
Admin Panel
- Domain management - create, switch, and delete domains from the UI
- Schema management - add schemas from connected databases, regenerate, or delete
- Ontology generation - introspect any connected database and auto-generate ontology with nodes, edges, enums, and PII detection
- Duplicate prevention - unique schema names per domain; add rejects duplicates, regenerate upserts
- Default roles & policies - every generated ontology bootstraps with admin/analyst/manager roles and common access functions (owns_record, same_department, same_region)
- API Keys - manage LLM provider keys (Anthropic, OpenAI, OpenRouter, Google); auto-detects active provider from saved keys
- Connector registry - add/remove PostgreSQL, MySQL, DuckDB connections
BYOLLM (Bring Your Own LLM)
- No bundled LLM - purely BYOLLM; bring Anthropic, OpenAI, OpenRouter, or Google keys
- Auto-configure - saving an API key automatically switches the active provider and model
- Provider priority - Anthropic → OpenAI → OpenRouter → Google
Developer Experience
- Playground UI - Monaco editor with syntax highlighting, schema explorer, SQL preview
- Role switcher - test access control live in the playground
- CLI -
nexaql install,nexaql serve,nexaql query - Zero config - ships with sample data, works after
nexaql install
Query Syntax Reference
Filters
# Equality
order(status: "DELIVERED")
# Comparison operators (object style)
order(total_amount: { gt: 100, lt: 1000 })
# Suffix shorthand
order(total_amount_gt: 100)
# Special filters (defined in ontology)
order(large_order: 500)
# Calc filters - computed conditions across entities
order_item(calc(quantity * unit_price): { gt: 500 })
Edge Traversals
{
customer {
name
orders { # edge → order table (auto-joined)
total_amount
items { # edge → order_item table
quantity
product { # edge → product table
name
price
}
}
}
}
}
Aggregations & Computed Fields
{
order {
customer_id
total_orders: count()
total_revenue: sum(total_amount)
avg_order: avg(total_amount)
}
}
{
order_item {
quantity
unit_price
line_total: calc(quantity * unit_price)
}
}
Access Control
NexaQL enforces privacy at the query engine level - RBAC, field-level security, row-level security, and PII masking.
Full access control guide: docs/access-control.md
Role Registry
Define valid roles in the ontology. All access policies validate against this list:
roles:
admin:
description: "Full access to all data"
manager:
description: "Regional data access with all fields"
analyst:
description: "Read access without PII or financial data"
Policy Functions
Reusable, named access policies for row-level security:
access_functions:
same_team:
description: "Records created by users on the same team"
sql: "{field} IN (SELECT user_id FROM employees WHERE team_id = '{user.team_id}')"
requires: ["user.team_id"]
nodes:
customer:
visible_to: [analyst, manager, admin]
row_policies:
- function: same_team # reference a named policy function
field: created_by # which column it applies to
roles: [manager]
except_roles: [admin]
fields:
email:
visible_to: [manager, admin]
pii: true
mask_with: email
Admin UI
Click the ⚙ gear icon in the playground header to manage:
- Domains & Schemas - organize ontologies by domain, add/delete schemas, switch active domain
- Connectors - connect databases and generate ontologies from them
- API Keys - configure LLM provider credentials
- Roles - define valid role names
- Policy Functions - create reusable access policies
- Per-node access - visible_to, row policies, field-level PII/masking
User Context
The calling application sends user identity via the X-User-Context header:
curl -H 'X-User-Context: {"user_id":"alice","roles":["manager"],"region":"US-EAST","team_id":"eng-platform"}' \
-X POST localhost:3717/api/execute \
-d '{"query": "{ customer @limit(5) { name email } }"}'
Standard user context fields: user_id, name, email, manager_id, region, department, team_id, level, job_role, org_id. Custom attributes are also supported.
MCP Server (AI Agent Integration)
NexaQL ships with a built-in Model Context Protocol server, so any MCP-compatible AI agent (Claude Desktop, Cursor, custom agents) can query your data with full access control enforcement.
Start the MCP Server
# stdio transport (for Claude Desktop / Cursor)
nexaql mcp
# HTTP transport (for remote agents)
nexaql mcp --transport streamable-http --port 8080
Claude Desktop Configuration
Add to your claude_desktop_config.json:
{
"mcpServers": {
"nexaql": {
"command": "nexaql",
"args": ["mcp"]
}
}
}
Tools
| Tool | Description |
|---|---|
ask |
Natural language question → NexaQL → SQL → results with summary |
query |
Execute a NexaQL query directly |
validate_query |
Parse and validate a query without executing it |
sql_preview |
Translate NexaQL to native SQL without executing |
list_domains |
List all available domains and the active domain |
switch_domain |
Switch the active domain |
describe_node |
Describe a node's fields, edges, types, and PII flags |
describe_ontology |
Full ontology overview — all nodes, fields, edges |
list_connectors |
List configured database connectors |
user_context_schema |
Discover supported user context fields, roles, and RLS attributes |
configure_auth |
Switch between dev and jwt auth modes |
Resources
| URI | Description |
|---|---|
nexaql://ontology |
Full ontology as JSON |
nexaql://grammar |
NexaQL query syntax reference |
nexaql://roles |
Defined roles with permissions and field restrictions |
Authentication
In dev mode (default), agents pass user identity as a raw dict:
result = await ask(
question="top customers by revenue",
user_context={"user_id": "alice", "roles": ["analyst"], "region": "US-EAST"}
)
In jwt mode (production), agents pass a signed JWT token. This prevents impersonation — the server verifies the signature before extracting user claims:
# Configure JWT mode (one-time setup)
await configure_auth(auth_mode="jwt", auth_secret="your-secret-key")
# Agents pass signed tokens
token = jwt.encode({"sub": "alice", "roles": ["analyst"]}, "your-secret-key", algorithm="HS256")
result = await ask(question="top customers", auth_token=token)
Access control (RBAC, field stripping, RLS, PII masking) is enforced on every query — including agent chat responses. An analyst asking "show me customer emails" gets results with the email column stripped automatically.
Configuration
All configuration lives in the bootstrap database (~/.nexaql/nexaql.db) and is managed through the Admin UI or API. No config files required.
# Add your LLM API key (enables agent chat)
curl -X POST localhost:3717/api/api-keys \
-H 'Content-Type: application/json' \
-d '{"provider":"anthropic","name":"anthropic","key":"sk-ant-..."}'
# Connect a database
curl -X POST localhost:3717/api/connectors \
-H 'Content-Type: application/json' \
-d '{"name":"prod","type":"postgresql","config":{"url":"postgresql://..."}}'
For advanced/legacy setups, a nexaql.yaml config file is also supported:
ontology:
path: ./ontologies/my_schema.yaml
datasources:
default:
type: duckdb
path: ":memory:"
seed_file: ./ontologies/sample_seed.sql
server:
host: 0.0.0.0
port: 3717
Ontology
The ontology is the single source of truth for your data graph. It defines what NexaQL can query, how tables relate, and who can see what:
version: "1"
domain: ecommerce
nodes:
customer:
table: customers
primary_key: id
visible_to: [analyst, manager, admin]
fields:
id: { type: integer, filterable: true }
name: { type: string, filterable: true }
email: { type: string, filterable: true, visible_to: [manager, admin], pii: true, mask_with: email }
edges:
orders:
node: order
join_steps:
- table: orders
alias_key: order
condition: "{order}.customer_id = {customer}.id"
special_filters:
active:
sql: "{customer}.status = 'ACTIVE'"
Architecture
┌─────────────┐
Natural Language │ Agent Chat │ "Which customers spent the most?"
└──────┬──────┘
↓
┌─────────────┐
NexaQL Query │ Parser │ { customer @limit(10) { name, total: sum(amount) } }
└──────┬──────┘
↓
┌─────────────┐
Access Control │ Enforcer │ Strip fields, inject RLS, flag PII masking
└──────┬──────┘
↓
┌─────────────┐
Validation │ Validator │ Check against ontology schema
└──────┬──────┘
↓
┌─────────────┐
Native SQL │ Translator │ SELECT ... FROM ... JOIN ... WHERE ...
└──────┬──────┘
↓
┌─────────────┐
Execution │ Adapter │ PostgreSQL / MySQL / DuckDB / ...
└──────┬──────┘
↓
┌─────────────┐
Post-processing │ Masker │ PII masking on result rows
└─────────────┘
nexaql/
├── src/nexaql/
│ ├── engine/ # Pure query engine (zero external deps)
│ │ ├── lexer.py # Tokenizer
│ │ ├── parser.py # Recursive descent parser → AST
│ │ ├── validator.py # AST validation against ontology
│ │ ├── translator.py # AST → SQL with join resolution
│ │ └── dialect.py # 8 SQL dialect translators
│ ├── policy/ # RBAC, field security, RLS, PII masking
│ ├── ontology/ # YAML schema + access policy loader
│ ├── adapters/ # PostgreSQL, DuckDB (+ pluggable base)
│ ├── api/ # FastAPI server + admin panel routes
│ ├── chat/ # NL → NexaQL agent pipeline
│ ├── mcp_server.py # MCP server (11 tools, 3 resources)
│ ├── auth.py # JWT authentication & user context resolution
│ ├── bootstrap.py # DuckDB-backed state (domains, schemas, connectors, keys)
│ └── cli.py # CLI: install, serve, query, mcp
├── frontend/ # React + Tailwind admin panel & playground
└── ontologies/ # Sample schema + seed data
Development
git clone https://github.com/karthikr004/nexaql
cd nexaql
pip install -e ".[dev]"
nexaql serve --reload
License
Apache 2.0
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file nexaql-0.8.3.tar.gz.
File metadata
- Download URL: nexaql-0.8.3.tar.gz
- Upload date:
- Size: 424.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
30e9237c91e5f776fc4907b6b24bf5ccef7e30a4a5c38b541ead3d2328b6441b
|
|
| MD5 |
c7d3341e099bdc1850d8e3f30ed65fb3
|
|
| BLAKE2b-256 |
625e9aa852fc2d9431f3961aacec90a3a279293837806cc7f3c01382331e7849
|
File details
Details for the file nexaql-0.8.3-py3-none-any.whl.
File metadata
- Download URL: nexaql-0.8.3-py3-none-any.whl
- Upload date:
- Size: 318.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
965f3410b8a3c00b9e5edda94f2d60ebf56219806f502c35c325419f6e4e69a1
|
|
| MD5 |
ea4ea08d8e7d0915ab0556a5e7932cd0
|
|
| BLAKE2b-256 |
04a5676bece62a62f1b34e35e8e7d18a50c8b8bf076ce8fec11cad6bbfe91584
|