Compare commits
463 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 342119c953 | |||
| 33e223eea1 | |||
| 2015b330be | |||
| 902267f5eb | |||
| bf315c53ac | |||
| 96afe1b0d6 | |||
| c571582ed8 | |||
| 50a22b63d0 | |||
| 04902c2d9f | |||
| a25af4714e | |||
| c8e46f774e | |||
| 7eff7c649c | |||
| 5bb9f181d8 | |||
| 2231bc21cd | |||
| 30cbcd5ce0 | |||
| 223dd0b22f | |||
| 0eabd81909 | |||
| cd3a3033d7 | |||
| a3a9d00267 | |||
| d0795502e6 | |||
| 1908cb1210 | |||
| 0f1cc85423 | |||
| 1bfa004e65 | |||
| ff6d56cf6a | |||
| 901130f3bd | |||
| 78f770de7d | |||
| e3aa4d54ef | |||
| c50a4cdeff | |||
| 16fb7a926d | |||
| 77878e4ad1 | |||
| 4ee66a55c6 | |||
| 6a41c780f4 | |||
| 3b4a3b9ef7 | |||
| f3625aaf71 | |||
| beef215394 | |||
| b551db8774 | |||
| 1546893cb6 | |||
| 20ff374438 | |||
| 111702881a | |||
| 17efc3d32c | |||
| c93594d8ca | |||
| 1a06cd2d22 | |||
| cdfbd73cc5 | |||
| 00c98b10b9 | |||
| d81826214f | |||
| 95b86f437e | |||
| 99876e3158 | |||
| 97215e31e9 | |||
| a1bf7ddb50 | |||
| 5ae33a320c | |||
| 2ef81942db | |||
| 571be4b3f9 | |||
| 12c9e638f5 | |||
| 2df98c610a | |||
| f59c6c9fe5 | |||
| 5b4e69fa0a | |||
| aad39227b7 | |||
| 2c2b4fc08f | |||
| 8910a6bd07 | |||
| 3b15d315ec | |||
| 1f8021833b | |||
| e2ce2f7057 | |||
| 17a3fec158 | |||
| 8ef8685e4e | |||
| 1c8e69225d | |||
| e7634a2521 | |||
| a03682fc8a | |||
| d185fe8a8c | |||
| 7e82e83faa | |||
| f85460cce8 | |||
| 34fce0ef58 | |||
| 7699cca6b9 | |||
| da5b96c0a8 | |||
| b503ea9a02 | |||
| c6c04b87e1 | |||
| 065af0b4a8 | |||
| f92153d957 | |||
| d60f052897 | |||
| f664771643 | |||
| 402ac726f3 | |||
| 8363a887bc | |||
| 760f5c9616 | |||
| ba74f98015 | |||
| b055fff6e0 | |||
| ed17b425a3 | |||
| 87fe62005d | |||
| fce2f9a46a | |||
| bb86a3b8cc | |||
| 77c5192798 | |||
| e5b47baee3 | |||
| 81fbed7a7f | |||
| 90262a0318 | |||
| 72633a0c0d | |||
| 246c54f863 | |||
| 68fd49c224 | |||
| d179f4c753 | |||
| 80624d3abe | |||
| 02aa94afc9 | |||
| 1b853dd3b3 | |||
| e715a95cc0 | |||
| 3ca0810756 | |||
| af4d354ff4 | |||
| ca1c7cc556 | |||
| cb2a0b2492 | |||
| 185699cb51 | |||
| ce85f8f94d | |||
| 39748bdd6c | |||
| dcaf8351b5 | |||
| 2fa48b1138 | |||
| 02ce6b0204 | |||
| af75858ce8 | |||
| 3760217ff0 | |||
| 624ada2873 | |||
| e7c64265ae | |||
| f47c83fece | |||
| 82601dea24 | |||
| da4f465ed3 | |||
| a5f39da8ae | |||
| 016e96d0e6 | |||
| 37c305461f | |||
| 9f204bf187 | |||
| 1c1aedcefe | |||
| 268bdcd50c | |||
| 8f05f4acb3 | |||
| aafa52db17 | |||
| 275c57631b | |||
| 61ec9e4365 | |||
| d348ef21db | |||
| b659136f64 | |||
| e568adc825 | |||
| eec1840e8a | |||
| a0de1b1171 | |||
| f6c90578a9 | |||
| 3776c00377 | |||
| f572c05a32 | |||
| 3895c6bb47 | |||
| 2e03056a15 | |||
| eaa5970a0f | |||
| e79e19c614 | |||
| 0ef5ac04d8 | |||
| 2cb3a6b446 | |||
| d75397d793 | |||
| 5b58ed9c26 | |||
| f4c39bbf3c | |||
| e2ce349a30 | |||
| 04a6540890 | |||
| b3b7d021c5 | |||
| b396c8f820 | |||
| 8cc8f9f0b9 | |||
| 3bbe06a55b | |||
| dfe37496f2 | |||
| 3fe13f0443 | |||
| a5b5f36298 | |||
| b9e2e51bd7 | |||
| 10e63f3e77 | |||
| 820044b489 | |||
| 89c904abc1 | |||
| c5a3ee01ee | |||
| a5cc99005a | |||
| 77ebf0051c | |||
| d5cee7b35b | |||
| 3ac96c4ae4 | |||
| 60545674c5 | |||
| b5c313e517 | |||
| e3bdad6d77 | |||
| 7453afa684 | |||
| 86eca6bc7e | |||
| 3c0bc69662 | |||
| 2baf9a1446 | |||
| 296038a3de | |||
| 71e1ea5736 | |||
| 32d05edb6a | |||
| ba608ff438 | |||
| 5d79b687d5 | |||
| ad1ec70e94 | |||
| 7e73f81d4e | |||
| 9a0ae06c87 | |||
| 902fbbf29a | |||
| a0df43484a | |||
| d2317ab908 | |||
| fa733e2285 | |||
| 134737d4a8 | |||
| def3141b6d | |||
| 057afe2bd5 | |||
| 40203f2823 | |||
| 9d711e45f9 | |||
| 35eb5716a5 | |||
| 7a10b85b4c | |||
| 7a2e86246d | |||
| 331b275105 | |||
| 3791fd568c | |||
| 6ed9bb0258 | |||
| e66f2fcd2d | |||
| 1250f5d25a | |||
| 4a3ef70979 | |||
| bf29ee430e | |||
| c7f1e75d22 | |||
| 0886712714 | |||
| f415c8bfe9 | |||
| 4c1ac0757c | |||
| 67a793038b | |||
| 05a65dab3c | |||
| 0d61e43431 | |||
| b6195603e8 | |||
| 6db306cb0c | |||
| 48140348e0 | |||
| 989574bb52 | |||
| 8f3c479642 | |||
| 4db464772e | |||
| 039d3b4058 | |||
| b99e3ed177 | |||
| 6519ba95bc | |||
| 6f22932b16 | |||
| bf725dd563 | |||
| dea6700a25 | |||
| b8ccae570e | |||
| 17fc6ccc2e | |||
| 112f310d13 | |||
| 8874589ed0 | |||
| f7621ae336 | |||
| b4cc211763 | |||
| 38263de9f1 | |||
| 260e3c50df | |||
| 57327623d1 | |||
| ff9f1d85ab | |||
| 661775d5af | |||
| dd1088d02d | |||
| 8d265ad6d2 | |||
| 346c530f76 | |||
| 870e3ad666 | |||
| e31ff5960c | |||
| 3fa71cc94a | |||
| f5ea87da7b | |||
| 643695bd2b | |||
| 697a9438c6 | |||
| 3ad665f80b | |||
| 7847eaa64d | |||
| bb870ec90f | |||
| 9959e61b35 | |||
| 92ead26873 | |||
| afb27ec989 | |||
| ff30b6511c | |||
| 56306aeaec | |||
| c2c354044a | |||
| ceb216df8c | |||
| b2994ede8c | |||
| 0b87c5085c | |||
| 2305d086cc | |||
| 6c3c1b377e | |||
| 07c4c89720 | |||
| fd30c40c5f | |||
| 1e59407954 | |||
| d10dc960c5 | |||
| 8f19ce2607 | |||
| af7c450f56 | |||
| 7467dd2f10 | |||
| 40440e957a | |||
| 26ff3f45f8 | |||
| 4d529e7e3f | |||
| 27311afb31 | |||
| 50b90e181a | |||
| 87efb92f07 | |||
| 6b8bf8161e | |||
| 6362e2137b | |||
| f6c8588573 | |||
| 2a47f60987 | |||
| bebdf3f43b | |||
| 2a34a3b48b | |||
| 9dbf720b64 | |||
| 05c8001c19 | |||
| ddcb7711d4 | |||
| 0c48a5ee09 | |||
| a76e742ce6 | |||
| 64c7072aca | |||
| 5d661ad3a3 | |||
| e8524d3fff | |||
| 352b443a3e | |||
| 623fda7e6e | |||
| 42e446bc36 | |||
| 36e37dcec9 | |||
| 875011a6ea | |||
| e544c12945 | |||
| f7b15cc17e | |||
| 202873be71 | |||
| fcbca318ef | |||
| fe055d4b70 | |||
| e480e08e0e | |||
| eb78481d70 | |||
| 912a9d5b51 | |||
| 78a59ae8dc | |||
| 433d3be8d5 | |||
| 35fc2e0f5b | |||
| 93edcc4d0a | |||
| 0a06ebf9c3 | |||
| 94804957e5 | |||
| fcaac322f2 | |||
| 5fa9a303cb | |||
| 5fa3f39f69 | |||
| b13f01d3b3 | |||
| b85334cb2d | |||
| 188c2b7a9c | |||
| b63987dbfc | |||
| 09bf18eeca | |||
| e5ec444e52 | |||
| e2c313af66 | |||
| 73d8ad36ad | |||
| 8896260df7 | |||
| e8433423b4 | |||
| 9af28e7d20 | |||
| d67637c7e8 | |||
| d058003cc9 | |||
| 1ccdf7f07e | |||
| 0d7d0bdd60 | |||
| d7e7b97fb8 | |||
| f360ba7187 | |||
| 84a67be272 | |||
| 54ddfe2ef9 | |||
| 3953f764a5 | |||
| 7ef7d9d2af | |||
| f45969f5d3 | |||
| 9dbe73d9c3 | |||
| 35a48fb3d5 | |||
| dd1cfa677f | |||
| cb2f6e6a78 | |||
| 55ad3a05e4 | |||
| e6a6534887 | |||
| 5fca73d531 | |||
| 081dff38e3 | |||
| 30f291c525 | |||
| dab9d33394 | |||
| e74521fdab | |||
| a5ad8dc5f6 | |||
| 28c554a75b | |||
| 41e55f329c | |||
| 73d3d00e9d | |||
| 54fec7fd6d | |||
| 0413075af6 | |||
| c733ac9c02 | |||
| 1970ec29c5 | |||
| 08fc3ffce4 | |||
| b057fcfb3e | |||
| 67504a9481 | |||
| 95cb8c7cb6 | |||
| 183365c461 | |||
| 4cce1f6670 | |||
| 777f72af88 | |||
| dacea78123 | |||
| 44b8a14868 | |||
| aa709dbee3 | |||
| b0c7adf0d1 | |||
| f345677d4f | |||
| cc82df92ae | |||
| b151a13f65 | |||
| 3f7caa6078 | |||
| db9133af51 | |||
| bfc3717dea | |||
| 13f30891b6 | |||
| aaf6715dd1 | |||
| 98b3b242eb | |||
| 843a080fb8 | |||
| f6e2ddbf11 | |||
| 06593a5835 | |||
| b2b6d4ad24 | |||
| c4789bbb9e | |||
| ae8b498300 | |||
| f0475be69a | |||
| db7d02de87 | |||
| 61a2398fb8 | |||
| 60464052e2 | |||
| 16dffba9a9 | |||
| f30b062431 | |||
| fea9c5dd66 | |||
| 0c843ea806 | |||
| decfe3197d | |||
| 6b1243eef5 | |||
| fcb87bbfc3 | |||
| 3f2635a421 | |||
| f70c554966 | |||
| 9abc835d53 | |||
| d5648cc944 | |||
| 44e0902ded | |||
| cc6bcfb4b3 | |||
| 688086e00f | |||
| 09498f2ac3 | |||
| 7dd9e9a9b1 | |||
| 19d135e435 | |||
| d453e52ff3 | |||
| 29a77cc053 | |||
| 11fd2d1d8a | |||
| b5fe8508b1 | |||
| 25881e80db | |||
| e43fa96e34 | |||
| 0200c7c78b | |||
| 42e573a3ae | |||
| 395b0a91b0 | |||
| 62c529cf50 | |||
| 00a169725e | |||
| bcf0bfd5ef | |||
| 19f769480b | |||
| 65eb89de95 | |||
| a6fec38d83 | |||
| cfa4e50084 | |||
| 8e18b4f692 | |||
| 82a9368d01 | |||
| 8db86c7e02 | |||
| b3c0aa1701 | |||
| 627ec520a5 | |||
| 91ae9a92af | |||
| 4c1c251aef | |||
| 9c1179c451 | |||
| 52ed8874e3 | |||
| 319e08f5f3 | |||
| c4491050cd | |||
| f0ea35d576 | |||
| 70d53e8abe | |||
| 8f28ce3659 | |||
| 050b46813f | |||
| 4bae23ecfa | |||
| 6eb16ad750 | |||
| 482a823f4f | |||
| 9d933d669a | |||
| e44a95d723 | |||
| cae882c8d6 | |||
| 026726a6ed | |||
| 70d06deeb0 | |||
| 6dfe9b798b | |||
| 73c14eba6d | |||
| 7c91dda170 | |||
| 40ebedaef0 | |||
| 614f852f71 | |||
| a8e3a6cfec | |||
| be053acf3c | |||
| 91741655b7 | |||
| ef25ea1885 | |||
| 6b6c5b945f | |||
| c57a67da09 | |||
| 2376cb30db | |||
| 3a08462018 | |||
| c95677bd83 | |||
| 8bffa4a7dd | |||
| 6d7cc7d441 | |||
| acc49273c1 | |||
| 640b53e45f | |||
| 7857771056 | |||
| b8513b3ecd | |||
| 0a56e3b782 | |||
| 87e75c6ba1 | |||
| cf5afb43eb | |||
| 2eb1c04fcf | |||
| 4a4c4b41c0 | |||
| 032eaf9eb0 | |||
| 06a028a093 | |||
| 21ceaecec6 | |||
| c5605d63ca | |||
| ca900b0cf0 | |||
| 7c5f274f83 | |||
| 3a396c6555 | |||
| 070ffe0c56 | |||
| f770b011ce | |||
| 0d806be3dc | |||
| 2716db4bf3 | |||
| 2d22fef06b | |||
| c78aefe2ab |
@@ -0,0 +1,12 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: dbgate
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: dbgate
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Electron app
|
||||
name: Electron app BETA
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -12,14 +12,14 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
# os: [ubuntu-18.04, windows-2016]
|
||||
os: [macOS-10.14, windows-2016, ubuntu-18.04]
|
||||
os: [macOS-10.15, windows-2016, ubuntu-18.04]
|
||||
|
||||
steps:
|
||||
- name: Context
|
||||
env:
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: echo "$GITHUB_CONTEXT"
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Use Node.js 10.x
|
||||
@@ -35,10 +35,14 @@ jobs:
|
||||
- name: fillNativeModulesElectron
|
||||
run: |
|
||||
yarn fillNativeModulesElectron
|
||||
- name: fillPackagedPlugins
|
||||
run: |
|
||||
yarn fillPackagedPlugins
|
||||
- name: Install Snapcraft
|
||||
if: matrix.os == 'ubuntu-18.04'
|
||||
uses: samuelmeuli/action-snapcraft@v1
|
||||
- name: Publish
|
||||
if: matrix.os != 'macOS-10.15'
|
||||
run: |
|
||||
yarn run build:app
|
||||
env:
|
||||
@@ -46,6 +50,13 @@ jobs:
|
||||
WIN_CSC_LINK: ${{ secrets.WINCERT_CERTIFICATE }}
|
||||
WIN_CSC_KEY_PASSWORD: ${{ secrets.WINCERT_PASSWORD }}
|
||||
|
||||
- name: Publish Mac
|
||||
if: matrix.os == 'macOS-10.15'
|
||||
run: |
|
||||
yarn run build:app:mac
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }} # token for electron publish
|
||||
|
||||
- name: Save snap login
|
||||
if: matrix.os == 'ubuntu-18.04'
|
||||
run: 'echo "$SNAPCRAFT_LOGIN" > snapcraft.login'
|
||||
@@ -64,10 +75,14 @@ jobs:
|
||||
mkdir artifacts
|
||||
|
||||
cp app/dist/*.deb artifacts/dbgate-beta.deb || true
|
||||
cp app/dist/*.AppImage artifacts/dbgate-beta.AppImage || true
|
||||
cp app/dist/*.exe artifacts/dbgate-beta.exe || true
|
||||
cp app/dist/*windows*.zip artifacts/dbgate-windows-beta.zip || true
|
||||
cp app/dist/*.dmg artifacts/dbgate-beta.dmg || true
|
||||
cp app/dist/*x86*.AppImage artifacts/dbgate-beta.AppImage || true
|
||||
cp app/dist/*arm64*.AppImage artifacts/dbgate-beta-arm64.AppImage || true
|
||||
cp app/dist/*armv7l*.AppImage artifacts/dbgate-beta-armv7l.AppImage || true
|
||||
cp app/dist/*win*.exe artifacts/dbgate-beta.exe || true
|
||||
cp app/dist/*win_x64.zip artifacts/dbgate-windows-beta.zip || true
|
||||
cp app/dist/*win_arm64.zip artifacts/dbgate-windows-beta-arm64.zip || true
|
||||
cp app/dist/*-mac.dmg artifacts/dbgate-beta.dmg || true
|
||||
cp app/dist/*-mac_arm64.dmg artifacts/dbgate-beta-arm64.dmg || true
|
||||
|
||||
mv app/dist/*.exe artifacts/ || true
|
||||
mv app/dist/*.zip artifacts/ || true
|
||||
|
||||
@@ -16,14 +16,14 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
# os: [ubuntu-18.04, windows-2016]
|
||||
os: [macOS-10.14, windows-2016, ubuntu-18.04]
|
||||
os: [macOS-10.15, windows-2016, ubuntu-18.04]
|
||||
|
||||
steps:
|
||||
- name: Context
|
||||
env:
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: echo "$GITHUB_CONTEXT"
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Use Node.js 10.x
|
||||
@@ -39,6 +39,9 @@ jobs:
|
||||
- name: fillNativeModulesElectron
|
||||
run: |
|
||||
yarn fillNativeModulesElectron
|
||||
- name: fillPackagedPlugins
|
||||
run: |
|
||||
yarn fillPackagedPlugins
|
||||
- name: Install Snapcraft
|
||||
if: matrix.os == 'ubuntu-18.04'
|
||||
uses: samuelmeuli/action-snapcraft@v1
|
||||
@@ -72,10 +75,14 @@ jobs:
|
||||
mkdir artifacts
|
||||
|
||||
cp app/dist/*.deb artifacts/dbgate-latest.deb || true
|
||||
cp app/dist/*.AppImage artifacts/dbgate-latest.AppImage || true
|
||||
cp app/dist/*x86*.AppImage artifacts/dbgate-latest.AppImage || true
|
||||
cp app/dist/*arm64*.AppImage artifacts/dbgate-latest-arm64.AppImage || true
|
||||
cp app/dist/*armv7l*.AppImage artifacts/dbgate-latest-armv7l.AppImage || true
|
||||
cp app/dist/*.exe artifacts/dbgate-latest.exe || true
|
||||
cp app/dist/*windows*.zip artifacts/dbgate-windows-latest.zip || true
|
||||
cp app/dist/*.dmg artifacts/dbgate-latest.dmg || true
|
||||
cp app/dist/*win_x64.zip artifacts/dbgate-windows-latest.zip || true
|
||||
cp app/dist/*win_arm64.zip artifacts/dbgate-windows-latest-arm64.zip || true
|
||||
cp app/dist/*-mac.dmg artifacts/dbgate-latest.dmg || true
|
||||
cp app/dist/*-mac_arm64.dmg artifacts/dbgate-latest-arm64.dmg || true
|
||||
|
||||
mv app/dist/*.exe artifacts/ || true
|
||||
mv app/dist/*.zip artifacts/ || true
|
||||
@@ -120,7 +127,7 @@ jobs:
|
||||
mv app/dist/latest-linux.yml artifacts/latest-linux.yml || true
|
||||
|
||||
- name: Copy latest-mac.yml
|
||||
if: matrix.os == 'macOS-10.14'
|
||||
if: matrix.os == 'macOS-10.15'
|
||||
run: |
|
||||
mv app/dist/latest-mac.yml artifacts/latest-mac.yml || true
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Docker image
|
||||
name: Docker image BETA
|
||||
|
||||
# on: [push]
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
env:
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: echo "$GITHUB_CONTEXT"
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Use Node.js 10.x
|
||||
|
||||
@@ -21,14 +21,13 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-18.04]
|
||||
# os: [macOS-10.14, windows-2016, ubuntu-18.04]
|
||||
|
||||
steps:
|
||||
- name: Context
|
||||
env:
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: echo "$GITHUB_CONTEXT"
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Use Node.js 10.x
|
||||
|
||||
@@ -21,14 +21,13 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-18.04]
|
||||
# os: [macOS-10.14, windows-2016, ubuntu-18.04]
|
||||
|
||||
steps:
|
||||
- name: Context
|
||||
env:
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: echo "$GITHUB_CONTEXT"
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Use Node.js 10.x
|
||||
@@ -80,6 +79,11 @@ jobs:
|
||||
run: |
|
||||
npm publish
|
||||
|
||||
- name: Publish query-splitter
|
||||
working-directory: packages/query-splitter
|
||||
run: |
|
||||
npm publish
|
||||
|
||||
- name: Publish web
|
||||
working-directory: packages/web
|
||||
run: |
|
||||
@@ -119,3 +123,8 @@ jobs:
|
||||
working-directory: plugins/dbgate-plugin-postgres
|
||||
run: |
|
||||
npm publish
|
||||
|
||||
- name: Publish dbgate-plugin-sqlite
|
||||
working-directory: plugins/dbgate-plugin-sqlite
|
||||
run: |
|
||||
npm publish
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
name: Run tests
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
|
||||
jobs:
|
||||
test-runner:
|
||||
runs-on: ubuntu-latest
|
||||
container: node:10.18-jessie
|
||||
|
||||
steps:
|
||||
- name: Context
|
||||
env:
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: echo "$GITHUB_CONTEXT"
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: yarn install
|
||||
run: |
|
||||
yarn install
|
||||
- name: Integration tests
|
||||
run: |
|
||||
cd integration-tests
|
||||
yarn test:ci
|
||||
# yarn wait:ci
|
||||
- name: Filter parser tests
|
||||
if: always()
|
||||
run: |
|
||||
cd packages/filterparser
|
||||
yarn test:ci
|
||||
- name: Query spliiter tests
|
||||
if: always()
|
||||
run: |
|
||||
cd packages/query-splitter
|
||||
yarn test:ci
|
||||
- uses: tanmen/jest-reporter@v1
|
||||
if: always()
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
result-file: integration-tests/result.json
|
||||
action-name: Integration tests
|
||||
- uses: tanmen/jest-reporter@v1
|
||||
if: always()
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
result-file: packages/filterparser/result.json
|
||||
action-name: Filter parser test results
|
||||
- uses: tanmen/jest-reporter@v1
|
||||
if: always()
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
result-file: packages/query-splitter/result.json
|
||||
action-name: Query splitter test results
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres
|
||||
env:
|
||||
POSTGRES_PASSWORD: Pwd2020Db
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
mysql:
|
||||
image: mysql:8.0.18
|
||||
env:
|
||||
MYSQL_ROOT_PASSWORD: Pwd2020Db
|
||||
|
||||
mssql:
|
||||
image: mcr.microsoft.com/mssql/server
|
||||
env:
|
||||
ACCEPT_EULA: Y
|
||||
SA_PASSWORD: Pwd2020Db
|
||||
MSSQL_PID: Express
|
||||
|
||||
# cockroachdb:
|
||||
# image: cockroachdb/cockroach
|
||||
@@ -30,4 +30,5 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
app/src/nativeModulesContent.js
|
||||
packages/api/src/nativeModulesContent.js
|
||||
packages/api/src/packagedPluginsContent.js
|
||||
.VSCodeCounter
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"jestrunner.jestCommand": "node_modules/.bin/cross-env DEVMODE=1 LOCALTEST=1 node_modules/.bin/jest"
|
||||
}
|
||||
@@ -1,5 +1,77 @@
|
||||
# ChangeLog
|
||||
|
||||
### 4.3.3
|
||||
- ADDED: Generate SQL from data (#176 - Copy row as INSERT/UPDATE statement)
|
||||
- ADDED: Datagrid keyboard column operations (Ctrl+F - find column, Ctrl+H - hide column) #180
|
||||
- FIXED: Make window remember that it was maximized
|
||||
- FIXED: Fixed lost focus after copy to clipboard and after inserting SQL join
|
||||
|
||||
### 4.3.2
|
||||
- FIXED: Sorted database list in PostgreSQL (#178)
|
||||
- FIXED: Loading stricture of PostgreSQL database, when it contains indexes on expressions (#175)
|
||||
- ADDED: Hotkey Shift+Alt+F for formatting SQL code
|
||||
|
||||
### 4.3.1
|
||||
- FIXED: #173 Using key phrase for SSH key file connection
|
||||
- ADDED: #172 Abiloity to quick search within database names
|
||||
- ADDED: Database search added to command palette (Ctrl+P)
|
||||
- FIXED: #171 fixed PostgreSQL analyser for older versions than 9.3 (matviews don't exist)
|
||||
- ADDED: DELETE cascade option - ability to delete all referenced rows, when deleting rows
|
||||
|
||||
### 4.3.0
|
||||
- ADDED: Table structure editor
|
||||
- ADDED: Index support
|
||||
- ADDED: Unique constraint support
|
||||
- ADDED: Context menu for drop/rename table/columns and for drop view/procedure/function
|
||||
- ADDED: Added support for Windows arm64 platform
|
||||
- FIXED: Search by _id in MongoDB
|
||||
|
||||
### 4.2.6
|
||||
- FIXED: Fixed MongoDB import
|
||||
- ADDED: Configurable thousands separator #136
|
||||
- ADDED: Using case insensitive text search in postgres
|
||||
|
||||
### 4.2.5
|
||||
- FIXED: Fixed crash when using large model on some installations
|
||||
- FIXED: Postgre SQL CREATE function
|
||||
- FIXED: Analysing of MySQL when modifyDate is not known
|
||||
|
||||
### 4.2.4
|
||||
- ADDED: Query history
|
||||
- ADDED: One-click exports in desktop app
|
||||
- ADDED: JSON array export
|
||||
- FIXED: Procedures in PostgreSQL #122
|
||||
- ADDED: Support of materialized views for PostgreSQL #123
|
||||
- ADDED: Integration tests
|
||||
- FIXED: Fixes in DB structure analysis in PostgreSQL, SQLite, MySQL
|
||||
- FIXED: Save data in SQLite, PostgreSQL
|
||||
- CHANGED: Introduced package dbgate-query-splitter, instead of sql-query-identifier and @verycrazydog/mysql-parse
|
||||
|
||||
### 4.2.3
|
||||
- ADDED: ARM builds for MacOS and Linux
|
||||
- ADDED: Filter by columns in form view
|
||||
|
||||
### 4.2.2
|
||||
- CHANGED: Further startup optimalization (approx. 2 times quicker start of electron app)
|
||||
|
||||
### 4.2.1
|
||||
- FIXED: Fixed+optimalized app startup (esp. on Windows)
|
||||
|
||||
### 4.2.0
|
||||
- ADDED: Support of SQLite database
|
||||
- ADDED: Support of Amazon Redshift database
|
||||
- ADDED: Support of CockcroachDB
|
||||
- CHANGED: DB Model is not auto-refreshed by default, refresh could be invoked from statusbar
|
||||
- FIXED: Fixed race conditions on startup
|
||||
- FIXED: Fixed broken style in data grid under strange circumstances
|
||||
- ADDED: Configure connections with commandline arguments #108
|
||||
- CHANGED: Optimalized algorithm of incremental DB model updates
|
||||
- CHANGED: Loading queries from PostgreSQL doesn't need cursors, using streamed query instead
|
||||
- ADDED: Disconnect command
|
||||
- ADDED: Query executed on server has tab marker (formerly it had only "No DB" marker)
|
||||
- ADDED: Horizontal scroll using shift+mouse wheel #113
|
||||
- ADDED: Cosmetic improvements of MariaDB support
|
||||
|
||||
### 4.1.11
|
||||
- FIX: Fixed crash of API process when using SSH tunnel connection (race condition)
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
[](https://snapcraft.io/dbgate)
|
||||
[](https://github.com/prettier/prettier)
|
||||
|
||||
# DbGate - database administration tool
|
||||
# DbGate - database manager
|
||||
|
||||
DbGate modern, fast and easy to use database manager
|
||||
DbGate is modern, fast and easy to use (no)SQL database client
|
||||
|
||||
* Try it online - [demo.dbgate.org](https://demo.dbgate.org) - online demo application
|
||||
* Download application for Windows, Linux or Mac from [dbgate.org](https://dbgate.org/download/)
|
||||
@@ -17,28 +17,41 @@ Supported databases:
|
||||
* PostgreSQL
|
||||
* SQL Server
|
||||
* MongoDB
|
||||
* SQLite
|
||||
* Amazon Redshift
|
||||
* CockroachDB
|
||||
* MariaDB
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
* Connect to Microsoft SQL Server, Postgre SQL, MySQL, MongoDB
|
||||
* Table data editing, with SQL change script preview
|
||||
* Edit table schema, indexes, primary and foreign keys
|
||||
* Light and dark theme
|
||||
* Master/detail views
|
||||
* Query designer
|
||||
* Form view for comfortable work with tables with many columns
|
||||
* JSON view on MongoDB collections
|
||||
* Explore tables, views, procedures, functions, MongoDB collections
|
||||
* SQL editor, execute SQL script, SQL code formatter, SQL code completion, SQL join wizard
|
||||
* SQL editor
|
||||
* execute SQL script
|
||||
* SQL code formatter
|
||||
* SQL code completion
|
||||
* Add SQL LEFT/INNER/RIGHT join utility
|
||||
* Mongo JavaScript editor, execute Mongo script (with NodeJs syntax)
|
||||
* Runs as application for Windows, Linux and Mac. Or in Docker container on server and in web Browser on client.
|
||||
* Import, export from/to CSV, Excel, JSON
|
||||
* Free table editor - quick table data editing (cleanup data after import/before export, prototype tables etc.)
|
||||
* Archives - backup your data in JSON files on local filesystem (or on DbGate server, when using web application)
|
||||
* Light and dark theme
|
||||
* Charts
|
||||
* For detailed info, how to run DbGate in docker container, visit [docker hub](https://hub.docker.com/r/dbgate/dbgate)
|
||||
* Extensible plugin architecture
|
||||
|
||||
## How to contribute
|
||||
Any contributions are welcome. If you want to contribute without coding, consider following:
|
||||
* Create issue, if you find problem in app, or you have idea to new feature. If issue already exists, you could leave comment on it, to prioritise most wanted issues.
|
||||
* Tell your friends about DbGate or share on social networks - when more people will use DbGate, it will grow to be better
|
||||
|
||||
## Why is DbGate different
|
||||
There are many database managers now, so why DbGate?
|
||||
* Works everywhere - Windows, Linux, Mac, Web browser (+mobile web is planned), without compromises in features
|
||||
@@ -52,8 +65,7 @@ There are many database managers now, so why DbGate?
|
||||
* Backend - NodeJs, ExpressJs, socket.io, database connection drivers
|
||||
* JavaScript + TypeScript
|
||||
* App - electron
|
||||
* There is plan to incorporate SQLite to support work with local datasets
|
||||
* Platform independed - will run as web application in single docker container on server, or as application using Electron platform on Linux, Windows and Mac
|
||||
* Platform independent - will run as web application in single docker container on server, or as application using Electron platform on Linux, Windows and Mac
|
||||
|
||||
## Plugins
|
||||
Plugins are standard NPM packages published on [npmjs.com](https://www.npmjs.com).
|
||||
@@ -101,15 +113,3 @@ yarn build:app:local
|
||||
yarn start:app:local
|
||||
```
|
||||
|
||||
## Packages
|
||||
Some dbgate packages can be used also without DbGate. You can find them on [NPM repository](https://www.npmjs.com/search?q=keywords:dbgate)
|
||||
|
||||
* [api](https://github.com/dbgate/dbgate/tree/master/packages/api) - backend, Javascript, ExpressJS [](https://www.npmjs.com/package/dbgate-api)
|
||||
* [datalib](https://github.com/dbgate/dbgate/tree/master/packages/datalib) - TypeScript library for utility classes [](https://www.npmjs.com/package/dbgate-datalib)
|
||||
* [app](https://github.com/dbgate/dbgate/tree/master/app) - application (JavaScript) structure, creating specific queries (JavaScript)
|
||||
* [filterparser](https://github.com/dbgate/dbgate/tree/master/packages/filterparser) - TypeScript library for parsing data filter expressions using parsimmon [](https://www.npmjs.com/package/dbgate-filterparser)
|
||||
* [sqltree](https://github.com/dbgate/dbgate/tree/master/packages/sqltree) - JSON representation of SQL query, functions converting to SQL (TypeScript) [](https://www.npmjs.com/package/dbgate-sqltree)
|
||||
* [types](https://github.com/dbgate/dbgate/tree/master/packages/types) - common TypeScript definitions [](https://www.npmjs.com/package/dbgate-types)
|
||||
* [web](https://github.com/dbgate/dbgate/tree/master/packages/web) - frontend in Svelte (JavaScript) [](https://www.npmjs.com/package/dbgate-web)
|
||||
* [tools](https://github.com/dbgate/dbgate/tree/master/packages/tools) - various tools [](https://www.npmjs.com/package/dbgate-tools)
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
@@ -8,42 +8,54 @@
|
||||
"better-sqlite3-with-prebuilds": "^7.1.8",
|
||||
"electron-log": "^4.3.1",
|
||||
"electron-store": "^5.1.1",
|
||||
"electron-updater": "^4.3.5"
|
||||
"electron-updater": "^4.3.5",
|
||||
"patch-package": "^6.4.7"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dbgate/dbgate.git"
|
||||
},
|
||||
"build": {
|
||||
"artifactName": "${productName}-${version}-${os}_${arch}.${ext}",
|
||||
"appId": "org.dbgate",
|
||||
"mac": {
|
||||
"category": "database",
|
||||
"icon": "icon512.png",
|
||||
"artifactName": "dbgate-mac-${version}.${ext}",
|
||||
"publish": [
|
||||
"github"
|
||||
],
|
||||
"target": {
|
||||
"target": "default",
|
||||
"arch": [
|
||||
"arm64",
|
||||
"x64"
|
||||
]
|
||||
}
|
||||
},
|
||||
"linux": {
|
||||
"target": [
|
||||
"AppImage",
|
||||
"deb",
|
||||
"snap"
|
||||
"snap",
|
||||
{
|
||||
"target": "AppImage",
|
||||
"arch": [
|
||||
"x64",
|
||||
"armv7l",
|
||||
"arm64"
|
||||
]
|
||||
}
|
||||
],
|
||||
"icon": "icon.png",
|
||||
"artifactName": "dbgate-linux-${version}.${ext}",
|
||||
"icon": "icons/",
|
||||
"category": "Development",
|
||||
"synopsis": "Database administration tool for MS SQL, MySQL and PostgreSQL",
|
||||
"synopsis": "Database manager for SQL Server, MySQL, PostgreSQL, MongoDB and SQLite",
|
||||
"publish": [
|
||||
"github"
|
||||
]
|
||||
},
|
||||
"appImage": {
|
||||
"license": "./LICENSE",
|
||||
"category": "Development"
|
||||
},
|
||||
"snap": {
|
||||
"publish": [
|
||||
"github",
|
||||
@@ -56,9 +68,14 @@
|
||||
"win": {
|
||||
"target": [
|
||||
"nsis",
|
||||
"zip"
|
||||
{
|
||||
"target": "zip",
|
||||
"arch": [
|
||||
"x64",
|
||||
"arm64"
|
||||
]
|
||||
}
|
||||
],
|
||||
"artifactName": "dbgate-windows-${version}.${ext}",
|
||||
"icon": "icon.ico",
|
||||
"publish": [
|
||||
"github"
|
||||
@@ -77,8 +94,9 @@
|
||||
"start:local": "cross-env electron .",
|
||||
"dist": "electron-builder",
|
||||
"build": "cd ../packages/api && yarn build && cd ../web && yarn build && cd ../../app && yarn dist",
|
||||
"build:mac": "cd ../packages/api && yarn build && cd ../web && yarn build && cd ../../app && node setMacPlatform x64 && yarn dist && node setMacPlatform arm64 && yarn dist",
|
||||
"build:local": "cd ../packages/api && yarn build && cd ../web && yarn build && cd ../../app && yarn predist",
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"postinstall": "electron-builder install-app-deps && patch-package",
|
||||
"predist": "copyfiles ../packages/api/dist/* packages && copyfiles \"../packages/web/public/*\" packages && copyfiles \"../packages/web/public/**/*\" packages && copyfiles --up 3 \"../plugins/dist/**/*\" packages/plugins"
|
||||
},
|
||||
"main": "src/electron.js",
|
||||
@@ -86,9 +104,9 @@
|
||||
"copyfiles": "^2.2.0",
|
||||
"cross-env": "^6.0.3",
|
||||
"electron": "11.2.3",
|
||||
"electron-builder": "22.9.1"
|
||||
"electron-builder": "22.10.5"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"msnodesqlv8": "^2.0.10"
|
||||
"msnodesqlv8": "^2.4.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
const fs = require('fs');
|
||||
|
||||
const text = fs.readFileSync('package.json', { encoding: 'utf-8' });
|
||||
const json = JSON.parse(text);
|
||||
|
||||
json.build.mac.target.arch = process.argv[2];
|
||||
|
||||
fs.writeFileSync('package.json', JSON.stringify(json, null, 2), { encoding: 'utf-8' });
|
||||
@@ -19,7 +19,6 @@ const store = new Store();
|
||||
// Keep a global reference of the window object, if you don't, the window will
|
||||
// be closed automatically when the JavaScript object is garbage collected.
|
||||
let mainWindow;
|
||||
let splashWindow;
|
||||
let mainMenu;
|
||||
|
||||
log.transports.file.level = 'debug';
|
||||
@@ -29,14 +28,6 @@ autoUpdater.logger = log;
|
||||
|
||||
let commands = {};
|
||||
|
||||
function hideSplash() {
|
||||
if (splashWindow) {
|
||||
splashWindow.destroy();
|
||||
splashWindow = null;
|
||||
}
|
||||
mainWindow.show();
|
||||
}
|
||||
|
||||
function commandItem(id) {
|
||||
const command = commands[id];
|
||||
return {
|
||||
@@ -59,8 +50,10 @@ function buildMenu() {
|
||||
commandItem('file.open'),
|
||||
commandItem('group.save'),
|
||||
commandItem('group.saveAs'),
|
||||
commandItem('database.search'),
|
||||
{ type: 'separator' },
|
||||
{ role: 'close' },
|
||||
commandItem('tabs.closeTab'),
|
||||
commandItem('file.exit'),
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -156,12 +149,14 @@ function createWindow() {
|
||||
title: 'DbGate',
|
||||
...bounds,
|
||||
icon: os.platform() == 'win32' ? 'icon.ico' : path.resolve(__dirname, '../icon.png'),
|
||||
show: false,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true,
|
||||
},
|
||||
});
|
||||
if (store.get('winIsMaximized')) {
|
||||
mainWindow.maximize();
|
||||
}
|
||||
|
||||
mainMenu = buildMenu();
|
||||
mainWindow.setMenu(mainMenu);
|
||||
@@ -175,10 +170,11 @@ function createWindow() {
|
||||
slashes: true,
|
||||
});
|
||||
mainWindow.webContents.on('did-finish-load', function () {
|
||||
hideSplash();
|
||||
// hideSplash();
|
||||
});
|
||||
mainWindow.on('close', () => {
|
||||
store.set('winBounds', mainWindow.getBounds());
|
||||
store.set('winIsMaximized', mainWindow.isMaximized());
|
||||
});
|
||||
mainWindow.loadURL(startUrl);
|
||||
if (os.platform() == 'linux') {
|
||||
@@ -186,20 +182,6 @@ function createWindow() {
|
||||
}
|
||||
}
|
||||
|
||||
splashWindow = new BrowserWindow({
|
||||
width: 300,
|
||||
height: 120,
|
||||
transparent: true,
|
||||
frame: false,
|
||||
});
|
||||
splashWindow.loadURL(
|
||||
url.format({
|
||||
pathname: path.join(__dirname, '../packages/web/build/splash.html'),
|
||||
protocol: 'file:',
|
||||
slashes: true,
|
||||
})
|
||||
);
|
||||
|
||||
if (process.env.ELECTRON_START_URL) {
|
||||
loadMainWindow();
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function load() {
|
||||
const plugins = {};
|
||||
|
||||
for (const packageName of fs.readdirSync('plugins')) {
|
||||
if (!packageName.startsWith('dbgate-plugin-')) continue;
|
||||
const dir = path.join('plugins', packageName);
|
||||
const frontend = fs.readFileSync(path.join(dir, 'dist', 'frontend.js'), 'utf-8');
|
||||
const readme = fs.readFileSync(path.join(dir, 'README.md'), 'utf-8');
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf-8'));
|
||||
plugins[packageName] = {
|
||||
manifest,
|
||||
frontend,
|
||||
readme,
|
||||
};
|
||||
}
|
||||
|
||||
return plugins;
|
||||
}
|
||||
|
||||
fs.writeFileSync('packages/api/src/packagedPluginsContent.js', `module.exports = () => (${JSON.stringify(load())});`);
|
||||
@@ -0,0 +1 @@
|
||||
dbtemp
|
||||
@@ -0,0 +1,68 @@
|
||||
const stableStringify = require('json-stable-stringify');
|
||||
const _ = require('lodash');
|
||||
const fp = require('lodash/fp');
|
||||
const uuidv1 = require('uuid/v1');
|
||||
const { testWrapper } = require('../tools');
|
||||
const engines = require('../engines');
|
||||
const { getAlterDatabaseScript, extendDatabaseInfo, generateDbPairingId } = require('dbgate-tools');
|
||||
|
||||
function flatSource() {
|
||||
return _.flatten(
|
||||
engines.map(engine => (engine.objects || []).map(object => [engine.label, object.type, object, engine]))
|
||||
);
|
||||
}
|
||||
|
||||
async function testDatabaseDiff(conn, driver, mangle, createObject = null) {
|
||||
await driver.query(conn, `create table t1 (id int not null primary key)`);
|
||||
|
||||
await driver.query(
|
||||
conn,
|
||||
`create table t2 (
|
||||
id int not null primary key,
|
||||
t1_id int null references t1(id)
|
||||
)`
|
||||
);
|
||||
|
||||
if (createObject) await driver.query(conn, createObject);
|
||||
|
||||
const structure1 = generateDbPairingId(extendDatabaseInfo(await driver.analyseFull(conn)));
|
||||
let structure2 = _.cloneDeep(structure1);
|
||||
mangle(structure2);
|
||||
structure2 = extendDatabaseInfo(structure2);
|
||||
|
||||
const { sql } = getAlterDatabaseScript(structure1, structure2, {}, structure2, driver);
|
||||
console.log('RUNNING ALTER SQL', driver.engine, ':', sql);
|
||||
|
||||
await driver.script(conn, sql);
|
||||
|
||||
const structure2Real = extendDatabaseInfo(await driver.analyseFull(conn));
|
||||
|
||||
expect(structure2Real.tables.length).toEqual(structure2.tables.length);
|
||||
return structure2Real;
|
||||
}
|
||||
|
||||
describe('Alter database', () => {
|
||||
test.each(engines.map(engine => [engine.label, engine]))(
|
||||
'Drop referenced table - %s',
|
||||
testWrapper(async (conn, driver, engine) => {
|
||||
await testDatabaseDiff(conn, driver, db => {
|
||||
_.remove(db.tables, x => x.pureName == 't1');
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
test.each(flatSource())(
|
||||
'Drop object - %s - %s',
|
||||
testWrapper(async (conn, driver, type, object, engine) => {
|
||||
const db = await testDatabaseDiff(
|
||||
conn,
|
||||
driver,
|
||||
db => {
|
||||
_.remove(db[type], x => x.pureName == 'obj1');
|
||||
},
|
||||
object.create1
|
||||
);
|
||||
expect(db[type].length).toEqual(0);
|
||||
})
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,124 @@
|
||||
const stableStringify = require('json-stable-stringify');
|
||||
const _ = require('lodash');
|
||||
const fp = require('lodash/fp');
|
||||
const uuidv1 = require('uuid/v1');
|
||||
const { testWrapper } = require('../tools');
|
||||
const engines = require('../engines');
|
||||
const { getAlterTableScript, extendDatabaseInfo, generateDbPairingId } = require('dbgate-tools');
|
||||
|
||||
function pickImportantTableInfo(table) {
|
||||
return {
|
||||
pureName: table.pureName,
|
||||
columns: table.columns
|
||||
.filter(x => x.columnName != 'rowid')
|
||||
.map(fp.pick(['columnName', 'notNull', 'autoIncrement'])),
|
||||
};
|
||||
}
|
||||
|
||||
function checkTableStructure(t1, t2) {
|
||||
// expect(t1.pureName).toEqual(t2.pureName)
|
||||
expect(pickImportantTableInfo(t1)).toEqual(pickImportantTableInfo(t2));
|
||||
}
|
||||
|
||||
async function testTableDiff(conn, driver, mangle) {
|
||||
await driver.query(conn, `create table t0 (id int not null primary key)`);
|
||||
|
||||
await driver.query(
|
||||
conn,
|
||||
`create table t1 (
|
||||
col_pk int not null primary key,
|
||||
col_std int null,
|
||||
col_def int null default 12,
|
||||
col_fk int null references t0(id),
|
||||
col_idx int null,
|
||||
col_uq int null unique,
|
||||
col_ref int null unique
|
||||
)`
|
||||
);
|
||||
|
||||
await driver.query(conn, `create index idx1 on t1(col_idx)`);
|
||||
|
||||
await driver.query(conn, `create table t2 (id int not null primary key, fkval int null references t1(col_ref))`);
|
||||
|
||||
const tget = x => x.tables.find(y => y.pureName == 't1');
|
||||
const structure1 = generateDbPairingId(extendDatabaseInfo(await driver.analyseFull(conn)));
|
||||
let structure2 = _.cloneDeep(structure1);
|
||||
mangle(tget(structure2));
|
||||
structure2 = extendDatabaseInfo(structure2);
|
||||
|
||||
const { sql } = getAlterTableScript(tget(structure1), tget(structure2), {}, structure2, driver);
|
||||
console.log('RUNNING ALTER SQL', driver.engine, ':', sql);
|
||||
|
||||
await driver.script(conn, sql);
|
||||
|
||||
const structure2Real = extendDatabaseInfo(await driver.analyseFull(conn));
|
||||
|
||||
checkTableStructure(tget(structure2Real), tget(structure2));
|
||||
// expect(stableStringify(structure2)).toEqual(stableStringify(structure2Real));
|
||||
}
|
||||
|
||||
const TESTED_COLUMNS = ['col_pk', 'col_std', 'col_def', 'col_fk', 'col_ref', 'col_idx', 'col_uq'];
|
||||
// const TESTED_COLUMNS = ['col_pk'];
|
||||
// const TESTED_COLUMNS = ['col_idx'];
|
||||
// const TESTED_COLUMNS = ['col_def'];
|
||||
// const TESTED_COLUMNS = ['col_std'];
|
||||
// const TESTED_COLUMNS = ['col_ref'];
|
||||
|
||||
function engines_columns_source() {
|
||||
return _.flatten(engines.map(engine => TESTED_COLUMNS.map(column => [engine.label, column, engine])));
|
||||
}
|
||||
|
||||
describe('Alter table', () => {
|
||||
test.each(engines.map(engine => [engine.label, engine]))(
|
||||
'Add column - %s',
|
||||
testWrapper(async (conn, driver, engine) => {
|
||||
await testTableDiff(conn, driver, tbl => {
|
||||
tbl.columns.push({
|
||||
columnName: 'added',
|
||||
dataType: 'int',
|
||||
pairingId: uuidv1(),
|
||||
notNull: false,
|
||||
autoIncrement: false,
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
test.each(engines_columns_source())(
|
||||
'Drop column - %s - %s',
|
||||
testWrapper(async (conn, driver, column, engine) => {
|
||||
await testTableDiff(conn, driver, tbl => (tbl.columns = tbl.columns.filter(x => x.columnName != column)));
|
||||
})
|
||||
);
|
||||
|
||||
test.each(engines_columns_source())(
|
||||
'Change nullability - %s - %s',
|
||||
testWrapper(async (conn, driver, column, engine) => {
|
||||
await testTableDiff(
|
||||
conn,
|
||||
driver,
|
||||
tbl => (tbl.columns = tbl.columns.map(x => (x.columnName == column ? { ...x, notNull: true } : x)))
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
test.each(engines_columns_source())(
|
||||
'Rename column - %s - %s',
|
||||
testWrapper(async (conn, driver, column, engine) => {
|
||||
await testTableDiff(
|
||||
conn,
|
||||
driver,
|
||||
tbl => (tbl.columns = tbl.columns.map(x => (x.columnName == column ? { ...x, columnName: 'col_renamed' } : x)))
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
test.each(engines.map(engine => [engine.label, engine]))(
|
||||
'Drop index - %s',
|
||||
testWrapper(async (conn, driver, engine) => {
|
||||
await testTableDiff(conn, driver, tbl => {
|
||||
tbl.indexes = [];
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
const { testWrapper } = require('../tools');
|
||||
const engines = require('../engines');
|
||||
const _ = require('lodash');
|
||||
|
||||
const initSql = ['CREATE TABLE t1 (id int)', 'CREATE TABLE t2 (id int)'];
|
||||
|
||||
function flatSource() {
|
||||
return _.flatten(
|
||||
engines.map(engine => (engine.objects || []).map(object => [engine.label, object.type, object, engine]))
|
||||
);
|
||||
}
|
||||
|
||||
const obj1Match = expect.objectContaining({
|
||||
pureName: 'obj1',
|
||||
});
|
||||
const view1Match = expect.objectContaining({
|
||||
pureName: 'obj1',
|
||||
columns: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
columnName: 'id',
|
||||
}),
|
||||
]),
|
||||
});
|
||||
|
||||
describe('Object analyse', () => {
|
||||
test.each(flatSource())(
|
||||
'Full analysis - %s - %s',
|
||||
testWrapper(async (conn, driver, type, object, engine) => {
|
||||
for (const sql of initSql) await driver.query(conn, sql);
|
||||
|
||||
await driver.query(conn, object.create1);
|
||||
const structure = await driver.analyseFull(conn);
|
||||
|
||||
expect(structure[type].length).toEqual(1);
|
||||
expect(structure[type][0]).toEqual(type.includes('views') ? view1Match : obj1Match);
|
||||
})
|
||||
);
|
||||
|
||||
test.each(flatSource())(
|
||||
'Incremental analysis - add - %s - %s',
|
||||
testWrapper(async (conn, driver, type, object, engine) => {
|
||||
for (const sql of initSql) await driver.query(conn, sql);
|
||||
|
||||
await driver.query(conn, object.create2);
|
||||
const structure1 = await driver.analyseFull(conn);
|
||||
await driver.query(conn, object.create1);
|
||||
const structure2 = await driver.analyseIncremental(conn, structure1);
|
||||
|
||||
expect(structure2[type].length).toEqual(2);
|
||||
expect(structure2[type].find(x => x.pureName == 'obj1')).toEqual(type.includes('views') ? view1Match : obj1Match);
|
||||
})
|
||||
);
|
||||
|
||||
test.each(flatSource())(
|
||||
'Incremental analysis - drop - %s - %s',
|
||||
testWrapper(async (conn, driver, type, object, engine) => {
|
||||
for (const sql of initSql) await driver.query(conn, sql);
|
||||
|
||||
await driver.query(conn, object.create1);
|
||||
await driver.query(conn, object.create2);
|
||||
const structure1 = await driver.analyseFull(conn);
|
||||
await driver.query(conn, object.drop2);
|
||||
const structure2 = await driver.analyseIncremental(conn, structure1);
|
||||
|
||||
expect(structure2[type].length).toEqual(1);
|
||||
expect(structure2[type][0]).toEqual(type.includes('views') ? view1Match : obj1Match);
|
||||
})
|
||||
);
|
||||
|
||||
test.each(flatSource())(
|
||||
'Create SQL - add - %s - %s',
|
||||
testWrapper(async (conn, driver, type, object, engine) => {
|
||||
for (const sql of initSql) await driver.query(conn, sql);
|
||||
|
||||
await driver.query(conn, object.create1);
|
||||
const structure1 = await driver.analyseFull(conn);
|
||||
await driver.query(conn, object.drop1);
|
||||
const structure2 = await driver.analyseIncremental(conn, structure1);
|
||||
expect(structure2[type].length).toEqual(0);
|
||||
|
||||
await driver.query(conn, structure1[type][0].createSql);
|
||||
|
||||
const structure3 = await driver.analyseIncremental(conn, structure2);
|
||||
|
||||
expect(structure3[type].length).toEqual(1);
|
||||
expect(structure3[type][0]).toEqual(type.includes('views') ? view1Match : obj1Match);
|
||||
})
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,163 @@
|
||||
const engines = require('../engines');
|
||||
const { splitQuery } = require('dbgate-query-splitter');
|
||||
const { testWrapper } = require('../tools');
|
||||
|
||||
const initSql = ['CREATE TABLE t1 (id int)', 'INSERT INTO t1 (id) VALUES (1)', 'INSERT INTO t1 (id) VALUES (2)'];
|
||||
|
||||
expect.extend({
|
||||
dataRow(row, expected) {
|
||||
for (const key in expected) {
|
||||
if (row[key] != expected[key]) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => `Different key: ${key}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
pass: true,
|
||||
message: () => '',
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
class StreamHandler {
|
||||
constructor(resolve) {
|
||||
this.results = [];
|
||||
this.resolve = resolve;
|
||||
this.infoRows = [];
|
||||
}
|
||||
row(row) {
|
||||
this.results[this.results.length - 1].rows.push(row);
|
||||
}
|
||||
recordset(columns) {
|
||||
this.results.push({
|
||||
columns,
|
||||
rows: [],
|
||||
});
|
||||
}
|
||||
done(result) {
|
||||
this.resolve(this.results);
|
||||
}
|
||||
info(msg) {
|
||||
this.infoRows.push(msg);
|
||||
}
|
||||
}
|
||||
|
||||
function executeStreamItem(driver, conn, sql) {
|
||||
return new Promise(resolve => {
|
||||
const handler = new StreamHandler(resolve);
|
||||
driver.stream(conn, sql, handler);
|
||||
});
|
||||
}
|
||||
|
||||
async function executeStream(driver, conn, sql) {
|
||||
const results = [];
|
||||
for (const sqlItem of splitQuery(sql, driver.getQuerySplitterOptions('stream'))) {
|
||||
const item = await executeStreamItem(driver, conn, sqlItem);
|
||||
results.push(...item);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
describe('Query', () => {
|
||||
test.each(engines.map(engine => [engine.label, engine]))(
|
||||
'Simple query - %s',
|
||||
testWrapper(async (conn, driver, engine) => {
|
||||
for (const sql of initSql) await driver.query(conn, sql);
|
||||
|
||||
const res = await driver.query(conn, 'SELECT id FROM t1 ORDER BY id');
|
||||
expect(res.columns).toEqual([
|
||||
expect.objectContaining({
|
||||
columnName: 'id',
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(res.rows).toEqual([
|
||||
expect.dataRow({
|
||||
id: 1,
|
||||
}),
|
||||
expect.dataRow({
|
||||
id: 2,
|
||||
}),
|
||||
]);
|
||||
})
|
||||
);
|
||||
|
||||
test.each(engines.map(engine => [engine.label, engine]))(
|
||||
'Simple stream query - %s',
|
||||
testWrapper(async (conn, driver, engine) => {
|
||||
for (const sql of initSql) await driver.query(conn, sql);
|
||||
const results = await executeStream(driver, conn, 'SELECT id FROM t1 ORDER BY id');
|
||||
expect(results.length).toEqual(1);
|
||||
const res = results[0];
|
||||
|
||||
expect(res.columns).toEqual([expect.objectContaining({ columnName: 'id' })]);
|
||||
expect(res.rows).toEqual([expect.dataRow({ id: 1 }), expect.dataRow({ id: 2 })]);
|
||||
})
|
||||
);
|
||||
|
||||
test.each(engines.map(engine => [engine.label, engine]))(
|
||||
'More queries - %s',
|
||||
testWrapper(async (conn, driver, engine) => {
|
||||
for (const sql of initSql) await driver.query(conn, sql);
|
||||
const results = await executeStream(
|
||||
driver,
|
||||
conn,
|
||||
'SELECT id FROM t1 ORDER BY id; SELECT id FROM t1 ORDER BY id DESC'
|
||||
);
|
||||
expect(results.length).toEqual(2);
|
||||
|
||||
const res1 = results[0];
|
||||
expect(res1.columns).toEqual([expect.objectContaining({ columnName: 'id' })]);
|
||||
expect(res1.rows).toEqual([expect.dataRow({ id: 1 }), expect.dataRow({ id: 2 })]);
|
||||
|
||||
const res2 = results[1];
|
||||
expect(res2.columns).toEqual([expect.objectContaining({ columnName: 'id' })]);
|
||||
expect(res2.rows).toEqual([expect.dataRow({ id: 2 }), expect.dataRow({ id: 1 })]);
|
||||
})
|
||||
);
|
||||
|
||||
test.each(engines.map(engine => [engine.label, engine]))(
|
||||
'Script - return data - %s',
|
||||
testWrapper(async (conn, driver, engine) => {
|
||||
const results = await executeStream(
|
||||
driver,
|
||||
conn,
|
||||
'CREATE TABLE t1 (id int); INSERT INTO t1 (id) VALUES (1); INSERT INTO t1 (id) VALUES (2); SELECT id FROM t1 ORDER BY id; '
|
||||
);
|
||||
expect(results.length).toEqual(1);
|
||||
|
||||
const res1 = results[0];
|
||||
expect(res1.columns).toEqual([expect.objectContaining({ columnName: 'id' })]);
|
||||
expect(res1.rows).toEqual([expect.dataRow({ id: 1 }), expect.dataRow({ id: 2 })]);
|
||||
})
|
||||
);
|
||||
|
||||
test.each(engines.map(engine => [engine.label, engine]))(
|
||||
'Script - no data - %s',
|
||||
testWrapper(async (conn, driver, engine) => {
|
||||
const results = await executeStream(
|
||||
driver,
|
||||
conn,
|
||||
'CREATE TABLE t1 (id int); INSERT INTO t1 (id) VALUES (1); INSERT INTO t1 (id) VALUES (2) '
|
||||
);
|
||||
expect(results.length).toEqual(0);
|
||||
})
|
||||
);
|
||||
|
||||
test.each(engines.map(engine => [engine.label, engine]))(
|
||||
'Save data query - %s',
|
||||
testWrapper(async (conn, driver, engine) => {
|
||||
for (const sql of initSql) await driver.query(conn, sql);
|
||||
|
||||
await driver.script(
|
||||
conn,
|
||||
'INSERT INTO t1 (id) VALUES (3);INSERT INTO t1 (id) VALUES (4);UPDATE t1 SET id=10 WHERE id=1;DELETE FROM t1 WHERE id=2;'
|
||||
);
|
||||
const res = await driver.query(conn, 'SELECT COUNT(*) AS cnt FROM t1');
|
||||
// console.log(res);
|
||||
expect(res.rows[0].cnt == 3).toBeTruthy();
|
||||
})
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,164 @@
|
||||
const engines = require('../engines');
|
||||
const { testWrapper } = require('../tools');
|
||||
|
||||
const t1Sql = 'CREATE TABLE t1 (id int not null primary key, val1 varchar(50) null)';
|
||||
const ix1Sql = 'CREATE index ix1 ON t1(val1, id)';
|
||||
const t2Sql = 'CREATE TABLE t2 (id int not null primary key, val2 varchar(50) null unique)';
|
||||
const t3Sql = 'CREATE TABLE t3 (id int not null primary key, valfk int, foreign key (valfk) references t2(id))';
|
||||
// const fkSql = 'ALTER TABLE t3 ADD FOREIGN KEY (valfk) REFERENCES t2(id)'
|
||||
|
||||
const txMatch = (tname, vcolname, nextcol) =>
|
||||
expect.objectContaining({
|
||||
pureName: tname,
|
||||
columns: [
|
||||
expect.objectContaining({
|
||||
columnName: 'id',
|
||||
notNull: true,
|
||||
dataType: expect.stringContaining('int'),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
columnName: vcolname,
|
||||
notNull: false,
|
||||
dataType: expect.stringMatching(/.*char.*\(50\)/),
|
||||
}),
|
||||
...(nextcol
|
||||
? [
|
||||
expect.objectContaining({
|
||||
columnName: 'nextcol',
|
||||
notNull: false,
|
||||
dataType: expect.stringMatching(/.*char.*\(50\)/),
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
],
|
||||
primaryKey: expect.objectContaining({
|
||||
columns: [
|
||||
expect.objectContaining({
|
||||
columnName: 'id',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
const t1Match = txMatch('t1', 'val1');
|
||||
const t2Match = txMatch('t2', 'val2');
|
||||
const t2NextColMatch = txMatch('t2', 'val2', true);
|
||||
|
||||
describe('Table analyse', () => {
|
||||
test.each(engines.map(engine => [engine.label, engine]))(
|
||||
'Table structure - full analysis - %s',
|
||||
testWrapper(async (conn, driver, engine) => {
|
||||
await driver.query(conn, t1Sql);
|
||||
|
||||
const structure = await driver.analyseFull(conn);
|
||||
|
||||
expect(structure.tables.length).toEqual(1);
|
||||
expect(structure.tables[0]).toEqual(t1Match);
|
||||
})
|
||||
);
|
||||
|
||||
test.each(engines.map(engine => [engine.label, engine]))(
|
||||
'Table add - incremental analysis - %s',
|
||||
testWrapper(async (conn, driver, engine) => {
|
||||
await driver.query(conn, t2Sql);
|
||||
|
||||
const structure1 = await driver.analyseFull(conn);
|
||||
expect(structure1.tables.length).toEqual(1);
|
||||
expect(structure1.tables[0]).toEqual(t2Match);
|
||||
|
||||
await driver.query(conn, t1Sql);
|
||||
const structure2 = await driver.analyseIncremental(conn, structure1);
|
||||
|
||||
expect(structure2.tables.length).toEqual(2);
|
||||
expect(structure2.tables.find(x => x.pureName == 't1')).toEqual(t1Match);
|
||||
expect(structure2.tables.find(x => x.pureName == 't2')).toEqual(t2Match);
|
||||
})
|
||||
);
|
||||
|
||||
test.each(engines.map(engine => [engine.label, engine]))(
|
||||
'Table remove - incremental analysis - %s',
|
||||
testWrapper(async (conn, driver, engine) => {
|
||||
await driver.query(conn, t1Sql);
|
||||
await driver.query(conn, t2Sql);
|
||||
const structure1 = await driver.analyseFull(conn);
|
||||
expect(structure1.tables.length).toEqual(2);
|
||||
expect(structure1.tables.find(x => x.pureName == 't1')).toEqual(t1Match);
|
||||
expect(structure1.tables.find(x => x.pureName == 't2')).toEqual(t2Match);
|
||||
|
||||
await driver.query(conn, 'DROP TABLE t2');
|
||||
const structure2 = await driver.analyseIncremental(conn, structure1);
|
||||
|
||||
expect(structure2.tables.length).toEqual(1);
|
||||
expect(structure2.tables[0]).toEqual(t1Match);
|
||||
})
|
||||
);
|
||||
|
||||
test.each(engines.map(engine => [engine.label, engine]))(
|
||||
'Table change - incremental analysis - %s',
|
||||
testWrapper(async (conn, driver, engine) => {
|
||||
await driver.query(conn, t1Sql);
|
||||
await driver.query(conn, t2Sql);
|
||||
const structure1 = await driver.analyseFull(conn);
|
||||
|
||||
if (engine.dbSnapshotBySeconds) await new Promise(resolve => setTimeout(resolve, 1100));
|
||||
|
||||
await driver.query(conn, 'ALTER TABLE t2 ADD nextcol varchar(50)');
|
||||
const structure2 = await driver.analyseIncremental(conn, structure1);
|
||||
|
||||
expect(structure2).toBeTruthy(); // if falsy, no modification is detected
|
||||
|
||||
expect(structure2.tables.length).toEqual(2);
|
||||
expect(structure2.tables.find(x => x.pureName == 't1')).toEqual(t1Match);
|
||||
expect(structure2.tables.find(x => x.pureName == 't2')).toEqual(t2NextColMatch);
|
||||
})
|
||||
);
|
||||
|
||||
test.each(engines.map(engine => [engine.label, engine]))(
|
||||
'Index - full analysis - %s',
|
||||
testWrapper(async (conn, driver, engine) => {
|
||||
await driver.query(conn, t1Sql);
|
||||
await driver.query(conn, ix1Sql);
|
||||
const structure = await driver.analyseFull(conn);
|
||||
|
||||
const t1 = structure.tables.find(x => x.pureName == 't1');
|
||||
expect(t1.indexes.length).toEqual(1);
|
||||
expect(t1.indexes[0].columns.length).toEqual(2);
|
||||
expect(t1.indexes[0].columns[0]).toEqual(expect.objectContaining({ columnName: 'val1' }));
|
||||
expect(t1.indexes[0].columns[1]).toEqual(expect.objectContaining({ columnName: 'id' }));
|
||||
})
|
||||
);
|
||||
|
||||
test.each(engines.map(engine => [engine.label, engine]))(
|
||||
'Unique - full analysis - %s',
|
||||
testWrapper(async (conn, driver, engine) => {
|
||||
await driver.query(conn, t2Sql);
|
||||
const structure = await driver.analyseFull(conn);
|
||||
|
||||
const t2 = structure.tables.find(x => x.pureName == 't2');
|
||||
// const indexesAndUniques = [...t2.uniques, ...t2.indexes];
|
||||
expect(t2.uniques.length).toEqual(1);
|
||||
expect(t2.uniques[0].columns.length).toEqual(1);
|
||||
expect(t2.uniques[0].columns[0]).toEqual(expect.objectContaining({ columnName: 'val2' }));
|
||||
})
|
||||
);
|
||||
|
||||
test.each(engines.map(engine => [engine.label, engine]))(
|
||||
'Foreign key - full analysis - %s',
|
||||
testWrapper(async (conn, driver, engine) => {
|
||||
await driver.query(conn, t2Sql);
|
||||
await driver.query(conn, t3Sql);
|
||||
// await driver.query(conn, fkSql);
|
||||
|
||||
const structure = await driver.analyseFull(conn);
|
||||
|
||||
const t3 = structure.tables.find(x => x.pureName == 't3');
|
||||
console.log('T3', t3.foreignKeys[0].columns);
|
||||
expect(t3.foreignKeys.length).toEqual(1);
|
||||
expect(t3.foreignKeys[0].columns.length).toEqual(1);
|
||||
expect(t3.foreignKeys[0]).toEqual(expect.objectContaining({ refTableName: 't2' }));
|
||||
expect(t3.foreignKeys[0].columns[0]).toEqual(
|
||||
expect.objectContaining({ columnName: 'valfk', refColumnName: 'id' })
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,153 @@
|
||||
const _ = require('lodash');
|
||||
const fp = require('lodash/fp');
|
||||
const engines = require('../engines');
|
||||
const { testWrapper } = require('../tools');
|
||||
const { extendDatabaseInfo } = require('dbgate-tools');
|
||||
|
||||
function createExpector(value) {
|
||||
return _.cloneDeepWith(value, x => {
|
||||
if (_.isPlainObject(x)) {
|
||||
return expect.objectContaining(_.mapValues(x, y => createExpector(y)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function omitTableSpecificInfo(table) {
|
||||
return {
|
||||
...table,
|
||||
columns: table.columns.map(fp.omit(['dataType'])),
|
||||
};
|
||||
}
|
||||
|
||||
function checkTableStructure2(t1, t2) {
|
||||
// expect(t1.pureName).toEqual(t2.pureName)
|
||||
expect(t2).toEqual(createExpector(omitTableSpecificInfo(t1)));
|
||||
}
|
||||
|
||||
async function testTableCreate(conn, driver, table) {
|
||||
await driver.query(conn, `create table t0 (id int not null primary key)`);
|
||||
|
||||
const dmp = driver.createDumper();
|
||||
const table1 = {
|
||||
...table,
|
||||
pureName: 'tested',
|
||||
};
|
||||
dmp.createTable(table1);
|
||||
|
||||
console.log('RUNNING CREATE SQL', driver.engine, ':', dmp.s);
|
||||
await driver.script(conn, dmp.s);
|
||||
|
||||
const db = extendDatabaseInfo(await driver.analyseFull(conn));
|
||||
const table2 = db.tables.find(x => x.pureName == 'tested');
|
||||
|
||||
checkTableStructure2(table1, table2);
|
||||
}
|
||||
|
||||
describe('Table create', () => {
|
||||
test.each(engines.map(engine => [engine.label, engine]))(
|
||||
'Simple table - %s',
|
||||
testWrapper(async (conn, driver, engine) => {
|
||||
await testTableCreate(conn, driver, {
|
||||
columns: [
|
||||
{
|
||||
columnName: 'col1',
|
||||
dataType: 'int',
|
||||
notNull: true,
|
||||
},
|
||||
],
|
||||
primaryKey: {
|
||||
columns: [{ columnName: 'col1' }],
|
||||
},
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
test.each(engines.map(engine => [engine.label, engine]))(
|
||||
'Table with index - %s',
|
||||
testWrapper(async (conn, driver, engine) => {
|
||||
await testTableCreate(conn, driver, {
|
||||
columns: [
|
||||
{
|
||||
columnName: 'col1',
|
||||
dataType: 'int',
|
||||
notNull: true,
|
||||
},
|
||||
{
|
||||
columnName: 'col2',
|
||||
dataType: 'int',
|
||||
notNull: true,
|
||||
},
|
||||
],
|
||||
primaryKey: {
|
||||
columns: [{ columnName: 'col1' }],
|
||||
},
|
||||
indexes: [
|
||||
{
|
||||
constraintName: 'ix1',
|
||||
pureName: 'tested',
|
||||
columns: [{ columnName: 'col2' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
test.each(engines.map(engine => [engine.label, engine]))(
|
||||
'Table with foreign key - %s',
|
||||
testWrapper(async (conn, driver, engine) => {
|
||||
await testTableCreate(conn, driver, {
|
||||
columns: [
|
||||
{
|
||||
columnName: 'col1',
|
||||
dataType: 'int',
|
||||
notNull: true,
|
||||
},
|
||||
{
|
||||
columnName: 'col2',
|
||||
dataType: 'int',
|
||||
notNull: true,
|
||||
},
|
||||
],
|
||||
primaryKey: {
|
||||
columns: [{ columnName: 'col1' }],
|
||||
},
|
||||
foreignKeys: [
|
||||
{
|
||||
pureName: 'tested',
|
||||
refTableName: 't0',
|
||||
columns: [{ columnName: 'col2', refColumnName: 'id' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
test.each(engines.map(engine => [engine.label, engine]))(
|
||||
'Table with unique - %s',
|
||||
testWrapper(async (conn, driver, engine) => {
|
||||
await testTableCreate(conn, driver, {
|
||||
columns: [
|
||||
{
|
||||
columnName: 'col1',
|
||||
dataType: 'int',
|
||||
notNull: true,
|
||||
},
|
||||
{
|
||||
columnName: 'col2',
|
||||
dataType: 'int',
|
||||
notNull: true,
|
||||
},
|
||||
],
|
||||
primaryKey: {
|
||||
columns: [{ columnName: 'col1' }],
|
||||
},
|
||||
uniques: [
|
||||
{
|
||||
pureName: 'tested',
|
||||
columns: [{ columnName: 'col2' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
version: '3'
|
||||
services:
|
||||
postgres:
|
||||
image: postgres
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_PASSWORD: Pwd2020Db
|
||||
ports:
|
||||
- 15000:5432
|
||||
|
||||
mysql:
|
||||
image: mysql:8.0.18
|
||||
command: --default-authentication-plugin=mysql_native_password
|
||||
restart: always
|
||||
ports:
|
||||
- 15001:3306
|
||||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=Pwd2020Db
|
||||
|
||||
mssql:
|
||||
image: mcr.microsoft.com/mssql/server
|
||||
restart: always
|
||||
ports:
|
||||
- 15002:1433
|
||||
environment:
|
||||
- ACCEPT_EULA=Y
|
||||
- SA_PASSWORD=Pwd2020Db
|
||||
- MSSQL_PID=Express
|
||||
|
||||
cockroachdb:
|
||||
image: cockroachdb/cockroach
|
||||
ports:
|
||||
- 15003:26257
|
||||
command: start-single-node --insecure
|
||||
|
||||
# mongodb:
|
||||
# image: mongo:4.0.12
|
||||
# restart: always
|
||||
# volumes:
|
||||
# - mongo-data:/data/db
|
||||
# - mongo-config:/data/configdb
|
||||
# ports:
|
||||
# - 27017:27017
|
||||
|
||||
|
||||
# cockroachdb-init:
|
||||
# image: cockroachdb/cockroach
|
||||
# # build: cockroach
|
||||
# # entrypoint: /cockroach/init.sh
|
||||
# entrypoint: ./cockroach sql --insecure --host="cockroachdb" --execute="CREATE DATABASE IF NOT EXISTS test;"
|
||||
|
||||
# depends_on:
|
||||
# - cockroachdb
|
||||
# restart: on-failure
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
const views = {
|
||||
type: 'views',
|
||||
create1: 'CREATE VIEW obj1 AS SELECT id FROM t1',
|
||||
create2: 'CREATE VIEW obj2 AS SELECT id FROM t2',
|
||||
drop1: 'DROP VIEW obj1',
|
||||
drop2: 'DROP VIEW obj2',
|
||||
};
|
||||
const matviews = {
|
||||
type: 'matviews',
|
||||
create1: 'CREATE MATERIALIZED VIEW obj1 AS SELECT id FROM t1',
|
||||
create2: 'CREATE MATERIALIZED VIEW obj2 AS SELECT id FROM t2',
|
||||
drop1: 'DROP MATERIALIZED VIEW obj1',
|
||||
drop2: 'DROP MATERIALIZED VIEW obj2',
|
||||
};
|
||||
|
||||
const engines = [
|
||||
{
|
||||
label: 'MySQL',
|
||||
connection: {
|
||||
engine: 'mysql@dbgate-plugin-mysql',
|
||||
password: 'Pwd2020Db',
|
||||
user: 'root',
|
||||
server: 'mysql',
|
||||
port: 3306,
|
||||
},
|
||||
local: {
|
||||
server: 'localhost',
|
||||
port: 15001,
|
||||
},
|
||||
// skipOnCI: true,
|
||||
objects: [views],
|
||||
dbSnapshotBySeconds: true,
|
||||
},
|
||||
{
|
||||
label: 'PostgreSQL',
|
||||
connection: {
|
||||
engine: 'postgres@dbgate-plugin-postgres',
|
||||
password: 'Pwd2020Db',
|
||||
user: 'postgres',
|
||||
server: 'postgres',
|
||||
port: 5432,
|
||||
},
|
||||
local: {
|
||||
server: 'localhost',
|
||||
port: 15000,
|
||||
},
|
||||
objects: [
|
||||
views,
|
||||
matviews,
|
||||
{
|
||||
type: 'procedures',
|
||||
create1: 'CREATE PROCEDURE obj1() LANGUAGE SQL AS $$ select * from t1 $$',
|
||||
create2: 'CREATE PROCEDURE obj2() LANGUAGE SQL AS $$ select * from t2 $$',
|
||||
drop1: 'DROP PROCEDURE obj1',
|
||||
drop2: 'DROP PROCEDURE obj2',
|
||||
},
|
||||
{
|
||||
type: 'functions',
|
||||
create1:
|
||||
'CREATE FUNCTION obj1() returns int LANGUAGE plpgsql AS $$ declare res integer; begin select count(*) into res from t1; return res; end; $$',
|
||||
create2:
|
||||
'CREATE FUNCTION obj2() returns int LANGUAGE plpgsql AS $$ declare res integer; begin select count(*) into res from t2; return res; end; $$',
|
||||
drop1: 'DROP FUNCTION obj1',
|
||||
drop2: 'DROP FUNCTION obj2',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'SQL Server',
|
||||
connection: {
|
||||
engine: 'mssql@dbgate-plugin-mssql',
|
||||
password: 'Pwd2020Db',
|
||||
user: 'sa',
|
||||
server: 'mssql',
|
||||
port: 1433,
|
||||
},
|
||||
local: {
|
||||
server: 'localhost',
|
||||
port: 15002,
|
||||
},
|
||||
objects: [
|
||||
views,
|
||||
{
|
||||
type: 'procedures',
|
||||
create1: 'CREATE PROCEDURE obj1 AS SELECT id FROM t1',
|
||||
create2: 'CREATE PROCEDURE obj2 AS SELECT id FROM t2',
|
||||
drop1: 'DROP PROCEDURE obj1',
|
||||
drop2: 'DROP PROCEDURE obj2',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'SQLite',
|
||||
generateDbFile: true,
|
||||
connection: {
|
||||
engine: 'sqlite@dbgate-plugin-sqlite',
|
||||
},
|
||||
objects: [views],
|
||||
},
|
||||
{
|
||||
label: 'CockroachDB',
|
||||
connection: {
|
||||
engine: 'cockroach@dbgate-plugin-postgres',
|
||||
user: 'root',
|
||||
server: 'cockroachdb',
|
||||
port: 26257,
|
||||
},
|
||||
local: {
|
||||
server: 'localhost',
|
||||
port: 15003,
|
||||
},
|
||||
skipOnCI: true,
|
||||
objects: [views, matviews],
|
||||
},
|
||||
];
|
||||
|
||||
const filterLocal = [
|
||||
// filter local testing
|
||||
'MySQL',
|
||||
'PostgreSQL',
|
||||
'SQL Server',
|
||||
'SQLite',
|
||||
'CockroachDB',
|
||||
];
|
||||
|
||||
module.exports = process.env.CITEST
|
||||
? engines.filter(x => !x.skipOnCI)
|
||||
: engines.filter(x => filterLocal.find(y => x.label == y));
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "dbgate-integration-tests",
|
||||
"version": "4.1.1",
|
||||
"homepage": "https://dbgate.org/",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dbgate/dbgate.git"
|
||||
},
|
||||
"author": "Jan Prochazka",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"wait:local": "cross-env DEVMODE=1 LOCALTEST=1 node wait.js",
|
||||
"wait:ci": "cross-env DEVMODE=1 CITEST=1 node wait.js",
|
||||
|
||||
"test:local": "cross-env DEVMODE=1 LOCALTEST=1 jest",
|
||||
"test:ci": "cross-env DEVMODE=1 CITEST=1 jest --runInBand --json --outputFile=result.json --testLocationInResults",
|
||||
|
||||
"run:local": "docker-compose down && docker-compose up -d && yarn wait:local && yarn test:local"
|
||||
},
|
||||
"jest": {
|
||||
"testTimeout": 5000
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "^7.0.3",
|
||||
"jest": "^27.0.1"
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
global.DBGATE_TOOLS = require('dbgate-tools');
|
||||
const requireEngineDriver = require('dbgate-api/src/utility/requireEngineDriver');
|
||||
const crypto = require('crypto');
|
||||
|
||||
function randomDbName() {
|
||||
const generatedKey = crypto.randomBytes(6);
|
||||
const newKey = generatedKey.toString('hex');
|
||||
return `db${newKey}`;
|
||||
}
|
||||
|
||||
function extractConnection(engine) {
|
||||
const { connection } = engine;
|
||||
|
||||
if (process.env.LOCALTEST && engine.local) {
|
||||
return {
|
||||
...connection,
|
||||
...engine.local,
|
||||
};
|
||||
}
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
async function connect(engine, database) {
|
||||
const connection = extractConnection(engine);
|
||||
const driver = requireEngineDriver(connection);
|
||||
|
||||
if (engine.generateDbFile) {
|
||||
const conn = await driver.connect({
|
||||
...connection,
|
||||
databaseFile: `dbtemp/${database}`,
|
||||
});
|
||||
return conn;
|
||||
} else {
|
||||
const conn = await driver.connect(connection);
|
||||
await driver.query(conn, `CREATE DATABASE ${database}`);
|
||||
await driver.close(conn);
|
||||
|
||||
const res = await driver.connect({
|
||||
...connection,
|
||||
database,
|
||||
});
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
const testWrapper = body => async (label, ...other) => {
|
||||
const engine = other[other.length - 1];
|
||||
const driver = requireEngineDriver(engine.connection);
|
||||
const conn = await connect(engine, randomDbName());
|
||||
try {
|
||||
await body(conn, driver, ...other);
|
||||
} finally {
|
||||
await driver.close(conn);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
randomDbName,
|
||||
connect,
|
||||
extractConnection,
|
||||
testWrapper,
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
const requireEngineDriver = require('dbgate-api/src/utility/requireEngineDriver');
|
||||
const engines = require('./engines');
|
||||
const { extractConnection } = require('./tools');
|
||||
global.DBGATE_TOOLS = require('dbgate-tools');
|
||||
|
||||
async function connectEngine(engine) {
|
||||
const connection = extractConnection(engine);
|
||||
const driver = requireEngineDriver(connection);
|
||||
for (;;) {
|
||||
try {
|
||||
const conn = await driver.connect(connection);
|
||||
await driver.getVersion(conn);
|
||||
console.log(`Connect to ${engine.label} - OK`);
|
||||
await driver.close(conn);
|
||||
return;
|
||||
} catch (err) {
|
||||
console.log(`Waiting for ${engine.label}, error: ${err.message}`);
|
||||
await new Promise(resolve => setTimeout(resolve, 2500));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function run() {
|
||||
await new Promise(resolve => setTimeout(resolve, 10000));
|
||||
await Promise.all(engines.map(engine => connectEngine(engine)));
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -1,10 +1,11 @@
|
||||
{
|
||||
"private": true,
|
||||
"version": "4.2.0-beta.3",
|
||||
"version": "4.3.4",
|
||||
"name": "dbgate-all",
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
"plugins/*"
|
||||
"plugins/*",
|
||||
"integration-tests"
|
||||
],
|
||||
"scripts": {
|
||||
"start:api": "yarn workspace dbgate-api start",
|
||||
@@ -15,12 +16,15 @@
|
||||
"start:tools": "yarn workspace dbgate-tools start",
|
||||
"start:datalib": "yarn workspace dbgate-datalib start",
|
||||
"start:filterparser": "yarn workspace dbgate-filterparser start",
|
||||
"start:querysplitter": "yarn workspace dbgate-query-splitter start",
|
||||
"build:sqltree": "yarn workspace dbgate-sqltree build",
|
||||
"build:datalib": "yarn workspace dbgate-datalib build",
|
||||
"build:filterparser": "yarn workspace dbgate-filterparser build",
|
||||
"build:querysplitter": "yarn workspace dbgate-query-splitter build",
|
||||
"build:tools": "yarn workspace dbgate-tools build",
|
||||
"build:lib": "yarn build:tools && yarn build:sqltree && yarn build:filterparser && yarn build:datalib",
|
||||
"build:lib": "yarn build:querysplitter && yarn build:tools && yarn build:sqltree && yarn build:filterparser && yarn build:datalib",
|
||||
"build:app": "yarn plugins:copydist && cd app && yarn install && yarn build",
|
||||
"build:app:mac": "yarn plugins:copydist && cd app && yarn install && yarn build:mac",
|
||||
"build:api": "yarn workspace dbgate-api build",
|
||||
"build:web:docker": "yarn workspace dbgate-web build",
|
||||
"build:plugins:frontend": "workspaces-run --only=\"dbgate-plugin-*\" -- yarn build:frontend",
|
||||
@@ -32,15 +36,17 @@
|
||||
"generatePadFile": "node generatePadFile",
|
||||
"fillNativeModules": "node fillNativeModules",
|
||||
"fillNativeModulesElectron": "node fillNativeModules --electron",
|
||||
"fillPackagedPlugins": "node fillPackagedPlugins",
|
||||
"resetPackagedPlugins": "node resetPackagedPlugins",
|
||||
"prettier": "prettier --write packages/api/src && prettier --write packages/datalib/src && prettier --write packages/filterparser/src && prettier --write packages/sqltree/src && prettier --write packages/tools/src && prettier --write packages/types && prettier --write packages/web/src && prettier --write app/src",
|
||||
"copy:docker:build": "copyfiles packages/api/dist/* docker -f && copyfiles packages/web/public/* docker -u 2 && copyfiles \"packages/web/public/**/*\" docker -u 2 && copyfiles \"plugins/dist/**/*\" docker/plugins -u 2",
|
||||
"prepare:docker": "yarn plugins:copydist && yarn build:web:docker && yarn build:api && yarn copy:docker:build",
|
||||
"start": "concurrently --kill-others-on-fail \"yarn start:api\" \"yarn start:web\"",
|
||||
"lib": "concurrently --kill-others-on-fail \"yarn start:sqltree\" \"yarn start:filterparser\" \"yarn start:datalib\" \"yarn start:tools\" \"yarn build:plugins:frontend:watch\"",
|
||||
"lib": "concurrently --kill-others-on-fail \"yarn start:sqltree\" \"yarn start:filterparser\" \"yarn start:datalib\" \"yarn start:tools\" \"yarn start:querysplitter\" \"yarn build:plugins:frontend:watch\"",
|
||||
"ts:api": "yarn workspace dbgate-api ts",
|
||||
"ts:web": "yarn workspace dbgate-web ts",
|
||||
"ts": "yarn ts:api && yarn ts:web",
|
||||
"postinstall": "yarn build:lib && patch-package && yarn fillNativeModules && yarn build:plugins:frontend"
|
||||
"postinstall": "yarn resetPackagedPlugins && yarn build:lib && patch-package && yarn fillNativeModules && yarn build:plugins:frontend"
|
||||
},
|
||||
"dependencies": {
|
||||
"concurrently": "^5.1.0",
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
DEVMODE=1
|
||||
|
||||
CONNECTIONS=mysql
|
||||
|
||||
LABEL_mysql=MySql
|
||||
SERVER_mysql=dbgate.org
|
||||
USER_mysql=reader
|
||||
PASSWORD_mysql=CovidReader2020
|
||||
PORT_mysql=3326
|
||||
ENGINE_mysql=mysql@dbgate-plugin-mysql
|
||||
SINGLE_CONNECTION=mysql
|
||||
SINGLE_DATABASE=covid
|
||||
|
||||
PERMISSIONS=files/charts/read
|
||||
@@ -0,0 +1,15 @@
|
||||
DEVMODE=1
|
||||
|
||||
CONNECTIONS=mysql
|
||||
|
||||
LABEL_mysql=MySql localhost
|
||||
SERVER_mysql=localhost
|
||||
USER_mysql=root
|
||||
PASSWORD_mysql=test
|
||||
PORT_mysql=3307
|
||||
ENGINE_mysql=mysql@dbgate-plugin-mysql
|
||||
|
||||
SINGLE_CONNECTION=mysql
|
||||
SINGLE_DATABASE=Chinook
|
||||
|
||||
PERMISSIONS=files/charts/read
|
||||
@@ -18,7 +18,7 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"async-lock": "^1.2.4",
|
||||
"axios": "^0.19.0",
|
||||
"axios": "^0.21.1",
|
||||
"better-sqlite3-with-prebuilds": "^7.1.8",
|
||||
"body-parser": "^1.19.0",
|
||||
"bufferutil": "^4.0.1",
|
||||
@@ -26,6 +26,7 @@
|
||||
"compare-versions": "^3.6.0",
|
||||
"cors": "^2.8.5",
|
||||
"cross-env": "^6.0.3",
|
||||
"dbgate-query-splitter": "^4.1.1",
|
||||
"dbgate-sqltree": "^4.1.1",
|
||||
"dbgate-tools": "^4.1.1",
|
||||
"eslint": "^6.8.0",
|
||||
@@ -33,11 +34,12 @@
|
||||
"express-basic-auth": "^1.2.0",
|
||||
"express-fileupload": "^1.2.0",
|
||||
"fs-extra": "^9.1.0",
|
||||
"fs-reverse": "^0.0.3",
|
||||
"get-port": "^5.1.1",
|
||||
"http": "^0.0.0",
|
||||
"json-stable-stringify": "^1.0.1",
|
||||
"line-reader": "^0.4.0",
|
||||
"lodash": "^4.17.15",
|
||||
"lodash": "^4.17.21",
|
||||
"ncp": "^2.0.0",
|
||||
"nedb-promises": "^4.0.1",
|
||||
"node-cron": "^2.0.3",
|
||||
@@ -51,7 +53,9 @@
|
||||
"scripts": {
|
||||
"start": "env-cmd node src/index.js",
|
||||
"start:portal": "env-cmd -f .env-portal node src/index.js",
|
||||
"start:covid": "env-cmd -f .env-covid node src/index.js",
|
||||
"start:singledb": "env-cmd -f .env-singledb node src/index.js",
|
||||
"start:filedb": "env-cmd node src/index.js /home/jena/test/chinook/Chinook.db",
|
||||
"start:singleconn": "env-cmd node src/index.js --server localhost --user root --port 3307 --engine mysql@dbgate-plugin-mysql --password test",
|
||||
"ts": "tsc",
|
||||
"build": "webpack"
|
||||
},
|
||||
@@ -62,11 +66,11 @@
|
||||
"env-cmd": "^10.1.0",
|
||||
"node-loader": "^1.0.2",
|
||||
"nodemon": "^2.0.2",
|
||||
"typescript": "^3.7.4",
|
||||
"typescript": "^4.4.3",
|
||||
"webpack": "^4.42.0",
|
||||
"webpack-cli": "^3.3.11"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"msnodesqlv8": "^2.0.10"
|
||||
"msnodesqlv8": "^2.4.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ const _ = require('lodash');
|
||||
|
||||
const currentVersion = require('../currentVersion');
|
||||
const platformInfo = require('../utility/platformInfo');
|
||||
const connections = require('../controllers/connections');
|
||||
|
||||
module.exports = {
|
||||
settingsValue: {},
|
||||
@@ -21,30 +22,11 @@ module.exports = {
|
||||
|
||||
get_meta: 'get',
|
||||
async get() {
|
||||
// const toolbarButtons = process.env.TOOLBAR;
|
||||
// const toolbar = toolbarButtons
|
||||
// ? toolbarButtons.split(',').map((name) => ({
|
||||
// name,
|
||||
// icon: process.env[`ICON_${name}`],
|
||||
// title: process.env[`TITLE_${name}`],
|
||||
// page: process.env[`PAGE_${name}`],
|
||||
// }))
|
||||
// : null;
|
||||
// const startupPages = process.env.STARTUP_PAGES ? process.env.STARTUP_PAGES.split(',') : [];
|
||||
const permissions = process.env.PERMISSIONS ? process.env.PERMISSIONS.split(',') : null;
|
||||
const singleDatabase =
|
||||
process.env.SINGLE_CONNECTION && process.env.SINGLE_DATABASE
|
||||
? {
|
||||
conid: process.env.SINGLE_CONNECTION,
|
||||
database: process.env.SINGLE_DATABASE,
|
||||
}
|
||||
: null;
|
||||
|
||||
return {
|
||||
runAsPortal: !!process.env.CONNECTIONS,
|
||||
// toolbar,
|
||||
// startupPages,
|
||||
singleDatabase,
|
||||
runAsPortal: !!connections.portalConnections,
|
||||
singleDatabase: connections.singleDatabase,
|
||||
permissions,
|
||||
...currentVersion,
|
||||
};
|
||||
|
||||
@@ -8,6 +8,32 @@ const socket = require('../utility/socket');
|
||||
const { encryptConnection } = require('../utility/crypting');
|
||||
const { handleProcessCommunication } = require('../utility/processComm');
|
||||
|
||||
function getNamedArgs() {
|
||||
const res = {};
|
||||
for (let i = 0; i < process.argv.length; i++) {
|
||||
const name = process.argv[i];
|
||||
if (name.startsWith('--')) {
|
||||
let value = process.argv[i + 1];
|
||||
if (value && value.startsWith('--')) value = null;
|
||||
res[name.substring(2)] = value == null ? true : value;
|
||||
i++;
|
||||
} else {
|
||||
if (name.endsWith('.db') || name.endsWith('.sqlite') || name.endsWith('.sqlite3')) {
|
||||
res.databaseFile = name;
|
||||
res.engine = 'sqlite@dbgate-plugin-sqlite';
|
||||
}
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function getDatabaseFileLabel(databaseFile) {
|
||||
if (!databaseFile) return databaseFile;
|
||||
const m = databaseFile.match(/[\/]([^\/]+)$/);
|
||||
if (m) return m[1];
|
||||
return databaseFile;
|
||||
}
|
||||
|
||||
function getPortalCollections() {
|
||||
if (process.env.CONNECTIONS) {
|
||||
return _.compact(process.env.CONNECTIONS.split(',')).map(id => ({
|
||||
@@ -17,16 +43,79 @@ function getPortalCollections() {
|
||||
user: process.env[`USER_${id}`],
|
||||
password: process.env[`PASSWORD_${id}`],
|
||||
port: process.env[`PORT_${id}`],
|
||||
databaseUrl: process.env[`URL_${id}`],
|
||||
databaseFile: process.env[`FILE_${id}`],
|
||||
defaultDatabase: process.env[`DATABASE_${id}`],
|
||||
singleDatabase: !!process.env[`DATABASE_${id}`],
|
||||
displayName: process.env[`LABEL_${id}`],
|
||||
}));
|
||||
}
|
||||
|
||||
const args = getNamedArgs();
|
||||
if (args.databaseFile) {
|
||||
return [
|
||||
{
|
||||
_id: 'argv',
|
||||
databaseFile: args.databaseFile,
|
||||
singleDatabase: true,
|
||||
defaultDatabase: getDatabaseFileLabel(args.databaseFile),
|
||||
engine: args.engine,
|
||||
},
|
||||
];
|
||||
}
|
||||
if (args.databaseUrl) {
|
||||
return [
|
||||
{
|
||||
_id: 'argv',
|
||||
useDatabaseUrl: true,
|
||||
...args,
|
||||
},
|
||||
];
|
||||
}
|
||||
if (args.server) {
|
||||
return [
|
||||
{
|
||||
_id: 'argv',
|
||||
...args,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
const portalConnections = getPortalCollections();
|
||||
|
||||
function getSingleDatabase() {
|
||||
if (process.env.SINGLE_CONNECTION && process.env.SINGLE_DATABASE) {
|
||||
// @ts-ignore
|
||||
const connection = portalConnections.find(x => x._id == process.env.SINGLE_CONNECTION);
|
||||
return {
|
||||
connection,
|
||||
name: process.env.SINGLE_DATABASE,
|
||||
};
|
||||
}
|
||||
// @ts-ignore
|
||||
const arg0 = (portalConnections || []).find(x => x._id == 'argv');
|
||||
if (arg0) {
|
||||
// @ts-ignore
|
||||
if (arg0.singleDatabase) {
|
||||
return {
|
||||
connection: arg0,
|
||||
// @ts-ignore
|
||||
name: arg0.defaultDatabase,
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const singleDatabase = getSingleDatabase();
|
||||
|
||||
module.exports = {
|
||||
datastore: null,
|
||||
opened: [],
|
||||
singleDatabase,
|
||||
portalConnections,
|
||||
|
||||
async _init() {
|
||||
const dir = datadir();
|
||||
|
||||
@@ -18,6 +18,12 @@ module.exports = {
|
||||
existing.structure = structure;
|
||||
socket.emitChanged(`database-structure-changed-${conid}-${database}`);
|
||||
},
|
||||
handle_structureTime(conid, database, { analysedTime }) {
|
||||
const existing = this.opened.find(x => x.conid == conid && x.database == database);
|
||||
if (!existing) return;
|
||||
existing.analysedTime = analysedTime;
|
||||
socket.emitChanged(`database-status-changed-${conid}-${database}`);
|
||||
},
|
||||
handle_version(conid, database, { version }) {
|
||||
const existing = this.opened.find(x => x.conid == conid && x.database == database);
|
||||
if (!existing) return;
|
||||
@@ -106,6 +112,14 @@ module.exports = {
|
||||
return res;
|
||||
},
|
||||
|
||||
runScript_meta: 'post',
|
||||
async runScript({ conid, database, sql }) {
|
||||
console.log(`Processing script, conid=${conid}, database=${database}, sql=${sql}`);
|
||||
const opened = await this.ensureOpened(conid, database);
|
||||
const res = await this.sendRequest(opened, { msgtype: 'runScript', sql });
|
||||
return res;
|
||||
},
|
||||
|
||||
collectionData_meta: 'post',
|
||||
async collectionData({ conid, database, options }) {
|
||||
const opened = await this.ensureOpened(conid, database);
|
||||
@@ -123,9 +137,19 @@ module.exports = {
|
||||
status_meta: 'get',
|
||||
async status({ conid, database }) {
|
||||
const existing = this.opened.find(x => x.conid == conid && x.database == database);
|
||||
if (existing) return existing.status;
|
||||
if (existing) {
|
||||
return {
|
||||
...existing.status,
|
||||
analysedTime: existing.analysedTime,
|
||||
};
|
||||
}
|
||||
const lastClosed = this.closed[`${conid}/${database}`];
|
||||
if (lastClosed) return lastClosed.status;
|
||||
if (lastClosed) {
|
||||
return {
|
||||
...lastClosed.status,
|
||||
analysedTime: lastClosed.analysedTime,
|
||||
};
|
||||
}
|
||||
return {
|
||||
name: 'error',
|
||||
message: 'Not connected',
|
||||
@@ -156,6 +180,13 @@ module.exports = {
|
||||
return { status: 'ok' };
|
||||
},
|
||||
|
||||
syncModel_meta: 'post',
|
||||
async syncModel({ conid, database }) {
|
||||
const conn = await this.ensureOpened(conid, database);
|
||||
conn.subprocess.send({ msgtype: 'syncModel' });
|
||||
return { status: 'ok' };
|
||||
},
|
||||
|
||||
close(conid, database, kill = true) {
|
||||
const existing = this.opened.find(x => x.conid == conid && x.database == database);
|
||||
if (existing) {
|
||||
|
||||
@@ -9,10 +9,17 @@ const requirePlugin = require('../shell/requirePlugin');
|
||||
const downloadPackage = require('../utility/downloadPackage');
|
||||
const hasPermission = require('../utility/hasPermission');
|
||||
const _ = require('lodash');
|
||||
const packagedPluginsContent = require('../packagedPluginsContent');
|
||||
|
||||
module.exports = {
|
||||
script_meta: 'get',
|
||||
async script({ packageName }) {
|
||||
const packagedContent = packagedPluginsContent();
|
||||
|
||||
if (packagedContent && packagedContent[packageName]) {
|
||||
return packagedContent[packageName].frontend;
|
||||
}
|
||||
|
||||
const file1 = path.join(packagedPluginsDir(), packageName, 'dist', 'frontend.js');
|
||||
const file2 = path.join(pluginsdir(), packageName, 'dist', 'frontend.js');
|
||||
// @ts-ignore
|
||||
@@ -58,26 +65,37 @@ module.exports = {
|
||||
|
||||
installed_meta: 'get',
|
||||
async installed() {
|
||||
const files1 = await fs.readdir(packagedPluginsDir());
|
||||
const packagedContent = packagedPluginsContent();
|
||||
|
||||
const files1 = packagedContent ? _.keys(packagedContent) : await fs.readdir(packagedPluginsDir());
|
||||
const files2 = await fs.readdir(pluginsdir());
|
||||
|
||||
const res = [];
|
||||
for (const packageName of _.union(files1, files2)) {
|
||||
if (!/^dbgate-plugin-.*$/.test(packageName)) continue;
|
||||
try {
|
||||
const isPackaged = files1.includes(packageName);
|
||||
const manifest = await fs
|
||||
.readFile(path.join(isPackaged ? packagedPluginsDir() : pluginsdir(), packageName, 'package.json'), {
|
||||
encoding: 'utf-8',
|
||||
})
|
||||
.then(x => JSON.parse(x));
|
||||
const readmeFile = path.join(isPackaged ? packagedPluginsDir() : pluginsdir(), packageName, 'README.md');
|
||||
// @ts-ignore
|
||||
if (await fs.exists(readmeFile)) {
|
||||
manifest.readme = await fs.readFile(readmeFile, { encoding: 'utf-8' });
|
||||
if (packagedContent && packagedContent[packageName]) {
|
||||
const manifest = {
|
||||
...packagedContent[packageName].manifest,
|
||||
};
|
||||
manifest.isPackaged = true;
|
||||
manifest.readme = packagedContent[packageName].readme;
|
||||
res.push(manifest);
|
||||
} else {
|
||||
const isPackaged = files1.includes(packageName);
|
||||
const manifest = await fs
|
||||
.readFile(path.join(isPackaged ? packagedPluginsDir() : pluginsdir(), packageName, 'package.json'), {
|
||||
encoding: 'utf-8',
|
||||
})
|
||||
.then(x => JSON.parse(x));
|
||||
const readmeFile = path.join(isPackaged ? packagedPluginsDir() : pluginsdir(), packageName, 'README.md');
|
||||
// @ts-ignore
|
||||
if (await fs.exists(readmeFile)) {
|
||||
manifest.readme = await fs.readFile(readmeFile, { encoding: 'utf-8' });
|
||||
}
|
||||
manifest.isPackaged = isPackaged;
|
||||
res.push(manifest);
|
||||
}
|
||||
manifest.isPackaged = isPackaged;
|
||||
res.push(manifest);
|
||||
} catch (err) {
|
||||
console.log(`Skipped plugin ${packageName}, error:`, err.message);
|
||||
}
|
||||
@@ -135,8 +153,9 @@ module.exports = {
|
||||
async authTypes({ engine }) {
|
||||
const packageName = extractPackageName(engine);
|
||||
const content = requirePlugin(packageName);
|
||||
if (!content.driver || content.driver.engine != engine || !content.driver.getAuthTypes) return null;
|
||||
return content.driver.getAuthTypes() || null;
|
||||
const driver = content.drivers.find(x => x.engine == engine);
|
||||
if (!driver || !driver.getAuthTypes) return null;
|
||||
return driver.getAuthTypes() || null;
|
||||
},
|
||||
|
||||
// async _init() {
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
const fsReverse = require('fs-reverse');
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const { datadir } = require('../utility/directories');
|
||||
const _ = require('lodash');
|
||||
const { filterName } = require('dbgate-tools');
|
||||
const socket = require('../utility/socket');
|
||||
|
||||
function readCore(reader, skip, limit, filter) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const res = [];
|
||||
let readed = 0;
|
||||
reader.on('data', line => {
|
||||
if (!line && !line.trim()) return;
|
||||
try {
|
||||
const json = JSON.parse(line);
|
||||
if (filterName(filter, json.sql, json.database)) {
|
||||
if (!skip || readed >= skip) {
|
||||
res.push(json);
|
||||
}
|
||||
readed++;
|
||||
if (limit && readed > (skip || 0) + limit) {
|
||||
reader.destroy();
|
||||
resolve(res);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
reader.destroy();
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
reader.on('end', () => resolve(res));
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
read_meta: 'get',
|
||||
async read({ skip, limit, filter }) {
|
||||
const fileName = path.join(datadir(), 'query-history.jsonl');
|
||||
// @ts-ignore
|
||||
if (!(await fs.exists(fileName))) return [];
|
||||
const reader = fsReverse(fileName);
|
||||
const res = await readCore(reader, skip, limit, filter);
|
||||
return res;
|
||||
},
|
||||
|
||||
write_meta: 'post',
|
||||
async write({ data }) {
|
||||
const fileName = path.join(datadir(), 'query-history.jsonl');
|
||||
await fs.appendFile(fileName, JSON.stringify(data) + '\n');
|
||||
socket.emitChanged('query-history-changed');
|
||||
return 'OK';
|
||||
},
|
||||
};
|
||||
@@ -1,5 +1,8 @@
|
||||
const shell = require('./shell');
|
||||
const processArgs = require('./utility/processArgs');
|
||||
const dbgateTools = require('dbgate-tools');
|
||||
|
||||
global['DBGATE_TOOLS'] = dbgateTools;
|
||||
|
||||
if (processArgs.startProcess) {
|
||||
const proc = require('./proc');
|
||||
|
||||
@@ -27,6 +27,7 @@ const uploads = require('./controllers/uploads');
|
||||
const plugins = require('./controllers/plugins');
|
||||
const files = require('./controllers/files');
|
||||
const scheduler = require('./controllers/scheduler');
|
||||
const queryHistory = require('./controllers/queryHistory');
|
||||
|
||||
const { rundir } = require('./utility/directories');
|
||||
const platformInfo = require('./utility/platformInfo');
|
||||
@@ -102,6 +103,7 @@ function start() {
|
||||
useController(app, '/plugins', plugins);
|
||||
useController(app, '/files', files);
|
||||
useController(app, '/scheduler', scheduler);
|
||||
useController(app, '/query-history', queryHistory);
|
||||
|
||||
// if (process.env.PAGES_DIRECTORY) {
|
||||
// app.use('/pages', express.static(process.env.PAGES_DIRECTORY));
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const stableStringify = require('json-stable-stringify');
|
||||
const { splitQuery } = require('dbgate-query-splitter');
|
||||
const childProcessChecker = require('../utility/childProcessChecker');
|
||||
const { extractBoolSettingsValue, extractIntSettingsValue } = require('dbgate-tools');
|
||||
const requireEngineDriver = require('../utility/requireEngineDriver');
|
||||
@@ -12,6 +13,8 @@ let afterConnectCallbacks = [];
|
||||
let analysedStructure = null;
|
||||
let lastPing = null;
|
||||
let lastStatus = null;
|
||||
let analysedTime = 0;
|
||||
let serverVersion;
|
||||
|
||||
async function checkedAsyncCall(promise) {
|
||||
try {
|
||||
@@ -28,23 +31,42 @@ async function checkedAsyncCall(promise) {
|
||||
}
|
||||
}
|
||||
|
||||
let loadingModel = false;
|
||||
|
||||
async function handleFullRefresh() {
|
||||
loadingModel = true;
|
||||
const driver = requireEngineDriver(storedConnection);
|
||||
setStatusName('loadStructure');
|
||||
analysedStructure = await checkedAsyncCall(driver.analyseFull(systemConnection));
|
||||
analysedStructure = await checkedAsyncCall(driver.analyseFull(systemConnection, serverVersion));
|
||||
analysedTime = new Date().getTime();
|
||||
process.send({ msgtype: 'structure', structure: analysedStructure });
|
||||
process.send({ msgtype: 'structureTime', analysedTime });
|
||||
setStatusName('ok');
|
||||
loadingModel = false;
|
||||
}
|
||||
|
||||
async function handleIncrementalRefresh() {
|
||||
async function handleIncrementalRefresh(forceSend) {
|
||||
loadingModel = true;
|
||||
const driver = requireEngineDriver(storedConnection);
|
||||
setStatusName('checkStructure');
|
||||
const newStructure = await checkedAsyncCall(driver.analyseIncremental(systemConnection, analysedStructure));
|
||||
const newStructure = await checkedAsyncCall(driver.analyseIncremental(systemConnection, analysedStructure, serverVersion));
|
||||
analysedTime = new Date().getTime();
|
||||
if (newStructure != null) {
|
||||
analysedStructure = newStructure;
|
||||
}
|
||||
|
||||
if (forceSend || newStructure != null) {
|
||||
process.send({ msgtype: 'structure', structure: analysedStructure });
|
||||
}
|
||||
|
||||
process.send({ msgtype: 'structureTime', analysedTime });
|
||||
setStatusName('ok');
|
||||
loadingModel = false;
|
||||
}
|
||||
|
||||
function handleSyncModel() {
|
||||
if (loadingModel) return;
|
||||
handleIncrementalRefresh();
|
||||
}
|
||||
|
||||
function setStatus(status) {
|
||||
@@ -63,6 +85,7 @@ async function readVersion() {
|
||||
const driver = requireEngineDriver(storedConnection);
|
||||
const version = await driver.getVersion(systemConnection);
|
||||
process.send({ msgtype: 'version', version });
|
||||
serverVersion = version;
|
||||
}
|
||||
|
||||
async function handleConnect({ connection, structure, globalSettings }) {
|
||||
@@ -72,15 +95,15 @@ async function handleConnect({ connection, structure, globalSettings }) {
|
||||
if (!structure) setStatusName('pending');
|
||||
const driver = requireEngineDriver(storedConnection);
|
||||
systemConnection = await checkedAsyncCall(connectUtility(driver, storedConnection));
|
||||
readVersion();
|
||||
await checkedAsyncCall(readVersion());
|
||||
if (structure) {
|
||||
analysedStructure = structure;
|
||||
handleIncrementalRefresh();
|
||||
handleIncrementalRefresh(true);
|
||||
} else {
|
||||
handleFullRefresh();
|
||||
}
|
||||
|
||||
if (extractBoolSettingsValue(globalSettings, 'connection.autoRefresh', true)) {
|
||||
if (extractBoolSettingsValue(globalSettings, 'connection.autoRefresh', false)) {
|
||||
setInterval(
|
||||
handleIncrementalRefresh,
|
||||
extractIntSettingsValue(globalSettings, 'connection.autoRefreshInterval', 30, 3, 3600) * 1000
|
||||
@@ -100,6 +123,17 @@ function waitConnected() {
|
||||
});
|
||||
}
|
||||
|
||||
async function handleRunScript({ msgid, sql }) {
|
||||
await waitConnected();
|
||||
const driver = requireEngineDriver(storedConnection);
|
||||
try {
|
||||
await driver.script(systemConnection, sql);
|
||||
process.send({ msgtype: 'response', msgid });
|
||||
} catch (err) {
|
||||
process.send({ msgtype: 'response', msgid, errorMessage: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
async function handleQueryData({ msgid, sql }) {
|
||||
await waitConnected();
|
||||
const driver = requireEngineDriver(storedConnection);
|
||||
@@ -168,10 +202,12 @@ function handlePing() {
|
||||
const messageHandlers = {
|
||||
connect: handleConnect,
|
||||
queryData: handleQueryData,
|
||||
runScript: handleRunScript,
|
||||
updateCollection: handleUpdateCollection,
|
||||
collectionData: handleCollectionData,
|
||||
sqlPreview: handleSqlPreview,
|
||||
ping: handlePing,
|
||||
syncModel: handleSyncModel,
|
||||
// runCommand: handleRunCommand,
|
||||
};
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ async function handleConnect(connection) {
|
||||
systemConnection = await connectUtility(driver, storedConnection);
|
||||
readVersion();
|
||||
handleRefresh();
|
||||
if (extractBoolSettingsValue(globalSettings, 'connection.autoRefresh', true)) {
|
||||
if (extractBoolSettingsValue(globalSettings, 'connection.autoRefresh', false)) {
|
||||
setInterval(handleRefresh, extractIntSettingsValue(globalSettings, 'connection.autoRefreshInterval', 30, 5, 3600) * 1000);
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
@@ -3,7 +3,7 @@ const path = require('path');
|
||||
const fs = require('fs');
|
||||
const _ = require('lodash');
|
||||
const childProcessChecker = require('../utility/childProcessChecker');
|
||||
const goSplit = require('../utility/goSplit');
|
||||
const { splitQuery } = require('dbgate-query-splitter');
|
||||
|
||||
const { jsldir } = require('../utility/directories');
|
||||
const requireEngineDriver = require('../utility/requireEngineDriver');
|
||||
@@ -166,7 +166,7 @@ async function handleExecuteQuery({ sql }) {
|
||||
const resultIndexHolder = {
|
||||
value: 0,
|
||||
};
|
||||
for (const sqlItem of goSplit(sql)) {
|
||||
for (const sqlItem of splitQuery(sql, driver.getQuerySplitterOptions('stream'))) {
|
||||
await handleStream(driver, resultIndexHolder, sqlItem);
|
||||
// const handler = new StreamHandler(resultIndex);
|
||||
// const stream = await driver.stream(systemConnection, sqlItem, handler);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
const goSplit = require('../utility/goSplit');
|
||||
const { splitQuery } = require('dbgate-query-splitter');
|
||||
const requireEngineDriver = require('../utility/requireEngineDriver');
|
||||
const { decryptConnection } = require('../utility/crypting');
|
||||
const connectUtility = require('../utility/connectUtility');
|
||||
|
||||
async function executeQuery({ connection, sql }) {
|
||||
@@ -10,9 +9,9 @@ async function executeQuery({ connection, sql }) {
|
||||
const pool = await connectUtility(driver, connection);
|
||||
console.log(`Connected.`);
|
||||
|
||||
for (const sqlItem of goSplit(sql)) {
|
||||
for (const sqlItem of splitQuery(sql, driver.getQuerySplitterOptions('script'))) {
|
||||
console.log('Executing query', sqlItem);
|
||||
await driver.query(pool, sqlItem);
|
||||
await driver.query(pool, sqlItem, { discardResult: true });
|
||||
}
|
||||
|
||||
console.log(`Query finished`);
|
||||
|
||||
@@ -6,7 +6,9 @@ const copyStream = require('./copyStream');
|
||||
const fakeObjectReader = require('./fakeObjectReader');
|
||||
const consoleObjectWriter = require('./consoleObjectWriter');
|
||||
const jsonLinesWriter = require('./jsonLinesWriter');
|
||||
const jsonArrayWriter = require('./jsonArrayWriter');
|
||||
const jsonLinesReader = require('./jsonLinesReader');
|
||||
const sqlDataWriter = require('./sqlDataWriter');
|
||||
const jslDataReader = require('./jslDataReader');
|
||||
const archiveWriter = require('./archiveWriter');
|
||||
const archiveReader = require('./archiveReader');
|
||||
@@ -26,7 +28,9 @@ const dbgateApi = {
|
||||
tableReader,
|
||||
copyStream,
|
||||
jsonLinesWriter,
|
||||
jsonArrayWriter,
|
||||
jsonLinesReader,
|
||||
sqlDataWriter,
|
||||
fakeObjectReader,
|
||||
consoleObjectWriter,
|
||||
jslDataReader,
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
const fs = require('fs');
|
||||
const stream = require('stream');
|
||||
|
||||
class StringifyStream extends stream.Transform {
|
||||
constructor() {
|
||||
super({ objectMode: true });
|
||||
this.wasHeader = false;
|
||||
this.wasRecord = false;
|
||||
}
|
||||
_transform(chunk, encoding, done) {
|
||||
let skip = false;
|
||||
|
||||
if (!this.wasHeader) {
|
||||
skip =
|
||||
chunk.__isStreamHeader ||
|
||||
// TODO remove isArray test
|
||||
Array.isArray(chunk.columns);
|
||||
this.wasHeader = true;
|
||||
}
|
||||
if (!skip) {
|
||||
if (!this.wasRecord) {
|
||||
this.push('[\n');
|
||||
} else {
|
||||
this.push(',\n');
|
||||
}
|
||||
this.wasRecord = true;
|
||||
|
||||
this.push(JSON.stringify(chunk));
|
||||
}
|
||||
done();
|
||||
}
|
||||
|
||||
_flush(done) {
|
||||
if (!this.wasRecord) {
|
||||
this.push('[]\n');
|
||||
} else {
|
||||
this.push('\n]\n');
|
||||
}
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
||||
async function jsonArrayWriter({ fileName, encoding = 'utf-8' }) {
|
||||
console.log(`Writing file ${fileName}`);
|
||||
const stringify = new StringifyStream();
|
||||
const fileStream = fs.createWriteStream(fileName, encoding);
|
||||
stringify.pipe(fileStream);
|
||||
stringify['finisher'] = fileStream;
|
||||
return stringify;
|
||||
}
|
||||
|
||||
module.exports = jsonArrayWriter;
|
||||
@@ -22,7 +22,7 @@ function requirePlugin(packageName, requiredPlugin = null) {
|
||||
// @ts-ignore
|
||||
module = __non_webpack_require__(modulePath);
|
||||
} catch (err) {
|
||||
console.log('Failed load webpacked module', err.message);
|
||||
// console.log('Failed load webpacked module', err.message);
|
||||
module = require(modulePath);
|
||||
}
|
||||
requiredPlugin = module.__esModule ? module.default : module;
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
const fs = require('fs');
|
||||
const stream = require('stream');
|
||||
const path = require('path');
|
||||
const { driverBase } = require('dbgate-tools');
|
||||
const requireEngineDriver = require('../utility/requireEngineDriver');
|
||||
|
||||
class SqlizeStream extends stream.Transform {
|
||||
constructor({ fileName }) {
|
||||
super({ objectMode: true });
|
||||
this.wasHeader = false;
|
||||
this.tableName = path.parse(fileName).name;
|
||||
this.driver = driverBase;
|
||||
}
|
||||
_transform(chunk, encoding, done) {
|
||||
let skip = false;
|
||||
if (!this.wasHeader) {
|
||||
if (
|
||||
chunk.__isStreamHeader ||
|
||||
// TODO remove isArray test
|
||||
Array.isArray(chunk.columns)
|
||||
) {
|
||||
skip = true;
|
||||
this.tableName = chunk.pureName;
|
||||
if (chunk.engine) {
|
||||
// @ts-ignore
|
||||
this.driver = requireEngineDriver(chunk.engine) || driverBase;
|
||||
}
|
||||
}
|
||||
this.wasHeader = true;
|
||||
}
|
||||
if (!skip) {
|
||||
const dmp = this.driver.createDumper();
|
||||
dmp.put(
|
||||
'^insert ^into %f (%,i) ^values (%,v);\n',
|
||||
{ pureName: this.tableName },
|
||||
Object.keys(chunk),
|
||||
Object.values(chunk)
|
||||
);
|
||||
this.push(dmp.s);
|
||||
}
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
||||
async function sqlDataWriter({ fileName, driver, encoding = 'utf-8' }) {
|
||||
console.log(`Writing file ${fileName}`);
|
||||
const stringify = new SqlizeStream({ fileName });
|
||||
const fileStream = fs.createWriteStream(fileName, encoding);
|
||||
stringify.pipe(fileStream);
|
||||
stringify['finisher'] = fileStream;
|
||||
return stringify;
|
||||
}
|
||||
|
||||
module.exports = sqlDataWriter;
|
||||
@@ -70,14 +70,14 @@ function decryptPasswordField(connection, field) {
|
||||
function encryptConnection(connection) {
|
||||
connection = encryptPasswordField(connection, 'password');
|
||||
connection = encryptPasswordField(connection, 'sshPassword');
|
||||
connection = encryptPasswordField(connection, 'sshKeyFilePassword');
|
||||
connection = encryptPasswordField(connection, 'sshKeyfilePassword');
|
||||
return connection;
|
||||
}
|
||||
|
||||
function decryptConnection(connection) {
|
||||
connection = decryptPasswordField(connection, 'password');
|
||||
connection = decryptPasswordField(connection, 'sshPassword');
|
||||
connection = decryptPasswordField(connection, 'sshKeyFilePassword');
|
||||
connection = decryptPasswordField(connection, 'sshKeyfilePassword');
|
||||
return connection;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
function goSplit(sql) {
|
||||
if (!sql) return [];
|
||||
const lines = sql.split('\n');
|
||||
const res = [];
|
||||
let buffer = '';
|
||||
for (const line of lines) {
|
||||
if (/^\s*go\s*$/i.test(line)) {
|
||||
if (buffer.trim()) res.push(buffer);
|
||||
buffer = '';
|
||||
} else {
|
||||
buffer += line + '\n';
|
||||
}
|
||||
}
|
||||
if (buffer.trim()) res.push(buffer);
|
||||
return res;
|
||||
}
|
||||
|
||||
module.exports = goSplit;
|
||||
@@ -37,7 +37,7 @@ const platformInfo = {
|
||||
environment: process.env.NODE_ENV,
|
||||
platform,
|
||||
runningInWebpack: !!process.env.WEBPACK_DEV_SERVER_URL,
|
||||
defaultKeyFile: path.join(os.homedir(), '.ssh/id_rsa'),
|
||||
defaultKeyfile: path.join(os.homedir(), '.ssh/id_rsa'),
|
||||
};
|
||||
|
||||
module.exports = platformInfo;
|
||||
|
||||
@@ -15,7 +15,7 @@ function requireEngineDriver(connection) {
|
||||
if (engine.includes('@')) {
|
||||
const [shortName, packageName] = engine.split('@');
|
||||
const plugin = requirePlugin(packageName);
|
||||
return plugin.driver;
|
||||
return plugin.drivers.find(x => x.engine == engine);
|
||||
}
|
||||
throw new Error(`Could not found engine driver ${engine}`);
|
||||
}
|
||||
|
||||
@@ -16,9 +16,9 @@ const CONNECTION_FIELDS = [
|
||||
'sshLogin',
|
||||
'sshPassword',
|
||||
'sshMode',
|
||||
'sshKeyFile',
|
||||
'sshKeyfile',
|
||||
'sshBastionHost',
|
||||
'sshKeyFilePassword',
|
||||
'sshKeyfilePassword',
|
||||
];
|
||||
const TUNNEL_FIELDS = [...CONNECTION_FIELDS, 'server', 'port'];
|
||||
|
||||
@@ -31,7 +31,7 @@ async function getSshConnection(connection) {
|
||||
endPort: connection.sshPort || 22,
|
||||
bastionHost: connection.sshBastionHost || '',
|
||||
agentForward: connection.sshMode == 'agent',
|
||||
passphrase: connection.sshMode == 'keyFile' ? connection.sshKeyFilePassword : undefined,
|
||||
passphrase: connection.sshMode == 'keyFile' ? connection.sshKeyfilePassword : undefined,
|
||||
username: connection.sshLogin,
|
||||
password: connection.sshMode == 'userPassword' ? connection.sshPassword : undefined,
|
||||
agentSocket: connection.sshMode == 'agent' ? platformInfo.sshAuthSock : undefined,
|
||||
|
||||
@@ -17,6 +17,6 @@
|
||||
"devDependencies": {
|
||||
"dbgate-types": "^4.1.1",
|
||||
"@types/node": "^13.7.0",
|
||||
"typescript": "^3.7.5"
|
||||
"typescript": "^4.4.3"
|
||||
}
|
||||
}
|
||||
@@ -118,7 +118,11 @@ export function setChangeSetValue(
|
||||
};
|
||||
}
|
||||
|
||||
export function setChangeSetRowData(changeSet: ChangeSet, definition: ChangeSetRowDefinition, document: any): ChangeSet {
|
||||
export function setChangeSetRowData(
|
||||
changeSet: ChangeSet,
|
||||
definition: ChangeSetRowDefinition,
|
||||
document: any
|
||||
): ChangeSet {
|
||||
if (!changeSet || !definition) return changeSet;
|
||||
let [fieldName, existingItem] = findExistingChangeSetItem(changeSet, definition);
|
||||
if (fieldName == 'deletes') {
|
||||
@@ -213,7 +217,7 @@ function extractFields(item: ChangeSetItem, allowNulls = true): UpdateField[] {
|
||||
}));
|
||||
}
|
||||
|
||||
function insertToSql(
|
||||
function changeSetInsertToSql(
|
||||
item: ChangeSetItem,
|
||||
dbinfo: DatabaseInfo = null
|
||||
): [AllowIdentityInsert, Insert, AllowIdentityInsert] {
|
||||
@@ -257,7 +261,7 @@ function insertToSql(
|
||||
];
|
||||
}
|
||||
|
||||
function extractCondition(item: ChangeSetItem): Condition {
|
||||
export function extractChangeSetCondition(item: ChangeSetItem, alias?: string): Condition {
|
||||
return {
|
||||
conditionType: 'and',
|
||||
conditions: _.keys(item.condition).map(columnName => ({
|
||||
@@ -271,6 +275,7 @@ function extractCondition(item: ChangeSetItem): Condition {
|
||||
pureName: item.pureName,
|
||||
schemaName: item.schemaName,
|
||||
},
|
||||
alias,
|
||||
},
|
||||
},
|
||||
right: {
|
||||
@@ -281,7 +286,7 @@ function extractCondition(item: ChangeSetItem): Condition {
|
||||
};
|
||||
}
|
||||
|
||||
function updateToSql(item: ChangeSetItem): Update {
|
||||
function changeSetUpdateToSql(item: ChangeSetItem): Update {
|
||||
return {
|
||||
from: {
|
||||
name: {
|
||||
@@ -291,11 +296,11 @@ function updateToSql(item: ChangeSetItem): Update {
|
||||
},
|
||||
commandType: 'update',
|
||||
fields: extractFields(item),
|
||||
where: extractCondition(item),
|
||||
where: extractChangeSetCondition(item),
|
||||
};
|
||||
}
|
||||
|
||||
function deleteToSql(item: ChangeSetItem): Delete {
|
||||
function changeSetDeleteToSql(item: ChangeSetItem): Delete {
|
||||
return {
|
||||
from: {
|
||||
name: {
|
||||
@@ -304,16 +309,16 @@ function deleteToSql(item: ChangeSetItem): Delete {
|
||||
},
|
||||
},
|
||||
commandType: 'delete',
|
||||
where: extractCondition(item),
|
||||
where: extractChangeSetCondition(item),
|
||||
};
|
||||
}
|
||||
|
||||
export function changeSetToSql(changeSet: ChangeSet, dbinfo: DatabaseInfo): Command[] {
|
||||
return _.compact(
|
||||
_.flatten([
|
||||
...(changeSet.inserts.map(item => insertToSql(item, dbinfo)) as any),
|
||||
...changeSet.updates.map(updateToSql),
|
||||
...changeSet.deletes.map(deleteToSql),
|
||||
...(changeSet.inserts.map(item => changeSetInsertToSql(item, dbinfo)) as any),
|
||||
...changeSet.updates.map(changeSetUpdateToSql),
|
||||
...changeSet.deletes.map(changeSetDeleteToSql),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,7 @@
|
||||
import _ from 'lodash';
|
||||
import { GridConfig, GridCache, GridConfigColumns, createGridCache, GroupFunc } from './GridConfig';
|
||||
import {
|
||||
ForeignKeyInfo,
|
||||
TableInfo,
|
||||
ColumnInfo,
|
||||
EngineDriver,
|
||||
NamedObjectInfo,
|
||||
DatabaseInfo,
|
||||
SqlDialect,
|
||||
} from 'dbgate-types';
|
||||
import { parseFilter, getFilterType, getFilterValueExpression } from 'dbgate-filterparser';
|
||||
import { filterName } from './filterName';
|
||||
import { ChangeSetFieldDefinition, ChangeSetRowDefinition } from './ChangeSet';
|
||||
import { Expression, Select, treeToSql, dumpSqlSelect, Condition } from 'dbgate-sqltree';
|
||||
import { isTypeLogical } from 'dbgate-tools';
|
||||
import { TableInfo, EngineDriver, DatabaseInfo, SqlDialect } from 'dbgate-types';
|
||||
import { getFilterValueExpression } from 'dbgate-filterparser';
|
||||
import { ChangeCacheFunc, ChangeConfigFunc, DisplayColumn } from './GridDisplay';
|
||||
|
||||
export class FormViewDisplay {
|
||||
|
||||
@@ -33,6 +33,7 @@ export interface GridConfig extends GridConfigColumns {
|
||||
formViewKey?: { [uniqueName: string]: string };
|
||||
formViewKeyRequested?: { [uniqueName: string]: string };
|
||||
formFilterColumns: string[];
|
||||
formColumnFilterText?: string;
|
||||
}
|
||||
|
||||
export interface GridCache {
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
SqlDialect,
|
||||
} from 'dbgate-types';
|
||||
import { parseFilter, getFilterType } from 'dbgate-filterparser';
|
||||
import { filterName } from './filterName';
|
||||
import { filterName } from 'dbgate-tools';
|
||||
import { ChangeSetFieldDefinition, ChangeSetRowDefinition } from './ChangeSet';
|
||||
import { Expression, Select, treeToSql, dumpSqlSelect, Condition } from 'dbgate-sqltree';
|
||||
import { isTypeLogical } from 'dbgate-tools';
|
||||
@@ -75,6 +75,7 @@ export abstract class GridDisplay {
|
||||
}
|
||||
changeSetKeyFields: string[] = null;
|
||||
sortable = false;
|
||||
groupable = false;
|
||||
filterable = false;
|
||||
editable = false;
|
||||
isLoadedCorrectly = true;
|
||||
@@ -114,6 +115,10 @@ export abstract class GridDisplay {
|
||||
return this.getColumns(null).filter(col => col.isChecked || col.uniquePath.length == 1);
|
||||
}
|
||||
|
||||
getFkTarget(column: DisplayColumn): TableInfo {
|
||||
return null;
|
||||
}
|
||||
|
||||
reload() {
|
||||
this.setCache(reloadDataCacheFunc);
|
||||
}
|
||||
@@ -288,8 +293,8 @@ export abstract class GridDisplay {
|
||||
return this.config.expandedColumns.includes(uniqueName);
|
||||
}
|
||||
|
||||
toggleExpandedColumn(uniqueName: string) {
|
||||
this.includeInColumnSet('expandedColumns', uniqueName, !this.isExpandedColumn(uniqueName));
|
||||
toggleExpandedColumn(uniqueName: string, value?: boolean) {
|
||||
this.includeInColumnSet('expandedColumns', uniqueName, value == null ? !this.isExpandedColumn(uniqueName) : value);
|
||||
}
|
||||
|
||||
getFilter(uniqueName: string) {
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
Condition,
|
||||
OrderByExpression,
|
||||
} from 'dbgate-sqltree';
|
||||
import { filterName } from './filterName';
|
||||
import { TableGridDisplay } from './TableGridDisplay';
|
||||
import stableStringify from 'json-stable-stringify';
|
||||
import { ChangeSetFieldDefinition, ChangeSetRowDefinition } from './ChangeSet';
|
||||
@@ -265,8 +264,8 @@ export class TableFormViewDisplay extends FormViewDisplay {
|
||||
};
|
||||
}
|
||||
|
||||
toggleExpandedColumn(uniqueName: string) {
|
||||
this.gridDisplay.toggleExpandedColumn(uniqueName);
|
||||
toggleExpandedColumn(uniqueName: string, value?: boolean) {
|
||||
this.gridDisplay.toggleExpandedColumn(uniqueName, value);
|
||||
this.gridDisplay.reload();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import _ from 'lodash';
|
||||
import { filterName } from 'dbgate-tools';
|
||||
import { GridDisplay, ChangeCacheFunc, DisplayColumn, DisplayedColumnInfo, ChangeConfigFunc } from './GridDisplay';
|
||||
import { TableInfo, EngineDriver, ViewInfo, ColumnInfo, NamedObjectInfo, DatabaseInfo } from 'dbgate-types';
|
||||
import { GridConfig, GridCache, createGridCache } from './GridConfig';
|
||||
import { Expression, Select, treeToSql, dumpSqlSelect } from 'dbgate-sqltree';
|
||||
import { filterName } from './filterName';
|
||||
|
||||
export class TableGridDisplay extends GridDisplay {
|
||||
public table: TableInfo;
|
||||
@@ -35,6 +35,7 @@ export class TableGridDisplay extends GridDisplay {
|
||||
this.columns = this.getDisplayColumns(this.table, []);
|
||||
this.filterable = true;
|
||||
this.sortable = true;
|
||||
this.groupable = true;
|
||||
this.editable = true;
|
||||
this.supportsReload = true;
|
||||
this.baseTable = this.table;
|
||||
|
||||
@@ -17,6 +17,7 @@ export class ViewGridDisplay extends GridDisplay {
|
||||
this.columns = this.getDisplayColumns(view);
|
||||
this.filterable = true;
|
||||
this.sortable = true;
|
||||
this.groupable = true;
|
||||
this.editable = false;
|
||||
this.supportsReload = true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
import _ from 'lodash';
|
||||
import { Command, Insert, Update, Delete, UpdateField, Condition, AllowIdentityInsert } from 'dbgate-sqltree';
|
||||
import { NamedObjectInfo, DatabaseInfo, ForeignKeyInfo, TableInfo } from 'dbgate-types';
|
||||
import { ChangeSet, ChangeSetItem, extractChangeSetCondition } from './ChangeSet';
|
||||
|
||||
export interface ChangeSetDeleteCascade {
|
||||
title: string;
|
||||
commands: Command[];
|
||||
}
|
||||
|
||||
// function getDeleteScript()
|
||||
|
||||
function processDependencies(
|
||||
changeSet: ChangeSet,
|
||||
result: ChangeSetDeleteCascade[],
|
||||
allForeignKeys: ForeignKeyInfo[],
|
||||
fkPath: ForeignKeyInfo[],
|
||||
table: TableInfo,
|
||||
baseCmd: ChangeSetItem,
|
||||
dbinfo: DatabaseInfo
|
||||
) {
|
||||
if (result.find(x => x.title == table.pureName)) return;
|
||||
|
||||
const dependencies = allForeignKeys.filter(
|
||||
x => x.refSchemaName == table.schemaName && x.refTableName == table.pureName
|
||||
);
|
||||
|
||||
for (const fk of dependencies) {
|
||||
const depTable = dbinfo.tables.find(x => x.pureName == fk.pureName && x.schemaName == fk.schemaName);
|
||||
const subFkPath = [...fkPath, fk];
|
||||
if (depTable && depTable.pureName != baseCmd.pureName) {
|
||||
processDependencies(changeSet, result, allForeignKeys, subFkPath, depTable, baseCmd, dbinfo);
|
||||
}
|
||||
|
||||
const refCmd: Delete = {
|
||||
commandType: 'delete',
|
||||
from: {
|
||||
name: {
|
||||
pureName: fk.pureName,
|
||||
schemaName: fk.schemaName,
|
||||
},
|
||||
},
|
||||
where: {
|
||||
conditionType: 'exists',
|
||||
subQuery: {
|
||||
commandType: 'select',
|
||||
selectAll: true,
|
||||
from: {
|
||||
name: {
|
||||
pureName: fk.pureName,
|
||||
schemaName: fk.schemaName,
|
||||
},
|
||||
alias: 't0',
|
||||
relations: subFkPath.map((fkItem, fkIndex) => ({
|
||||
joinType: 'INNER JOIN',
|
||||
alias: `t${fkIndex + 1}`,
|
||||
name: {
|
||||
pureName: fkItem.refTableName,
|
||||
schemaName: fkItem.refSchemaName,
|
||||
},
|
||||
conditions: fkItem.columns.map(column => ({
|
||||
conditionType: 'binary',
|
||||
operator: '=',
|
||||
left: {
|
||||
exprType: 'column',
|
||||
columnName: column.columnName,
|
||||
source: { alias: `t${fkIndex}` },
|
||||
},
|
||||
right: {
|
||||
exprType: 'column',
|
||||
columnName: column.refColumnName,
|
||||
source: { alias: `t${fkIndex + 1}` },
|
||||
},
|
||||
})),
|
||||
})),
|
||||
},
|
||||
where: {
|
||||
conditionType: 'and',
|
||||
conditions: [
|
||||
extractChangeSetCondition(baseCmd, `t${subFkPath.length}`),
|
||||
// @ts-ignore
|
||||
...table.primaryKey.columns.map(column => ({
|
||||
conditionType: 'binary',
|
||||
operator: '=',
|
||||
left: {
|
||||
exprType: 'column',
|
||||
columnName: column.columnName,
|
||||
source: { alias: 't0' },
|
||||
},
|
||||
right: {
|
||||
exprType: 'column',
|
||||
columnName: column.columnName,
|
||||
source: {
|
||||
name: fk,
|
||||
},
|
||||
},
|
||||
})),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
let resItem = result.find(x => x.title == fk.pureName);
|
||||
if (!resItem) {
|
||||
resItem = {
|
||||
title: fk.pureName,
|
||||
commands: [],
|
||||
};
|
||||
result.push(resItem);
|
||||
}
|
||||
resItem.commands.push(refCmd);
|
||||
}
|
||||
}
|
||||
|
||||
export function getDeleteCascades(changeSet: ChangeSet, dbinfo: DatabaseInfo): ChangeSetDeleteCascade[] {
|
||||
const result: ChangeSetDeleteCascade[] = [];
|
||||
const allForeignKeys = _.flatten(dbinfo.tables.map(x => x.foreignKeys));
|
||||
for (const baseCmd of changeSet.deletes) {
|
||||
const table = dbinfo.tables.find(x => x.pureName == baseCmd.pureName && x.schemaName == baseCmd.schemaName);
|
||||
if (!table.primaryKey) continue;
|
||||
|
||||
processDependencies(changeSet, result, allForeignKeys, [], table, baseCmd, dbinfo);
|
||||
|
||||
// let resItem = result.find(x => x.title == baseCmd.pureName);
|
||||
// if (!resItem) {
|
||||
// resItem = {
|
||||
// title: baseCmd.pureName,
|
||||
// commands: [],
|
||||
// };
|
||||
// result.push(resItem);
|
||||
// }
|
||||
// resItem.commands.push(changeSetDeleteToSql(baseCmd));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -4,7 +4,6 @@ export * from './TableGridDisplay';
|
||||
export * from './ViewGridDisplay';
|
||||
export * from './JslGridDisplay';
|
||||
export * from './ChangeSet';
|
||||
export * from './filterName';
|
||||
export * from './FreeTableGridDisplay';
|
||||
export * from './FreeTableModel';
|
||||
export * from './MacroDefinition';
|
||||
@@ -12,3 +11,4 @@ export * from './runMacro';
|
||||
export * from './FormViewDisplay';
|
||||
export * from './TableFormViewDisplay';
|
||||
export * from './CollectionGridDisplay';
|
||||
export * from './deleteCascade';
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "tsc --watch",
|
||||
"test": "jest"
|
||||
"test": "jest",
|
||||
"test:ci": "jest --json --outputFile=result.json --testLocationInResults"
|
||||
},
|
||||
"files": [
|
||||
"lib"
|
||||
@@ -17,12 +18,12 @@
|
||||
"@types/node": "^13.7.0",
|
||||
"jest": "^24.9.0",
|
||||
"ts-jest": "^25.2.1",
|
||||
"typescript": "^3.7.5"
|
||||
"typescript": "^4.4.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/parsimmon": "^1.10.1",
|
||||
"dbgate-tools": "^4.1.1",
|
||||
"lodash": "^4.17.15",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.24.0",
|
||||
"parsimmon": "^1.13.0"
|
||||
}
|
||||
|
||||
@@ -28,6 +28,20 @@ const numberTestCondition = () => value => ({
|
||||
],
|
||||
});
|
||||
|
||||
const objectIdTestCondition = () => value => ({
|
||||
$or: [
|
||||
{
|
||||
__placeholder__: {
|
||||
$regex: `.*${value}.*`,
|
||||
$options: 'i',
|
||||
},
|
||||
},
|
||||
{
|
||||
__placeholder__: { $oid: value },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const testCondition = (operator, value) => () => ({
|
||||
__placeholder__: {
|
||||
[operator]: value,
|
||||
@@ -64,9 +78,12 @@ const createParser = () => {
|
||||
.map(Number)
|
||||
.desc('number'),
|
||||
|
||||
objectid: () => token(P.regexp(/[0-9a-f]{24}/)).desc('ObjectId'),
|
||||
|
||||
noQuotedString: () => P.regexp(/[^\s^,^'^"]+/).desc('string unquoted'),
|
||||
|
||||
value: r => P.alt(r.string1, r.string2, r.number, r.noQuotedString),
|
||||
value: r => P.alt(r.objectid, r.string1, r.string2, r.number, r.noQuotedString),
|
||||
valueTestObjectId: r => r.objectid.map(objectIdTestCondition()),
|
||||
valueTestNum: r => r.number.map(numberTestCondition()),
|
||||
valueTest: r => r.value.map(regexCondition('.*#VALUE#.*')),
|
||||
|
||||
@@ -108,6 +125,7 @@ const createParser = () => {
|
||||
r.startsWithNot,
|
||||
r.endsWithNot,
|
||||
r.containsNot,
|
||||
r.valueTestObjectId,
|
||||
r.valueTestNum,
|
||||
r.valueTest
|
||||
).trim(whitespace),
|
||||
|
||||
@@ -3,5 +3,9 @@ import { parseFilter } from './parseFilter';
|
||||
test('parse string', () => {
|
||||
const ast = parseFilter('"123"', 'string');
|
||||
console.log(JSON.stringify(ast));
|
||||
expect(ast).toBe(3);
|
||||
expect(ast).toEqual({
|
||||
conditionType: 'like',
|
||||
left: { exprType: 'placeholder' },
|
||||
right: { exprType: 'value', value: '%123%' },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
lib
|
||||
@@ -0,0 +1,36 @@
|
||||
[](https://www.npmjs.com/package/dbgate-query-splitter)
|
||||
|
||||
dbgate-query-splitter
|
||||
====================
|
||||
|
||||
Splits long SQL query into into particular statements. Designed to have zero dependencies and to be fast.
|
||||
|
||||
Supports following SQL dialects:
|
||||
* MySQL
|
||||
* PostgreSQL
|
||||
* SQLite
|
||||
* Microsoft SQL Server
|
||||
|
||||
## Usage
|
||||
|
||||
```js
|
||||
import { splitQuery, mysqlSplitterOptions, mssqlSplitterOptions, postgreSplitterOptions } from 'dbgate-query-splitter';
|
||||
|
||||
const output = splitQuery('SELECT * FROM `table1`;SELECT * FROM `table2`;', mysqlSplitterOptions);
|
||||
|
||||
// output is ['SELECT * FROM `table1`', 'SELECT * FROM `table2`']
|
||||
|
||||
```
|
||||
|
||||
## Contributing
|
||||
Please run tests before pushing any changes.
|
||||
|
||||
```sh
|
||||
yarn test
|
||||
```
|
||||
|
||||
## Supported syntax
|
||||
* Comments
|
||||
* Dollar strings (PostgreSQL)
|
||||
* GO separators (MS SQL)
|
||||
* Custom delimiter, setby DELIMITER keyword (MySQL)
|
||||
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
moduleFileExtensions: ['js'],
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"version": "4.1.1",
|
||||
"name": "dbgate-query-splitter",
|
||||
"main": "lib/index.js",
|
||||
"typings": "lib/index.d.ts",
|
||||
"description": "SQL Query splitter for verious database engines",
|
||||
"homepage": "https://github.com/dbgate/dbgate/tree/master/packages/query-splitter",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dbgate/dbgate"
|
||||
},
|
||||
"author": "Jan Prochazka",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"SQL",
|
||||
"query",
|
||||
"split",
|
||||
"parse"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "tsc --watch",
|
||||
"test": "jest",
|
||||
"test:ci": "jest --json --outputFile=result.json --testLocationInResults"
|
||||
},
|
||||
"files": [
|
||||
"lib"
|
||||
],
|
||||
"devDependencies": {
|
||||
"dbgate-types": "^4.1.1",
|
||||
"@types/jest": "^25.1.4",
|
||||
"@types/node": "^13.7.0",
|
||||
"jest": "^24.9.0",
|
||||
"ts-jest": "^25.2.1",
|
||||
"typescript": "^4.4.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './splitQuery';
|
||||
export * from './options';
|
||||
@@ -0,0 +1,66 @@
|
||||
export interface SplitterOptions {
|
||||
stringsBegins: string[];
|
||||
stringsEnds: { [begin: string]: string };
|
||||
stringEscapes: { [begin: string]: string };
|
||||
|
||||
allowSemicolon: boolean;
|
||||
allowCustomDelimiter: boolean;
|
||||
allowGoDelimiter: boolean;
|
||||
allowDollarDollarString: boolean;
|
||||
noSplit: boolean;
|
||||
}
|
||||
|
||||
export const defaultSplitterOptions: SplitterOptions = {
|
||||
stringsBegins: ["'"],
|
||||
stringsEnds: { "'": "'" },
|
||||
stringEscapes: { "'": "'" },
|
||||
|
||||
allowSemicolon: true,
|
||||
allowCustomDelimiter: false,
|
||||
allowGoDelimiter: false,
|
||||
allowDollarDollarString: false,
|
||||
noSplit: false,
|
||||
};
|
||||
|
||||
export const mysqlSplitterOptions: SplitterOptions = {
|
||||
...defaultSplitterOptions,
|
||||
|
||||
allowCustomDelimiter: true,
|
||||
stringsBegins: ["'", '`'],
|
||||
stringsEnds: { "'": "'", '`': '`' },
|
||||
stringEscapes: { "'": '\\', '`': '`' },
|
||||
};
|
||||
|
||||
export const mssqlSplitterOptions: SplitterOptions = {
|
||||
...defaultSplitterOptions,
|
||||
allowSemicolon: false,
|
||||
allowGoDelimiter: true,
|
||||
|
||||
stringsBegins: ["'", '['],
|
||||
stringsEnds: { "'": "'", '[': ']' },
|
||||
stringEscapes: { "'": "'" },
|
||||
};
|
||||
|
||||
export const postgreSplitterOptions: SplitterOptions = {
|
||||
...defaultSplitterOptions,
|
||||
|
||||
allowDollarDollarString: true,
|
||||
|
||||
stringsBegins: ["'", '"'],
|
||||
stringsEnds: { "'": "'", '"': '"' },
|
||||
stringEscapes: { "'": "'", '"': '"' },
|
||||
};
|
||||
|
||||
export const sqliteSplitterOptions: SplitterOptions = {
|
||||
...defaultSplitterOptions,
|
||||
|
||||
stringsBegins: ["'", '"'],
|
||||
stringsEnds: { "'": "'", '"': '"' },
|
||||
stringEscapes: { "'": "'", '"': '"' },
|
||||
};
|
||||
|
||||
export const noSplitSplitterOptions: SplitterOptions = {
|
||||
...defaultSplitterOptions,
|
||||
|
||||
noSplit: true,
|
||||
};
|
||||
@@ -0,0 +1,257 @@
|
||||
import { SplitterOptions, defaultSplitterOptions } from './options';
|
||||
|
||||
const SEMICOLON = ';';
|
||||
|
||||
interface SplitExecutionContext {
|
||||
options: SplitterOptions;
|
||||
source: string;
|
||||
position: number;
|
||||
currentDelimiter: string;
|
||||
output: string[];
|
||||
end: number;
|
||||
wasDataOnLine: boolean;
|
||||
currentCommandStart: number;
|
||||
|
||||
// unread: string;
|
||||
// currentStatement: string;
|
||||
// semicolonKeyTokenRegex: RegExp;
|
||||
}
|
||||
|
||||
function isStringEnd(s: string, pos: number, endch: string, escapech: string) {
|
||||
if (!escapech) {
|
||||
return s[pos] == endch;
|
||||
}
|
||||
if (endch == escapech) {
|
||||
return s[pos] == endch && s[pos + 1] != endch;
|
||||
} else {
|
||||
return s[pos] == endch && s[pos - 1] != escapech;
|
||||
}
|
||||
}
|
||||
|
||||
interface Token {
|
||||
type: 'string' | 'delimiter' | 'whitespace' | 'eoln' | 'data' | 'set_delimiter' | 'comment' | 'go_delimiter';
|
||||
length: number;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
const WHITESPACE_TOKEN: Token = {
|
||||
type: 'whitespace',
|
||||
length: 1,
|
||||
};
|
||||
const EOLN_TOKEN: Token = {
|
||||
type: 'eoln',
|
||||
length: 1,
|
||||
};
|
||||
const DATA_TOKEN: Token = {
|
||||
type: 'data',
|
||||
length: 1,
|
||||
};
|
||||
|
||||
function scanDollarQuotedString(context: SplitExecutionContext): Token {
|
||||
if (!context.options.allowDollarDollarString) return null;
|
||||
|
||||
let pos = context.position;
|
||||
const s = context.source;
|
||||
|
||||
const match = /^(\$[a-zA-Z0-9_]*\$)/.exec(s.slice(pos));
|
||||
if (!match) return null;
|
||||
const label = match[1];
|
||||
pos += label.length;
|
||||
|
||||
while (pos < context.end) {
|
||||
if (s.slice(pos).startsWith(label)) {
|
||||
return {
|
||||
type: 'string',
|
||||
length: pos + label.length - context.position,
|
||||
};
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function scanToken(context: SplitExecutionContext): Token {
|
||||
let pos = context.position;
|
||||
const s = context.source;
|
||||
const ch = s[pos];
|
||||
|
||||
if (context.options.stringsBegins.includes(ch)) {
|
||||
pos++;
|
||||
const endch = context.options.stringsEnds[ch];
|
||||
const escapech = context.options.stringEscapes[ch];
|
||||
while (pos < context.end && !isStringEnd(s, pos, endch, escapech)) {
|
||||
if (endch == escapech && s[pos] == endch && s[pos + 1] == endch) {
|
||||
pos += 2;
|
||||
} else {
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
return {
|
||||
type: 'string',
|
||||
length: pos - context.position + 1,
|
||||
};
|
||||
}
|
||||
|
||||
if (context.currentDelimiter && s.slice(pos).startsWith(context.currentDelimiter)) {
|
||||
return {
|
||||
type: 'delimiter',
|
||||
length: context.currentDelimiter.length,
|
||||
};
|
||||
}
|
||||
|
||||
if (ch == ' ' || ch == '\t' || ch == '\r') {
|
||||
return WHITESPACE_TOKEN;
|
||||
}
|
||||
|
||||
if (ch == '\n') {
|
||||
return EOLN_TOKEN;
|
||||
}
|
||||
|
||||
if (ch == '-' && s[pos + 1] == '-') {
|
||||
while (pos < context.end && s[pos] != '\n') pos++;
|
||||
return {
|
||||
type: 'comment',
|
||||
length: pos - context.position,
|
||||
};
|
||||
}
|
||||
|
||||
if (ch == '/' && s[pos + 1] == '*') {
|
||||
pos += 2;
|
||||
while (pos < context.end) {
|
||||
if (s[pos] == '*' && s[pos + 1] == '/') break;
|
||||
pos++;
|
||||
}
|
||||
return {
|
||||
type: 'comment',
|
||||
length: pos - context.position + 2,
|
||||
};
|
||||
}
|
||||
|
||||
if (context.options.allowCustomDelimiter && !context.wasDataOnLine) {
|
||||
const m = s.slice(pos).match(/^DELIMITER[ \t]+([^\n]+)/i);
|
||||
if (m) {
|
||||
return {
|
||||
type: 'set_delimiter',
|
||||
value: m[1].trim(),
|
||||
length: m[0].length,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (context.options.allowGoDelimiter && !context.wasDataOnLine) {
|
||||
const m = s.slice(pos).match(/^GO[\t\r ]*(\n|$)/i);
|
||||
if (m) {
|
||||
return {
|
||||
type: 'go_delimiter',
|
||||
length: m[0].length - 1,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const dollarString = scanDollarQuotedString(context);
|
||||
if (dollarString) return dollarString;
|
||||
|
||||
return DATA_TOKEN;
|
||||
}
|
||||
|
||||
function pushQuery(context) {
|
||||
const sql = context.source.slice(context.currentCommandStart, context.position);
|
||||
const trimmed = sql.trim();
|
||||
if (trimmed) context.output.push(trimmed);
|
||||
}
|
||||
|
||||
export function splitQuery(sql: string, options: SplitterOptions = null): string[] {
|
||||
const usedOptions = {
|
||||
...defaultSplitterOptions,
|
||||
...options,
|
||||
};
|
||||
|
||||
if (usedOptions.noSplit) {
|
||||
return [sql];
|
||||
}
|
||||
|
||||
const context: SplitExecutionContext = {
|
||||
source: sql,
|
||||
end: sql.length,
|
||||
currentDelimiter: options?.allowSemicolon === false ? null : SEMICOLON,
|
||||
position: 0,
|
||||
currentCommandStart: 0,
|
||||
output: [],
|
||||
wasDataOnLine: false,
|
||||
options: usedOptions,
|
||||
};
|
||||
|
||||
while (context.position < context.end) {
|
||||
const token = scanToken(context);
|
||||
if (!token) {
|
||||
// nothing special, move forward
|
||||
context.position += 1;
|
||||
continue;
|
||||
}
|
||||
switch (token.type) {
|
||||
case 'string':
|
||||
context.position += token.length;
|
||||
context.wasDataOnLine = true;
|
||||
break;
|
||||
case 'comment':
|
||||
context.position += token.length;
|
||||
context.wasDataOnLine = true;
|
||||
break;
|
||||
case 'eoln':
|
||||
context.position += token.length;
|
||||
context.wasDataOnLine = false;
|
||||
break;
|
||||
case 'data':
|
||||
context.position += token.length;
|
||||
context.wasDataOnLine = true;
|
||||
break;
|
||||
case 'whitespace':
|
||||
context.position += token.length;
|
||||
break;
|
||||
case 'set_delimiter':
|
||||
pushQuery(context);
|
||||
context.currentDelimiter = token.value;
|
||||
context.position += token.length;
|
||||
context.currentCommandStart = context.position;
|
||||
break;
|
||||
case 'go_delimiter':
|
||||
pushQuery(context);
|
||||
context.position += token.length;
|
||||
context.currentCommandStart = context.position;
|
||||
break;
|
||||
case 'delimiter':
|
||||
pushQuery(context);
|
||||
context.position += token.length;
|
||||
context.currentCommandStart = context.position;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (context.end > context.currentCommandStart) {
|
||||
pushQuery(context);
|
||||
}
|
||||
|
||||
// context.semicolonKeyTokenRegex = buildKeyTokenRegex(SEMICOLON, context);
|
||||
// let findResult: FindExpResult = {
|
||||
// expIndex: -1,
|
||||
// exp: null,
|
||||
// nextIndex: 0,
|
||||
// };
|
||||
// let lastUnreadLength;
|
||||
// do {
|
||||
// // console.log('context.unread', context.unread);
|
||||
// lastUnreadLength = context.unread.length;
|
||||
// findResult = findKeyToken(context.unread, context.currentDelimiter, context);
|
||||
// handleKeyTokenFindResult(context, findResult);
|
||||
// // Prevent infinite loop by returning incorrect result
|
||||
// if (lastUnreadLength === context.unread.length) {
|
||||
// read(context, context.unread.length);
|
||||
// }
|
||||
// } while (context.unread !== '');
|
||||
// publishStatement(context);
|
||||
|
||||
// console.log('RESULT', context.output);
|
||||
|
||||
return context.output;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { mysqlSplitterOptions, mssqlSplitterOptions, postgreSplitterOptions, noSplitSplitterOptions } from './options';
|
||||
import { splitQuery } from './splitQuery';
|
||||
|
||||
test('simple query', () => {
|
||||
const output = splitQuery('select * from A');
|
||||
expect(output).toEqual(['select * from A']);
|
||||
});
|
||||
|
||||
test('correct split 2 queries', () => {
|
||||
const output = splitQuery('SELECT * FROM `table1`;SELECT * FROM `table2`;', mysqlSplitterOptions);
|
||||
expect(output).toEqual(['SELECT * FROM `table1`', 'SELECT * FROM `table2`']);
|
||||
});
|
||||
|
||||
test('correct split 2 queries - no end semicolon', () => {
|
||||
const output = splitQuery('SELECT * FROM `table1`;SELECT * FROM `table2`', mysqlSplitterOptions);
|
||||
expect(output).toEqual(['SELECT * FROM `table1`', 'SELECT * FROM `table2`']);
|
||||
});
|
||||
|
||||
test('delete empty query', () => {
|
||||
const output = splitQuery(';;;\n;;SELECT * FROM `table1`;;;;;SELECT * FROM `table2`;;; ;;;', mysqlSplitterOptions);
|
||||
expect(output).toEqual(['SELECT * FROM `table1`', 'SELECT * FROM `table2`']);
|
||||
});
|
||||
|
||||
test('should handle double backtick', () => {
|
||||
const input = ['CREATE TABLE `a``b` (`c"d` INT)', 'CREATE TABLE `a````b` (`c"d` INT)'];
|
||||
const output = splitQuery(input.join(';\n') + ';', mysqlSplitterOptions);
|
||||
expect(output).toEqual(input);
|
||||
});
|
||||
|
||||
test('semicolon inside string', () => {
|
||||
const input = ['CREATE TABLE a', "INSERT INTO a (x) VALUES ('1;2;3;4')"];
|
||||
const output = splitQuery(input.join(';\n') + ';', mysqlSplitterOptions);
|
||||
expect(output).toEqual(input);
|
||||
});
|
||||
|
||||
test('semicolon inside identyifier - mssql', () => {
|
||||
const input = ['CREATE TABLE [a;1]', "INSERT INTO [a;1] (x) VALUES ('1')"];
|
||||
const output = splitQuery(input.join(';\n') + ';', {
|
||||
...mssqlSplitterOptions,
|
||||
allowSemicolon: true,
|
||||
});
|
||||
expect(output).toEqual(input);
|
||||
});
|
||||
|
||||
test('delimiter test', () => {
|
||||
const input = 'SELECT 1;\n DELIMITER $$\n SELECT 2; SELECT 3; \n DELIMITER ;';
|
||||
const output = splitQuery(input, mysqlSplitterOptions);
|
||||
expect(output).toEqual(['SELECT 1', 'SELECT 2; SELECT 3;']);
|
||||
});
|
||||
|
||||
test('one line comment test', () => {
|
||||
const input = 'SELECT 1 -- comment1;comment2\n;SELECT 2';
|
||||
const output = splitQuery(input, mysqlSplitterOptions);
|
||||
expect(output).toEqual(['SELECT 1 -- comment1;comment2', 'SELECT 2']);
|
||||
});
|
||||
|
||||
test('multi line comment test', () => {
|
||||
const input = 'SELECT 1 /* comment1;comment2\ncomment3*/;SELECT 2';
|
||||
const output = splitQuery(input, mysqlSplitterOptions);
|
||||
expect(output).toEqual(['SELECT 1 /* comment1;comment2\ncomment3*/', 'SELECT 2']);
|
||||
});
|
||||
|
||||
test('dollar string', () => {
|
||||
const input = 'CREATE PROC $$ SELECT 1; SELECT 2; $$ ; SELECT 3';
|
||||
const output = splitQuery(input, postgreSplitterOptions);
|
||||
expect(output).toEqual(['CREATE PROC $$ SELECT 1; SELECT 2; $$', 'SELECT 3']);
|
||||
});
|
||||
|
||||
test('go delimiter', () => {
|
||||
const input = 'SELECT 1\ngo\nSELECT 2';
|
||||
const output = splitQuery(input, mssqlSplitterOptions);
|
||||
expect(output).toEqual(['SELECT 1', 'SELECT 2']);
|
||||
});
|
||||
|
||||
test('no split', () => {
|
||||
const input = 'SELECT 1;SELECT 2';
|
||||
const output = splitQuery(input, noSplitSplitterOptions);
|
||||
expect(output).toEqual(['SELECT 1;SELECT 2']);
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2015",
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "lib",
|
||||
"preserveWatchOutput": true,
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
||||
@@ -28,9 +28,9 @@
|
||||
"devDependencies": {
|
||||
"@types/node": "^13.7.0",
|
||||
"dbgate-types": "^4.1.1",
|
||||
"typescript": "^3.7.5"
|
||||
"typescript": "^4.4.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.15"
|
||||
"lodash": "^4.17.21"
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,7 @@ export function dumpSqlCondition(dmp: SqlDumper, condition: Condition) {
|
||||
break;
|
||||
case 'like':
|
||||
dumpSqlExpression(dmp, condition.left);
|
||||
dmp.put(' ^like ');
|
||||
dmp.put(dmp.dialect.ilike ? ' ^ilike ' : ' ^like ');
|
||||
dumpSqlExpression(dmp, condition.right);
|
||||
break;
|
||||
case 'notLike':
|
||||
|
||||
@@ -28,9 +28,11 @@
|
||||
"dbgate-types": "^4.1.1",
|
||||
"jest": "^24.9.0",
|
||||
"ts-jest": "^25.2.1",
|
||||
"typescript": "^3.7.5"
|
||||
"typescript": "^4.4.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.15"
|
||||
"lodash": "^4.17.21",
|
||||
"dbgate-query-splitter": "^4.1.1",
|
||||
"uuid": "^3.4.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,64 @@
|
||||
import { DatabaseInfo, DatabaseModification, EngineDriver } from 'dbgate-types';
|
||||
import { DatabaseInfo, DatabaseModification, EngineDriver, SqlDialect } from 'dbgate-types';
|
||||
import _sortBy from 'lodash/sortBy';
|
||||
import _groupBy from 'lodash/groupBy';
|
||||
import _pick from 'lodash/pick';
|
||||
import _compact from 'lodash/compact';
|
||||
|
||||
const STRUCTURE_FIELDS = ['tables', 'collections', 'views', 'matviews', 'functions', 'procedures', 'triggers'];
|
||||
|
||||
const fp_pick = arg => array => _pick(array, arg);
|
||||
|
||||
export class DatabaseAnalyser {
|
||||
structure: DatabaseInfo;
|
||||
modifications: DatabaseModification[];
|
||||
singleObjectFilter: any;
|
||||
singleObjectId: string = null;
|
||||
dialect: SqlDialect;
|
||||
|
||||
constructor(public pool, public driver: EngineDriver) {}
|
||||
constructor(public pool, public driver: EngineDriver, version) {
|
||||
this.dialect = (driver?.dialectByVersion && driver?.dialectByVersion(version)) || driver?.dialect;
|
||||
}
|
||||
|
||||
async _runAnalysis() {
|
||||
return DatabaseAnalyser.createEmptyStructure();
|
||||
}
|
||||
|
||||
/** @returns {Promise<import('dbgate-types').DatabaseModification[]>} */
|
||||
async getModifications() {
|
||||
if (this.structure == null) throw new Error('DatabaseAnalyse.getModifications - structure must be filled');
|
||||
|
||||
async _getFastSnapshot(): Promise<DatabaseInfo> {
|
||||
return null;
|
||||
}
|
||||
|
||||
async _computeSingleObjectId() {}
|
||||
|
||||
addEngineField(db: DatabaseInfo) {
|
||||
if (!this.driver?.engine) return;
|
||||
for (const field of STRUCTURE_FIELDS) {
|
||||
if (!db[field]) continue;
|
||||
for (const item of db[field]) {
|
||||
item.engine = this.driver.engine;
|
||||
}
|
||||
}
|
||||
db.engine = this.driver.engine;
|
||||
return db;
|
||||
}
|
||||
|
||||
async fullAnalysis() {
|
||||
return this._runAnalysis();
|
||||
const res = this.addEngineField(await this._runAnalysis());
|
||||
// console.log('FULL ANALYSIS', res);
|
||||
return res;
|
||||
}
|
||||
|
||||
async singleObjectAnalysis(name, typeField) {
|
||||
// console.log('Analysing SINGLE OBJECT', name, typeField);
|
||||
this.singleObjectFilter = { ...name, typeField };
|
||||
await this._computeSingleObjectId();
|
||||
const res = this.addEngineField(await this._runAnalysis());
|
||||
// console.log('SINGLE OBJECT RES', res);
|
||||
const obj =
|
||||
res[typeField]?.length == 1
|
||||
? res[typeField][0]
|
||||
: res[typeField]?.find(x => x.pureName == name.pureName && x.schemaName == name.schemaName);
|
||||
// console.log('SINGLE OBJECT', obj);
|
||||
return obj;
|
||||
}
|
||||
|
||||
async incrementalAnalysis(structure) {
|
||||
@@ -33,11 +68,11 @@ export class DatabaseAnalyser {
|
||||
if (this.modifications == null) {
|
||||
// modifications not implemented, perform full analysis
|
||||
this.structure = null;
|
||||
return this._runAnalysis();
|
||||
return this.addEngineField(await this._runAnalysis());
|
||||
}
|
||||
if (this.modifications.length == 0) return null;
|
||||
console.log('DB modifications detected:', this.modifications);
|
||||
return this._runAnalysis();
|
||||
return this.addEngineField(this.mergeAnalyseResult(await this._runAnalysis()));
|
||||
}
|
||||
|
||||
mergeAnalyseResult(newlyAnalysed) {
|
||||
@@ -49,7 +84,7 @@ export class DatabaseAnalyser {
|
||||
}
|
||||
|
||||
const res = {};
|
||||
for (const field of ['tables', 'collections', 'views', 'functions', 'procedures', 'triggers']) {
|
||||
for (const field of STRUCTURE_FIELDS) {
|
||||
const removedIds = this.modifications
|
||||
.filter(x => x.action == 'remove' && x.objectTypeField == field)
|
||||
.map(x => x.objectId);
|
||||
@@ -57,9 +92,19 @@ export class DatabaseAnalyser {
|
||||
const addedChangedIds = newArray.map(x => x.objectId);
|
||||
const removeAllIds = [...removedIds, ...addedChangedIds];
|
||||
res[field] = _sortBy(
|
||||
[...this.structure[field].filter(x => !removeAllIds.includes(x.objectId)), ...newArray],
|
||||
[...(this.structure[field] || []).filter(x => !removeAllIds.includes(x.objectId)), ...newArray],
|
||||
x => x.pureName
|
||||
);
|
||||
|
||||
// merge missing data from old structure
|
||||
for (const item of res[field]) {
|
||||
const original = (this.structure[field] || []).find(x => x.objectId == item.objectId);
|
||||
if (original) {
|
||||
for (const key in original) {
|
||||
if (!item[key]) item[key] = original[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
@@ -77,7 +122,10 @@ export class DatabaseAnalyser {
|
||||
if (typeField == objectTypeField) return [pureName];
|
||||
}
|
||||
if (this.modifications) {
|
||||
return this.modifications.filter(x => x.objectTypeField == objectTypeField).map(x => x.newName.pureName);
|
||||
return this.modifications
|
||||
.filter(x => x.objectTypeField == objectTypeField)
|
||||
.filter(x => x.newName)
|
||||
.map(x => x.newName.pureName);
|
||||
}
|
||||
return allPureNames;
|
||||
}
|
||||
@@ -86,11 +134,107 @@ export class DatabaseAnalyser {
|
||||
// return this.structure.tables.find((x) => x.objectId == id);
|
||||
// }
|
||||
|
||||
createQuery(template, typeFields) {
|
||||
// let res = template;
|
||||
if (this.singleObjectFilter) {
|
||||
const { typeField } = this.singleObjectFilter;
|
||||
if (!this.singleObjectId) return null;
|
||||
if (!typeFields || !typeFields.includes(typeField)) return null;
|
||||
return template.replace(/=OBJECT_ID_CONDITION/g, ` = '${this.singleObjectId}'`);
|
||||
}
|
||||
if (!this.modifications || !typeFields || this.modifications.length == 0) {
|
||||
return template.replace(/=OBJECT_ID_CONDITION/g, ' is not null');
|
||||
}
|
||||
if (this.modifications.some(x => typeFields.includes(x.objectTypeField) && x.action == 'all')) {
|
||||
// do not filter objects
|
||||
return template.replace(/=OBJECT_ID_CONDITION/g, ' is not null');
|
||||
}
|
||||
const filterIds = this.modifications
|
||||
.filter(x => typeFields.includes(x.objectTypeField) && (x.action == 'add' || x.action == 'change'))
|
||||
.map(x => x.objectId);
|
||||
if (filterIds.length == 0) {
|
||||
return template.replace(/=OBJECT_ID_CONDITION/g, " = '0'");
|
||||
}
|
||||
return template.replace(/=OBJECT_ID_CONDITION/g, ` in (${filterIds.map(x => `'${x}'`).join(',')})`);
|
||||
}
|
||||
|
||||
getDeletedObjectsForField(snapshot, objectTypeField) {
|
||||
const items = snapshot[objectTypeField];
|
||||
if (!items) return [];
|
||||
if (!this.structure[objectTypeField]) return [];
|
||||
return this.structure[objectTypeField]
|
||||
.filter(x => !items.find(y => x.objectId == y.objectId))
|
||||
.map(x => ({
|
||||
oldName: _pick(x, ['schemaName', 'pureName']),
|
||||
objectId: x.objectId,
|
||||
action: 'remove',
|
||||
objectTypeField,
|
||||
}));
|
||||
}
|
||||
|
||||
getDeletedObjects(snapshot) {
|
||||
return [
|
||||
...this.getDeletedObjectsForField(snapshot, 'tables'),
|
||||
...this.getDeletedObjectsForField(snapshot, 'collections'),
|
||||
...this.getDeletedObjectsForField(snapshot, 'views'),
|
||||
...this.getDeletedObjectsForField(snapshot, 'matviews'),
|
||||
...this.getDeletedObjectsForField(snapshot, 'procedures'),
|
||||
...this.getDeletedObjectsForField(snapshot, 'functions'),
|
||||
...this.getDeletedObjectsForField(snapshot, 'triggers'),
|
||||
];
|
||||
}
|
||||
|
||||
async getModifications() {
|
||||
const snapshot = await this._getFastSnapshot();
|
||||
if (!snapshot) return null;
|
||||
|
||||
// console.log('STRUCTURE', this.structure);
|
||||
// console.log('SNAPSHOT', snapshot);
|
||||
|
||||
const res = [];
|
||||
for (const field in snapshot) {
|
||||
const items = snapshot[field];
|
||||
if (items === null) {
|
||||
res.push({ objectTypeField: field, action: 'all' });
|
||||
continue;
|
||||
}
|
||||
if (items === undefined) {
|
||||
// skip - undefined meens, that field is not supported
|
||||
continue;
|
||||
}
|
||||
for (const item of items) {
|
||||
const { objectId, schemaName, pureName, contentHash } = item;
|
||||
const obj = this.structure[field].find(x => x.objectId == objectId);
|
||||
|
||||
if (obj && contentHash && obj.contentHash == contentHash) continue;
|
||||
|
||||
const action = obj
|
||||
? {
|
||||
newName: { schemaName, pureName },
|
||||
oldName: _pick(obj, ['schemaName', 'pureName']),
|
||||
action: 'change',
|
||||
objectTypeField: field,
|
||||
objectId,
|
||||
}
|
||||
: {
|
||||
newName: { schemaName, pureName },
|
||||
action: 'add',
|
||||
objectTypeField: field,
|
||||
objectId,
|
||||
};
|
||||
res.push(action);
|
||||
}
|
||||
}
|
||||
|
||||
return [..._compact(res), ...this.getDeletedObjects(snapshot)];
|
||||
}
|
||||
|
||||
static createEmptyStructure(): DatabaseInfo {
|
||||
return {
|
||||
tables: [],
|
||||
collections: [],
|
||||
views: [],
|
||||
matviews: [],
|
||||
functions: [],
|
||||
procedures: [],
|
||||
triggers: [],
|
||||
|
||||
@@ -15,12 +15,15 @@ import {
|
||||
IndexInfo,
|
||||
UniqueInfo,
|
||||
CheckInfo,
|
||||
AlterProcessor,
|
||||
SqlObjectInfo,
|
||||
} from 'dbgate-types';
|
||||
import _isString from 'lodash/isString';
|
||||
import _isNumber from 'lodash/isNumber';
|
||||
import _isDate from 'lodash/isDate';
|
||||
import uuidv1 from 'uuid/v1';
|
||||
|
||||
export class SqlDumper {
|
||||
export class SqlDumper implements AlterProcessor {
|
||||
s = '';
|
||||
driver: EngineDriver;
|
||||
dialect: SqlDialect;
|
||||
@@ -188,7 +191,7 @@ export class SqlDumper {
|
||||
if (includeNullable) {
|
||||
this.put(column.notNull ? '^not ^null' : '^null');
|
||||
}
|
||||
if (includeDefault && column.defaultValue != null) {
|
||||
if (includeDefault && column.defaultValue?.trim()) {
|
||||
this.columnDefault(column);
|
||||
}
|
||||
}
|
||||
@@ -227,30 +230,25 @@ export class SqlDumper {
|
||||
table.primaryKey.columns.map(x => x.columnName)
|
||||
);
|
||||
}
|
||||
if (table.foreignKeys) {
|
||||
table.foreignKeys.forEach(fk => {
|
||||
this.put(',&n');
|
||||
this.createForeignKeyFore(fk);
|
||||
});
|
||||
}
|
||||
// foreach (var cnt in table.Uniques)
|
||||
// {
|
||||
// if (!first) this.put(", &n");
|
||||
// first = false;
|
||||
// CreateUniqueCore(cnt);
|
||||
// }
|
||||
// foreach (var cnt in table.Checks)
|
||||
// {
|
||||
// if (!first) this.put(", &n");
|
||||
// first = false;
|
||||
// CreateCheckCore(cnt);
|
||||
// }
|
||||
|
||||
(table.foreignKeys || []).forEach(fk => {
|
||||
this.put(',&n');
|
||||
this.createForeignKeyFore(fk);
|
||||
});
|
||||
(table.uniques || []).forEach(uq => {
|
||||
this.put(',&n');
|
||||
this.createUniqueCore(uq);
|
||||
});
|
||||
(table.checks || []).forEach(chk => {
|
||||
this.put(',&n');
|
||||
this.createCheckCore(chk);
|
||||
});
|
||||
|
||||
this.put('&<&n)');
|
||||
this.endCommand();
|
||||
// foreach (var ix in table.Indexes)
|
||||
// {
|
||||
// CreateIndex(ix);
|
||||
// }
|
||||
(table.indexes || []).forEach(ix => {
|
||||
this.createIndex(ix);
|
||||
});
|
||||
}
|
||||
|
||||
createForeignKeyFore(fk: ForeignKeyInfo) {
|
||||
@@ -293,6 +291,20 @@ export class SqlDumper {
|
||||
changeViewSchema(obj: ViewInfo, newSchema: string) {}
|
||||
renameView(obj: ViewInfo, newSchema: string) {}
|
||||
|
||||
createMatview(obj: ViewInfo) {
|
||||
this.putRaw(obj.createSql);
|
||||
this.endCommand();
|
||||
}
|
||||
dropMatview(obj: ViewInfo, { testIfExists = false }) {
|
||||
this.putCmd('^drop ^materialized ^view %f', obj);
|
||||
}
|
||||
alterMatview(obj: ViewInfo) {
|
||||
this.putRaw(obj.createSql.replace(/create\s+view/i, 'ALTER VIEW'));
|
||||
this.endCommand();
|
||||
}
|
||||
changeMatviewSchema(obj: ViewInfo, newSchema: string) {}
|
||||
renameMatview(obj: ViewInfo, newSchema: string) {}
|
||||
|
||||
createProcedure(obj: ProcedureInfo) {
|
||||
this.putRaw(obj.createSql);
|
||||
this.endCommand();
|
||||
@@ -335,14 +347,53 @@ export class SqlDumper {
|
||||
changeTriggerSchema(obj: TriggerInfo, newSchema: string) {}
|
||||
renameTrigger(obj: TriggerInfo, newSchema: string) {}
|
||||
|
||||
dropConstraint(cnt: ConstraintInfo) {
|
||||
dropConstraintCore(cnt: ConstraintInfo) {
|
||||
this.putCmd('^alter ^table %f ^drop ^constraint %i', cnt, cnt.constraintName);
|
||||
}
|
||||
dropConstraint(cnt: ConstraintInfo) {
|
||||
switch (cnt.constraintType) {
|
||||
case 'primaryKey':
|
||||
this.dropPrimaryKey(cnt as PrimaryKeyInfo);
|
||||
break;
|
||||
case 'foreignKey':
|
||||
this.dropForeignKey(cnt as ForeignKeyInfo);
|
||||
break;
|
||||
case 'unique':
|
||||
this.dropUnique(cnt as UniqueInfo);
|
||||
break;
|
||||
case 'check':
|
||||
this.dropCheck(cnt as CheckInfo);
|
||||
break;
|
||||
case 'index':
|
||||
this.dropIndex(cnt as IndexInfo);
|
||||
break;
|
||||
}
|
||||
}
|
||||
createConstraint(cnt: ConstraintInfo) {
|
||||
switch (cnt.constraintType) {
|
||||
case 'primaryKey':
|
||||
this.createPrimaryKey(cnt as PrimaryKeyInfo);
|
||||
break;
|
||||
case 'foreignKey':
|
||||
this.createForeignKey(cnt as ForeignKeyInfo);
|
||||
break;
|
||||
case 'unique':
|
||||
this.createUnique(cnt as UniqueInfo);
|
||||
break;
|
||||
case 'check':
|
||||
this.createCheck(cnt as CheckInfo);
|
||||
break;
|
||||
case 'index':
|
||||
this.createIndex(cnt as IndexInfo);
|
||||
break;
|
||||
}
|
||||
}
|
||||
changeConstraint(oldConstraint: ConstraintInfo, newConstraint: ConstraintInfo) {}
|
||||
dropForeignKey(fk: ForeignKeyInfo) {
|
||||
if (this.dialect.explicitDropConstraint) {
|
||||
this.putCmd('^alter ^table %f ^drop ^foreign ^key %i', fk, fk.constraintName);
|
||||
} else {
|
||||
this.dropConstraint(fk);
|
||||
this.dropConstraintCore(fk);
|
||||
}
|
||||
}
|
||||
createForeignKey(fk: ForeignKeyInfo) {
|
||||
@@ -354,7 +405,7 @@ export class SqlDumper {
|
||||
if (this.dialect.explicitDropConstraint) {
|
||||
this.putCmd('^alter ^table %f ^drop ^primary ^key', pk);
|
||||
} else {
|
||||
this.dropConstraint(pk);
|
||||
this.dropConstraintCore(pk);
|
||||
}
|
||||
}
|
||||
createPrimaryKey(pk: PrimaryKeyInfo) {
|
||||
@@ -366,11 +417,26 @@ export class SqlDumper {
|
||||
);
|
||||
}
|
||||
|
||||
dropIndex(ix: IndexInfo) {}
|
||||
createIndex(ix: IndexInfo) {}
|
||||
dropIndex(ix: IndexInfo) {
|
||||
this.put('^drop ^index %i', ix.constraintName);
|
||||
if (this.dialect.dropIndexContainsTableSpec) {
|
||||
this.put(' ^on %f', ix);
|
||||
}
|
||||
this.endCommand();
|
||||
}
|
||||
createIndex(ix: IndexInfo) {
|
||||
this.put('^create');
|
||||
if (ix.isUnique) this.put(' ^unique');
|
||||
this.put(' ^index %i &n^on %f (&>&n', ix.constraintName, ix);
|
||||
this.putCollection(',&n', ix.columns, col => {
|
||||
this.put('%i %k', col.columnName, col.isDescending == true ? 'DESC' : 'ASC');
|
||||
});
|
||||
this.put('&<&n)');
|
||||
this.endCommand();
|
||||
}
|
||||
|
||||
dropUnique(uq: UniqueInfo) {
|
||||
this.dropConstraint(uq);
|
||||
this.dropConstraintCore(uq);
|
||||
}
|
||||
createUniqueCore(uq: UniqueInfo) {
|
||||
this.put(
|
||||
@@ -387,7 +453,7 @@ export class SqlDumper {
|
||||
}
|
||||
|
||||
dropCheck(ch: CheckInfo) {
|
||||
this.dropConstraint(ch);
|
||||
this.dropConstraintCore(ch);
|
||||
}
|
||||
|
||||
createCheckCore(ch: CheckInfo) {
|
||||
@@ -402,8 +468,8 @@ export class SqlDumper {
|
||||
|
||||
renameConstraint(constraint: ConstraintInfo, newName: string) {}
|
||||
|
||||
createColumn(table: TableInfo, column: ColumnInfo, constraints: ConstraintInfo[]) {
|
||||
this.put('^alter ^table %f ^add %i ', table, column.columnName);
|
||||
createColumn(column: ColumnInfo, constraints: ConstraintInfo[]) {
|
||||
this.put('^alter ^table %f ^add %i ', column, column.columnName);
|
||||
this.columnDefinition(column);
|
||||
this.inlineConstraints(constraints);
|
||||
this.endCommand();
|
||||
@@ -429,7 +495,7 @@ export class SqlDumper {
|
||||
|
||||
changeColumn(oldcol: ColumnInfo, newcol: ColumnInfo, constraints: ConstraintInfo[]) {}
|
||||
|
||||
dropTable(obj: TableInfo, { testIfExists = false }) {
|
||||
dropTable(obj: TableInfo, { testIfExists = false } = {}) {
|
||||
this.putCmd('^drop ^table %f', obj);
|
||||
}
|
||||
|
||||
@@ -455,4 +521,87 @@ export class SqlDumper {
|
||||
truncateTable(name: NamedObjectInfo) {
|
||||
this.putCmd('^delete ^from %f', name);
|
||||
}
|
||||
|
||||
dropConstraints(table: TableInfo, dropReferences = false) {
|
||||
if (dropReferences && this.dialect.dropForeignKey) {
|
||||
table.dependencies.forEach(cnt => this.dropConstraint(cnt));
|
||||
}
|
||||
if (this.dialect.dropIndex) {
|
||||
table.indexes.forEach(cnt => this.dropIndex(cnt));
|
||||
}
|
||||
if (this.dialect.dropForeignKey) {
|
||||
table.foreignKeys.forEach(cnt => this.dropForeignKey(cnt));
|
||||
}
|
||||
if (this.dialect.dropPrimaryKey && table.primaryKey) {
|
||||
this.dropPrimaryKey(table.primaryKey);
|
||||
}
|
||||
}
|
||||
|
||||
recreateTable(oldTable: TableInfo, newTable: TableInfo) {
|
||||
if (!oldTable.pairingId || !newTable.pairingId || oldTable.pairingId != newTable.pairingId) {
|
||||
throw new Error('Recreate is not possible: oldTable.paringId != newTable.paringId');
|
||||
}
|
||||
const tmpTable = `temp_${uuidv1()}`;
|
||||
|
||||
// console.log('oldTable', oldTable);
|
||||
// console.log('newTable', newTable);
|
||||
|
||||
const columnPairs = oldTable.columns
|
||||
.map(oldcol => ({
|
||||
oldcol,
|
||||
newcol: newTable.columns.find(x => x.pairingId == oldcol.pairingId),
|
||||
}))
|
||||
.filter(x => x.newcol);
|
||||
|
||||
this.dropConstraints(oldTable, true);
|
||||
this.renameTable(oldTable, tmpTable);
|
||||
|
||||
this.createTable(newTable);
|
||||
|
||||
const autoinc = newTable.columns.find(x => x.autoIncrement);
|
||||
if (autoinc) {
|
||||
this.allowIdentityInsert(newTable, true);
|
||||
}
|
||||
|
||||
this.putCmd(
|
||||
'^insert ^into %f (%,i) select %,s ^from %f',
|
||||
newTable,
|
||||
columnPairs.map(x => x.newcol.columnName),
|
||||
columnPairs.map(x => x.oldcol.columnName),
|
||||
{ ...oldTable, pureName: tmpTable }
|
||||
);
|
||||
|
||||
if (autoinc) {
|
||||
this.allowIdentityInsert(newTable, false);
|
||||
}
|
||||
|
||||
if (this.dialect.dropForeignKey) {
|
||||
newTable.dependencies.forEach(cnt => this.createConstraint(cnt));
|
||||
}
|
||||
|
||||
this.dropTable({ ...oldTable, pureName: tmpTable });
|
||||
}
|
||||
|
||||
createSqlObject(obj: SqlObjectInfo) {
|
||||
this.putCmd(obj.createSql);
|
||||
}
|
||||
|
||||
getSqlObjectSqlName(ojectTypeField: string) {
|
||||
switch (ojectTypeField) {
|
||||
case 'procedures':
|
||||
return 'PROCEDURE';
|
||||
case 'views':
|
||||
return 'VIEW';
|
||||
case 'functions':
|
||||
return 'FUNCTION';
|
||||
case 'triggers':
|
||||
return 'TRIGGER';
|
||||
case 'matviews':
|
||||
return 'MATERIALIZED VIEW';
|
||||
}
|
||||
}
|
||||
|
||||
dropSqlObject(obj: SqlObjectInfo) {
|
||||
this.putCmd('^drop %s %f', this.getSqlObjectSqlName(obj.objectTypeField), obj);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ import {
|
||||
TriggerInfo,
|
||||
ViewInfo,
|
||||
} from 'dbgate-types';
|
||||
import _ from 'lodash';
|
||||
import _flatten from 'lodash/flatten';
|
||||
import _uniqBy from 'lodash/uniqBy'
|
||||
import { SqlDumper } from './SqlDumper';
|
||||
import { extendDatabaseInfo } from './structureTools';
|
||||
|
||||
@@ -29,6 +30,10 @@ interface SqlGeneratorOptions {
|
||||
checkIfViewExists: boolean;
|
||||
createViews: boolean;
|
||||
|
||||
dropMatviews: boolean;
|
||||
checkIfMatviewExists: boolean;
|
||||
createMatviews: boolean;
|
||||
|
||||
dropProcedures: boolean;
|
||||
checkIfProcedureExists: boolean;
|
||||
createProcedures: boolean;
|
||||
@@ -51,6 +56,7 @@ interface SqlGeneratorObject {
|
||||
export class SqlGenerator {
|
||||
private tables: TableInfo[];
|
||||
private views: ViewInfo[];
|
||||
private matviews: ViewInfo[];
|
||||
private procedures: ProcedureInfo[];
|
||||
private functions: FunctionInfo[];
|
||||
private triggers: TriggerInfo[];
|
||||
@@ -69,6 +75,7 @@ export class SqlGenerator {
|
||||
this.dbinfo = extendDatabaseInfo(dbinfo);
|
||||
this.tables = this.extract('tables');
|
||||
this.views = this.extract('views');
|
||||
this.matviews = this.extract('matviews');
|
||||
this.procedures = this.extract('procedures');
|
||||
this.functions = this.extract('functions');
|
||||
this.triggers = this.extract('triggers');
|
||||
@@ -89,6 +96,8 @@ export class SqlGenerator {
|
||||
if (this.checkDumper()) return;
|
||||
this.dropObjects(this.views, 'View');
|
||||
if (this.checkDumper()) return;
|
||||
this.dropObjects(this.matviews, 'Matview');
|
||||
if (this.checkDumper()) return;
|
||||
this.dropObjects(this.triggers, 'Trigger');
|
||||
if (this.checkDumper()) return;
|
||||
|
||||
@@ -113,6 +122,8 @@ export class SqlGenerator {
|
||||
if (this.checkDumper()) return;
|
||||
this.createObjects(this.views, 'View');
|
||||
if (this.checkDumper()) return;
|
||||
this.createObjects(this.matviews, 'Matview');
|
||||
if (this.checkDumper()) return;
|
||||
this.createObjects(this.triggers, 'Trigger');
|
||||
if (this.checkDumper()) return;
|
||||
} finally {
|
||||
@@ -122,9 +133,9 @@ export class SqlGenerator {
|
||||
|
||||
createForeignKeys() {
|
||||
const fks = [];
|
||||
if (this.options.createForeignKeys) fks.push(..._.flatten(this.tables.map(x => x.foreignKeys || [])));
|
||||
if (this.options.createReferences) fks.push(..._.flatten(this.tables.map(x => x.dependencies || [])));
|
||||
for (const fk of _.uniqBy(fks, 'constraintName')) {
|
||||
if (this.options.createForeignKeys) fks.push(..._flatten(this.tables.map(x => x.foreignKeys || [])));
|
||||
if (this.options.createReferences) fks.push(..._flatten(this.tables.map(x => x.dependencies || [])));
|
||||
for (const fk of _uniqBy(fks, 'constraintName')) {
|
||||
this.dmp.createForeignKey(fk);
|
||||
if (this.checkDumper()) return;
|
||||
}
|
||||
@@ -152,7 +163,7 @@ export class SqlGenerator {
|
||||
}
|
||||
}
|
||||
if (this.options.createIndexes) {
|
||||
for (const index of _.flatten(this.tables.map(x => x.indexes || []))) {
|
||||
for (const index of _flatten(this.tables.map(x => x.indexes || []))) {
|
||||
this.dmp.createIndex(index);
|
||||
}
|
||||
}
|
||||
@@ -204,7 +215,7 @@ export class SqlGenerator {
|
||||
|
||||
dropTables() {
|
||||
if (this.options.dropReferences) {
|
||||
for (const fk of _.flatten(this.tables.map(x => x.dependencies || []))) {
|
||||
for (const fk of _flatten(this.tables.map(x => x.dependencies || []))) {
|
||||
this.dmp.dropForeignKey(fk);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,488 @@
|
||||
import _ from 'lodash';
|
||||
import { generateTablePairingId } from '.';
|
||||
import {
|
||||
AlterProcessor,
|
||||
ColumnInfo,
|
||||
ConstraintInfo,
|
||||
DatabaseInfo,
|
||||
SqlObjectInfo,
|
||||
SqlDialect,
|
||||
TableInfo,
|
||||
} from '../../types';
|
||||
import { DatabaseInfoAlterProcessor } from './database-info-alter-processor';
|
||||
import { DatabaseAnalyser } from './DatabaseAnalyser';
|
||||
|
||||
interface AlterOperation_CreateTable {
|
||||
operationType: 'createTable';
|
||||
newObject: TableInfo;
|
||||
}
|
||||
|
||||
interface AlterOperation_DropTable {
|
||||
operationType: 'dropTable';
|
||||
oldObject: TableInfo;
|
||||
}
|
||||
|
||||
interface AlterOperation_CreateSqlObject {
|
||||
operationType: 'createSqlObject';
|
||||
newObject: SqlObjectInfo;
|
||||
}
|
||||
|
||||
interface AlterOperation_DropSqlObject {
|
||||
operationType: 'dropSqlObject';
|
||||
oldObject: SqlObjectInfo;
|
||||
}
|
||||
|
||||
interface AlterOperation_RenameTable {
|
||||
operationType: 'renameTable';
|
||||
object: TableInfo;
|
||||
newName: string;
|
||||
}
|
||||
|
||||
interface AlterOperation_CreateColumn {
|
||||
operationType: 'createColumn';
|
||||
newObject: ColumnInfo;
|
||||
}
|
||||
|
||||
interface AlterOperation_ChangeColumn {
|
||||
operationType: 'changeColumn';
|
||||
oldObject: ColumnInfo;
|
||||
newObject: ColumnInfo;
|
||||
}
|
||||
|
||||
interface AlterOperation_RenameColumn {
|
||||
operationType: 'renameColumn';
|
||||
object: ColumnInfo;
|
||||
newName: string;
|
||||
}
|
||||
|
||||
interface AlterOperation_DropColumn {
|
||||
operationType: 'dropColumn';
|
||||
oldObject: ColumnInfo;
|
||||
}
|
||||
|
||||
interface AlterOperation_CreateConstraint {
|
||||
operationType: 'createConstraint';
|
||||
newObject: ConstraintInfo;
|
||||
}
|
||||
|
||||
interface AlterOperation_ChangeConstraint {
|
||||
operationType: 'changeConstraint';
|
||||
oldObject: ConstraintInfo;
|
||||
newObject: ConstraintInfo;
|
||||
}
|
||||
|
||||
interface AlterOperation_DropConstraint {
|
||||
operationType: 'dropConstraint';
|
||||
oldObject: ConstraintInfo;
|
||||
}
|
||||
|
||||
interface AlterOperation_RenameConstraint {
|
||||
operationType: 'renameConstraint';
|
||||
object: ConstraintInfo;
|
||||
newName: string;
|
||||
}
|
||||
interface AlterOperation_RecreateTable {
|
||||
operationType: 'recreateTable';
|
||||
table: TableInfo;
|
||||
operations: AlterOperation[];
|
||||
}
|
||||
|
||||
type AlterOperation =
|
||||
| AlterOperation_CreateColumn
|
||||
| AlterOperation_ChangeColumn
|
||||
| AlterOperation_DropColumn
|
||||
| AlterOperation_CreateConstraint
|
||||
| AlterOperation_ChangeConstraint
|
||||
| AlterOperation_DropConstraint
|
||||
| AlterOperation_CreateTable
|
||||
| AlterOperation_DropTable
|
||||
| AlterOperation_RenameTable
|
||||
| AlterOperation_RenameColumn
|
||||
| AlterOperation_RenameConstraint
|
||||
| AlterOperation_CreateSqlObject
|
||||
| AlterOperation_DropSqlObject
|
||||
| AlterOperation_RecreateTable;
|
||||
|
||||
export class AlterPlan {
|
||||
recreates = {
|
||||
tables: 0,
|
||||
constraints: 0,
|
||||
sqlObjects: 0,
|
||||
};
|
||||
|
||||
public operations: AlterOperation[] = [];
|
||||
constructor(public db: DatabaseInfo, public dialect: SqlDialect) {}
|
||||
|
||||
createTable(table: TableInfo) {
|
||||
this.operations.push({
|
||||
operationType: 'createTable',
|
||||
newObject: table,
|
||||
});
|
||||
}
|
||||
|
||||
dropTable(table: TableInfo) {
|
||||
this.operations.push({
|
||||
operationType: 'dropTable',
|
||||
oldObject: table,
|
||||
});
|
||||
}
|
||||
|
||||
createSqlObject(obj: SqlObjectInfo) {
|
||||
this.operations.push({
|
||||
operationType: 'createSqlObject',
|
||||
newObject: obj,
|
||||
});
|
||||
}
|
||||
|
||||
dropSqlObject(obj: SqlObjectInfo) {
|
||||
this.operations.push({
|
||||
operationType: 'dropSqlObject',
|
||||
oldObject: obj,
|
||||
});
|
||||
}
|
||||
|
||||
createColumn(column: ColumnInfo) {
|
||||
this.operations.push({
|
||||
operationType: 'createColumn',
|
||||
newObject: column,
|
||||
});
|
||||
}
|
||||
|
||||
changeColumn(oldColumn: ColumnInfo, newColumn: ColumnInfo) {
|
||||
this.operations.push({
|
||||
operationType: 'changeColumn',
|
||||
oldObject: oldColumn,
|
||||
newObject: newColumn,
|
||||
});
|
||||
}
|
||||
|
||||
dropColumn(column: ColumnInfo) {
|
||||
this.operations.push({
|
||||
operationType: 'dropColumn',
|
||||
oldObject: column,
|
||||
});
|
||||
}
|
||||
|
||||
createConstraint(constraint: ConstraintInfo) {
|
||||
this.operations.push({
|
||||
operationType: 'createConstraint',
|
||||
newObject: constraint,
|
||||
});
|
||||
}
|
||||
|
||||
changeConstraint(oldConstraint: ConstraintInfo, newConstraint: ConstraintInfo) {
|
||||
this.operations.push({
|
||||
operationType: 'changeConstraint',
|
||||
oldObject: oldConstraint,
|
||||
newObject: newConstraint,
|
||||
});
|
||||
}
|
||||
|
||||
dropConstraint(constraint: ConstraintInfo) {
|
||||
this.operations.push({
|
||||
operationType: 'dropConstraint',
|
||||
oldObject: constraint,
|
||||
});
|
||||
}
|
||||
|
||||
renameTable(table: TableInfo, newName: string) {
|
||||
this.operations.push({
|
||||
operationType: 'renameTable',
|
||||
object: table,
|
||||
newName,
|
||||
});
|
||||
}
|
||||
|
||||
renameColumn(column: ColumnInfo, newName: string) {
|
||||
this.operations.push({
|
||||
operationType: 'renameColumn',
|
||||
object: column,
|
||||
newName,
|
||||
});
|
||||
}
|
||||
|
||||
renameConstraint(constraint: ConstraintInfo, newName: string) {
|
||||
this.operations.push({
|
||||
operationType: 'renameConstraint',
|
||||
object: constraint,
|
||||
newName,
|
||||
});
|
||||
}
|
||||
|
||||
recreateTable(table: TableInfo, operations: AlterOperation[]) {
|
||||
this.operations.push({
|
||||
operationType: 'recreateTable',
|
||||
table,
|
||||
operations,
|
||||
});
|
||||
this.recreates.tables += 1;
|
||||
}
|
||||
|
||||
run(processor: AlterProcessor) {
|
||||
for (const op of this.operations) {
|
||||
runAlterOperation(op, processor);
|
||||
}
|
||||
}
|
||||
|
||||
_getDependendColumnConstraints(column: ColumnInfo, dependencyDefinition) {
|
||||
const table = this.db.tables.find(x => x.pureName == column.pureName && x.schemaName == column.schemaName);
|
||||
const fks = dependencyDefinition?.includes('dependencies')
|
||||
? table.dependencies.filter(fk => fk.columns.find(col => col.refColumnName == column.columnName))
|
||||
: [];
|
||||
const constraints = _.compact([
|
||||
dependencyDefinition?.includes('primaryKey') ? table.primaryKey : null,
|
||||
...(dependencyDefinition?.includes('foreignKeys') ? table.foreignKeys : []),
|
||||
...(dependencyDefinition?.includes('indexes') ? table.indexes : []),
|
||||
...(dependencyDefinition?.includes('uniques') ? table.uniques : []),
|
||||
]).filter(cnt => cnt.columns.find(col => col.columnName == column.columnName));
|
||||
|
||||
return [...fks, ...constraints];
|
||||
}
|
||||
|
||||
_addLogicalDependencies(): AlterOperation[] {
|
||||
const lists = this.operations.map(op => {
|
||||
if (op.operationType == 'dropColumn') {
|
||||
const constraints = this._getDependendColumnConstraints(op.oldObject, this.dialect.dropColumnDependencies);
|
||||
|
||||
const res: AlterOperation[] = [
|
||||
...constraints.map(oldObject => {
|
||||
const opRes: AlterOperation = {
|
||||
operationType: 'dropConstraint',
|
||||
oldObject,
|
||||
};
|
||||
return opRes;
|
||||
}),
|
||||
op,
|
||||
];
|
||||
return res;
|
||||
}
|
||||
|
||||
if (op.operationType == 'changeColumn') {
|
||||
const constraints = this._getDependendColumnConstraints(op.oldObject, this.dialect.changeColumnDependencies);
|
||||
|
||||
const res: AlterOperation[] = [
|
||||
...constraints.map(oldObject => {
|
||||
const opRes: AlterOperation = {
|
||||
operationType: 'dropConstraint',
|
||||
oldObject,
|
||||
};
|
||||
return opRes;
|
||||
}),
|
||||
op,
|
||||
..._.reverse([...constraints]).map(newObject => {
|
||||
const opRes: AlterOperation = {
|
||||
operationType: 'createConstraint',
|
||||
newObject,
|
||||
};
|
||||
return opRes;
|
||||
}),
|
||||
];
|
||||
|
||||
if (constraints.length > 0) {
|
||||
this.recreates.constraints += 1;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
if (op.operationType == 'dropTable') {
|
||||
return [
|
||||
...(this.dialect.dropReferencesWhenDropTable
|
||||
? (op.oldObject.dependencies || []).map(oldObject => {
|
||||
const opRes: AlterOperation = {
|
||||
operationType: 'dropConstraint',
|
||||
oldObject,
|
||||
};
|
||||
return opRes;
|
||||
})
|
||||
: []),
|
||||
op,
|
||||
];
|
||||
}
|
||||
|
||||
if (op.operationType == 'changeConstraint') {
|
||||
this.recreates.constraints += 1;
|
||||
const opDrop: AlterOperation = {
|
||||
operationType: 'dropConstraint',
|
||||
oldObject: op.oldObject,
|
||||
};
|
||||
const opCreate: AlterOperation = {
|
||||
operationType: 'createConstraint',
|
||||
newObject: op.newObject,
|
||||
};
|
||||
return [opDrop, opCreate];
|
||||
}
|
||||
|
||||
return [op];
|
||||
});
|
||||
|
||||
return _.flatten(lists);
|
||||
}
|
||||
|
||||
_transformToImplementedOps(): AlterOperation[] {
|
||||
const lists = this.operations.map(op => {
|
||||
return (
|
||||
this._testTableRecreate(op, 'createColumn', this.dialect.createColumn, 'newObject') ||
|
||||
this._testTableRecreate(op, 'dropColumn', this.dialect.dropColumn, 'oldObject') ||
|
||||
this._testTableRecreate(op, 'createConstraint', obj => this._canCreateConstraint(obj), 'newObject') ||
|
||||
this._testTableRecreate(op, 'dropConstraint', obj => this._canDropConstraint(obj), 'oldObject') ||
|
||||
this._testTableRecreate(op, 'changeColumn', this.dialect.changeColumn, 'newObject') || [op]
|
||||
);
|
||||
});
|
||||
|
||||
return _.flatten(lists);
|
||||
}
|
||||
|
||||
_canCreateConstraint(cnt: ConstraintInfo) {
|
||||
if (cnt.constraintType == 'primaryKey') return this.dialect.createPrimaryKey;
|
||||
if (cnt.constraintType == 'foreignKey') return this.dialect.createForeignKey;
|
||||
if (cnt.constraintType == 'index') return this.dialect.createIndex;
|
||||
if (cnt.constraintType == 'unique') return this.dialect.createUnique;
|
||||
if (cnt.constraintType == 'check') return this.dialect.createCheck;
|
||||
return null;
|
||||
}
|
||||
|
||||
_canDropConstraint(cnt: ConstraintInfo) {
|
||||
if (cnt.constraintType == 'primaryKey') return this.dialect.dropPrimaryKey;
|
||||
if (cnt.constraintType == 'foreignKey') return this.dialect.dropForeignKey;
|
||||
if (cnt.constraintType == 'index') return this.dialect.dropIndex;
|
||||
if (cnt.constraintType == 'unique') return this.dialect.dropUnique;
|
||||
if (cnt.constraintType == 'check') return this.dialect.dropCheck;
|
||||
return null;
|
||||
}
|
||||
|
||||
_testTableRecreate(
|
||||
op: AlterOperation,
|
||||
operationType: string,
|
||||
isAllowed: boolean | Function,
|
||||
objectField: string
|
||||
): AlterOperation[] | null {
|
||||
if (op.operationType == operationType) {
|
||||
if (_.isFunction(isAllowed)) {
|
||||
if (isAllowed(op[objectField])) return null;
|
||||
} else {
|
||||
if (isAllowed) return null;
|
||||
}
|
||||
|
||||
// console.log('*****************RECREATED NEEDED', op, operationType, isAllowed);
|
||||
// console.log(this.dialect);
|
||||
const table = this.db.tables.find(
|
||||
x => x.pureName == op[objectField].pureName && x.schemaName == op[objectField].schemaName
|
||||
);
|
||||
this.recreates.tables += 1;
|
||||
return [
|
||||
{
|
||||
operationType: 'recreateTable',
|
||||
table,
|
||||
operations: [op],
|
||||
},
|
||||
];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
_groupTableRecreations(): AlterOperation[] {
|
||||
const res = [];
|
||||
const recreates = {};
|
||||
for (const op of this.operations) {
|
||||
if (op.operationType == 'recreateTable') {
|
||||
const existingRecreate = recreates[`${op.table.schemaName}||${op.table.pureName}`];
|
||||
if (existingRecreate) {
|
||||
existingRecreate.operations.push(...op.operations);
|
||||
} else {
|
||||
const recreate = {
|
||||
...op,
|
||||
operations: [...op.operations],
|
||||
};
|
||||
res.push(recreate);
|
||||
recreates[`${op.table.schemaName}||${op.table.pureName}`] = recreate;
|
||||
}
|
||||
} else {
|
||||
// @ts-ignore
|
||||
const oldObject: TableInfo = op.oldObject;
|
||||
if (oldObject) {
|
||||
const recreated = recreates[`${oldObject.schemaName}||${oldObject.pureName}`];
|
||||
if (recreated) {
|
||||
recreated.operations.push(op);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
res.push(op);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
transformPlan() {
|
||||
// console.log('*****************OPERATIONS0', this.operations);
|
||||
|
||||
this.operations = this._addLogicalDependencies();
|
||||
|
||||
// console.log('*****************OPERATIONS1', this.operations);
|
||||
|
||||
this.operations = this._transformToImplementedOps();
|
||||
|
||||
// console.log('*****************OPERATIONS2', this.operations);
|
||||
|
||||
this.operations = this._groupTableRecreations();
|
||||
|
||||
// console.log('*****************OPERATIONS3', this.operations);
|
||||
}
|
||||
}
|
||||
|
||||
export function runAlterOperation(op: AlterOperation, processor: AlterProcessor) {
|
||||
switch (op.operationType) {
|
||||
case 'createTable':
|
||||
processor.createTable(op.newObject);
|
||||
break;
|
||||
case 'changeColumn':
|
||||
processor.changeColumn(op.oldObject, op.newObject);
|
||||
break;
|
||||
case 'createColumn':
|
||||
processor.createColumn(op.newObject, []);
|
||||
break;
|
||||
case 'dropColumn':
|
||||
processor.dropColumn(op.oldObject);
|
||||
break;
|
||||
case 'dropTable':
|
||||
processor.dropTable(op.oldObject);
|
||||
break;
|
||||
case 'changeConstraint':
|
||||
processor.changeConstraint(op.oldObject, op.newObject);
|
||||
break;
|
||||
case 'createConstraint':
|
||||
processor.createConstraint(op.newObject);
|
||||
break;
|
||||
case 'dropConstraint':
|
||||
processor.dropConstraint(op.oldObject);
|
||||
break;
|
||||
case 'renameColumn':
|
||||
processor.renameColumn(op.object, op.newName);
|
||||
break;
|
||||
case 'renameTable':
|
||||
processor.renameTable(op.object, op.newName);
|
||||
break;
|
||||
case 'renameConstraint':
|
||||
processor.renameConstraint(op.object, op.newName);
|
||||
break;
|
||||
case 'createSqlObject':
|
||||
processor.createSqlObject(op.newObject);
|
||||
break;
|
||||
case 'dropSqlObject':
|
||||
processor.dropSqlObject(op.oldObject);
|
||||
break;
|
||||
case 'recreateTable':
|
||||
{
|
||||
const oldTable = generateTablePairingId(op.table);
|
||||
const newTable = _.cloneDeep(oldTable);
|
||||
const newDb = DatabaseAnalyser.createEmptyStructure();
|
||||
newDb.tables.push(newTable);
|
||||
// console.log('////////////////////////////newTable1', newTable);
|
||||
op.operations.forEach(child => runAlterOperation(child, new DatabaseInfoAlterProcessor(newDb)));
|
||||
// console.log('////////////////////////////op.operations', op.operations);
|
||||
// console.log('////////////////////////////op.table', op.table);
|
||||
// console.log('////////////////////////////newTable2', newTable);
|
||||
processor.recreateTable(oldTable, newTable);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import _ from 'lodash';
|
||||
import {
|
||||
ColumnInfo,
|
||||
ConstraintInfo,
|
||||
DatabaseInfo,
|
||||
ForeignKeyInfo,
|
||||
PrimaryKeyInfo,
|
||||
TableInfo,
|
||||
IndexInfo,
|
||||
CheckInfo,
|
||||
UniqueInfo,
|
||||
SqlObjectInfo,
|
||||
} from '../../types';
|
||||
|
||||
export class DatabaseInfoAlterProcessor {
|
||||
constructor(public db: DatabaseInfo) {}
|
||||
|
||||
createTable(table: TableInfo) {
|
||||
this.db.tables.push(table);
|
||||
}
|
||||
|
||||
dropTable(table: TableInfo) {
|
||||
_.remove(this.db.tables, x => x.pureName == table.pureName && x.schemaName == table.schemaName);
|
||||
}
|
||||
|
||||
createSqlObject(obj: SqlObjectInfo) {
|
||||
this.db[obj.objectTypeField].push(obj);
|
||||
}
|
||||
|
||||
dropSqlObject(obj: SqlObjectInfo) {
|
||||
_.remove(
|
||||
this.db[obj.objectTypeField] as SqlObjectInfo[],
|
||||
x => x.pureName == obj.pureName && x.schemaName == obj.schemaName
|
||||
);
|
||||
}
|
||||
|
||||
createColumn(column: ColumnInfo) {
|
||||
const table = this.db.tables.find(x => x.pureName == column.pureName && x.schemaName == column.schemaName);
|
||||
table.columns.push(column);
|
||||
}
|
||||
|
||||
changeColumn(oldColumn: ColumnInfo, newColumn: ColumnInfo) {
|
||||
const table = this.db.tables.find(x => x.pureName == oldColumn.pureName && x.schemaName == oldColumn.schemaName);
|
||||
table.columns = table.columns.map(x => (x.columnName == oldColumn.columnName ? newColumn : x));
|
||||
}
|
||||
|
||||
dropColumn(column: ColumnInfo) {
|
||||
const table = this.db.tables.find(x => x.pureName == column.pureName && x.schemaName == column.schemaName);
|
||||
_.remove(table.columns, x => x.columnName == column.columnName);
|
||||
}
|
||||
|
||||
createConstraint(constraint: ConstraintInfo) {
|
||||
const table = this.db.tables.find(x => x.pureName == constraint.pureName && x.schemaName == constraint.schemaName);
|
||||
switch (constraint.constraintType) {
|
||||
case 'primaryKey':
|
||||
table.primaryKey = constraint as PrimaryKeyInfo;
|
||||
break;
|
||||
case 'foreignKey':
|
||||
table.foreignKeys.push(constraint as ForeignKeyInfo);
|
||||
break;
|
||||
case 'index':
|
||||
table.indexes.push(constraint as IndexInfo);
|
||||
break;
|
||||
case 'unique':
|
||||
table.uniques.push(constraint as UniqueInfo);
|
||||
break;
|
||||
case 'check':
|
||||
table.checks.push(constraint as CheckInfo);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
changeConstraint(oldConstraint: ConstraintInfo, newConstraint: ConstraintInfo) {
|
||||
const table = this.db.tables.find(
|
||||
x => x.pureName == oldConstraint.pureName && x.schemaName == oldConstraint.schemaName
|
||||
);
|
||||
}
|
||||
|
||||
dropConstraint(constraint: ConstraintInfo) {
|
||||
const table = this.db.tables.find(x => x.pureName == constraint.pureName && x.schemaName == constraint.schemaName);
|
||||
switch (constraint.constraintType) {
|
||||
case 'primaryKey':
|
||||
table.primaryKey = null;
|
||||
break;
|
||||
case 'foreignKey':
|
||||
table.foreignKeys = table.foreignKeys.filter(x => x.constraintName != constraint.constraintName);
|
||||
break;
|
||||
case 'index':
|
||||
table.indexes = table.indexes.filter(x => x.constraintName != constraint.constraintName);
|
||||
break;
|
||||
case 'unique':
|
||||
table.uniques = table.uniques.filter(x => x.constraintName != constraint.constraintName);
|
||||
break;
|
||||
case 'check':
|
||||
table.checks = table.checks.filter(x => x.constraintName != constraint.constraintName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
renameTable(table: TableInfo, newName: string) {
|
||||
this.db.tables.find(x => x.pureName == table.pureName && x.schemaName == table.schemaName).pureName = newName;
|
||||
}
|
||||
|
||||
renameColumn(column: ColumnInfo, newName: string) {
|
||||
const table = this.db.tables.find(x => x.pureName == column.pureName && x.schemaName == column.schemaName);
|
||||
table.columns.find(x => x.columnName == column.columnName).columnName = newName;
|
||||
}
|
||||
|
||||
renameConstraint(constraint: ConstraintInfo, newName: string) {}
|
||||
|
||||
recreateTable(oldTable: TableInfo, newTable: TableInfo) {
|
||||
throw new Error('recreateTable not implemented for DatabaseInfoAlterProcessor');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,404 @@
|
||||
import {
|
||||
ColumnInfo,
|
||||
ConstraintInfo,
|
||||
DatabaseInfo,
|
||||
EngineDriver,
|
||||
NamedObjectInfo,
|
||||
SqlDialect,
|
||||
TableInfo,
|
||||
} from 'dbgate-types';
|
||||
import _ from 'lodash';
|
||||
import uuidv1 from 'uuid/v1';
|
||||
import { AlterPlan } from './alterPlan';
|
||||
import stableStringify from 'json-stable-stringify';
|
||||
import { isArray } from 'lodash';
|
||||
|
||||
type DbDiffSchemaMode = 'strict' | 'ignore' | 'ignoreImplicit';
|
||||
|
||||
export interface DbDiffOptions {
|
||||
allowRecreateTable?: boolean;
|
||||
allowRecreateConstraint?: boolean;
|
||||
allowRecreateSpecificObject?: boolean;
|
||||
allowPairRenamedTables?: boolean;
|
||||
|
||||
ignoreCase?: boolean;
|
||||
schemaMode?: DbDiffSchemaMode;
|
||||
leftImplicitSchema?: string;
|
||||
rightImplicitSchema?: string;
|
||||
}
|
||||
|
||||
export function generateTablePairingId(table: TableInfo): TableInfo {
|
||||
if (!table) return table;
|
||||
if (!table.pairingId) {
|
||||
return {
|
||||
...table,
|
||||
columns: table.columns?.map(col => ({
|
||||
...col,
|
||||
pairingId: col.pairingId || uuidv1(),
|
||||
})),
|
||||
foreignKeys: table.foreignKeys?.map(cnt => ({
|
||||
...cnt,
|
||||
pairingId: cnt.pairingId || uuidv1(),
|
||||
})),
|
||||
checks: table.checks?.map(cnt => ({
|
||||
...cnt,
|
||||
pairingId: cnt.pairingId || uuidv1(),
|
||||
})),
|
||||
indexes: table.indexes?.map(cnt => ({
|
||||
...cnt,
|
||||
pairingId: cnt.pairingId || uuidv1(),
|
||||
})),
|
||||
uniques: table.uniques?.map(cnt => ({
|
||||
...cnt,
|
||||
pairingId: cnt.pairingId || uuidv1(),
|
||||
})),
|
||||
pairingId: table.pairingId || uuidv1(),
|
||||
};
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
function generateObjectPairingId(obj) {
|
||||
if (obj.objectTypeField)
|
||||
return {
|
||||
...obj,
|
||||
pairingId: obj.pairingId || uuidv1(),
|
||||
};
|
||||
return obj;
|
||||
}
|
||||
|
||||
export function generateDbPairingId(db: DatabaseInfo): DatabaseInfo {
|
||||
if (!db) return db;
|
||||
|
||||
return {
|
||||
...db,
|
||||
// ..._.mapValues(db, v => (_.isArray(v) ? v.map(generateObjectPairingId) : v)),
|
||||
tables: db.tables?.map(generateTablePairingId),
|
||||
views: db.views?.map(generateObjectPairingId),
|
||||
procedures: db.procedures?.map(generateObjectPairingId),
|
||||
functions: db.functions?.map(generateObjectPairingId),
|
||||
triggers: db.triggers?.map(generateObjectPairingId),
|
||||
matviews: db.matviews?.map(generateObjectPairingId),
|
||||
};
|
||||
}
|
||||
|
||||
function testEqualNames(a: string, b: string, opts: DbDiffOptions) {
|
||||
if (opts.ignoreCase) return a.toLowerCase() == b.toLowerCase();
|
||||
return a == b;
|
||||
}
|
||||
|
||||
function testEqualSchemas(lschema: string, rschema: string, opts: DbDiffOptions) {
|
||||
if (opts.schemaMode == 'ignore') lschema = null;
|
||||
if (opts.schemaMode == 'ignoreImplicit' && lschema == opts.leftImplicitSchema) lschema = null;
|
||||
if (opts.schemaMode == 'ignore') rschema = null;
|
||||
if (opts.schemaMode == 'ignoreImplicit' && rschema == opts.rightImplicitSchema) rschema = null;
|
||||
return testEqualNames(lschema, rschema, opts);
|
||||
}
|
||||
|
||||
function testEqualFullNames(lft: NamedObjectInfo, rgt: NamedObjectInfo, opts: DbDiffOptions) {
|
||||
if (lft == null || rgt == null) return lft == rgt;
|
||||
return testEqualSchemas(lft.schemaName, rgt.schemaName, opts) && testEqualNames(lft.pureName, rgt.pureName, opts);
|
||||
}
|
||||
|
||||
export function testEqualColumns(
|
||||
a: ColumnInfo,
|
||||
b: ColumnInfo,
|
||||
checkName: boolean,
|
||||
checkDefault: boolean,
|
||||
opts: DbDiffOptions = {}
|
||||
) {
|
||||
if (checkName && !testEqualNames(a.columnName, b.columnName, opts)) {
|
||||
// opts.DiffLogger.Trace("Column, different name: {0}; {1}", a, b);
|
||||
return false;
|
||||
}
|
||||
//if (!DbDiffTool.EqualFullNames(a.Domain, b.Domain, opts))
|
||||
//{
|
||||
// opts.DiffLogger.Trace("Column {0}, {1}: different domain: {2}; {3}", a, b, a.Domain, b.Domain);
|
||||
// return false;
|
||||
//}
|
||||
if (a.computedExpression != b.computedExpression) {
|
||||
// opts.DiffLogger.Trace(
|
||||
// 'Column {0}, {1}: different computed expression: {2}; {3}',
|
||||
// a,
|
||||
// b,
|
||||
// a.ComputedExpression,
|
||||
// b.ComputedExpression
|
||||
// );
|
||||
return false;
|
||||
}
|
||||
if (a.computedExpression != null) {
|
||||
return true;
|
||||
}
|
||||
if (checkDefault) {
|
||||
if (a.defaultValue == null) {
|
||||
if (a.defaultValue != b.defaultValue) {
|
||||
// opts.DiffLogger.Trace(
|
||||
// 'Column {0}, {1}: different default values: {2}; {3}',
|
||||
// a,
|
||||
// b,
|
||||
// a.DefaultValue,
|
||||
// b.DefaultValue
|
||||
// );
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (a.defaultValue != b.defaultValue) {
|
||||
// opts.DiffLogger.Trace(
|
||||
// 'Column {0}, {1}: different default values: {2}; {3}',
|
||||
// a,
|
||||
// b,
|
||||
// a.DefaultValue,
|
||||
// b.DefaultValue
|
||||
// );
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (a.defaultConstraint != b.defaultConstraint) {
|
||||
// opts.DiffLogger.Trace(
|
||||
// 'Column {0}, {1}: different default constraint names: {2}; {3}',
|
||||
// a,
|
||||
// b,
|
||||
// a.DefaultConstraint,
|
||||
// b.DefaultConstraint
|
||||
// );
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (a.notNull != b.notNull) {
|
||||
// opts.DiffLogger.Trace('Column {0}, {1}: different nullable: {2}; {3}', a, b, a.NotNull, b.NotNull);
|
||||
return false;
|
||||
}
|
||||
if (a.autoIncrement != b.autoIncrement) {
|
||||
// opts.DiffLogger.Trace('Column {0}, {1}: different autoincrement: {2}; {3}', a, b, a.AutoIncrement, b.AutoIncrement);
|
||||
return false;
|
||||
}
|
||||
if (a.isSparse != b.isSparse) {
|
||||
// opts.DiffLogger.Trace('Column {0}, {1}: different is_sparse: {2}; {3}', a, b, a.IsSparse, b.IsSparse);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!testEqualTypes(a, b, opts)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
//var btype = b.DataType;
|
||||
//var atype = a.DataType;
|
||||
//if (pairing != null && pairing.Target != null && pairing.Source.Dialect != null)
|
||||
//{
|
||||
// btype = pairing.Source.Dialect.MigrateDataType(b, btype, pairing.Source.Dialect.GetDefaultMigrationProfile(), null);
|
||||
// btype = pairing.Source.Dialect.GenericTypeToSpecific(btype).ToGenericType();
|
||||
|
||||
// // normalize type
|
||||
// atype = pairing.Source.Dialect.GenericTypeToSpecific(atype).ToGenericType();
|
||||
//}
|
||||
//if (!EqualTypes(atype, btype, opts))
|
||||
//{
|
||||
// opts.DiffLogger.Trace("Column {0}, {1}: different types: {2}; {3}", a, b, a.DataType, b.DataType);
|
||||
// return false;
|
||||
//}
|
||||
//if (!opts.IgnoreColumnCollation && a.Collation != b.Collation)
|
||||
//{
|
||||
// opts.DiffLogger.Trace("Column {0}, {1}: different collations: {2}; {3}", a, b, a.Collation, b.Collation);
|
||||
// return false;
|
||||
//}
|
||||
//if (!opts.IgnoreColumnCharacterSet && a.CharacterSet != b.CharacterSet)
|
||||
//{
|
||||
// opts.DiffLogger.Trace("Column {0}, {1}: different character sets: {2}; {3}", a, b, a.CharacterSet, b.CharacterSet);
|
||||
// return false;
|
||||
//}
|
||||
return true;
|
||||
}
|
||||
|
||||
function testEqualConstraints(a: ConstraintInfo, b: ConstraintInfo, opts: DbDiffOptions = {}) {
|
||||
return stableStringify(a) == stableStringify(b);
|
||||
}
|
||||
|
||||
export function testEqualTypes(a: ColumnInfo, b: ColumnInfo, opts: DbDiffOptions = {}) {
|
||||
if (a.dataType != b.dataType) {
|
||||
// opts.DiffLogger.Trace("Column {0}, {1}: different types: {2}; {3}", a, b, a.DataType, b.DataType);
|
||||
return false;
|
||||
}
|
||||
|
||||
//if (a.Length != b.Length)
|
||||
//{
|
||||
// opts.DiffLogger.Trace("Column {0}, {1}: different lengths: {2}; {3}", a, b, a.Length, b.Length);
|
||||
// return false;
|
||||
//}
|
||||
|
||||
//if (a.Precision != b.Precision)
|
||||
//{
|
||||
// opts.DiffLogger.Trace("Column {0}, {1}: different lengths: {2}; {3}", a, b, a.Precision, b.Precision);
|
||||
// return false;
|
||||
//}
|
||||
|
||||
//if (a.Scale != b.Scale)
|
||||
//{
|
||||
// opts.DiffLogger.Trace("Column {0}, {1}: different scale: {2}; {3}", a, b, a.Scale, b.Scale);
|
||||
// return false;
|
||||
//}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function getTableConstraints(table: TableInfo) {
|
||||
const res = [];
|
||||
if (table.primaryKey) res.push(table.primaryKey);
|
||||
if (table.foreignKeys) res.push(...table.foreignKeys);
|
||||
if (table.indexes) res.push(...table.indexes);
|
||||
if (table.uniques) res.push(...table.uniques);
|
||||
if (table.checks) res.push(...table.checks);
|
||||
return res;
|
||||
}
|
||||
|
||||
function createPairs(oldList, newList, additionalCondition = null) {
|
||||
const res = [];
|
||||
for (const a of oldList) {
|
||||
const b = newList.find(x => x.pairingId == a.pairingId || (additionalCondition && additionalCondition(a, x)));
|
||||
if (b) {
|
||||
res.push([a, b]);
|
||||
} else {
|
||||
res.push([a, null]);
|
||||
}
|
||||
}
|
||||
for (const b of newList) {
|
||||
if (!res.find(x => x[1] == b)) {
|
||||
res.push([null, b]);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function planAlterTable(plan: AlterPlan, oldTable: TableInfo, newTable: TableInfo, opts: DbDiffOptions) {
|
||||
// if (oldTable.primaryKey)
|
||||
|
||||
const columnPairs = createPairs(oldTable.columns, newTable.columns);
|
||||
const constraintPairs = createPairs(
|
||||
getTableConstraints(oldTable),
|
||||
getTableConstraints(newTable),
|
||||
(a, b) => a.constraintType == 'primaryKey' && b.constraintType == 'primaryKey'
|
||||
);
|
||||
|
||||
constraintPairs.filter(x => x[1] == null).forEach(x => plan.dropConstraint(x[0]));
|
||||
columnPairs.filter(x => x[1] == null).forEach(x => plan.dropColumn(x[0]));
|
||||
|
||||
if (!testEqualFullNames(oldTable, newTable, opts)) {
|
||||
plan.renameTable(oldTable, newTable.pureName);
|
||||
}
|
||||
|
||||
columnPairs.filter(x => x[0] == null).forEach(x => plan.createColumn(x[1]));
|
||||
|
||||
columnPairs
|
||||
.filter(x => x[0] && x[1])
|
||||
.forEach(x => {
|
||||
if (!testEqualColumns(x[0], x[1], true, true, opts)) {
|
||||
if (testEqualColumns(x[0], x[1], false, true, opts)) {
|
||||
// console.log('PLAN RENAME COLUMN')
|
||||
plan.renameColumn(x[0], x[1].columnName);
|
||||
} else {
|
||||
// console.log('PLAN CHANGE COLUMN')
|
||||
plan.changeColumn(x[0], x[1]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
constraintPairs
|
||||
.filter(x => x[0] && x[1])
|
||||
.forEach(x => {
|
||||
if (!testEqualConstraints(x[0], x[1], opts)) {
|
||||
// console.log('PLAN CHANGE CONSTRAINT', x[0], x[1]);
|
||||
plan.changeConstraint(x[0], x[1]);
|
||||
}
|
||||
});
|
||||
|
||||
constraintPairs.filter(x => x[0] == null).forEach(x => plan.createConstraint(x[1]));
|
||||
}
|
||||
|
||||
export function createAlterTablePlan(
|
||||
oldTable: TableInfo,
|
||||
newTable: TableInfo,
|
||||
opts: DbDiffOptions,
|
||||
db: DatabaseInfo,
|
||||
driver: EngineDriver
|
||||
): AlterPlan {
|
||||
const plan = new AlterPlan(db, driver.dialect);
|
||||
if (oldTable == null) {
|
||||
plan.createTable(newTable);
|
||||
} else {
|
||||
planAlterTable(plan, oldTable, newTable, opts);
|
||||
}
|
||||
plan.transformPlan();
|
||||
return plan;
|
||||
}
|
||||
|
||||
export function createAlterDatabasePlan(
|
||||
oldDb: DatabaseInfo,
|
||||
newDb: DatabaseInfo,
|
||||
opts: DbDiffOptions,
|
||||
db: DatabaseInfo,
|
||||
driver: EngineDriver
|
||||
): AlterPlan {
|
||||
const plan = new AlterPlan(db, driver.dialect);
|
||||
|
||||
for (const objectTypeField of ['tables', 'views', 'procedures', 'matviews', 'functions']) {
|
||||
for (const oldobj of oldDb[objectTypeField] || []) {
|
||||
const newobj = (newDb[objectTypeField] || []).find(x => x.pairingId == oldobj.pairingId);
|
||||
if (objectTypeField == 'tables') {
|
||||
if (newobj == null) plan.dropTable(oldobj);
|
||||
else planAlterTable(plan, oldobj, newobj, opts);
|
||||
} else {
|
||||
if (newobj == null) plan.dropSqlObject(oldobj);
|
||||
else if (newobj.createSql != oldobj.createSql) {
|
||||
plan.recreates.sqlObjects += 1;
|
||||
plan.dropSqlObject(oldobj);
|
||||
plan.createSqlObject(newobj);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const newobj of newDb[objectTypeField] || []) {
|
||||
const oldobj = (oldDb[objectTypeField] || []).find(x => x.pairingId == newobj.pairingId);
|
||||
if (objectTypeField == 'tables') {
|
||||
if (newobj == null) plan.createTable(newobj);
|
||||
} else {
|
||||
if (newobj == null) plan.createSqlObject(newobj);
|
||||
}
|
||||
}
|
||||
}
|
||||
plan.transformPlan();
|
||||
return plan;
|
||||
}
|
||||
|
||||
export function getAlterTableScript(
|
||||
oldTable: TableInfo,
|
||||
newTable: TableInfo,
|
||||
opts: DbDiffOptions,
|
||||
db: DatabaseInfo,
|
||||
driver: EngineDriver
|
||||
) {
|
||||
const plan = createAlterTablePlan(oldTable, newTable, opts, db, driver);
|
||||
const dmp = driver.createDumper();
|
||||
if (!driver.dialect.disableExplicitTransaction) dmp.beginTransaction();
|
||||
plan.run(dmp);
|
||||
if (!driver.dialect.disableExplicitTransaction) dmp.commitTransaction();
|
||||
return {
|
||||
sql: dmp.s,
|
||||
recreates: plan.recreates,
|
||||
};
|
||||
}
|
||||
|
||||
export function getAlterDatabaseScript(
|
||||
oldDb: DatabaseInfo,
|
||||
newDb: DatabaseInfo,
|
||||
opts: DbDiffOptions,
|
||||
db: DatabaseInfo,
|
||||
driver: EngineDriver
|
||||
) {
|
||||
const plan = createAlterDatabasePlan(oldDb, newDb, opts, db, driver);
|
||||
const dmp = driver.createDumper();
|
||||
if (!driver.dialect.disableExplicitTransaction) dmp.beginTransaction();
|
||||
plan.run(dmp);
|
||||
if (!driver.dialect.disableExplicitTransaction) dmp.commitTransaction();
|
||||
return {
|
||||
sql: dmp.s,
|
||||
recreates: plan.recreates,
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { SqlDumper } from './SqlDumper';
|
||||
import { splitQuery } from 'dbgate-query-splitter';
|
||||
|
||||
const dialect = {
|
||||
limitSelect: true,
|
||||
@@ -16,27 +17,27 @@ export const driverBase = {
|
||||
dumperClass: SqlDumper,
|
||||
dialect,
|
||||
|
||||
async analyseFull(pool) {
|
||||
const analyser = new this.analyserClass(pool, this);
|
||||
async analyseFull(pool, version) {
|
||||
const analyser = new this.analyserClass(pool, this, version);
|
||||
return analyser.fullAnalysis();
|
||||
},
|
||||
async analyseSingleObject(pool, name, typeField = 'tables') {
|
||||
const analyser = new this.analyserClass(pool, this);
|
||||
analyser.singleObjectFilter = { ...name, typeField };
|
||||
const res = await analyser.fullAnalysis();
|
||||
if (res[typeField].length == 1) return res[typeField][0];
|
||||
const obj = res[typeField].find(x => x.pureName == name.pureName && x.schemaName == name.schemaName);
|
||||
// console.log('FIND', name, obj);
|
||||
return obj;
|
||||
return analyser.singleObjectAnalysis(name, typeField);
|
||||
},
|
||||
analyseSingleTable(pool, name) {
|
||||
return this.analyseSingleObject(pool, name, 'tables');
|
||||
},
|
||||
async analyseIncremental(pool, structure) {
|
||||
const analyser = new this.analyserClass(pool, this);
|
||||
async analyseIncremental(pool, structure, version) {
|
||||
const analyser = new this.analyserClass(pool, this, version);
|
||||
return analyser.incrementalAnalysis(structure);
|
||||
},
|
||||
createDumper() {
|
||||
return new this.dumperClass(this);
|
||||
},
|
||||
async script(pool, sql) {
|
||||
for (const sqlItem of splitQuery(sql, this.getQuerySplitterOptions('script'))) {
|
||||
await this.query(pool, sqlItem, { discardResult: true });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import _ from 'lodash';
|
||||
import _compact from 'lodash/compact';
|
||||
|
||||
// original C# variant
|
||||
// public bool Match(string value)
|
||||
@@ -39,6 +39,12 @@ export function filterName(filter: string, ...names: string[]) {
|
||||
// const camelVariants = [name.replace(/[^A-Z]/g, '')]
|
||||
const tokens = filter.split(' ').map(x => x.trim());
|
||||
|
||||
return !!_.compact(names).find(name => !tokens.find(token => !name.toUpperCase().includes(token.toUpperCase())));
|
||||
// return name.toUpperCase().includes(filter.toUpperCase());
|
||||
const namesCompacted = _compact(names);
|
||||
for (const token of tokens) {
|
||||
const tokenUpper = token.toUpperCase();
|
||||
const found = namesCompacted.find(name => name.toUpperCase().includes(tokenUpper));
|
||||
if (!found) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -10,3 +10,7 @@ export * from './testPermission';
|
||||
export * from './SqlGenerator';
|
||||
export * from './structureTools';
|
||||
export * from './settingsExtractors';
|
||||
export * from './filterName';
|
||||
export * from './diffTools';
|
||||
export * from './schemaEditorTools';
|
||||
export * from './stringTools';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ColumnInfo, DatabaseInfo, DatabaseInfoObjects, TableInfo } from 'dbgate-types';
|
||||
import _ from 'lodash';
|
||||
import { ColumnInfo, ColumnReference, DatabaseInfo, DatabaseInfoObjects, SqlDialect, TableInfo } from 'dbgate-types';
|
||||
|
||||
export function fullNameFromString(name) {
|
||||
const m = name.match(/\[([^\]]+)\]\.\[([^\]]+)\]/);
|
||||
@@ -21,6 +22,13 @@ export function fullNameToString({ schemaName, pureName }) {
|
||||
return pureName;
|
||||
}
|
||||
|
||||
export function fullNameToLabel({ schemaName, pureName }) {
|
||||
if (schemaName) {
|
||||
return `${schemaName}.${pureName}`;
|
||||
}
|
||||
return pureName;
|
||||
}
|
||||
|
||||
export function quoteFullName(dialect, { schemaName, pureName }) {
|
||||
if (schemaName) return `${dialect.quoteIdentifier(schemaName)}.${dialect.quoteIdentifier(pureName)}`;
|
||||
return `${dialect.quoteIdentifier(pureName)}`;
|
||||
@@ -61,3 +69,28 @@ export function makeUniqueColumnNames(res: ColumnInfo[]) {
|
||||
usedNames.add(res[i].columnName);
|
||||
}
|
||||
}
|
||||
|
||||
function columnsConstraintName(prefix: string, table: TableInfo, columns: ColumnReference[]) {
|
||||
return `${prefix}_${table.pureName}_${columns.map(x => x.columnName.replace(' ', '_')).join('_')}`;
|
||||
}
|
||||
|
||||
export function fillConstraintNames(table: TableInfo, dialect: SqlDialect) {
|
||||
if (!table) return table;
|
||||
const res = _.cloneDeep(table);
|
||||
if (res.primaryKey && !res.primaryKey.constraintName && !dialect.anonymousPrimaryKey) {
|
||||
res.primaryKey.constraintName = `PK_${res.pureName}`;
|
||||
}
|
||||
for (const fk of res.foreignKeys || []) {
|
||||
if (fk.constraintName) continue;
|
||||
fk.constraintName = columnsConstraintName('FK', res, fk.columns);
|
||||
}
|
||||
for (const ix of res.indexes || []) {
|
||||
if (ix.constraintName) continue;
|
||||
ix.constraintName = columnsConstraintName('IX', res, ix.columns);
|
||||
}
|
||||
for (const uq of res.uniques || []) {
|
||||
if (uq.constraintName) continue;
|
||||
uq.constraintName = columnsConstraintName('UQ', res, uq.columns);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
import uuidv1 from 'uuid/v1';
|
||||
import _omit from 'lodash/omit';
|
||||
import {
|
||||
ColumnInfo,
|
||||
ConstraintInfo,
|
||||
ForeignKeyInfo,
|
||||
IndexInfo,
|
||||
PrimaryKeyInfo,
|
||||
TableInfo,
|
||||
UniqueInfo,
|
||||
} from 'dbgate-types';
|
||||
import _ from 'lodash';
|
||||
|
||||
export interface EditorColumnInfo extends ColumnInfo {
|
||||
isPrimaryKey?: boolean;
|
||||
}
|
||||
|
||||
export function fillEditorColumnInfo(column: ColumnInfo, table: TableInfo): EditorColumnInfo {
|
||||
return {
|
||||
isPrimaryKey: !!table?.primaryKey?.columns?.find(x => x.columnName == column.columnName),
|
||||
dataType: _.isEmpty(column) ? 'int' : undefined,
|
||||
...column,
|
||||
};
|
||||
}
|
||||
|
||||
function processPrimaryKey(table: TableInfo, oldColumn: EditorColumnInfo, newColumn: EditorColumnInfo): TableInfo {
|
||||
if (!oldColumn?.isPrimaryKey && newColumn?.isPrimaryKey) {
|
||||
let primaryKey = table?.primaryKey;
|
||||
if (!primaryKey) {
|
||||
primaryKey = {
|
||||
constraintType: 'primaryKey',
|
||||
pureName: table.pureName,
|
||||
schemaName: table.schemaName,
|
||||
columns: [],
|
||||
};
|
||||
}
|
||||
return {
|
||||
...table,
|
||||
primaryKey: {
|
||||
...primaryKey,
|
||||
columns: [
|
||||
...primaryKey.columns,
|
||||
{
|
||||
columnName: newColumn.columnName,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (oldColumn?.isPrimaryKey && !newColumn?.isPrimaryKey) {
|
||||
let primaryKey = table?.primaryKey;
|
||||
if (primaryKey) {
|
||||
primaryKey = {
|
||||
...primaryKey,
|
||||
columns: table.primaryKey.columns.filter(x => x.columnName != oldColumn.columnName),
|
||||
};
|
||||
if (primaryKey.columns.length == 0) {
|
||||
return {
|
||||
...table,
|
||||
primaryKey: null,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...table,
|
||||
primaryKey,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return table;
|
||||
}
|
||||
|
||||
export function editorAddColumn(table: TableInfo, column: EditorColumnInfo): TableInfo {
|
||||
let res = {
|
||||
...table,
|
||||
columns: [...(table?.columns || []), { ...column, pairingId: uuidv1() }],
|
||||
};
|
||||
|
||||
res = processPrimaryKey(res, null, column);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
export function editorModifyColumn(table: TableInfo, column: EditorColumnInfo): TableInfo {
|
||||
const oldColumn = table?.columns?.find(x => x.pairingId == column.pairingId);
|
||||
|
||||
let res = {
|
||||
...table,
|
||||
columns: table.columns.map(col => (col.pairingId == column.pairingId ? _omit(column, ['isPrimaryKey']) : col)),
|
||||
};
|
||||
res = processPrimaryKey(res, fillEditorColumnInfo(oldColumn, table), column);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
export function editorDeleteColumn(table: TableInfo, column: EditorColumnInfo): TableInfo {
|
||||
let res = {
|
||||
...table,
|
||||
columns: table.columns.filter(col => col.pairingId != column.pairingId),
|
||||
};
|
||||
|
||||
res = processPrimaryKey(res, column, null);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
export function editorAddConstraint(table: TableInfo, constraint: ConstraintInfo): TableInfo {
|
||||
const res = {
|
||||
...table,
|
||||
};
|
||||
|
||||
if (constraint.constraintType == 'primaryKey') {
|
||||
res.primaryKey = {
|
||||
pairingId: uuidv1(),
|
||||
...constraint,
|
||||
} as PrimaryKeyInfo;
|
||||
}
|
||||
|
||||
if (constraint.constraintType == 'foreignKey') {
|
||||
res.foreignKeys = [
|
||||
...(res.foreignKeys || []),
|
||||
{
|
||||
pairingId: uuidv1(),
|
||||
...constraint,
|
||||
} as ForeignKeyInfo,
|
||||
];
|
||||
}
|
||||
|
||||
if (constraint.constraintType == 'index') {
|
||||
res.indexes = [
|
||||
...(res.indexes || []),
|
||||
{
|
||||
pairingId: uuidv1(),
|
||||
...constraint,
|
||||
} as IndexInfo,
|
||||
];
|
||||
}
|
||||
|
||||
if (constraint.constraintType == 'unique') {
|
||||
res.uniques = [
|
||||
...(res.uniques || []),
|
||||
{
|
||||
pairingId: uuidv1(),
|
||||
...constraint,
|
||||
} as UniqueInfo,
|
||||
];
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
export function editorModifyConstraint(table: TableInfo, constraint: ConstraintInfo): TableInfo {
|
||||
const res = {
|
||||
...table,
|
||||
};
|
||||
|
||||
if (constraint.constraintType == 'primaryKey') {
|
||||
res.primaryKey = {
|
||||
...res.primaryKey,
|
||||
...constraint,
|
||||
};
|
||||
}
|
||||
|
||||
if (constraint.constraintType == 'foreignKey') {
|
||||
res.foreignKeys = table.foreignKeys.map(fk =>
|
||||
fk.pairingId == constraint.pairingId ? { ...fk, ...constraint } : fk
|
||||
);
|
||||
}
|
||||
|
||||
if (constraint.constraintType == 'index') {
|
||||
res.indexes = table.indexes.map(fk => (fk.pairingId == constraint.pairingId ? { ...fk, ...constraint } : fk));
|
||||
}
|
||||
|
||||
if (constraint.constraintType == 'unique') {
|
||||
res.uniques = table.uniques.map(fk => (fk.pairingId == constraint.pairingId ? { ...fk, ...constraint } : fk));
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
export function editorDeleteConstraint(table: TableInfo, constraint: ConstraintInfo): TableInfo {
|
||||
const res = {
|
||||
...table,
|
||||
};
|
||||
|
||||
if (constraint.constraintType == 'primaryKey') {
|
||||
res.primaryKey = null;
|
||||
}
|
||||
|
||||
if (constraint.constraintType == 'foreignKey') {
|
||||
res.foreignKeys = table.foreignKeys.filter(x => x.pairingId != constraint.pairingId);
|
||||
}
|
||||
|
||||
if (constraint.constraintType == 'index') {
|
||||
res.indexes = table.indexes.filter(x => x.pairingId != constraint.pairingId);
|
||||
}
|
||||
|
||||
if (constraint.constraintType == 'unique') {
|
||||
res.uniques = table.uniques.filter(x => x.pairingId != constraint.pairingId);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
import _ from 'lodash';
|
||||
import _isNaN from 'lodash/isNaN';
|
||||
import _isNumber from 'lodash/isNumber';
|
||||
|
||||
export function extractIntSettingsValue(settings, name, defaultValue, min = null, max = null) {
|
||||
const parsed = parseInt(settings[name]);
|
||||
if (_.isNaN(parsed)) {
|
||||
if (_isNaN(parsed)) {
|
||||
return defaultValue;
|
||||
}
|
||||
if (_.isNumber(parsed)) {
|
||||
if (_isNumber(parsed)) {
|
||||
if (min != null && parsed < min) return min;
|
||||
if (max != null && parsed > max) return max;
|
||||
return parsed;
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
export function arrayToHexString(byteArray) {
|
||||
return byteArray.reduce((output, elem) => output + ('0' + elem.toString(16)).slice(-2), '');
|
||||
}
|
||||
|
||||
export function hexStringToArray(inputString) {
|
||||
var hex = inputString.toString();
|
||||
var res = [];
|
||||
for (var n = 0; n < hex.length; n += 2) {
|
||||
res.push(parseInt(hex.substr(n, 2), 16));
|
||||
}
|
||||
return res;
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { DatabaseInfo } from 'dbgate-types';
|
||||
import _ from 'lodash';
|
||||
import { DatabaseInfo, TableInfo } from 'dbgate-types';
|
||||
import _flatten from 'lodash/flatten';
|
||||
|
||||
export function addTableDependencies(db: DatabaseInfo): DatabaseInfo {
|
||||
const allForeignKeys = _.flatten(db.tables.map(x => x.foreignKeys || []));
|
||||
const allForeignKeys = _flatten(db.tables.map(x => x.foreignKeys || []));
|
||||
return {
|
||||
...db,
|
||||
tables: db.tables.map(table => ({
|
||||
@@ -12,50 +12,54 @@ export function addTableDependencies(db: DatabaseInfo): DatabaseInfo {
|
||||
};
|
||||
}
|
||||
|
||||
function fillTableExtendedInfo(db: DatabaseInfo): DatabaseInfo {
|
||||
export function extendTableInfo(table: TableInfo): TableInfo {
|
||||
return {
|
||||
...table,
|
||||
objectTypeField: 'tables',
|
||||
columns: (table.columns || []).map(column => ({
|
||||
pureName: table.pureName,
|
||||
schemaName: table.schemaName,
|
||||
...column,
|
||||
})),
|
||||
primaryKey: table.primaryKey
|
||||
? {
|
||||
...table.primaryKey,
|
||||
pureName: table.pureName,
|
||||
schemaName: table.schemaName,
|
||||
constraintType: 'primaryKey',
|
||||
}
|
||||
: undefined,
|
||||
foreignKeys: (table.foreignKeys || []).map(cnt => ({
|
||||
...cnt,
|
||||
pureName: table.pureName,
|
||||
schemaName: table.schemaName,
|
||||
constraintType: 'foreignKey',
|
||||
})),
|
||||
indexes: (table.indexes || []).map(cnt => ({
|
||||
...cnt,
|
||||
pureName: table.pureName,
|
||||
schemaName: table.schemaName,
|
||||
constraintType: 'index',
|
||||
})),
|
||||
checks: (table.checks || []).map(cnt => ({
|
||||
...cnt,
|
||||
pureName: table.pureName,
|
||||
schemaName: table.schemaName,
|
||||
constraintType: 'check',
|
||||
})),
|
||||
uniques: (table.uniques || []).map(cnt => ({
|
||||
...cnt,
|
||||
pureName: table.pureName,
|
||||
schemaName: table.schemaName,
|
||||
constraintType: 'unique',
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function fillDatabaseExtendedInfo(db: DatabaseInfo): DatabaseInfo {
|
||||
return {
|
||||
...db,
|
||||
tables: (db.tables || []).map(obj => ({
|
||||
...obj,
|
||||
objectTypeField: 'tables',
|
||||
columns: (obj.columns || []).map(column => ({
|
||||
pureName: obj.pureName,
|
||||
schemaName: obj.schemaName,
|
||||
...column,
|
||||
})),
|
||||
primaryKey: obj.primaryKey
|
||||
? {
|
||||
...obj.primaryKey,
|
||||
pureName: obj.pureName,
|
||||
schemaName: obj.schemaName,
|
||||
constraintType: 'primaryKey',
|
||||
}
|
||||
: undefined,
|
||||
foreignKeys: (obj.foreignKeys || []).map(cnt => ({
|
||||
...cnt,
|
||||
pureName: obj.pureName,
|
||||
schemaName: obj.schemaName,
|
||||
constraintType: 'foreignKey',
|
||||
})),
|
||||
indexes: (obj.indexes || []).map(cnt => ({
|
||||
...cnt,
|
||||
pureName: obj.pureName,
|
||||
schemaName: obj.schemaName,
|
||||
constraintType: 'index',
|
||||
})),
|
||||
checks: (obj.checks || []).map(cnt => ({
|
||||
...cnt,
|
||||
pureName: obj.pureName,
|
||||
schemaName: obj.schemaName,
|
||||
constraintType: 'check',
|
||||
})),
|
||||
uniques: (obj.uniques || []).map(cnt => ({
|
||||
...cnt,
|
||||
pureName: obj.pureName,
|
||||
schemaName: obj.schemaName,
|
||||
constraintType: 'unique',
|
||||
})),
|
||||
})),
|
||||
tables: (db.tables || []).map(extendTableInfo),
|
||||
collections: (db.collections || []).map(obj => ({
|
||||
...obj,
|
||||
objectTypeField: 'collections',
|
||||
@@ -64,6 +68,10 @@ function fillTableExtendedInfo(db: DatabaseInfo): DatabaseInfo {
|
||||
...obj,
|
||||
objectTypeField: 'views',
|
||||
})),
|
||||
matviews: (db.matviews || []).map(obj => ({
|
||||
...obj,
|
||||
objectTypeField: 'matviews',
|
||||
})),
|
||||
procedures: (db.procedures || []).map(obj => ({
|
||||
...obj,
|
||||
objectTypeField: 'procedures',
|
||||
@@ -80,5 +88,5 @@ function fillTableExtendedInfo(db: DatabaseInfo): DatabaseInfo {
|
||||
}
|
||||
|
||||
export function extendDatabaseInfo(db: DatabaseInfo): DatabaseInfo {
|
||||
return fillTableExtendedInfo(addTableDependencies(db));
|
||||
return fillDatabaseExtendedInfo(addTableDependencies(db));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { ColumnInfo, ConstraintInfo, TableInfo, SqlObjectInfo } from './dbinfo';
|
||||
|
||||
export interface AlterProcessor {
|
||||
createTable(table: TableInfo);
|
||||
dropTable(table: TableInfo);
|
||||
createColumn(column: ColumnInfo, constraints: ConstraintInfo[]);
|
||||
changeColumn(oldColumn: ColumnInfo, newColumn: ColumnInfo, constraints?: ConstraintInfo[]);
|
||||
dropColumn(column: ColumnInfo);
|
||||
createConstraint(constraint: ConstraintInfo);
|
||||
changeConstraint(oldConstraint: ConstraintInfo, newConstraint: ConstraintInfo);
|
||||
dropConstraint(constraint: ConstraintInfo);
|
||||
renameTable(table: TableInfo, newName: string);
|
||||
renameColumn(column: ColumnInfo, newName: string);
|
||||
renameConstraint(constraint: ConstraintInfo, newName: string);
|
||||
recreateTable(oldTable: TableInfo, newTable: TableInfo);
|
||||
createSqlObject(obj: SqlObjectInfo);
|
||||
dropSqlObject(obj: SqlObjectInfo);
|
||||
}
|
||||