A frontend for vim-dadbod implemented with the modern neovim lua API
  • Lua 97.4%
  • Shell 1.5%
  • Dockerfile 0.4%
  • HTML 0.3%
  • Makefile 0.2%
  • Other 0.1%
Find a file
Noe Trevino 91ffef2696
All checks were successful
integration / stack (push) Successful in 2s
CI / Check for debug messages (push) Successful in 3s
CI / config (push) Successful in 4s
integration / integration-extra (push) Has been skipped
CI / stylua (push) Successful in 6s
CI / tests (push) Successful in 14s
CI / docs (push) Successful in 8s
integration / integration (push) Successful in 26s
CI / ci (push) Successful in 0s
Merge pull request 'docs: move the templates to .forgejo, leave GitHub a pointer' (#135) from noe.trevino/docs/templates-forgejo into main
2026-09-30 03:42:15 -05:00
.forgejo docs: move the issue and pull request templates to .forgejo, leave GitHub a pointer 2026-09-30 01:41:33 -07:00
.github docs: move the issue and pull request templates to .forgejo, leave GitHub a pointer 2026-09-30 01:41:33 -07:00
autoload docs: describe behavior directly instead of referencing other projects 2026-07-02 17:09:38 -07:00
doc chore(build): auto-generated vimdocs 2026-07-06 00:49:37 +00:00
docs ci: move workflows to .forgejo/workflows 2026-09-29 14:49:16 -07:00
integration ci: publish the runner image by hand, the job needs Docker 2026-09-30 01:02:27 -07:00
lua/dadbod-ui feat(classifier): one module answering what kind of statement this is (#101) 2026-07-18 00:07:39 -07:00
plugin refactor(core): cache dadbod probe, per-node detail highlight, loud hook typos 2026-07-07 01:46:00 -07:00
scripts chore(tests): simplify the output filter to a single grep pattern 2026-07-17 23:46:40 -07:00
specs docs: describe behavior directly instead of referencing other projects 2026-07-02 17:09:38 -07:00
tests feat(classifier): one module answering what kind of statement this is (#101) 2026-07-18 00:07:39 -07:00
.gitignore test: run the suite under lazy.minit + mini.test 2026-07-01 20:47:13 -07:00
.luarc.json feat: vim-dadbod bridge and test harness 2026-06-27 15:24:11 -07:00
AGENTS.md docs: update claude/agents md 2026-07-16 08:05:31 -07:00
CLAUDE.md docs: update claude/agents md 2026-07-16 08:05:31 -07:00
CONFIGURATION.md feat(query): settle scratch buffers on quit instead of prompting 2026-07-15 11:24:01 -07:00
CONTRIBUTING.md docs: point contributors at Forgejo 2026-09-30 00:13:59 -07:00
CURRENT_SUPPORT.md docs: update and create documentation dir 2026-07-16 08:05:32 -07:00
Makefile build: mark publish-runner-image phony 2026-09-30 01:03:59 -07:00
MIGRATION.md fix: data path 2026-07-07 21:31:02 -07:00
README.md docs: point contributors at Forgejo 2026-09-30 00:13:59 -07:00
stylua.toml ci(format): precommit hook and stylua config 2026-06-29 19:07:53 -07:00

dadbod-ui.nvim

Development happens at https://git.thenoetrevino.com/noe.trevino/dadbod-ui.nvim. GitHub is a read-only mirror. Please open issues and pull requests there.

A Neovim-native user interface for tpope/vim-dadbod, written in Lua.

Browse your connections, schemas, and tables in a drawer, fire off queries, and read the results - all without leaving Neovim. It's a ground-up Lua rewrite of vim-dadbod-ui: the same familiar workflow, but API-first, deeply configurable, and typed to the teeth.

Note

Considering migrating from kristijanhusak/vim-dadbod-ui? Read the migration guide for a tutorial

Features

  • A drawer for your databases
    • browse connections, schemas, tables, saved queries, and stored procedures
  • Scratch & saved query buffers
    • SQL filetype so formatters and LSPs attach and work as expected
  • Paginated result buffers
    • don't bomb your ran, keep things quick and responsive
  • Inline query timing and row counts, right where you executed
  • Native CLI export to CSV / TSV / JSON (and consistent Lua formatters as a fallback)
  • A connection picker backed by snacks.nvim, telescope.nvim, or fzf-lua
  • Fully remappable, per-buffer keymaps - and your own named actions
  • Lifecycle hooks for scripting
  • API focused: everything is reachable from require('dadbod-ui.api'), no commands to memorise

Requirements

Installation

Here's a recommended setup for lazy.nvim that shows off the good stuff. Every option is optional - trim it to taste, or drop opts entirely to run on defaults.

local prefix = "<localleader>d"
return {
  "TheNoeTrevino/dadbod-ui.nvim",
  dependencies = { "tpope/vim-dadbod" },
  -- No `:DBUI*` commands are shipped: drive everything from `require('dadbod-ui.api')`.
  -- These `keys` both define your mappings and lazy-load the plugin on first use.
  keys = {
    { prefix .. "d", function() require("dadbod-ui.api").toggle() end, desc = "Toggle Dadbod-UI" },
    { prefix .. "o", function() require("dadbod-ui.api").open() end, desc = "Open Dadbod-UI" },
    { prefix .. "f", function() require("dadbod-ui.api").buf.find() end, desc = "Dadbod-UI find buffer" },
    { prefix .. "a", function() require("dadbod-ui.api").add_connection() end, desc = "Dadbod-UI add connection" },
    { prefix .. "i", function() require("dadbod-ui.api").buf.last_query_info() end, desc = "Dadbod-UI last query info" },
  },
  ---@type DadbodUI.Config
  opts = {
    use_nerd_fonts = true,
    -- Pick a connection with your fuzzy finder of choice (api.pick())
    -- auto goes snacks -> telescope -> fzf -> `vim.ui.select`, picking the first available.
    picker = "snacks", -- "auto" | "snacks" | "telescope" | "fzf" | "fallback"

    drawer = {
      position = "right",
      show_database_icon = true,
      -- Each context's `keys` deep-merges over the defaults, so you only
      -- declare what you change. `false` unbinds a key; `keys = false` disables
      -- the whole context.
      keys = {
        Y = "yank_url", -- a custom action, defined below
      },
    },

    query = {
      execute_on_save = false,
    },

    -- Custom named actions, referenced by name from any context's `keys`. The
    -- drawer ctx carries `{ mode, bufnr, drawer, item, connection }`.
    actions = {
      yank_url = {
        desc = "Yank the connection URL",
        fn = function(ctx) vim.fn.setreg("+", ctx.connection.url) end,
      },
    },

    -- Lifecycle hooks. `resolve_bind_params` supplies bind-param values before
    -- dadbod-ui prompts; `on_connect` may rewrite the url (e.g. inject a secret).
    hooks = {
      resolve_bind_params = function(_names, _known)
        return { [":env"] = vim.env.APP_ENV }
      end,
    },
  },
}

Point it at your databases with vim.g.dbs (or dadbod's other connection sources), then open the drawer:

vim.g.dbs = {
  { name = "dev", url = "postgres://localhost/dev" },
  { name = "staging", url = "postgres://localhost/staging" },
}

Configuration

The block above is a curated slice. For the complete option reference - every setting, its default, and how to use it, plus the deep dive on keymaps, custom actions, and hooks - see CONFIGURATION.md.

Usage

Everything the plugin does is reachable from Lua through require('dadbod-ui.api'). Connections are addressed by name; when a name is reused across groups, disambiguate with "{group}/{name}" or the full key_name from api.list().

local api = require('dadbod-ui.api')

Drawer

api.open()            -- open the drawer (accepts mods, e.g. api.open('tab'))
api.toggle()
api.close()
api.reveal('dev')     -- open + expand + focus a connection (introspects it)
api.refresh('dev')    -- re-scan schemas/tables + reload saved queries

Connections

for _, c in ipairs(api.list()) do print(c.group, c.name, c.is_connected) end
local info = api.info('analytics/prod')  -- url, scheme, tables, schemas, connected…
api.is_connected('dev')

-- Manage the connections.json store (non-interactive; file-backed conns only):
api.add({ name = 'dev', url = 'postgres://localhost/dev', group = 'local' })
api.rename('dev', 'development')
api.duplicate('development', 'dev-copy', 'scratch')  -- clone into another group
api.set_group('dev-copy', 'archive')                 -- '' to ungroup
api.move('development', 'up')                         -- reorder among siblings
api.remove('dev-copy')

-- Live connection lifecycle:
api.connect('dev', function(ok, err) end)  -- async; no-op if already connected
api.disconnect('dev')                      -- drop the live handle (next use reconnects)

Querying

-- Async, returns raw adapter output lines; never opens a result window:
api.query('dev', 'select count(*) from users', function(rows, err)
  if err then return vim.notify(err, vim.log.levels.ERROR) end
  vim.print(rows)
end)

local rows, err = api.query_sync('dev', 'select 1')  -- blocking dual for scripts
api.execute('dev', 'select * from users')            -- run through :DB, open .dbout
api.open_query('dev')                                -- fresh scratch buffer bound to dev
api.buf.switch('prod')                               -- reassign the current query buffer

Introspection

api.tables('dev', function(tables) vim.print(tables) end)
api.schemas('dev', function(schemas) vim.print(schemas) end)
api.introspect('dev', function(data) vim.print(data.tables, data.schemas, data.routines) end)

Export

api.export({ name = 'dev', sql = 'select * from users', format = 'csv', path = '/tmp/users.csv' })

Events

Observe the connect / execute / cancel lifecycle at runtime. Unlike the single-slot hooks in setup{}, any number of listeners can subscribe and they compose with a configured hook rather than replacing it (listeners are observers - an on_connect listener can't rewrite the url).

local handle = api.on('on_execute_query_post', function(ev)
  vim.notify(('query finished in %ss (exit %s)'):format(ev.runtime, ev.exit_status))
  -- ev.rows() reads the result lazily; ev.query is the executed statement
end)

api.off(handle)  -- unsubscribe

Events: on_connect, on_connect_post, on_execute_query, on_execute_query_post, on_cancel_query, on_cancel_query_post.

Custom adapters

Some adapters that are supported by vim-dadbod are not supported by us yet. Make your own with the public API:

Register an adapter
require('dadbod-ui.api').register_adapter({
  name = 'snowflake',                                       -- required; doubles as the url scheme
  table_helpers = { List = 'SELECT * FROM "{table}" LIMIT 200' },
  explain = { plain = 'EXPLAIN {sql}' },
  pagination = 'limit_offset',
})

Feel free to contribute the configuration.

The name doubles as the url scheme, so it has to be one vim-dadbod can already connect to (it ships snowflake, presto, redis, and more that dadbod-ui has no built-in spec for) - dadbod does the connecting and querying, this spec adds dadbod-ui's features on top.

Only name is required; every other field is optional, and an absent field just means that capability is off for the adapter. Reusing a built-in's name replaces it completely (no merge), so extend a built-in by copying its spec first.

The full field-by-field breakdown lives in docs/ADAPTERS.md. The DadbodUI.Adapter type is annotated in lua/dadbod-ui/types.lua.

Migrating from vim-dadbod-ui

Every g:db_ui_* global maps to a grouped opts field, your connections and default keys carry over untouched, and the old :DBUI* commands are a few lines away. See MIGRATION.md for the full mapping.

Support

CURRENT_SUPPORT.md has the full feature-by-adapter support matrix. The roadmap lives in the GitHub milestones.

Contributing

See CONTRIBUTING.md