Skip to content

aaronshahriari/dblite.nvim

Repository files navigation

dblite.nvim

dblite.nvim

Query Oracle and SQL Server from Neovim — write SQL in any buffer, run it, and page results in a split.

Neovim 0.11+ GraalVM native

Features · Install · Quick start · Configuration

The database work runs in a native binary (GraalVM), so there's no JVM at runtime — it's downloaded pre-built on install, falling back to a source build only if no binary matches your platform.

Features

  • Run from any buffer — the whole buffer, or just the statement under the cursor (treesitter-aware).
  • Paginated result split with column-type annotations, query timing, and a per-session result history you can page back through.
  • Named connections with $ENV_VAR password references, stored at chmod 600.
  • Typed bind parameters from a dblite.binds.json file — numbers, quoted strings, and raw SQL expressions.
  • Export the entire result set (not just the current page) to CSV or JSON.
  • Bulk background exports — stream huge queries straight to a file asynchronously (no row cap), tracked in a jobs panel with live progress, while you keep working.
  • Load a CSV into a table with a SQL*Loader-style LOAD DATA block — previewed as INSERTs before you commit.
  • Inspect any page untruncated as JSON, table, or CSV.
  • SQL autocomplete via blink.cmp — tables, columns, and bind names from the live schema.
  • Connection UI — a built-in side panel, or an opt-in telescope.nvim picker.

Requirements

  • Neovim 0.11+
  • Optional: jq (prettier JSON), telescope.nvim (picker), blink.cmp (completion)
  • Only if building from source (no prebuilt binary for your platform): GraalVM native-image

Installation

The binary is fetched automatically on install and update.

vim.pack (Neovim 0.11+) — register the PackChanged hook before vim.pack.add():

vim.api.nvim_create_autocmd('PackChanged', {
  callback = function(ev)
    local name, kind = ev.data.spec.name, ev.data.kind
    if name == 'dblite' and (kind == 'install' or kind == 'update') then
      require('dblite.download').download_or_build()
    end
  end,
})

vim.pack.add({ { src = 'https://github.com/aaronshahriari/dblite.nvim' } })
require('dblite').setup()

If the hook wasn't in place on first install, run :DbliteBuild manually.

lazy.nvim — the bundled build.lua is picked up automatically, so no build = key is needed:

{ 'aaronshahriari/dblite.nvim', config = function() require('dblite').setup() end }
Other plugin managers
" vim-plug
Plug 'aaronshahriari/dblite.nvim', { 'do': ':DbliteBuild' }
-- packer.nvim
use { 'aaronshahriari/dblite.nvim', run = ':DbliteBuild' }
# Manual
git clone https://github.com/aaronshahriari/dblite.nvim

For a manual install, add the directory to runtimepath, call require('dblite').setup(), and run :DbliteBuild.

Quick start

:DbliteAddConn oracle://system:oracle@localhost:1521/XEPDB1   " add a connection
:DbliteUseConn XEPDB1                                          " make it active

Then write SQL in any buffer and run it:

:Dblite run       " run the whole buffer
:Dblite run at    " run the statement under the cursor

Results open in the dbout split. Page with L/H, walk history with [/], hover K to see the executed SQL.

Connections

Connections live at ~/.local/share/nvim/dblite/connections.json (chmod 600). Passwords can be stored as $ENV_VAR references and are expanded from the environment at query time.

Command Description
:DbliteAddConn [uri] Add a connection (URI, or prompts field-by-field)
:DbliteListConns List connections; active one marked *
:DbliteUseConn <name> Set the active connection
:DbliteEditConn <name> Edit a saved connection
:DbliteDeleteConn <name> Delete a connection
:Dblite conn file Open the raw connections JSON
:DbliteConnPicker Pick a connection with a telescope picker

Name arguments support tab-completion.

URI formats — port defaults to 1521 (Oracle) / 1433 (SQL Server) when omitted:

oracle://user[:password]@host[:port]/service
sqlserver://user[:password]@host[:port]/database

SQL Server connections use encrypt=true;trustServerCertificate=true for broad compatibility with local dev and Azure SQL.

Running queries

Command Description
:Dblite run Run the entire buffer
:Dblite run at Run the statement under the cursor (treesitter-aware)
:Dblite toggle dbout Show/hide the result window (query keeps running if in-flight)
:Dblite inspect [json|table|csv] Open the current page untruncated in a scratch window
:Dblite export <csv|json> [path] Write the entire result set to a file
:Dblite run bulk <csv|json> [path] Run the current query in the background, streaming the full result straight to a file
:Dblite jobs Toggle the background-jobs panel
:Dblite load Load a CSV into a table from a LOAD DATA control block (preview, then commit)

