Natural language database queries for your Rails app — powered by RAG and LLMs.
Glancer is a Ruby on Rails engine that mounts a full chat interface inside your app and lets anyone on your team query the database in plain language, no SQL required. You ask a question, Glancer retrieves the relevant schema context, generates a query, validates and executes it safely, then returns the results with a human-readable explanation.
"How many orders were placed in the last 30 days, grouped by status?"
→ SELECT executed, results shown, answer written in plain language.
Click to see demo. ☝
Why Glancer?
Every Rails app accumulates tables and columns whose meaning lives in the heads of a few engineers. Product managers open tickets for simple questions. Data teams copy-paste schemas into ChatGPT. Engineers write one-off queries for stakeholders.
Glancer removes that friction. It gives your app a persistent, context-aware database assistant that understands your domain,not just generic SQL, because it is taught your schema, your models, and your business rules through a plain Markdown file.
Key design decisions:
- Safety first — all queries run inside a transaction that always rolls back. No write statement can ever reach the database.
- Your LLM, your cost — bring your own Gemini, OpenAI, or OpenRouter key. Mix providers per role to balance cost and quality.
- No external vector store — embeddings live in your existing database. No Pinecone, no Weaviate, no extra infrastructure.
- Rails-native — mounted as an engine, uses Turbo and Stimulus, installs in under five minutes.
- Dual query modes — generate raw
SELECTstatements or Ruby ActiveRecord expressions depending on what fits your domain better.
Requirements
| Dependency | Minimum version |
|---|---|
| Ruby | 3.3 |
| Rails | 7.0 |
| Database | SQLite, PostgreSQL, or MySQL / MariaDB |
| LLM provider | Gemini, OpenAI, or OpenRouter API key |
Glancer is built on top of ruby_llm, a provider-agnostic LLM client for Ruby. All LLM calls (query generation, humanized responses, embeddings, and optional question enrichment) go through ruby_llm, so any model it supports works with Glancer.
Installation
1. Add to your Gemfile
gem "glancer"
bundle install
2. Run the install generator
rails generate glancer:install
This creates:
config/initializers/glancer.rb— your main configuration fileconfig/glancer/llm_context.glancer.md— optional domain context written in Markdown- Mounts the engine at
/glancerinconfig/routes.rb
3. Migrate the database
rails db:migrate
4. Index your schema
rails glancer:index:all
5. Start asking questions
http://localhost:3000/glancer
Configuration
Edit config/initializers/glancer.rb. Minimal working setup:
Glancer.configure do |config|
config.llm_provider = :gemini
config.llm_model = "gemini-2.0-flash"
config.gemini_api_key = ENV["GEMINI_API_KEY"]
config. = true # allow indexing db/schema.rb
end
Query mode
config.query_mode = :sql # (default) LLM generates a SELECT statement
config.query_mode = :activerecord # LLM generates a Ruby/ActiveRecord expression
ActiveRecord mode lets the LLM leverage model scopes, named associations, and Ruby idioms. SQL mode is more portable and works without any models loaded.
Split providers per role
Different models can handle different responsibilities. This is the recommended setup for cost optimization:
Glancer.configure do |config|
config.llm_provider = :gemini # fallback for any unspecified role
# A capable model for accurate query generation
config.code_provider = :openai
config.code_model = "gpt-4o"
# A cheaper model for writing human-readable responses
config.chat_provider = :gemini
config.chat_model = "gemini-2.0-flash"
# Embedding model
config. = :gemini
config. = "text-embedding-004"
# Optional: separate model to enrich ambiguous questions before retrieval
config.query_enrichment_enabled = true
config.enrichment_provider = :gemini
config.enrichment_model = "gemini-2.0-flash"
end
Read-only replica
Route all queries to a replica to offload your primary database:
config.read_only_db = ENV["REPLICA_DATABASE_URL"]
Full configuration reference
| Option | Default | Description |
|---|---|---|
adapter |
auto-detected | :postgres, :mysql, :mysql2, or :sqlite |
query_mode |
:sql |
:sql (raw SELECT) or :activerecord (Ruby expression) |
read_only_db |
nil |
Replica connection URL |
statement_timeout |
30.seconds |
Max execution time; enforced server-side on PG and MySQL |
llm_provider |
:gemini |
Default provider for all roles (:gemini, :openai, :openrouter) |
llm_model |
"gemini-2.0-flash" |
Default model for all roles |
code_provider / code_model |
inherits default | Provider/model for query generation |
chat_provider / chat_model |
inherits default | Provider/model for humanized responses |
embedding_provider / embedding_model |
inherits default | Provider/model for embeddings |
enrichment_provider / enrichment_model |
inherits default | Provider/model for question enrichment |
query_enrichment_enabled |
false |
Pre-retrieval question rewriting to inject table hints |
gemini_api_key |
nil |
Gemini API key |
openai_api_key |
nil |
OpenAI API key |
openrouter_api_key |
nil |
OpenRouter API key |
schema_permission |
false |
Index db/schema.rb |
models_permission |
false |
Index app/models/**/*.rb |
context_file_path |
"config/glancer/llm_context.glancer.md" |
Custom domain context file |
chunk_size |
1000 |
Max characters per embedding chunk |
chunk_overlap |
150 |
Overlap between consecutive chunks |
k |
5 |
Top-k chunks retrieved per question |
min_score |
0.6 |
Minimum cosine similarity score (0.0–1.0) |
schema_documents_weight |
1.3 |
Score boost for schema chunks |
context_documents_weight |
1.2 |
Score boost for context chunks |
models_documents_weight |
1.1 |
Score boost for model chunks |
history_limit |
6 |
Prior conversation turns included in the LLM prompt |
workflow_cache_ttl |
5.minutes |
In-memory result cache TTL; 0 to disable |
log_verbosity |
:info |
:silent, :none, :info, or :debug |
log_output_path |
nil |
Log file path; nil writes to stdout |
blazer_path |
nil |
Blazer base path; auto-detected when blazer gem is present |
Indexing
Glancer embeds your schema, models, and custom context into the glancer_embeddings table. Re-run indexing whenever the schema changes significantly.
rails glancer:index:all # Schema + models + context (prompts confirmation)
rails glancer:index:schema # db/schema.rb only
rails glancer:index:models # app/models/**/*.rb only
rails glancer:index:context # Custom context Markdown file only
rails glancer:version # Print the installed gem version
The schema indexer automatically enriches each table chunk with model association metadata (has_many, belongs_to, etc.) and generates a dedicated foreign key chunk, so the LLM understands relationships without you having to describe them manually.
Custom context file
config/glancer/llm_context.glancer.md is where you document domain knowledge that lives outside the schema — enum values, business definitions, metric formulas, naming conventions:
# Domain context
- `orders.status` values: "pending" | "paid" | "shipped" | "refunded".
- `users.role` can be "admin", "agent", or "customer". Admins are excluded from retention metrics.
- Monthly revenue = SUM(orders.total) WHERE status = "paid".
- When asked about "churn", use the `churned_at` column on the `subscriptions` table.
Add --glancer-ignore as the first line of the file to exclude it from indexing.
Chat interface
Visit /glancer in your browser.
- Async processing — messages are handled in a background thread; the UI polls for completion so you can open a new chat while another query runs.
- Step labels — the interface shows what the pipeline is doing: enriching, retrieving context, generating code, validating, executing, preparing response.
- @mention autocomplete — type
@table_nameto pin a specific table to your question; it renders as a chip linked to the schema viewer. - Dual query modes — generated SQL or Ruby is syntax-highlighted in the response.
- Inline code editing — modify the generated query and re-run it without asking a new question. Edited versions show a badge.
- Results table — with one-click CSV export (client-side, no backend endpoint).
- Charts — bar, line, doughnut, and scatter charts are auto-generated from the result set where meaningful.
- Fullscreen charts — expand any chart to a fullscreen dialog for detailed inspection.
- Blazer integration — open the SQL in Blazer pre-filled, if the gem is installed.
- Audio input — click the microphone to dictate your question.
- Multi-language — ask in any language; the LLM responds in the same language.
- Custom instructions — set persistent system instructions at
/glancer/settings. - Schema viewer — browse all indexed tables and columns at
/glancer/db-schema. - Message details panel — shows generated code, edit history, execution audit, sources used, and enriched question.
Safety
Glancer is designed to be safe on production databases.
SQL mode
| Layer | Mechanism |
|---|---|
| No writes | All queries run inside a transaction that unconditionally rolls back |
| Keyword blocklist | DELETE, UPDATE, INSERT, DROP, TRUNCATE, ALTER, CREATE, REPLACE are rejected before execution |
| Table validation | Referenced tables are checked against the indexed schema; unknown tables return a friendly error |
| Statement timeout | statement_timeout (PG) / max_execution_time (MySQL) kills runaway queries server-side |
| Audit trail | Every execution is recorded in glancer_audits with a unique run_id UUID |
| Replica support | Route queries to a read-only replica via config.read_only_db |
ActiveRecord mode
| Layer | Mechanism |
|---|---|
| No writes | Same rolled-back transaction as SQL mode |
| Method blocklist | .destroy, .delete, .update, .save, .create, .insert, .upsert, .touch and variants are rejected |
| Shell blocklist | Backticks, system(), exec(), spawn() are rejected |
| Eval blocklist | eval, instance_eval, class_eval are rejected |
| File write blocklist | FileUtils, File.write, IO.write are rejected |
| Dynamic load blocklist | require, load, autoload are rejected |
| Audit trail | Same as SQL mode; code_type: "activerecord" recorded in glancer_audits |
Internal database tables
| Table | Purpose |
|---|---|
glancer_chats |
Conversation containers |
glancer_messages |
User/assistant turns; stores generated code, code type, and processing status |
glancer_embeddings |
Vector store: content, embedding (JSONB on PG / JSON elsewhere), source type and path |
glancer_audits |
Immutable query log with unique run_id per execution |
glancer_code_versions |
Code edit history per message |
glancer_settings |
Runtime configuration (e.g. custom instructions) |
Usage from Ruby
You can call Glancer's internals directly from the Rails console or your own code:
# Re-index everything
Glancer::Indexer.rebuild_all!
# Run the full pipeline
result = Glancer::Workflow.run(chat.id, "Which products have never been ordered?")
# => { content: "...", code: "SELECT ...", code_type: "sql", successful: true, sources: [...] }
# Retrieve relevant chunks for a question (without running the full pipeline)
chunks = Glancer::Retriever.search("monthly revenue by region")
# Check SQL against the safety layer
Glancer::Workflow::SQLSanitizer.ensure_safe!("SELECT * FROM users")
# Check an ActiveRecord expression against the safety layer
Glancer::Workflow::ARSanitizer.ensure_safe!("User.where(active: true).count")
# Validate table references against the indexed schema
Glancer::Workflow::SQLValidator.validate_tables_exist!("SELECT * FROM orders JOIN unknown_table")
Development
git clone https://github.com/ErnaneJ/glancer
cd glancer
bundle install
bundle exec rake # Tests + RuboCop (mirrors CI)
bundle exec rake spec # RSpec only
bundle exec rake rubocop # RuboCop only
# Run a single spec file
bundle exec rspec spec/lib/glancer/workflow/executor_spec.rb
# Run tests with coverage report
COVERAGE=1 bundle exec rspec
To iterate against a host Rails app, point the Gemfile to the local path:
gem "glancer", path: "../glancer"
Contributing
Bug reports, feature requests, and pull requests are welcome on GitHub.
Before opening a pull request:
- Fork the repository and create a feature branch from
main. - Write or update tests for your changes —
bundle exec rake specmust stay green. - Ensure RuboCop is clean —
bundle exec rake rubocop. - Add an entry to
CHANGELOG.mdunder[Unreleased]. - Open a pull request with a clear description of what changed and why.
Please read the Code of Conduct before contributing.
License
Glancer is available as open source under the MIT License.
