Skip to main content

Schema intelligence layer for LLMs — enriched database context, not SQL generation.

Project description

SQLens

Schema intelligence layer for LLMs.
Enriched database context.

Python 3.10+ version MIT License

Better SQL starts with better context, not a better prompt.

SQLens introspects your database schema, enriches it with descriptions, statistics, inferred relationships, sample data, and business domain tags, then retrieves only the relevant tables for any natural language query — formatted and optimized for an LLM's context window.

Why?

Most NL-to-SQL tools fail not because of the LLM, but because of the garbage context they feed it. A raw DDL dump tells a model nothing about what usr_acct_bal_dt means, which tables relate to each other implicitly, or what values country_code actually contains.

SQLens solves the context problem and stays out of the generation problem. It's the R+A in RAG — retrieval and augmentation — without the G. You bring your own LLM.


Contents


Install

pip install sqlens                   # core — keyword retrieval only
pip install sqlens[bigquery]         # + BigQuery connector
pip install sqlens[postgresql]       # + PostgreSQL connector
pip install sqlens[numpy]            # + cosine similarity retrieval
pip install sqlens[vector]           # + chromadb vector search
pip install sqlens[all]              # everything

Quick start

from sqlens import SQLens

# 1. Connect and introspect
ctx = SQLens.from_bigquery(project="my-project", dataset="analytics")
# ctx = SQLens.from_postgresql("postgresql://user:pass@localhost:5432/mydb")

# 2. Enrich once, save to disk
ctx.enrich(
    descriptions=True,   # human-readable column and table descriptions
    stats=True,          # cardinality, null %, min/max, top values
    relations=True,      # infer implicit foreign keys
    samples=3,           # representative rows per table
    domains=True,        # auto-tag tables by business domain
)
ctx.save("./catalog.json")

# 3. Load and query
ctx = SQLens.load("./catalog.json")

context = ctx.get_context(
    "monthly active users by country",
    max_tables=5,
    level="standard",    # compact | standard | full
    domain="auto",       # classify query → filter by domain → retrieve
)

# 4. Feed to any LLM
print(context.to_prompt())  # optimized text block
print(context.to_dict())    # structured dict / JSON

How it works

┌─────────────────────────────────────────────────────────┐
│  Database  (BigQuery · PostgreSQL · custom connector)    │
└───────────────────────────┬─────────────────────────────┘
                            │
                            ▼
              ┌─────────────────────────┐
              │     Introspection       │
              │  tables · columns ·     │
              │  types · PKs · FKs      │
              │  PK heuristic fallback  │
              └────────────┬────────────┘
                           │
                           ▼
        ┌──────────────────────────────────────┐
        │          Enrichment Pipeline          │
        │                                       │
        │  descriptions  rule-based + LLM       │
        │  stats         cardinality · nulls    │
        │  relations     inferred foreign keys  │
        │  samples       representative rows    │
        │  domains       business domain tags   │
        └──────────────────┬───────────────────┘
                           │
                           ▼
              ┌─────────────────────────┐
              │   Catalog  (JSON)       │
              │   fingerprinted ·       │
              │   incremental cache     │
              └────────────┬────────────┘
                           │
              ┌────────────▼────────────┐
              │     Domain Filter       │  optional
              │   83 tables → ~12       │
              └────────────┬────────────┘
                           │
        ┌──────────────────▼───────────────────┐
        │       Retrieval  (auto-detected)      │
        │                                       │
        │  keyword   TF-IDF · zero deps         │
        │  cosine    numpy + sentence-transformers│
        │  vector    chromadb / lancedb         │
        └──────────────────┬───────────────────┘
                           │
                           ▼
              ┌─────────────────────────┐
              │    Context Output       │
              │  .to_dict()             │
              │  .to_prompt()           │
              └─────────────────────────┘

Enrichment

Each enricher is optional and composable. Run them once, save the catalog, reuse forever.

Descriptions

Rule-based heuristics expand abbreviations and detect naming patterns automatically:

Input Output
usr_acct_bal_dt "user account balance date"
is_active "boolean flag for active status"
created_at "creation timestamp"
order_id "foreign key reference to orders"

For higher quality, pass any LLM as a callable — it fills in what the rules miss:

# Anthropic Claude
import anthropic
client = anthropic.Anthropic()