Trailing semicolons are stripped automatically. The legacy :DbliteRun, :DbliteRunAt, and :DbliteToggleOut commands remain as aliases. Running from a different tab moves the dbout split to that tab.

dbout keymaps:

Key Action Key Action
L / H Next / previous page [ / ] Previous / next result in history
K Hover the query that produced this result d Toggle column type annotations
gi Inspect current page (untruncated) <leader>l Toggle dbout fullscreen
<C-c> Cancel in-flight query

<C-c> also cancels from any buffer while a query runs — dblite sets it globally for the duration and restores your mapping afterward.

More

Bind parameters

Bind params come from a dblite.binds.json file in the current working directory. Create/edit it with :Dblite binds or <leader>b; it's re-read on every query. When you run a query with missing params, the file opens so you can fill them in, then re-run.

{
  "status": "pending",
  "user_id": 42,
  "name": "O'Brien",
  "dt": "~SYSDATE"
}

Values are typed and formatted for SQL automatically:

JSON / prefix SQL output
JSON number verbatim — 4242
String auto-quoted, single-quotes escaped — "O'Brien"'O''Brien'
String starting with ~ raw SQL expression — "~SYSDATE"SYSDATE

The binds window is a vertical split by default; set binds_split.style = 'float' for a centered float. Add "binds_file" to style.dbout.sections to show a binds badge when the file exists in the cwd.

Exporting results

:Dblite export csv|json (or :DbliteExport) writes the full result set — every row, not just the current page — to a file:

:Dblite export csv ~/exports/jobs.csv
:Dblite export json ./out/jobs.json
:DbliteExport csv                       " omit the path to be prompted (with completion)

~, env vars, and relative paths are expanded, and missing parent directories are created. CSV is RFC-4180 escaped; JSON is pretty-printed via jq when available (compact fallback otherwise).

export works off the result set already loaded in dbout, so it's capped by max_rows. To dump more rows than max_rows — or to keep working while a huge query runs — use a bulk background export instead (below).

Bulk background exports

:Dblite run bulk csv|json [path] (or :DbliteRunBulk) runs the query at the cursor (or the whole buffer) asynchronously, streaming the full result set straight to a file via the native binary — no max_rows cap and no in-editor buffering, so it handles arbitrarily large pulls (e.g. 50k+ rows) without blocking your session:

:Dblite run bulk csv ~/exports/big.csv
:Dblite run bulk json ./out/big.json
:DbliteRunBulk csv                      " omit the path to be prompted (with completion)
:'<,'>DbliteRunBulk csv                 " export just the selected statement(s)

Both commands accept a range, so you can visually select some SQL and dump exactly that — the selection is expanded to the whole statement(s) it touches. The run_bulk keymap also fires from visual mode.

Jobs run in the background, so you can keep running normal queries meanwhile. :Dblite jobs (or :DbliteJobs) toggles a floating jobs panel showing status, output file name, duration, and exported row count.

Persistent history: finished jobs are recorded to a shared store (stdpath('data')/dblite/jobs.json by default), so the panel shows your past exports even across restarts and across every Neovim instance on the machine. Control it via jobs.history: show (how many past jobs to display, e.g. last 10/50), max_entries (how many to keep on disk), or enabled = false for in-memory only. Deleting a finished entry (x) prompts first, then removes it from the store; cancelling a running job also prompts first.

Jobs panel keymaps:

Key Action
<CR> Open the output file of the job under the cursor in a new tab
x Cancel a running job / delete a finished one from history
q Close the panel
keymaps.jobs.toggle Toggle the panel from inside (off by default; set to your open key for symmetry)

There is no client-side query timeout, so a bulk export runs until the database returns — fine for multi-minute queries. Bind parameters are resolved the same way as normal queries.

Note: bulk export needs a native binary that includes --to-file support. If you installed a pre-built release binary, force a source rebuild with :DbliteBuild! (or :Dblite build force) to pick it up — plain :DbliteBuild downloads the latest release, which may not include it yet.

Loading CSV data

Write a SQL*Loader-style control block in any buffer and run :Dblite load (or :DbliteLoad). dblite parses it, reads the CSV, and opens a preview of the INSERTs it will run — nothing touches the database until you commit:

LOAD DATA
INFILE 'employees.csv'
INTO TABLE employees
SKIP 1
FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"'
TRAILING NULLCOLS
( employee_id, name, department )

In the preview buffer, press <CR> to commit — the INSERTs run through script mode and land as a per-row OK/ERROR log in dbout — or q to cancel. The preview is editable SQL, so you can tweak it before committing. Keys are set by keymaps.load; the window style by load_view (tab | vertical | horizontal | float).

