diff --git a/.all-contributorsrc b/.all-contributorsrc index 39e1aa91..531e03bd 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1,5 +1,5 @@ { - "projectName": "Antares", + "projectName": "antares", "projectOwner": "Fabio286", "repoType": "github", "repoHost": "https://github.com", @@ -111,7 +111,35 @@ "contributions": [ "platform" ] + }, + { + "login": "kilianstallz", + "name": "Kilian Stallinger", + "avatar_url": "https://avatars.githubusercontent.com/u/5290318?v=4", + "profile": "https://kilianstallinger.com", + "contributions": [ + "code" + ] + }, + { + "login": "wenj91", + "name": "文杰", + "avatar_url": "https://avatars.githubusercontent.com/u/12549338?v=4", + "profile": "https://github.com/wenj91", + "contributions": [ + "code" + ] + }, + { + "login": "goYou", + "name": "goYou", + "avatar_url": "https://avatars.githubusercontent.com/u/62732795?v=4", + "profile": "https://github.com/goYou", + "contributions": [ + "translation" + ] } ], - "contributorsPerLine": 7 + "contributorsPerLine": 7, + "skipCi": true } diff --git a/.eslintignore b/.eslintignore index c87fbd4d..d3405a7a 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,4 +1,4 @@ -/node_modules -/assets/vendor -/out -/dist \ No newline at end of file +node_modules +assets +out +dist \ No newline at end of file diff --git a/.eslintrc b/.eslintrc index 5c86654c..54031e45 100644 --- a/.eslintrc +++ b/.eslintrc @@ -45,6 +45,7 @@ "no-console": "off", "no-undef": "off", "vue/no-side-effects-in-computed-properties": "off", + "vue/multi-word-component-names": "off", "vue/require-default-prop": "off", "vue/comment-directive": "off", "vue/no-v-html": "off", @@ -61,10 +62,11 @@ "vue/max-attributes-per-line": [ "error", { - "singleline": 2, + "singleline": { + "max": 2 + }, "multiline": { - "max": 1, - "allowFirstLine": false + "max": 1 } } ] diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index d0a63f9c..edb77ac1 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -12,12 +12,18 @@ jobs: steps: - name: Check out Git repository - uses: actions/checkout@v1 + uses: actions/checkout@v2 - name: Install Node.js, NPM and Yarn uses: actions/setup-node@v1 with: node-version: 14 + + - name: Install dependencies + run: npm i + + - name: Run tests + run: npm run test - name: Build/release Electron app uses: samuelmeuli/action-electron-builder@v1 diff --git a/.github/workflows/build-mac.yml b/.github/workflows/build-mac.yml index 95679142..9981e0a8 100644 --- a/.github/workflows/build-mac.yml +++ b/.github/workflows/build-mac.yml @@ -12,12 +12,18 @@ jobs: steps: - name: Check out Git repository - uses: actions/checkout@v1 + uses: actions/checkout@v2 - name: Install Node.js, NPM and Yarn uses: actions/setup-node@v1 with: node-version: 14 + + - name: Install dependencies + run: npm i + + - name: Run tests + run: npm run test - name: Build/release Electron app uses: samuelmeuli/action-electron-builder@v1 diff --git a/.github/workflows/build-win.yml b/.github/workflows/build-win.yml index 7495faa5..747902d8 100644 --- a/.github/workflows/build-win.yml +++ b/.github/workflows/build-win.yml @@ -12,13 +12,19 @@ jobs: steps: - name: Check out Git repository - uses: actions/checkout@v1 + uses: actions/checkout@v2 - name: Install Node.js, NPM and Yarn uses: actions/setup-node@v1 with: node-version: 14 + - name: Install dependencies + run: npm i + + - name: Run tests + run: npm run test + - name: Build/release Electron app uses: samuelmeuli/action-electron-builder@v1 with: diff --git a/.vscode/settings.json b/.vscode/settings.json index 5b0c8130..768edbfb 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,7 +3,9 @@ "UI", "core", "MySQL", - "PostgreSQL" + "PostgreSQL", + "SQLite", + "Windows" ], "svg.preview.background": "transparent" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 0402fdd1..eef86af4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,128 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### [0.4.3](https://github.com/Fabio286/antares/compare/v0.4.2...v0.4.3) (2022-01-30) + + +### Features + +* add Simplified Chinese translation ([6ef565c](https://github.com/Fabio286/antares/commit/6ef565cf078cb3f5b7bcdc226894cddeb6239db9)) +* **MySQL:** spatial fields support ([#165](https://github.com/Fabio286/antares/issues/165)) ([48ebf23](https://github.com/Fabio286/antares/commit/48ebf23bd1574408f429f2e1200ce878352007f6)) + + +### Bug Fixes + +* cell copy returns "undefined" in some conditions, closes [#170](https://github.com/Fabio286/antares/issues/170) ([8fb1f08](https://github.com/Fabio286/antares/commit/8fb1f0803efd9df0b66521e73bb6e1a229cf9691)) +* indexes and foreign keys not cleared after deletion of related field, closes [#182](https://github.com/Fabio286/antares/issues/182) ([9f033fb](https://github.com/Fabio286/antares/commit/9f033fb994916b4fb165e81e55e86127ca817791)) +* **PostgreSQL:** schema different than public not automatically selected, closes [#172](https://github.com/Fabio286/antares/issues/172) ([46b45c8](https://github.com/Fabio286/antares/commit/46b45c8ab64fb6837a532c4f8342167e4fd794bb)) +* scale on numeric fields that doesn't support it ([0cfd793](https://github.com/Fabio286/antares/commit/0cfd7938ee7d607dbad66ae452d0200223a6bab2)) +* **Windows:** temporary fix to Windows 7 style frame on app startup, closes [#169](https://github.com/Fabio286/antares/issues/169) ([1356011](https://github.com/Fabio286/antares/commit/1356011ba3b7dd72e12cb252a8787ce48a364fd4)) + + +### Improvements + +* support of scale in field's length setting ([eef7c1d](https://github.com/Fabio286/antares/commit/eef7c1dcecc6593ab0e69ed678187a57fe0a4fb6)) + +### [0.4.2](https://github.com/Fabio286/antares/compare/v0.4.1...v0.4.2) (2022-01-10) + + +### Features + +* **MySQL:** ability to cancel queries ([a59f77f](https://github.com/Fabio286/antares/commit/a59f77f618aea6156fc80fb832d3efcb9848411f)) +* **PostgreSQL:** ability to cancel queries ([0c00291](https://github.com/Fabio286/antares/commit/0c002918eb0226f6b3f21ed62117495f86396fb1)) +* save window state ([8f9385d](https://github.com/Fabio286/antares/commit/8f9385d50815635d091758ecd5d00884e3297ca0)) +* **UI:** textarea autofocus selecting a query tab, closes [#166](https://github.com/Fabio286/antares/issues/166) ([b4545b1](https://github.com/Fabio286/antares/commit/b4545b178f795712c781a3f4fc35eec31b5ad902)) + + +### Bug Fixes + +* **SQLite:** exception with some fields ([e7a1858](https://github.com/Fabio286/antares/commit/e7a18580915e7739bfa97948c6a0c4fc90a7e78a)) + + +### Improvements + +* hash for foreign key default names ([48c3e6a](https://github.com/Fabio286/antares/commit/48c3e6afc43c51f70a16703f1a71194f43da7a3e)) +* **MySQL:** support to ANSI_QUOTES sql_mode, closes [#158](https://github.com/Fabio286/antares/issues/158) ([d9a3eab](https://github.com/Fabio286/antares/commit/d9a3eab015302e9f23112f659658073ab3242191)) + +### [0.4.1](https://github.com/Fabio286/antares/compare/v0.4.0...v0.4.1) (2021-12-11) + + +### Features + +* language format detection for text fields ([a5fdcc1](https://github.com/Fabio286/antares/commit/a5fdcc1a85aa188ff1b9a15b1a768aced026f360)) + + +### Bug Fixes + +* cell disappear on edit in one column tables ([aaa5549](https://github.com/Fabio286/antares/commit/aaa5549609664665bd4513632d621cb249b379c1)) +* false positive with Windows Defender ([992a033](https://github.com/Fabio286/antares/commit/992a033cb2bede3d1eb52e19482d810f6692de1e)) +* **MySQL:** wrong datetime fields default in table filler in some cases ([8da0224](https://github.com/Fabio286/antares/commit/8da022487650039b7f34a9c86a7bd9045eba65e2)) +* **MySQL:** wrong value for fields "on update" in some conditions ([359e14a](https://github.com/Fabio286/antares/commit/359e14a9ebd48f86069ba7762fe00a7056f52d47)) +* select all rows with ctrl+a when editing a cell ([35cb7e1](https://github.com/Fabio286/antares/commit/35cb7e1dc48d3a74e9d106cb1a37f454c1b4a4d1)) +* **SQLite:** update rows with a text primary key ([d7f1aa9](https://github.com/Fabio286/antares/commit/d7f1aa97af32a4c51fc7022498bd47e15fa08430)) + + +### Improvements + +* **UI:** avoid columns size change when editing cells or scrolling results ([813aa32](https://github.com/Fabio286/antares/commit/813aa320d9ab799efea38a7110b7c0bdf7549123)) +* **UI:** disable save button in table creation when no fields are added ([e8af2d2](https://github.com/Fabio286/antares/commit/e8af2d24a869f7667c069936648808952d2062ab)) + +## [0.4.0](https://github.com/Fabio286/antares/compare/v0.3.9...v0.4.0) (2021-11-24) + + +### Features + +* **MySQL:** read-only mode ([4437d44](https://github.com/Fabio286/antares/commit/4437d44486c4f20b0bec4bf89d56016b08e36e79)) +* **PostgreSQL:** read-only mode ([5d48fe0](https://github.com/Fabio286/antares/commit/5d48fe08c77755ed18b3f7a9ea834268e317e7ef)) +* **SQLite:** cell update in data tabs ([604b371](https://github.com/Fabio286/antares/commit/604b3719204f7473ce4846624f08f8be9eec8b8f)) +* **SQLite:** connection add/edit masks ([c54438d](https://github.com/Fabio286/antares/commit/c54438d6d3bad38bc76dfcd61f58929fe30279cb)) +* **SQLite:** keys support ([fd321be](https://github.com/Fabio286/antares/commit/fd321beece075d3ad23fdd8541f9beb5727045a5)) +* **SQLite:** readonly mode ([3fc227d](https://github.com/Fabio286/antares/commit/3fc227d2de53aae115226ad3c965bfb6e9f3eca6)) +* **SQLite:** table data visualization ([f2fcc98](https://github.com/Fabio286/antares/commit/f2fcc9883972402eab4d51ef2a9796638dde2d3d)) +* **SQLite:** tables management ([3efeb45](https://github.com/Fabio286/antares/commit/3efeb45c460f178b794de72367f8d542fd8ddd56)) +* **SQLite:** triggers management ([f40e9c5](https://github.com/Fabio286/antares/commit/f40e9c592eeffd204aba21a0a0767a0c523fca49)) +* **SQLite:** views management ([7671c58](https://github.com/Fabio286/antares/commit/7671c585f5f8049bd863db190d4fc60d8f0c6c66)) + + +### Bug Fixes + +* **SQLite:** hide schema creation ([98165ca](https://github.com/Fabio286/antares/commit/98165cacaa158c85ead0490d3caf579e2a17319f)) +* **UI:** hide tools menu if no tools available ([da1947e](https://github.com/Fabio286/antares/commit/da1947e4efa7f0a26d6a231fadf750be055fbdd5)) +* **UI:** notifications timeout anomalies ([cc99491](https://github.com/Fabio286/antares/commit/cc99491fe4a15812368f6c928b8c7801d7b255aa)) + + +### Improvements + +* **SQLite:** improvements in data visualization ([94c899e](https://github.com/Fabio286/antares/commit/94c899eb8288b41a5962ac3d24365227e1f9f485)) +* **SQLite:** improvements in field length detection ([93b4a70](https://github.com/Fabio286/antares/commit/93b4a7063beeb5a7001cb06a74f05b23105212f5)) +* update italian traslation ([9fe3680](https://github.com/Fabio286/antares/commit/9fe3680bbb17c192cffa85348e68794ab49beb81)) + +### [0.3.9](https://github.com/Fabio286/antares/compare/v0.3.8...v0.3.9) (2021-11-14) + + +### Features + +* added macos basic shortcusts and menu ([430490a](https://github.com/Fabio286/antares/commit/430490ad93f3148962ced1f13a5330c79cd86b3b)) +* **MySQL:** enable/disable schedulers from contextual menu ([5ca3a22](https://github.com/Fabio286/antares/commit/5ca3a22dc538b27a4bf6402f1288c4b9f5bc5a90)) +* **MySQL:** scheduler status indicator in explore bar ([5c66824](https://github.com/Fabio286/antares/commit/5c668249cf102cd9d601f9f7b4943c7155775217)) +* **PostgreSQL:** enable/disable triggers from contextual menu ([534659f](https://github.com/Fabio286/antares/commit/534659f9aee12eb5ac477f91bfe5d764387dc17e)) +* schema size in explore bar ([fd25f88](https://github.com/Fabio286/antares/commit/fd25f881f95779709156cbad93a41d6b391f1a45)) +* **UI:** double click on the title bar will toggle window fullscreen size ([a35566f](https://github.com/Fabio286/antares/commit/a35566f273322602abe434b8bd30817ba8885900)) +* **UI:** improved topbar look&feel on MacOS ([7657d05](https://github.com/Fabio286/antares/commit/7657d05edfbeaed6a14eb337fc562da5126e6ba0)) + + +### Bug Fixes + +* copy&paste and basic usability on macOS ([1ddf8f0](https://github.com/Fabio286/antares/commit/1ddf8f0dbe22f94d6bffddf70636706d2d142ecf)) +* **PostgreSQL:** bigint fetched as string instead of number, closes [#134](https://github.com/Fabio286/antares/issues/134) ([39b9a59](https://github.com/Fabio286/antares/commit/39b9a59143b457a96f0711a3b8588c92dd80e28d)) +* row selection problems after a deletion fail, closes [#128](https://github.com/Fabio286/antares/issues/128) ([89fdd21](https://github.com/Fabio286/antares/commit/89fdd210ca48fc9ae399b195ea796c8523619627)) +* temporary solution on MacOS for unsigned app updates ([c00fd13](https://github.com/Fabio286/antares/commit/c00fd1381f451ba7aace7047b28b904ddcaf18f0)) + + +### Improvements + +* **UI:** improved function and routine parameters modals ([d19f475](https://github.com/Fabio286/antares/commit/d19f475fc28c0367ada569cb634769fa618b48b4)) + ### [0.3.8](https://github.com/Fabio286/antares/compare/v0.3.7...v0.3.8) (2021-10-23) diff --git a/README.md b/README.md index af1fe470..c0aa199b 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Antares is an SQL client based on [Electron.js](https://github.com/electron/electron) and [Vue.js](https://github.com/vuejs/vue) that aims to become a useful tool, especially for developers. Our target is to support as many databases as possible, and all major operating systems, including the ARM versions. -**At the moment this application is in development state, many features will come in future updates**, and supports only MySQL/MariaDB and PostgreSQL. +**At the moment this application is in development state, many features will come in future updates**, and supports only MySQL/MariaDB, PostgreSQL and SQLite. At the moment, however, there are all the features necessary to have a pleasant database management experience, so give it a chance and send us your feedback, we would really appreciate it. We are actively working on it, hoping to provide new cool features, improvements and fixes as soon as possible. @@ -31,10 +31,9 @@ We are actively working on it, hoping to provide new cool features, improvements - Query suggestions and auto complete. - Query history: search through the last 1000 queries. - SSH tunnel support. +- Manual commit mode. - Dark and light theme. - Editor themes. -- Scratchpad. -- Secure password storage. ## Philosophy @@ -68,12 +67,12 @@ On macOS you can run `.dmg` distribution following [this guide](https://support. This is a roadmap with major features will come in near future. -- Support for other databases. - Database tools. - Users management (add/edit/delete). - More context menu shortcuts. - More keyboard shortcuts. - Import/export and migration. +- Support for other databases. - Apple Silicon distribution ## Currently supported @@ -82,7 +81,7 @@ This is a roadmap with major features will come in near future. - [x] MySQL/MariaDB - [x] PostgreSQL -- [ ] SQLite +- [x] SQLite - [ ] MSSQL - [ ] OracleDB - [ ] More... @@ -116,9 +115,9 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d - - - + + + @@ -129,6 +128,9 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d + + +