ctx.enrich(
    descriptions=lambda prompt: client.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=200,
        messages=[{"role": "user", "content": prompt}],
    ).content[0].text
)

# Google Gemini
import google.generativeai as genai
genai.configure(api_key="YOUR_KEY")
model = genai.GenerativeModel("gemini-2.0-flash")

ctx.enrich(descriptions=lambda prompt: model.generate_content(prompt).text)

Stats

Column-level statistics collected per table. Dialect-aware: each connector runs native SQL (COUNT(DISTINCT …) on PostgreSQL, APPROX_COUNT_DISTINCT on BigQuery).

ctx.enrich(stats=True)
# → cardinality, null_pct, min, max per column
# → top_values for string / low-cardinality columns

Relations

Infers implicit foreign keys from naming patterns — finds joins that don't exist in INFORMATION_SCHEMA:

ctx.enrich(relations=True)
# "user_id" in "orders" → inferred FK to "users.id"  (confidence: 0.9)
# "product_id" in "order_items" → inferred FK to "products.id"  (confidence: 0.9)

Samples

Stratified representative rows per table:

ctx.enrich(samples=3)

Domains

Auto-tags tables using name patterns, column signatures, and FK propagation. Manual overrides always win:

ctx.enrich(domains=True)
# orders, payments       → ["sales", "finance"]
# users, accounts        → ["users"]
# campaign_clicks        → ["marketing"]
# audit_log              → ["ops"]

# Manual overrides
ctx.set_domain("orders", ["sales", "finance"])
ctx.set_domain("dim_date", ["finance", "ops"])

Retrieval

Auto-detect cascade

SQLens picks the best available retriever at runtime — no configuration required:

chromadb / lancedb installed?  →  VectorDBRetriever    (ANN semantic search)
numpy installed?               →  NumpyCosineRetriever (cosine similarity)
fallback                       →  KeywordRetriever     (TF-IDF, zero deps)

With sentence-transformers installed alongside numpy, the cosine retriever uses a real semantic embedding model. The model loads once per SQLens instance and is cached — repeated get_context() calls pay zero reload cost.

pip install sqlens[numpy]                          # cosine with hash embeddings
pip install "sqlens[numpy]" sentence-transformers  # cosine with semantic model
pip install sqlens[vector]                         # full vector DB

Force a specific retriever

ctx.get_context(query, retrieval="keyword")
ctx.get_context(query, retrieval="cosine")
ctx.get_context(query, retrieval="vector")

Inspect retrieval metadata

context = ctx.get_context("monthly active users by country", domain="auto")

context.metadata["retrieval_method"]            # "cosine"
context.metadata["domain_filter_applied"]       # "users"
context.metadata["total_tables_in_catalog"]     # 83
context.metadata["tables_after_domain_filter"]  # 11
context.metadata["tables_included"]             # 5

Domain-scoped retrieval

On large schemas, vector search across all tables introduces noise. A query about "sales in Ecuador" might surface audit_log (which has an amount column) ahead of orders. Domain filtering narrows the candidate set before retrieval runs.

# Explicit domain
ctx.get_context("revenue Q1", domain="sales")

# Auto-detect from query — keyword classifier + optional LLM tier
# Supports English and Spanish out of the box
ctx.get_context("ventas en Ecuador último trimestre", domain="auto")
# → "ventas" matches "sales" → 83 tables filtered to 12 → cosine on 12

# No filter (default, backwards compatible)
ctx.get_context("revenue Q1")

Impact by schema size:

Schema size Without domain filter With domain filter
< 30 tables Minimal difference Not needed
30–100 tables Some noise ~70–80% smaller search space
100–500 tables Significant precision loss Critical
500+ tables Vector search struggles Essential

Output levels

Three levels control how much metadata is included:

Level Includes
compact Table names, columns, types, explicit FKs
standard + descriptions, stats (cardinality/nulls), inferred FKs, 2 sample rows
full + all stats, all samples, all inferred FKs with confidence scores
context = ctx.get_context(query, level="standard")  # default
context.to_prompt()   # text block ready to paste into a system prompt
context.to_dict()     # structured dict for programmatic use

Example .to_prompt() output (standard level):

DATABASE SCHEMA CONTEXT
Source: bigquery://my-project.analytics
Tables included: 3 of 83  ·  query: "monthly active users by country"

TABLE: users
Description: Core user accounts with registration and profile data
Row count: 1,240,000  ·  Domains: sales, users

