├── .gitignore ├── LICENSE ├── README.md └── lua ├── .editorconfig ├── nvim-databasehelper.lua └── nvim-databasehelper ├── dadbod.lua ├── databases ├── functions.lua └── postgresql.lua ├── docker.lua ├── functions.lua ├── lsp.lua └── lsps ├── functions.lua ├── sqlls.lua └── sqls.lua /.gitignore: -------------------------------------------------------------------------------- 1 | .luarc.json 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Andrej Benz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nvim-databasehelper 2 | 3 | # Features 4 | 5 | - define various connections 6 | - discover Docker containers on demand 7 | - discover databases on connections/containers 8 | - change running database connection 9 | - restart LSP with proper connection 10 | - update [vim-dadbod](https://github.com/tpope/vim-dadbod) global connection string 11 | - execute statements (via vim-dadbod) 12 | - execute for different database/connection 13 | - caches Docker information for repeated usage 14 | - start/stop docker containers 15 | 16 | ## Currently supported language servers 17 | 18 | - Postgres: sqls, sqlls is broken (see [here](https://github.com/joe-re/sql-language-server/issues/128)) 19 | 20 | ## Requires 21 | 22 | - [nvim-plenary](https://github.com/nvim-lua/plenary.nvim) (for Docker containers) 23 | - [nvim-lspconfig](https://github.com/neovim/nvim-lspconfig) 24 | - [vim-dadbod](https://github.com/tpope/vim-dadbod) (for statement execution) 25 | 26 | ## Example Setup 27 | 28 | WARNING: don't setup your LSP server manually, as you'll end up with multiple active clients. 29 | 30 | ```lua 31 | require('nvim-databasehelper').setup( 32 | { 33 | lsp = { 34 | sqls = config, -- config you'd pass to lspconfig["sqls"].setup(). Omit the connections! 35 | }, 36 | docker = { 37 | enabled = true, 38 | must_contain = { 'some' }, -- only show Docker containers that contain one of the given strings 39 | defaults = { -- when selecting a Docker container you'll be prompted for various parameters, you can define default values here 40 | postgresql = { 41 | user = 'postgres', 42 | password = 'somePassword', 43 | initial_database = 'testdb', 44 | } 45 | } 46 | }, 47 | dadbod = { 48 | enabled = true, 49 | var = 'dadbodstring', -- global Vim variable to use for dadbod ":DB g: ..." 50 | }, 51 | connections = { 52 | system = { 53 | initial_database = 'benchmark', 54 | driver = 'postgresql', 55 | host = '127.0.0.1', 56 | port = '5432', 57 | user = 'postgres', 58 | password = '', 59 | } 60 | }, 61 | initial_window_height = 10, 62 | } 63 | ) 64 | ``` 65 | 66 | ## Commands 67 | 68 | | Command | Function | 69 | | ------------------- | -------------------------------------------------------------------------------------------------------------- | 70 | | SwitchDatabase | switch database. Autocomplete or select window. | 71 | | ExecuteOnDatabase | execute buffer or visual selection on specific database. | 72 | | ExecuteOnConnection | execute buffer or visual selection on specific connection. | 73 | | OpenDatabaseWindow | opens a new buffer in the current window where you can write your query. Useful if you want LSP functionality. | 74 | | StartContainer | Starts the selected docker container | 75 | | StopContainer | Stops the selected docker container | 76 | -------------------------------------------------------------------------------- /lua/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.lua] 2 | indent_style = space 3 | indent_size = 4 4 | quote_style = single 5 | -------------------------------------------------------------------------------- /lua/nvim-databasehelper.lua: -------------------------------------------------------------------------------- 1 | local functions = require('nvim-databasehelper.functions') 2 | local lsps = require('nvim-databasehelper.lsps.functions') 3 | local docker = require('nvim-databasehelper.docker') 4 | local dadbod = require('nvim-databasehelper.dadbod') 5 | 6 | local M = {} 7 | 8 | local default = { 9 | lsp = {}, 10 | connections = {}, 11 | dadbod = { 12 | enabled = false, 13 | var = 'prod' 14 | }, 15 | initial_connection = '', 16 | docker = { 17 | enabled = false, 18 | must_contain = {}, 19 | defaults = { 20 | driver = 'postgresql', 21 | postgresql = { 22 | user = '', 23 | password = '', 24 | initial_database = '', 25 | } 26 | } 27 | }, 28 | initial_window_height = 10, 29 | } 30 | 31 | local setup_commands = function(dadbodconf, dockerconf) 32 | vim.api.nvim_create_user_command('OpenDatabaseWindow', functions.open_database_window, { nargs = 0 }) 33 | 34 | vim.api.nvim_create_user_command('SwitchDatabase', 35 | function(...) 36 | functions.set_current_database({ ... }) 37 | end, 38 | { nargs = '?', complete = functions.get_databases } 39 | ) 40 | 41 | if dockerconf.enabled then 42 | vim.api.nvim_create_user_command('StartContainer', 43 | function(...) 44 | docker.handle_container({ ... }, 'start') 45 | end, 46 | { nargs = '?', range = true, 47 | complete = function() return docker.list_containers({ 'ps', '--filter', 'status=exited', '--format', 48 | '{{.Names}}' }) 49 | end } 50 | ) 51 | 52 | vim.api.nvim_create_user_command('StopContainer', 53 | function(...) 54 | docker.handle_container({ ... }, 'stop') 55 | end, 56 | { nargs = '?', range = true, 57 | complete = function() return docker.list_containers({ 'ps', '--format', '{{.Names}}' }) end } 58 | ) 59 | end 60 | 61 | if dadbodconf.enabled then 62 | vim.api.nvim_create_user_command('ExecuteOnConnection', 63 | function(...) 64 | functions.execute_on_connection({ ... }) 65 | end, 66 | { nargs = '?', range = true, complete = functions.get_connections } 67 | ) 68 | 69 | vim.api.nvim_create_user_command('ExecuteOnDatabase', 70 | function(...) 71 | functions.execute_on_database({ ... }) 72 | end, 73 | { nargs = '?', range = true, complete = functions.get_databases } 74 | ) 75 | end 76 | end 77 | 78 | M.setup = function(opt) 79 | local config = vim.tbl_deep_extend('force', default, opt or {}) 80 | 81 | functions.config = config 82 | 83 | if config.initial_connection ~= '' then 84 | functions.connection = config.connections[config.initial_connection] 85 | functions.database = config.connections[config.initial_connection].initial_database 86 | end 87 | 88 | if config.docker.enabled then 89 | docker.config = config.docker 90 | end 91 | 92 | if config.dadbod.enabled then 93 | dadbod.config = config.dadbod 94 | 95 | if functions.database ~= nil and functions.connection ~= nil then 96 | dadbod.set_global(functions.connection, functions.database) 97 | end 98 | end 99 | 100 | for k, v in pairs(config.lsp) do 101 | if functions.database ~= nil and functions.connection ~= nil then 102 | lsps[k](v, functions.connection, functions.database) 103 | end 104 | end 105 | 106 | setup_commands(config.dadbod, config.docker) 107 | end 108 | 109 | return M 110 | -------------------------------------------------------------------------------- /lua/nvim-databasehelper/dadbod.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | M.config = nil 3 | 4 | local drivers = { 5 | postgresql = 'postgres' 6 | } 7 | 8 | local get_string = function(connection, database) 9 | local user_string = connection.user 10 | 11 | if connection.password ~= '' then 12 | user_string = connection.user .. ':' .. connection.password 13 | end 14 | 15 | return drivers[connection.driver] .. 16 | '://' .. user_string .. '@' .. connection.host .. ':' .. connection.port .. '/' .. database 17 | end 18 | 19 | M.set_global = function(connection, database) 20 | vim.g[M.config.var] = get_string(connection, database) 21 | end 22 | 23 | M.execute = function(connection, database, args) 24 | local pre = '' 25 | 26 | if args[1].range > 0 and args[1].line1 ~= '' and args[1].line2 ~= '' then 27 | pre = tostring(args[1].line1) .. ',' .. tostring(args[1].line2) 28 | end 29 | 30 | local final = pre .. '%DB ' .. get_string(connection, database) 31 | 32 | vim.cmd(final) 33 | end 34 | 35 | return M 36 | -------------------------------------------------------------------------------- /lua/nvim-databasehelper/databases/functions.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | local database_functions = { 4 | postgresql = require('nvim-databasehelper.databases.postgresql').get_databases 5 | } 6 | 7 | M.get_databases = function(connection) 8 | return database_functions[connection.driver](connection) 9 | end 10 | 11 | return M 12 | -------------------------------------------------------------------------------- /lua/nvim-databasehelper/databases/postgresql.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | M.get_databases = function(connection) 4 | vim.env.PGPASSWORD = connection.password 5 | 6 | local Job = require 'plenary.job' 7 | 8 | local job = Job:new { 9 | command = 'psql', 10 | args = { '-h', connection.host, '-p', connection.port, '-U', connection.user, '-t', '-c', 11 | 'SELECT datname FROM pg_database WHERE datname <> ALL (\'{template0,template1,postgres}\')' } 12 | } 13 | 14 | job:sync() 15 | local result = job:result() 16 | 17 | local databases = {} 18 | 19 | for _, value in pairs(result) do 20 | value = vim.trim(value) 21 | 22 | if value ~= '' then 23 | table.insert(databases, value) 24 | end 25 | end 26 | 27 | return databases 28 | end 29 | 30 | return M 31 | -------------------------------------------------------------------------------- /lua/nvim-databasehelper/docker.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | M.config = nil 3 | M.cache = {} 4 | 5 | local supported_drivers = { 'postgresql' } 6 | 7 | local get_container_host = function(containers, input) 8 | for _, value in pairs(containers) do 9 | for key, host in pairs(value) do 10 | if input == key then 11 | return host 12 | end 13 | end 14 | end 15 | 16 | return nil 17 | end 18 | 19 | local create_container_list = function(result, ignore_cache) 20 | local containers = {} 21 | 22 | for _, value in pairs(result) do 23 | local parts = vim.split(value, ';') 24 | local name = parts[1] 25 | 26 | local found = false 27 | 28 | if #M.config.must_contain ~= 0 then 29 | for _, filter in pairs(M.config.must_contain) do 30 | if string.find(name, filter) then 31 | found = true 32 | end 33 | end 34 | else 35 | found = true 36 | end 37 | 38 | if found then 39 | local host = vim.split(parts[2], '-')[1] 40 | 41 | if M.cache[name] == nil or ignore_cache then 42 | table.insert(containers, { [name] = host }) 43 | end 44 | end 45 | end 46 | 47 | return containers 48 | end 49 | 50 | M.list_containers = function(args) 51 | local Job = require 'plenary.job' 52 | local job = Job:new { 53 | command = 'docker', 54 | args = args 55 | } 56 | 57 | job:sync() 58 | local result = job:result() 59 | 60 | local containers = {} 61 | 62 | for _, v in pairs(result) do 63 | table.insert(containers, v) 64 | end 65 | 66 | return containers 67 | end 68 | 69 | M.handle_container = function(args, action) 70 | local arg = args[1].args 71 | 72 | local listFunc = function() return M.list_containers({ 'ps', '--format', '{{.Names}}' }) 73 | end 74 | 75 | if action == 'start' then 76 | listFunc = function() return M.list_containers({ 'ps', '--filter', 'status=exited', '--format', 77 | '{{.Names}}' }) 78 | end 79 | end 80 | 81 | if arg == '' then 82 | vim.ui.select( 83 | listFunc(), 84 | { prompt = 'Select container:' }, 85 | function(selection) 86 | local Job = require 'plenary.job' 87 | local job = Job:new { 88 | command = 'docker', 89 | args = { action, selection } 90 | } 91 | 92 | job:sync() 93 | end 94 | ) 95 | else 96 | local Job = require 'plenary.job' 97 | local job = Job:new { 98 | command = 'docker', 99 | args = { action, arg } 100 | } 101 | 102 | job:sync() 103 | end 104 | end 105 | 106 | 107 | M.get_containers = function(ignore_cache) 108 | local Job = require 'plenary.job' 109 | local job = Job:new { 110 | command = 'docker', 111 | args = { 'ps', '--format', '{{.Names}};{{.Ports}}' } 112 | } 113 | 114 | job:sync() 115 | local result = job:result() 116 | 117 | return create_container_list(result, ignore_cache) 118 | end 119 | 120 | M.handle_selection = function(selection) 121 | if M.cache[selection] ~= nil then 122 | return M.cache[selection] 123 | end 124 | 125 | local containers = M.get_containers() 126 | local config = {} 127 | local container_host = get_container_host(containers, selection) 128 | 129 | if container_host == nil then 130 | return nil 131 | end 132 | 133 | local host_port = vim.split(container_host, ':') 134 | config.host = host_port[1] 135 | config.port = host_port[2] 136 | 137 | vim.ui.input({ prompt = 'Driver (default = ' .. M.config.defaults.driver .. '): ' }, 138 | function(input) 139 | config.driver = input or M.config.defaults.driver 140 | end) 141 | 142 | if not vim.tbl_contains(supported_drivers, config.driver) then 143 | print('Unsupported driver.') 144 | return nil; 145 | end 146 | 147 | local d = vim.tbl_get(M.config.defaults, config.driver) 148 | 149 | if d ~= nil then 150 | config = vim.tbl_deep_extend('force', config, d) 151 | end 152 | 153 | local user_prompt = 'Username: ' 154 | if config.user ~= '' then 155 | user_prompt = 'Username (default = ' .. config.user .. '): ' 156 | end 157 | 158 | local password_prompt = 'Password: ' 159 | if config.password ~= '' then 160 | password_prompt = 'Password (default: ): ' 161 | end 162 | 163 | local database_prompt = 'Initial Database: ' 164 | if config.initial_database ~= '' then 165 | database_prompt = 'Initial Database (default = ' .. config.initial_database .. '): ' 166 | end 167 | 168 | vim.ui.input({ prompt = user_prompt }, 169 | function(input) config.user = input or config.user end) 170 | 171 | vim.ui.input({ prompt = password_prompt }, 172 | function(input) config.password = input or config.password end) 173 | 174 | vim.ui.input({ prompt = database_prompt }, 175 | function(input) config.initial_database = input or config.initial_database end) 176 | 177 | M.cache[selection] = config 178 | 179 | return config 180 | end 181 | 182 | return M 183 | -------------------------------------------------------------------------------- /lua/nvim-databasehelper/functions.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | M.database = nil 3 | M.connection = nil 4 | M.config = nil 5 | 6 | local filetypes = { 7 | postgresql = 'sql' 8 | } 9 | 10 | local docker = require('nvim-databasehelper.docker') 11 | local lsp = require('nvim-databasehelper.lsp') 12 | local dadbod = require('nvim-databasehelper.dadbod') 13 | local databases = require('nvim-databasehelper.databases.functions') 14 | 15 | local get_databases = function(connections) 16 | local list = {} 17 | 18 | for name, connection in pairs(connections) do 19 | local result = databases.get_databases(connection) 20 | 21 | for _, v in pairs(result) do 22 | table.insert(list, name .. ': ' .. v) 23 | end 24 | end 25 | 26 | return list 27 | end 28 | 29 | M.get_databases = function() 30 | local list = {} 31 | 32 | for _, v in pairs(get_databases(M.config.connections)) do 33 | table.insert(list, v) 34 | end 35 | 36 | for _, v in pairs(get_databases(docker.cache)) do 37 | table.insert(list, v) 38 | end 39 | 40 | if M.config.docker.enabled then 41 | local result = docker.get_containers(false) 42 | 43 | for _, v in pairs(result) do 44 | for k, _ in pairs(v) do 45 | table.insert(list, 'docker: ' .. k) 46 | end 47 | end 48 | end 49 | 50 | return list 51 | end 52 | 53 | local set_database = function(connection, database) 54 | M.database = database 55 | M.connection = connection 56 | 57 | if M.config.dadbod.enabled then 58 | dadbod.set_global(M.connection, M.database) 59 | end 60 | 61 | lsp.restart_clients(M.config.lsp, M.connection, M.database) 62 | end 63 | 64 | local perform_with_database = function(input, action) 65 | local parts = vim.split(input, ' ') 66 | local connection = table.remove(parts, 1):gsub(':', '') 67 | local database_or_container = table.concat(parts, ' ') 68 | 69 | if M.config.docker.enabled then 70 | if connection == 'docker' then 71 | local container = table.concat(parts, ' ') 72 | local docker_connection = docker.handle_selection(container) 73 | 74 | local list = {} 75 | 76 | local result = databases.get_databases(docker_connection) 77 | 78 | for _, v in pairs(result) do 79 | table.insert(list, v) 80 | end 81 | 82 | if docker_connection ~= nil then 83 | vim.ui.select( 84 | list, 85 | { prompt = 'Select database:' }, 86 | function(selection) 87 | action(docker_connection, selection) 88 | end 89 | ) 90 | end 91 | 92 | return 93 | end 94 | 95 | if docker.cache[connection] ~= nil then 96 | action(docker.cache[connection], database_or_container) 97 | end 98 | end 99 | 100 | action(M.config.connections[connection], database_or_container) 101 | end 102 | 103 | M.set_current_database = function(args) 104 | local arg = args[1].args 105 | 106 | if arg == '' then 107 | vim.ui.select( 108 | M.get_databases(), 109 | { prompt = 'Select database:' }, 110 | function(selection) 111 | perform_with_database(selection, set_database) 112 | end 113 | ) 114 | else 115 | perform_with_database(arg, set_database) 116 | end 117 | end 118 | 119 | M.open_database_window = function() 120 | local api = vim.api 121 | api.nvim_command('belowright split dbh.in') 122 | api.nvim_win_set_height(0, M.config.initial_window_height) 123 | api.nvim_command('set ft=' .. filetypes[M.connection.driver]) 124 | api.nvim_command('setlocal bt=nofile') 125 | end 126 | 127 | 128 | M.get_connections = function() 129 | local list = {} 130 | 131 | for k, _ in pairs(M.config.connections) do 132 | table.insert(list, k) 133 | end 134 | 135 | if M.config.docker.enabled then 136 | for _, v in pairs(docker.get_containers(true)) do 137 | for container, _ in pairs(v) do 138 | table.insert(list, 'docker: ' .. container) 139 | end 140 | end 141 | end 142 | 143 | return list 144 | end 145 | 146 | local get_connection_data = function(connection) 147 | local parts = vim.split(connection, ' ') 148 | 149 | if #parts > 1 then 150 | table.remove(parts, 1) 151 | end 152 | 153 | connection = table.concat(parts, ' ') 154 | 155 | local res = nil 156 | 157 | if M.config.connections[connection] ~= nil then 158 | res = M.config.connections[connection] 159 | end 160 | 161 | if M.config.docker.enabled and res == nil then 162 | res = docker.handle_selection(connection) 163 | end 164 | 165 | return res 166 | end 167 | 168 | M.execute_on_connection = function(args) 169 | local connection = args[1].args 170 | 171 | if connection == '' then 172 | vim.ui.select( 173 | M.get_connections(), 174 | { prompt = 'Select connection:' }, 175 | function(selection) 176 | dadbod.execute(get_connection_data(selection), '', args) 177 | end 178 | ) 179 | else 180 | dadbod.execute(get_connection_data(connection), '', args) 181 | end 182 | end 183 | 184 | M.execute_on_database = function(args) 185 | local arg = args[1].args 186 | 187 | if arg == '' then 188 | vim.ui.select( 189 | M.get_databases(), 190 | { prompt = 'Select database:' }, 191 | function(selection) 192 | perform_with_database(selection, function(connection, database) 193 | dadbod.execute(connection, database, args) 194 | end) 195 | end 196 | ) 197 | else 198 | perform_with_database(arg, function(connection, database) 199 | dadbod.execute(connection, database, args) 200 | end) 201 | end 202 | end 203 | 204 | return M 205 | -------------------------------------------------------------------------------- /lua/nvim-databasehelper/lsp.lua: -------------------------------------------------------------------------------- 1 | local lsps = require('nvim-databasehelper.lsps.functions') 2 | 3 | local M = {} 4 | 5 | M.stop_clients = function(lsp) 6 | local clients = vim.lsp.get_active_clients() 7 | 8 | for k, _ in pairs(lsp) do 9 | local client = nil 10 | 11 | for _, c in pairs(clients) do 12 | if c.name == k then 13 | client = c 14 | end 15 | end 16 | 17 | if client ~= nil then 18 | vim.lsp.stop_client(client.id, true) 19 | end 20 | end 21 | end 22 | 23 | M.restart_clients = function(config, connection, database) 24 | M.stop_clients(config) 25 | 26 | for k, v in pairs(config) do 27 | lsps[k](v, connection, database) 28 | end 29 | end 30 | 31 | return M 32 | -------------------------------------------------------------------------------- /lua/nvim-databasehelper/lsps/functions.lua: -------------------------------------------------------------------------------- 1 | local M = { 2 | sqls = require('nvim-databasehelper.lsps.sqls'), 3 | sqlls = require('nvim-databasehelper.lsps.sqlls') 4 | } 5 | 6 | return M 7 | -------------------------------------------------------------------------------- /lua/nvim-databasehelper/lsps/sqlls.lua: -------------------------------------------------------------------------------- 1 | local start = function(lsp_config, connection, database) 2 | local new_config = { 3 | settings = { 4 | sqlLanguageServer = { 5 | connections = { 6 | { 7 | adapter = connection.driver, 8 | host = connection.host, 9 | port = connection.port, 10 | user = connection.user, 11 | password = connection.password, 12 | database = database 13 | } 14 | } 15 | } 16 | } 17 | } 18 | 19 | local merged_config = vim.tbl_deep_extend('force', lsp_config, new_config) 20 | 21 | require('lspconfig')['sqlls'].setup(merged_config) 22 | end 23 | 24 | return start 25 | -------------------------------------------------------------------------------- /lua/nvim-databasehelper/lsps/sqls.lua: -------------------------------------------------------------------------------- 1 | local start = function(lsp_config, connection, database) 2 | local dataSourceName = 'host=' .. 3 | connection.host .. 4 | ' port=' .. 5 | connection.port .. ' user=' .. 6 | connection.user .. ' password=' .. connection.password .. ' sslmode=disable dbname=' .. database 7 | 8 | local new_config = { 9 | settings = { 10 | sqls = { 11 | connections = { 12 | { 13 | driver = connection.driver, 14 | dataSourceName = dataSourceName 15 | } 16 | } 17 | } 18 | } 19 | } 20 | 21 | local merged_config = vim.tbl_deep_extend('force', lsp_config, new_config) 22 | 23 | require('lspconfig')['sqls'].setup(merged_config) 24 | end 25 | 26 | return start 27 | --------------------------------------------------------------------------------