diff --git a/package-lock.json b/package-lock.json index a8340fb..d1194b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20443,8 +20443,7 @@ "@emotion/use-insertion-effect-with-fallbacks": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.0.tgz", - "integrity": "sha512-1eEgUGmkaljiBnRMTdksDV1W4kUnmwgp7X9G8B++9GYwl1lUdqSndSriIrTJ0N7LQaoauY9JJ2yhiOYK5+NI4A==", - "requires": {} + "integrity": "sha512-1eEgUGmkaljiBnRMTdksDV1W4kUnmwgp7X9G8B++9GYwl1lUdqSndSriIrTJ0N7LQaoauY9JJ2yhiOYK5+NI4A==" }, "@emotion/utils": { "version": "1.2.0", @@ -21199,57 +21198,49 @@ "version": "6.5.1", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-6.5.1.tgz", "integrity": "sha512-9PYGcXrAxitycIjRmZB+Q0JaN07GZIWaTBIGQzfaZv+qr1n8X1XUEJ5rZ/vx6OVD9RRYlrNnXWExQXcmZeD/BQ==", - "dev": true, - "requires": {} + "dev": true }, "@svgr/babel-plugin-remove-jsx-attribute": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-7.0.0.tgz", "integrity": "sha512-iiZaIvb3H/c7d3TH2HBeK91uI2rMhZNwnsIrvd7ZwGLkFw6mmunOCoVnjdYua662MqGFxlN9xTq4fv9hgR4VXQ==", - "dev": true, - "requires": {} + "dev": true }, "@svgr/babel-plugin-remove-jsx-empty-expression": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-7.0.0.tgz", "integrity": "sha512-sQQmyo+qegBx8DfFc04PFmIO1FP1MHI1/QEpzcIcclo5OAISsOJPW76ZIs0bDyO/DBSJEa/tDa1W26pVtt0FRw==", - "dev": true, - "requires": {} + "dev": true }, "@svgr/babel-plugin-replace-jsx-attribute-value": { "version": "6.5.1", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-6.5.1.tgz", "integrity": "sha512-8DPaVVE3fd5JKuIC29dqyMB54sA6mfgki2H2+swh+zNJoynC8pMPzOkidqHOSc6Wj032fhl8Z0TVn1GiPpAiJg==", - "dev": true, - "requires": {} + "dev": true }, "@svgr/babel-plugin-svg-dynamic-title": { "version": "6.5.1", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-6.5.1.tgz", "integrity": "sha512-FwOEi0Il72iAzlkaHrlemVurgSQRDFbk0OC8dSvD5fSBPHltNh7JtLsxmZUhjYBZo2PpcU/RJvvi6Q0l7O7ogw==", - "dev": true, - "requires": {} + "dev": true }, "@svgr/babel-plugin-svg-em-dimensions": { "version": "6.5.1", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-6.5.1.tgz", "integrity": "sha512-gWGsiwjb4tw+ITOJ86ndY/DZZ6cuXMNE/SjcDRg+HLuCmwpcjOktwRF9WgAiycTqJD/QXqL2f8IzE2Rzh7aVXA==", - "dev": true, - "requires": {} + "dev": true }, "@svgr/babel-plugin-transform-react-native-svg": { "version": "6.5.1", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-6.5.1.tgz", "integrity": "sha512-2jT3nTayyYP7kI6aGutkyfJ7UMGtuguD72OjeGLwVNyfPRBD8zQthlvL+fAbAKk5n9ZNcvFkp/b1lZ7VsYqVJg==", - "dev": true, - "requires": {} + "dev": true }, "@svgr/babel-plugin-transform-svg-component": { "version": "6.5.1", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-6.5.1.tgz", "integrity": "sha512-a1p6LF5Jt33O3rZoVRBqdxL350oge54iZWHNI6LJB5tQ7EelvD/Mb1mfBiZNAan0dt4i3VArkFRjA4iObuNykQ==", - "dev": true, - "requires": {} + "dev": true }, "@svgr/babel-preset": { "version": "6.5.1", @@ -22377,22 +22368,19 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.0.1.tgz", "integrity": "sha512-njsdJXJSiS2iNbQVS0eT8A/KPnmyH4pv1APj2K0d1wrZcBLw+yppxOy4CGqa0OxDJkzfL/XELDhD8rocnIwB5A==", - "dev": true, - "requires": {} + "dev": true }, "@webpack-cli/info": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.1.tgz", "integrity": "sha512-fE1UEWTwsAxRhrJNikE7v4EotYflkEhBL7EbajfkPlf6E37/2QshOy/D48Mw8G5XMFlQtS6YV42vtbG9zBpIQA==", - "dev": true, - "requires": {} + "dev": true }, "@webpack-cli/serve": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.2.tgz", "integrity": "sha512-S9h3GmOmzUseyeFW3tYNnWS7gNUuwxZ3mmMq0JyW78Vx1SGKPSkt5bT4pB0rUnVfHjP0EL9gW2bOzmtiTfQt0A==", - "dev": true, - "requires": {} + "dev": true }, "@xtuc/ieee754": { "version": "1.2.0", @@ -22454,15 +22442,13 @@ "version": "1.8.0", "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", - "dev": true, - "requires": {} + "dev": true }, "acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "requires": {} + "dev": true }, "acorn-walk": { "version": "8.2.0", @@ -22556,8 +22542,7 @@ "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "requires": {} + "dev": true }, "ansi-escapes": { "version": "4.3.2", @@ -23200,8 +23185,7 @@ "version": "0.0.3", "resolved": "https://registry.npmjs.org/browserslist-config-erb/-/browserslist-config-erb-0.0.3.tgz", "integrity": "sha512-y47DryCY92lxkKyRVMlaZvXAolIY7U33q9e4CS0MdWeJkoAht7OzsrkfdZFCBOP3H5q1EVUxS0L7VVsKM6gZCQ==", - "dev": true, - "requires": {} + "dev": true }, "bs-logger": { "version": "0.2.6", @@ -24005,8 +23989,7 @@ "version": "6.4.0", "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.4.0.tgz", "integrity": "sha512-jDfsatwWMWN0MODAFuHszfjphEXfNw9JUAhmY4pLu3TyTU+ohUpsbVtbU+1MZn4a47D9kqh03i4eyOm+74+zew==", - "dev": true, - "requires": {} + "dev": true }, "css-loader": { "version": "6.7.3", @@ -24186,8 +24169,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-3.1.0.tgz", "integrity": "sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==", - "dev": true, - "requires": {} + "dev": true }, "csso": { "version": "4.2.0", @@ -25391,8 +25373,7 @@ "version": "8.8.0", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz", "integrity": "sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==", - "dev": true, - "requires": {} + "dev": true }, "eslint-import-resolver-node": { "version": "0.3.7", @@ -25671,8 +25652,7 @@ "version": "6.1.1", "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.1.1.tgz", "integrity": "sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==", - "dev": true, - "requires": {} + "dev": true }, "eslint-plugin-react": { "version": "7.32.2", @@ -25729,8 +25709,7 @@ "version": "4.6.0", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", - "dev": true, - "requires": {} + "dev": true }, "eslint-scope": { "version": "5.1.1", @@ -26884,8 +26863,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "dev": true, - "requires": {} + "dev": true }, "identity-obj-proxy": { "version": "3.0.0", @@ -27835,8 +27813,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "dev": true, - "requires": {} + "dev": true }, "jest-regex-util": { "version": "29.4.3", @@ -29554,29 +29531,25 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz", "integrity": "sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==", - "dev": true, - "requires": {} + "dev": true }, "postcss-discard-duplicates": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz", "integrity": "sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==", - "dev": true, - "requires": {} + "dev": true }, "postcss-discard-empty": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz", "integrity": "sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==", - "dev": true, - "requires": {} + "dev": true }, "postcss-discard-overridden": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz", "integrity": "sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==", - "dev": true, - "requires": {} + "dev": true }, "postcss-merge-longhand": { "version": "5.1.7", @@ -29644,8 +29617,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", - "dev": true, - "requires": {} + "dev": true }, "postcss-modules-local-by-default": { "version": "4.0.0", @@ -29680,8 +29652,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz", "integrity": "sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==", - "dev": true, - "requires": {} + "dev": true }, "postcss-normalize-display-values": { "version": "5.1.0", @@ -31157,8 +31128,7 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.3.tgz", "integrity": "sha512-53BiGLXAcll9maCYtZi2RCQZKa8NQQai5C4horqKyRmHj9H7QmcUyucrH+4KW/gBQbXM2AsB0axoEcFZPlfPcw==", - "dev": true, - "requires": {} + "dev": true }, "style-mod": { "version": "4.0.3", @@ -32030,8 +32000,7 @@ "version": "7.5.9", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", - "dev": true, - "requires": {} + "dev": true } } }, @@ -32426,8 +32395,7 @@ "version": "8.13.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", - "dev": true, - "requires": {} + "dev": true }, "xlsx": { "version": "0.18.5", diff --git a/src/libs/GenerateSqlFromChanges.test.ts b/src/libs/GenerateSqlFromChanges.test.ts index 8c5fe10..98c1fc8 100644 --- a/src/libs/GenerateSqlFromChanges.test.ts +++ b/src/libs/GenerateSqlFromChanges.test.ts @@ -52,8 +52,8 @@ test('Generate Sql from changes with primary key', () => { }; const changes = new ResultChangeCollector(); - changes.add(1, 1, 'Jenny 2'); - changes.add(2, 2, 35); + changes.addChange(1, 1, 'Jenny 2'); + changes.addChange(2, 2, 35); expect(generateSqlFromChanges(schema, data, changes.getChanges())).toEqual([ { diff --git a/src/libs/GenerateSqlFromChanges.ts b/src/libs/GenerateSqlFromChanges.ts index 559d0bd..20c4569 100644 --- a/src/libs/GenerateSqlFromChanges.ts +++ b/src/libs/GenerateSqlFromChanges.ts @@ -1,6 +1,9 @@ import { QueryResult, QueryResultHeader } from 'types/SqlResult'; import { DatabaseSchema } from 'types/SqlSchema'; -import { ResultChangeCollectorItem } from './ResultChangeCollector'; +import { + ResultChangeCollectorItem, + ResultChanges, +} from './ResultChangeCollector'; import { SqlStatementPlan } from 'types/SqlStatement'; type UpdatableTableDict = Record< @@ -64,7 +67,64 @@ export function getUpdatableTable( return result; } -function getSqlPlanFromChange( +function buildWhere( + tableName: string, + rowIndex: number, + data: QueryResult, + updatable: UpdatableTableDict +): Record { + const rows = data.rows; + const headers = data.headers; + + return updatable[tableName].reduce((a, b) => { + return { + ...a, + [b.columnNames]: rows[rowIndex][headers[b.columnNumber].name], + }; + }, {}); +} + +function buildRemovePlan( + removeIndex: number, + data: QueryResult, + updatable: UpdatableTableDict +): SqlStatementPlan[] { + // We will not remove if there is more than one table inside the result + const entries = Object.entries(updatable); + if (entries.length === 0) return []; + if (entries.length > 1) return []; + + const [tableName] = entries[0]; + return [ + { + type: 'delete', + table: tableName, + where: buildWhere(tableName, removeIndex, data, updatable), + }, + ]; +} + +function buildInsertPlan( + changes: ResultChangeCollectorItem, + headers: QueryResultHeader[] +): SqlStatementPlan[] { + const values: Record = {}; + + const uniqueTable = new Set( + headers + .filter((header) => header.schema) + .map((header) => header.schema?.table) + ); + if (uniqueTable.size > 1) return []; + + for (const { col, value } of changes.cols) { + values[headers[col].name] = value; + } + + return [{ type: 'insert', table: Array.from(uniqueTable)[0] ?? '', values }]; +} + +function buildUpdatePlan( change: ResultChangeCollectorItem, data: QueryResult, updatable: UpdatableTableDict @@ -89,13 +149,7 @@ function getSqlPlanFromChange( values: { [header.name]: col.value, }, - where: updatable[tableName].reduce((a, b) => { - return { - ...a, - [b.columnNames]: - data.rows[change.row][headers[b.columnNumber].name], - }; - }, {}), + where: buildWhere(tableName, change.row, data, updatable), }; } } @@ -108,18 +162,30 @@ function getSqlPlanFromChange( export default function generateSqlFromChanges( schema: DatabaseSchema, currentData: QueryResult, - changes: ResultChangeCollectorItem[] + changes: ResultChanges ): SqlStatementPlan[] { const updatableTables = getUpdatableTable(currentData.headers, schema); // Prepare the statement plans let plans: SqlStatementPlan[] = []; - for (const change of changes) { + + for (const change of changes.changes) { plans = [ ...plans, - ...getSqlPlanFromChange(change, currentData, updatableTables), + ...buildUpdatePlan(change, currentData, updatableTables), ]; } + for (const removeIndex of changes.remove) { + plans = [ + ...plans, + ...buildRemovePlan(removeIndex, currentData, updatableTables), + ]; + } + + for (const change of changes.new) { + plans = [...plans, ...buildInsertPlan(change, currentData.headers)]; + } + return plans; } diff --git a/src/libs/GenerateSqlFromPlan.ts b/src/libs/GenerateSqlFromPlan.ts index be54ea5..61ffee0 100644 --- a/src/libs/GenerateSqlFromPlan.ts +++ b/src/libs/GenerateSqlFromPlan.ts @@ -1,5 +1,5 @@ import { SqlStatementPlan } from 'types/SqlStatement'; -import { QueryBuilder } from './QueryBuilder'; +import { QueryBuilder, qb } from './QueryBuilder'; function convertUnsupportedValue(value: Record) { const result = { ...value }; @@ -16,13 +16,26 @@ function convertUnsupportedValue(value: Record) { export default function generateSqlFromPlan(plan: SqlStatementPlan) { if (plan.type === 'update') { - console.log(convertUnsupportedValue(plan.values), plan.values); - - const qb = new QueryBuilder('mysql') - .table(plan.table) - .update(convertUnsupportedValue(plan.values)); - if (plan.where) qb.where(convertUnsupportedValue(plan.where)); - return qb.toRawSQL(); + if (plan.values) { + const qb = new QueryBuilder('mysql') + .table(plan.table) + .update(convertUnsupportedValue(plan.values)); + if (plan.where) qb.where(convertUnsupportedValue(plan.where)); + return qb.toRawSQL(); + } + } else if (plan.type === 'delete') { + if (plan.where) { + return qb('mysql') + .table(plan.table) + .where(plan.where) + .delete() + .toRawSQL(); + } + } else if (plan.type === 'insert') { + if (plan.values) { + return qb('mysql').table(plan.table).insert(plan.values).toRawSQL(); + } } + return ''; } diff --git a/src/libs/QueryBuilder.test.ts b/src/libs/QueryBuilder.test.ts index 21f42ae..8779824 100644 --- a/src/libs/QueryBuilder.test.ts +++ b/src/libs/QueryBuilder.test.ts @@ -37,3 +37,10 @@ test('Delete table record with where', () => { 'DELETE FROM `users` WHERE `id`=5;' ); }); + +test('Insert table', () => { + const qb = new QueryBuilder('mysql'); + expect( + qb.table('users').insert({ username: 'visal', age: 5 }).toRawSQL() + ).toBe("INSERT INTO `users`(`username`, `age`) VALUES('visal', 5);"); +}); diff --git a/src/libs/QueryBuilder.ts b/src/libs/QueryBuilder.ts index f6febd1..641d78b 100644 --- a/src/libs/QueryBuilder.ts +++ b/src/libs/QueryBuilder.ts @@ -19,7 +19,7 @@ interface QueryWhere { interface QueryStates { table?: string; type: 'select' | 'update' | 'insert' | 'delete'; - insert?: Record[]; + insert?: Record; update?: Record; where: QueryWhere[]; select: string[]; @@ -65,7 +65,8 @@ export class QueryBuilder { } insert(value: Record) { - this.states.insert?.push({ ...value }); + this.states.type = 'insert'; + this.states.insert = { ...this.states.insert, ...value }; return this; } @@ -181,7 +182,11 @@ export class QueryBuilder { const { sql: whereSql, binding: whereBinding } = this.buildWhere( this.states.where ); + binding = binding.concat(...whereBinding); + if (this.states.limit) { + binding.push(this.states.limit); + } const sql = [ @@ -190,7 +195,7 @@ export class QueryBuilder { 'FROM', this.dialect.escapeIdentifier(this.states.table), whereSql ? 'WHERE ' + whereSql : whereSql, - this.states.limit ? `LIMIT ${this.states.limit}` : null, + this.states.limit ? `LIMIT ?` : null, ] .filter(Boolean) .join(' ') + ';'; @@ -218,6 +223,32 @@ export class QueryBuilder { .join(' ') + ';'; return { sql, binding }; + } else if (this.states.type === 'insert') { + if (!this.states.table) throw 'no table specified'; + + if (this.states.insert) { + const binding: unknown[] = []; + const fields: string[] = []; + + for (const [field, value] of Object.entries(this.states.insert)) { + binding.push(value); + fields.push(field); + } + + const sql = + [ + 'INSERT INTO', + this.dialect.escapeIdentifier(this.states.table) + + '(' + + fields + .map((field) => this.dialect.escapeIdentifier(field)) + .join(', ') + + ')', + 'VALUES(' + new Array(binding.length).fill('?').join(', ') + ')', + ].join(' ') + ';'; + + return { sql, binding }; + } } throw 'not implemented'; diff --git a/src/libs/ResultChangeCollector.test.ts b/src/libs/ResultChangeCollector.test.ts index 2e15dac..eb3f7ae 100644 --- a/src/libs/ResultChangeCollector.test.ts +++ b/src/libs/ResultChangeCollector.test.ts @@ -2,28 +2,73 @@ import ResultChangeCollector, { ResultChangeCollectorItem, } from './ResultChangeCollector'; -test('Test ResultChangeCollector', () => { - const collection = new ResultChangeCollector(); - collection.add(1, 3, 100); - collection.add(1, 3, 200); - collection.add(1, 2, 'query master'); - collection.add(2, 1, 300); - - expect(collection.getChangesCount()).toBe(2); - expect(collection.getChange(1, 3)).toBe(200); - expect(collection.getChange(1, 10)).toBeUndefined(); - - let changes = collection.getChanges(); - - expect(changes.length).toBe(2); - expect(getChangeCellCount(changes, 1)).toBe(2); - expect(getChangeCellValue(changes, 1, 3)).toBe(200); - expect(getChangeCellValue(changes, 1, 2)).toBe('query master'); - expect(getChangeCellValue(changes, 2, 1)).toBe(300); - - collection.remove(2, 1); - changes = collection.getChanges(); - expect(changes.length).toBe(1); +describe('ResultChangeCollector', () => { + test('Add change and remove change', () => { + const collection = new ResultChangeCollector(); + collection.addChange(1, 3, 100); + collection.addChange(1, 3, 200); + collection.addChange(1, 2, 'query master'); + collection.addChange(2, 1, 300); + + expect(collection.getChangesCount()).toBe(2); + expect(collection.getChange(1, 3)).toBe(200); + expect(collection.getChange(1, 10)).toBeUndefined(); + + let { changes } = collection.getChanges(); + + expect(changes.length).toBe(2); + expect(getChangeCellCount(changes, 1)).toBe(2); + expect(getChangeCellValue(changes, 1, 3)).toBe(200); + expect(getChangeCellValue(changes, 1, 2)).toBe('query master'); + expect(getChangeCellValue(changes, 2, 1)).toBe(300); + + collection.removeChange(2, 1); + + changes = collection.getChanges().changes; + expect(changes.length).toBe(1); + }); + + test('Remove rows', () => { + const collection = new ResultChangeCollector(); + collection.addChange(1, 2, 100); + collection.addChange(2, 2, 200); + collection.removeRow(2); + + expect(collection.getChangesCount()).toBe(2); + expect(collection.getRemovedRowsIndex()).toEqual([2]); + const changes = collection.getChanges(); + expect(changes.changes.length).toBe(1); + expect(getChangeCellValue(changes.changes, 1, 2)).toBe(100); + expect(getChangeCellValue(changes.changes, 2, 2)).toBe(undefined); + + collection.discardRemoveRow(2); + expect(collection.getChangesCount()).toBe(2); + expect(collection.getRemovedRowsIndex()).toEqual([]); + const changes2 = collection.getChanges(); + expect(changes2.changes.length).toBe(2); + expect(getChangeCellValue(changes2.changes, 2, 2)).toBe(200); + }); + + test('Insert new rows', () => { + const collection = new ResultChangeCollector(); + collection.addChange(1, 2, 100); + collection.addChange(2, 2, 200); + + collection.createNewRow(); + collection.createNewRow(); + + expect(collection.getNewRowCount()).toBe(2); + expect(collection.getChangesCount()).toBe(4); + expect(collection.getChanges().changes.length).toBe(2); + + collection.addChange(-1, 2, 50); + expect(collection.getChangesCount()).toBe(4); + const changes = collection.getChanges(); + expect(changes.new.length).toBe(2); + expect(getChangeCellValue(changes.changes, -1, 2)).toBe(undefined); + expect(getChangeCellValue(changes.new, -1, 2)).toBe(50); + expect(getChangeCellValue(changes.changes, 2, 2)).toBe(200); + }); }); function getChangeCellCount(changes: ResultChangeCollectorItem[], row: number) { diff --git a/src/libs/ResultChangeCollector.ts b/src/libs/ResultChangeCollector.ts index 38ae150..fe57e40 100644 --- a/src/libs/ResultChangeCollector.ts +++ b/src/libs/ResultChangeCollector.ts @@ -5,10 +5,18 @@ export interface ResultChangeCollectorItem { export type ResultChangeEventHandler = (count: number) => void; +export interface ResultChanges { + new: ResultChangeCollectorItem[]; + changes: ResultChangeCollectorItem[]; + remove: number[]; +} + /** * Collect all the changes and arrange it in the friendly way */ export default class ResultChangeCollector { + protected newRowCount = 0; + protected removedRowIndex = new Set(); protected changes: Record> = {}; protected onChangeListeners: ResultChangeEventHandler[] = []; @@ -21,13 +29,28 @@ export default class ResultChangeCollector { } protected triggerOnChange() { + const count = this.getChangesCount(); for (const cb of this.onChangeListeners) { - const count = this.getChangesCount(); cb(count); } } - remove(rowNumber: number, cellNumber: number) { + removeRow(rowNumber: number) { + if (rowNumber < 0) { + // Remove the new created row. + } else { + // Remove the existing row. We just mark it as removed + this.removedRowIndex.add(rowNumber); + } + this.triggerOnChange(); + } + + discardRemoveRow(rowNumber: number) { + this.removedRowIndex.delete(rowNumber); + this.triggerOnChange(); + } + + removeChange(rowNumber: number, cellNumber: number) { if (this.changes[rowNumber]) { delete this.changes[rowNumber][cellNumber]; if (Object.entries(this.changes[rowNumber]).length === 0) { @@ -38,7 +61,12 @@ export default class ResultChangeCollector { this.triggerOnChange(); } - add(rowNumber: number, cellNumber: number, value: unknown) { + createNewRow() { + this.newRowCount++; + this.triggerOnChange(); + } + + addChange(rowNumber: number, cellNumber: number, value: unknown) { if (!this.changes[rowNumber]) { this.changes[rowNumber] = {}; } @@ -49,6 +77,8 @@ export default class ResultChangeCollector { clear() { this.changes = {}; + this.removedRowIndex.clear(); + this.newRowCount = 0; this.triggerOnChange(); } @@ -66,20 +96,63 @@ export default class ResultChangeCollector { } getChangesCount() { - return Object.entries(this.changes).length; + const changes = this.getChanges(); + return changes.changes.length + changes.new.length + changes.remove.length; } - getChanges(): ResultChangeCollectorItem[] { - return Object.entries(this.changes).map(([rowNumber, columnList]) => { - return { - row: Number(rowNumber), - cols: Object.entries(columnList).map(([colNumber, value]) => { - return { - col: Number(colNumber), - value, - }; - }), - }; - }); + getNewRowCount(): number { + return this.newRowCount; + } + + getRemovedRowsIndex(): number[] { + return Array.from(this.removedRowIndex); + } + + /** + * Describe all changes including updating cells, + * removing rows, and adding new rows + * @returns + */ + getChanges(): ResultChanges { + const changes = Object.entries(this.changes) + .filter(([rowNumber]) => { + // We filter out the changes in new rows and removed rows + const rowIndexNumber = Number(rowNumber); + return rowIndexNumber >= 0 && !this.removedRowIndex.has(rowIndexNumber); + }) + .map(([rowNumber, columnList]) => { + return { + row: Number(rowNumber), + cols: Object.entries(columnList).map(([colNumber, value]) => { + return { + col: Number(colNumber), + value, + }; + }), + }; + }); + + const newRowChanges = new Array(this.newRowCount) + .fill(undefined) + .map((_, newRowIndex) => { + const realNewRowindex = -(newRowIndex + 1); + const newRowChanged = this.changes[realNewRowindex]; + const colChanges = newRowChanged + ? Object.entries(newRowChanged).map(([colNumber, value]) => { + return { col: Number(colNumber), value }; + }) + : []; + + return { + row: realNewRowindex, + cols: colChanges, + }; + }); + + return { + changes, + new: newRowChanges, + remove: Array.from(this.removedRowIndex), + }; } } diff --git a/src/renderer/App.css b/src/renderer/App.css index 25d9950..f7935ac 100644 --- a/src/renderer/App.css +++ b/src/renderer/App.css @@ -42,6 +42,8 @@ body { --color-table-cell-code: #95a5a6; --color-table-row-selected: #e4e6f1; + --color-table-row-removed: #ff6b81; + --color-table-row-new: #7bed9f; /* For Scroll */ --color-scrollbar: #0001; @@ -91,6 +93,8 @@ body.dark { --color-table-cell-code: #95a5a6; --color-table-row-selected: #37373d; + --color-table-row-removed: #ff4757; + --color-table-row-new: #27ae60; /* For Scroll */ --color-scrollbar-track: #fff1; diff --git a/src/renderer/components/OptimizeTable/TableHeaderResizeHandler.tsx b/src/renderer/components/OptimizeTable/TableHeaderResizeHandler.tsx new file mode 100644 index 0000000..6e8c1f4 --- /dev/null +++ b/src/renderer/components/OptimizeTable/TableHeaderResizeHandler.tsx @@ -0,0 +1,97 @@ +import { useRef, useState, useEffect } from 'react'; +import styles from './styles.module.css'; + +export default function TableHeaderResizeHandler({ + idx, + onResize, +}: { + idx: number; + onResize: (idx: number, newSize: number) => void; +}) { + const handlerRef = useRef(null); + const [resizing, setResizing] = useState(false); + + useEffect(() => { + if (handlerRef.current && resizing) { + const table = handlerRef.current?.parentNode?.parentNode?.parentNode + ?.parentNode as HTMLTableElement; + + const tableWrapper = table.parentNode?.parentNode as HTMLDivElement; + + let lastX = -100; + + if (table && tableWrapper) { + const onMouseMove = (e: MouseEvent) => + requestAnimationFrame(() => { + if (lastX < 0) { + lastX = e.clientX; + return; + } + + const edgeDirection = + tableWrapper.getBoundingClientRect().right - e.clientX > 0 + ? 1 + : -1; + const edgeResizing = + Math.abs(tableWrapper.getBoundingClientRect().right - e.clientX) < + 3; + + let gain = 0; + if (edgeResizing) { + gain = edgeDirection * 2; + } else { + gain = e.clientX - lastX; + } + + const cell = handlerRef.current?.parentNode as HTMLTableCellElement; + const cellWidth = cell.getBoundingClientRect().width; + + let width = cellWidth; + if (cellWidth + gain >= 100) { + width = cellWidth + gain; + } else { + width = 100; + gain = 0; + } + + lastX = e.clientX; + if (edgeResizing) { + tableWrapper.scrollLeft += gain; + } + + onResize(idx, width); + + if (table) { + const columns = table.style.gridTemplateColumns.split(' '); + columns[idx] = width + 'px'; + table.style.gridTemplateColumns = columns.join(' '); + } + + if (edgeResizing) { + tableWrapper.scrollLeft += gain; + } + }); + + const onMouseUp = () => { + setResizing(false); + }; + + document.addEventListener('mousemove', onMouseMove); + document.addEventListener('mouseup', onMouseUp); + + return () => { + document.removeEventListener('mousemove', onMouseMove); + document.removeEventListener('mouseup', onMouseUp); + }; + } + } + }, [handlerRef, idx, resizing, setResizing, onResize]); + + return ( +
setResizing(true)} + >
+ ); +} diff --git a/src/renderer/components/OptimizeTable/index.tsx b/src/renderer/components/OptimizeTable/index.tsx index a3266da..eadcfaf 100644 --- a/src/renderer/components/OptimizeTable/index.tsx +++ b/src/renderer/components/OptimizeTable/index.tsx @@ -7,6 +7,7 @@ import React, { useMemo, } from 'react'; import styles from './styles.module.css'; +import TableHeaderResizeHandler from './TableHeaderResizeHandler'; interface OptimizeTableProps { data: unknown[]; @@ -19,103 +20,12 @@ interface OptimizeTableProps { renderCell: (y: number, x: number) => ReactElement; rowHeight: number; renderAhead: number; - selectedRowsIndex: number[]; // Array of selected row indices - onSelectedRowsIndexChanged: (selectedRows: number[]) => void; // Callback for row selection changes -} -function ResizeHandler({ - idx, - onResize, -}: { - idx: number; - onResize: (idx: number, newSize: number) => void; -}) { - const handlerRef = useRef(null); - const [resizing, setResizing] = useState(false); + newRowsIndex?: number[]; + removedRowsIndex?: number[]; - useEffect(() => { - if (handlerRef.current && resizing) { - const table = handlerRef.current?.parentNode?.parentNode?.parentNode - ?.parentNode as HTMLTableElement; - - const tableWrapper = table.parentNode?.parentNode as HTMLDivElement; - - let lastX = -100; - - if (table && tableWrapper) { - const onMouseMove = (e: MouseEvent) => - requestAnimationFrame(() => { - if (lastX < 0) { - lastX = e.clientX; - return; - } - - const edgeDirection = - tableWrapper.getBoundingClientRect().right - e.clientX > 0 - ? 1 - : -1; - const edgeResizing = - Math.abs(tableWrapper.getBoundingClientRect().right - e.clientX) < - 3; - - let gain = 0; - if (edgeResizing) { - gain = edgeDirection * 2; - } else { - gain = e.clientX - lastX; - } - - const cell = handlerRef.current?.parentNode as HTMLTableCellElement; - const cellWidth = cell.getBoundingClientRect().width; - - let width = cellWidth; - if (cellWidth + gain >= 100) { - width = cellWidth + gain; - } else { - width = 100; - gain = 0; - } - - lastX = e.clientX; - if (edgeResizing) { - tableWrapper.scrollLeft += gain; - } - - onResize(idx, width); - - if (table) { - const columns = table.style.gridTemplateColumns.split(' '); - columns[idx] = width + 'px'; - table.style.gridTemplateColumns = columns.join(' '); - } - - if (edgeResizing) { - tableWrapper.scrollLeft += gain; - } - }); - - const onMouseUp = () => { - setResizing(false); - }; - - document.addEventListener('mousemove', onMouseMove); - document.addEventListener('mouseup', onMouseUp); - - return () => { - document.removeEventListener('mousemove', onMouseMove); - document.removeEventListener('mouseup', onMouseUp); - }; - } - } - }, [handlerRef, idx, resizing, setResizing, onResize]); - - return ( -
setResizing(true)} - >
- ); + selectedRowsIndex: number[]; // Array of selected row indices + onSelectedRowsIndexChanged: (selectedRows: number[]) => void; // Callback for row selection changes } export default function OptimizeTable({ @@ -124,6 +34,8 @@ export default function OptimizeTable({ renderCell, rowHeight, renderAhead, + newRowsIndex, + removedRowsIndex, selectedRowsIndex, onSelectedRowsIndexChanged, }: OptimizeTableProps) { @@ -145,6 +57,16 @@ export default function OptimizeTable({ return headers.map((header) => header.initialSize); }); + const newRowsIndexSet = useMemo( + () => new Set(newRowsIndex ?? []), + [newRowsIndex] + ); + + const removedRowsIndexSet = useMemo( + () => new Set(removedRowsIndex ?? []), + [removedRowsIndex] + ); + const recalculateVisible = useCallback( (e: HTMLDivElement) => { const currentRowStart = Math.max( @@ -284,12 +206,22 @@ export default function OptimizeTable({ const cells = windowArray.map((row, rowIndex) => { const absoluteRowIndex = rowIndex + visibleDebounce.rowStart; - const isRowSelected = selectedRowsIndex.includes(absoluteRowIndex); + + let rowClass = undefined; + + if (newRowsIndexSet.has(absoluteRowIndex)) { + rowClass = styles.newRow; + } else if (removedRowsIndexSet.has(absoluteRowIndex)) { + rowClass = styles.removedRow; + } else if (selectedRowsIndex.includes(absoluteRowIndex)) { + rowClass = styles.selectedRow; + } + return ( handleRowClick(e, absoluteRowIndex)} - className={isRowSelected ? styles.selectedRow : undefined} + onMouseDown={(e) => handleRowClick(e, absoluteRowIndex)} + className={rowClass} > {visibleDebounce.colStart > 0 && ( {header.name} {header.resizable && ( - + )} ))} @@ -405,5 +340,7 @@ export default function OptimizeTable({ headers, onHeaderResize, selectedRowsIndex, + newRowsIndexSet, + removedRowsIndexSet, ]); } diff --git a/src/renderer/components/OptimizeTable/styles.module.css b/src/renderer/components/OptimizeTable/styles.module.css index 49ff309..664ee3c 100644 --- a/src/renderer/components/OptimizeTable/styles.module.css +++ b/src/renderer/components/OptimizeTable/styles.module.css @@ -44,6 +44,7 @@ text-align: left; display: flex; flex-direction: row; + z-index: 10; } .tableHeaderIcon { @@ -70,3 +71,12 @@ .selectedRow td { background-color: var(--color-table-row-selected); } + +.newRow td { + background-color: var(--color-table-row-new); + color: var(--color-text); +} + +.removedRow td { + background-color: var(--color-table-row-removed); +} diff --git a/src/renderer/components/ResizableTable/TableCellContent.module.scss b/src/renderer/components/ResizableTable/TableCellContent.module.scss index 8147232..865e343 100644 --- a/src/renderer/components/ResizableTable/TableCellContent.module.scss +++ b/src/renderer/components/ResizableTable/TableCellContent.module.scss @@ -37,7 +37,8 @@ } .null { - color: #27ae60; + color: var(--color-text); + opacity: 0.5; } .right { @@ -47,4 +48,4 @@ .number { text-align: right; color: var(--color-table-cell-number); -} \ No newline at end of file +} diff --git a/src/renderer/contexts/QueryResultChangeProvider.tsx b/src/renderer/contexts/QueryResultChangeProvider.tsx index d8fd00b..a4dae00 100644 --- a/src/renderer/contexts/QueryResultChangeProvider.tsx +++ b/src/renderer/contexts/QueryResultChangeProvider.tsx @@ -34,14 +34,14 @@ export function QueryResultChangeProvider({ children }: PropsWithChildren) { const setChange = useCallback( (row: number, col: number, value: unknown) => { - collector.add(row, col, value); + collector.addChange(row, col, value); }, [collector] ); const removeChange = useCallback( (row: number, col: number) => { - collector.remove(row, col); + collector.removeChange(row, col); }, [collector] ); diff --git a/src/renderer/screens/DatabaseScreen/QueryMultipleResultViewer.tsx b/src/renderer/screens/DatabaseScreen/QueryMultipleResultViewer.tsx index f32a087..5952410 100644 --- a/src/renderer/screens/DatabaseScreen/QueryMultipleResultViewer.tsx +++ b/src/renderer/screens/DatabaseScreen/QueryMultipleResultViewer.tsx @@ -17,7 +17,12 @@ export default memo(function QueryMultipleResultViewer({ if (queryResultOnly.length === 0) return
; if (queryResultOnly.length === 1) - return ; + return ( + + ); return ( setSelected(tab.key)} tabs={queryResultOnly.map((result, idx) => { return { - component: , + component: ( + + ), key: `query_${idx}`, name: `Query ${idx + 1}`, }; diff --git a/src/renderer/screens/DatabaseScreen/QueryResultViewer/QueryResultAction.tsx b/src/renderer/screens/DatabaseScreen/QueryResultViewer/QueryResultAction.tsx index d7ab28c..f68e8fd 100644 --- a/src/renderer/screens/DatabaseScreen/QueryResultViewer/QueryResultAction.tsx +++ b/src/renderer/screens/DatabaseScreen/QueryResultViewer/QueryResultAction.tsx @@ -10,10 +10,12 @@ import { QueryResult } from 'types/SqlResult'; import styles from './styles.module.scss'; import ButtonGroup from 'renderer/components/ButtonGroup'; import ExportModal from '../ExportModal'; +import { useDialog } from 'renderer/contexts/DialogProvider'; interface QueryResultActionProps { result: QueryResult; onResultChange: React.Dispatch>; + onRequestRefetch: () => void; page: number; pageSize: number; onPageChange: React.Dispatch>; @@ -22,10 +24,12 @@ interface QueryResultActionProps { export default function QueryResultAction({ result, onResultChange, + onRequestRefetch, page, pageSize, onPageChange, }: QueryResultActionProps) { + const { showErrorDialog } = useDialog(); const [changeCount, setChangeCount] = useState(0); const [showExportModal, setShowExportModal] = useState(false); const { clearChange, collector } = useQueryResultChange(); @@ -62,16 +66,32 @@ export default function QueryResultAction({ runner .execute(rawSql) .then(() => { - onResultChange((prev) => { - const changes = collector.getChanges(); - clearChange(); - return applyQueryResultChanges(prev, changes); - }); + const changes = collector.getChanges(); + + if (changes.new.length === 0 && changes.remove.length === 0) { + onResultChange((prev) => { + clearChange(); + return applyQueryResultChanges(prev, changes.changes); + }); + } else { + onRequestRefetch(); + } }) - .catch(console.error); + .catch((e) => { + if (e.message) { + showErrorDialog(e.message); + } + }); } } - }, [collector, schema, currentDatabase, clearChange, onResultChange]); + }, [ + collector, + schema, + currentDatabase, + clearChange, + onResultChange, + onRequestRefetch, + ]); return (
diff --git a/src/renderer/screens/DatabaseScreen/QueryResultViewer/QueryResultTable.tsx b/src/renderer/screens/DatabaseScreen/QueryResultViewer/QueryResultTable.tsx index 5e5b1bd..9f1974f 100644 --- a/src/renderer/screens/DatabaseScreen/QueryResultViewer/QueryResultTable.tsx +++ b/src/renderer/screens/DatabaseScreen/QueryResultViewer/QueryResultTable.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import styles from './styles.module.scss'; import TableCell from 'renderer/screens/DatabaseScreen/QueryResultViewer/TableCell/TableCell'; import { QueryResult, QueryResultHeader } from 'types/SqlResult'; @@ -9,6 +9,8 @@ import { useQueryResultChange } from 'renderer/contexts/QueryResultChangeProvide import { useTableCellManager } from './TableCellManager'; import OptimizeTable from 'renderer/components/OptimizeTable'; import Icon from 'renderer/components/Icon'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faPlusCircle, faTimesCircle } from '@fortawesome/free-solid-svg-icons'; interface QueryResultTableProps { result: QueryResult; @@ -17,10 +19,28 @@ interface QueryResultTableProps { } function QueryResultTable({ result, page, pageSize }: QueryResultTableProps) { + const [newRowCount, setNewRowCount] = useState(0); const { collector } = useQueryResultChange(); const { cellManager } = useTableCellManager(); const { schema, currentDatabase } = useSchmea(); + const [selectedRowsIndex, setSelectedRowsIndex] = useState([]); + const [removeRowsIndex, setRemoveRowsIndex] = useState([]); + + const newRowsIndex = useMemo( + () => new Array(newRowCount).fill(0).map((_, idx) => idx), + [newRowCount] + ); + + useEffect(() => { + const onChangeUpdate = () => { + setNewRowCount(collector.getNewRowCount()); + setRemoveRowsIndex(collector.getRemovedRowsIndex()); + }; + + collector.registerChange(onChangeUpdate); + return () => collector.unregisterChange(onChangeUpdate); + }, [collector, setNewRowCount]); const handleSelectedRowsChange = (selectedRows: number[]) => { setSelectedRowsIndex(selectedRows); @@ -30,6 +50,33 @@ function QueryResultTable({ result, page, pageSize }: QueryResultTableProps) { const selectedCell = cellManager.getFocusCell(); return [ + { + text: 'Insert new row', + onClick: () => { + collector.createNewRow(); + }, + icon: , + }, + { + text: 'Remove selected rows', + destructive: true, + disabled: selectedRowsIndex.length === 0, + onClick: () => { + for (const selectedRowIndex of selectedRowsIndex) { + collector.removeRow( + selectedRowIndex - newRowCount < 0 + ? selectedRowIndex - newRowCount + : selectedRowIndex - newRowCount + page * pageSize + ); + } + }, + icon: , + }, + { + text: '', + disabled: true, + separator: true, + }, { text: 'Insert NULL', disabled: !selectedCell, @@ -58,11 +105,12 @@ function QueryResultTable({ result, page, pageSize }: QueryResultTableProps) { separator: true, }, { - text: `Discard Changes`, + text: `Discard All Changes`, destructive: true, disabled: !collector.getChangesCount(), onClick: () => { - const rows = collector.getChanges(); + const rows = collector.getChanges().changes; + for (const row of rows) { for (const col of row.cols) { const cell = cellManager.get(row.row, col.col); @@ -71,14 +119,39 @@ function QueryResultTable({ result, page, pageSize }: QueryResultTableProps) { } } } + + collector.clear(); }, }, ]; - }, [collector]); + }, [collector, newRowCount, selectedRowsIndex, page, pageSize]); + + const data: { data: Record; rowIndex: number }[] = + useMemo(() => { + const newRows = new Array(newRowCount) + .fill(false) + .map((_, newRowIndex) => { + return { + rowIndex: -(newRowIndex + 1), + data: result.headers.reduce( + (prev, header) => ({ ...prev, [header.name]: undefined }), + {} + ), + }; + }); - const data = useMemo(() => { - return result.rows.slice(page * pageSize, (page + 1) * pageSize); - }, [page, pageSize, result.rows]); + return [ + ...newRows, + ...result.rows + .slice(page * pageSize, (page + 1) * pageSize) + .map((value, rowIndex) => { + return { + rowIndex: rowIndex + page * pageSize, + data: value, + }; + }), + ]; + }, [page, pageSize, result, newRowCount]); const updatableTables = useMemo(() => { if (result?.headers && currentDatabase && schema) { @@ -92,10 +165,7 @@ function QueryResultTable({ result, page, pageSize }: QueryResultTableProps) { } const headerMemo = useMemo(() => { - function getInitialSizeByHeaderType( - idx: number, - header: QueryResultHeader - ) { + function getInitialSizeByHeaderType(_: number, header: QueryResultHeader) { if (header.type.type === 'number') { return 100; } else if ( @@ -134,18 +204,24 @@ function QueryResultTable({ result, page, pageSize }: QueryResultTableProps) { (y: number, x: number) => { return ( ); }, - [data, updatableTables, page, pageSize] + [data, result, updatableTables, page, pageSize, newRowCount] ); + const relativeRemoveRowsIndex = useMemo(() => { + return removeRowsIndex.map((removeIndex) => { + return data.findIndex(({ rowIndex }) => rowIndex === removeIndex) ?? 0; + }); + }, [removeRowsIndex, data]); + return (
diff --git a/src/renderer/screens/DatabaseScreen/QueryResultViewer/TableCell/TableCellNumber.tsx b/src/renderer/screens/DatabaseScreen/QueryResultViewer/TableCell/TableCellNumber.tsx index 0ac573b..10a68e7 100644 --- a/src/renderer/screens/DatabaseScreen/QueryResultViewer/TableCell/TableCellNumber.tsx +++ b/src/renderer/screens/DatabaseScreen/QueryResultViewer/TableCell/TableCellNumber.tsx @@ -12,13 +12,18 @@ function TableCellNumberEditor({ onExit, readOnly, }: TableEditableEditorProps) { - const [editValue, setEditValue] = useState( - value !== null ? (value as number).toString() : null + const [editValue, setEditValue] = useState( + value !== undefined && value !== null ? (value as number).toString() : value ); const onLostFocus = useCallback(() => { if (onExit) { - onExit(false, editValue === null ? editValue : Number(editValue)); + onExit( + false, + editValue === null || editValue === undefined + ? editValue + : Number(editValue) + ); } }, [onExit, editValue]); diff --git a/src/renderer/screens/DatabaseScreen/QueryResultViewer/index.tsx b/src/renderer/screens/DatabaseScreen/QueryResultViewer/index.tsx index 548f807..ebfd979 100644 --- a/src/renderer/screens/DatabaseScreen/QueryResultViewer/index.tsx +++ b/src/renderer/screens/DatabaseScreen/QueryResultViewer/index.tsx @@ -1,18 +1,39 @@ -import React, { useState } from 'react'; +import React, { useCallback, useState } from 'react'; import { QueryResultChangeProvider } from 'renderer/contexts/QueryResultChangeProvider'; import QueryResultTable from './QueryResultTable'; import styles from './styles.module.scss'; import { TableCellManagerProvider } from './TableCellManager'; import QueryResultAction from './QueryResultAction'; import { QueryResult } from 'types/SqlResult'; +import { SqlStatement } from 'types/SqlStatement'; +import { useSqlExecute } from 'renderer/contexts/SqlExecuteProvider'; -function QueryResultViewer({ result }: { result: QueryResult }) { +function QueryResultViewer({ + result, + statement, +}: { + result: QueryResult; + statement: SqlStatement; +}) { + const { runner } = useSqlExecute(); + // This is use remount the component + const [runningIndex, setRunningIndex] = useState(0); const [cacheResult, setCacheResult] = useState(result); const [page, setPage] = useState(0); const pageSize = 1000; + const onRequestRefetch = useCallback(() => { + runner + .execute([statement]) + .then((result) => { + setCacheResult(result[0].result); + setRunningIndex((prev) => prev + 1); + }) + .catch(console.error); + }, [statement, runner, setCacheResult, setRunningIndex]); + return ( - +
diff --git a/src/types/SqlStatement.ts b/src/types/SqlStatement.ts index 8693799..6471b0a 100644 --- a/src/types/SqlStatement.ts +++ b/src/types/SqlStatement.ts @@ -10,6 +10,6 @@ export interface SqlStatement { export interface SqlStatementPlan { type: 'insert' | 'delete' | 'update'; table: string; - values: Record; + values?: Record; where?: Record; }