├── rplugin └── python3 │ └── database │ ├── states │ ├── __init__.py │ └── state.py │ ├── utils │ ├── __init__.py │ ├── strings.py │ ├── files.py │ ├── log.py │ ├── ascii_table.py │ └── nvim.py │ ├── views │ ├── __init__.py │ ├── database_window.py │ └── query_window.py │ ├── concurrents │ ├── __init__.py │ ├── executors.py │ └── executor_service.py │ ├── configs │ ├── __init__.py │ └── config.py │ ├── sql_clients │ ├── __init__.py │ ├── sql_client_factory.py │ ├── sql_client.py │ ├── sqlite_client.py │ ├── psql_client.py │ └── mysql_client.py │ ├── storages │ ├── __init__.py │ └── connection.py │ ├── transitions │ ├── __init__.py │ ├── shared │ │ ├── __init__.py │ │ ├── get_current_row_idx.py │ │ ├── show_ascii_table.py │ │ ├── get_primary_key_value.py │ │ └── show_table_data.py │ ├── view_ops.py │ ├── database_ops.py │ ├── lsp_ops.py │ ├── table_ops.py │ ├── query_ops.py │ ├── data_ops.py │ └── connection_ops.py │ └── __init__.py ├── .gitignore ├── LICENSE ├── plugin └── vim_database.vim └── README.md /rplugin/python3/database/states/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rplugin/python3/database/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rplugin/python3/database/views/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rplugin/python3/database/concurrents/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rplugin/python3/database/configs/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rplugin/python3/database/sql_clients/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rplugin/python3/database/storages/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rplugin/python3/database/transitions/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rplugin/python3/database/transitions/shared/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | __pycache__ 3 | 4 | # Mac 5 | .DS_Store 6 | 7 | .idea/ 8 | -------------------------------------------------------------------------------- /rplugin/python3/database/utils/strings.py: -------------------------------------------------------------------------------- 1 | def string_compose(target: str, pos: int, source: str) -> str: 2 | if source == '' or pos < 0: 3 | return target 4 | 5 | result = target[0:pos] 6 | if len(result) < pos: 7 | result += (" " * pos - len(result)) 8 | result += source 9 | result += ' ' + target[pos + len(source) + 1:] 10 | 11 | return result 12 | -------------------------------------------------------------------------------- /rplugin/python3/database/concurrents/executors.py: -------------------------------------------------------------------------------- 1 | from asyncio import get_running_loop 2 | from functools import partial 3 | from typing import Any, Callable, TypeVar 4 | 5 | T = TypeVar("T") 6 | 7 | 8 | async def run_in_executor(func: Callable[..., T], *args: Any, **kwargs: Any) -> T: 9 | loop = get_running_loop() 10 | return await loop.run_in_executor(None, partial(func, *args, **kwargs)) 11 | -------------------------------------------------------------------------------- /rplugin/python3/database/transitions/shared/get_current_row_idx.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from ...states.state import State 4 | from ...views.database_window import get_current_database_window_row 5 | 6 | 7 | def get_current_row_idx(state: State) -> Optional[int]: 8 | _, rows = state.table_data 9 | # Minus 4 for header of the table 10 | row = get_current_database_window_row() - 4 11 | 12 | return None if row < 0 or row >= len(rows) else row 13 | -------------------------------------------------------------------------------- /rplugin/python3/database/utils/files.py: -------------------------------------------------------------------------------- 1 | import os 2 | from os import path 3 | 4 | 5 | def is_file_exists(file_path: str) -> bool: 6 | return path.exists(file_path) and path.isfile(file_path) 7 | 8 | 9 | def create_folder_if_not_present(folder_path: str) -> None: 10 | if not is_folder_exists(folder_path): 11 | os.makedirs(folder_path) 12 | 13 | 14 | def is_folder_exists(folder_path: str) -> bool: 15 | return path.exists(folder_path) and path.isdir(folder_path) 16 | -------------------------------------------------------------------------------- /rplugin/python3/database/transitions/shared/show_ascii_table.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | 3 | from ...configs.config import UserConfig 4 | from ...utils.ascii_table import ascii_table 5 | from ...utils.nvim import ( 6 | async_call, 7 | set_cursor, 8 | render, 9 | ) 10 | from ...views.database_window import ( 11 | open_database_window,) 12 | 13 | 14 | async def show_ascii_table(configs: UserConfig, headers: list, rows: list) -> None: 15 | window = await async_call(partial(open_database_window, configs)) 16 | 17 | await async_call(partial(render, window, ascii_table(headers, rows))) 18 | await async_call(partial(set_cursor, window, (4, 0))) 19 | -------------------------------------------------------------------------------- /rplugin/python3/database/sql_clients/sql_client_factory.py: -------------------------------------------------------------------------------- 1 | from .mysql_client import MySqlClient 2 | from .psql_client import PostgreSqlClient 3 | from .sql_client import SqlClient 4 | from .sqlite_client import SqliteClient 5 | from ..storages.connection import Connection, ConnectionType 6 | 7 | 8 | class SqlClientFactory(object): 9 | 10 | @staticmethod 11 | def create(connection: Connection) -> SqlClient: 12 | if connection.connection_type == ConnectionType.SQLITE: 13 | return SqliteClient(connection) 14 | if connection.connection_type == ConnectionType.MYSQL: 15 | return MySqlClient(connection) 16 | if connection.connection_type == ConnectionType.POSTGRESQL: 17 | return PostgreSqlClient(connection) 18 | assert 0, "Bad sql client creation: " + connection.connection_type.to_string() 19 | -------------------------------------------------------------------------------- /rplugin/python3/database/transitions/shared/get_primary_key_value.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | from typing import Tuple, Optional 3 | 4 | from ...concurrents.executors import run_in_executor 5 | from ...states.state import State 6 | from ...utils.log import log 7 | 8 | 9 | async def get_primary_key_value(state: State, row: int) -> Tuple[Optional[str], Optional[str]]: 10 | primary_key = await run_in_executor( 11 | partial(state.sql_client.get_primary_key, state.selected_database, state.selected_table)) 12 | if primary_key is None: 13 | log.info("[vim-database] No primary key found for table " + state.selected_table) 14 | return None, None 15 | 16 | headers, rows = state.table_data 17 | 18 | for header_idx, header in enumerate(headers): 19 | if header == primary_key: 20 | return primary_key, rows[row][header_idx] 21 | 22 | # Not reachable 23 | return None, None 24 | -------------------------------------------------------------------------------- /rplugin/python3/database/concurrents/executor_service.py: -------------------------------------------------------------------------------- 1 | from concurrent.futures import Future 2 | from queue import SimpleQueue 3 | from threading import Thread 4 | from typing import Any, Callable, TypeVar 5 | 6 | T = TypeVar("T") 7 | 8 | 9 | class ExecutorService: 10 | 11 | def __init__(self) -> None: 12 | self.__thread = Thread(target=self._loop, daemon=True) 13 | self._queue: SimpleQueue = SimpleQueue() 14 | self.__thread.start() 15 | 16 | def _loop(self) -> None: 17 | while True: 18 | func = self._queue.get() 19 | func() 20 | 21 | def run_sync(self, func: Callable[..., T], *args: Any, **kwargs: Any) -> Future: 22 | future = Future() 23 | 24 | def run() -> None: 25 | try: 26 | future.set_result(func(*args, **kwargs)) 27 | except BaseException as e: 28 | future.set_exception(e) 29 | 30 | self._queue.put_nowait(run) 31 | 32 | return future 33 | -------------------------------------------------------------------------------- /rplugin/python3/database/utils/log.py: -------------------------------------------------------------------------------- 1 | from logging import Formatter, Handler, LogRecord, ERROR, INFO, getLogger 2 | 3 | from pynvim import Nvim 4 | 5 | _LOGGER_NAME = 'VimDatabase' 6 | 7 | _DATE_FMT = "%Y-%m-%d %H:%M:%S" 8 | _LOG_FMT = """ 9 | -- {name} level: {levelname} 10 | time: {asctime} 11 | module: {module} 12 | line: {lineno} 13 | function: {funcName} 14 | {message} 15 | """ 16 | 17 | 18 | def init_log(nvim: Nvim) -> None: 19 | 20 | class NvimHandler(Handler): 21 | 22 | def handle(self, log_record: LogRecord) -> None: 23 | message = self.format(log_record) 24 | if log_record.levelno >= ERROR: 25 | nvim.async_call(nvim.err_write, message) 26 | else: 27 | nvim.async_call(nvim.out_write, message) 28 | 29 | handler = NvimHandler() 30 | handler.setFormatter(Formatter(fmt=_LOG_FMT, datefmt=_DATE_FMT, style="{")) 31 | 32 | logger = getLogger(_LOGGER_NAME) 33 | logger.addHandler(handler) 34 | logger.setLevel(INFO) 35 | 36 | 37 | log = getLogger(_LOGGER_NAME) 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Huy Duong 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 | -------------------------------------------------------------------------------- /rplugin/python3/database/utils/ascii_table.py: -------------------------------------------------------------------------------- 1 | def ascii_table(headers: list, rows: list) -> list: 2 | lines = [] 3 | lens = [] 4 | num_rows = len(rows) 5 | num_columns = len(headers) 6 | for i in range(num_rows): 7 | while len(rows[i]) < num_columns: 8 | rows[i].append("") 9 | 10 | while len(rows[i]) > num_columns: 11 | rows[i] = rows[i][:-1] 12 | 13 | for i in range(num_columns): 14 | lens.append(len(max([x[i] for x in rows] + [headers[i]], key=lambda x: len(str(x))))) 15 | formats = [] 16 | hformats = [] 17 | for i in range(len(headers)): 18 | formats.append("%%-%ds" % lens[i]) 19 | hformats.append("%%-%ds" % lens[i]) 20 | pattern = " | ".join(formats) 21 | hpattern = " | ".join(hformats) 22 | separator = "+-" + "-+-".join(['-' * n for n in lens]) + "-+" 23 | lines.append(separator) 24 | lines.append("| " + hpattern % tuple(headers) + " |") 25 | lines.append(separator) 26 | for line in rows: 27 | lines.append("| " + pattern % tuple(t for t in line) + " |") 28 | 29 | if len(rows) == 0: 30 | pattern = " ".join(formats) 31 | lines.append("| " + pattern % tuple(" " for _ in headers) + " |") 32 | 33 | lines.append(separator) 34 | 35 | return lines 36 | -------------------------------------------------------------------------------- /plugin/vim_database.vim: -------------------------------------------------------------------------------- 1 | if exists('s:vim_database_loaded') 2 | finish 3 | endif 4 | 5 | let s:vim_database_loaded = 1 6 | 7 | let g:database_workspace = getcwd() 8 | 9 | function! s:IsWinExist(winid) abort 10 | return !empty(getwininfo(a:winid)) 11 | endfunction 12 | 13 | function! CloseVimDatabaseQuery(bufnr, ...) abort 14 | call CloseVimDatabaseQueryBorder(a:bufnr) 15 | 16 | let s:winids = win_findbuf(a:bufnr) 17 | for winid in s:winids 18 | call nvim_win_close(winid, v:true) 19 | endfor 20 | endfunction 21 | 22 | function! CloseVimDatabaseQueryBorder(bufnr, ...) abort 23 | let s:winid = getbufvar(a:bufnr, 'border_winid', -1) 24 | if s:winid != v:null && s:IsWinExist(s:winid) 25 | call nvim_win_close(s:winid, v:true) 26 | endif 27 | 28 | call setbufvar(a:bufnr, 'border_winid', -1) 29 | endfunction 30 | 31 | function! s:VimDatabaseSelectTable(table) abort 32 | call VimDatabase_select_table_fzf(a:table) 33 | endfunction 34 | 35 | function! VimDatabaseSelectTables(tables) abort 36 | call fzf#run(fzf#wrap({ 37 | \ 'source': a:tables, 38 | \ 'sink': function('s:VimDatabaseSelectTable') 39 | \ })) 40 | endfunction 41 | 42 | command! VimDatabaseListTablesFzf call VimDatabase_list_tables_fzf() 43 | 44 | if get(g:, 'huy_duong_workspace', 0) == 1 45 | nnoremap dbb :VDToggleDatabase 46 | nnoremap dbr :VDToggleQuery 47 | 48 | let g:vim_database_window_layout = "below" 49 | let g:vim_database_window_size = 25 50 | endif 51 | 52 | -------------------------------------------------------------------------------- /rplugin/python3/database/storages/connection.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shelve 3 | from dataclasses import dataclass 4 | from enum import Enum 5 | from hashlib import md5 6 | from os import path 7 | from typing import Optional, Iterator 8 | 9 | _CONNECTION_TYPES = {1: "SQLite", 2: "MySQL", 3: "PostgreSQL"} 10 | 11 | 12 | class ConnectionType(Enum): 13 | SQLITE = 1 14 | MYSQL = 2 15 | POSTGRESQL = 3 16 | 17 | def to_string(self) -> str: 18 | return _CONNECTION_TYPES[self.value] 19 | 20 | 21 | @dataclass(frozen=True) 22 | class Connection: 23 | name: str 24 | connection_type: ConnectionType 25 | host: Optional[str] 26 | port: Optional[str] 27 | username: Optional[str] 28 | password: Optional[str] 29 | database: Optional[str] 30 | 31 | 32 | def store_connection(connection: Connection) -> None: 33 | with shelve.open(_get_connection_store_file_path()) as connection_store: 34 | connection_store[connection.name] = connection 35 | 36 | 37 | def remove_connection(connection: Connection) -> None: 38 | with shelve.open(_get_connection_store_file_path()) as connection_store: 39 | del connection_store[connection.name] 40 | 41 | 42 | def get_connections() -> Iterator[Connection]: 43 | with shelve.open(_get_connection_store_file_path()) as connection_store: 44 | for connection_name in connection_store: 45 | yield connection_store[connection_name] 46 | 47 | 48 | def _get_connection_store_file_path() -> str: 49 | connection_store_file_name = md5(os.getcwd().encode('utf-8')).hexdigest() 50 | return path.join(path.expanduser("~"), ".vim-database", connection_store_file_name) 51 | -------------------------------------------------------------------------------- /rplugin/python3/database/transitions/shared/show_table_data.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | 3 | from .show_ascii_table import show_ascii_table 4 | from ...concurrents.executors import run_in_executor 5 | from ...configs.config import UserConfig 6 | from ...states.state import Mode, State 7 | from ...utils.log import log 8 | 9 | 10 | async def show_table_data(configs: UserConfig, state: State, table: str) -> None: 11 | if not state.selected_connection: 12 | log.info("[vim-database] No connection found") 13 | return 14 | 15 | query = "SELECT * FROM " + table 16 | if state.query_conditions is not None: 17 | query += " WHERE " + state.query_conditions 18 | if state.order is not None: 19 | ordering_column, order = state.order 20 | query += " ORDER BY " + ordering_column + " " + order 21 | query += " LIMIT " + str(configs.rows_limit) 22 | query += " OFFSET " + str(configs.rows_limit * (state.current_page - 1)) 23 | 24 | table_content = await run_in_executor(partial(state.sql_client.run_query, state.selected_database, query)) 25 | if table_content is None: 26 | # Error 27 | state.query_conditions = None 28 | return 29 | 30 | table_empty = len(table_content) == 0 31 | headers = [table] if table_empty else table_content[0] 32 | rows = [] if table_empty else table_content[1:] 33 | state.selected_table = table 34 | state.table_data = (headers, rows) 35 | state.mode = Mode.QUERY 36 | state.user_query = False 37 | 38 | if state.filtered_columns: 39 | filtered_idx = \ 40 | set([header_idx for header_idx, header in enumerate(headers) if header in state.filtered_columns]) 41 | headers = [header for header in headers if header in state.filtered_columns] 42 | rows = \ 43 | list(map(lambda row: [column for column_idx, column in enumerate(row) if column_idx in filtered_idx], rows)) 44 | 45 | await show_ascii_table(configs, headers, rows) 46 | -------------------------------------------------------------------------------- /rplugin/python3/database/configs/config.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from functools import partial 3 | from typing import Dict 4 | 5 | from ..utils.nvim import get_global_var, async_call 6 | 7 | _DEFAULT_DATABASE_MAPPINGS = { 8 | "show_connections": ["c"], 9 | "show_databases": ["d"], 10 | "show_tables": ["t"], 11 | "show_query": ["r"], 12 | "quit": ["q"], 13 | "delete": ["dd"], 14 | "new": ["c"], 15 | "show_insert_query": ["C"], 16 | "copy": ["p"], 17 | "show_copy_query": ["P"], 18 | "edit": ["m"], 19 | "show_update_query": ["M"], 20 | "info": ["."], 21 | "select": ["s"], 22 | "order": ["o"], 23 | "order_desc": ["O"], 24 | "filter": ["f"], 25 | "refresh": ["r"], 26 | "next": [""], 27 | "previous": [""], 28 | "filter_columns": ["a"], 29 | "clear_filter_column": ["A"], 30 | "clear_filter": ["F"], 31 | 'bigger': ["=", "+"], 32 | 'smaller': ["_", "-"], 33 | } 34 | 35 | _DEFAULT_DATABASE_QUERY_MAPPINGS = { 36 | "quit": ["q"], 37 | "run_query": ["r"], 38 | } 39 | 40 | 41 | @dataclass(frozen=True) 42 | class UserConfig: 43 | rows_limit: int 44 | window_layout: str 45 | window_size: int 46 | mappings: Dict 47 | query_mappings: Dict 48 | 49 | 50 | async def load_config() -> UserConfig: 51 | mappings = await async_call(partial(get_global_var, "vim_database_mappings", _DEFAULT_DATABASE_MAPPINGS)) 52 | mappings = {f"VimDatabase_{function}": mappings for function, mappings in mappings.items()} 53 | 54 | query_mappings = await async_call( 55 | partial(get_global_var, "vim_database_query_mappings", _DEFAULT_DATABASE_QUERY_MAPPINGS)) 56 | query_mappings = { 57 | f"VimDatabaseQuery_{function}": query_mappings for function, query_mappings in query_mappings.items() 58 | } 59 | 60 | rows_limit = await async_call(partial(get_global_var, "vim_database_rows_limit", 50)) 61 | window_layout = await async_call(partial(get_global_var, "vim_database_window_layout", "left")) 62 | window_size = await async_call(partial(get_global_var, "vim_database_window_size", 100)) 63 | 64 | return UserConfig(rows_limit=rows_limit, 65 | window_layout=window_layout, 66 | window_size=window_size, 67 | mappings=mappings, 68 | query_mappings=query_mappings) 69 | -------------------------------------------------------------------------------- /rplugin/python3/database/sql_clients/sql_client.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import subprocess 3 | from dataclasses import dataclass 4 | from typing import Optional, Tuple 5 | 6 | from ..storages.connection import Connection 7 | 8 | 9 | @dataclass(frozen=True) 10 | class CommandResult: 11 | error: bool 12 | data: str 13 | 14 | 15 | class SqlClient(metaclass=abc.ABCMeta): 16 | 17 | def __init__(self, connection: Connection): 18 | self.connection = connection 19 | 20 | def run_command(self, command: list, environment: dict = None) -> CommandResult: 21 | if environment is None: 22 | result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) 23 | else: 24 | result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, env=environment) 25 | if result.returncode == 0: 26 | return CommandResult(error=False, data=result.stdout.rstrip()) 27 | 28 | return CommandResult(error=True, data=result.stderr.rstrip()) 29 | 30 | @abc.abstractmethod 31 | def get_databases(self) -> list: 32 | pass 33 | 34 | @abc.abstractmethod 35 | def get_tables(self, database: str) -> list: 36 | pass 37 | 38 | @abc.abstractmethod 39 | def delete_table(self, database: str, table: str) -> None: 40 | pass 41 | 42 | @abc.abstractmethod 43 | def describe_table(self, database: str, table: str) -> Optional[list]: 44 | pass 45 | 46 | @abc.abstractmethod 47 | def run_query(self, database: str, query: str) -> Optional[list]: 48 | pass 49 | 50 | @abc.abstractmethod 51 | def copy(self, database: str, table: str, unique_columns: list, new_unique_column_values: list) -> bool: 52 | pass 53 | 54 | @abc.abstractmethod 55 | def update(self, database: str, table: str, update: Tuple[str, str], condition: Tuple[str, str]) -> bool: 56 | pass 57 | 58 | @abc.abstractmethod 59 | def delete(self, database: str, table: str, condition: Tuple[str, str]) -> bool: 60 | pass 61 | 62 | @abc.abstractmethod 63 | def get_primary_key(self, database: str, table: str) -> Optional[str]: 64 | pass 65 | 66 | @abc.abstractmethod 67 | def get_unique_columns(self, database: str, table: str) -> Optional[list]: 68 | pass 69 | 70 | @abc.abstractmethod 71 | def get_template_insert_query(self, database: str, table: str) -> Optional[list]: 72 | pass 73 | -------------------------------------------------------------------------------- /rplugin/python3/database/states/state.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from enum import Enum 3 | from typing import Optional, Tuple 4 | 5 | from ..concurrents.executors import run_in_executor 6 | from ..sql_clients.sql_client import SqlClient 7 | from ..sql_clients.sql_client_factory import SqlClientFactory 8 | from ..storages.connection import ( 9 | Connection, 10 | get_connections, 11 | ) 12 | 13 | 14 | class Mode(Enum): 15 | CONNECTION = 1 16 | DATABASE = 2 17 | TABLE = 3 18 | QUERY = 4 19 | TABLE_INFO = 5 20 | 21 | 22 | @dataclass(frozen=False) 23 | class State: 24 | mode: Mode 25 | connections: list 26 | selected_connection: Optional[Connection] 27 | sql_client: Optional[SqlClient] 28 | databases: list 29 | selected_database: Optional[str] 30 | tables: list 31 | selected_table: Optional[str] 32 | table_data: Optional[Tuple[list, list]] 33 | filtered_tables: Optional[str] 34 | filtered_columns: set[str] 35 | query_conditions: Optional[str] 36 | order: Optional[Tuple[str, str]] 37 | user_query: bool 38 | current_page: int 39 | 40 | def load_default_connection(self): 41 | if self.connections: 42 | self.selected_connection = self.connections[0] 43 | for connection in self.connections: 44 | if connection.name == "default": 45 | self.selected_connection = connection 46 | break 47 | self.selected_database = self.selected_connection.database 48 | self.sql_client = SqlClientFactory.create(self.selected_connection) 49 | 50 | 51 | async def init_state() -> State: 52 | state = State(mode=Mode.CONNECTION, 53 | connections=list(), 54 | selected_connection=None, 55 | databases=list(), 56 | selected_database=None, 57 | sql_client=None, 58 | tables=list(), 59 | selected_table=None, 60 | table_data=None, 61 | filtered_tables=None, 62 | filtered_columns=set(), 63 | query_conditions=None, 64 | order=None, 65 | user_query=False, 66 | current_page=1) 67 | 68 | def _get_connections() -> list: 69 | return list(get_connections()) 70 | 71 | state.connections = await run_in_executor(_get_connections) 72 | state.load_default_connection() 73 | 74 | return state 75 | -------------------------------------------------------------------------------- /rplugin/python3/database/transitions/view_ops.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | 3 | from .shared.show_table_data import show_table_data 4 | from ..configs.config import UserConfig 5 | from ..states.state import Mode, State 6 | from ..transitions.connection_ops import show_connections 7 | from ..transitions.database_ops import show_databases 8 | from ..transitions.table_ops import (show_tables, describe_table) 9 | from ..utils.log import log 10 | from ..utils.nvim import async_call 11 | from ..views.database_window import ( 12 | close_database_window, 13 | resize_width, 14 | resize_height, 15 | is_database_window_open, 16 | ) 17 | from ..views.query_window import ( 18 | open_query_window, 19 | close_query_window, 20 | is_query_window_opened, 21 | ) 22 | 23 | 24 | async def toggle(configs: UserConfig, state: State) -> None: 25 | is_window_open = await async_call(is_database_window_open) 26 | if is_window_open: 27 | await async_call(close_database_window) 28 | return 29 | 30 | if state.mode == Mode.DATABASE: 31 | await show_databases(configs, state) 32 | elif state.mode == Mode.TABLE: 33 | await show_tables(configs, state) 34 | elif state.mode == Mode.QUERY and not state.user_query: 35 | await show_table_data(configs, state, state.selected_table) 36 | elif state.mode == Mode.TABLE_INFO: 37 | await describe_table(configs, state, state.selected_table) 38 | else: 39 | # Fallback 40 | await show_connections(configs, state) 41 | 42 | 43 | async def close(_: UserConfig, __: State) -> None: 44 | await async_call(close_database_window) 45 | 46 | 47 | async def resize_database(config: UserConfig, _: State, direction: int) -> None: 48 | if config.window_layout == "left" or config.window_layout == "right": 49 | await async_call(partial(resize_width, direction)) 50 | else: 51 | await async_call(partial(resize_height, direction)) 52 | 53 | 54 | async def show_query(configs: UserConfig, state: State) -> None: 55 | if not state.connections: 56 | log.info("[vim-database] No connection found") 57 | return 58 | 59 | await async_call(partial(open_query_window, configs)) 60 | 61 | 62 | async def close_query(_: UserConfig, __: State) -> None: 63 | await async_call(close_query_window) 64 | 65 | 66 | async def toggle_query(configs: UserConfig, state: State) -> None: 67 | is_opened = await async_call(is_query_window_opened) 68 | if is_opened: 69 | await close_query(configs, state) 70 | else: 71 | await show_query(configs, state) 72 | -------------------------------------------------------------------------------- /rplugin/python3/database/transitions/database_ops.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | from typing import Optional, Tuple 3 | 4 | from ..concurrents.executors import run_in_executor 5 | from ..configs.config import UserConfig 6 | from ..sql_clients.sql_client_factory import SqlClientFactory 7 | from ..states.state import Mode, State 8 | from ..utils.ascii_table import ascii_table 9 | from ..utils.log import log 10 | from ..utils.nvim import ( 11 | async_call, 12 | set_cursor, 13 | render, 14 | ) 15 | from ..views.database_window import ( 16 | open_database_window, 17 | get_current_database_window_row, 18 | ) 19 | 20 | 21 | async def show_databases(configs: UserConfig, state: State) -> None: 22 | if not state.connections: 23 | log.info("[vim-database] No connection found") 24 | return 25 | 26 | state.mode = Mode.DATABASE 27 | window = await async_call(partial(open_database_window, configs)) 28 | 29 | state.databases = await run_in_executor(state.sql_client.get_databases) 30 | 31 | database_headers, database_rows, selected_index = _get_databases_from_state(state) 32 | await async_call(partial(render, window, ascii_table(database_headers, database_rows))) 33 | await async_call(partial(set_cursor, window, (selected_index + 4, 0))) 34 | 35 | 36 | async def select_database(configs: UserConfig, state: State) -> None: 37 | database_idx = await async_call(partial(_get_database_index, state)) 38 | if database_idx is None: 39 | return 40 | 41 | state.selected_database = state.databases[database_idx] 42 | state.sql_client = SqlClientFactory.create(state.selected_connection) 43 | 44 | # Update databases table 45 | window = await async_call(partial(open_database_window, configs)) 46 | database_headers, database_rows, selected_index = _get_databases_from_state(state) 47 | await async_call(partial(render, window, ascii_table(database_headers, database_rows))) 48 | await async_call(partial(set_cursor, window, (selected_index + 4, 0))) 49 | 50 | await show_databases(configs, state) 51 | 52 | 53 | def _get_database_index(state: State) -> Optional[int]: 54 | row = get_current_database_window_row() 55 | database_size = len(state.databases) 56 | # Minus 4 for header of the table 57 | database_index = row - 4 58 | if database_index < 0 or database_index >= database_size: 59 | return None 60 | 61 | return database_index 62 | 63 | 64 | def _get_databases_from_state(state: State) -> Tuple[list, list, int]: 65 | databases = [] 66 | selected_idx = 0 67 | for index, database in enumerate(state.databases): 68 | if state.selected_database == database: 69 | databases.append([database + " (*)"]) 70 | selected_idx = index 71 | else: 72 | databases.append([database]) 73 | 74 | return ["Database"], databases, selected_idx 75 | -------------------------------------------------------------------------------- /rplugin/python3/database/transitions/lsp_ops.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | from os import path 3 | 4 | import yaml 5 | 6 | from ..concurrents.executors import run_in_executor 7 | from ..configs.config import UserConfig 8 | from ..states.state import State 9 | from ..storages.connection import Connection, ConnectionType 10 | from ..utils.files import is_file_exists, create_folder_if_not_present 11 | from ..utils.log import log 12 | 13 | 14 | class YamlDumper(yaml.Dumper): 15 | 16 | def increase_indent(self, flow=False, *args, **kwargs): 17 | return super().increase_indent(flow=flow, indentless=False) 18 | 19 | 20 | async def lsp_config(_: UserConfig, state: State) -> None: 21 | if not state.connections: 22 | log.info("[vim-database] No connection found") 23 | return 24 | 25 | await run_in_executor(partial(_switch_database_connection, state.selected_connection, state.selected_database)) 26 | 27 | 28 | def _switch_database_connection(connection: Connection, database: str) -> None: 29 | config_connection_name = "vim-database" 30 | config_folder = path.join(path.expanduser("~"), ".config/sqls") 31 | config_path = path.join(config_folder, "config.yml") 32 | 33 | config_connections = [] 34 | if not is_file_exists(config_path): 35 | create_folder_if_not_present(config_folder) 36 | else: 37 | with open(config_path) as config_file: 38 | data = yaml.load(config_file) 39 | config_connections = data["connections"] 40 | 41 | if connection.connection_type == ConnectionType.SQLITE: 42 | new_config_connection = { 43 | "alias": config_connection_name, 44 | "driver": "sqlite3", 45 | "dataSourceName": connection.database, 46 | } 47 | elif connection.connection_type == ConnectionType.MYSQL or connection.connection_type == ConnectionType.POSTGRESQL: 48 | new_config_connection = { 49 | "alias": config_connection_name, 50 | "driver": "mysql" if connection.connection_type == ConnectionType.MYSQL else "postgres", 51 | "host": connection.host, 52 | "port": connection.port, 53 | "user": connection.username, 54 | "passwd": connection.password, 55 | "dbName": database 56 | } 57 | else: 58 | log.info("[vim-database] Connection type is not supported") 59 | return 60 | 61 | for index, config_connection in enumerate(config_connections): 62 | if config_connection_name == config_connection["alias"]: 63 | del config_connections[index] 64 | break 65 | config_connections.append(new_config_connection) 66 | 67 | config_file = open(config_path, "w") 68 | config_file.write( 69 | yaml.dump({ 70 | "lowercaseKeywords": False, 71 | "connections": config_connections 72 | }, Dumper=YamlDumper, sort_keys=False)) 73 | 74 | config_file.close() 75 | 76 | log.info("[vim-database] Switch database connection successfully.") 77 | -------------------------------------------------------------------------------- /rplugin/python3/database/views/database_window.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Tuple 2 | 3 | from pynvim.api.buffer import Buffer 4 | from pynvim.api.window import Window 5 | 6 | from ..configs.config import UserConfig 7 | from ..utils.nvim import ( 8 | find_windows_in_tab, 9 | get_buffer_option, 10 | create_buffer, 11 | create_window, 12 | close_window, 13 | set_buffer_in_window, 14 | get_buffer_in_window, 15 | get_current_cursor, 16 | get_lines, 17 | get_window_width, 18 | set_window_width, 19 | WindowLayout, 20 | get_window_height, 21 | set_window_height, 22 | ) 23 | 24 | _VIM_DATABASE_FILE_TYPE = 'VimDatabase' 25 | 26 | 27 | def open_database_window(settings: UserConfig) -> Window: 28 | window = _find_database_window_in_tab() 29 | if window is None: 30 | window = _open_database_window(settings) 31 | 32 | return window 33 | 34 | 35 | def close_database_window() -> None: 36 | window = _find_database_window_in_tab() 37 | if window is not None: 38 | close_window(window, True) 39 | 40 | 41 | def get_current_database_window_line() -> Optional[str]: 42 | window = _find_database_window_in_tab() 43 | if window is None: 44 | return None 45 | 46 | buffer: Buffer = get_buffer_in_window(window) 47 | row, _ = get_current_cursor(window) 48 | lines = get_lines(buffer, row - 1, row) 49 | return None if len(lines) == 0 else lines[0] 50 | 51 | 52 | def get_current_database_window_cursor() -> Optional[Tuple[int, int]]: 53 | window = _find_database_window_in_tab() 54 | if window is None: 55 | return None 56 | 57 | return get_current_cursor(window) 58 | 59 | 60 | def get_current_database_window_row() -> Optional[int]: 61 | window = _find_database_window_in_tab() 62 | if window is None: 63 | return None 64 | row, _ = get_current_cursor(window) 65 | return row 66 | 67 | 68 | def is_database_window_open() -> bool: 69 | return _find_database_window_in_tab() is not None 70 | 71 | 72 | def resize_width(direction: int) -> None: 73 | window = _find_database_window_in_tab() 74 | if window is None: 75 | return 76 | 77 | width = get_window_width(window) 78 | set_window_width(window, width + direction) 79 | 80 | 81 | def resize_height(direction: int) -> None: 82 | window = _find_database_window_in_tab() 83 | if window is None: 84 | return 85 | 86 | width = get_window_height(window) 87 | set_window_height(window, width + direction) 88 | 89 | 90 | def _find_database_window_in_tab() -> Optional[Window]: 91 | for window in find_windows_in_tab(): 92 | buffer: Buffer = get_buffer_in_window(window) 93 | buffer_file_type = get_buffer_option(buffer, 'filetype') 94 | if buffer_file_type == _VIM_DATABASE_FILE_TYPE: 95 | return window 96 | return None 97 | 98 | 99 | def _get_window_layout(window_layout: str) -> WindowLayout: 100 | if window_layout == "left": 101 | return WindowLayout.LEFT 102 | if window_layout == "right": 103 | return WindowLayout.RIGHT 104 | if window_layout == "above": 105 | return WindowLayout.ABOVE 106 | if window_layout == "below": 107 | return WindowLayout.BELOW 108 | 109 | # Fallback layout 110 | return WindowLayout.LEFT 111 | 112 | 113 | def _open_database_window(settings: UserConfig) -> Window: 114 | buffer = create_buffer( 115 | settings.mappings, { 116 | 'buftype': 'nofile', 117 | 'bufhidden': 'hide', 118 | 'swapfile': False, 119 | 'buflisted': False, 120 | 'modifiable': False, 121 | 'filetype': _VIM_DATABASE_FILE_TYPE, 122 | }) 123 | window = create_window(settings.window_size, _get_window_layout(settings.window_layout), { 124 | 'list': False, 125 | 'number': False, 126 | 'relativenumber': False, 127 | 'wrap': False, 128 | }) 129 | set_buffer_in_window(window, buffer) 130 | return window 131 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vim-database 2 | 3 | ## Introduction 4 | 5 | Database management for Neovim. 6 | 7 | - Database explorer 8 | - Ability to execute queries 9 | - Edit/ copy/ delete data 10 | - ... 11 | 12 | The supported databases at the moment: 13 | 14 | - SQLite 15 | - MySQL 16 | - PostgreSQL 17 | 18 | ## Requirements 19 | 20 | - Neovim (vim is not supported) 21 | - Python 3.9 22 | - pynvim 23 | - pyyaml 24 | - sqlite3 25 | - psql 26 | - mysql 27 | 28 | ## Install 29 | 30 | Install pynvim 31 | 32 | ```sh 33 | pip3 install pynvim 34 | ``` 35 | 36 | Install pyyaml 37 | 38 | ```sh 39 | pip3 install pyyaml 40 | ``` 41 | 42 | Install Neovim plugin 43 | 44 | ```sh 45 | Plug 'dinhhuy258/vim-database', {'branch': 'master', 'do': ':UpdateRemotePlugins'} 46 | ``` 47 | 48 | Install sqlite3 (Optional - skip if you don't use sqlite) 49 | 50 | ```sh 51 | brew install sqlite 52 | ``` 53 | 54 | Install postgres client (Optional - skip if you don't use postgres) 55 | 56 | ```sh 57 | brew install postgresql 58 | ``` 59 | 60 | Install msyql client (Optional - skip if you don't use mysql) 61 | 62 | ```sh 63 | brew install mysql 64 | ``` 65 | 66 | ## Usage 67 | 68 | - `VDToggleDatabase`: Open or close database management 69 | - `VDToggleQuery`: Open or close query terminal 70 | - `VimDatabaseListTablesFzf`: List all tables in fzf 71 | 72 | You can map these commands to another keys: 73 | 74 | ```VimL 75 | nnoremap :VDToggleDatabase 76 | nnoremap :VDToggleQuery 77 | nmap fd :VimDatabaseListTablesFzf 78 | ``` 79 | 80 | If you see an error `Not and editor command: VDToggleDatabase` you need to run `:UpdateRemotePlugins`. If the error still occurs, run the following command 81 | 82 | ```sh 83 | pip install --user --upgrade pynvim 84 | ``` 85 | 86 | then restart nvim and re-run `:UpdateRemotePlugins` and finally restart nvim, `:VDToggleDatabase` will exist 87 | 88 | ## Key bindings 89 | 90 | Check the default key binding [here](https://github.com/dinhhuy258/vim-database/blob/master/rplugin/python3/database/configs/config.py) 91 | 92 | ## Configuration 93 | 94 | You can tweak the behavior of Database by setting a few variables in your vim setting file. For example: 95 | 96 | ```VimL 97 | let g:vim_database_rows_limit = 50 98 | let g:vim_database_window_layout = "bottom" 99 | ... 100 | ``` 101 | 102 | ### g:vim_database_rows_limit 103 | 104 | The maximum number of rows to display in table results. 105 | 106 | Default: `50` 107 | 108 | ### g:vim_database_window_layout 109 | 110 | Set the layout for database window. 111 | 112 | Possible values: 113 | - `left` 114 | - `right` 115 | - `above` 116 | - `below` 117 | 118 | Default: `left` 119 | 120 | ### g:vim_database_window_size 121 | 122 | Set size for database window. 123 | 124 | Default: `100` 125 | 126 | 127 | ## Features 128 | 129 | Navigate between connection, database, table mode: 130 | 131 | - ` c`: Go to connection mode 132 | - ` d`: Go to database mode 133 | - ` t`: Go to table mode 134 | 135 | ### Connection mode 136 | 137 | - New connection (press `c`) 138 | - Delete connection (press `dd`) 139 | - Select connection (press `s`) 140 | - Modify connection (press `m`) 141 | 142 | ![](https://user-images.githubusercontent.com/17776979/126873230-3040adc1-a447-48c8-8d08-ee48c1b7f6c7.gif) 143 | 144 | ![](https://user-images.githubusercontent.com/17776979/126873229-b11b7b64-21d8-4d6b-baa0-0715fea4df6e.gif) 145 | 146 | ### Database mode 147 | 148 | - Select database (press `s`) 149 | 150 | ![](https://user-images.githubusercontent.com/17776979/126873228-c7557467-a8c2-48bf-854e-a1b4f7bc6900.gif) 151 | 152 | ### Table mode 153 | 154 | - Filter table (press `f`) 155 | - Clear filter (press `F`) 156 | - Select table (press `s`) 157 | - Delete table (press `dd`) 158 | - Describe table (press `.`) 159 | 160 | ![](https://user-images.githubusercontent.com/17776979/126873227-156b4675-a757-438a-be9d-445bf2e76933.gif) 161 | 162 | ### Data mode 163 | 164 | - Sort asc (press `o`) 165 | - Sort desc (press `O`) 166 | - Filter (press `f`) 167 | - Clear filter (press `F`) 168 | - Filter columns (press `a`) 169 | - Clear filter columns (press `A`) 170 | - Delete row (press `dd`) 171 | - Modify row at column (press `m`) 172 | - Copy row (press `p`) 173 | - Show create row query (press `C`) 174 | - Show update row query (press `M`) 175 | - Show update row query (press `P`) 176 | - Describe table (press `.`) 177 | - Next page (press `right-arrow`) 178 | - Previous page (press `left-arrow`) 179 | 180 | ![](https://user-images.githubusercontent.com/17776979/126873221-ecc5081e-ecf2-4ca5-be0f-2b9c1658495a.gif) 181 | 182 | ### Query mode 183 | 184 | - Execute the query (press `r`) 185 | 186 | ![](https://user-images.githubusercontent.com/17776979/126873722-d9445e96-555b-4c5a-8eab-0f3495994c73.gif) 187 | -------------------------------------------------------------------------------- /rplugin/python3/database/transitions/table_ops.py: -------------------------------------------------------------------------------- 1 | import re 2 | from functools import partial 3 | from typing import Optional, Tuple 4 | 5 | from .shared.show_ascii_table import show_ascii_table 6 | from .shared.show_table_data import show_table_data 7 | from ..concurrents.executors import run_in_executor 8 | from ..configs.config import UserConfig 9 | from ..states.state import Mode, State 10 | from ..utils.ascii_table import ascii_table 11 | from ..utils.log import log 12 | from ..utils.nvim import ( 13 | async_call, 14 | confirm, 15 | get_input, 16 | set_cursor, 17 | render, 18 | call_function, 19 | ) 20 | from ..views.database_window import ( 21 | open_database_window, 22 | get_current_database_window_row, 23 | ) 24 | 25 | 26 | async def list_tables_fzf(_: UserConfig, state: State) -> None: 27 | if not state.connections: 28 | log.info("[vim-database] No connection found") 29 | return 30 | 31 | tables = await run_in_executor(partial(state.sql_client.get_tables, state.selected_database)) 32 | 33 | await async_call(partial(call_function, "VimDatabaseSelectTables", tables)) 34 | 35 | 36 | async def describe_current_table(configs: UserConfig, state: State) -> None: 37 | table_idx = await async_call(partial(get_table_idx, state)) 38 | if table_idx is None: 39 | return 40 | 41 | table = state.tables[table_idx] 42 | state.selected_table = table 43 | await describe_table(configs, state, table) 44 | 45 | 46 | async def describe_table(configs: UserConfig, state: State, table: str) -> None: 47 | table_info = await run_in_executor(partial(state.sql_client.describe_table, state.selected_database, table)) 48 | if table_info is None: 49 | return 50 | 51 | state.mode = Mode.TABLE_INFO 52 | await show_ascii_table(configs, table_info[0], table_info[1:]) 53 | 54 | 55 | async def select_table(configs: UserConfig, state: State) -> None: 56 | state.query_conditions = None 57 | state.filtered_columns.clear() 58 | state.order = None 59 | state.current_page = 1 60 | 61 | table_idx = await async_call(partial(get_table_idx, state)) 62 | if table_idx is None: 63 | return 64 | 65 | table = state.tables[table_idx] 66 | 67 | await show_table_data(configs, state, table) 68 | 69 | 70 | async def table_filter(configs: UserConfig, state: State) -> None: 71 | 72 | def get_filtered_tables() -> Optional[str]: 73 | _filtered_tables = state.filtered_tables if state.filtered_tables is not None else "" 74 | return get_input("New table filter: ", _filtered_tables) 75 | 76 | filtered_tables = await async_call(get_filtered_tables) 77 | if filtered_tables: 78 | state.filtered_tables = filtered_tables.strip() 79 | await show_tables(configs, state) 80 | 81 | 82 | async def show_tables(configs: UserConfig, state: State) -> None: 83 | if not state.connections: 84 | log.info("[vim-database] No connection found") 85 | return 86 | 87 | state.mode = Mode.TABLE 88 | window = await async_call(partial(open_database_window, configs)) 89 | 90 | def _get_tables(): 91 | return list( 92 | filter(lambda table: state.filtered_tables is None or re.search(state.filtered_tables, table), 93 | state.sql_client.get_tables(state.selected_database))) 94 | 95 | state.tables = await run_in_executor(_get_tables) 96 | table_headers, table_rows, selected_idx = _get_tables_from_state(state) 97 | await async_call(partial(render, window, ascii_table(table_headers, table_rows))) 98 | await async_call(partial(set_cursor, window, (selected_idx + 4, 0))) 99 | 100 | 101 | async def delete_table(configs: UserConfig, state: State) -> None: 102 | table_index = await async_call(partial(get_table_idx, state)) 103 | if table_index is None: 104 | return 105 | 106 | table = state.tables[table_index] 107 | ans = await async_call(partial(confirm, "Do you want to delete table " + table + "?")) 108 | if not ans: 109 | return 110 | 111 | await run_in_executor(partial(state.sql_client.delete_table, state.selected_database, table)) 112 | 113 | # Refresh tables 114 | await show_tables(configs, state) 115 | 116 | 117 | def get_table_idx(state: State) -> Optional[int]: 118 | row = get_current_database_window_row() 119 | table_size = len(state.tables) 120 | # Minus 4 for header of the table 121 | table_idx = row - 4 122 | if table_idx < 0 or table_idx >= table_size: 123 | return None 124 | 125 | return table_idx 126 | 127 | 128 | def _get_tables_from_state(state: State) -> Tuple[list, list, int]: 129 | tables = [] 130 | selected_idx = 0 131 | for idx, table in enumerate(state.tables): 132 | tables.append([table]) 133 | if table == state.selected_table: 134 | selected_idx = idx 135 | 136 | return ["Table"], tables, selected_idx 137 | -------------------------------------------------------------------------------- /rplugin/python3/database/transitions/query_ops.py: -------------------------------------------------------------------------------- 1 | import re 2 | from functools import partial 3 | 4 | from .data_ops import show_table_data 5 | from .database_ops import show_databases 6 | from .shared.get_current_row_idx import get_current_row_idx 7 | from .shared.get_primary_key_value import get_primary_key_value 8 | from .table_ops import (show_tables) 9 | from ..concurrents.executors import run_in_executor 10 | from ..configs.config import UserConfig 11 | from ..states.state import Mode, State 12 | from ..transitions.shared.show_ascii_table import show_ascii_table 13 | from ..utils.log import log 14 | from ..utils.nvim import ( 15 | async_call, 16 | render, 17 | ) 18 | from ..views.query_window import ( 19 | open_query_window, 20 | close_query_window, 21 | get_query, 22 | ) 23 | 24 | 25 | async def run_query(configs: UserConfig, state: State) -> None: 26 | if not state.connections: 27 | log.info("[vim-database] No connection found") 28 | return 29 | 30 | query = await async_call(get_query) 31 | if query is None: 32 | return 33 | 34 | query_result = await run_in_executor(partial(state.sql_client.run_query, state.selected_database, query)) 35 | if query_result is None: 36 | return 37 | 38 | await async_call(close_query_window) 39 | 40 | if len(query_result) < 2 and not query.lower().startswith("select "): 41 | log.info("[vim-database] Query executed successfully") 42 | 43 | if state.mode == Mode.DATABASE and state.databases: 44 | await show_databases(configs, state) 45 | elif state.mode == Mode.TABLE and state.tables: 46 | await show_tables(configs, state) 47 | elif state.mode == Mode.QUERY and not state.user_query: 48 | await show_table_data(configs, state, state.selected_table) 49 | return 50 | elif len(query_result) < 2: 51 | matches = re.search(r'(?<=from)(\s+\w+\b)', query, re.IGNORECASE) 52 | if matches is None: 53 | log.info("[vim-database] Can not be able to identify table name in select query") 54 | return 55 | table_name = matches.group(0).strip() 56 | query_result = [[table_name]] 57 | 58 | state.selected_table = None 59 | state.table_data = None 60 | state.mode = Mode.QUERY 61 | state.user_query = True 62 | await show_ascii_table(configs, query_result[0], query_result[1:]) 63 | 64 | 65 | async def show_insert_query(configs: UserConfig, state: State) -> None: 66 | if state.mode == Mode.QUERY and not state.user_query: 67 | 68 | insert_query = await run_in_executor( 69 | partial(state.sql_client.get_template_insert_query, state.selected_database, state.selected_table)) 70 | if insert_query is None: 71 | return 72 | 73 | query_window = await async_call(partial(open_query_window, configs)) 74 | await async_call(partial(render, query_window, insert_query)) 75 | elif state.mode == Mode.TABLE: 76 | create_table_query = ["CREATE TABLE table_name (", "\t", ")"] 77 | query_window = await async_call(partial(open_query_window, configs)) 78 | await async_call(partial(render, query_window, create_table_query)) 79 | 80 | 81 | async def show_update_query(configs: UserConfig, state: State) -> None: 82 | if state.mode != Mode.QUERY or state.user_query: 83 | return 84 | 85 | headers, rows = state.table_data 86 | row_idx = await async_call(partial(get_current_row_idx, state)) 87 | if row_idx is None: 88 | return 89 | 90 | row = rows[row_idx] 91 | primary_key, primary_key_value = await get_primary_key_value(state, row_idx) 92 | if primary_key is None: 93 | return 94 | 95 | update_query = ["UPDATE " + state.selected_table + " SET "] 96 | num_columns = len(headers) 97 | for i in range(num_columns): 98 | column = headers[i] 99 | column_value = row[i] 100 | if column != primary_key: 101 | update_query.append("\t" + column + " = \'" + column_value + "\',") 102 | update_query[-1] = update_query[-1][:-1] 103 | update_query.append("WHERE " + primary_key + " = \'" + primary_key_value + "\'") 104 | query_window = await async_call(partial(open_query_window, configs)) 105 | await async_call(partial(render, query_window, update_query)) 106 | 107 | 108 | async def show_copy_query(configs: UserConfig, state: State) -> None: 109 | if state.mode != Mode.QUERY or state.user_query: 110 | return 111 | 112 | headers, rows = state.table_data 113 | row_idx = await async_call(partial(get_current_row_idx, state)) 114 | if row_idx is None: 115 | return 116 | 117 | row = rows[row_idx] 118 | insert_query = ["INSERT INTO " + state.selected_table + " ("] 119 | num_columns = len(headers) 120 | for i in range(num_columns): 121 | column_name = headers[i] 122 | insert_query.append("\t" + column_name) 123 | if i != num_columns - 1: 124 | insert_query[-1] += "," 125 | 126 | insert_query.append(") VALUES (") 127 | for i in range(num_columns): 128 | column_value = row[i] 129 | insert_query.append("\t" + (column_value if column_value == 'NULL' else ("\'" + column_value + "\'"))) 130 | if i != num_columns - 1: 131 | insert_query[-1] += "," 132 | insert_query.append(")") 133 | 134 | query_window = await async_call(partial(open_query_window, configs)) 135 | await async_call(partial(render, query_window, insert_query)) 136 | -------------------------------------------------------------------------------- /rplugin/python3/database/views/query_window.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from pynvim.api.buffer import Buffer 4 | from pynvim.api.window import Window 5 | 6 | from ..configs.config import UserConfig 7 | from ..utils.nvim import ( 8 | execute, 9 | get_option, 10 | create_buffer, 11 | set_buffer_var, 12 | get_buffer_var, 13 | open_window, 14 | get_window_info, 15 | set_window_option, 16 | find_windows_in_tab, 17 | get_buffer_in_window, 18 | close_window, 19 | get_buffer_content, 20 | render, 21 | ) 22 | from ..utils.strings import string_compose 23 | 24 | _VIM_DATABASE_QUERY_TITLE = "[vim-database] Query" 25 | _VIM_DATABASE_QUERY_BORDER_CHARS = ['─', '│', '─', '│', '┌', '┐', '┘', '└'] 26 | _query_buffer: Optional[Buffer] = None 27 | 28 | 29 | def close_query_window() -> None: 30 | query_window = _find_query_window() 31 | if query_window is not None: 32 | close_window(query_window, True) 33 | 34 | 35 | def open_query_window(settings: UserConfig) -> Optional[Window]: 36 | query_window = _find_query_window() 37 | if query_window is not None: 38 | return query_window 39 | 40 | global _query_buffer 41 | if _query_buffer is None: 42 | _query_buffer = create_buffer( 43 | settings.query_mappings, { 44 | "buftype": "", 45 | "bufhidden": "hide", 46 | "swapfile": False, 47 | "buflisted": False, 48 | "modifiable": True, 49 | "filetype": "sql" 50 | }) 51 | 52 | border_winid = get_buffer_var(_query_buffer.handle, "border_winid", -1) 53 | if len(get_window_info(border_winid)) != 0: 54 | border_window = _find_window_by_winid(border_winid) 55 | if border_window is not None: 56 | close_window(border_window, True) 57 | 58 | height = int((get_option("lines") - 2) / 1.5) 59 | width = int(get_option("columns") / 1.5) 60 | row = int((get_option("lines") - height) / 2) 61 | col = int((get_option("columns") - width) / 2) 62 | window = open_window(_query_buffer, True, { 63 | "relative": "editor", 64 | "width": width, 65 | "height": height, 66 | "col": col, 67 | "row": row, 68 | "anchor": "NW", 69 | "style": "minimal", 70 | }) 71 | set_window_option(window, "winblend", 0) 72 | set_window_option(window, "winhl", "Normal:Normal,NormalNC:Normal") 73 | 74 | # Border 75 | top = _VIM_DATABASE_QUERY_BORDER_CHARS[4] + (_VIM_DATABASE_QUERY_BORDER_CHARS[0] * 76 | width) + _VIM_DATABASE_QUERY_BORDER_CHARS[5] 77 | mid = _VIM_DATABASE_QUERY_BORDER_CHARS[3] + (' ' * width) + _VIM_DATABASE_QUERY_BORDER_CHARS[1] 78 | bot = _VIM_DATABASE_QUERY_BORDER_CHARS[7] + (_VIM_DATABASE_QUERY_BORDER_CHARS[2] * 79 | width) + _VIM_DATABASE_QUERY_BORDER_CHARS[6] 80 | top = string_compose(top, 1, _VIM_DATABASE_QUERY_TITLE) 81 | 82 | border_lines = [top] + ([mid] * height) + [bot] 83 | border_buffer = create_buffer({}, { 84 | "buftype": "nofile", 85 | "bufhidden": "wipe", 86 | "synmaxcol": 3000, 87 | "swapfile": False, 88 | "buflisted": False, 89 | "modifiable": False, 90 | }) 91 | 92 | border_window = open_window( 93 | border_buffer, False, { 94 | "relative": "editor", 95 | "width": width + 2, 96 | "height": height + 2, 97 | "col": max(col - 1, 0), 98 | "row": max(row - 1, 0), 99 | "anchor": "NW", 100 | "style": "minimal", 101 | "focusable": False, 102 | }) 103 | 104 | execute("autocmd BufHidden ++once call CloseVimDatabaseQueryBorder(" + 105 | str(_query_buffer.handle) + ")") 106 | execute("autocmd BufLeave ++once call CloseVimDatabaseQuery(" + 107 | str(_query_buffer.handle) + ")") 108 | set_buffer_var(_query_buffer.handle, "border_winid", border_window.handle) 109 | set_window_option(border_window, "winhl", "Normal:Normal") 110 | set_window_option(border_window, "cursorcolumn", False) 111 | set_window_option(border_window, "colorcolumn", "") 112 | 113 | render(border_window, border_lines, False) 114 | 115 | return window 116 | 117 | 118 | def get_query() -> Optional[str]: 119 | buffer = _find_query_buffer() 120 | if buffer is None: 121 | return None 122 | buffer_content = get_buffer_content(buffer) 123 | buffer_content = list(map(lambda line: line.strip(), buffer_content)) 124 | sql_query = ' '.join(buffer_content).strip() 125 | 126 | return None if len(sql_query) == 0 else sql_query 127 | 128 | 129 | def is_query_window_opened() -> bool: 130 | query_window = _find_query_window() 131 | 132 | return query_window is not None 133 | 134 | 135 | def _find_window_by_winid(winid: int) -> Optional[Window]: 136 | for window in find_windows_in_tab(): 137 | if window.handle == winid: 138 | return window 139 | 140 | return None 141 | 142 | 143 | def _find_query_window() -> Optional[Window]: 144 | if _query_buffer is None: 145 | return None 146 | 147 | for window in find_windows_in_tab(): 148 | buffer: Buffer = get_buffer_in_window(window) 149 | if buffer.handle == _query_buffer.handle: 150 | return window 151 | 152 | return None 153 | 154 | 155 | def _find_query_buffer() -> Optional[Buffer]: 156 | query_window = _find_query_window() 157 | if query_window is not None: 158 | return query_window.buffer 159 | 160 | return None 161 | -------------------------------------------------------------------------------- /rplugin/python3/database/sql_clients/sqlite_client.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Tuple 2 | 3 | from .sql_client import SqlClient 4 | from ..storages.connection import Connection 5 | from ..utils.log import log 6 | 7 | 8 | class SqliteClient(SqlClient): 9 | 10 | def __init__(self, connection: Connection): 11 | SqlClient.__init__(self, connection) 12 | 13 | def get_databases(self) -> list: 14 | result = self.run_command(["sqlite3", self.connection.database, ".database"]) 15 | if result.error: 16 | log.info("[vim-database] " + result.data) 17 | return list() 18 | return list([result.data.split()[-1]]) 19 | 20 | def get_tables(self, database: str) -> list: 21 | result = self.run_command(["sqlite3", database, ".table"]) 22 | if result.error: 23 | log.info("[vim-database] " + result.data) 24 | return list() 25 | return result.data.split() 26 | 27 | def delete_table(self, database: str, table: str) -> None: 28 | delete_table_query = "DROP TABLE " + table 29 | result = self.run_command(["sqlite3", database, delete_table_query]) 30 | if result.error: 31 | log.info("[vim-database] " + result.data) 32 | 33 | def describe_table(self, database: str, table: str) -> Optional[list]: 34 | describe_table_query = "PRAGMA table_info(" + table + ")" 35 | result = self.run_command(["sqlite3", database, "--header", describe_table_query]) 36 | if result.error: 37 | log.info("[vim-database] " + result.data) 38 | return None 39 | 40 | lines = result.data.splitlines() 41 | if len(lines) < 2: 42 | log.info("[vim-database] No table information found") 43 | return None 44 | 45 | return list(map(lambda data: data.split("|"), lines)) 46 | 47 | def run_query(self, database: str, query: str) -> Optional[list]: 48 | result = self.run_command(["sqlite3", database, "--header", query]) 49 | if result.error: 50 | log.info("[vim-database] " + result.data) 51 | return None 52 | 53 | lines = result.data.splitlines() 54 | return list(map(lambda data: data.split("|"), lines)) 55 | 56 | def update(self, database: str, table: str, update: Tuple[str, str], condition: Tuple[str, str]) -> bool: 57 | update_column, update_value = update 58 | update_query = "UPDATE " + table + " SET " + update_column + " = " + update_value 59 | 60 | condition_column, condition_value = condition 61 | update_query = update_query + " WHERE " + condition_column + " = " + condition_value 62 | 63 | result = self.run_command(["sqlite3", database, update_query]) 64 | if result.error: 65 | log.info("[vim-database] " + result.data) 66 | return False 67 | 68 | return True 69 | 70 | def copy(self, database: str, table: str, unique_columns: list, new_unique_column_values: list) -> bool: 71 | log.info("[vim-database] Not supported for sqlite") 72 | return False 73 | 74 | def delete(self, database: str, table: str, condition: Tuple[str, str]) -> bool: 75 | condition_column, condition_value = condition 76 | delete_query = "DELETE FROM " + table + " WHERE " + condition_column + " = " + condition_value 77 | 78 | result = self.run_command(["sqlite3", database, delete_query]) 79 | if result.error: 80 | log.info("[vim-database] " + result.data) 81 | return False 82 | 83 | return True 84 | 85 | def get_primary_key(self, database: str, table: str) -> Optional[str]: 86 | table_info = self.describe_table(database, table) 87 | if table_info is None: 88 | return None 89 | headers = table_info[0] 90 | columns = table_info[1:] 91 | pk_index = -1 92 | name_index = -1 93 | 94 | index = 0 95 | for header in headers: 96 | if header == "pk": 97 | pk_index = index 98 | if header == "name": 99 | name_index = index 100 | index = index + 1 101 | 102 | if pk_index == -1 or name_index == -1: 103 | return None 104 | 105 | for column_info in columns: 106 | if column_info[pk_index] == "1": 107 | return column_info[name_index] 108 | 109 | return None 110 | 111 | def get_unique_columns(self, database: str, table: str) -> Optional[list]: 112 | primary_key = self.get_primary_key(database, table) 113 | return [] if primary_key is None else [primary_key] 114 | 115 | def get_template_insert_query(self, database: str, table: str) -> Optional[list]: 116 | table_info = self.describe_table(database, table) 117 | if table_info is None: 118 | return None 119 | headers = table_info[0] 120 | columns = table_info[1:] 121 | name_index = -1 122 | default_value_index = -1 123 | 124 | for index, header in enumerate(headers): 125 | if header == "name": 126 | name_index = index 127 | elif header == "dflt_value": 128 | default_value_index = index 129 | 130 | if name_index == -1 or default_value_index == -1: 131 | log.info("[vim-database] Invalid column structure") 132 | return None 133 | 134 | insert_query = ["INSERT INTO " + table + " ("] 135 | columns_len = len(columns) 136 | for index, column in enumerate(columns): 137 | insert_query.append("\t" + column[name_index]) 138 | if index != columns_len - 1: 139 | insert_query[-1] += "," 140 | insert_query.append(") VALUES (") 141 | 142 | for index, column in enumerate(columns): 143 | insert_query.append("\t" + (column[default_value_index] if column[default_value_index] == "NULL" else 144 | ("\'" + column[default_value_index] + "\'"))) 145 | if index != columns_len - 1: 146 | insert_query[-1] += "," 147 | insert_query.append(")") 148 | 149 | return insert_query 150 | -------------------------------------------------------------------------------- /rplugin/python3/database/utils/nvim.py: -------------------------------------------------------------------------------- 1 | from asyncio import Future 2 | from enum import Enum 3 | from typing import ( 4 | Any, 5 | Awaitable, 6 | Callable, 7 | TypeVar, 8 | Iterator, 9 | Tuple, 10 | Dict, 11 | Sequence, 12 | Optional, 13 | ) 14 | 15 | from pynvim import Nvim 16 | from pynvim.api.buffer import Buffer 17 | from pynvim.api.common import NvimError 18 | from pynvim.api.tabpage import Tabpage 19 | from pynvim.api.window import Window 20 | 21 | T = TypeVar("T") 22 | 23 | 24 | class WindowLayout(Enum): 25 | LEFT = 1 26 | RIGHT = 2 27 | ABOVE = 3 28 | BELOW = 4 29 | 30 | 31 | def init_nvim(nvim: Nvim) -> None: 32 | global _nvim 33 | _nvim = nvim 34 | 35 | 36 | def call_atomic(*instructions: Tuple[str, Sequence[Any]]) -> None: 37 | inst = tuple((f"{instruction}", args) for instruction, args in instructions) 38 | out, error = _nvim.api.call_atomic(inst) 39 | if error: 40 | raise NvimError(error) 41 | 42 | 43 | def call_function(name, argument) -> Any: 44 | return _nvim.call(name, argument) 45 | 46 | 47 | def async_call(func: Callable[[], T]) -> Awaitable[T]: 48 | future: Future = Future() 49 | 50 | def run() -> None: 51 | try: 52 | ret = func() 53 | except Exception as e: 54 | future.set_exception(e) 55 | else: 56 | future.set_result(ret) 57 | 58 | _nvim.async_call(run) 59 | return future 60 | 61 | 62 | def execute(command: str) -> Any: 63 | return _nvim.funcs.execute(command) 64 | 65 | 66 | def find_windows_in_tab() -> Iterator[Window]: 67 | 68 | def key_by(win: Window) -> Tuple[int, int]: 69 | row, col = _nvim.api.win_get_position(win) 70 | return col, row 71 | 72 | tab: Tabpage = _nvim.api.get_current_tabpage() 73 | windows: Sequence[Window] = _nvim.api.tabpage_list_wins(tab) 74 | 75 | for window in sorted(windows, key=key_by): 76 | if not _nvim.api.win_get_option(window, "previewwindow"): 77 | yield window 78 | 79 | 80 | def create_buffer(keymaps: Dict[str, Sequence[str]] = dict, options: Dict[str, Any] = dict) -> Buffer: 81 | mapping_options = {"noremap": True, "silent": True, "nowait": True} 82 | buffer: Buffer = _nvim.api.create_buf(False, True) 83 | 84 | for function, mappings in keymaps.items(): 85 | for mapping in mappings: 86 | _nvim.api.buf_set_keymap(buffer, "n", mapping, f"call {function}(v:false)", mapping_options) 87 | 88 | for option_name, option_value in options.items(): 89 | _nvim.api.buf_set_option(buffer, option_name, option_value) 90 | 91 | return buffer 92 | 93 | 94 | def set_buffer_var(buffer_handle: int, var_name: str, var_value: Any) -> None: 95 | _nvim.funcs.setbufvar(buffer_handle, var_name, var_value) 96 | 97 | 98 | def get_buffer_var(buffer_handle: int, var_name: str, default_value: Any) -> int: 99 | return _nvim.funcs.getbufvar(buffer_handle, var_name, default_value) 100 | 101 | 102 | def create_window(size: int, layout: WindowLayout, options: Dict[str, Any] = dict) -> Window: 103 | split_right = _nvim.api.get_option("splitright") 104 | split_below = _nvim.api.get_option("splitbelow") 105 | 106 | windows: Sequence[Window] = tuple(window for window in find_windows_in_tab()) 107 | 108 | focus_win = windows[0] 109 | 110 | _nvim.api.set_current_win(focus_win) 111 | if layout is WindowLayout.LEFT: 112 | _nvim.api.set_option("splitright", False) 113 | _nvim.command(f"{size}vsplit") 114 | elif layout is WindowLayout.RIGHT: 115 | _nvim.api.set_option("splitright", True) 116 | _nvim.command(f"{size}vsplit") 117 | elif layout is WindowLayout.BELOW: 118 | _nvim.api.set_option("splitbelow", True) 119 | _nvim.command(f"{size}split") 120 | else: 121 | # Above layout 122 | _nvim.api.set_option("splitbelow", False) 123 | _nvim.command(f"{size}split") 124 | 125 | _nvim.api.set_option("splitright", split_right) 126 | _nvim.api.set_option("splitbelow", split_below) 127 | 128 | window: Window = _nvim.api.get_current_win() 129 | for option_name, option_value in options.items(): 130 | _nvim.api.win_set_option(window, option_name, option_value) 131 | 132 | return window 133 | 134 | 135 | def get_window_info(winid: int) -> list: 136 | return _nvim.funcs.getwininfo(winid) 137 | 138 | 139 | def open_window(buffer: Buffer, enter: bool, opts: Dict[str, Any]) -> Window: 140 | return _nvim.api.open_win(buffer, enter, opts) 141 | 142 | 143 | def set_window_option(window: Window, option_name: str, option_value: Any) -> None: 144 | _nvim.api.win_set_option(window, option_name, option_value) 145 | 146 | 147 | def get_current_cursor(window: Window) -> Tuple[int, int]: 148 | return _nvim.api.win_get_cursor(window) 149 | 150 | 151 | def close_window(window: Window, force: bool) -> None: 152 | _nvim.api.win_close(window, force) 153 | 154 | 155 | def set_buffer_in_window(window: Window, buffer: Buffer) -> None: 156 | _nvim.api.win_set_buf(window, buffer) 157 | 158 | 159 | def get_buffer_in_window(window: Window) -> Buffer: 160 | return _nvim.api.win_get_buf(window) 161 | 162 | 163 | def get_buffer_option(buffer: Buffer, option: str) -> Any: 164 | return _nvim.api.buf_get_option(buffer, option) 165 | 166 | 167 | def get_buffer_content(buffer: Buffer) -> list: 168 | return get_lines(buffer, 0, get_line_count(buffer)) 169 | 170 | 171 | def get_lines(buffer: Buffer, start: int, end: int) -> list: 172 | return _nvim.api.buf_get_lines(buffer, start, end, False) 173 | 174 | 175 | def get_line_count(buffer: Buffer) -> int: 176 | return _nvim.api.buf_line_count(buffer) 177 | 178 | 179 | def confirm(question: str) -> bool: 180 | return _nvim.funcs.confirm(question, "&Yes\n&No", 2) == 1 181 | 182 | 183 | def get_input(question: str, default: str = "") -> str: 184 | return _nvim.funcs.input(question, default) 185 | 186 | 187 | def get_global_var(name: str, default_value: Any) -> Any: 188 | try: 189 | return _nvim.api.get_var(name) 190 | except: 191 | return default_value 192 | 193 | 194 | def get_option(name: str) -> Any: 195 | return _nvim.api.get_option(name) 196 | 197 | 198 | def get_window_width(window: Window) -> int: 199 | return _nvim.api.win_get_width(window) 200 | 201 | 202 | def set_window_width(window: Window, width: int) -> None: 203 | _nvim.api.win_set_width(window, width) 204 | 205 | 206 | def get_window_height(window: Window) -> int: 207 | return _nvim.api.win_get_height(window) 208 | 209 | 210 | def set_window_height(window: Window, width: int) -> None: 211 | _nvim.api.win_set_height(window, width) 212 | 213 | 214 | def set_cursor(window: Window, cursor: Tuple[int, int]) -> None: 215 | _nvim.api.win_set_cursor(window, cursor) 216 | 217 | 218 | def render(window: Window, lines: list, modifiable: Optional[bool] = None) -> None: 219 | buffer: Buffer = get_buffer_in_window(window) 220 | instruction = _buf_set_lines(buffer, lines, modifiable) 221 | call_atomic(*instruction) 222 | 223 | 224 | def _buf_set_lines(buffer: Buffer, 225 | lines: list, 226 | modifiable: Optional[bool] = None) -> Iterator[Tuple[str, Sequence[Any]]]: 227 | modifiable = modifiable if modifiable is not None else get_buffer_option(buffer, "modifiable") 228 | if not modifiable: 229 | yield "nvim_buf_set_option", (buffer, "modifiable", True) 230 | 231 | yield "nvim_buf_set_lines", (buffer, 0, -1, True, [line.rstrip('\n') for line in lines]) 232 | if not modifiable: 233 | yield "nvim_buf_set_option", (buffer, "modifiable", False) 234 | -------------------------------------------------------------------------------- /rplugin/python3/database/sql_clients/psql_client.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Optional, Tuple 3 | 4 | from .sql_client import SqlClient, CommandResult 5 | from ..storages.connection import Connection 6 | from ..utils.log import log 7 | 8 | 9 | class PostgreSqlClient(SqlClient): 10 | 11 | def __init__(self, connection: Connection): 12 | SqlClient.__init__(self, connection) 13 | 14 | def _run_query(self, query: str, options: list = []) -> CommandResult: 15 | return self.run_command([ 16 | "psql", 17 | "--host=" + self.connection.host, 18 | "--port=" + self.connection.port, 19 | "--username=" + self.connection.username, 20 | "--pset=footer", 21 | "-c", 22 | query, 23 | ] + options, dict(os.environ, PGPASSWORD=self.connection.password, PGCONNECT_TIMEOUT="10")) 24 | 25 | def get_databases(self) -> list: 26 | result = self._run_query("SELECT datname FROM pg_database WHERE datistemplate = false", ["--tuples-only"]) 27 | if result.error: 28 | log.info("[vim-database] " + ". ".join(result.data.splitlines())) 29 | return list() 30 | 31 | return list(map(lambda database: database.strip(), result.data.splitlines())) 32 | 33 | def get_tables(self, database: str) -> list: 34 | result = self._run_query( 35 | "SELECT tablename " 36 | "FROM pg_catalog.pg_tables " 37 | "WHERE schemaname != \'pg_catalog\' AND schemaname != \'information_schema\'", 38 | ["--tuples-only", "--dbname=" + database]) 39 | 40 | if result.error: 41 | log.info("[vim-database] " + ". ".join(result.data.splitlines())) 42 | return list() 43 | 44 | return list(map(lambda table: table.strip(), result.data.splitlines())) 45 | 46 | def delete_table(self, database: str, table: str) -> None: 47 | result = self._run_query("DROP TABLE " + table, ["--tuples-only", "--dbname=" + database]) 48 | if result.error: 49 | log.info("[vim-database] " + ". ".join(result.data.splitlines())) 50 | 51 | def describe_table(self, database: str, table: str) -> Optional[list]: 52 | result = self._run_query( 53 | "SELECT column_name, column_default, is_nullable, data_type " 54 | "FROM information_schema.columns " 55 | "WHERE table_name = \'" + table + "\'", ["--tuples-only", "--dbname=" + database]) 56 | 57 | if result.error: 58 | log.info("[vim-database] " + ". ".join(result.data.splitlines())) 59 | return None 60 | 61 | lines = result.data.splitlines() 62 | if len(lines) == 0: 63 | log.info("[vim-database] No table information found") 64 | return None 65 | 66 | data = list(map(lambda line: [column.strip() for column in line.split("|")], lines)) 67 | data.insert(0, ["column_name", "column_default", "is_nullable", "data_type"]) 68 | 69 | return data 70 | 71 | def run_query(self, database: str, query: str) -> Optional[list]: 72 | result = self._run_query(query, ["--dbname=" + database]) 73 | if result.error: 74 | log.info("[vim-database] " + ". ".join(result.data.splitlines())) 75 | return None 76 | lines = result.data.splitlines() 77 | 78 | data = list(map(lambda line: [column.strip() for column in line.split("|")], lines)) 79 | if len(data) <= 1: 80 | return list() 81 | 82 | del data[1] 83 | return data 84 | 85 | def update(self, database: str, table: str, update: Tuple[str, str], condition: Tuple[str, str]) -> bool: 86 | update_column, update_value = update 87 | update_query = "UPDATE " + table + " SET " + update_column + " = " + update_value 88 | 89 | condition_column, condition_value = condition 90 | update_query = update_query + " WHERE " + condition_column + " = " + condition_value 91 | 92 | result = self._run_query(update_query, ["--dbname=" + database]) 93 | if result.error: 94 | log.info("[vim-database] " + ". ".join(result.data.splitlines())) 95 | return False 96 | 97 | return True 98 | 99 | def copy(self, database: str, table: str, unique_columns: list, new_unique_column_values: list) -> bool: 100 | log.info("[vim-database] Not supported for psql") 101 | return False 102 | 103 | def delete(self, database: str, table: str, condition: Tuple[str, str]) -> bool: 104 | condition_column, condition_value = condition 105 | delete_query = "DELETE FROM " + table + " WHERE " + condition_column + " = " + condition_value 106 | 107 | result = self._run_query(delete_query, ["--tuples-only", "--dbname=" + database]) 108 | if result.error: 109 | log.info("[vim-database] " + ". ".join(result.data.splitlines())) 110 | return False 111 | 112 | return True 113 | 114 | def get_primary_key(self, database: str, table: str) -> Optional[str]: 115 | get_primary_key_query = "SELECT a.attname " \ 116 | "FROM pg_index i " \ 117 | "JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) " \ 118 | "WHERE i.indrelid = \'" + table + "\'::regclass AND i.indisprimary" 119 | result = self._run_query(get_primary_key_query, ["--tuples-only", "--dbname=" + database]) 120 | if result.error: 121 | log.info("[vim-database] " + ". ".join(result.data.splitlines())) 122 | return None 123 | 124 | return result.data.strip() 125 | 126 | def get_unique_columns(self, database: str, table: str) -> Optional[list]: 127 | get_unique_keys_query = "SELECT a.attname " \ 128 | "FROM pg_index i " \ 129 | "JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) " \ 130 | "WHERE i.indrelid = \'" + table + "\'::regclass AND i.indisunique" 131 | result = self._run_query(get_unique_keys_query, ["--tuples-only", "--dbname=" + database]) 132 | if result.error: 133 | log.info("[vim-database] " + ". ".join(result.data.splitlines())) 134 | return None 135 | 136 | return list(map(lambda column: column.strip(), result.data.splitlines())) 137 | 138 | def get_template_insert_query(self, database: str, table: str) -> Optional[list]: 139 | result = self._run_query( 140 | "SELECT column_name, column_default, is_nullable FROM information_schema.columns WHERE table_name = \'" + 141 | table + "\'", ["--tuples-only", "--dbname=" + database]) 142 | 143 | if result.error: 144 | log.info("[vim-database] " + ". ".join(result.data.splitlines())) 145 | return None 146 | 147 | lines = result.data.splitlines() 148 | if len(lines) == 0: 149 | log.info("[vim-database] No table information found") 150 | return None 151 | 152 | columns = list(map(lambda line: [col.strip() for col in line.split("|")], lines)) 153 | 154 | name_index = 0 155 | default_value_index = 1 156 | is_nullable_index = 2 157 | insert_query = ["INSERT INTO " + table + " ("] 158 | columns_len = len(columns) 159 | for index, column in enumerate(columns): 160 | insert_query.append("\t" + column[name_index]) 161 | if index != columns_len - 1: 162 | insert_query[-1] += "," 163 | insert_query.append(") VALUES (") 164 | 165 | for index, column in enumerate(columns): 166 | default_value = column[default_value_index] 167 | colon_index = default_value.find(':') 168 | if colon_index != -1: 169 | default_value = default_value[0:colon_index] 170 | 171 | if default_value != 'NULL' or (default_value == 'NULL' and column[is_nullable_index].lower() == 'yes'): 172 | insert_query.append("\t" + default_value) 173 | else: 174 | insert_query.append("\t") 175 | 176 | if index != columns_len - 1: 177 | insert_query[-1] += "," 178 | insert_query.append(")") 179 | 180 | return insert_query 181 | -------------------------------------------------------------------------------- /rplugin/python3/database/transitions/data_ops.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | from typing import Optional, Tuple 3 | 4 | from .shared.get_primary_key_value import get_primary_key_value 5 | from ..concurrents.executors import run_in_executor 6 | from ..configs.config import UserConfig 7 | from ..states.state import Mode, State 8 | from ..transitions.shared.get_current_row_idx import get_current_row_idx 9 | from ..transitions.shared.show_ascii_table import show_ascii_table 10 | from ..transitions.shared.show_table_data import show_table_data 11 | from ..utils.log import log 12 | from ..utils.nvim import ( 13 | async_call, 14 | confirm, 15 | get_input, 16 | ) 17 | from ..views.database_window import ( 18 | get_current_database_window_line, 19 | get_current_database_window_cursor, 20 | ) 21 | 22 | 23 | async def delete_row(configs: UserConfig, state: State) -> None: 24 | row_idx = await async_call(partial(get_current_row_idx, state)) 25 | if row_idx is None: 26 | return 27 | headers, rows = state.table_data 28 | 29 | primary_key, primary_key_value = await get_primary_key_value(state, row_idx) 30 | if primary_key is None: 31 | return 32 | 33 | ans = await async_call( 34 | partial(confirm, "DELETE FROM " + state.selected_table + " WHERE " + primary_key + " = " + primary_key_value)) 35 | if not ans: 36 | return 37 | 38 | delete_success = await run_in_executor(state.sql_client.delete, state.selected_database, state.selected_table, 39 | (primary_key, "\'" + primary_key_value + "\'")) 40 | if delete_success: 41 | del rows[row_idx] 42 | state.table_data = (headers, rows) 43 | await show_ascii_table(configs, headers, rows) 44 | 45 | 46 | async def copy_row(configs: UserConfig, state: State) -> None: 47 | if state.mode != Mode.QUERY or state.user_query: 48 | return 49 | 50 | row_idx = await async_call(partial(get_current_row_idx, state)) 51 | if row_idx is None: 52 | return 53 | 54 | headers, rows = state.table_data 55 | row = rows[row_idx][:] 56 | 57 | ans = await async_call(partial(confirm, "Do you want to copy this row?")) 58 | if not ans: 59 | return 60 | 61 | header_map = dict() 62 | for header_idx, header in enumerate(headers): 63 | header_map[header] = header_idx 64 | 65 | unique_column_names = await run_in_executor( 66 | partial(state.sql_client.get_unique_columns, state.selected_database, state.selected_table)) 67 | if len(unique_column_names) == 0: 68 | log.info("[vim-database] No unique column found") 69 | return 70 | 71 | unique_columns = [] 72 | new_unique_column_values = [] 73 | for unique_column in unique_column_names: 74 | new_unique_column_value = await async_call(partial(get_input, "New unique value " + unique_column + ": ")) 75 | if new_unique_column_value: 76 | unique_columns.append((unique_column, row[header_map[unique_column]])) 77 | new_unique_column_values.append(new_unique_column_value) 78 | row[header_map[unique_column]] = new_unique_column_value 79 | else: 80 | return 81 | 82 | copy_result = await run_in_executor( 83 | partial(state.sql_client.copy, state.selected_database, state.selected_table, unique_columns, 84 | new_unique_column_values)) 85 | if copy_result: 86 | rows.append(row) 87 | state.table_data = (headers, rows) 88 | await show_ascii_table(configs, headers, rows) 89 | 90 | 91 | async def edit_row(configs: UserConfig, state: State) -> None: 92 | edit_column, edit_value, row_idx, column_idx = await _get_current_cell_value(state) 93 | if edit_column is None: 94 | return 95 | 96 | new_value = await async_call(partial(get_input, "Edit column " + edit_column + ": ", edit_value)) 97 | if new_value and new_value != edit_value: 98 | primary_key, primary_key_value = await get_primary_key_value(state, row_idx) 99 | if primary_key is None: 100 | return 101 | 102 | ans = await async_call( 103 | partial( 104 | confirm, "UPDATE " + state.selected_table + " SET " + edit_column + " = " + new_value + " WHERE " + 105 | primary_key + " = " + primary_key_value)) 106 | if not ans: 107 | return 108 | 109 | update_success = await run_in_executor( 110 | partial(state.sql_client.update, state.selected_database, state.selected_table, 111 | (edit_column, "\'" + new_value + "\'"), (primary_key, "\'" + primary_key_value + "\'"))) 112 | if update_success: 113 | data_headers, data_rows = state.table_data 114 | data_rows[row_idx][column_idx] = new_value 115 | state.table_data = (data_headers, data_rows) 116 | await show_ascii_table(configs, data_headers, data_rows) 117 | 118 | 119 | async def filter_columns(configs: UserConfig, state: State) -> None: 120 | if state.mode != Mode.QUERY or state.user_query: 121 | return 122 | 123 | filtered_columns = await async_call(partial(get_input, "Filter columns: ", ", ".join(state.filtered_columns))) 124 | filtered_columns = filtered_columns if filtered_columns is None else filtered_columns.strip() 125 | if filtered_columns: 126 | state.filtered_columns.clear() 127 | columns = filtered_columns.split(",") 128 | for column in columns: 129 | state.filtered_columns.add(column.strip()) 130 | 131 | await show_table_data(configs, state, state.selected_table) 132 | 133 | 134 | async def order(configs: UserConfig, state: State, orientation: str) -> None: 135 | if state.mode != Mode.QUERY or state.user_query: 136 | return 137 | 138 | order_column, _, _, _ = await _get_current_cell_value(state) 139 | if order_column is None: 140 | return 141 | 142 | state.order = (order_column, orientation) 143 | 144 | await show_table_data(configs, state, state.selected_table) 145 | 146 | 147 | async def row_filter(configs: UserConfig, state: State) -> None: 148 | 149 | def get_filter_condition() -> Optional[str]: 150 | condition = state.query_conditions if state.query_conditions is not None else "" 151 | return get_input("New query conditions: ", condition) 152 | 153 | filter_condition = await async_call(get_filter_condition) 154 | filter_condition = filter_condition if filter_condition is None else filter_condition.strip() 155 | if filter_condition: 156 | state.current_page = 1 157 | state.query_conditions = filter_condition 158 | await show_table_data(configs, state, state.selected_table) 159 | 160 | 161 | async def next_page(configs: UserConfig, state: State) -> None: 162 | state.current_page += 1 163 | await show_table_data(configs, state, state.selected_table) 164 | 165 | if not state.table_data[1]: 166 | state.current_page -= 1 167 | await show_table_data(configs, state, state.selected_table) 168 | 169 | log.info("[vim-database] Page " + str(state.current_page)) 170 | 171 | 172 | async def previous_page(configs: UserConfig, state: State) -> None: 173 | if state.current_page <= 1: 174 | return 175 | 176 | state.current_page -= 1 177 | await show_table_data(configs, state, state.selected_table) 178 | 179 | log.info("[vim-database] Page " + str(state.current_page)) 180 | 181 | 182 | def _get_current_row_and_column(state: State) -> Tuple[Optional[int], Optional[int]]: 183 | row_cursor, column_cursor = get_current_database_window_cursor() 184 | 185 | _, rows = state.table_data 186 | row_size = len(rows) 187 | 188 | # Minus 4 for header of the table 189 | row_idx = row_cursor - 4 190 | line = get_current_database_window_line() 191 | if row_idx < 0 or row_idx >= row_size or line is None or line[column_cursor] == '|': 192 | return None, None 193 | 194 | column_idx = 0 195 | for i in range(column_cursor): 196 | if line[i] == '|': 197 | column_idx += 1 198 | 199 | return row_idx, column_idx - 1 200 | 201 | 202 | async def _get_current_cell_value(state: State) -> Tuple[Optional[str], Optional[str], Optional[int], Optional[int]]: 203 | row, column = await async_call(partial(_get_current_row_and_column, state)) 204 | if row is None: 205 | return None, None, None, None 206 | 207 | data_headers, data_rows = state.table_data 208 | return data_headers[column], data_rows[row][column], row, column 209 | -------------------------------------------------------------------------------- /rplugin/python3/database/sql_clients/mysql_client.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Tuple 2 | 3 | from .sql_client import SqlClient, CommandResult 4 | from ..storages.connection import Connection 5 | from ..utils.log import log 6 | 7 | 8 | class MySqlClient(SqlClient): 9 | 10 | def __init__(self, connection: Connection): 11 | SqlClient.__init__(self, connection) 12 | 13 | def _run_query(self, query: str, options: list = []) -> CommandResult: 14 | return self.run_command([ 15 | "mysql", 16 | "--unbuffered", 17 | "--batch", 18 | "--connect-timeout=10", 19 | "--host=" + self.connection.host, 20 | "--port=" + self.connection.port, 21 | "--user=" + self.connection.username, 22 | "--password=" + self.connection.password, 23 | "-e", 24 | query, 25 | ] + options) 26 | 27 | def get_databases(self) -> list: 28 | result = self._run_query("SHOW DATABASES", ["--skip-column-names"]) 29 | if result.error: 30 | log.info("[vim-database] " + result.data) 31 | return list() 32 | return result.data.splitlines() 33 | 34 | def get_tables(self, database: str) -> list: 35 | result = self._run_query("SHOW TABLES FROM " + database, ["--skip-column-names"]) 36 | 37 | if result.error: 38 | log.info("[vim-database] " + result.data) 39 | return list() 40 | return result.data.splitlines() 41 | 42 | def delete_table(self, database: str, table: str) -> None: 43 | result = self._run_query("DROP TABLE " + database + "." + table) 44 | if result.error: 45 | log.info("[vim-database] " + result.data) 46 | 47 | def describe_table(self, database: str, table: str) -> Optional[list]: 48 | result = self._run_query("DESCRIBE " + database + "." + table) 49 | if result.error: 50 | log.info("[vim-database] " + result.data) 51 | return None 52 | 53 | lines = result.data.splitlines() 54 | if len(lines) < 2: 55 | log.info("[vim-database] No table information found") 56 | return None 57 | 58 | return list(map(lambda line: line.split("\t"), lines)) 59 | 60 | def run_query(self, database: str, query: str) -> Optional[list]: 61 | result = self._run_query(query, ["--database=" + database]) 62 | if result.error: 63 | log.info("[vim-database] " + result.data) 64 | return None 65 | 66 | lines = result.data.splitlines() 67 | return list(map(lambda line: line.split("\t"), lines)) 68 | 69 | def update(self, database: str, table: str, update: Tuple[str, str], condition: Tuple[str, str]) -> bool: 70 | update_column, update_value = update 71 | update_query = "UPDATE " + table + " SET " + update_column + " = " + update_value 72 | 73 | condition_column, condition_value = condition 74 | update_query = update_query + " WHERE " + condition_column + " = " + condition_value 75 | 76 | result = self._run_query(update_query, ["--database=" + database]) 77 | if result.error: 78 | log.info("[vim-database] " + result.data) 79 | return False 80 | 81 | return True 82 | 83 | def copy(self, database: str, table: str, unique_columns: list, new_unique_column_values: list) -> bool: 84 | num_unique_columns = len(unique_columns) 85 | if num_unique_columns != len(new_unique_column_values): 86 | log.info("[vim-database] The lenght of unique columns must be equal to new unique column values") 87 | return False 88 | 89 | assign_query = "" 90 | condition_query = "" 91 | for index in range(num_unique_columns): 92 | unique_column, unique_column_value = unique_columns[index] 93 | new_unique_column_value = new_unique_column_values[index] 94 | if index != 0: 95 | assign_query += ", " 96 | condition_query += " AND " 97 | assign_query += unique_column + " = " + (new_unique_column_value if new_unique_column_value == 'NULL' else 98 | ("\'" + new_unique_column_value + "\'")) 99 | if unique_column_value == 'NULL': 100 | condition_query += unique_column + " is NULL" 101 | else: 102 | condition_query += unique_column + " = + \'" + unique_column_value + "\'" 103 | 104 | create_temporary_query = \ 105 | "CREATE TEMPORARY TABLE tmptable_1 SELECT * FROM " + table + " WHERE " + condition_query + ";" 106 | update_primary_key_temporary_query = "UPDATE tmptable_1 SET " + assign_query + ";" 107 | insert_query = "INSERT INTO " + table + " SELECT * FROM tmptable_1;" 108 | delete_temporary_query = "DROP TEMPORARY TABLE IF EXISTS tmptable_1;" 109 | copy_query = create_temporary_query + update_primary_key_temporary_query + insert_query + delete_temporary_query 110 | 111 | result = self._run_query(copy_query, ["--database=" + database]) 112 | if result.error: 113 | log.info("[vim-database] " + result.data) 114 | return False 115 | 116 | return True 117 | 118 | def delete(self, database: str, table: str, condition: Tuple[str, str]) -> bool: 119 | condition_column, condition_value = condition 120 | delete_query = "DELETE FROM " + table + " WHERE " + condition_column + " = " + condition_value 121 | 122 | result = self._run_query(delete_query, ["--database=" + database]) 123 | if result.error: 124 | log.info("[vim-database] " + result.data) 125 | return False 126 | 127 | return True 128 | 129 | def get_primary_key(self, database: str, table: str) -> Optional[str]: 130 | get_primary_key_query = \ 131 | "SELECT COLUMN_NAME " \ 132 | "FROM information_schema.KEY_COLUMN_USAGE " \ 133 | "WHERE TABLE_NAME = \'" + table + "\' " \ 134 | "AND CONSTRAINT_NAME = 'PRIMARY' " \ 135 | "AND CONSTRAINT_SCHEMA=\'" + database + "\'" 136 | result = self._run_query(get_primary_key_query, ["--skip-column-names"]) 137 | if result.error: 138 | log.info("[vim-database] " + result.data) 139 | return None 140 | 141 | return result.data 142 | 143 | def get_unique_columns(self, database: str, table: str) -> Optional[list]: 144 | get_unique_keys_query = \ 145 | "SELECT COLUMN_NAME " \ 146 | "FROM information_schema.STATISTICS " \ 147 | "WHERE TABLE_SCHEMA = \'" + database + "\' AND TABLE_NAME = \'" + table + "\' AND NON_UNIQUE = 0" 148 | result = self._run_query(get_unique_keys_query, ["--skip-column-names"]) 149 | if result.error: 150 | log.info("[vim-database] " + result.data) 151 | return None 152 | 153 | return result.data.splitlines() 154 | 155 | def get_template_insert_query(self, database: str, table: str) -> Optional[list]: 156 | get_columns_query = \ 157 | "SELECT COLUMN_NAME, COLUMN_DEFAULT, IS_NULLABLE " \ 158 | "FROM information_schema.COLUMNS " \ 159 | "WHERE TABLE_NAME = \'" + table + "\' AND TABLE_SCHEMA=\'" + database + "\'" 160 | result = self._run_query(get_columns_query, ["--skip-column-names"]) 161 | if result.error: 162 | log.info("[vim-database] " + result.data) 163 | return None 164 | 165 | lines = result.data.splitlines() 166 | columns = list(map(lambda line: line.split("\t"), lines)) 167 | insert_query = ["INSERT INTO " + table + " ("] 168 | columns_len = len(columns) 169 | for index, column in enumerate(columns): 170 | insert_query.append("\t" + column[0]) 171 | if index != columns_len - 1: 172 | insert_query[-1] += "," 173 | insert_query.append(") VALUES (") 174 | 175 | for index, column in enumerate(columns): 176 | if column[1] != 'NULL' or (column[1] == 'NULL' and column[2].lower() == 'yes'): 177 | insert_query.append("\t" + (column[1] if column[1] == 'NULL' else ("\'" + column[1] + "\'"))) 178 | else: 179 | insert_query.append("\t") 180 | if index != columns_len - 1: 181 | insert_query[-1] += "," 182 | insert_query.append(")") 183 | 184 | return insert_query 185 | -------------------------------------------------------------------------------- /rplugin/python3/database/transitions/connection_ops.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | from typing import Optional, Tuple 3 | 4 | from ..concurrents.executors import run_in_executor 5 | from ..configs.config import UserConfig 6 | from ..sql_clients.sql_client_factory import SqlClientFactory 7 | from ..states.state import Mode, State 8 | from ..storages.connection import (Connection, ConnectionType, store_connection, remove_connection) 9 | from ..utils.ascii_table import ascii_table 10 | from ..utils.files import is_file_exists 11 | from ..utils.log import log 12 | from ..utils.nvim import ( 13 | async_call, 14 | confirm, 15 | get_input, 16 | set_cursor, 17 | render, 18 | ) 19 | from ..views.database_window import (open_database_window, get_current_database_window_row) 20 | 21 | 22 | async def new_connection(settings: UserConfig, state: State) -> None: 23 | connection = await async_call(_new_connection) 24 | if connection is not None: 25 | # Store the connection 26 | await run_in_executor(partial(store_connection, connection)) 27 | 28 | state.connections.append(connection) 29 | if state.selected_connection is None: 30 | state.selected_connection = connection 31 | state.selected_database = connection.database 32 | state.sql_client = SqlClientFactory.create(state.selected_connection) 33 | 34 | # Refresh connections table 35 | await show_connections(settings, state) 36 | 37 | log.info('[vim-database] Connection created') 38 | 39 | return None 40 | 41 | 42 | async def show_connections(settings: UserConfig, state: State) -> None: 43 | window = await async_call(partial(open_database_window, settings)) 44 | state.mode = Mode.CONNECTION 45 | 46 | connection_headers, connection_rows, selected_idx = _get_connections_from_state(state) 47 | await async_call(partial(render, window, ascii_table(connection_headers, connection_rows))) 48 | await async_call(partial(set_cursor, window, (selected_idx + 4, 0))) 49 | 50 | 51 | async def select_connection(settings: UserConfig, state: State) -> None: 52 | if state.mode != Mode.CONNECTION or not state.connections: 53 | return 54 | 55 | connection_idx = await async_call(partial(_get_connection_idx, state)) 56 | if connection_idx is None: 57 | return 58 | 59 | state.selected_connection = state.connections[connection_idx] 60 | state.selected_database = state.selected_connection.database 61 | state.sql_client = SqlClientFactory.create(state.selected_connection) 62 | 63 | # Update connections table 64 | window = await async_call(partial(open_database_window, settings)) 65 | connection_headers, connection_rows, selected_idx = _get_connections_from_state(state) 66 | await async_call(partial(render, window, ascii_table(connection_headers, connection_rows))) 67 | await async_call(partial(set_cursor, window, (selected_idx + 4, 0))) 68 | 69 | 70 | async def edit_connection(configs: UserConfig, state: State) -> None: 71 | connection_idx = await async_call(partial(_get_connection_idx, state)) 72 | if connection_idx is None: 73 | return 74 | 75 | old_connection = state.connections[connection_idx] 76 | connection: Optional[Connection] = None 77 | if old_connection.connection_type is ConnectionType.SQLITE: 78 | connection = await async_call(partial(_new_sqlite_connection, old_connection)) 79 | else: 80 | connection = await async_call( 81 | partial(_new_mysql_or_postgresql_connection, old_connection.connection_type, old_connection)) 82 | 83 | if connection is None: 84 | return 85 | 86 | # Delete old connection 87 | await run_in_executor(partial(remove_connection, old_connection)) 88 | del state.connections[connection_idx] 89 | 90 | # Store the new connection 91 | await run_in_executor(partial(store_connection, connection)) 92 | state.connections.append(connection) 93 | 94 | if old_connection.name == state.selected_connection.name: 95 | state.load_default_connection() 96 | 97 | # Refresh connections table 98 | await show_connections(configs, state) 99 | 100 | log.info('[vim-database] Connection updated') 101 | 102 | 103 | async def delete_connection(settings: UserConfig, state: State) -> None: 104 | connection_idx = await async_call(partial(_get_connection_idx, state)) 105 | if connection_idx is None: 106 | return 107 | 108 | connection = state.connections[connection_idx] 109 | 110 | ans = await async_call(partial(confirm, "Do you want to delete connection " + connection.name + "?")) 111 | if not ans: 112 | return 113 | 114 | await run_in_executor(partial(remove_connection, connection)) 115 | 116 | del state.connections[connection_idx] 117 | if connection.name == state.selected_connection.name: 118 | state.load_default_connection() 119 | 120 | # Update connections table 121 | await show_connections(settings, state) 122 | 123 | log.info('[vim-database] Connection deleted') 124 | 125 | 126 | def _get_connections_from_state(state: State) -> Tuple[list, list, int]: 127 | connections = [] 128 | selected_idx = 0 129 | for index, connection in enumerate(state.connections): 130 | if state.selected_connection.name == connection.name: 131 | selected_idx = index 132 | connections.append([ 133 | connection.name + " (*)" if state.selected_connection.name == connection.name else connection.name, 134 | connection.connection_type.to_string(), "" if connection.host is None else connection.host, 135 | "" if connection.port is None else connection.port, 136 | "" if connection.username is None else connection.username, 137 | "" if connection.password is None else connection.password, connection.database 138 | ]) 139 | 140 | return ["Name", "Type", "Host", "Port", "Username", "Password", "Database"], connections, selected_idx 141 | 142 | 143 | def _get_connection_idx(state: State) -> Optional[int]: 144 | row = get_current_database_window_row() 145 | connections_size = len(state.connections) 146 | # Minus 4 for header of the table 147 | connection_idx = row - 4 148 | if connection_idx < 0 or connection_idx >= connections_size: 149 | return None 150 | 151 | return connection_idx 152 | 153 | 154 | def _new_sqlite_connection(connection: Optional[Connection] = None) -> Optional[Connection]: 155 | name = get_input("Name: ", connection.name if connection else "") 156 | if not name: 157 | return None 158 | file = get_input("File: ", connection.database if connection else "") 159 | if not file: 160 | return None 161 | if not is_file_exists(file): 162 | log.info('[vim-database] File not found: ' + file) 163 | return None 164 | 165 | return Connection(name=name, 166 | connection_type=ConnectionType.SQLITE, 167 | host=None, 168 | port=None, 169 | username=None, 170 | password=None, 171 | database=file) 172 | 173 | 174 | def _new_mysql_or_postgresql_connection(connection_type: ConnectionType, 175 | connection: Optional[Connection] = None) -> Optional[Connection]: 176 | name = get_input("Name: ", connection.name if connection else "") 177 | if not name: 178 | return None 179 | host = get_input("Host: ", connection.host if connection else "") 180 | if not host: 181 | return None 182 | port = get_input("Port: ", connection.port if connection else "") 183 | if not port: 184 | return None 185 | username = get_input("Username: ", connection.username if connection else "") 186 | if not username: 187 | return None 188 | password = get_input("Password: ", connection.password if connection else "") 189 | if not password: 190 | return None 191 | database = get_input("Database: ", connection.database if connection else "") 192 | if not database: 193 | return None 194 | 195 | return Connection(name=name, 196 | connection_type=connection_type, 197 | host=host, 198 | port=port, 199 | username=username, 200 | password=password, 201 | database=database) 202 | 203 | 204 | def _new_connection() -> Optional[Connection]: 205 | try: 206 | connection_type_value = get_input("Connection type (1: SQLite, 2: MySQL, 3: PostgreSQL): ") 207 | if connection_type_value: 208 | connection_type = ConnectionType(int(connection_type_value)) 209 | if connection_type is ConnectionType.SQLITE: 210 | return _new_sqlite_connection() 211 | elif connection_type is ConnectionType.MYSQL: 212 | return _new_mysql_or_postgresql_connection(ConnectionType.MYSQL) 213 | elif connection_type is ConnectionType.POSTGRESQL: 214 | return _new_mysql_or_postgresql_connection(ConnectionType.POSTGRESQL) 215 | except: 216 | log.info('[vim-database] Invalid connection type') 217 | return None 218 | -------------------------------------------------------------------------------- /rplugin/python3/database/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | from asyncio import AbstractEventLoop, Lock, run_coroutine_threadsafe 3 | from typing import Any, Awaitable, Callable, Sequence 4 | 5 | from pynvim import Nvim, plugin, command, function 6 | 7 | from .concurrents.executor_service import ExecutorService 8 | from .configs.config import load_config 9 | from .states.state import init_state, Mode 10 | from .transitions.connection_ops import show_connections, select_connection, delete_connection, new_connection, \ 11 | edit_connection 12 | from .transitions.data_ops import ( 13 | copy_row, 14 | edit_row, 15 | filter_columns, 16 | order, 17 | show_table_data, 18 | delete_row, 19 | row_filter, 20 | next_page, 21 | previous_page, 22 | ) 23 | from .transitions.database_ops import show_databases, select_database 24 | from .transitions.lsp_ops import lsp_config 25 | from .transitions.query_ops import run_query, show_update_query, show_copy_query, show_insert_query 26 | from .transitions.table_ops import (list_tables_fzf, describe_table, select_table, delete_table, describe_current_table, 27 | show_tables, table_filter) 28 | from .transitions.view_ops import (resize_database, close_query, show_query, toggle_query, close, toggle) 29 | from .utils.files import create_folder_if_not_present 30 | from .utils.log import log, init_log 31 | from .utils.nvim import init_nvim, get_global_var 32 | 33 | 34 | @plugin 35 | class DatabasePlugin(object): 36 | 37 | def __init__(self, nvim: Nvim) -> None: 38 | self._nvim = nvim 39 | self._lock = Lock() 40 | self._executor = ExecutorService() 41 | init_nvim(self._nvim) 42 | init_log(self._nvim) 43 | self._configs = None 44 | self._state = None 45 | database_workspace = get_global_var("database_workspace", os.getcwd()) 46 | os.chdir(database_workspace) 47 | 48 | create_folder_if_not_present(os.path.join(os.path.expanduser("~"), ".vim-database")) 49 | 50 | def _submit(self, coro: Awaitable[None]) -> None: 51 | loop: AbstractEventLoop = self._nvim.loop 52 | 53 | def submit() -> None: 54 | future = run_coroutine_threadsafe(coro, loop) 55 | 56 | try: 57 | future.result() 58 | except Exception as e: 59 | log.exception("%s", str(e)) 60 | 61 | self._executor.run_sync(submit) 62 | 63 | def _run(self, func: Callable[..., Awaitable[None]], *args: Any) -> None: 64 | 65 | async def run() -> None: 66 | async with self._lock: 67 | if self._configs is None: 68 | self._configs = await load_config() 69 | if self._state is None: 70 | self._state = await init_state() 71 | await func(self._configs, self._state, *args) 72 | 73 | self._submit(run()) 74 | 75 | @command('VDToggleDatabase') 76 | def toggle_command(self) -> None: 77 | self._run(toggle) 78 | 79 | @command('VDToggleQuery') 80 | def toggle_query_command(self) -> None: 81 | self._run(toggle_query) 82 | 83 | @command('VDLSPConfig') 84 | def lsp_config_command(self) -> None: 85 | self._run(lsp_config) 86 | 87 | @function('VimDatabase_quit') 88 | def quit_function(self, _: Sequence[Any]) -> None: 89 | self._run(close) 90 | 91 | @function('VimDatabase_show_connections') 92 | def show_connections_function(self, _: Sequence[Any]) -> None: 93 | self._run(show_connections) 94 | 95 | @function('VimDatabase_show_databases') 96 | def show_databases_function(self, _: Sequence[Any]) -> None: 97 | self._run(show_databases) 98 | 99 | @function('VimDatabase_show_tables') 100 | def show_tables_function(self, _: Sequence[Any]) -> None: 101 | self._run(show_tables) 102 | 103 | @function('VimDatabase_show_query') 104 | def show_query_function(self, _: Sequence[Any]) -> None: 105 | self._run(show_query) 106 | 107 | @function('VimDatabase_select') 108 | def select_function(self, _: Sequence[Any]) -> None: 109 | if self._state.mode == Mode.CONNECTION and self._state.connections: 110 | self._run(select_connection) 111 | elif self._state.mode == Mode.DATABASE and self._state.databases: 112 | self._run(select_database) 113 | elif self._state.mode == Mode.TABLE and self._state.tables: 114 | self._run(select_table) 115 | elif self._state.mode == Mode.TABLE_INFO: 116 | self._run(show_table_data, self._state.selected_table) 117 | 118 | @function('VimDatabase_delete') 119 | def delete_function(self, _: Sequence[Any]) -> None: 120 | if self._state.mode == Mode.CONNECTION and self._state.connections: 121 | self._run(delete_connection) 122 | elif self._state.mode == Mode.TABLE and self._state.tables: 123 | self._run(delete_table) 124 | elif self._state.mode == Mode.QUERY and not self._state.user_query: 125 | self._run(delete_row) 126 | 127 | @function('VimDatabase_new') 128 | def new_function(self, _: Sequence[Any]) -> None: 129 | if self._state.mode == Mode.CONNECTION: 130 | self._run(new_connection) 131 | 132 | @function('VimDatabase_copy') 133 | def copy_function(self, _: Sequence[Any]) -> None: 134 | self._run(copy_row) 135 | 136 | @function('VimDatabase_edit') 137 | def edit_function(self, _: Sequence[Any]) -> None: 138 | if self._state.mode == Mode.CONNECTION: 139 | self._run(edit_connection) 140 | elif self._state.mode == Mode.QUERY and not self._state.user_query: 141 | self._run(edit_row) 142 | 143 | @function('VimDatabase_show_update_query') 144 | def show_update_query_function(self, _: Sequence[Any]) -> None: 145 | self._run(show_update_query) 146 | 147 | @function('VimDatabase_show_copy_query') 148 | def show_copy_query_function(self, _: Sequence[Any]) -> None: 149 | self._run(show_copy_query) 150 | 151 | @function('VimDatabase_show_insert_query') 152 | def show_insert_query_function(self, _: Sequence[Any]) -> None: 153 | self._run(show_insert_query) 154 | 155 | @function('VimDatabase_info') 156 | def info_function(self, _: Sequence[Any]) -> None: 157 | if self._state.mode == Mode.TABLE and self._state.tables: 158 | self._run(describe_current_table) 159 | elif self._state.mode == Mode.QUERY and not self._state.user_query: 160 | self._run(describe_table, self._state.selected_table) 161 | 162 | @function('VimDatabase_filter') 163 | def filter_function(self, _: Sequence[Any]) -> None: 164 | if self._state.mode == Mode.TABLE: 165 | self._run(table_filter) 166 | elif self._state.mode == Mode.QUERY and not self._state.user_query: 167 | self._run(row_filter) 168 | 169 | @function('VimDatabase_clear_filter') 170 | def clear_filter_function(self, _: Sequence[Any]) -> None: 171 | self._state.filtered_tables = None 172 | self._state.query_conditions = None 173 | log.info("[vim-database] Filter was cleared") 174 | 175 | if self._state.mode == Mode.TABLE: 176 | self._run(show_tables) 177 | elif self._state.mode == Mode.QUERY and not self._state.user_query: 178 | self._run(show_table_data, self._state.selected_table) 179 | 180 | @function('VimDatabase_filter_columns') 181 | def filter_columns_function(self, _: Sequence[Any]) -> None: 182 | self._run(filter_columns) 183 | 184 | @function('VimDatabase_order') 185 | def order_function(self, _: Sequence[Any]) -> None: 186 | self._run(order, "ASC") 187 | 188 | @function('VimDatabase_order_desc') 189 | def order_desc_function(self, _: Sequence[Any]) -> None: 190 | self._run(order, "DESC") 191 | 192 | @function('VimDatabase_next') 193 | def next_function(self, _: Sequence[Any]) -> None: 194 | if self._state.mode == Mode.QUERY and not self._state.user_query: 195 | self._run(next_page) 196 | 197 | @function('VimDatabase_previous') 198 | def previous_function(self, _: Sequence[Any]) -> None: 199 | if self._state.mode == Mode.QUERY and not self._state.user_query: 200 | self._run(previous_page) 201 | 202 | @function('VimDatabase_clear_filter_column') 203 | def clear_filter_column_function(self, _: Sequence[Any]) -> None: 204 | if self._state.mode != Mode.QUERY or self._state.user_query: 205 | return 206 | 207 | if self._state.filtered_columns: 208 | log.info("[vim-database] Filter columns was cleared") 209 | self._state.filtered_columns.clear() 210 | self._run(show_table_data, self._state.selected_table) 211 | 212 | @function('VimDatabase_refresh') 213 | def refresh_function(self, _: Sequence[Any]) -> None: 214 | if self._state.mode == Mode.DATABASE and self._state.databases: 215 | self._run(show_databases) 216 | elif self._state.mode == Mode.TABLE and self._state.tables: 217 | self._run(show_tables) 218 | elif self._state.mode == Mode.QUERY and not self._state.user_query: 219 | self._run(show_table_data, self._state.selected_table) 220 | elif self._state.mode == Mode.TABLE_INFO: 221 | self._run(describe_table, self._state.selected_table) 222 | 223 | @function('VimDatabase_bigger') 224 | def bigger_function(self, _: Sequence[Any]) -> None: 225 | self._run(resize_database, 2) 226 | 227 | @function('VimDatabase_smaller') 228 | def smaller_function(self, _: Sequence[Any]) -> None: 229 | self._run(resize_database, -2) 230 | 231 | @function('VimDatabase_list_tables_fzf') 232 | def list_tables_fzf_function(self, _: Sequence[Any]) -> None: 233 | self._run(list_tables_fzf) 234 | 235 | @function('VimDatabase_select_table_fzf') 236 | def select_table_fzf_table(self, args: Sequence[Any]) -> None: 237 | self._run(show_table_data, str(args[0])) 238 | 239 | @function('VimDatabaseQuery_quit') 240 | def quit_query_function(self, _: Sequence[Any]) -> None: 241 | self._run(close_query) 242 | 243 | @function('VimDatabaseQuery_run_query') 244 | def run_query_function(self, _: Sequence[Any]) -> None: 245 | self._run(run_query) 246 | --------------------------------------------------------------------------------