Fabio Di Stasio

💻 🌍 📖

Giulio Ganci

💻

Christian Ratz

💻 🌍

Fabio Di Stasio

💻 🌍 📖

Giulio Ganci

💻

Christian Ratz

💻 🌍

Giuseppe Gigliotti

🌍

Mohd-PH

🌍

hongkfui

🌍

Ngô Quốc Đạt

🌍

Isamu Sugiura

🌍

Riccardo Sacchetto

📦

Kilian Stallinger

💻

文杰

💻

goYou

🌍
diff --git a/assets/appx/LargeTile.png b/assets/appx/LargeTile.png deleted file mode 100644 index afef8cac..00000000 Binary files a/assets/appx/LargeTile.png and /dev/null differ diff --git a/assets/appx/SmallTile.png b/assets/appx/SmallTile.png deleted file mode 100644 index 91b41331..00000000 Binary files a/assets/appx/SmallTile.png and /dev/null differ diff --git a/assets/appx/Square150x150Logo.png b/assets/appx/Square150x150Logo.png index 730eb946..007497bb 100644 Binary files a/assets/appx/Square150x150Logo.png and b/assets/appx/Square150x150Logo.png differ diff --git a/assets/appx/Square44x44Logo.png b/assets/appx/Square44x44Logo.png index 444becef..6cfb69b7 100644 Binary files a/assets/appx/Square44x44Logo.png and b/assets/appx/Square44x44Logo.png differ diff --git a/assets/appx/StoreLogo.png b/assets/appx/StoreLogo.png index 0ba72c45..6b03a448 100644 Binary files a/assets/appx/StoreLogo.png and b/assets/appx/StoreLogo.png differ diff --git a/assets/appx/Wide310x150Logo.png b/assets/appx/Wide310x150Logo.png index fbe0e56b..4ffec399 100644 Binary files a/assets/appx/Wide310x150Logo.png and b/assets/appx/Wide310x150Logo.png differ diff --git a/package.json b/package.json index 08617900..cf93c684 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "antares", "productName": "Antares", - "version": "0.3.8", - "description": "A cross-platform easy to use SQL client.", + "version": "0.4.3", + "description": "A modern, fast and productivity driven SQL client with a focus in UX.", "license": "MIT", "repository": "https://github.com/Fabio286/antares.git", "scripts": { @@ -14,11 +14,11 @@ "build": "cross-env NODE_ENV=production npm run compile", "build:local": "npm run build && electron-builder", "build:appx": "npm run build:local -- --win appx", - "rebuild:electron": "npm run postinstall && electron-rebuild", + "rebuild:electron": "npm run postinstall", "release": "standard-version", "release:pre": "npm run release -- --prerelease alpha", "postinstall": "electron-builder install-app-deps", - "test": "npm run lint", + "test": "npm run compile && node tests/app.spec.js", "lint": "eslint . --ext .js,.vue && stylelint \"./src/**/*.{css,scss,sass,vue}\"", "lint:fix": "eslint . --ext .js,.vue --fix && stylelint \"./src/**/*.{css,scss,sass,vue}\" --fix", "contributors:add": "all-contributors add", @@ -30,6 +30,7 @@ "appId": "com.fabio286.antares", "artifactName": "${productName}-${version}-${os}_${arch}.${ext}", "asar": true, + "buildDependenciesFromSource": true, "directories": { "output": "build", "buildResources": "assets" @@ -50,7 +51,8 @@ "target": { "target": "default", "arch": [ - "x64" + "x64", + "arm64" ] } }, @@ -79,7 +81,9 @@ "artifactName": "${productName}-${version}-portable.exe" }, "appx": { - "displayName": "Antares SQL Client", + "displayName": "Antares SQL", + "backgroundColor": "transparent", + "showNameOnTiles": true, "identityName": "62514FabioDiStasio.AntaresSQLClient", "publisher": "CN=1A2729ED-865C-41D2-9038-39AE2A63AA52", "applicationId": "FabioDiStasio.AntaresSQLClient" @@ -102,12 +106,17 @@ "dependencies": { "@electron/remote": "^2.0.1", "@mdi/font": "^6.1.95", + "@turf/helpers": "^6.5.0", + "@vscode/vscode-languagedetection": "^1.0.21", "ace-builds": "^1.4.13", + "better-sqlite3": "^7.4.4", "electron-log": "^4.4.1", "electron-store": "^8.0.1", - "electron-updater": "^4.3.9", + "electron-updater": "^4.6.1", + "electron-window-state": "^5.0.3", "faker": "^5.5.3", - "marked": "^3.0.4", + "leaflet": "^1.7.1", + "marked": "^4.0.0", "moment": "^2.29.1", "mysql2": "^2.3.2", "pg": "^8.7.1", @@ -130,23 +139,23 @@ "clean-webpack-plugin": "^4.0.0", "cross-env": "^7.0.2", "css-loader": "^6.5.0", - "electron": "^15.3.0", - "electron-builder": "^22.13.1", + "electron": "^17.0.1", + "electron-builder": "^22.14.11", "electron-devtools-installer": "^3.2.0", - "electron-rebuild": "^3.2.3", "eslint": "^7.32.0", "eslint-config-standard": "^16.0.3", "eslint-plugin-import": "^2.24.2", "eslint-plugin-node": "^11.1.0", - "eslint-plugin-promise": "^5.1.0", - "eslint-plugin-vue": "^7.18.0", + "eslint-plugin-promise": "^5.2.0", + "eslint-plugin-vue": "^8.0.3", "file-loader": "^6.2.0", "html-webpack-plugin": "^5.5.0", - "mini-css-extract-plugin": "^2.4.3", + "mini-css-extract-plugin": "~2.4.5", "node-loader": "^2.0.0", + "playwright": "^1.18.1", "progress-webpack-plugin": "^1.0.12", "sass": "^1.42.1", - "sass-loader": "^10.2.0", + "sass-loader": "^12.3.0", "standard-version": "^9.3.1", "style-loader": "^3.3.1", "stylelint": "^13.13.1", @@ -158,6 +167,6 @@ "vue-template-compiler": "^2.6.14", "webpack": "^5.60.0", "webpack-cli": "^4.9.1", - "webpack-dev-server": "^3.11.2" + "webpack-dev-server": "^4.4.0" } -} \ No newline at end of file +} diff --git a/scripts/devRunner.js b/scripts/devRunner.js index bfbea89d..ed3eff40 100644 --- a/scripts/devRunner.js +++ b/scripts/devRunner.js @@ -113,14 +113,15 @@ function startRenderer (callback) { }); const server = new WebpackDevServer(compiler, { - contentBase: path.join(__dirname, '../'), hot: true, - noInfo: true, - overlay: true, - clientLogLevel: 'warning' + port: 9080, + client: { + overlay: true, + logging: 'warn' + } }); - server.listen(9080, '', err => { + server.startCallback(err => { if (err) console.error(chalk.red(err)); callback(); diff --git a/src/common/FakerMethods.js b/src/common/FakerMethods.js index fe7c7ec4..e401fa11 100644 --- a/src/common/FakerMethods.js +++ b/src/common/FakerMethods.js @@ -134,7 +134,7 @@ export default class { { name: 'phoneNumberFormat', group: 'phone', types: ['string'] }, { name: 'phoneFormats', group: 'phone', types: ['string'] }, - { name: 'number', group: 'random', types: ['string', 'number'], params: ['min', 'max'] }, + { name: 'number', group: 'datatype', types: ['string', 'number'], params: ['min', 'max'] }, { name: 'float', group: 'random', types: ['string', 'float'], params: ['min', 'max'] }, { name: 'arrayElement', group: 'random', types: ['string'] }, { name: 'arrayElements', group: 'random', types: ['string'] }, diff --git a/src/common/customizations/defaults.js b/src/common/customizations/defaults.js index 1cc0fe47..ad5788df 100644 --- a/src/common/customizations/defaults.js +++ b/src/common/customizations/defaults.js @@ -8,6 +8,10 @@ module.exports = { collations: false, engines: false, connectionSchema: false, + sslConnection: false, + sshConnection: false, + fileConnection: false, + cancelQueries: false, // Tools processesList: false, usersManagement: false, @@ -33,7 +37,12 @@ module.exports = { schedulerAdd: false, databaseEdit: false, schemaEdit: false, + schemaDrop: false, + schemaExport: false, tableSettings: false, + tableOptions: false, + tableArray: false, + tableRealCount: false, viewSettings: false, triggerSettings: false, triggerFunctionSettings: false, @@ -45,14 +54,13 @@ module.exports = { sortableFields: false, unsigned: false, nullable: false, + nullablePrimary: false, zerofill: false, - tableOptions: false, autoIncrement: false, comment: false, collation: false, definer: false, onUpdate: false, - tableArray: false, viewAlgorithm: false, viewSqlSecurity: false, viewUpdateOption: false, @@ -72,8 +80,10 @@ module.exports = { triggerTableInName: false, triggerUpdateColumns: false, triggerOnlyRename: false, + triggerEnableDisable: false, triggerFunctionSql: false, triggerFunctionlanguages: false, parametersLength: false, - languages: false + languages: false, + readOnlyMode: false }; diff --git a/src/common/customizations/index.js b/src/common/customizations/index.js index 304889a4..931e18f2 100644 --- a/src/common/customizations/index.js +++ b/src/common/customizations/index.js @@ -1,5 +1,6 @@ module.exports = { maria: require('./mysql'), mysql: require('./mysql'), - pg: require('./postgresql') + pg: require('./postgresql'), + sqlite: require('./sqlite') }; diff --git a/src/common/customizations/mysql.js b/src/common/customizations/mysql.js index 79fae6bc..d037d26c 100644 --- a/src/common/customizations/mysql.js +++ b/src/common/customizations/mysql.js @@ -10,6 +10,9 @@ module.exports = { connectionSchema: true, collations: true, engines: true, + sslConnection: true, + sshConnection: true, + cancelQueries: true, // Tools processesList: true, // Structure @@ -30,6 +33,7 @@ module.exports = { functionAdd: true, schedulerAdd: true, schemaEdit: true, + schemaDrop: true, schemaExport: true, tableSettings: true, viewSettings: true, @@ -60,5 +64,6 @@ module.exports = { functionDeterministic: true, functionDataAccess: true, functionSql: 'BEGIN\r\n\r\nEND', - parametersLength: true + parametersLength: true, + readOnlyMode: true }; diff --git a/src/common/customizations/postgresql.js b/src/common/customizations/postgresql.js index 655a716b..3d827c11 100644 --- a/src/common/customizations/postgresql.js +++ b/src/common/customizations/postgresql.js @@ -8,9 +8,13 @@ module.exports = { defaultDatabase: 'postgres', // Core database: true, + sslConnection: true, + sshConnection: true, + cancelQueries: true, // Tools processesList: true, // Structure + schemas: true, tables: true, views: true, triggers: true, @@ -26,6 +30,7 @@ module.exports = { triggerFunctionAdd: true, routineAdd: true, functionAdd: true, + schemaDrop: true, databaseEdit: false, schemaExport: true, tableSettings: true, @@ -51,5 +56,7 @@ module.exports = { triggerMultipleEvents: true, triggerTableInName: true, triggerOnlyRename: false, - languages: ['sql', 'plpgsql', 'c', 'internal'] + triggerEnableDisable: true, + languages: ['sql', 'plpgsql', 'c', 'internal'], + readOnlyMode: true }; diff --git a/src/common/customizations/sqlite.js b/src/common/customizations/sqlite.js new file mode 100644 index 00000000..83c6cdb3 --- /dev/null +++ b/src/common/customizations/sqlite.js @@ -0,0 +1,27 @@ +module.exports = { + // Core + fileConnection: true, + // Structure + schemas: false, + tables: true, + views: true, + triggers: true, + // Settings + elementsWrapper: '"', + stringsWrapper: '\'', + tableAdd: true, + viewAdd: true, + triggerAdd: true, + schemaEdit: false, + tableSettings: true, + tableRealCount: true, + viewSettings: true, + triggerSettings: true, + indexes: true, + foreigns: true, + sortableFields: true, + nullable: true, + nullablePrimary: true, + triggerSql: 'BEGIN\r\n\r\nEND', + readOnlyMode: true +}; diff --git a/src/common/data-types/mysql.js b/src/common/data-types/mysql.js index 78e3d2c9..14b61229 100644 --- a/src/common/data-types/mysql.js +++ b/src/common/data-types/mysql.js @@ -66,6 +66,7 @@ module.exports = [ { name: 'DECIMAL', length: true, + scale: true, collation: false, unsigned: false, zerofill: false @@ -120,7 +121,7 @@ module.exports = [ { name: 'JSON', length: false, - collation: true, + collation: false, unsigned: false, zerofill: false } @@ -218,56 +219,56 @@ module.exports = [ types: [ { name: 'POINT', - length: true, + length: false, collation: false, unsigned: false, zerofill: false }, { name: 'LINESTRING', - length: true, + length: false, collation: false, unsigned: false, zerofill: false }, { name: 'POLYGON', - length: true, + length: false, collation: false, unsigned: false, zerofill: false }, { name: 'GEOMETRY', - length: true, + length: false, collation: false, unsigned: false, zerofill: false }, { name: 'MULTIPOINT', - length: true, + length: false, collation: false, unsigned: false, zerofill: false }, { name: 'MULTILINESTRING', - length: true, + length: false, collation: false, unsigned: false, zerofill: false }, { name: 'MULTIPOLYGON', - length: true, + length: false, collation: false, unsigned: false, zerofill: false }, { - name: 'GEOMETRYCOLLECTION', - length: true, + name: 'GEOMCOLLECTION', + length: false, collation: false, unsigned: false, zerofill: false diff --git a/src/common/data-types/postgresql.js b/src/common/data-types/postgresql.js index bfe9e50d..e666229b 100644 --- a/src/common/data-types/postgresql.js +++ b/src/common/data-types/postgresql.js @@ -22,11 +22,6 @@ module.exports = [ length: false, unsigned: true }, - { - name: 'NUMERIC', - length: true, - unsigned: true - }, { name: 'SMALLSERIAL', length: false, @@ -52,6 +47,12 @@ module.exports = [ length: false, unsigned: true }, + { + name: 'NUMERIC', + length: true, + unsigned: true, + scale: true + }, { name: 'DOUBLE PRECISION', length: false, diff --git a/src/common/data-types/sqlite.js b/src/common/data-types/sqlite.js new file mode 100644 index 00000000..c7ac6b32 --- /dev/null +++ b/src/common/data-types/sqlite.js @@ -0,0 +1,137 @@ +module.exports = [ + { + group: 'integer', + types: [ + { + name: 'INT', + length: true, + collation: false, + unsigned: true, + zerofill: true + }, + { + name: 'INTEGER', + length: true, + collation: false, + unsigned: true, + zerofill: true + }, + { + name: 'BIGINT', + length: true, + collation: false, + unsigned: true, + zerofill: true + }, + { + name: 'NUMERIC', + length: true, + collation: false, + unsigned: true, + zerofill: true + }, + { + name: 'BOOLEAN', + length: false, + collation: false, + unsigned: true, + zerofill: true + } + ] + }, + { + group: 'float', + types: [ + { + name: 'FLOAT', + length: true, + collation: false, + unsigned: false, + zerofill: false + }, + { + name: 'REAL', + length: true, + collation: false, + unsigned: false, + zerofill: false + } + ] + }, + { + group: 'string', + types: [ + { + name: 'CHAR', + length: true, + collation: true, + unsigned: false, + zerofill: false + }, + { + name: 'VARCHAR', + length: true, + collation: true, + unsigned: false, + zerofill: false + }, + { + name: 'TEXT', + length: true, + collation: true, + unsigned: false, + zerofill: false + } + ] + }, + { + group: 'binary', + types: [ + { + name: 'BLOB', + length: true, + collation: false, + unsigned: false, + zerofill: false + } + ] + }, + { + group: 'time', + types: [ + { + name: 'DATE', + length: false, + collation: false, + unsigned: false, + zerofill: false + }, + { + name: 'TIME', + length: true, + collation: false, + unsigned: false, + zerofill: false + }, + { + name: 'DATETIME', + length: true, + collation: false, + unsigned: false, + zerofill: false + } + ] + }, + { + group: 'other', + types: [ + { + name: 'NONE', + length: false, + collation: false, + unsigned: false, + zerofill: false + } + ] + } +]; diff --git a/src/common/fieldTypes.js b/src/common/fieldTypes.js index d3a8a1db..a7d3399f 100644 --- a/src/common/fieldTypes.js +++ b/src/common/fieldTypes.js @@ -8,7 +8,9 @@ export const TEXT = [ export const LONG_TEXT = [ 'TEXT', 'MEDIUMTEXT', - 'LONGTEXT' + 'LONGTEXT', + 'JSON', + 'VARBINARY' ]; export const ARRAY = [ @@ -82,3 +84,24 @@ export const BIT = [ 'BIT', 'BIT VARYING' ]; + +export const SPATIAL = [ + 'POINT', + 'LINESTRING', + 'POLYGON', + 'GEOMETRY', + 'MULTIPOINT', + 'MULTILINESTRING', + 'MULTIPOLYGON', + 'GEOMCOLLECTION', + 'GEOMETRYCOLLECTION' +]; + +// Used to check multi spatial fields only +export const IS_MULTI_SPATIAL = [ + 'MULTIPOINT', + 'MULTILINESTRING', + 'MULTIPOLYGON', + 'GEOMCOLLECTION', + 'GEOMETRYCOLLECTION' +]; diff --git a/src/common/index-types/sqlite.js b/src/common/index-types/sqlite.js new file mode 100644 index 00000000..edc2f2a3 --- /dev/null +++ b/src/common/index-types/sqlite.js @@ -0,0 +1,5 @@ +module.exports = [ + 'PRIMARY', + 'INDEX', + 'UNIQUE' +]; diff --git a/src/common/libs/getArrayDepth.js b/src/common/libs/getArrayDepth.js new file mode 100644 index 00000000..04e62e77 --- /dev/null +++ b/src/common/libs/getArrayDepth.js @@ -0,0 +1,10 @@ +/** + * + * @param {any[]} array + * @returns {number} + */ +export function getArrayDepth (array) { + return Array.isArray(array) + ? 1 + Math.max(0, ...array.map(getArrayDepth)) + : 0; +} diff --git a/src/main/ipc-handlers/connection.js b/src/main/ipc-handlers/connection.js index aa2c1378..c539f75a 100644 --- a/src/main/ipc-handlers/connection.js +++ b/src/main/ipc-handlers/connection.js @@ -9,12 +9,16 @@ export default connections => { port: +conn.port, user: conn.user, password: conn.password, - application_name: 'Antares SQL' + application_name: 'Antares SQL', + readonly: conn.readonly }; if (conn.database) params.database = conn.database; + if (conn.databasePath) + params.databasePath = conn.databasePath; + if (conn.ssl) { params.ssl = { key: conn.key ? fs.readFileSync(conn.key) : null, @@ -48,7 +52,7 @@ export default connections => { return { status: 'success' }; } catch (err) { - return { status: 'error', response: err }; + return { status: 'error', response: err.toString() }; } }); @@ -62,12 +66,16 @@ export default connections => { port: +conn.port, user: conn.user, password: conn.password, - application_name: 'Antares SQL' + application_name: 'Antares SQL', + readonly: conn.readonly }; if (conn.database) params.database = conn.database; + if (conn.databasePath) + params.databasePath = conn.databasePath; + if (conn.schema) params.schema = conn.schema; diff --git a/src/main/ipc-handlers/schedulers.js b/src/main/ipc-handlers/schedulers.js index 4133fbed..f270beb3 100644 --- a/src/main/ipc-handlers/schedulers.js +++ b/src/main/ipc-handlers/schedulers.js @@ -40,4 +40,17 @@ export default (connections) => { return { status: 'error', response: err.toString() }; } }); + + ipcMain.handle('toggle-scheduler', async (event, params) => { + try { + if (!params.enabled) + await connections[params.uid].enableEvent({ ...params }); + else + await connections[params.uid].disableEvent({ ...params }); + return { status: 'success' }; + } + catch (err) { + return { status: 'error', response: err.toString() }; + } + }); }; diff --git a/src/main/ipc-handlers/schema.js b/src/main/ipc-handlers/schema.js index c59d9e0f..f161a5ae 100644 --- a/src/main/ipc-handlers/schema.js +++ b/src/main/ipc-handlers/schema.js @@ -149,7 +149,7 @@ export default connections => { } }); - ipcMain.handle('raw-query', async (event, { uid, query, schema }) => { + ipcMain.handle('raw-query', async (event, { uid, query, schema, tabUid, autocommit }) => { if (!query) return; try { @@ -157,6 +157,8 @@ export default connections => { nest: true, details: true, schema, + tabUid, + autocommit, comments: false }); @@ -263,4 +265,51 @@ export default connections => { return { status: 'success', response: { willAbort } }; }); + ipcMain.handle('kill-tab-query', async (event, { uid, tabUid }) => { + if (!tabUid) return; + + try { + await connections[uid].killTabQuery(tabUid); + return { status: 'success' }; + } + catch (err) { + return { status: 'error', response: err.toString() }; + } + }); + + ipcMain.handle('commit-tab', async (event, { uid, tabUid }) => { + if (!tabUid) return; + + try { + await connections[uid].commitTab(tabUid); + return { status: 'success' }; + } + catch (err) { + return { status: 'error', response: err.toString() }; + } + }); + + ipcMain.handle('rollback-tab', async (event, { uid, tabUid }) => { + if (!tabUid) return; + + try { + await connections[uid].rollbackTab(tabUid); + return { status: 'success' }; + } + catch (err) { + return { status: 'error', response: err.toString() }; + } + }); + + ipcMain.handle('destroy-connection-to-commit', async (event, { uid, tabUid }) => { + if (!tabUid) return; + + try { + await connections[uid].destroyConnectionToCommit(tabUid); + return { status: 'success' }; + } + catch (err) { + return { status: 'error', response: err.toString() }; + } + }); }; diff --git a/src/main/ipc-handlers/tables.js b/src/main/ipc-handlers/tables.js index 47ef823f..7791aa5b 100644 --- a/src/main/ipc-handlers/tables.js +++ b/src/main/ipc-handlers/tables.js @@ -3,6 +3,7 @@ import faker from 'faker'; import moment from 'moment'; import { sqlEscaper } from 'common/libs/sqlEscaper'; import { TEXT, LONG_TEXT, ARRAY, TEXT_SEARCH, NUMBER, FLOAT, BLOB, BIT, DATE, DATETIME } from 'common/fieldTypes'; +import * as customizations from 'common/customizations'; import fs from 'fs'; export default (connections) => { @@ -84,12 +85,13 @@ export default (connections) => { }); ipcMain.handle('update-table-cell', async (event, params) => { - delete params.row._id; + delete params.row._antares_id; + const { stringsWrapper: sw } = customizations[connections[params.uid]._client]; try { // TODO: move to client classes let escapedParam; let reload = false; - const id = typeof params.id === 'number' ? params.id : `"${params.id}"`; + const id = typeof params.id === 'number' ? params.id : `${sw}${params.id}${sw}`; if ([...NUMBER, ...FLOAT].includes(params.type)) escapedParam = params.content; @@ -102,6 +104,9 @@ export default (connections) => { case 'pg': escapedParam = `'${params.content.replaceAll('\'', '\'\'')}'`; break; + case 'sqlite': + escapedParam = `'${params.content.replaceAll('\'', '\'\'')}'`; + break; } } else if (ARRAY.includes(params.type)) @@ -122,6 +127,10 @@ export default (connections) => { fileBlob = fs.readFileSync(params.content); escapedParam = `decode('${fileBlob.toString('hex')}', 'hex')`; break; + case 'sqlite': + fileBlob = fs.readFileSync(params.content); + escapedParam = `X'${fileBlob.toString('hex')}'`; + break; } reload = true; } @@ -134,6 +143,9 @@ export default (connections) => { case 'pg': escapedParam = 'decode(\'\', \'hex\')'; break; + case 'sqlite': + escapedParam = 'X\'\''; + break; } } } @@ -188,7 +200,7 @@ export default (connections) => { const fieldName = Object.keys(row)[0].includes('.') ? `${params.table}.${params.primary}` : params.primary; return typeof row[fieldName] === 'string' - ? `"${row[fieldName]}"` + ? `'${row[fieldName]}'` : row[fieldName]; }).join(','); diff --git a/src/main/ipc-handlers/triggers.js b/src/main/ipc-handlers/triggers.js index b7edaeab..89df9a15 100644 --- a/src/main/ipc-handlers/triggers.js +++ b/src/main/ipc-handlers/triggers.js @@ -40,4 +40,17 @@ export default (connections) => { return { status: 'error', response: err.toString() }; } }); + + ipcMain.handle('toggle-trigger', async (event, params) => { + try { + if (!params.enabled) + await connections[params.uid].enableTrigger(params); + else + await connections[params.uid].disableTrigger(params); + return { status: 'success' }; + } + catch (err) { + return { status: 'error', response: err.toString() }; + } + }); }; diff --git a/src/main/ipc-handlers/updates.js b/src/main/ipc-handlers/updates.js index ff72702e..19636b52 100644 --- a/src/main/ipc-handlers/updates.js +++ b/src/main/ipc-handlers/updates.js @@ -2,6 +2,7 @@ import { ipcMain } from 'electron'; import { autoUpdater } from 'electron-updater'; import Store from 'electron-store'; const persistentStore = new Store({ name: 'settings' }); +const isMacOS = process.platform === 'darwin'; let mainWindow; autoUpdater.allowPrerelease = persistentStore.get('allow_prerelease', true); @@ -11,6 +12,9 @@ export default () => { mainWindow = event; if (process.windowsStore || (process.platform === 'linux' && !process.env.APPIMAGE)) mainWindow.reply('no-auto-update'); + else if (isMacOS) { // Temporary solution on MacOS for unsigned app updates + autoUpdater.autoDownload = false; + } else { autoUpdater.checkForUpdatesAndNotify().catch(() => { mainWindow.reply('check-failed'); @@ -28,7 +32,10 @@ export default () => { }); autoUpdater.on('update-available', () => { - mainWindow.reply('update-available'); + if (isMacOS) + mainWindow.reply('link-to-download'); + else + mainWindow.reply('update-available'); }); autoUpdater.on('update-not-available', () => { diff --git a/src/main/libs/ClientsFactory.js b/src/main/libs/ClientsFactory.js index 54211908..6543eaff 100644 --- a/src/main/libs/ClientsFactory.js +++ b/src/main/libs/ClientsFactory.js @@ -1,6 +1,7 @@ 'use strict'; import { MySQLClient } from './clients/MySQLClient'; import { PostgreSQLClient } from './clients/PostgreSQLClient'; +import { SQLiteClient } from './clients/SQLiteClient'; const queryLogger = sql => { // Remove comments, newlines and multiple spaces @@ -37,6 +38,8 @@ export class ClientsFactory { return new MySQLClient(args); case 'pg': return new PostgreSQLClient(args); + case 'sqlite': + return new SQLiteClient(args); default: throw new Error(`Unknown database client: ${args.client}`); } diff --git a/src/main/libs/clients/MySQLClient.js b/src/main/libs/clients/MySQLClient.js index 1fce40f9..d31e1be6 100644 --- a/src/main/libs/clients/MySQLClient.js +++ b/src/main/libs/clients/MySQLClient.js @@ -9,6 +9,8 @@ export class MySQLClient extends AntaresCore { super(args); this._schema = null; + this._runningConnections = new Map(); + this._connectionsToCommit = new Map(); this.types = { 0: 'DECIMAL', @@ -100,9 +102,11 @@ export class MySQLClient extends AntaresCore { } /** + * + * @returns dbConfig * @memberof MySQLClient */ - async connect () { + async getDbConfig () { delete this._params.application_name; const dbConfig = { @@ -133,20 +137,17 @@ export class MySQLClient extends AntaresCore { } } + return dbConfig; + } + + /** + * @memberof MySQLClient + */ + async connect () { if (!this._poolSize) - this._connection = await mysql.createConnection(dbConfig); - else { - this._connection = mysql.createPool({ - ...dbConfig, - connectionLimit: this._poolSize, - typeCast: (field, next) => { - if (field.type === 'DATETIME') - return field.string(); - else - return next(); - } - }); - } + this._connection = await this.getConnection(); + else + this._connection = await this.getConnectionPool(); } /** @@ -157,6 +158,64 @@ export class MySQLClient extends AntaresCore { if (this._ssh) this._ssh.close(); } + async getConnection () { + const dbConfig = await this.getDbConfig(); + const connection = await mysql.createConnection({ + ...dbConfig, + typeCast: (field, next) => { + if (field.type === 'DATETIME') + return field.string(); + else + return next(); + } + }); + + // ANSI_QUOTES check + const [res] = await connection.query('SHOW GLOBAL VARIABLES LIKE \'%sql_mode%\''); + const sqlMode = res[0]?.Variable_name?.split(','); + const hasAnsiQuotes = sqlMode.includes('ANSI_QUOTES'); + + if (this._params.readonly) + await connection.query('SET SESSION TRANSACTION READ ONLY'); + + if (hasAnsiQuotes) + await connection.query(`SET SESSION sql_mode = "${sqlMode.filter(m => m !== 'ANSI_QUOTES').join(',')}"`); + + return connection; + } + + async getConnectionPool () { + const dbConfig = await this.getDbConfig(); + const connection = mysql.createPool({ + ...dbConfig, + connectionLimit: this._poolSize, + typeCast: (field, next) => { + if (field.type === 'DATETIME') + return field.string(); + else + return next(); + } + }); + + // ANSI_QUOTES check + const [res] = await connection.query('SHOW GLOBAL VARIABLES LIKE \'%sql_mode%\''); + const sqlMode = res[0]?.Variable_name?.split(','); + const hasAnsiQuotes = sqlMode.includes('ANSI_QUOTES'); + + if (hasAnsiQuotes) + await connection.query(`SET SESSION sql_mode = "${sqlMode.filter(m => m !== 'ANSI_QUOTES').join(',')}"`); + + connection.on('connection', conn => { + if (this._params.readonly) + conn.query('SET SESSION TRANSACTION READ ONLY'); + + if (hasAnsiQuotes) + conn.query(`SET SESSION sql_mode = "${sqlMode.filter(m => m !== 'ANSI_QUOTES').join(',')}"`); + }); + + return connection; + } + /** * Executes an USE query * @@ -187,6 +246,7 @@ export class MySQLClient extends AntaresCore { const tablesArr = []; const triggersArr = []; + let schemaSize = 0; for (const db of filteredDatabases) { if (!schemas.has(db.Database)) continue; @@ -224,6 +284,9 @@ export class MySQLClient extends AntaresCore { break; } + const tableSize = table.Data_length + table.Index_length; + schemaSize += tableSize; + return { name: table.Name, type: tableType, @@ -232,7 +295,7 @@ export class MySQLClient extends AntaresCore { updated: table.Update_time, engine: table.Engine, comment: table.Comment, - size: table.Data_length + table.Index_length, + size: tableSize, autoIncrement: table.Auto_increment, collation: table.Collation }; @@ -276,7 +339,7 @@ export class MySQLClient extends AntaresCore { body: scheduler.EVENT_BODY, starts: scheduler.STARTS, ends: scheduler.ENDS, - status: scheduler.STATUS, + enabled: scheduler.STATUS === 'ENABLED', executeAt: scheduler.EXECUTE_AT, intervalField: scheduler.INTERVAL_FIELD, intervalValue: scheduler.INTERVAL_VALUE, @@ -309,6 +372,7 @@ export class MySQLClient extends AntaresCore { return { name: db.Database, + size: schemaSize, tables: remappedTables, functions: remappedFunctions, procedures: remappedProcedures, @@ -319,6 +383,7 @@ export class MySQLClient extends AntaresCore { else { return { name: db.Database, + size: 0, tables: [], functions: [], procedures: [], @@ -360,7 +425,7 @@ export class MySQLClient extends AntaresCore { return acc; }, '') .replaceAll('\n', '') - .split(',') + .split(/,\s?(?![^(]*\))/) .map(f => { try { const fieldArr = f.trim().split(' '); @@ -400,18 +465,25 @@ export class MySQLClient extends AntaresCore { return rows.map(field => { let numLength = field.COLUMN_TYPE.match(/int\(([^)]+)\)/); - numLength = numLength ? +numLength.pop() : null; + numLength = numLength ? +numLength.pop() : field.NUMERIC_PRECISION || null; const enumValues = /(enum|set)/.test(field.COLUMN_TYPE) ? field.COLUMN_TYPE.match(/\(([^)]+)\)/)[0].slice(1, -1) : null; + const defaultValue = (remappedFields && remappedFields[field.COLUMN_NAME]) + ? remappedFields[field.COLUMN_NAME].default + : field.COLUMN_DEFAULT; + return { name: field.COLUMN_NAME, key: field.COLUMN_KEY.toLowerCase(), - type: remappedFields ? remappedFields[field.COLUMN_NAME].type : field.DATA_TYPE, + type: (remappedFields && remappedFields[field.COLUMN_NAME]) + ? remappedFields[field.COLUMN_NAME].type + : field.DATA_TYPE.toUpperCase(), schema: field.TABLE_SCHEMA, table: field.TABLE_NAME, numPrecision: field.NUMERIC_PRECISION, + numScale: field.NUMERIC_SCALE, numLength, enumValues, datePrecision: field.DATETIME_PRECISION, @@ -420,11 +492,13 @@ export class MySQLClient extends AntaresCore { unsigned: field.COLUMN_TYPE.includes('unsigned'), zerofill: field.COLUMN_TYPE.includes('zerofill'), order: field.ORDINAL_POSITION, - default: remappedFields ? remappedFields[field.COLUMN_NAME].default : field.COLUMN_DEFAULT, + default: defaultValue, charset: field.CHARACTER_SET_NAME, collation: field.COLLATION_NAME, autoIncrement: field.EXTRA.includes('auto_increment'), - onUpdate: field.EXTRA.toLowerCase().includes('on update') ? field.EXTRA.replace('on update', '') : '', + onUpdate: field.EXTRA.toLowerCase().includes('on update') + ? field.EXTRA.substr(field.EXTRA.indexOf('on update') + 9, field.EXTRA.length).trim() + : '', comment: field.COLUMN_COMMENT }; }); @@ -565,7 +639,7 @@ export class MySQLClient extends AntaresCore { /** * CREATE DATABASE * - * @returns {Array.} parameters + * @returns {Promise} * @memberof MySQLClient */ async createSchema (params) { @@ -575,7 +649,7 @@ export class MySQLClient extends AntaresCore { /** * ALTER DATABASE * - * @returns {Array.} parameters + * @returns {Promise} * @memberof MySQLClient */ async alterSchema (params) { @@ -585,7 +659,7 @@ export class MySQLClient extends AntaresCore { /** * DROP DATABASE * - * @returns {Array.} parameters + * @returns {Promise} * @memberof MySQLClient */ async dropSchema (params) { @@ -625,7 +699,7 @@ export class MySQLClient extends AntaresCore { /** * DROP VIEW * - * @returns {Array.} parameters + * @returns {Promise} * @memberof MySQLClient */ async dropView (params) { @@ -636,7 +710,7 @@ export class MySQLClient extends AntaresCore { /** * ALTER VIEW * - * @returns {Array.} parameters + * @returns {Promise} * @memberof MySQLClient */ async alterView (params) { @@ -657,7 +731,7 @@ export class MySQLClient extends AntaresCore { /** * CREATE VIEW * - * @returns {Array.} parameters + * @returns {Promise} * @memberof MySQLClient */ async createView (params) { @@ -690,7 +764,7 @@ export class MySQLClient extends AntaresCore { /** * DROP TRIGGER * - * @returns {Array.} parameters + * @returns {Promise} * @memberof MySQLClient */ async dropTrigger (params) { @@ -701,7 +775,7 @@ export class MySQLClient extends AntaresCore { /** * ALTER TRIGGER * - * @returns {Array.} parameters + * @returns {Promise} * @memberof MySQLClient */ async alterTrigger (params) { @@ -723,7 +797,7 @@ export class MySQLClient extends AntaresCore { /** * CREATE TRIGGER * - * @returns {Array.} parameters + * @returns {Promise} * @memberof MySQLClient */ async createTrigger (params) { @@ -797,7 +871,7 @@ export class MySQLClient extends AntaresCore { /** * DROP PROCEDURE * - * @returns {Array.} parameters + * @returns {Promise} * @memberof MySQLClient */ async dropRoutine (params) { @@ -808,7 +882,7 @@ export class MySQLClient extends AntaresCore { /** * ALTER PROCEDURE * - * @returns {Array.} parameters + * @returns {Promise} * @memberof MySQLClient */ async alterRoutine (params) { @@ -830,7 +904,7 @@ export class MySQLClient extends AntaresCore { /** * CREATE PROCEDURE * - * @returns {Array.} parameters + * @returns {Promise} * @memberof MySQLClient */ async createRoutine (params) { @@ -924,7 +998,7 @@ export class MySQLClient extends AntaresCore { /** * DROP FUNCTION * - * @returns {Array.} parameters + * @returns {Promise} * @memberof MySQLClient */ async dropFunction (params) { @@ -935,7 +1009,7 @@ export class MySQLClient extends AntaresCore { /** * ALTER FUNCTION * - * @returns {Array.} parameters + * @returns {Promise} * @memberof MySQLClient */ async alterFunction (params) { @@ -957,7 +1031,7 @@ export class MySQLClient extends AntaresCore { /** * CREATE FUNCTION * - * @returns {Array.} parameters + * @returns {Promise} * @memberof MySQLClient */ async createFunction (params) { @@ -1018,7 +1092,7 @@ export class MySQLClient extends AntaresCore { /** * DROP EVENT * - * @returns {Array.} parameters + * @returns {Promise} * @memberof MySQLClient */ async dropEvent (params) { @@ -1029,7 +1103,7 @@ export class MySQLClient extends AntaresCore { /** * ALTER EVENT * - * @returns {Array.} parameters + * @returns {Promise} * @memberof MySQLClient */ async alterEvent (params) { @@ -1055,7 +1129,7 @@ export class MySQLClient extends AntaresCore { /** * CREATE EVENT * - * @returns {Array.} parameters + * @returns {Promise} * @memberof MySQLClient */ async createEvent (params) { @@ -1072,6 +1146,16 @@ export class MySQLClient extends AntaresCore { return await this.raw(sql, { split: false }); } + async enableEvent ({ schema, scheduler }) { + const sql = `ALTER EVENT \`${schema}\`.\`${scheduler}\` ENABLE`; + return await this.raw(sql, { split: false }); + } + + async disableEvent ({ schema, scheduler }) { + const sql = `ALTER EVENT \`${schema}\`.\`${scheduler}\` DISABLE`; + return await this.raw(sql, { split: false }); + } + /** * SHOW COLLATION * @@ -1111,6 +1195,26 @@ export class MySQLClient extends AntaresCore { }); } + /** + * SHOW VARIABLES LIKE %variable% + * + * @param {String} variable + * @param {'global'|'session'|null} level + * @returns {Object} variable + * @memberof MySQLClient + */ + async getVariable (variable, level) { + const sql = `SHOW${level ? ' ' + level.toUpperCase() : ''} VARIABLES LIKE '%${variable}%'`; + const results = await this.raw(sql); + + if (results.rows.length) { + return { + name: results.rows[0].Variable_name, + value: results.rows[0].Value + }; + } + } + /** * SHOW ENGINES * @@ -1182,14 +1286,60 @@ export class MySQLClient extends AntaresCore { }); } + /** + * + * @param {number} id + * @returns {Promise} + */ async killProcess (id) { return await this.raw(`KILL ${id}`); } + /** + * + * @param {string} tabUid + * @returns {Promise} + */ + async killTabQuery (tabUid) { + const id = this._runningConnections.get(tabUid); + if (id) + return await this.killProcess(id); + } + + /** + * + * @param {string} tabUid + * @returns {Promise} + */ + async commitTab (tabUid) { + const connection = this._connectionsToCommit.get(tabUid); + if (connection) + return await connection.query('COMMIT'); + } + + /** + * + * @param {string} tabUid + * @returns {Promise} + */ + async rollbackTab (tabUid) { + const connection = this._connectionsToCommit.get(tabUid); + if (connection) + return await connection.query('ROLLBACK'); + } + + destroyConnectionToCommit (tabUid) { + const connection = this._connectionsToCommit.get(tabUid); + if (connection) { + connection.destroy(); + this._connectionsToCommit.delete(tabUid); + } + } + /** * CREATE TABLE * - * @returns {Array.} parameters + * @returns {Promise} * @memberof MySQLClient */ async createTable (params) { @@ -1212,7 +1362,7 @@ export class MySQLClient extends AntaresCore { const length = typeInfo.length ? field.enumValues || field.numLength || field.charLength || field.datePrecision : false; newColumns.push(`\`${field.name}\` - ${field.type.toUpperCase()}${length ? `(${length})` : ''} + ${field.type.toUpperCase()}${length ? `(${length}${field.numScale ? `,${field.numScale}` : ''})` : ''} ${field.unsigned ? 'UNSIGNED' : ''} ${field.zerofill ? 'ZEROFILL' : ''} ${field.nullable ? 'NULL' : 'NOT NULL'} @@ -1251,7 +1401,7 @@ export class MySQLClient extends AntaresCore { /** * ALTER TABLE * - * @returns {Array.} parameters + * @returns {Promise} * @memberof MySQLClient */ async alterTable (params) { @@ -1281,7 +1431,7 @@ export class MySQLClient extends AntaresCore { const length = typeInfo.length ? addition.enumValues || addition.numLength || addition.charLength || addition.datePrecision : false; alterColumns.push(`ADD COLUMN \`${addition.name}\` - ${addition.type.toUpperCase()}${length ? `(${length})` : ''} + ${addition.type.toUpperCase()}${length ? `(${length}${addition.numScale ? `,${addition.numScale}` : ''})` : ''} ${addition.unsigned ? 'UNSIGNED' : ''} ${addition.zerofill ? 'ZEROFILL' : ''} ${addition.nullable ? 'NULL' : 'NOT NULL'} @@ -1319,7 +1469,7 @@ export class MySQLClient extends AntaresCore { const length = typeInfo.length ? change.enumValues || change.numLength || change.charLength || change.datePrecision : false; alterColumns.push(`CHANGE COLUMN \`${change.orgName}\` \`${change.name}\` - ${change.type.toUpperCase()}${length ? `(${length})` : ''} + ${change.type.toUpperCase()}${length ? `(${length}${change.numScale ? `,${change.numScale}` : ''})` : ''} ${change.unsigned ? 'UNSIGNED' : ''} ${change.zerofill ? 'ZEROFILL' : ''} ${change.nullable ? 'NULL' : 'NOT NULL'} @@ -1386,7 +1536,7 @@ export class MySQLClient extends AntaresCore { /** * DUPLICATE TABLE * - * @returns {Array.} parameters + * @returns {Promise} * @memberof MySQLClient */ async duplicateTable (params) { @@ -1397,7 +1547,7 @@ export class MySQLClient extends AntaresCore { /** * TRUNCATE TABLE * - * @returns {Array.} parameters + * @returns {Promise} * @memberof MySQLClient */ async truncateTable (params) { @@ -1408,7 +1558,7 @@ export class MySQLClient extends AntaresCore { /** * DROP TABLE * - * @returns {Array.} parameters + * @returns {Promise} * @memberof MySQLClient */ async dropTable (params) { @@ -1490,6 +1640,7 @@ export class MySQLClient extends AntaresCore { details: false, split: true, comments: true, + autocommit: true, ...args }; @@ -1504,8 +1655,24 @@ export class MySQLClient extends AntaresCore { .filter(Boolean) .map(q => q.trim()) : [sql]; + + let connection; const isPool = typeof this._connection.getConnection === 'function'; - const connection = isPool ? await this._connection.getConnection() : this._connection; + + if (!args.autocommit && args.tabUid) { // autocommit OFF + if (this._connectionsToCommit.has(args.tabUid)) + connection = this._connectionsToCommit.get(args.tabUid); + else { + connection = await this.getConnection(); + await connection.query('SET SESSION autocommit=0'); + this._connectionsToCommit.set(args.tabUid, connection); + } + } + else// autocommit ON + connection = isPool ? await this._connection.getConnection() : this._connection; + + if (args.tabUid && isPool) + this._runningConnections.set(args.tabUid, connection.connection.connectionId); if (args.schema) await connection.query(`USE \`${args.schema}\``); @@ -1567,7 +1734,10 @@ export class MySQLClient extends AntaresCore { }); } catch (err) { - if (isPool) connection.release(); + if (isPool && args.autocommit) { + connection.release(); + this._runningConnections.delete(args.tabUid); + } reject(err); } @@ -1576,7 +1746,10 @@ export class MySQLClient extends AntaresCore { keysArr = keysArr ? [...keysArr, ...response] : response; } catch (err) { - if (isPool) connection.release(); + if (isPool && args.autocommit) { + connection.release(); + this._runningConnections.delete(args.tabUid); + } reject(err); } } @@ -1591,7 +1764,10 @@ export class MySQLClient extends AntaresCore { keys: keysArr }); }).catch((err) => { - if (isPool) connection.release(); + if (isPool && args.autocommit) { + connection.release(); + this._runningConnections.delete(args.tabUid); + } reject(err); }); }); @@ -1599,7 +1775,10 @@ export class MySQLClient extends AntaresCore { resultsArr.push({ rows, report, fields, keys, duration }); } - if (isPool) connection.release(); + if (isPool && args.autocommit) { + connection.release(); + this._runningConnections.delete(args.tabUid); + } return resultsArr.length === 1 ? resultsArr[0] : resultsArr; } diff --git a/src/main/libs/clients/PostgreSQLClient.js b/src/main/libs/clients/PostgreSQLClient.js index 7d9125e7..af9a4a56 100644 --- a/src/main/libs/clients/PostgreSQLClient.js +++ b/src/main/libs/clients/PostgreSQLClient.js @@ -9,6 +9,7 @@ function pgToString (value) { return value.toString(); } +types.setTypeParser(20, a => parseInt(a));// bigint string to number types.setTypeParser(1082, pgToString); // date types.setTypeParser(1083, pgToString); // time types.setTypeParser(1114, pgToString); // timestamp @@ -20,6 +21,8 @@ export class PostgreSQLClient extends AntaresCore { super(args); this._schema = null; + this._runningConnections = new Map(); + this._connectionsToCommit = new Map(); this.types = {}; for (const key in types.builtins) @@ -69,9 +72,11 @@ export class PostgreSQLClient extends AntaresCore { } /** + * + * @returns dbConfig * @memberof PostgreSQLClient */ - async connect () { + async getDbConfig () { const dbConfig = { host: this._params.host, port: this._params.port, @@ -100,15 +105,43 @@ export class PostgreSQLClient extends AntaresCore { } } - if (!this._poolSize) { - const client = new Client(dbConfig); - await client.connect(); - this._connection = client; - } - else { - const pool = new Pool({ ...dbConfig, max: this._poolSize }); - this._connection = pool; + return dbConfig; + } + + /** + * @memberof PostgreSQLClient + */ + async connect () { + if (!this._poolSize) + this._connection = await this.getConnection(); + else + this._connection = await this.getConnectionPool(); + } + + async getConnection () { + const dbConfig = await this.getDbConfig(); + const client = new Client(dbConfig); + await client.connect(); + const connection = client; + + if (this._params.readonly) + await connection.query('SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY'); + + return connection; + } + + async getConnectionPool () { + const dbConfig = await this.getDbConfig(); + const pool = new Pool({ ...dbConfig, max: this._poolSize }); + const connection = pool; + + if (this._params.readonly) { + connection.on('connect', conn => { + conn.query('SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY'); + }); } + + return connection; } /** @@ -120,15 +153,23 @@ export class PostgreSQLClient extends AntaresCore { } /** - * Executes an "USE" query + * Executes an 'SET search_path TO "${schema}"' query * * @param {String} schema + * @param {Object?} connection optional * @memberof PostgreSQLClient */ - use (schema) { + use (schema, connection) { this._schema = schema; - if (schema) - return this.raw(`SET search_path TO "${schema}"`); + + if (schema) { + const sql = `SET search_path TO "${schema}"`; + + if (connection === undefined) + return this.raw(sql); + else + return connection.query(sql); + } } /** @@ -143,6 +184,7 @@ export class PostgreSQLClient extends AntaresCore { const tablesArr = []; const triggersArr = []; + let schemaSize = 0; for (const db of databases) { if (!schemas.has(db.database)) continue; @@ -168,19 +210,20 @@ export class PostgreSQLClient extends AntaresCore { } let { rows: triggers } = await this.raw(` - SELECT event_object_schema AS table_schema, - event_object_table AS table_name, - trigger_schema, - trigger_name, - string_agg(event_manipulation, ',') AS event, - action_timing AS activation, - action_condition AS condition, - action_statement AS definition - FROM information_schema.triggers + SELECT + pg_class.relname AS table_name, + pg_trigger.tgname AS trigger_name, + pg_namespace.nspname AS trigger_schema, + (pg_trigger.tgenabled != 'D')::bool AS enabled + FROM pg_trigger + JOIN pg_class ON pg_trigger.tgrelid = pg_class.oid + JOIN pg_namespace ON pg_namespace.oid = pg_class.relnamespace + JOIN information_schema.triggers ON information_schema.triggers.trigger_schema = pg_namespace.nspname + AND information_schema.triggers.event_object_table = pg_class.relname + AND information_schema.triggers.trigger_name = pg_trigger.tgname WHERE trigger_schema = '${db.database}' - GROUP BY 1,2,3,4,6,7,8 - ORDER BY table_schema, - table_name + GROUP BY 1, 2, 3, 4 + ORDER BY table_name `); if (triggers.length) { @@ -196,11 +239,14 @@ export class PostgreSQLClient extends AntaresCore { if (schemas.has(db.database)) { // TABLES const remappedTables = tablesArr.filter(table => table.Db === db.database).map(table => { + const tableSize = +table.data_length + table.index_length; + schemaSize += tableSize; + return { name: table.table_name, type: table.table_type === 'VIEW' ? 'view' : 'table', rows: table.reltuples, - size: +table.data_length + +table.index_length, + size: tableSize, collation: table.Collation, comment: table.comment, engine: '' @@ -239,17 +285,16 @@ export class PostgreSQLClient extends AntaresCore { return { name: `${trigger.table_name}.${trigger.trigger_name}`, orgName: trigger.trigger_name, - timing: trigger.activation, definer: '', - definition: trigger.definition, - event: trigger.event, table: trigger.table_name, - sqlMode: '' + sqlMode: '', + enabled: trigger.enabled }; }); return { name: db.database, + size: schemaSize, tables: remappedTables, functions: remappedFunctions, procedures: remappedProcedures, @@ -261,6 +306,7 @@ export class PostgreSQLClient extends AntaresCore { else { return { name: db.database, + size: 0, tables: [], functions: [], procedures: [], @@ -302,6 +348,7 @@ export class PostgreSQLClient extends AntaresCore { isArray, schema: field.table_schema, table: field.table_name, + numScale: field.numeric_scale, numPrecision: field.numeric_precision, datePrecision: field.datetime_precision, charLength: field.character_maximum_length, @@ -500,7 +547,7 @@ export class PostgreSQLClient extends AntaresCore { /** * CREATE SCHEMA * - * @returns {Array.} parameters + * @returns {Promise} * @memberof MySQLClient */ async createSchema (params) { @@ -510,7 +557,7 @@ export class PostgreSQLClient extends AntaresCore { /** * ALTER DATABASE * - * @returns {Array.} parameters + * @returns {Promise} * @memberof MySQLClient */ async alterSchema (params) { @@ -520,7 +567,7 @@ export class PostgreSQLClient extends AntaresCore { /** * DROP DATABASE * - * @returns {Array.} parameters + * @returns {Promise} * @memberof MySQLClient */ async dropSchema (params) { @@ -552,7 +599,7 @@ export class PostgreSQLClient extends AntaresCore { /** * DROP VIEW * - * @returns {Array.} parameters + * @returns {Promise} * @memberof PostgreSQLClient */ async dropView (params) { @@ -563,7 +610,7 @@ export class PostgreSQLClient extends AntaresCore { /** * ALTER VIEW * - * @returns {Array.} parameters + * @returns {Promise} * @memberof PostgreSQLClient */ async alterView (params) { @@ -579,7 +626,7 @@ export class PostgreSQLClient extends AntaresCore { /** * CREATE VIEW * - * @returns {Array.} parameters + * @returns {Promise} * @memberof PostgreSQLClient */ async createView (params) { @@ -597,19 +644,25 @@ export class PostgreSQLClient extends AntaresCore { const [table, triggerName] = trigger.split('.'); const results = await this.raw(` - SELECT event_object_schema AS table_schema, - event_object_table AS table_name, - trigger_schema, - trigger_name, - string_agg(event_manipulation, ',') AS event, + SELECT + information_schema.triggers.event_object_schema AS table_schema, + information_schema.triggers.event_object_table AS table_name, + information_schema.triggers.trigger_schema, + information_schema.triggers.trigger_name, + string_agg(event_manipulation, ',') AS EVENT, action_timing AS activation, action_condition AS condition, - action_statement AS definition - FROM information_schema.triggers + action_statement AS definition, + (pg_trigger.tgenabled != 'D')::bool AS enabled + FROM pg_trigger + JOIN pg_class ON pg_trigger.tgrelid = pg_class.oid + JOIN pg_namespace ON pg_namespace.oid = pg_class.relnamespace + JOIN information_schema.triggers ON pg_namespace.nspname = information_schema.triggers.trigger_schema + AND pg_class.relname = information_schema.triggers.event_object_table WHERE trigger_schema = '${schema}' AND trigger_name = '${triggerName}' AND event_object_table = '${table}' - GROUP BY 1,2,3,4,6,7,8 + GROUP BY 1,2,3,4,6,7,8,9 ORDER BY table_schema, table_name `); @@ -619,7 +672,7 @@ export class PostgreSQLClient extends AntaresCore { sql: row.definition, name: row.trigger_name, table: row.table_name, - event: row.event.split(','), + event: [...new Set(row.event.split(','))], activation: row.activation }; })[0]; @@ -628,7 +681,7 @@ export class PostgreSQLClient extends AntaresCore { /** * DROP TRIGGER * - * @returns {Array.} parameters + * @returns {Promise} * @memberof PostgreSQLClient */ async dropTrigger (params) { @@ -640,7 +693,7 @@ export class PostgreSQLClient extends AntaresCore { /** * ALTER TRIGGER * - * @returns {Array.} parameters + * @returns {Promise} * @memberof PostgreSQLClient */ async alterTrigger (params) { @@ -662,7 +715,7 @@ export class PostgreSQLClient extends AntaresCore { /** * CREATE TRIGGER * - * @returns {Array.} parameters + * @returns {Promise} * @memberof PostgreSQLClient */ async createTrigger (params) { @@ -671,6 +724,18 @@ export class PostgreSQLClient extends AntaresCore { return await this.raw(sql, { split: false }); } + async enableTrigger ({ schema, trigger }) { + const [table, triggerName] = trigger.split('.'); + const sql = `ALTER TABLE "${schema}"."${table}" ENABLE TRIGGER "${triggerName}"`; + return await this.raw(sql, { split: false }); + } + + async disableTrigger ({ schema, trigger }) { + const [table, triggerName] = trigger.split('.'); + const sql = `ALTER TABLE "${schema}"."${table}" DISABLE TRIGGER "${triggerName}"`; + return await this.raw(sql, { split: false }); + } + /** * SHOW CREATE PROCEDURE * @@ -1055,14 +1120,64 @@ export class PostgreSQLClient extends AntaresCore { }); } + /** + * + * @param {number} id + * @returns {Promise} + */ async killProcess (id) { return await this.raw(`SELECT pg_terminate_backend(${id})`); } + /** + * + * @param {string} tabUid + * @returns {Promise} + */ + async killTabQuery (tabUid) { + const id = this._runningConnections.get(tabUid); + if (id) + return await this.raw(`SELECT pg_cancel_backend(${id})`); + } + + /** + * + * @param {string} tabUid + * @returns {Promise} + */ + async commitTab (tabUid) { + const connection = this._connectionsToCommit.get(tabUid); + if (connection) { + await connection.query('COMMIT'); + return this.destroyConnectionToCommit(tabUid); + } + } + + /** + * + * @param {string} tabUid + * @returns {Promise} + */ + async rollbackTab (tabUid) { + const connection = this._connectionsToCommit.get(tabUid); + if (connection) { + await connection.query('ROLLBACK'); + return this.destroyConnectionToCommit(tabUid); + } + } + + destroyConnectionToCommit (tabUid) { + const connection = this._connectionsToCommit.get(tabUid); + if (connection) { + connection.end(); + this._connectionsToCommit.delete(tabUid); + } + } + /** * CREATE TABLE * - * @returns {Array.} parameters + * @returns {Promise} * @memberof PostgreSQLClient */ async createTable (params) { @@ -1086,7 +1201,7 @@ export class PostgreSQLClient extends AntaresCore { const length = typeInfo.length ? field.enumValues || field.numLength || field.charLength || field.datePrecision : false; newColumns.push(`"${field.name}" - ${field.type.toUpperCase()}${length ? `(${length})` : ''} + ${field.type.toUpperCase()}${length ? `(${length}${field.numScale !== null ? `,${field.numScale}` : ''})` : ''} ${field.unsigned ? 'UNSIGNED' : ''} ${field.zerofill ? 'ZEROFILL' : ''} ${field.nullable ? 'NULL' : 'NOT NULL'} @@ -1120,7 +1235,7 @@ export class PostgreSQLClient extends AntaresCore { /** * ALTER TABLE * - * @returns {Array.} parameters + * @returns {Promise} * @memberof PostgreSQLClient */ async alterTable (params) { @@ -1150,7 +1265,7 @@ export class PostgreSQLClient extends AntaresCore { const length = typeInfo.length ? addition.numLength || addition.charLength || addition.datePrecision : false; alterColumns.push(`ADD COLUMN "${addition.name}" - ${addition.type.toUpperCase()}${length ? `(${length})` : ''}${addition.isArray ? '[]' : ''} + ${addition.type.toUpperCase()}${length ? `(${length}${addition.numScale !== null ? `,${addition.numScale}` : ''})` : ''}${addition.isArray ? '[]' : ''} ${addition.unsigned ? 'UNSIGNED' : ''} ${addition.zerofill ? 'ZEROFILL' : ''} ${addition.nullable ? 'NULL' : 'NOT NULL'} @@ -1196,7 +1311,7 @@ export class PostgreSQLClient extends AntaresCore { localType = change.type.toLowerCase(); } - alterColumns.push(`ALTER COLUMN "${change.name}" TYPE ${localType}${length ? `(${length})` : ''}${change.isArray ? '[]' : ''} USING "${change.name}"::${localType}`); + alterColumns.push(`ALTER COLUMN "${change.name}" TYPE ${localType}${length ? `(${length}${change.numScale ? `,${change.numScale}` : ''})` : ''}${change.isArray ? '[]' : ''} USING "${change.name}"::${localType}`); alterColumns.push(`ALTER COLUMN "${change.name}" ${change.nullable ? 'DROP NOT NULL' : 'SET NOT NULL'}`); alterColumns.push(`ALTER COLUMN "${change.name}" ${change.default ? `SET DEFAULT ${change.default}` : 'DROP DEFAULT'}`); @@ -1266,7 +1381,7 @@ export class PostgreSQLClient extends AntaresCore { /** * DUPLICATE TABLE * - * @returns {Array.} parameters + * @returns {Promise} * @memberof PostgreSQLClient */ async duplicateTable (params) { @@ -1277,7 +1392,7 @@ export class PostgreSQLClient extends AntaresCore { /** * TRUNCATE TABLE * - * @returns {Array.} parameters + * @returns {Promise} * @memberof PostgreSQLClient */ async truncateTable (params) { @@ -1288,7 +1403,7 @@ export class PostgreSQLClient extends AntaresCore { /** * DROP TABLE * - * @returns {Array.} parameters + * @returns {Promise} * @memberof PostgreSQLClient */ async dropTable (params) { @@ -1363,17 +1478,17 @@ export class PostgreSQLClient extends AntaresCore { * @memberof PostgreSQLClient */ async raw (sql, args) { + if (process.env.NODE_ENV === 'development') this._logger(sql);// TODO: replace BLOB content with a placeholder + args = { nest: false, details: false, split: true, comments: true, + autocommit: true, ...args }; - if (args.schema && args.schema !== 'public') - await this.use(args.schema); - if (!args.comments) sql = sql.replace(/(\/\*(.|[\r\n])*?\*\/)|(--(.*|[\r\n]))/gm, '');// Remove comments @@ -1385,7 +1500,26 @@ export class PostgreSQLClient extends AntaresCore { .map(q => q.trim()) : [sql]; - if (process.env.NODE_ENV === 'development') this._logger(sql);// TODO: replace BLOB content with a placeholder + let connection; + const isPool = this._connection instanceof Pool; + + if (!args.autocommit && args.tabUid) { // autocommit OFF + if (this._connectionsToCommit.has(args.tabUid)) + connection = this._connectionsToCommit.get(args.tabUid); + else { + connection = await this.getConnection(); + await connection.query('START TRANSACTION'); + this._connectionsToCommit.set(args.tabUid, connection); + } + } + else// autocommit ON + connection = isPool ? await this._connection.connect() : this._connection; + + if (args.tabUid && isPool) + this._runningConnections.set(args.tabUid, connection.processID); + + if (args.schema && args.schema !== 'public') + await this.use(args.schema, connection); for (const query of queries) { if (!query) continue; @@ -1395,15 +1529,12 @@ export class PostgreSQLClient extends AntaresCore { let keysArr = []; const { rows, report, fields, keys, duration } = await new Promise((resolve, reject) => { - this._connection.query({ - rowMode: args.nest ? 'array' : null, - text: query - }, async (err, res) => { - timeStop = new Date(); + (async () => { + try { + const res = await connection.query({ rowMode: args.nest ? 'array' : null, text: query }); + + timeStop = new Date(); - if (err) - reject(err); - else { let ast; try { @@ -1492,6 +1623,10 @@ export class PostgreSQLClient extends AntaresCore { }); } catch (err) { + if (isPool && args.autocommit) { + connection.release(); + this._runningConnections.delete(args.tabUid); + } reject(err); } @@ -1500,6 +1635,10 @@ export class PostgreSQLClient extends AntaresCore { keysArr = keysArr ? [...keysArr, ...response] : response; } catch (err) { + if (isPool && args.autocommit) { + connection.release(); + this._runningConnections.delete(args.tabUid); + } reject(err); } } @@ -1514,12 +1653,24 @@ export class PostgreSQLClient extends AntaresCore { keys: keysArr }); } - }); + catch (err) { + if (isPool && args.autocommit) { + connection.release(); + this._runningConnections.delete(args.tabUid); + } + reject(err); + } + })(); }); resultsArr.push({ rows, report, fields, keys, duration }); } + if (isPool && args.autocommit) { + connection.release(); + this._runningConnections.delete(args.tabUid); + } + return resultsArr.length === 1 ? resultsArr[0] : resultsArr; } } diff --git a/src/main/libs/clients/SQLiteClient.js b/src/main/libs/clients/SQLiteClient.js new file mode 100644 index 00000000..1323434e --- /dev/null +++ b/src/main/libs/clients/SQLiteClient.js @@ -0,0 +1,859 @@ +'use strict'; +import sqlite from 'better-sqlite3'; +import { AntaresCore } from '../AntaresCore'; +import dataTypes from 'common/data-types/sqlite'; +import { NUMBER, FLOAT, TIME, DATETIME } from 'common/fieldTypes'; + +export class SQLiteClient extends AntaresCore { + constructor (args) { + super(args); + + this._schema = null; + this._connectionsToCommit = new Map(); + } + + _getTypeInfo (type) { + return dataTypes + .reduce((acc, group) => [...acc, ...group.types], []) + .filter(_type => _type.name === type.toUpperCase())[0]; + } + + /** + * @memberof SQLiteClient + */ + async connect () { + this._connection = this.getConnection(); + } + + getConnection () { + return sqlite(this._params.databasePath, { + fileMustExist: true, + readonly: this._params.readonly + }); + } + + /** + * @memberof SQLiteClient + */ + destroy () {} + + /** + * Executes an USE query + * + * @memberof SQLiteClient + */ + use () {} + + /** + * @param {Array} schemas list + * @returns {Array.} databases scructure + * @memberof SQLiteClient + */ + async getStructure (schemas) { + const { rows: databases } = await this.raw('SELECT * FROM pragma_database_list'); + + const filteredDatabases = databases; + + const tablesArr = []; + const triggersArr = []; + let schemaSize = 0; + + for (const db of filteredDatabases) { + if (!schemas.has(db.name)) continue; + + let { rows: tables } = await this.raw(` + SELECT * + FROM "${db.name}".sqlite_master + WHERE type IN ('table', 'view') + AND name NOT LIKE 'sqlite_%' + ORDER BY name + `); + if (tables.length) { + tables = tables.map(table => { + table.Db = db.name; + return table; + }); + tablesArr.push(...tables); + } + + let { rows: triggers } = await this.raw(`SELECT * FROM "${db.name}".sqlite_master WHERE type='trigger'`); + if (triggers.length) { + triggers = triggers.map(trigger => { + trigger.Db = db.name; + return trigger; + }); + triggersArr.push(...triggers); + } + } + + return filteredDatabases.map(db => { + if (schemas.has(db.name)) { + // TABLES + const remappedTables = tablesArr.filter(table => table.Db === db.name).map(table => { + const tableSize = 0; + schemaSize += tableSize; + + return { + name: table.name, + type: table.type, + rows: false, + size: false + }; + }); + + // TRIGGERS + const remappedTriggers = triggersArr.filter(trigger => trigger.Db === db.name).map(trigger => { + return { + name: trigger.name, + table: trigger.tbl_name + }; + }); + + return { + name: db.name, + size: schemaSize, + tables: remappedTables, + functions: [], + procedures: [], + triggers: remappedTriggers, + schedulers: [] + }; + } + else { + return { + name: db.name, + size: 0, + tables: [], + functions: [], + procedures: [], + triggers: [], + schedulers: [] + }; + } + }); + } + + /** + * @param {Object} params + * @param {String} params.schema + * @param {String} params.table + * @returns {Object} table scructure + * @memberof SQLiteClient + */ + async getTableColumns ({ schema, table }) { + const { rows: fields } = await this.raw(`SELECT * FROM "${schema}".pragma_table_info('${table}')`); + + return fields.map(field => { + const [type, length] = field.type.includes('(') + ? field.type.replace(')', '').split('(').map(el => { + if (!isNaN(el)) el = +el; + return el; + }) + : [field.type, null]; + + return { + name: field.name, + key: null, + type: type.trim(), + schema: schema, + table: table, + numPrecision: [...NUMBER, ...FLOAT].includes(type) ? length : null, + datePrecision: null, + charLength: ![...NUMBER, ...FLOAT].includes(type) ? length : null, + nullable: !field.notnull, + unsigned: null, + zerofill: null, + order: field.cid + 1, + default: field.dflt_value, + charset: null, + collation: null, + autoIncrement: false, + onUpdate: null, + comment: '' + }; + }); + } + + /** + * @param {Object} params + * @param {String} params.schema + * @param {String} params.table + * @returns {Object} table row count + * @memberof SQLiteClient + */ + async getTableApproximateCount ({ schema, table }) { + const { rows } = await this.raw(`SELECT COUNT(*) AS count FROM "${schema}"."${table}"`); + + return rows.length ? rows[0].count : 0; + } + + /** + * @param {Object} params + * @param {String} params.schema + * @param {String} params.table + * @returns {Object} table options + * @memberof SQLiteClient + */ + async getTableOptions ({ schema, table }) { + return { name: table }; + } + + /** + * @param {Object} params + * @param {String} params.schema + * @param {String} params.table + * @returns {Object} table indexes + * @memberof SQLiteClient + */ + async getTableIndexes ({ schema, table }) { + const remappedIndexes = []; + const { rows: primaryKeys } = await this.raw(`SELECT * FROM "${schema}".pragma_table_info('${table}') WHERE pk != 0`); + + for (const key of primaryKeys) { + remappedIndexes.push({ + name: 'PRIMARY', + column: key.name, + indexType: null, + type: 'PRIMARY', + cardinality: null, + comment: '', + indexComment: '' + }); + } + + const { rows: indexes } = await this.raw(`SELECT * FROM "${schema}".pragma_index_list('${table}');`); + + for (const index of indexes) { + const { rows: details } = await this.raw(`SELECT * FROM "${schema}".pragma_index_info('${index.name}');`); + + for (const detail of details) { + remappedIndexes.push({ + name: index.name, + column: detail.name, + indexType: null, + type: index.unique === 1 ? 'UNIQUE' : 'INDEX', + cardinality: null, + comment: '', + indexComment: '' + }); + } + } + + return remappedIndexes; + } + + /** + * @param {Object} params + * @param {String} params.schema + * @param {String} params.table + * @returns {Object} table key usage + * @memberof SQLiteClient + */ + async getKeyUsage ({ schema, table }) { + const { rows } = await this.raw(`SELECT * FROM "${schema}".pragma_foreign_key_list('${table}');`); + + return rows.map(field => { + return { + schema: schema, + table: table, + field: field.from, + position: field.id + 1, + constraintPosition: null, + constraintName: field.id, + refSchema: schema, + refTable: field.table, + refField: field.to, + onUpdate: field.on_update, + onDelete: field.on_delete + }; + }); + } + + async getUsers () {} + + /** + * SHOW CREATE VIEW + * + * @returns {Array.} view informations + * @memberof SQLiteClient + */ + async getViewInformations ({ schema, view }) { + const sql = `SELECT "sql" FROM "${schema}".sqlite_master WHERE "type"='view' AND name='${view}'`; + const results = await this.raw(sql); + + return results.rows.map(row => { + return { + sql: row.sql.match(/(?<=AS ).*?$/gs)[0], + name: view + }; + })[0]; + } + + /** + * DROP VIEW + * + * @returns {Array.} parameters + * @memberof SQLiteClient + */ + async dropView (params) { + const sql = `DROP VIEW "${params.schema}"."${params.view}"`; + return await this.raw(sql); + } + + /** + * ALTER VIEW + * + * @returns {Array.} parameters + * @memberof SQLiteClient + */ + async alterView (params) { + const { view } = params; + try { + await this.dropView({ schema: view.schema, view: view.oldName }); + await this.createView(view); + } + catch (err) { + return Promise.reject(err); + } + } + + /** + * CREATE VIEW + * + * @returns {Array.} parameters + * @memberof SQLiteClient + */ + async createView (params) { + const sql = `CREATE VIEW "${params.schema}"."${params.name}" AS ${params.sql}`; + return await this.raw(sql); + } + + /** + * SHOW CREATE TRIGGER + * + * @returns {Array.} view informations + * @memberof SQLiteClient + */ + async getTriggerInformations ({ schema, trigger }) { + const sql = `SELECT "sql" FROM "${schema}".sqlite_master WHERE "type"='trigger' AND name='${trigger}'`; + const results = await this.raw(sql); + + return results.rows.map(row => { + return { + sql: row.sql.match(/(BEGIN|begin)(.*)(END|end)/gs)[0], + name: trigger, + table: row.sql.match(/(?<=ON `).*?(?=`)/gs)[0], + activation: row.sql.match(/(BEFORE|AFTER)/gs)[0], + event: row.sql.match(/(INSERT|UPDATE|DELETE)/gs)[0] + }; + })[0]; + } + + /** + * DROP TRIGGER + * + * @returns {Array.} parameters + * @memberof SQLiteClient + */ + async dropTrigger (params) { + const sql = `DROP TRIGGER \`${params.schema}\`.\`${params.trigger}\``; + return await this.raw(sql); + } + + /** + * ALTER TRIGGER + * + * @returns {Array.} parameters + * @memberof SQLiteClient + */ + async alterTrigger (params) { + const { trigger } = params; + const tempTrigger = Object.assign({}, trigger); + tempTrigger.name = `Antares_${tempTrigger.name}_tmp`; + + try { + await this.createTrigger(tempTrigger); + await this.dropTrigger({ schema: trigger.schema, trigger: tempTrigger.name }); + await this.dropTrigger({ schema: trigger.schema, trigger: trigger.oldName }); + await this.createTrigger(trigger); + } + catch (err) { + return Promise.reject(err); + } + } + + /** + * CREATE TRIGGER + * + * @returns {Array.} parameters + * @memberof SQLiteClient + */ + async createTrigger (params) { + const sql = `CREATE ${params.definer ? `DEFINER=${params.definer} ` : ''}TRIGGER \`${params.schema}\`.\`${params.name}\` ${params.activation} ${params.event} ON \`${params.table}\` FOR EACH ROW ${params.sql}`; + return await this.raw(sql, { split: false }); + } + + /** + * SHOW COLLATION + * + * @returns {Array.} collations list + * @memberof SQLiteClient + */ + async getCollations () { + return []; + } + + /** + * SHOW VARIABLES + * + * @returns {Array.} variables list + * @memberof SQLiteClient + */ + async getVariables () { + return []; + } + + /** + * SHOW ENGINES + * + * @returns {Array.} engines list + * @memberof SQLiteClient + */ + async getEngines () { + return { + name: 'SQLite', + support: 'YES', + comment: '', + isDefault: true + }; + } + + /** + * SHOW VARIABLES LIKE '%vers%' + * + * @returns {Array.} version parameters + * @memberof SQLiteClient + */ + async getVersion () { + const os = require('os'); + const sql = 'SELECT sqlite_version() AS version'; + const { rows } = await this.raw(sql); + + return { + number: rows[0].version, + name: 'SQLite', + arch: process.arch, + os: `${os.type()} ${os.release()}` + }; + } + + async getProcesses () {} + + async killProcess () {} + + /** + * + * @param {string} tabUid + * @returns {Promise} + */ + async commitTab (tabUid) { + const connection = this._connectionsToCommit.get(tabUid); + if (connection) { + connection.prepare('COMMIT').run(); + return this.destroyConnectionToCommit(tabUid); + } + } + + /** + * + * @param {string} tabUid + * @returns {Promise} + */ + async rollbackTab (tabUid) { + const connection = this._connectionsToCommit.get(tabUid); + if (connection) { + connection.prepare('ROLLBACK').run(); + return this.destroyConnectionToCommit(tabUid); + } + } + + destroyConnectionToCommit (tabUid) { + const connection = this._connectionsToCommit.get(tabUid); + if (connection) { + connection.close(); + this._connectionsToCommit.delete(tabUid); + } + } + + /** + * CREATE TABLE + * + * @returns {Promise} + * @memberof SQLiteClient + */ + async createTable (params) { + const { + schema, + fields, + foreigns, + indexes, + options + } = params; + const newColumns = []; + const newIndexes = []; + const manageIndexes = []; + const newForeigns = []; + + let sql = `CREATE TABLE "${schema}"."${options.name}"`; + + // ADD FIELDS + fields.forEach(field => { + const typeInfo = this._getTypeInfo(field.type); + const length = typeInfo?.length ? field.enumValues || field.numLength || field.charLength || field.datePrecision : false; + + newColumns.push(`"${field.name}" + ${field.type.toUpperCase()}${length && length !== true ? `(${length})` : ''} + ${field.unsigned ? 'UNSIGNED' : ''} + ${field.nullable ? 'NULL' : 'NOT NULL'} + ${field.autoIncrement ? 'AUTO_INCREMENT' : ''} + ${field.default ? `DEFAULT ${field.default}` : ''} + ${field.onUpdate ? `ON UPDATE ${field.onUpdate}` : ''}`); + }); + + // ADD INDEX + indexes.forEach(index => { + const fields = index.fields.map(field => `"${field}"`).join(','); + const type = index.type; + + if (type === 'PRIMARY') + newIndexes.push(`PRIMARY KEY (${fields})`); + else + manageIndexes.push(`CREATE ${type === 'UNIQUE' ? type : ''} INDEX "${index.name}" ON "${options.name}" (${fields})`); + }); + + // ADD FOREIGN KEYS + foreigns.forEach(foreign => { + newForeigns.push(`CONSTRAINT "${foreign.constraintName}" FOREIGN KEY ("${foreign.field}") REFERENCES "${foreign.refTable}" ("${foreign.refField}") ON UPDATE ${foreign.onUpdate} ON DELETE ${foreign.onDelete}`); + }); + + sql = `${sql} (${[...newColumns, ...newIndexes, ...newForeigns].join(', ')})`; + if (manageIndexes.length) sql = `${sql}; ${manageIndexes.join(';')}`; + + return await this.raw(sql); + } + + /** + * ALTER TABLE + * + * @returns {Promise} + * @memberof SQLiteClient + */ + async alterTable (params) { + try { + await this.raw('BEGIN TRANSACTION'); + await this.raw('PRAGMA foreign_keys = 0'); + + const tmpName = `Antares_${params.table}_tmp`; + await this.raw(`CREATE TABLE "${tmpName}" AS SELECT * FROM "${params.table}"`); + await this.dropTable(params); + + const createTableParams = { + schema: params.schema, + fields: params.tableStructure.fields, + foreigns: params.tableStructure.foreigns, + indexes: params.tableStructure.indexes.filter(index => !index.name.includes('sqlite_autoindex')), + options: { name: params.tableStructure.name } + }; + await this.createTable(createTableParams); + const insertFields = createTableParams.fields + .filter(field => { + return ( + params.additions.every(add => add.name !== field.name) && + params.deletions.every(del => del.name !== field.name) + ); + }) + .reduce((acc, curr) => { + acc.push(`"${curr.name}"`); + return acc; + }, []); + + const selectFields = insertFields.map(field => { + const renamedField = params.changes.find(change => `"${change.name}"` === field); + if (renamedField) + return `"${renamedField.orgName}"`; + return field; + }); + + await this.raw(`INSERT INTO "${createTableParams.options.name}" (${insertFields.join(',')}) SELECT ${selectFields.join(',')} FROM "${tmpName}"`); + + await this.dropTable({ schema: params.schema, table: tmpName }); + await this.raw('PRAGMA foreign_keys = 1'); + await this.raw('COMMIT'); + } + catch (err) { + await this.raw('ROLLBACK'); + return Promise.reject(err); + } + } + + /** + * DUPLICATE TABLE + * + * @returns {Promise} + * @memberof SQLiteClient + */ + async duplicateTable (params) { // TODO: retrive table informations and create a copy + const sql = `CREATE TABLE "${params.schema}"."${params.table}_copy" AS SELECT * FROM "${params.schema}"."${params.table}"`; + return await this.raw(sql); + } + + /** + * TRUNCATE TABLE + * + * @returns {Promise} + * @memberof SQLiteClient + */ + async truncateTable (params) { + const sql = `DELETE FROM "${params.schema}"."${params.table}"`; + return await this.raw(sql); + } + + /** + * DROP TABLE + * + * @returns {Promise} + * @memberof SQLiteClient + */ + async dropTable (params) { + const sql = `DROP TABLE "${params.schema}"."${params.table}"`; + return await this.raw(sql); + } + + /** + * @returns {String} SQL string + * @memberof SQLiteClient + */ + getSQL () { + // SELECT + const selectArray = this._query.select.reduce(this._reducer, []); + let selectRaw = ''; + + if (selectArray.length) + selectRaw = selectArray.length ? `SELECT ${selectArray.join(', ')} ` : 'SELECT * '; + + // FROM + let fromRaw = ''; + + if (!this._query.update.length && !Object.keys(this._query.insert).length && !!this._query.from) + fromRaw = 'FROM'; + else if (Object.keys(this._query.insert).length) + fromRaw = 'INTO'; + + fromRaw += this._query.from ? ` ${this._query.schema ? `"${this._query.schema}".` : ''}"${this._query.from}" ` : ''; + + // WHERE + const whereArray = this._query.where + .reduce(this._reducer, []) + ?.map(clausole => clausole.replace('= null', 'IS NULL')); + const whereRaw = whereArray.length ? `WHERE ${whereArray.join(' AND ')} ` : ''; + + // UPDATE + const updateArray = this._query.update.reduce(this._reducer, []); + const updateRaw = updateArray.length ? `SET ${updateArray.join(', ')} ` : ''; + + // INSERT + let insertRaw = ''; + + if (this._query.insert.length) { + const fieldsList = Object.keys(this._query.insert[0]); + const rowsList = this._query.insert.map(el => `(${Object.values(el).join(', ')})`); + + insertRaw = `(${fieldsList.join(', ')}) VALUES ${rowsList.join(', ')} `; + } + + // GROUP BY + const groupByArray = this._query.groupBy.reduce(this._reducer, []); + const groupByRaw = groupByArray.length ? `GROUP BY ${groupByArray.join(', ')} ` : ''; + + // ORDER BY + const orderByArray = this._query.orderBy.reduce(this._reducer, []); + const orderByRaw = orderByArray.length ? `ORDER BY ${orderByArray.join(', ')} ` : ''; + + // LIMIT + const limitRaw = this._query.limit.length ? `LIMIT ${this._query.limit.join(', ')} ` : ''; + + // OFFSET + const offsetRaw = this._query.offset.length ? `OFFSET ${this._query.offset.join(', ')} ` : ''; + + return `${selectRaw}${updateRaw ? 'UPDATE' : ''}${insertRaw ? 'INSERT ' : ''}${this._query.delete ? 'DELETE ' : ''}${fromRaw}${updateRaw}${whereRaw}${groupByRaw}${orderByRaw}${limitRaw}${offsetRaw}${insertRaw}`; + } + + /** + * @param {string} sql raw SQL query + * @param {object} args + * @param {boolean} args.nest + * @param {boolean} args.details + * @param {boolean} args.split + * @returns {Promise} + * @memberof SQLiteClient + */ + async raw (sql, args) { + if (process.env.NODE_ENV === 'development') this._logger(sql);// TODO: replace BLOB content with a placeholder + + args = { + nest: false, + details: false, + split: true, + comments: true, + autocommit: true, + ...args + }; + + if (!args.comments) + sql = sql.replace(/(\/\*(.|[\r\n])*?\*\/)|(--(.*|[\r\n]))/gm, '');// Remove comments + + const resultsArr = []; + let paramsArr = []; + const queries = args.split + ? sql.split(/((?:[^;'"]*(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*')[^;'"]*)+)|;/gm) + .filter(Boolean) + .map(q => q.trim()) + : [sql]; + + let connection; + + if (!args.autocommit && args.tabUid) { // autocommit OFF + if (this._connectionsToCommit.has(args.tabUid)) + connection = this._connectionsToCommit.get(args.tabUid); + else { + connection = this.getConnection(); + connection.prepare('BEGIN TRANSACTION').run(); + this._connectionsToCommit.set(args.tabUid, connection); + } + } + else// autocommit ON + connection = this._connection; + + for (const query of queries) { + if (!query) continue; + const timeStart = new Date(); + let timeStop; + const keysArr = []; + + const { rows, report, fields, keys, duration } = await new Promise((resolve, reject) => { + (async () => { + let queryResult; + let affectedRows; + let fields; + const detectedTypes = {}; + + try { + const stmt = connection.prepare(query); + + if (stmt.reader) { + queryResult = stmt.all(); + fields = stmt.columns(); + + if (queryResult.length) { + fields.forEach(field => { + detectedTypes[field.name] = typeof queryResult[0][field.name]; + }); + } + } + else { + const info = queryResult = stmt.run(); + affectedRows = info.changes; + } + } + catch (err) { + reject(err); + } + + timeStop = new Date(); + + let remappedFields = fields + ? fields.map(field => { + let [parsedType, length] = field.type?.includes('(') + ? field.type.replace(')', '').split('(').map(el => { + if (!isNaN(el)) + el = +el; + else + el = el.trim(); + return el; + }) + : [field.type, null]; + + if ([...TIME, ...DATETIME].includes(parsedType)) { + const firstNotNull = queryResult.find(res => res[field.name] !== null); + if (firstNotNull && firstNotNull[field.name].includes('.')) + length = firstNotNull[field.name].split('.').pop().length; + } + + return { + name: field.name, + alias: field.name, + orgName: field.column, + schema: field.database, + table: field.table, + tableAlias: field.table, + orgTable: field.table, + type: field.type !== null ? parsedType : detectedTypes[field.name], + length + }; + }).filter(Boolean) + : []; + + if (args.details) { + paramsArr = remappedFields.map(field => { + return { + table: field.table, + schema: field.schema + }; + }).filter((val, i, arr) => arr.findIndex(el => el.schema === val.schema && el.table === val.table) === i); + + for (const paramObj of paramsArr) { + if (!paramObj.table || !paramObj.schema) continue; + + try { + const indexes = await this.getTableIndexes(paramObj); + + remappedFields = remappedFields.map(field => { + // const detailedField = columns.find(f => f.name === field.name); + const fieldIndex = indexes.find(i => i.column === field.name); + if (field.table === paramObj.table && field.schema === paramObj.schema) { + // if (detailedField) { + // const length = detailedField.numPrecision || detailedField.charLength || detailedField.datePrecision || null; + // field = { ...field, ...detailedField, length }; + // } + + if (fieldIndex) { + const key = fieldIndex.type === 'PRIMARY' ? 'pri' : fieldIndex.type === 'UNIQUE' ? 'uni' : 'mul'; + field = { ...field, key }; + }; + } + + return field; + }); + } + catch (err) { + reject(err); + } + } + } + + resolve({ + duration: timeStop - timeStart, + rows: Array.isArray(queryResult) ? queryResult.some(el => Array.isArray(el)) ? [] : queryResult : false, + report: affectedRows !== undefined ? { affectedRows } : null, + fields: remappedFields, + keys: keysArr + }); + })(); + }); + + resultsArr.push({ rows, report, fields, keys, duration }); + } + + return resultsArr.length === 1 ? resultsArr[0] : resultsArr; + } +} diff --git a/src/main/main.js b/src/main/main.js index 570f5b54..87c8ca67 100644 --- a/src/main/main.js +++ b/src/main/main.js @@ -3,6 +3,7 @@ import { app, BrowserWindow, /* session, */ nativeImage, Menu } from 'electron'; import * as path from 'path'; import Store from 'electron-store'; +import * as windowStateKeeper from 'electron-window-state'; import * as remoteMain from '@electron/remote/main'; import ipcHandlers from './ipc-handlers'; @@ -18,15 +19,18 @@ process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 'true'; // global reference to mainWindow (necessary to prevent window from being garbage collected) let mainWindow; +let mainWindowState; async function createMainWindow () { const icon = require('../renderer/images/logo-32.png'); const window = new BrowserWindow({ - width: 1024, - height: 800, + width: mainWindowState.width, + height: mainWindowState.height, + x: mainWindowState.x, + y: mainWindowState.y, minWidth: 900, minHeight: 550, - title: 'Antares', + title: 'Antares SQL', autoHideMenuBar: true, icon: nativeImage.createFromDataURL(icon.default), webPreferences: { @@ -41,6 +45,9 @@ async function createMainWindow () { backgroundColor: '#1d1d1d' }); + mainWindowState.manage(window); + window.on('moved', saveWindowState); + remoteMain.enable(window.webContents); try { @@ -70,16 +77,10 @@ async function createMainWindow () { } window.on('closed', () => { + window.removeListener('moved', saveWindowState); mainWindow = null; }); - window.webContents.on('devtools-opened', () => { - window.focus(); - setImmediate(() => { - window.focus(); - }); - }); - return window; } @@ -104,17 +105,22 @@ else { // create main BrowserWindow when electron is ready app.on('ready', async () => { + mainWindowState = windowStateKeeper({ + defaultWidth: 1024, + defaultHeight: 800 + }); + mainWindow = await createMainWindow(); createAppMenu(); - if (isDevelopment) - mainWindow.webContents.openDevTools(); + // if (isDevelopment) + // mainWindow.webContents.openDevTools(); - process.on('uncaughtException', (error) => { + process.on('uncaughtException', error => { mainWindow.webContents.send('unhandled-exception', error); }); - process.on('unhandledRejection', (error) => { + process.on('unhandledRejection', error => { mainWindow.webContents.send('unhandled-exception', error); }); }); @@ -160,3 +166,7 @@ function createAppMenu () { Menu.setApplicationMenu(menu); } + +function saveWindowState () { + mainWindowState.saveState(mainWindow); +} diff --git a/src/renderer/components/BaseMap.vue b/src/renderer/components/BaseMap.vue new file mode 100644 index 00000000..c1cbb836 --- /dev/null +++ b/src/renderer/components/BaseMap.vue @@ -0,0 +1,108 @@ + + + + diff --git a/src/renderer/components/FakerSelect.vue b/src/renderer/components/FakerSelect.vue index 53e7a6b9..fc1627b0 100644 --- a/src/renderer/components/FakerSelect.vue +++ b/src/renderer/components/FakerSelect.vue @@ -89,7 +89,7 @@ :type="inputProps().type" :disabled="!isChecked" > -