···11+-- Source: https://github.com/hendrikmi/dotfiles/blob/main/nvim/lua/tools/sql-runner.lua
22+local M = {}
33+44+-- Cache file for storing user-defined commands
55+local cache_dir = vim.fn.stdpath 'data' .. '/sql-runner'
66+local cache_file = cache_dir .. '/commands.json'
77+88+-- Currently selected command
99+M.selected_command = nil
1010+1111+-- Load commands from cache
1212+local function load_commands()
1313+ if vim.fn.filereadable(cache_file) == 0 then
1414+ return {}
1515+ end
1616+1717+ local file = io.open(cache_file, 'r')
1818+ if not file then
1919+ return {}
2020+ end
2121+2222+ local content = file:read '*a'
2323+ file:close()
2424+2525+ local ok, commands = pcall(vim.json.decode, content)
2626+ if ok and commands then
2727+ return commands
2828+ end
2929+3030+ return {}
3131+end
3232+3333+-- Save commands to cache
3434+local function save_commands(commands)
3535+ -- Ensure cache directory exists
3636+ vim.fn.mkdir(cache_dir, 'p')
3737+3838+ local file = io.open(cache_file, 'w')
3939+ if not file then
4040+ vim.notify('[sql-runner] Failed to save commands', vim.log.levels.ERROR)
4141+ return
4242+ end
4343+4444+ file:write(vim.json.encode(commands))
4545+ file:close()
4646+end
4747+4848+-- Find an existing window showing the given absolute path
4949+local function find_win_by_path(abs_path)
5050+ for _, win in ipairs(vim.api.nvim_list_wins()) do
5151+ local buf = vim.api.nvim_win_get_buf(win)
5252+ local name = vim.api.nvim_buf_get_name(buf)
5353+ if name == abs_path then
5454+ return win
5555+ end
5656+ end
5757+ return nil
5858+end
5959+6060+-- Common function to run queries with any backend
6161+local function run_query(name, cmd)
6262+ local outfile = vim.fn.stdpath 'data' .. '/sql-runner/sql.out'
6363+ local abs_out = vim.fn.fnamemodify(outfile, ':p')
6464+6565+ -- Status: running
6666+ vim.api.nvim_echo({ { '[sql-runner] Running ' .. name .. ' query…', 'ModeMsg' } }, false, {})
6767+ local t0 = vim.loop.hrtime()
6868+6969+ -- Run the command
7070+ local full_cmd = cmd .. ' > ' .. vim.fn.shellescape(outfile)
7171+ local vim_cmd = string.format("'<,'>write !%s", full_cmd)
7272+7373+ -- Debug: show exact command being run
7474+ -- vim.api.nvim_echo({ { '[sql-runner] Executing: ' .. vim_cmd, 'Comment' } }, false, {})
7575+7676+ vim.cmd(vim_cmd)
7777+7878+ -- Open or focus existing results split, then reload contents
7979+ local win = find_win_by_path(abs_out)
8080+ if win then
8181+ vim.api.nvim_set_current_win(win)
8282+ vim.cmd 'noautocmd edit' -- reload file without flicker
8383+ else
8484+ vim.cmd('split ' .. vim.fn.fnameescape(outfile))
8585+ end
8686+8787+ -- Done message with timing
8888+ local ms = math.floor((vim.loop.hrtime() - t0) / 1e6)
8989+ vim.api.nvim_echo({ { string.format('[sql-runner] %s query done in %d ms', name, ms), 'ModeMsg' } }, false, {})
9090+end
9191+9292+-- Add a new SQL command
9393+function M.add_command()
9494+ -- Get alias
9595+ vim.ui.input({
9696+ prompt = 'Enter command alias (e.g. postgres, mysql): ',
9797+ }, function(alias)
9898+ if not alias or alias == '' then
9999+ return
100100+ end
101101+102102+ -- Get command
103103+ vim.ui.input({
104104+ prompt = 'Enter command (selection will be piped to this): ',
105105+ }, function(cmd)
106106+ if not cmd or cmd == '' then
107107+ return
108108+ end
109109+110110+ -- Add to cached commands
111111+ local commands = load_commands()
112112+ commands[alias] = cmd
113113+ save_commands(commands)
114114+115115+ vim.notify(string.format('[sql-runner] Added command "%s"', alias), vim.log.levels.INFO)
116116+ end)
117117+ end)
118118+end
119119+120120+-- Remove commands
121121+function M.remove_command()
122122+ local commands = load_commands()
123123+124124+ if vim.tbl_isempty(commands) then
125125+ vim.notify('[sql-runner] No commands to remove.', vim.log.levels.WARN)
126126+ return
127127+ end
128128+129129+ local items = {}
130130+ for alias, cmd in pairs(commands) do
131131+ table.insert(items, alias)
132132+ end
133133+134134+ table.sort(items)
135135+136136+ -- Use vim.ui.select with multiple selection
137137+ local function remove_multiple()
138138+ vim.ui.select(items, {
139139+ prompt = 'Select commands to remove (ESC when done):',
140140+ format_item = function(item)
141141+ return item
142142+ end,
143143+ }, function(choice)
144144+ if choice then
145145+ commands[choice] = nil
146146+147147+ -- Clear selected command if it was removed
148148+ if M.selected_command and M.selected_command.alias == choice then
149149+ M.selected_command = nil
150150+ end
151151+152152+ -- Remove from items list
153153+ for i, item in ipairs(items) do
154154+ if item == choice then
155155+ table.remove(items, i)
156156+ break
157157+ end
158158+ end
159159+160160+ vim.notify(string.format('[sql-runner] Removed command "%s"', choice), vim.log.levels.INFO)
161161+162162+ -- Continue removing if there are more items
163163+ if #items > 0 then
164164+ vim.schedule(function()
165165+ remove_multiple()
166166+ end)
167167+ else
168168+ save_commands(commands)
169169+ vim.notify('[sql-runner] All commands removed.', vim.log.levels.INFO)
170170+ end
171171+ else
172172+ -- User pressed ESC, save and finish
173173+ save_commands(commands)
174174+ if vim.tbl_count(commands) == 0 then
175175+ vim.notify('[sql-runner] All commands removed.', vim.log.levels.INFO)
176176+ end
177177+ end
178178+ end)
179179+ end
180180+181181+ remove_multiple()
182182+end
183183+184184+-- Select a command to use
185185+function M.select_command()
186186+ local commands = load_commands()
187187+188188+ if vim.tbl_isempty(commands) then
189189+ vim.notify('[sql-runner] No commands configured. Use :AddSqlCmd to add one.', vim.log.levels.WARN)
190190+ return
191191+ end
192192+193193+ local items = {}
194194+ for alias, cmd in pairs(commands) do
195195+ table.insert(items, {
196196+ alias = alias,
197197+ cmd = cmd,
198198+ })
199199+ end
200200+201201+ -- Sort items by alias for consistent ordering
202202+ table.sort(items, function(a, b)
203203+ return a.alias < b.alias
204204+ end)
205205+206206+ -- Add currently selected marker
207207+ local current_alias = M.selected_command and M.selected_command.alias or nil
208208+209209+ vim.ui.select(items, {
210210+ prompt = 'Select SQL command:',
211211+ format_item = function(item)
212212+ local marker = (item.alias == current_alias) and ' [current]' or ''
213213+ return item.alias .. marker
214214+ end,
215215+ }, function(choice)
216216+ if choice then
217217+ M.selected_command = choice
218218+ vim.notify(string.format('[sql-runner] Selected "%s"', choice.alias), vim.log.levels.INFO)
219219+ end
220220+ end)
221221+end
222222+223223+-- Run SQL with the selected command
224224+function M.run_sql()
225225+ if not M.selected_command then
226226+ -- If no command selected, prompt to select one
227227+ local commands = load_commands()
228228+229229+ if vim.tbl_isempty(commands) then
230230+ vim.notify('[sql-runner] No commands configured. Use :AddSqlCmd to add one.', vim.log.levels.ERROR)
231231+ return
232232+ end
233233+234234+ local items = {}
235235+ for alias, cmd in pairs(commands) do
236236+ table.insert(items, {
237237+ alias = alias,
238238+ cmd = cmd,
239239+ })
240240+ end
241241+242242+ -- Sort items by alias for consistent ordering
243243+ table.sort(items, function(a, b)
244244+ return a.alias < b.alias
245245+ end)
246246+247247+ vim.ui.select(items, {
248248+ prompt = 'Select SQL command to run:',
249249+ format_item = function(item)
250250+ return item.alias
251251+ end,
252252+ }, function(choice)
253253+ if choice then
254254+ M.selected_command = choice
255255+ run_query(choice.alias, choice.cmd)
256256+ end
257257+ end)
258258+ else
259259+ run_query(M.selected_command.alias, M.selected_command.cmd)
260260+ end
261261+end
262262+263263+-- Register commands
264264+vim.api.nvim_create_user_command('AddSqlCmd', M.add_command, {})
265265+vim.api.nvim_create_user_command('RemoveSqlCmd', M.remove_command, {})
266266+vim.api.nvim_create_user_command('SelectSqlCmd', M.select_command, {})
267267+vim.api.nvim_create_user_command('RunSQL', M.run_sql, { range = true })
268268+269269+-- Set up keymaps
270270+vim.keymap.set('v', '<leader>pr', ':RunSQL<CR>', { silent = true, desc = 'Run SQL with selected backend' })
271271+vim.keymap.set('n', '<leader>ps', ':SelectSqlCmd<CR>', { silent = true, desc = 'Select SQL backend' })
272272+vim.keymap.set('n', '<leader>pa', ':AddSqlCmd<CR>', { silent = true, desc = 'Add SQL command' })
273273+vim.keymap.set('n', '<leader>px', ':RemoveSqlCmd<CR>', { silent = true, desc = 'Remove SQL command' })
274274+275275+return M