This is an emulation, not real sqlldr — the dblite binary has no Oracle client, so a practical subset of the control syntax is converted to INSERTs and run over your existing JDBC connection.

Control clause Behaviour
INFILE 'path' CSV path — relative to cwd, ~ and $ENV_VAR expanded (INFILE * unsupported)
INTO TABLE name Target table; optional APPEND (default), REPLACE (DELETE first), or TRUNCATE
SKIP n Skip the first n rows (e.g. a header)
FIELDS TERMINATED BY 'c' Field separator (default ,; also X'09' hex, e.g. tab)
(OPTIONALLY) ENCLOSED BY 'c' Quote character (default ")
TRAILING NULLCOLS Pad rows with fewer fields than columns as NULL
( col, col, ... ) Target columns, in file order

Values are typed best-effort: numbers unquoted, empty fields become NULL, everything else is single-quoted (with '' escaping). Dates rely on Oracle's implicit NLS conversion. Per-column datatypes/transforms, positional fields, and direct-path are not supported.

Result history

Every successful query is saved to a history ring. Page past results with [ / ] — the status line shows ◀ 2/5 ▶ when multiple entries exist. Press K to hover the executed SQL (bind params already substituted), SQL-highlighted, auto-dismissing on cursor move.

Size is controlled by max_history (default 20; 0 = unlimited).

Column types

Press d in the dbout buffer to toggle database type annotations in the header:

EMPLOYEE_ID [NUMBER] | FIRST_NAME [VARCHAR2] | HIRE_DATE [DATE]

Show them by default with show_column_types = true. Annotations use the DbliteColumnType highlight (links to Comment); override via style.dbout.column_type_hl.

Inspect

gi (or :Dblite inspect) opens the current page in a scratch window with no truncation. Tab-complete the format:

Format Description
json Pretty-printed via jq (raw fallback)
table Same layout as dbout, widths fit content
csv RFC-4180 escaped

Opens per json_view (default "tab"); q closes. In json, cell values that are themselves serialized JSON are decoded and nested inline instead of shown as an escaped blob — set inspect_expand_json = false to keep raw strings.

Autocomplete (blink.cmp)

Add the source to your blink config:

sources = {
  providers = { dblite = { module = 'dblite.blink', name = 'dblite' } },
  default = { 'lsp', 'path', 'snippets', 'buffer', 'dblite' },
}
Context Completions
Any SQL buffer SQL keywords + table names
After FROM / JOIN / INTO / UPDATE table names first
After table. that table's columns
After : existing dblite.binds.json keys + columns as bind suggestions
Inside dblite.binds.json dotted column keys like orders.id

Schema is fetched once per connection switch in the background, then served from cache. It uses whatever connection :DbliteUseConn set — no extra config.

Connections panel & telescope picker

:DblitePanel toggles a side panel of saved connections (active one marked ):

Key Action
<CR> Activate the connection under the cursor
cw Edit it
q Close the panel

Prefer a fuzzy picker? Set connection_picker = "telescope" (requires telescope.nvim, off by default). Then :DblitePanel opens the picker instead; the active connection is marked and the preview pane masks the password. :DbliteConnPicker always opens the picker regardless of the setting, so you can bind it directly:

require('dblite').setup({
  connection_picker = 'telescope',
  telescope_picker = {
    preview       = true, -- show the details preview pane
    width         = 0.4,  -- fraction of editor (<= 1) or absolute columns (> 1)
    height        = 0.4,
    preview_width = 0.5,  -- preview width as a fraction of the picker
  },
})
vim.keymap.set('n', '<leader>dc', '<cmd>DbliteConnPicker<cr>', { desc = 'dblite: pick connection' })

API

Everything is callable from Lua — handy for custom keymaps:

local db = require('dblite')
db.execute()               -- run the current buffer
db.execute_at_cursor()     -- run the statement under the cursor
db.toggle_dbout()          -- show/hide the result window
db.inspect(format)         -- 'json' | 'table' | 'csv'
db.load()                  -- preview + commit a LOAD DATA control block in the buffer
db.toggle_binds()          -- toggle the dblite.binds.json split
db.toggle_panel()          -- toggle the connections panel
db.get_active_conn()       -- active connection object, or nil
db.get_flat_binds()        -- flattened dblite.binds.json as a table
Full API surface
db.open_binds()            -- open/focus the binds split (does not close)
db.edit_binds()            -- alias for toggle_binds()
db.edit_connections_file() -- open connections JSON for direct editing
db.open_panel()            -- open the panel
db.close_panel()           -- close the panel
db.is_panel_open()         -- true/false

Configuration

setup() takes no options if you're happy with the defaults. Common ones:

require('dblite').setup({
  split_dir         = 'horizontal', -- 'vertical' | 'horizontal' | 'tab'
  page_size         = 100,          -- rows per page
  max_rows          = 10000,        -- hard cap on rows returned
  max_col_width     = 50,           -- truncate wider cells; 0 = no limit
  max_history       = 20,           -- results kept in history; 0 = unlimited
  show_column_types = false,        -- show [TYPE] headers by default
  connection_picker = 'panel',      -- 'panel' | 'telescope'
})
All options & defaults
require('dblite').setup({
  split_dir      = 'horizontal',  -- 'vertical' | 'horizontal' | 'tab'
  split_size     = { width = 80, height = 20 },
  page_size      = 100,           -- rows per page in the result buffer
  max_rows       = 10000,         -- hard cap on rows returned
  max_col_width  = 50,            -- truncate cells wider than this; 0 = no limit
  max_history    = 20,            -- past query results to keep; 0 = unlimited
  show_column_types = false,      -- show [TYPE] next to column headers by default
  filetypes      = { 'sql', 'plsql', 'mysql', 'sqlite' }, -- buffers dblite attaches to (editor keymaps + on_attach)
  on_attach      = nil,           -- function(bufnr) run per SQL buffer for custom buffer-local keybinds
  filetype       = '',            -- filetype for the result buffer ('' = no highlighting)
  flash_timeout  = 2000,          -- ms to hold the query highlight; 0 = hold until results
  json_view      = 'tab',         -- where inspect opens: 'tab' | 'vertical' | 'horizontal' | 'float'
  load_view      = 'tab',         -- where the CSV-load preview opens: 'tab' | 'vertical' | 'horizontal' | 'float'
  inspect_format = 'json',        -- default inspect format: 'json' | 'table' | 'csv'
  inspect_expand_json = true,     -- json inspect: decode cell values that are themselves JSON strings
  panel = {
    width = 30,                   -- side panel width in columns
  },
  jobs = {                        -- background bulk-export jobs (:Dblite run bulk)
    panel = { width = 46 },       -- jobs-panel width in columns
    cleanup_delay  = 300,         -- seconds a finished job lingers in the live list; 0 = keep until dismissed
    default_format = 'csv',       -- default bulk format: 'csv' | 'json'
    open_on_start  = true,        -- auto-open the jobs panel when a bulk export starts
    focus          = true,        -- move the cursor into the panel when you toggle it open
    history = {                   -- persistent job history, shared across all Neovim instances
      enabled     = true,         -- record finished jobs to disk (false = in-memory only)
      show        = 20,           -- how many past jobs to display in the panel (0 = all kept)
      max_entries = 200,          -- hard cap on stored jobs; oldest dropped past this
      -- file = stdpath('data')..'/dblite/jobs.json'  -- override the store location
    },
  },
  connection_picker = 'panel',    -- 'panel' | 'telescope' (requires telescope.nvim)
  telescope_picker = {
    preview       = true,         -- show the connection-details preview (password masked)
    width         = 0.4,          -- fraction of editor (<= 1) or absolute columns (> 1)
    height        = 0.4,
    preview_width = 0.5,          -- preview pane width as a fraction of the picker
  },
  binds_split = {
    style        = 'split',       -- 'split' | 'float'
    split_dir    = 'vertical',    -- 'vertical' | 'horizontal' (split only)
    width        = 40,            -- columns for vertical split. 0 = let nvim decide.
    height       = 20,            -- rows for horizontal split. 0 = let nvim decide.
    float_width  = 0,             -- float width in columns.  0 = 70% of editor width.
    float_height = 0,             -- float height in rows.    0 = 60% of editor lines.
  },
  style = {
    dbout = {
      cursorline = false,         -- highlight the line under the cursor
      column_type_hl = 'DbliteColumnType',
      -- Status line sections. Each entry: { "item", sep = "…", hl = "HlGroup" }
      -- Available items: "history" | "pagination" | "query_time" | "connection" | "binds_file"
      sections = {
        { "history" },
        { "pagination", sep = "  " },
        { "query_time", sep = "" },
        { "connection", sep = "  ·  " },
      },
    },
  },
  keymaps = {
    global = {  -- active from any buffer/window. '' = disabled.
      run           = '',          -- run the whole buffer
      run_at        = '',          -- run the statement under the cursor
      run_script    = '',          -- run the buffer as a SQL*Plus script
      run_bulk      = '',          -- background bulk export to a file
      toggle_dbout  = '',          -- show/hide the result window
      toggle_panel  = '',          -- toggle the connections panel
      toggle_jobs   = '',          -- toggle the background-jobs panel
      toggle_binds  = '',          -- toggle dblite.binds.json
      inspect       = '',          -- inspect current page untruncated
      fullscreen    = '',          -- toggle dbout fullscreen
      connections   = '',          -- open the connections JSON file
    },
    dbout = {
      next = 'L', prev = 'H', cancel = '<C-c>', inspect = 'gi',
      history_prev = '[', history_next = ']', hover_query = 'K', toggle_types = 'd',
      toggle_dbout = '',
    },
    editor = {  -- buffer-local, set only in `filetypes` buffers. '' = disabled.
      run          = '',          -- run the whole buffer
      run_at       = '',          -- run the statement under the cursor
      run_script   = '',          -- run the buffer as a SQL*Plus script
      run_bulk     = '',          -- background bulk export to a file
      toggle_dbout = '',          -- show/hide the result window
      toggle_panel = '',          -- toggle the connections panel
      toggle_jobs  = '',          -- toggle the background-jobs panel
      inspect      = '',          -- inspect current page untruncated
      binds        = '<leader>b', -- toggle dblite.binds.json split
      connections  = '',          -- open the connections JSON file
      fullscreen   = '<leader>l', -- toggle dbout fullscreen
      hover_bind   = 'K',         -- hover the bind value under the cursor
    },
    panel = { select = '<CR>', edit = 'cw', close = 'q', toggle = '' },
    binds = { toggle = '' },
    jobs  = { open = '<CR>', cancel = 'x', close = 'q', toggle = '' },  -- background-jobs panel
    load  = { commit = '<CR>', cancel = 'q' },  -- CSV-load preview buffer
  },
})
Custom keybindings

dblite supports optional global keymaps for cross-cutting actions that should work from anywhere. They all default to '' (disabled):

require('dblite').setup({
  keymaps = {
    global = {
      toggle_dbout = '<leader>o',
      toggle_jobs  = '<leader>j',
      toggle_binds = '<leader>b',
    },
  },
})

Editor keymaps remain buffer-local and are set only in the buffers listed in filetypes (default sql, plsql, mysql, sqlite), so they never fire in unrelated buffers or windows. Use them for SQL-buffer-only actions:

require('dblite').setup({
  keymaps = {
    editor = {
      run      = '<leader>r',   -- run the whole buffer
      run_at   = '<leader>rr',  -- run the statement under the cursor
      run_bulk = '<leader>rb',  -- background bulk export
      toggle_dbout = '<leader>o',
      toggle_jobs  = '<leader>j',
    },
  },
})

For anything more custom (conditional maps, visual-mode maps, extra behaviour), use the on_attach(bufnr) hook — the idiomatic replacement for manual FileType/BufWinEnter autocmds. It runs once per SQL buffer, after the built-in editor maps:

require('dblite').setup({
  on_attach = function(buf)
    local d, o = require('dblite'), { buffer = buf, silent = true }
    vim.keymap.set('n', '<leader>r',  d.execute,                    o)
    vim.keymap.set('n', '<leader>rr', d.execute_at_cursor,          o)
    vim.keymap.set('n', '<leader>rb', function() d.run_async() end, o)
    vim.keymap.set('n', '<leader>j',  d.toggle_jobs,                o)
  end,
})

Managing attachment yourself? require('dblite').attach(bufnr) applies the configured editor maps + runs on_attach for a buffer on demand.

Public API (all on require('dblite')):

Function Action
execute() Run the whole buffer
execute_at_cursor() Run the statement under the cursor
execute_script() Run the buffer as a SQL*Plus script
run_async(format?, path?) Background bulk export (prompts if args omitted)
toggle_dbout() Show/hide the result window
toggle_panel() / open_panel() / close_panel() / is_panel_open() Connections panel
toggle_jobs() / open_jobs() / close_jobs() / is_jobs_open() Background-jobs panel
toggle_fullscreen() Toggle dbout fullscreen
inspect(format?) Inspect the current page untruncated
export(format, path?) Write the full result set to a file
load() Load a CSV via a LOAD DATA control block
edit_binds() / hover_bind() Bind-parameter helpers
pick_connection() / get_active_conn() Connection helpers
attach(bufnr) Apply editor maps + on_attach to a buffer

Full reference is also available in :help dblite.

Roadmap

  • MySQL support

License

MIT © 2026 Aaron Shahriari

About

No description, website, or topics provided.

Resources

License

Stars

9 stars

Watchers

1 watching

Forks

Packages

 
 
 

Contributors