Columns:
  - id             STRING  PK  NOT NULL   Unique user identifier  [cardinality: 1,240,000]
  - country_code   STRING  NULLABLE       ISO 3166-1 alpha-2 country code  [cardinality: 89, 2% nulls]
  - last_active_at TIMESTAMP  NULLABLE    Last login or API activity  [5% nulls · 2023-01-01 → 2026-03-18]

Relationships:
  - users.id → events.user_id  (inferred · confidence 0.95)

Sample rows:
  | id      | country_code | last_active_at           |
  | abc-123 | EC           | 2026-03-17T10:30:00Z     |
  | def-456 | US           | 2026-03-10T08:15:00Z     |

Primary key inference

Some databases (BigQuery, Redshift) don't enforce or expose primary key constraints. sqlens infers them automatically so the relation inferrer and context output still work correctly.

Three rules, applied in order — first match wins:

Rule Pattern Example
1 Column named exactly id id in any table
2 Column named {singular_table}_id user_id in users
3 First NOT NULL column with _id suffix account_id if rules 1–2 don't match

Inferred keys are marked pk_source: "inferred" in the catalog metadata, distinguishable from "database" (declared constraints).


Incremental updates

SQLens SHA-256 fingerprints each table's structure (column names, types, PKs, FKs). On re-introspection, only tables where the fingerprint changed are re-enriched — unchanged tables keep their existing metadata.

ctx = SQLens.load("./catalog.json")
ctx.set_connector(BigQueryConnector(...))
ctx.refresh()   # re-enriches only changed or new tables
ctx.save("./catalog.json")

CLI

# Introspect a database and save the catalog
sqlens inspect --bigquery my-project.analytics -o catalog.json
sqlens inspect --postgresql "postgresql://user:pass@host/db" -o catalog.json
sqlens inspect --postgresql "..." --schema reporting -o catalog.json

# Enrich (any combination of flags)
sqlens enrich catalog.json --descriptions --stats --relations --samples 3 --domains

# Retrieve context for a natural language query
sqlens context catalog.json "monthly active users by country"
sqlens context catalog.json "revenue by region" --domain auto --level full --max-tables 8
sqlens context catalog.json "orders last week" --json     # structured JSON output

Add -v / --verbose to any command for diagnostic output.


Catalog inspection

ctx = SQLens.load("./catalog.json")

ctx.table_count                  # 83
ctx.tables                       # ["users", "orders", "order_items", ...]
ctx.enrichers_applied            # ["descriptions", "stats", "relations", "samples", "domains"]
ctx.domains                      # ["sales", "users", "marketing", "finance", "ops"]
ctx.tables_in_domain("sales")    # ["orders", "order_items", "payments", ...]
ctx.fingerprint("users")         # "a3f8c2d1..."

# Full catalog output without query filtering
ctx.to_dict(level="compact")
ctx.to_prompt(level="full")

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

sqlens-0.6.1.tar.gz (59.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

sqlens-0.6.1-py3-none-any.whl (49.8 kB view details)

Uploaded Python 3

File details

Details for the file sqlens-0.6.1.tar.gz.

File metadata

  • Download URL: sqlens-0.6.1.tar.gz
  • Upload date:
  • Size: 59.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.0

File hashes

Hashes for sqlens-0.6.1.tar.gz
Algorithm Hash digest
SHA256 cfa7a5dfdd40e441ec0820729307a78c7a08da927a3ff1d6d20c747e9b30917a
MD5 4f83fbbbb11fd5d5853df7f8eb3d2457
BLAKE2b-256 b164bd4adc72a664cbbc0f14998db11a37f4adc981684eba04ea4b3bea8a8d7b

See more details on using hashes here.

File details

Details for the file sqlens-0.6.1-py3-none-any.whl.

File metadata

  • Download URL: sqlens-0.6.1-py3-none-any.whl
  • Upload date:
  • Size: 49.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.0

File hashes

Hashes for sqlens-0.6.1-py3-none-any.whl
Algorithm Hash digest
SHA256 7743df7fa6858c2ea3c39d4fa5ca202163cfa0ca11f95d3735a27685d784c5cd
MD5 38398d7f3553eb51ab4e212a8c2c306e
BLAKE2b-256 a0266a0b62cf2794b156bd660864d4b11ae49ca587c592aa922514014201df9f

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page