From 487db539c77b687899e075699adbc9f721722c2d Mon Sep 17 00:00:00 2001 From: Michael Ionov Date: Tue, 9 Jan 2024 20:20:58 +0200 Subject: [PATCH] feat: add update mechanism --- package.json | 1 + pnpm-lock.yaml | 38 +++++---------- src-tauri/src/database/connections.rs | 8 +--- src-tauri/src/database/engine/mysql/query.rs | 7 ++- .../src/database/engine/postgresql/query.rs | 31 +++--------- .../src/database/engine/postgresql/tables.rs | 10 ++-- src-tauri/src/handlers/queries.rs | 7 ++- src-tauri/src/queues/query.rs | 16 ++----- src-tauri/src/utils/error.rs | 2 +- .../Console/Content/QueryTab/Results.tsx | 47 +++++++------------ .../QueryTab/components/Pagination.tsx | 13 +++-- .../Screens/Console/Sidebar/Sidebar.tsx | 16 +++---- .../Console/Sidebar/TableColumnsCollapse.tsx | 20 ++------ .../Home/Connections/AddConnectionForm.tsx | 8 ++-- src/services/Backend.ts | 23 ++++++++- src/utils/i18n/en.json | 4 +- src/utils/utils.ts | 8 ++-- 17 files changed, 108 insertions(+), 151 deletions(-) diff --git a/package.json b/package.json index b95aac4..157b6a9 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "solid-form-handler": "^1.2.0", "solid-js": "^1.7.7", "split.js": "^1.6.5", + "sql-bricks": "^3.0.1", "sql-formatter": "^12.2.3", "tauri-plugin-store-api": "github:tauri-apps/tauri-plugin-store#v1", "tauri-plugin-window-state-api": "github:tauri-apps/tauri-plugin-window-state#v1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6cb376f..ffcf27e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -70,6 +70,9 @@ dependencies: split.js: specifier: ^1.6.5 version: 1.6.5 + sql-bricks: + specifier: ^3.0.1 + version: 3.0.1 sql-formatter: specifier: ^12.2.3 version: 12.2.3 @@ -2056,7 +2059,7 @@ packages: is-regex: 1.1.4 is-shared-array-buffer: 1.0.2 is-string: 1.0.7 - is-typed-array: 1.1.10 + is-typed-array: 1.1.12 is-weakref: 1.0.2 object-inspect: 1.12.3 object-keys: 1.1.1 @@ -2068,7 +2071,7 @@ packages: string.prototype.trimstart: 1.0.6 typed-array-length: 1.0.4 unbox-primitive: 1.0.2 - which-typed-array: 1.1.9 + which-typed-array: 1.1.11 /es-abstract@1.22.2: resolution: {integrity: sha512-YoxfFcDmhjOgWPWsV13+2RNjq1F6UQnfs+8TftwNqtzlmFzEXvlUwdrNrYeaizfjQzRMxkZ6ElWMOJIFKdVqwA==} @@ -2671,7 +2674,7 @@ packages: dependencies: call-bind: 1.0.2 get-intrinsic: 1.2.1 - is-typed-array: 1.1.10 + is-typed-array: 1.1.12 /is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} @@ -2795,22 +2798,11 @@ packages: dependencies: has-symbols: 1.0.3 - /is-typed-array@1.1.10: - resolution: {integrity: sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==} - engines: {node: '>= 0.4'} - dependencies: - available-typed-arrays: 1.0.5 - call-bind: 1.0.2 - for-each: 0.3.3 - gopd: 1.0.1 - has-tostringtag: 1.0.0 - /is-typed-array@1.1.12: resolution: {integrity: sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==} engines: {node: '>= 0.4'} dependencies: which-typed-array: 1.1.11 - dev: true /is-weakref@1.0.2: resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} @@ -3759,6 +3751,10 @@ packages: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} dev: false + /sql-bricks@3.0.1: + resolution: {integrity: sha512-ZkU/R+bwf7d9FxlwMJp/31P5bluVCjUuftutkqJjQKH1QMCE1iaEc0xeY0aVepc38fxC+ljUrqausGCzzcHzHQ==} + dev: false + /sql-formatter@12.2.3: resolution: {integrity: sha512-sVRjEBTKJ5to2kfn11eDHcfVswz1//AL6HdGbPVN8ROWQ/XTv7E3z7rjgRxEimaBq5yDBE55JCljgcJ8a3+s7Q==} hasBin: true @@ -4071,7 +4067,7 @@ packages: dependencies: call-bind: 1.0.2 for-each: 0.3.3 - is-typed-array: 1.1.10 + is-typed-array: 1.1.12 /typescript@4.9.5: resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} @@ -4244,18 +4240,6 @@ packages: for-each: 0.3.3 gopd: 1.0.1 has-tostringtag: 1.0.0 - dev: true - - /which-typed-array@1.1.9: - resolution: {integrity: sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==} - engines: {node: '>= 0.4'} - dependencies: - available-typed-arrays: 1.0.5 - call-bind: 1.0.2 - for-each: 0.3.3 - gopd: 1.0.1 - has-tostringtag: 1.0.0 - is-typed-array: 1.1.10 /which@1.3.1: resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} diff --git a/src-tauri/src/database/connections.rs b/src-tauri/src/database/connections.rs index 733d6b0..c6647ba 100644 --- a/src-tauri/src/database/connections.rs +++ b/src-tauri/src/database/connections.rs @@ -106,12 +106,6 @@ pub struct InitiatedConnection { pub schema: String, } -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PreparedStatement { - pub statement: String, - pub params: Vec, -} - impl ConnectionConfig { pub fn new( dialect: Dialect, @@ -396,7 +390,7 @@ impl InitiatedConnection { } } - pub async fn execute_tx(&self, queries: Vec) -> Result<(), Error> { + pub async fn execute_tx(&self, queries: Vec<&str>) -> Result<(), Error> { match &self.pool { ConnectionPool::Mysql(pool) => engine::mysql::query::execute_tx(pool, queries), ConnectionPool::Postgresql(pool) => { diff --git a/src-tauri/src/database/engine/mysql/query.rs b/src-tauri/src/database/engine/mysql/query.rs index a5303a9..952f34c 100644 --- a/src-tauri/src/database/engine/mysql/query.rs +++ b/src-tauri/src/database/engine/mysql/query.rs @@ -4,7 +4,7 @@ use mysql::{from_row, Pool, PooledConn, Row, TxOpts}; use serde_json::Value; use tracing::info; -use crate::database::connections::{PreparedStatement, ResultSet, TableMetadata}; +use crate::database::connections::{ResultSet, TableMetadata}; use crate::utils::error::Error; use super::utils::row_to_object; @@ -56,14 +56,13 @@ pub fn execute_query(pool: &Pool, query: &str) -> Result { }); } -pub fn execute_tx(pool: &Pool, queries: Vec) -> Result<(), Error> { +pub fn execute_tx(pool: &Pool, queries: Vec<&str>) -> Result<(), Error> { match pool .start_transaction(TxOpts::default()) .and_then(|mut tx| { let mut error = None; for q in queries { - info!(?q.statement, ?q.params, "Executing query"); - let success = tx.exec_iter(q.statement, q.params); + let success = tx.query_iter(q); if success.is_err() { error = Some(success.err().unwrap()); break; diff --git a/src-tauri/src/database/engine/postgresql/query.rs b/src-tauri/src/database/engine/postgresql/query.rs index f883918..71e5c11 100644 --- a/src-tauri/src/database/engine/postgresql/query.rs +++ b/src-tauri/src/database/engine/postgresql/query.rs @@ -1,11 +1,11 @@ use anyhow::Result; -use deadpool_postgres::{GenericClient, Pool}; +use deadpool_postgres::Pool; use futures::{pin_mut, TryStreamExt}; use serde_json::Value; use tracing::debug; use crate::{ - database::connections::{PreparedStatement, ResultSet, TableMetadata}, + database::connections::{ResultSet, TableMetadata}, utils::error::Error, }; @@ -48,32 +48,13 @@ pub async fn execute_query(pool: &Pool, query: &str) -> Result { Ok(set) } -pub async fn execute_tx(pool: &Pool, queries: Vec) -> Result<(), Error> { +pub async fn execute_tx(pool: &Pool, queries: Vec<&str>) -> Result<(), Error> { let mut conn = pool.get().await?; let tx = conn.transaction().await?; for q in queries { - debug!(?q.statement, ?q.params, "Executing query"); - // replace each occurence of ? in string with $1, $2, $3, etc. - let mut i = 0; - let query = q - .statement - .clone() - .split("") - .enumerate() - .map(|(_, c)| { - if c == "?" { - i += 1; - format!("${}", i) - } else { - c.to_string() - } - }) - .collect::>() - .join(""); - - debug!(?query, "Executing query"); - - match tx.execute_raw(&query, &q.params).await { + debug!(?q, "Executing query"); + let params: Vec = vec![]; + match tx.execute_raw(q, ¶ms).await { Ok(..) => {} Err(e) => { tx.rollback().await?; diff --git a/src-tauri/src/database/engine/postgresql/tables.rs b/src-tauri/src/database/engine/postgresql/tables.rs index ee9a7e9..e1f2937 100644 --- a/src-tauri/src/database/engine/postgresql/tables.rs +++ b/src-tauri/src/database/engine/postgresql/tables.rs @@ -73,12 +73,16 @@ pub async fn get_constraints( ) -> Result> { let schema = conn.get_schema(); let query = format!( - "SELECT CONSTRAINT_NAME, TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, ORDINAL_POSITION - FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA = '{}'", + "SELECT c.column_name, c.table_name, tc.constraint_name, c.table_schema, c.ordinal_position + FROM information_schema.table_constraints tc + JOIN information_schema.constraint_column_usage AS ccu USING (constraint_schema, constraint_name) + JOIN information_schema.columns AS c ON c.table_schema = tc.constraint_schema + AND tc.table_name = c.table_name AND ccu.column_name = c.column_name + WHERE constraint_type = 'PRIMARY KEY' and c.table_schema = '{}'", schema ); let query = match table { - Some(table) => format!("{} AND TABLE_NAME = '{}'", query, table), + Some(table) => format!("{} AND c.table_name = '{}'", query, table), None => format!("{};", query), }; Ok(raw_query(pool.clone(), &query).await?) diff --git a/src-tauri/src/handlers/queries.rs b/src-tauri/src/handlers/queries.rs index 78240f2..4d0bd6c 100644 --- a/src-tauri/src/handlers/queries.rs +++ b/src-tauri/src/handlers/queries.rs @@ -1,8 +1,7 @@ use std::fs::read_to_string; use crate::{ - database::connections::PreparedStatement, - queues::query::{QueryTask, QueryTaskEnqueueResult, QueryTaskStatus, TableQuery}, + queues::query::{QueryTask, QueryTaskEnqueueResult, QueryTaskStatus}, state::{AsyncState, ServiceAccess}, utils::{ self, @@ -26,7 +25,7 @@ pub async fn enqueue_query( tab_idx: usize, sql: &str, auto_limit: bool, - table: Option, + table: Option, ) -> CommandResult { info!(sql, conn_id, tab_idx, "enqueue_query"); let conn = app_handle.acquire_connection(conn_id.clone()); @@ -103,7 +102,7 @@ pub async fn get_views(app_handle: AppHandle, conn_id: String) -> CommandResult< pub async fn execute_tx( app_handle: AppHandle, conn_id: String, - queries: Vec, + queries: Vec<&str>, ) -> CommandResult<()> { let connection = app_handle.acquire_connection(conn_id); connection.execute_tx(queries).await?; diff --git a/src-tauri/src/queues/query.rs b/src-tauri/src/queues/query.rs index 68cce5f..6eb158a 100644 --- a/src-tauri/src/queues/query.rs +++ b/src-tauri/src/queues/query.rs @@ -33,12 +33,6 @@ impl Default for QueryTaskStatus { } } -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TableQuery { - pub table: String, - pub with_constraints: bool, -} - #[derive(Debug, Clone)] pub struct QueryTask { pub conn: InitiatedConnection, @@ -47,7 +41,7 @@ pub struct QueryTask { pub status: QueryTaskStatus, pub tab_idx: usize, pub query_idx: usize, - pub table: Option, + pub table: Option, } impl QueryTask { @@ -57,7 +51,7 @@ impl QueryTask { query_id: String, tab_idx: usize, query_idx: usize, - table: Option, + table: Option, ) -> Self { QueryTask { conn, @@ -104,10 +98,10 @@ pub async fn async_process_model( match task.conn.execute_query(&task.query).await { Ok(mut result_set) => { if let Some(table) = task.table { - let constraints = task.conn.get_constraints(&table.table).await?; - let columns = task.conn.get_columns(Some(&table.table)).await?; + let constraints = task.conn.get_constraints(&table).await?; + let columns = task.conn.get_columns(Some(&table)).await?; result_set.table = TableMetadata { - table: table.table, + table, constraints: Some(constraints), columns: Some(columns), } diff --git a/src-tauri/src/utils/error.rs b/src-tauri/src/utils/error.rs index 870d201..04c10f6 100644 --- a/src-tauri/src/utils/error.rs +++ b/src-tauri/src/utils/error.rs @@ -30,7 +30,7 @@ pub enum Error { General(#[from] anyhow::Error), #[error("Query results expired, please re-run the query.")] QueryExpired, - #[error("Transaction failed: {0}")] + #[error("{0}")] TxError(String), } diff --git a/src/components/Screens/Console/Content/QueryTab/Results.tsx b/src/components/Screens/Console/Content/QueryTab/Results.tsx index 8475fe4..77d5f45 100644 --- a/src/components/Screens/Console/Content/QueryTab/Results.tsx +++ b/src/components/Screens/Console/Content/QueryTab/Results.tsx @@ -9,7 +9,7 @@ import AgGridSolid, { AgGridSolidRef } from 'ag-grid-solid'; import { useContextMenu, Menu, animation, Item } from 'solid-contextmenu'; import { useAppSelector } from 'services/Context'; -import { QueryTaskEnqueueResult, Row } from 'interfaces'; +import { Row } from 'interfaces'; import { ContentTabData } from 'services/Connections'; import { Pagination } from './components/Pagination'; import { NoResults } from './components/NoResults'; @@ -20,6 +20,7 @@ import { getAnyCase, parseObjRecursive } from 'utils/utils'; import { save } from '@tauri-apps/api/dialog'; import { Key } from 'components/UI/Icons'; import { invoke } from '@tauri-apps/api'; +import { update } from 'sql-bricks'; const getColumnDefs = (rows: Row[], columns: Row[], constraints: Row[]): ColDef[] => { if (!rows || rows.length === 0) { @@ -27,17 +28,17 @@ const getColumnDefs = (rows: Row[], columns: Row[], constraints: Row[]): ColDef[ } return Object.keys(rows[0]).map((field, _i) => { const key = constraints.find((c) => { - const t = getAnyCase(c, 'COLUMN_NAME'); + const t = getAnyCase(c, 'column_name'); return t === field; }); const col = columns.find((c) => { - const t = getAnyCase(c, 'COLUMN_NAME'); + const t = getAnyCase(c, 'column_name'); if (t === field) { return true; } }) ?? {}; - const visible_type = getAnyCase(col, 'COLUMN_TYPE'); + const visible_type = getAnyCase(col, 'column_type'); // const type = visible_type?.split('(')[0]; // let gridType = 'text'; // if (type.includes('int') || type === 'decimal' || type === 'float' || type === 'double') { @@ -51,7 +52,7 @@ const getColumnDefs = (rows: Row[], columns: Row[], constraints: Row[]): ColDef[ // } return { - // editable: !key, + editable: !key, // checkboxSelection: _i === 0, filter: true, // type: gridType, @@ -86,7 +87,7 @@ type Changes = { export const Results = (props: { editable?: boolean; table?: string }) => { const { connections: { queryIdx, contentStore, getConnection, updateContentTab }, - backend: { getQueryResults, pageSize, downloadCsv }, + backend: { getQueryResults, pageSize, downloadCsv, selectAllFrom }, messages: { notify }, } = useAppSelector(); const [code, setCode] = createSignal(''); @@ -180,35 +181,21 @@ export const Results = (props: { editable?: boolean; table?: string }) => { const applyChanges = async () => { const allChanges = changes(); try { + const conn = getConnection(); + const t = table(); const queries = Object.keys(allChanges).map((rowIndex) => { - let statement = `UPDATE ${table()} SET `; const row = allChanges[rowIndex]; - const params = Object.values(row.changes) - .reduce((acc, val) => [...acc, val], [] as string[]) - .concat(row.updateVal) - .map(String); - const _changes = Object.keys(row.changes).map((key) => key + ' = ?'); - statement += _changes.join(', ') + ` WHERE ${row.updateKey} = ?`; - return { statement, params }; + return update(t, row.changes) + .where({ [row.updateKey]: row.updateVal }) + .toString(); }); - const conn = getConnection(); await invoke('execute_tx', { queries, connId: conn.id }); - setChanges({}); await invoke('invalidate_query', { path: data()?.path }); - const query = 'SELECT * from ' + props.table!; - const { result_sets } = await invoke('enqueue_query', { - connId: conn.id, - sql: query, - autoLimit: true, - tabIdx: contentStore.idx, - table: { - table, - with_constraints: true, - }, - }); + const results_sets = await selectAllFrom(props.table!, conn.id, contentStore.idx); updateContentTab('data', { - result_sets: result_sets.map((id) => ({ id })), + result_sets: results_sets.map((id) => ({ id })), }); + setChanges({}); } catch (error) { notify(error); } @@ -280,8 +267,8 @@ export const Results = (props: { editable?: boolean; table?: string }) => { suppressCsvExport={false} onCellEditingStopped={(e) => { if (e.valueChanged) { - const updateCol = constraints().find((c) => +getAnyCase(c, 'ORDINAL_POSITION') === 1); - const updateKey = updateCol ? getAnyCase(updateCol, 'COLUMN_NAME') : Object.keys(e.data)[0]; + const updateCol = constraints().find((c) => +getAnyCase(c, 'ordinal_position') === 1); + const updateKey = updateCol ? getAnyCase(updateCol, 'column_name') : Object.keys(e.data)[0]; const change = e.column.getColId(); const _changes = changes(); setChanges({ diff --git a/src/components/Screens/Console/Content/QueryTab/components/Pagination.tsx b/src/components/Screens/Console/Content/QueryTab/components/Pagination.tsx index 78a6051..eae10a2 100644 --- a/src/components/Screens/Console/Content/QueryTab/components/Pagination.tsx +++ b/src/components/Screens/Console/Content/QueryTab/components/Pagination.tsx @@ -71,17 +71,20 @@ export const Pagination = (props: PaginationProps) => {
0}> -
- -
- +