
Autorag Query
- 1 installs
- 147 repo stars
- Updated July 13, 2026
- nomadamas/autorag-research
Helps with ai & agent building tasks.
About
autorag-query is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- autorag-query
- AI & Agent Building
- AI-coding skill
Autorag Query by the numbers
- 1 all-time installs (skills.sh)
- Ranked #14,102 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/nomadamas/autorag-research --skill autorag-queryAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 147 |
| Last updated | July 13, 2026 |
| Repository | nomadamas/autorag-research ↗ |
What it does
Helps with ai & agent building tasks.
Files
AutoRAG-Query: Text2SQL Agent Skill
Query AutoRAG pipeline results with natural language. Converts to SQL, executes safely, returns tables/JSON/CSV.
Quick Example
User: "Which pipeline has the best BLEU score?"
Agent: 1. Read references/schema.sql (understand tables) 2. Generate SQL:
SELECT p.name, s.metric_result
FROM summary s
JOIN pipeline p ON s.pipeline_id = p.id
JOIN metric m ON s.metric_id = m.id
WHERE m.name = 'bleu'
ORDER BY s.metric_result DESC LIMIT 1;3. Execute: uv run python .agents/skills/autorag-query/scripts/query_executor.py --query "..." 4. Present: "hybrid_search_v2 has best BLEU: 0.85"
Workflow
1. Parse intent: What data? (metrics/pipelines/queries) What operation? (rank/aggregate/filter) 2. Load schema: Read references/schema.sql - key tables:
summary: Aggregated pipeline metrics (best for rankings)evaluation_result: Per-query scores (detailed analysis)executor_result: Generation outputs withtoken_usageJSONBchunk_retrieved_result: Retrieval scores/ranks
3. Generate SQL following rules:
- ✅ SELECT-only, ⛔ Never: INSERT/UPDATE/DELETE/DROP/CREATE
- ⛔ Exclude vector columns:
embedding,embeddings,bm25_tokens(cause type errors) - Add
LIMIT 100if not specified - Use JOINs:
query_id → query.id,pipeline_id → pipeline.id,metric_id → metric.id - JSONB:
token_usage->>'field'(text) or(token_usage->>'field')::int(cast)
4. Execute: uv run python .agents/skills/autorag-query/scripts/query_executor.py --query "..." [--format json|csv|table] 5. Present: Summarize findings, show table, highlight insights
Key Tables
| Table | Purpose | Key Columns |
|---|---|---|
pipeline | Pipeline definitions | id, name, pipeline_type |
metric | Metric definitions | id, name, metric_type (retrieval/generation) |
query | Search queries | id, query, ground_truths, dataset_name |
executor_result | Generation outputs | query_id, pipeline_id, generation_result, token_usage (JSONB), execution_time |
evaluation_result | Per-query scores | query_id, pipeline_id, metric_id, metric_result |
summary | Aggregated metrics | pipeline_id, metric_id, metric_result |
chunk_retrieved_result | Retrieval outputs | query_id, pipeline_id, chunk_id, score, rank |
Relationships: query_id → query.id, pipeline_id → pipeline.id, metric_id → metric.id, chunk_id → chunk.id
Common Queries
See references/common-queries.md for 20+ templates.
Pipeline ranking:
SELECT p.name, s.metric_result
FROM summary s
JOIN pipeline p ON s.pipeline_id = p.id
JOIN metric m ON s.metric_id = m.id
WHERE m.name = 'bleu'
ORDER BY s.metric_result DESC;Token usage:
SELECT p.name,
SUM((exe.token_usage->>'total_tokens')::int) AS total_tokens,
AVG((exe.token_usage->>'total_tokens')::int) AS avg_per_query
FROM executor_result exe
JOIN pipeline p ON exe.pipeline_id = p.id
WHERE exe.token_usage IS NOT NULL
GROUP BY p.name
ORDER BY total_tokens DESC;Retrieval results:
SELECT c.content, crr.score, crr.rank
FROM chunk_retrieved_result crr
JOIN chunk c ON crr.chunk_id = c.id
WHERE crr.query_id = :query_id AND crr.pipeline_id = :pipeline_id
ORDER BY crr.rank LIMIT 10;JSONB Extraction
executor_result.token_usage:
{"prompt_tokens": 150, "completion_tokens": 50, "total_tokens": 200}Extract:
- Text:
token_usage->>'prompt_tokens'→"150" - Integer:
(token_usage->>'total_tokens')::int→200 - JSON:
token_usage->'embedding_tokens'→ preserves type
pipeline.config: config->>'model' → "gpt-4"
Critical Rules
1. ⛔ Always exclude: embedding, embeddings, bm25_tokens columns (cause type errors) 2. ✅ SELECT-only: Script validates and rejects DDL/DML 3. 📏 Add LIMIT: Prevent large result sets 4. 🔗 Use JOINs: Connect via foreign keys 5. ⚡ Timeout: 10s default (add WHERE filters if slow)
Script Usage
uv run python .agents/skills/autorag-query/scripts/query_executor.py \
--query "SELECT ..." \
--format table|json|csv \
--timeout 10 \
--limit 10000 \
--database autorag_research # optionalConnection: Auto-loads from configs/db.yaml or POSTGRES_* env vars using DBConnection class.
Output formats:
table: ASCII table (default)json: JSON arraycsv: CSV with headers
Row count: Printed to stderr: (N rows)
Error Handling
| Error | Cause | Fix |
|---|---|---|
| "Forbidden keyword" | Non-SELECT query | Use SELECT-only |
| "Vector type error" | Selected vector columns | Exclude embedding, embeddings, bm25_tokens from SELECT |
| "Query timeout" | Query too slow | Add WHERE/LIMIT |
| "Connection failed" | Missing credentials | Check configs/db.yaml or set env vars |
Advanced: Window Functions & Pivots
Ranking:
SELECT p.name, m.name, s.metric_result,
RANK() OVER (PARTITION BY m.name ORDER BY s.metric_result DESC) AS rank
FROM summary s
JOIN pipeline p ON s.pipeline_id = p.id
JOIN metric m ON s.metric_id = m.id;Pivot:
SELECT p.name,
MAX(CASE WHEN m.name = 'bleu' THEN s.metric_result END) AS bleu,
MAX(CASE WHEN m.name = 'rouge' THEN s.metric_result END) AS rouge
FROM summary s
JOIN pipeline p ON s.pipeline_id = p.id
JOIN metric m ON s.metric_id = m.id
GROUP BY p.name;References
- Schema:
references/schema.sql- full DB schema with comments - Templates:
references/common-queries.md- 20+ query examples - Executor:
scripts/query_executor.py- safe SQL execution script
Installation: Works from .agents/skills/autorag-query/ (auto-detected by agents).
Common Query Templates
This document provides curated SQL query templates for common analysis tasks in AutoRAG-Research.
IMPORTANT: Always exclude vector/embedding columns: embedding, embeddings, bm25_tokens
Parameterized Queries: Templates use :param_name syntax for safe value substitution. Pass parameters via --params '{"param_name": "value"}' to the query executor.
Pipeline Performance Comparison
Top pipelines by metric score
SELECT
p.name AS pipeline_name,
m.name AS metric_name,
s.metric_result,
s.created_at
FROM summary s
JOIN pipeline p ON s.pipeline_id = p.id
JOIN metric m ON s.metric_id = m.id
WHERE m.name = :metric_name
ORDER BY s.metric_result DESC
LIMIT :top_n;Parameters: :metric_name (e.g., 'bleu', 'retrieval_precision'), :top_n (e.g., 10)
Example output:
pipeline_name | metric_name | metric_result | created_at
--------------------|-------------|---------------|------------
hybrid_search_v2 | bleu | 0.85 | 2025-01-15
naive_rag | bleu | 0.72 | 2025-01-14---
Average metrics across all queries
SELECT
p.name AS pipeline_name,
m.name AS metric_name,
AVG(er.metric_result) AS avg_score,
COUNT(*) AS query_count
FROM evaluation_result er
JOIN pipeline p ON er.pipeline_id = p.id
JOIN metric m ON er.metric_id = m.id
GROUP BY p.name, m.name
ORDER BY avg_score DESC;Description: Shows average metric scores per pipeline across all queries.
---
Pipeline ranking by multiple metrics
SELECT
p.name AS pipeline_name,
MAX(CASE WHEN m.name = 'bleu' THEN s.metric_result END) AS bleu,
MAX(CASE WHEN m.name = 'retrieval_precision' THEN s.metric_result END) AS precision,
MAX(CASE WHEN m.name = 'retrieval_recall' THEN s.metric_result END) AS recall
FROM summary s
JOIN pipeline p ON s.pipeline_id = p.id
JOIN metric m ON s.metric_id = m.id
WHERE m.name IN ('bleu', 'retrieval_precision', 'retrieval_recall')
GROUP BY p.name
ORDER BY bleu DESC, precision DESC;Description: Compare pipelines across multiple metrics using pivot table.
---
Per-Query Analysis
Query-level metric breakdown
SELECT
q.query AS question,
p.name AS pipeline_name,
m.name AS metric_name,
er.metric_result,
er.created_at
FROM evaluation_result er
JOIN query q ON er.query_id = q.id
JOIN pipeline p ON er.pipeline_id = p.id
JOIN metric m ON er.metric_id = m.id
WHERE q.id = :query_id
ORDER BY m.name, p.name;Parameters: :query_id (specific query to analyze)
Description: Shows all metric scores for a specific query across all pipelines.
---
Compare generated vs ground truth
SELECT
q.query AS question,
q.ground_truths,
p.name AS pipeline_name,
exe.generation_result,
(exe.token_usage->>'total_tokens')::int AS total_tokens
FROM executor_result exe
JOIN query q ON exe.query_id = q.id
JOIN pipeline p ON exe.pipeline_id = p.id
WHERE q.id = :query_id
ORDER BY p.name;Parameters: :query_id
Description: Compare generation outputs against ground truth answers.
JSONB Note: Use token_usage->>'field' to extract text, cast to ::int for numbers.
---
Find worst-performing queries
SELECT
q.query AS question,
p.name AS pipeline_name,
m.name AS metric_name,
er.metric_result,
q.ground_truths
FROM evaluation_result er
JOIN query q ON er.query_id = q.id
JOIN pipeline p ON er.pipeline_id = p.id
JOIN metric m ON er.metric_id = m.id
WHERE m.name = :metric_name
AND p.name = :pipeline_name
ORDER BY er.metric_result ASC
LIMIT :bottom_n;Parameters: :metric_name, :pipeline_name, :bottom_n
Description: Identify queries with lowest scores for error analysis.
---
Retrieval Results Analysis
Top retrieved chunks for a query
SELECT
q.query AS question,
c.content AS chunk_text,
crr.score AS retrieval_score,
crr.rank,
p.name AS pipeline_name
FROM chunk_retrieved_result crr
JOIN query q ON crr.query_id = q.id
JOIN chunk c ON crr.chunk_id = c.id
JOIN pipeline p ON crr.pipeline_id = p.id
WHERE q.id = :query_id
AND p.name = :pipeline_name
ORDER BY crr.rank ASC
LIMIT :top_k;Parameters: :query_id, :pipeline_name, :top_k
Description: Shows actual retrieved chunks with scores and rankings.
---
Retrieval score distribution
SELECT
p.name AS pipeline_name,
AVG(crr.score) AS avg_score,
MIN(crr.score) AS min_score,
MAX(crr.score) AS max_score,
COUNT(*) AS total_retrievals
FROM chunk_retrieved_result crr
JOIN pipeline p ON crr.pipeline_id = p.id
GROUP BY p.name
ORDER BY avg_score DESC;Description: Analyze retrieval score statistics per pipeline.
---
Ground truth comparison (retrieval)
SELECT
q.query AS question,
p.name AS pipeline_name,
COUNT(DISTINCT rr.chunk_id) AS total_relevant_chunks,
COUNT(DISTINCT CASE WHEN crr.chunk_id IS NOT NULL THEN rr.chunk_id END) AS retrieved_relevant_chunks,
CAST(COUNT(DISTINCT CASE WHEN crr.chunk_id IS NOT NULL THEN rr.chunk_id END) AS FLOAT) /
NULLIF(COUNT(DISTINCT rr.chunk_id), 0) AS recall
FROM query q
JOIN retrieval_relation rr ON q.id = rr.query_id
JOIN pipeline p ON p.id = :pipeline_id
LEFT JOIN chunk_retrieved_result crr ON
rr.query_id = crr.query_id
AND rr.chunk_id = crr.chunk_id
AND crr.pipeline_id = p.id
WHERE q.id = :query_id
GROUP BY q.query, p.name;Parameters: :query_id, :pipeline_id
Description: Calculate recall by comparing retrieved chunks against ground truth.
---
Token Usage Analysis
Token usage by pipeline
SELECT
p.name AS pipeline_name,
COUNT(*) AS query_count,
SUM((exe.token_usage->>'prompt_tokens')::int) AS total_prompt_tokens,
SUM((exe.token_usage->>'completion_tokens')::int) AS total_completion_tokens,
SUM((exe.token_usage->>'total_tokens')::int) AS total_tokens,
AVG((exe.token_usage->>'total_tokens')::int) AS avg_tokens_per_query
FROM executor_result exe
JOIN pipeline p ON exe.pipeline_id = p.id
WHERE exe.token_usage IS NOT NULL
GROUP BY p.name
ORDER BY total_tokens DESC;Description: Aggregate token usage statistics per pipeline.
JSONB Extraction: Cast to ::int after extracting with ->>.
---
Most expensive queries
SELECT
q.query AS question,
p.name AS pipeline_name,
(exe.token_usage->>'total_tokens')::int AS total_tokens,
(exe.token_usage->>'prompt_tokens')::int AS prompt_tokens,
(exe.token_usage->>'completion_tokens')::int AS completion_tokens,
exe.execution_time
FROM executor_result exe
JOIN query q ON exe.query_id = q.id
JOIN pipeline p ON exe.pipeline_id = p.id
WHERE exe.token_usage IS NOT NULL
ORDER BY (exe.token_usage->>'total_tokens')::int DESC
LIMIT :top_n;Parameters: :top_n
Description: Find queries with highest token consumption.
---
Token usage over time
SELECT
DATE(exe.created_at) AS date,
p.name AS pipeline_name,
SUM((exe.token_usage->>'total_tokens')::int) AS daily_tokens,
COUNT(*) AS query_count
FROM executor_result exe
JOIN pipeline p ON exe.pipeline_id = p.id
WHERE exe.token_usage IS NOT NULL
AND exe.created_at >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY DATE(exe.created_at), p.name
ORDER BY date DESC, daily_tokens DESC;Description: Track token usage trends over last 30 days.
---
Execution Performance
Slowest queries by execution time
SELECT
q.query AS question,
p.name AS pipeline_name,
exe.execution_time,
(exe.token_usage->>'total_tokens')::int AS total_tokens,
exe.created_at
FROM executor_result exe
JOIN query q ON exe.query_id = q.id
JOIN pipeline p ON exe.pipeline_id = p.id
ORDER BY exe.execution_time DESC
LIMIT :top_n;Parameters: :top_n
Description: Identify performance bottlenecks.
---
Average execution time by pipeline
SELECT
p.name AS pipeline_name,
COUNT(*) AS query_count,
AVG(exe.execution_time) AS avg_execution_time,
MIN(exe.execution_time) AS min_execution_time,
MAX(exe.execution_time) AS max_execution_time
FROM executor_result exe
JOIN pipeline p ON exe.pipeline_id = p.id
GROUP BY p.name
ORDER BY avg_execution_time DESC;Description: Compare pipeline performance across all queries.
---
JSONB Extraction Patterns
Extract nested token usage details
SELECT
p.name AS pipeline_name,
q.query AS question,
exe.token_usage->>'prompt_tokens' AS prompt_tokens_text,
(exe.token_usage->>'prompt_tokens')::int AS prompt_tokens_int,
exe.token_usage->'embedding_tokens' AS embedding_tokens_json,
exe.token_usage AS full_token_usage
FROM executor_result exe
JOIN pipeline p ON exe.pipeline_id = p.id
JOIN query q ON exe.query_id = q.id
WHERE exe.token_usage IS NOT NULL
LIMIT 5;JSONB Operators:
->extracts as JSON (preserves type)->>extracts as text- Cast with
::int,::float, etc.
---
Parse pipeline config JSONB
SELECT
name AS pipeline_name,
config->>'type' AS pipeline_type,
config->>'model' AS model_name,
(config->>'top_k')::int AS top_k,
config AS full_config
FROM pipeline
WHERE config IS NOT NULL
LIMIT 10;Description: Extract structured data from pipeline configuration.
---
Complex Multi-Table JOINs
Full pipeline execution report
SELECT
p.name AS pipeline_name,
q.query AS question,
exe.generation_result,
m.name AS metric_name,
er.metric_result AS score,
(exe.token_usage->>'total_tokens')::int AS tokens,
exe.execution_time
FROM executor_result exe
JOIN query q ON exe.query_id = q.id
JOIN pipeline p ON exe.pipeline_id = p.id
LEFT JOIN evaluation_result er ON
er.query_id = exe.query_id
AND er.pipeline_id = exe.pipeline_id
LEFT JOIN metric m ON er.metric_id = m.id
WHERE p.name = :pipeline_name
ORDER BY q.query, m.name;Parameters: :pipeline_name
Description: Comprehensive view of pipeline execution with all metrics.
---
Retrieval + Generation + Evaluation combined
SELECT
q.query AS question,
p.name AS pipeline_name,
STRING_AGG(DISTINCT c.content, ' | ') AS retrieved_chunks,
exe.generation_result,
MAX(CASE WHEN m.name = 'bleu' THEN er.metric_result END) AS bleu,
MAX(CASE WHEN m.name = 'rouge' THEN er.metric_result END) AS rouge
FROM query q
JOIN pipeline p ON p.id = :pipeline_id
LEFT JOIN chunk_retrieved_result crr ON
crr.query_id = q.id
AND crr.pipeline_id = p.id
LEFT JOIN chunk c ON crr.chunk_id = c.id
LEFT JOIN executor_result exe ON
exe.query_id = q.id
AND exe.pipeline_id = p.id
LEFT JOIN evaluation_result er ON
er.query_id = q.id
AND er.pipeline_id = p.id
LEFT JOIN metric m ON er.metric_id = m.id
WHERE q.id = :query_id
GROUP BY q.query, p.name, exe.generation_result;Parameters: :query_id, :pipeline_id
Description: Complete RAG pipeline trace for a single query.
---
Metadata Queries
List all pipelines
SELECT
id,
name,
pipeline_type,
created_at,
config->>'model' AS model,
config->>'type' AS config_type
FROM pipeline
ORDER BY created_at DESC;---
List all metrics
SELECT
id,
name,
metric_type,
created_at
FROM metric
ORDER BY metric_type, name;Note: metric_type is either 'retrieval' or 'generation'.
---
Count queries by dataset
SELECT
dataset_name,
COUNT(*) AS query_count,
COUNT(DISTINCT ground_truths) AS unique_ground_truths
FROM query
GROUP BY dataset_name
ORDER BY query_count DESC;---
Notes
- Always exclude:
embedding,embeddings,bm25_tokenscolumns - JSONB extraction: Use
->>for text, cast to type for numbers - Performance: Add
LIMITclauses to prevent large result sets - Parameterization: Use
:param_namewith--params '{"param_name": "value"}'for safe value substitution - NULL handling: Use
NULLIF()andCOALESCE()for division and defaults - Example with params:
python query_executor.py -q "SELECT * FROM pipeline WHERE name = :name" -p '{"name": "naive_rag"}'
-- ============================================================================
-- AutoRAG-Research Database Schema
-- ============================================================================
--
-- IMPORTANT FOR TEXT2SQL AGENTS:
-- ⚠️ ALWAYS EXCLUDE these columns from SELECT queries:
-- - embedding, embeddings (pgvector type - causes DuckDB type errors)
-- - bm25_tokens (bm25vector type - causes type resolution errors)
--
-- KEY TABLES FOR PIPELINE RESULTS:
-- - summary: Aggregated pipeline metrics (best for ranking pipelines)
-- - evaluation_result: Per-query metric scores (best for detailed analysis)
-- - executor_result: Generation outputs with token_usage JSONB
-- - chunk_retrieved_result: Retrieval outputs with scores and ranks
--
-- COMMON RELATIONSHIPS:
-- query_id → query.id
-- pipeline_id → pipeline.id
-- metric_id → metric.id
-- chunk_id → chunk.id
--
-- JSONB EXTRACTION PATTERNS:
-- token_usage->>'prompt_tokens' (text)
-- (token_usage->>'total_tokens')::int (cast to integer)
-- token_usage->'embedding_tokens' (preserve JSON type)
--
-- ============================================================================
-- Prefer VectorChord's extension; load alternatives only if needed
DO $$
BEGIN
BEGIN
CREATE EXTENSION IF NOT EXISTS vchord CASCADE;
EXCEPTION WHEN others THEN
PERFORM 1;
END;
BEGIN
CREATE EXTENSION IF NOT EXISTS vectors;
EXCEPTION WHEN others THEN
PERFORM 1;
END;
BEGIN
CREATE EXTENSION IF NOT EXISTS vector;
EXCEPTION WHEN others THEN
PERFORM 1;
END;
END $$;
-- VectorChord-BM25 extensions for sparse retrieval
DO $$
BEGIN
BEGIN
CREATE EXTENSION IF NOT EXISTS vchord_bm25 CASCADE;
EXCEPTION WHEN others THEN
PERFORM 1;
END;
BEGIN
CREATE EXTENSION IF NOT EXISTS pg_tokenizer CASCADE;
EXCEPTION WHEN others THEN
PERFORM 1;
END;
END $$;
-- Create BM25 tokenizers (requires pg_tokenizer extension)
-- Available pre-built models from pg_tokenizer:
-- bert: bert-base-uncased (Hugging Face)
-- wiki_tocken: Wikitext-103 trained model
-- gemma2b: Google lightweight model (~100MB memory)
-- llmlingua2: Microsoft summarization model (~200MB memory, default preload)
-- See: https://github.com/tensorchord/pg_tokenizer.rs/blob/main/docs/06-model.md
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pg_tokenizer') THEN
-- bert_base_uncased (Hugging Face) - uses underscores per pg_tokenizer model naming
BEGIN
PERFORM create_tokenizer('bert', 'model = "bert_base_uncased"');
EXCEPTION WHEN others THEN PERFORM 1; END;
-- wiki_tocken (Wikitext-103)
BEGIN
PERFORM create_tokenizer('wiki_tocken', 'model = "wiki_tocken"');
EXCEPTION WHEN others THEN PERFORM 1; END;
-- gemma2b (Google, ~100MB)
BEGIN
PERFORM create_tokenizer('gemma2b', 'model = "gemma2b"');
EXCEPTION WHEN others THEN PERFORM 1; END;
-- llmlingua2 (Microsoft, ~200MB, default preload)
BEGIN
PERFORM create_tokenizer('llmlingua2', 'model = "llmlingua2"');
EXCEPTION WHEN others THEN PERFORM 1; END;
END IF;
END $$;
-- Schema DDL matching the provided design
-- File
CREATE TABLE IF NOT EXISTS file (
id BIGSERIAL PRIMARY KEY,
type VARCHAR(255) NOT NULL,
path VARCHAR(255) NOT NULL
);
-- Document
CREATE TABLE IF NOT EXISTS document (
id BIGSERIAL PRIMARY KEY,
path BIGINT REFERENCES file(id),
filename TEXT,
author TEXT,
title TEXT,
doc_metadata JSONB
);
-- Page
CREATE TABLE IF NOT EXISTS page (
id BIGSERIAL PRIMARY KEY,
page_num INT NOT NULL,
document_id BIGINT NOT NULL REFERENCES document(id),
image_contents BYTEA,
mimetype VARCHAR(255),
page_metadata JSONB,
CONSTRAINT uq_page_per_doc UNIQUE (document_id, page_num)
);
-- Chunk
-- embeddings column supports VectorChord's MaxSim operator (@#) for late interaction models
-- bm25_tokens column supports VectorChord-BM25 sparse retrieval (added conditionally)
CREATE TABLE IF NOT EXISTS chunk (
id BIGSERIAL PRIMARY KEY,
contents TEXT NOT NULL,
embedding VECTOR(768),
embeddings VECTOR(768)[], -- Multi-vector for ColBERT/ColPali style retrieval
bm25_tokens bm25vector, -- Tokenized sparse vector for BM25 retrieval
is_table BOOLEAN DEFAULT FALSE,
table_type VARCHAR(255)
);
CREATE INDEX IF NOT EXISTS idx_chunk_bm25 ON chunk USING bm25 (bm25_tokens bm25_ops);
-- ImageChunk
-- embeddings column supports VectorChord's MaxSim operator (@#) for late interaction models
CREATE TABLE IF NOT EXISTS image_chunk (
id BIGSERIAL PRIMARY KEY,
parent_page BIGINT REFERENCES page(id),
contents BYTEA NOT NULL,
mimetype VARCHAR(255) NOT NULL,
embedding VECTOR(768),
embeddings VECTOR(768)[] -- Multi-vector for ColPali style image retrieval
);
-- PageChunkRelation
CREATE TABLE IF NOT EXISTS page_chunk_relation (
page_id BIGINT NOT NULL REFERENCES page(id),
chunk_id BIGINT NOT NULL REFERENCES chunk(id),
PRIMARY KEY (page_id, chunk_id)
);
-- Query
-- embeddings column supports VectorChord's MaxSim operator (@#) for late interaction models
CREATE TABLE IF NOT EXISTS query (
id BIGSERIAL PRIMARY KEY,
contents TEXT NOT NULL,
query_to_llm TEXT,
generation_gt TEXT[],
embedding VECTOR(768),
embeddings VECTOR(768)[], -- Multi-vector for ColBERT/ColPali style retrieval
bm25_tokens bm25vector -- Tokenized sparse vector for BM25 retrieval
);
-- RetrievalRelation
CREATE TABLE IF NOT EXISTS retrieval_relation (
query_id BIGINT NOT NULL REFERENCES query(id),
group_index INT NOT NULL,
group_order INT NOT NULL,
chunk_id BIGINT REFERENCES chunk(id),
image_chunk_id BIGINT REFERENCES image_chunk(id),
score INT DEFAULT 1, -- graded relevance (0=not relevant, 1=somewhat relevant, 2=highly relevant)
PRIMARY KEY (query_id, group_index, group_order),
CONSTRAINT ck_rr_one_only CHECK ((chunk_id IS NULL) <> (image_chunk_id IS NULL))
);
-- Pipeline
CREATE TABLE IF NOT EXISTS pipeline (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
config JSONB NOT NULL
);
-- Metric
CREATE TABLE IF NOT EXISTS metric (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
type VARCHAR(255) NOT NULL
);
-- ExperimentResult
CREATE TABLE IF NOT EXISTS executor_result (
query_id BIGINT NOT NULL REFERENCES query(id),
pipeline_id BIGINT NOT NULL REFERENCES pipeline(id),
generation_result TEXT,
token_usage JSONB,
execution_time INT,
result_metadata JSONB,
PRIMARY KEY (query_id, pipeline_id)
);
CREATE TABLE IF NOT EXISTS evaluation_result (
query_id BIGINT NOT NULL REFERENCES query(id),
pipeline_id BIGINT NOT NULL REFERENCES pipeline(id),
metric_id BIGINT NOT NULL REFERENCES metric(id),
metric_result FLOAT NOT NULL,
PRIMARY KEY (query_id, pipeline_id, metric_id)
);
-- ImageChunkRetrievedResult
CREATE TABLE IF NOT EXISTS image_chunk_retrieved_result (
query_id BIGINT NOT NULL REFERENCES query(id),
pipeline_id BIGINT NOT NULL REFERENCES pipeline(id),
image_chunk_id BIGINT NOT NULL REFERENCES image_chunk(id),
rel_score FLOAT,
PRIMARY KEY (query_id, pipeline_id, image_chunk_id)
);
-- ChunkRetrievedResult
CREATE TABLE IF NOT EXISTS chunk_retrieved_result (
query_id BIGINT NOT NULL REFERENCES query(id),
pipeline_id BIGINT NOT NULL REFERENCES pipeline(id),
chunk_id BIGINT NOT NULL REFERENCES chunk(id),
rel_score FLOAT,
PRIMARY KEY (query_id, pipeline_id, chunk_id)
);
-- Summary
CREATE TABLE IF NOT EXISTS summary (
pipeline_id BIGINT NOT NULL REFERENCES pipeline(id),
metric_id BIGINT NOT NULL REFERENCES metric(id),
metric_result FLOAT NOT NULL,
token_usage JSONB,
execution_time INT,
result_metadata JSONB,
PRIMARY KEY (pipeline_id, metric_id)
);
#!/usr/bin/env python3
"""
Safe SQL query executor for AutoRAG-Research database.
Loads connection from configs/db.yaml or environment variables,
validates queries for safety (SELECT-only), and executes with
timeout and result limits.
"""
import json
import re
from pathlib import Path
from typing import Any
import typer
from sqlalchemy import create_engine, text
from sqlalchemy.engine import Engine
from tabulate import tabulate
app = typer.Typer()
def load_db_connection(config_path: Path | None = None, database: str | None = None) -> str:
"""Load database connection string using DBConnection class.
Args:
config_path: Explicit path to configs directory containing db.yaml.
database: Database name override.
"""
from autorag_research.orm.connection import DBConnection
# Try explicit config path first
if config_path and (config_path / "db.yaml").exists():
try:
db_conn = DBConnection.from_config(config_path)
if database:
db_conn.database = database
except Exception as e:
typer.echo(f"Warning: Failed to load from config file: {e}", err=True)
typer.echo("Falling back to environment variables...", err=True)
else:
return db_conn.db_url
# Fallback to environment variables
try:
db_conn = DBConnection.from_env()
if database:
db_conn.database = database
except Exception as e:
msg = (
f"Failed to load database connection: {e}\n"
"Either:\n"
"1. Provide --config-path pointing to configs directory with db.yaml, or\n"
"2. Set POSTGRES_* environment variables (POSTGRES_HOST, POSTGRES_PORT, "
"POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB)"
)
typer.echo(msg, err=True)
raise typer.Exit(code=1) from e
else:
return db_conn.db_url
def validate_query(query: str) -> None:
"""Validate query is SELECT-only and safe."""
# Remove comments and normalize whitespace
query_clean = re.sub(r"--.*$", "", query, flags=re.MULTILINE)
query_clean = re.sub(r"/\*.*?\*/", "", query_clean, flags=re.DOTALL)
query_clean = " ".join(query_clean.split()).upper()
# Must contain SELECT
if "SELECT" not in query_clean:
msg = "Query must be a SELECT statement"
raise ValueError(msg)
# Reject DDL/DML keywords
forbidden = [
"INSERT",
"UPDATE",
"DELETE",
"DROP",
"CREATE",
"ALTER",
"TRUNCATE",
"GRANT",
"REVOKE",
"EXECUTE",
"CALL",
]
for keyword in forbidden:
if re.search(rf"\b{keyword}\b", query_clean):
msg = f"Forbidden keyword: {keyword}. Only SELECT queries allowed."
raise ValueError(msg)
# Reject dangerous system functions
dangerous_funcs = ["PG_READ_FILE", "PG_EXECUTE", "PG_LS_DIR", "COPY", "LO_IMPORT", "LO_EXPORT"]
for func in dangerous_funcs:
if func in query_clean:
msg = f"Forbidden function: {func}"
raise ValueError(msg)
def enforce_limit(query: str, max_limit: int) -> str:
"""Enforce maximum result limit with subquery wrapper."""
if max_limit <= 0:
return query
return f"SELECT * FROM ({query.rstrip(';')}) AS limited LIMIT {max_limit}" # noqa: S608
def execute_query(
engine: Engine, query: str, timeout: int, limit: int, params: dict[str, Any] | None = None
) -> list[dict[str, Any]]:
"""Execute query with timeout and return results."""
# Enforce max limit via subquery wrapper (prevents LIMIT bypass)
query = enforce_limit(query, limit)
with engine.connect() as conn:
# Set statement timeout
conn.execute(text(f"SET statement_timeout = '{timeout}s'"))
try:
result = conn.execute(text(query), params or {})
rows = result.fetchall()
columns = result.keys()
# Convert to list of dicts
return [dict(zip(columns, row, strict=True)) for row in rows]
except Exception as e:
error_msg = str(e).lower()
if "vector" in error_msg or "bm25" in error_msg:
hint = "SELECT id, contents FROM chunk WHERE ..."
msg = (
f"Error: Query contains vector/embedding columns that cannot be serialized.\n"
f"Original error: {e}\n"
f"Exclude: embedding, embeddings, bm25_tokens, bm25vector\n"
f"Example: {hint}"
)
raise ValueError(msg) from e
raise
def format_output(results: list[dict[str, Any]], output_format: str) -> str:
"""Format results as table, JSON, or CSV."""
if not results:
return "No results found."
if output_format == "json":
# Handle non-serializable types
def json_serializer(obj):
if hasattr(obj, "isoformat"):
return obj.isoformat()
return str(obj)
return json.dumps(results, indent=2, default=json_serializer)
elif output_format == "csv":
import csv
import io
output = io.StringIO()
writer = csv.DictWriter(output, fieldnames=results[0].keys())
writer.writeheader()
writer.writerows(results)
return output.getvalue()
else: # table
# Convert dicts to list of lists
headers = list(results[0].keys())
rows = [[str(row[key]) if row[key] is not None else "NULL" for key in headers] for row in results]
return tabulate(rows, headers=headers, tablefmt="simple")
@app.command()
def main(
query: str = typer.Option(..., "--query", "-q", help="SQL query to execute (SELECT only)"),
output_format: str = typer.Option("table", "--format", "-f", help="Output format: table, json, or csv"),
timeout: int = typer.Option(10, "--timeout", "-t", help="Query timeout in seconds"),
limit: int = typer.Option(10000, "--limit", "-l", help="Maximum rows to return (0=unlimited)"),
database: str | None = typer.Option(None, "--database", "-d", help="Database name (overrides config/env)"),
config_path: Path | None = typer.Option( # noqa: B008
None, "--config-path", "-c", help="Path to configs directory with db.yaml"
),
params: str | None = typer.Option(
None, "--params", "-p", help='JSON params for :param_name placeholders, e.g. \'{"metric_name": "bleu"}\''
),
):
"""Execute SELECT queries against AutoRAG-Research database."""
if output_format not in ["table", "json", "csv"]:
typer.echo(f"Error: Invalid format '{output_format}'. Choose: table, json, or csv", err=True)
raise typer.Exit(code=1)
# Parse params JSON
param_dict: dict[str, Any] | None = None
if params:
try:
param_dict = json.loads(params)
except json.JSONDecodeError as e:
typer.echo(f"Error: Invalid JSON for --params: {e}", err=True)
raise typer.Exit(code=1) from e
engine = None
try:
# Validate query
validate_query(query)
# Load connection
conn_string = load_db_connection(config_path, database)
# Create engine with limited pool
engine = create_engine(conn_string, pool_size=1, max_overflow=0)
# Execute query
results = execute_query(engine, query, timeout, limit, param_dict)
# Format and print output
output = format_output(results, output_format)
typer.echo(output)
# Print row count to stderr
typer.echo(f"\n({len(results)} rows)", err=True)
except ValueError as e:
typer.echo(f"Error: {e}", err=True)
raise typer.Exit(code=1) from e
except Exception as e:
typer.echo(f"Error: {e}", err=True)
raise typer.Exit(code=1) from e
finally:
if engine is not None:
engine.dispose()
if __name__ == "__main__":
app()