mirror of
https://github.com/lana-k/sqliteviz.git
synced 2025-12-06 10:08:52 +08:00
add CSV support #27
This commit is contained in:
@@ -12,7 +12,8 @@ module.exports = {
|
||||
},
|
||||
rules: {
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'no-case-declarations': 'off'
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
|
||||
@@ -25,13 +25,6 @@ module.exports = function (config) {
|
||||
included: false,
|
||||
served: true,
|
||||
nocache: false
|
||||
},
|
||||
{
|
||||
pattern: 'node_modules/sql.js/dist/worker.sql-wasm.js',
|
||||
watched: false,
|
||||
included: false,
|
||||
served: true,
|
||||
nocache: false
|
||||
}
|
||||
],
|
||||
|
||||
@@ -136,6 +129,10 @@ module.exports = function (config) {
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.worker\.js$/,
|
||||
loader: 'worker-loader'
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
|
||||
loader: 'url-loader'
|
||||
@@ -181,7 +178,8 @@ module.exports = function (config) {
|
||||
}
|
||||
},
|
||||
proxies: {
|
||||
'/js/': '/base/node_modules/sql.js/dist/'
|
||||
'/_karma_webpack_/sql-wasm.wasm': '/base/node_modules/sql.js/dist/sql-wasm.wasm',
|
||||
'/base/sql-wasm.wasm': '/base/node_modules/sql.js/dist/sql-wasm.wasm'
|
||||
}
|
||||
})
|
||||
// Fix the timezone
|
||||
|
||||
181
package-lock.json
generated
181
package-lock.json
generated
@@ -13,7 +13,9 @@
|
||||
"core-js": "^3.6.5",
|
||||
"debounce": "^1.2.0",
|
||||
"nanoid": "^3.1.12",
|
||||
"papaparse": "^5.3.0",
|
||||
"plotly.js": "^1.57.1",
|
||||
"promise-worker": "^2.0.1",
|
||||
"react": "^16.13.1",
|
||||
"react-chart-editor": "^0.42.0",
|
||||
"react-dom": "^16.13.1",
|
||||
@@ -37,6 +39,7 @@
|
||||
"@vue/test-utils": "^1.1.2",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"chai": "^4.1.2",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-import": "^2.20.2",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
@@ -46,7 +49,8 @@
|
||||
"karma": "^3.1.4",
|
||||
"karma-webpack": "^4.0.2",
|
||||
"vue-cli-plugin-ui-karma": "^0.2.5",
|
||||
"vue-template-compiler": "^2.6.11"
|
||||
"vue-template-compiler": "^2.6.11",
|
||||
"worker-loader": "^3.0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
@@ -1559,9 +1563,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/json-schema": {
|
||||
"version": "7.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.5.tgz",
|
||||
"integrity": "sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ==",
|
||||
"version": "7.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz",
|
||||
"integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/json5": {
|
||||
@@ -2510,15 +2514,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "6.12.3",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.3.tgz",
|
||||
"integrity": "sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA==",
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
"json-schema-traverse": "^0.4.1",
|
||||
"uri-js": "^4.2.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv-errors": {
|
||||
@@ -2528,10 +2536,13 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/ajv-keywords": {
|
||||
"version": "3.5.1",
|
||||
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.1.tgz",
|
||||
"integrity": "sha512-KWcq3xN8fDjSB+IMoh2VaXVhRI0BBGxoYp3rx7Pkb6z0cFjYR9Q9l4yZqqals0/zsioCmocC5H6UvsGD4MoIBA==",
|
||||
"dev": true
|
||||
"version": "3.5.2",
|
||||
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
|
||||
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
|
||||
"dev": true,
|
||||
"peerDependencies": {
|
||||
"ajv": "^6.9.1"
|
||||
}
|
||||
},
|
||||
"node_modules/align-text": {
|
||||
"version": "0.1.4",
|
||||
@@ -4296,6 +4307,18 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/chai-as-promised": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.1.tgz",
|
||||
"integrity": "sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"check-error": "^1.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"chai": ">= 2.1.2 < 5"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
|
||||
@@ -14826,6 +14849,11 @@
|
||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/papaparse": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.3.0.tgz",
|
||||
"integrity": "sha512-Lb7jN/4bTpiuGPrYy4tkKoUS8sTki8zacB5ke1p5zolhcSE4TlWgrlsxjrDTbG/dFVh07ck7X36hUf/b5V68pg=="
|
||||
},
|
||||
"node_modules/parallel-transform": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz",
|
||||
@@ -16287,6 +16315,11 @@
|
||||
"integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/promise-worker": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/promise-worker/-/promise-worker-2.0.1.tgz",
|
||||
"integrity": "sha512-jR7vHqMEwWJ15i9vA3qyCKwRHihyLJp1sAa3RyY5F35m3u5s2lQUfq0nzVjbA8Xc7+3mL3Y9+9MHBO9UFRpFxA=="
|
||||
},
|
||||
"node_modules/prop-types": {
|
||||
"version": "15.7.2",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
|
||||
@@ -21999,6 +22032,58 @@
|
||||
"errno": "~0.1.7"
|
||||
}
|
||||
},
|
||||
"node_modules/worker-loader": {
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/worker-loader/-/worker-loader-3.0.8.tgz",
|
||||
"integrity": "sha512-XQyQkIFeRVC7f7uRhFdNMe/iJOdO6zxAaR3EWbDp45v3mDhrTi+++oswKNxShUNjPC/1xUp5DB29YKLhFo129g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"loader-utils": "^2.0.0",
|
||||
"schema-utils": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/webpack"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"webpack": "^4.0.0 || ^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/worker-loader/node_modules/loader-utils": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
|
||||
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"big.js": "^5.2.2",
|
||||
"emojis-list": "^3.0.0",
|
||||
"json5": "^2.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/worker-loader/node_modules/schema-utils": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz",
|
||||
"integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.6",
|
||||
"ajv": "^6.12.5",
|
||||
"ajv-keywords": "^3.5.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/world-calendars": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/world-calendars/-/world-calendars-1.0.3.tgz",
|
||||
@@ -23752,9 +23837,9 @@
|
||||
}
|
||||
},
|
||||
"@types/json-schema": {
|
||||
"version": "7.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.5.tgz",
|
||||
"integrity": "sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ==",
|
||||
"version": "7.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz",
|
||||
"integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/json5": {
|
||||
@@ -24613,9 +24698,9 @@
|
||||
}
|
||||
},
|
||||
"ajv": {
|
||||
"version": "6.12.3",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.3.tgz",
|
||||
"integrity": "sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA==",
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
@@ -24631,10 +24716,11 @@
|
||||
"dev": true
|
||||
},
|
||||
"ajv-keywords": {
|
||||
"version": "3.5.1",
|
||||
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.1.tgz",
|
||||
"integrity": "sha512-KWcq3xN8fDjSB+IMoh2VaXVhRI0BBGxoYp3rx7Pkb6z0cFjYR9Q9l4yZqqals0/zsioCmocC5H6UvsGD4MoIBA==",
|
||||
"dev": true
|
||||
"version": "3.5.2",
|
||||
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
|
||||
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"align-text": {
|
||||
"version": "0.1.4",
|
||||
@@ -26184,6 +26270,15 @@
|
||||
"type-detect": "^4.0.5"
|
||||
}
|
||||
},
|
||||
"chai-as-promised": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.1.tgz",
|
||||
"integrity": "sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"check-error": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
|
||||
@@ -35229,6 +35324,11 @@
|
||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
||||
"dev": true
|
||||
},
|
||||
"papaparse": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.3.0.tgz",
|
||||
"integrity": "sha512-Lb7jN/4bTpiuGPrYy4tkKoUS8sTki8zacB5ke1p5zolhcSE4TlWgrlsxjrDTbG/dFVh07ck7X36hUf/b5V68pg=="
|
||||
},
|
||||
"parallel-transform": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz",
|
||||
@@ -36516,6 +36616,11 @@
|
||||
"integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=",
|
||||
"dev": true
|
||||
},
|
||||
"promise-worker": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/promise-worker/-/promise-worker-2.0.1.tgz",
|
||||
"integrity": "sha512-jR7vHqMEwWJ15i9vA3qyCKwRHihyLJp1sAa3RyY5F35m3u5s2lQUfq0nzVjbA8Xc7+3mL3Y9+9MHBO9UFRpFxA=="
|
||||
},
|
||||
"prop-types": {
|
||||
"version": "15.7.2",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
|
||||
@@ -41542,6 +41647,40 @@
|
||||
"errno": "~0.1.7"
|
||||
}
|
||||
},
|
||||
"worker-loader": {
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/worker-loader/-/worker-loader-3.0.8.tgz",
|
||||
"integrity": "sha512-XQyQkIFeRVC7f7uRhFdNMe/iJOdO6zxAaR3EWbDp45v3mDhrTi+++oswKNxShUNjPC/1xUp5DB29YKLhFo129g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"loader-utils": "^2.0.0",
|
||||
"schema-utils": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"loader-utils": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
|
||||
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"big.js": "^5.2.2",
|
||||
"emojis-list": "^3.0.0",
|
||||
"json5": "^2.1.2"
|
||||
}
|
||||
},
|
||||
"schema-utils": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz",
|
||||
"integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/json-schema": "^7.0.6",
|
||||
"ajv": "^6.12.5",
|
||||
"ajv-keywords": "^3.5.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"world-calendars": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/world-calendars/-/world-calendars-1.0.3.tgz",
|
||||
|
||||
@@ -14,7 +14,9 @@
|
||||
"core-js": "^3.6.5",
|
||||
"debounce": "^1.2.0",
|
||||
"nanoid": "^3.1.12",
|
||||
"papaparse": "^5.3.0",
|
||||
"plotly.js": "^1.57.1",
|
||||
"promise-worker": "^2.0.1",
|
||||
"react": "^16.13.1",
|
||||
"react-chart-editor": "^0.42.0",
|
||||
"react-dom": "^16.13.1",
|
||||
@@ -38,6 +40,7 @@
|
||||
"@vue/test-utils": "^1.1.2",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"chai": "^4.1.2",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-import": "^2.20.2",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
@@ -47,6 +50,7 @@
|
||||
"karma": "^3.1.4",
|
||||
"karma-webpack": "^4.0.2",
|
||||
"vue-cli-plugin-ui-karma": "^0.2.5",
|
||||
"vue-template-compiler": "^2.6.11"
|
||||
"vue-template-compiler": "^2.6.11",
|
||||
"worker-loader": "^3.0.8"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div class="db-upload-container">
|
||||
<div class="drop-area-container">
|
||||
<change-db-icon v-if="type === 'small'" @click.native="browse"/>
|
||||
<div v-if="['regular', 'illustrated'].includes(type)" class="drop-area-container">
|
||||
<div
|
||||
class="drop-area"
|
||||
@dragover.prevent="state = 'dragover'"
|
||||
@@ -13,7 +14,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="illustrated" id="img-container">
|
||||
<div v-if="type === 'illustrated'" id="img-container">
|
||||
<img id="drop-file-top-img" :src="require('@/assets/images/top.svg')" />
|
||||
<img
|
||||
id="left-arm-img"
|
||||
@@ -38,29 +39,141 @@
|
||||
/>
|
||||
</div>
|
||||
<div id="error" class="error"></div>
|
||||
|
||||
<!--Parse csv dialog -->
|
||||
<modal name="parse" classes="dialog" height="auto" width="60%" :clickToClose="false">
|
||||
<div class="dialog-header">
|
||||
Import CSV
|
||||
<close-icon @click="cancelCsvImport"/>
|
||||
</div>
|
||||
<div class="dialog-body">
|
||||
<div class="chars">
|
||||
<delimiter-selector
|
||||
v-model="delimiter"
|
||||
width="210px"
|
||||
class="char-input"
|
||||
@input="previewCSV"
|
||||
:disabled="disableDialog"
|
||||
/>
|
||||
<text-field
|
||||
label="Quote char"
|
||||
hint="The character used to quote fields."
|
||||
v-model="quoteChar"
|
||||
width="93px"
|
||||
:disabled="disableDialog"
|
||||
class="char-input"
|
||||
/>
|
||||
<text-field
|
||||
label="Escape char"
|
||||
hint='The character used to escape the quote character within a field (e.g. "column with ""quotes"" in text").'
|
||||
max-hint-width="242px"
|
||||
v-model="escapeChar"
|
||||
width="93px"
|
||||
:disabled="disableDialog"
|
||||
class="char-input"
|
||||
/>
|
||||
</div>
|
||||
<check-box
|
||||
@click="header = $event"
|
||||
:init="true"
|
||||
label="Use first row as column headers"
|
||||
:disabled="disableDialog"
|
||||
/>
|
||||
<sql-table
|
||||
v-if="previewData"
|
||||
:data-set="previewData"
|
||||
height="160"
|
||||
class="preview-table"
|
||||
:preview="true"
|
||||
/>
|
||||
<div v-if="!previewData" class="no-data">No data</div>
|
||||
<logs
|
||||
class="import-csv-errors"
|
||||
:messages="importCsvMessages"
|
||||
/>
|
||||
</div>
|
||||
<div class="dialog-buttons-container">
|
||||
<button
|
||||
class="secondary"
|
||||
:disabled="disableDialog"
|
||||
@click="cancelCsvImport"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
v-show="!importCsvCompleted"
|
||||
class="primary"
|
||||
:disabled="disableDialog"
|
||||
@click="loadFromCsv(file)"
|
||||
>
|
||||
Import
|
||||
</button>
|
||||
<button
|
||||
v-show="importCsvCompleted"
|
||||
class="primary"
|
||||
:disabled="disableDialog"
|
||||
@click="finish"
|
||||
>
|
||||
Finish
|
||||
</button>
|
||||
</div>
|
||||
</modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import fu from '@/fileUtils'
|
||||
import csv from '@/csv'
|
||||
import CloseIcon from '@/components/svg/close'
|
||||
import TextField from '@/components/TextField'
|
||||
import DelimiterSelector from '@/components/DelimiterSelector'
|
||||
import CheckBox from '@/components/CheckBox'
|
||||
import SqlTable from '@/components/SqlTable'
|
||||
import Logs from '@/components/Logs'
|
||||
import ChangeDbIcon from '@/components/svg/changeDb'
|
||||
import time from '@/time'
|
||||
import database from '@/database'
|
||||
|
||||
export default {
|
||||
name: 'DbUpload',
|
||||
props: {
|
||||
illustrated: {
|
||||
type: Boolean,
|
||||
type: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: false
|
||||
default: 'regular',
|
||||
validator: (value) => {
|
||||
return ['regular', 'illustrated', 'small'].includes(value)
|
||||
}
|
||||
}
|
||||
},
|
||||
components: {
|
||||
ChangeDbIcon,
|
||||
TextField,
|
||||
DelimiterSelector,
|
||||
CloseIcon,
|
||||
CheckBox,
|
||||
SqlTable,
|
||||
Logs
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
state: '',
|
||||
animationPromise: Promise.resolve()
|
||||
animationPromise: Promise.resolve(),
|
||||
file: null,
|
||||
schema: null,
|
||||
delimiter: '',
|
||||
quoteChar: '"',
|
||||
escapeChar: '"',
|
||||
header: false,
|
||||
previewData: null,
|
||||
importCsvMessages: [],
|
||||
disableDialog: false,
|
||||
importCsvCompleted: false,
|
||||
newDb: null
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
if (this.illustrated) {
|
||||
if (this.type === 'illustrated') {
|
||||
this.animationPromise = new Promise((resolve) => {
|
||||
this.$refs.fileImg.addEventListener('animationend', event => {
|
||||
if (event.animationName.startsWith('fly')) {
|
||||
@@ -70,29 +183,216 @@ export default {
|
||||
})
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
quoteChar () {
|
||||
this.previewCSV()
|
||||
},
|
||||
|
||||
escapeChar () {
|
||||
this.previewCSV()
|
||||
},
|
||||
|
||||
header () {
|
||||
this.previewCSV()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loadDb (file) {
|
||||
this.state = 'drop'
|
||||
return Promise.all([this.$db.loadDb(file), this.animationPromise])
|
||||
.then(([schema]) => {
|
||||
this.$store.commit('saveSchema', schema)
|
||||
cancelCsvImport () {
|
||||
if (!this.disableDialog) {
|
||||
this.$modal.hide('parse')
|
||||
if (this.newDb) {
|
||||
this.newDb.shutDown()
|
||||
this.newDb = null
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async finish () {
|
||||
this.$store.commit('setDb', this.newDb)
|
||||
this.$store.commit('saveSchema', this.schema)
|
||||
if (this.importCsvCompleted) {
|
||||
this.$modal.hide('parse')
|
||||
const tabId = await this.$store.dispatch('addTab', { query: 'select * from csv_import' })
|
||||
this.$store.commit('setCurrentTabId', tabId)
|
||||
}
|
||||
if (this.$route.path !== '/editor') {
|
||||
this.$router.push('/editor')
|
||||
}
|
||||
},
|
||||
|
||||
async previewCSV () {
|
||||
this.importCsvCompleted = false
|
||||
const config = {
|
||||
preview: 3,
|
||||
quoteChar: this.quoteChar || '"',
|
||||
escapeChar: this.escapeChar,
|
||||
header: this.header,
|
||||
delimiter: this.delimiter
|
||||
}
|
||||
try {
|
||||
const start = new Date()
|
||||
const parseResult = await csv.parse(this.file, config)
|
||||
const end = new Date()
|
||||
this.previewData = parseResult.data
|
||||
this.delimiter = parseResult.delimiter
|
||||
|
||||
// In parseResult.messages we can get parse errors
|
||||
this.importCsvMessages = parseResult.messages
|
||||
|
||||
if (parseResult.messages.length === 0) {
|
||||
this.importCsvMessages.push({
|
||||
message: `Preview parsing is completed in ${time.getPeriod(start, end)}.`,
|
||||
type: 'success'
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
this.importCsvMessages = [{
|
||||
message: err,
|
||||
type: 'error'
|
||||
}]
|
||||
}
|
||||
},
|
||||
|
||||
loadDb (file) {
|
||||
this.newDb = database.getNewDatabase()
|
||||
return Promise.all([this.newDb.loadDb(file), this.animationPromise])
|
||||
.then(([schema]) => {
|
||||
this.schema = schema
|
||||
this.finish()
|
||||
})
|
||||
},
|
||||
browse () {
|
||||
fu.getFileFromUser('.db,.sqlite,.sqlite3')
|
||||
.then(this.loadDb)
|
||||
|
||||
async loadFromCsv (file) {
|
||||
this.disableDialog = true
|
||||
const config = {
|
||||
quoteChar: this.quoteChar || '"',
|
||||
escapeChar: this.escapeChar,
|
||||
header: this.header,
|
||||
delimiter: this.delimiter
|
||||
}
|
||||
const parseCsvMsg = {
|
||||
message: 'Parsing CSV...',
|
||||
type: 'info'
|
||||
}
|
||||
this.importCsvMessages.push(parseCsvMsg)
|
||||
const parseCsvLoadingIndicator = setTimeout(() => { parseCsvMsg.type = 'loading' }, 1000)
|
||||
|
||||
const importMsg = {
|
||||
message: 'Importing CSV into a SQLite database...',
|
||||
type: 'info'
|
||||
}
|
||||
let importLoadingIndicator = null
|
||||
|
||||
const updateProgress = e => {
|
||||
this.$set(importMsg, 'progress', e.detail)
|
||||
}
|
||||
this.newDb = database.getNewDatabase()
|
||||
const progressCounterId = this.newDb.createProgressCounter(updateProgress)
|
||||
|
||||
try {
|
||||
let start = new Date()
|
||||
const parseResult = await csv.parse(this.file, config)
|
||||
let end = new Date()
|
||||
|
||||
if (!parseResult.hasErrors) {
|
||||
const rowCount = parseResult.data.values.length
|
||||
let period = time.getPeriod(start, end)
|
||||
parseCsvMsg.type = 'success'
|
||||
|
||||
if (parseResult.messages.length > 0) {
|
||||
this.importCsvMessages = this.importCsvMessages.concat(parseResult.messages)
|
||||
parseCsvMsg.message = `${rowCount} rows are parsed in ${period}.`
|
||||
} else {
|
||||
// Inform about csv parsing success
|
||||
parseCsvMsg.message = `${rowCount} rows are parsed successfully in ${period}.`
|
||||
}
|
||||
|
||||
// Loading indicator for csv parsing is not needed anymore
|
||||
clearTimeout(parseCsvLoadingIndicator)
|
||||
|
||||
// Add info about import start
|
||||
this.importCsvMessages.push(importMsg)
|
||||
|
||||
// Show import progress after 1 second
|
||||
importLoadingIndicator = setTimeout(() => {
|
||||
importMsg.type = 'loading'
|
||||
}, 1000)
|
||||
|
||||
// Create db with csv table and get schema
|
||||
start = new Date()
|
||||
this.schema = await this.newDb.createDb(file.name, parseResult.data, progressCounterId)
|
||||
end = new Date()
|
||||
if (this.schema.error) {
|
||||
throw this.schema.error
|
||||
}
|
||||
|
||||
// Inform about import success
|
||||
period = time.getPeriod(start, end)
|
||||
importMsg.message = `Importing CSV into a SQLite database is completed in ${period}.`
|
||||
importMsg.type = 'success'
|
||||
|
||||
// Loading indicator for import is not needed anymore
|
||||
clearTimeout(importLoadingIndicator)
|
||||
|
||||
this.importCsvCompleted = true
|
||||
} else {
|
||||
parseCsvMsg.message = 'Parsing ended with errors.'
|
||||
parseCsvMsg.type = 'info'
|
||||
this.importCsvMessages = this.importCsvMessages.concat(parseResult.messages)
|
||||
}
|
||||
} catch (err) {
|
||||
if (parseCsvMsg.type === 'loading') {
|
||||
parseCsvMsg.type = 'info'
|
||||
}
|
||||
|
||||
if (importMsg.type === 'loading') {
|
||||
importMsg.type = 'info'
|
||||
}
|
||||
|
||||
this.importCsvMessages.push({
|
||||
message: err,
|
||||
type: 'error'
|
||||
})
|
||||
}
|
||||
|
||||
clearTimeout(parseCsvLoadingIndicator)
|
||||
clearTimeout(importLoadingIndicator)
|
||||
this.newDb.deleteProgressCounter(progressCounterId)
|
||||
this.disableDialog = false
|
||||
},
|
||||
|
||||
async checkFile (file) {
|
||||
this.state = 'drop'
|
||||
if (file.type === 'text/csv') {
|
||||
this.file = file
|
||||
this.header = true
|
||||
this.quoteChar = '"'
|
||||
this.escapeChar = '"'
|
||||
this.delimiter = ''
|
||||
return Promise.all([this.previewCSV(), this.animationPromise])
|
||||
.then(() => {
|
||||
this.$modal.show('parse')
|
||||
})
|
||||
} else {
|
||||
this.loadDb(file)
|
||||
}
|
||||
},
|
||||
browse () {
|
||||
fu.getFileFromUser('.db,.sqlite,.sqlite3,.csv')
|
||||
.then(this.checkFile)
|
||||
},
|
||||
|
||||
drop (event) {
|
||||
this.loadDb(event.dataTransfer.files[0])
|
||||
this.checkFile(event.dataTransfer.files[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.db-upload-container {
|
||||
position: relative;
|
||||
}
|
||||
.drop-area-container {
|
||||
display: inline-block;
|
||||
border: 1px dashed var(--color-border);
|
||||
@@ -118,9 +418,9 @@ export default {
|
||||
|
||||
#img-container {
|
||||
position: absolute;
|
||||
top: calc(50% - 120px);
|
||||
top: 54px;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
transform: translate(-50%, 0);
|
||||
width: 450px;
|
||||
height: 338px;
|
||||
pointer-events: none;
|
||||
@@ -192,4 +492,35 @@ export default {
|
||||
@keyframes fly {
|
||||
100% { transform: rotate(360deg) scale(0.5); }
|
||||
}
|
||||
/* Parse CSV dialog */
|
||||
.chars {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.char-input {
|
||||
margin-right: 44px;
|
||||
}
|
||||
.preview-table {
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
.import-csv-errors {
|
||||
height: 160px;
|
||||
margin-top: 32px;
|
||||
}
|
||||
.no-data {
|
||||
margin-top: 32px;
|
||||
background-color: white;
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
border: 1px solid var(--color-border-light);
|
||||
box-sizing: border-box;
|
||||
height: 160px;
|
||||
font-size: 13px;
|
||||
color: var(--color-text-base);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,9 +8,7 @@
|
||||
<tree-chevron :expanded="schemaVisible"/>
|
||||
{{ dbName }}
|
||||
</div>
|
||||
<div id="db-edit" @click="changeDb">
|
||||
<change-db-icon />
|
||||
</div>
|
||||
<db-upload id="db-edit" type="small" />
|
||||
</div>
|
||||
<div v-show="schemaVisible" class="schema">
|
||||
<table-description
|
||||
@@ -26,17 +24,16 @@
|
||||
<script>
|
||||
import TableDescription from '@/components/TableDescription'
|
||||
import TextField from '@/components/TextField'
|
||||
import ChangeDbIcon from '@/components/svg/changeDb'
|
||||
import TreeChevron from '@/components/svg/treeChevron'
|
||||
import fu from '@/fileUtils'
|
||||
import dbUpload from '@/components/DbUpload'
|
||||
|
||||
export default {
|
||||
name: 'Schema',
|
||||
components: {
|
||||
TableDescription,
|
||||
TextField,
|
||||
ChangeDbIcon,
|
||||
TreeChevron
|
||||
TreeChevron,
|
||||
dbUpload
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
@@ -59,17 +56,6 @@ export default {
|
||||
dbName () {
|
||||
return this.$store.state.dbName
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeDb () {
|
||||
fu.getFileFromUser('.db,.sqlite,.sqlite3')
|
||||
.then(file => {
|
||||
return this.$db.loadDb(file)
|
||||
})
|
||||
.then((schema) => {
|
||||
this.$store.commit('saveSchema', schema)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -104,21 +104,16 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
// Run a command in the database
|
||||
execute () {
|
||||
// this.$refs.output.textContent = 'Fetching results...' */
|
||||
async execute () {
|
||||
this.isGettingResults = true
|
||||
this.result = null
|
||||
this.error = null
|
||||
return this.$db.execute(this.query + ';')
|
||||
.then(result => {
|
||||
this.result = result
|
||||
})
|
||||
.catch(err => {
|
||||
try {
|
||||
this.result = await this.$store.state.db.execute(this.query + ';')
|
||||
} catch (err) {
|
||||
this.error = err
|
||||
})
|
||||
.finally(() => {
|
||||
}
|
||||
this.isGettingResults = false
|
||||
})
|
||||
},
|
||||
handleResize () {
|
||||
if (this.view === 'chart') {
|
||||
|
||||
78
src/csv.js
Normal file
78
src/csv.js
Normal file
@@ -0,0 +1,78 @@
|
||||
import Papa from 'papaparse'
|
||||
|
||||
const hintsByCode = {
|
||||
MissingQuotes: 'Edit your CSV so that the field has a closing quote char.',
|
||||
TooFewFields: 'Add fields or try another delimiter.',
|
||||
TooManyFields: 'Edit your CSV or try another delimiter.'
|
||||
}
|
||||
|
||||
export default {
|
||||
getResut (source) {
|
||||
const result = {}
|
||||
if (source.meta.fields) {
|
||||
result.columns = source.meta.fields
|
||||
result.values = source.data.map(row => {
|
||||
const resultRow = []
|
||||
result.columns.forEach(col => { resultRow.push(row[col]) })
|
||||
return resultRow
|
||||
})
|
||||
} else {
|
||||
result.values = source.data
|
||||
result.columns = []
|
||||
for (let i = 1; i <= source.data[0].length; i++) {
|
||||
result.columns.push(`col ${i}`)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
},
|
||||
|
||||
parse (file, config = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const defaultConfig = {
|
||||
delimiter: '', // auto-detect
|
||||
newline: '', // auto-detect
|
||||
quoteChar: '"',
|
||||
escapeChar: '"',
|
||||
header: false,
|
||||
transformHeader: undefined,
|
||||
dynamicTyping: true,
|
||||
preview: 0,
|
||||
encoding: 'UTF-8',
|
||||
worker: true,
|
||||
comments: false,
|
||||
step: undefined,
|
||||
complete: results => {
|
||||
const res = {
|
||||
data: this.getResut(results),
|
||||
delimiter: results.meta.delimiter,
|
||||
hasErrors: false
|
||||
}
|
||||
res.messages = results.errors.map(msg => {
|
||||
msg.type = msg.code === 'UndetectableDelimiter' ? 'info' : 'error'
|
||||
if (msg.type === 'error') res.hasErrors = true
|
||||
msg.hint = hintsByCode[msg.code]
|
||||
return msg
|
||||
})
|
||||
resolve(res)
|
||||
},
|
||||
error: (error, file) => {
|
||||
reject(error)
|
||||
},
|
||||
download: false,
|
||||
downloadRequestHeaders: undefined,
|
||||
downloadRequestBody: undefined,
|
||||
skipEmptyLines: 'greedy',
|
||||
chunk: undefined,
|
||||
chunkSize: undefined,
|
||||
fastMode: undefined,
|
||||
beforeFirstChunk: undefined,
|
||||
withCredentials: undefined,
|
||||
transform: undefined,
|
||||
delimitersToGuess: [',', '\t', '|', ';', Papa.RECORD_SEP, Papa.UNIT_SEP]
|
||||
}
|
||||
|
||||
Papa.parse(file, { ...defaultConfig, ...config })
|
||||
})
|
||||
}
|
||||
}
|
||||
123
src/database.js
123
src/database.js
@@ -1,21 +1,88 @@
|
||||
import sqliteParser from 'sqlite-parser'
|
||||
const worker = new Worker('js/worker.sql-wasm.js')
|
||||
import fu from '@/fileUtils'
|
||||
// We can import workers like so because of worker-loader:
|
||||
// https://webpack.js.org/loaders/worker-loader/
|
||||
import Worker from '@/db.worker.js'
|
||||
|
||||
// Use promise-worker in order to turn worker into the promise based one:
|
||||
// https://github.com/nolanlawson/promise-worker
|
||||
import PromiseWorker from 'promise-worker'
|
||||
|
||||
function getNewDatabase () {
|
||||
const worker = new Worker()
|
||||
return new Database(worker)
|
||||
}
|
||||
|
||||
export default {
|
||||
loadDb (file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const f = file
|
||||
const r = new FileReader()
|
||||
r.onload = () => {
|
||||
// on 'action: open' completed
|
||||
worker.onmessage = () => {
|
||||
getNewDatabase
|
||||
}
|
||||
|
||||
let progressCounterIds = 0
|
||||
class Database {
|
||||
constructor (worker) {
|
||||
this.worker = worker
|
||||
this.pw = new PromiseWorker(worker)
|
||||
|
||||
this.importProgresses = {}
|
||||
worker.addEventListener('message', e => {
|
||||
const progress = e.data.progress
|
||||
if (progress !== undefined) {
|
||||
const id = e.data.id
|
||||
this.importProgresses[id].dispatchEvent(new CustomEvent('progress', {
|
||||
detail: progress
|
||||
}))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
shutDown () {
|
||||
this.worker.terminate()
|
||||
}
|
||||
|
||||
createProgressCounter (callback) {
|
||||
const id = progressCounterIds++
|
||||
this.importProgresses[id] = new EventTarget()
|
||||
this.importProgresses[id].addEventListener('progress', callback)
|
||||
return id
|
||||
}
|
||||
|
||||
deleteProgressCounter (id) {
|
||||
delete this.importProgresses[id]
|
||||
}
|
||||
|
||||
async createDb (name, data, progressCounterId) {
|
||||
const result = await this.pw.postMessage({
|
||||
action: 'import',
|
||||
columns: data.columns,
|
||||
values: data.values,
|
||||
progressCounterId
|
||||
})
|
||||
|
||||
if (result.error) {
|
||||
throw result.error
|
||||
}
|
||||
|
||||
return await this.getSchema(name)
|
||||
}
|
||||
|
||||
async loadDb (file) {
|
||||
const fileContent = await fu.readAsArrayBuffer(file)
|
||||
const res = await this.pw.postMessage({ action: 'open', buffer: fileContent })
|
||||
|
||||
if (res.error) {
|
||||
throw res.error
|
||||
}
|
||||
|
||||
return this.getSchema(file.name)
|
||||
}
|
||||
|
||||
async getSchema (name) {
|
||||
const getSchemaSql = `
|
||||
SELECT name, sql
|
||||
FROM sqlite_master
|
||||
WHERE type='table' AND name NOT LIKE 'sqlite_%';`
|
||||
|
||||
this.execute(getSchemaSql)
|
||||
.then(result => {
|
||||
WHERE type='table' AND name NOT LIKE 'sqlite_%';
|
||||
`
|
||||
const result = await this.execute(getSchemaSql)
|
||||
// Parse DDL statements to get column names and types
|
||||
const parsedSchema = []
|
||||
result.values.forEach(item => {
|
||||
@@ -26,34 +93,20 @@ export default {
|
||||
})
|
||||
|
||||
// Return db name and schema
|
||||
resolve({
|
||||
dbName: file.name,
|
||||
return {
|
||||
dbName: name,
|
||||
schema: parsedSchema
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
worker.postMessage({ action: 'open', buffer: r.result }, [r.result])
|
||||
} catch (exception) {
|
||||
worker.postMessage({ action: 'open', buffer: r.result })
|
||||
async execute (commands) {
|
||||
const results = await this.pw.postMessage({ action: 'exec', sql: commands })
|
||||
|
||||
if (results.error) {
|
||||
throw results.error
|
||||
}
|
||||
}
|
||||
r.readAsArrayBuffer(f)
|
||||
})
|
||||
},
|
||||
execute (commands) {
|
||||
return new Promise((resolve, reject) => {
|
||||
worker.onmessage = (event) => {
|
||||
if (event.data.error) {
|
||||
reject(event.data.error)
|
||||
} else {
|
||||
// if it was more than one select - take only the first one
|
||||
resolve(event.data.results[0])
|
||||
}
|
||||
}
|
||||
worker.postMessage({ action: 'exec', sql: commands })
|
||||
})
|
||||
return results[0]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
98
src/db.worker.js
Normal file
98
src/db.worker.js
Normal file
@@ -0,0 +1,98 @@
|
||||
import registerPromiseWorker from 'promise-worker/register'
|
||||
import initSqlJs from 'sql.js/dist/sql-wasm.js'
|
||||
import dbUtils from '@/dbUtils'
|
||||
|
||||
const sqlModuleReady = initSqlJs()
|
||||
let db = null
|
||||
|
||||
function onModuleReady (SQL) {
|
||||
function createDb (data) {
|
||||
if (db != null) db.close()
|
||||
db = new SQL.Database(data)
|
||||
return db
|
||||
}
|
||||
|
||||
const data = this
|
||||
|
||||
switch (data && data.action) {
|
||||
case 'open':
|
||||
const buff = data.buffer
|
||||
createDb(buff && new Uint8Array(buff))
|
||||
return {
|
||||
ready: true
|
||||
}
|
||||
case 'exec':
|
||||
if (db === null) {
|
||||
createDb()
|
||||
}
|
||||
if (!data.sql) {
|
||||
throw new Error('exec: Missing query string')
|
||||
}
|
||||
return db.exec(data.sql, data.params)
|
||||
case 'each':
|
||||
if (db === null) {
|
||||
createDb()
|
||||
}
|
||||
const callback = function callback (row) {
|
||||
return {
|
||||
row: row,
|
||||
finished: false
|
||||
}
|
||||
}
|
||||
const done = function done () {
|
||||
return {
|
||||
finished: true
|
||||
}
|
||||
}
|
||||
return db.each(data.sql, data.params, callback, done)
|
||||
case 'import':
|
||||
createDb()
|
||||
const values = data.values
|
||||
const columns = data.columns
|
||||
const chunkSize = 1500
|
||||
db.exec(dbUtils.getCreateStatement(columns, values))
|
||||
const chunks = dbUtils.generateChunks(values, chunkSize)
|
||||
const chunksAmount = Math.ceil(values.length / chunkSize)
|
||||
let count = 0
|
||||
const insertStr = dbUtils.getInsertStmt(columns)
|
||||
const insertStmt = db.prepare(insertStr)
|
||||
|
||||
postMessage({ progress: 0, id: data.progressCounterId })
|
||||
for (const chunk of chunks) {
|
||||
db.exec('BEGIN')
|
||||
for (const row of chunk) {
|
||||
insertStmt.run(row)
|
||||
}
|
||||
db.exec('COMMIT')
|
||||
count++
|
||||
postMessage({ progress: 100 * (count / chunksAmount), id: data.progressCounterId })
|
||||
}
|
||||
|
||||
return {
|
||||
finish: true
|
||||
}
|
||||
case 'export':
|
||||
return db.export()
|
||||
case 'close':
|
||||
if (db) {
|
||||
db.close()
|
||||
}
|
||||
return {
|
||||
finished: true
|
||||
}
|
||||
default:
|
||||
throw new Error('Invalid action : ' + (data && data.action))
|
||||
}
|
||||
}
|
||||
|
||||
function onError (err) {
|
||||
return {
|
||||
error: new Error(err.message)
|
||||
}
|
||||
}
|
||||
|
||||
registerPromiseWorker(data => {
|
||||
return sqlModuleReady
|
||||
.then(onModuleReady.bind(data))
|
||||
.catch(onError)
|
||||
})
|
||||
44
src/dbUtils.js
Normal file
44
src/dbUtils.js
Normal file
@@ -0,0 +1,44 @@
|
||||
export default {
|
||||
* generateChunks (arr, size) {
|
||||
const count = Math.ceil(arr.length / size)
|
||||
|
||||
for (let i = 0; i <= count - 1; i++) {
|
||||
const start = size * i
|
||||
const end = start + size
|
||||
yield arr.slice(start, end)
|
||||
}
|
||||
},
|
||||
|
||||
getInsertStmt (columns) {
|
||||
const colList = `"${columns.join('", "')}"`
|
||||
const params = columns.map(() => '?').join(' ,')
|
||||
return `INSERT INTO csv_import (${colList}) VALUES (${params});`
|
||||
},
|
||||
|
||||
getCreateStatement (columns, values) {
|
||||
let result = 'CREATE table csv_import('
|
||||
columns.forEach((col, index) => {
|
||||
// Get the first row of values to determine types
|
||||
const value = values[0][index]
|
||||
let type = ''
|
||||
switch (typeof value) {
|
||||
case 'number': {
|
||||
type = 'REAL'
|
||||
break
|
||||
}
|
||||
case 'boolean': {
|
||||
type = 'INTEGER'
|
||||
break
|
||||
}
|
||||
case 'string': {
|
||||
type = 'TEXT'
|
||||
break
|
||||
}
|
||||
default: type = 'TEXT'
|
||||
}
|
||||
result += `"${col}" ${type},`
|
||||
})
|
||||
result = result.replace(/.$/, ');')
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -50,5 +50,21 @@ export default {
|
||||
|
||||
readFile (path) {
|
||||
return fetch(path)
|
||||
},
|
||||
|
||||
readAsArrayBuffer (file) {
|
||||
const fileReader = new FileReader()
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
fileReader.onerror = () => {
|
||||
fileReader.abort()
|
||||
reject(new DOMException('Problem parsing input file.'))
|
||||
}
|
||||
|
||||
fileReader.onload = () => {
|
||||
resolve(fileReader.result)
|
||||
}
|
||||
fileReader.readAsArrayBuffer(file)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import router from './router'
|
||||
import store from './store'
|
||||
import { VuePlugin } from 'vuera'
|
||||
import VModal from 'vue-js-modal'
|
||||
import db from '@/database'
|
||||
|
||||
import '@/assets/styles/variables.css'
|
||||
import '@/assets/styles/buttons.css'
|
||||
@@ -17,7 +16,6 @@ Vue.use(VuePlugin)
|
||||
Vue.use(VModal)
|
||||
|
||||
Vue.config.productionTip = false
|
||||
Vue.prototype.$db = db
|
||||
|
||||
new Vue({
|
||||
router,
|
||||
|
||||
@@ -12,10 +12,17 @@ export const state = {
|
||||
currentTab: null,
|
||||
currentTabId: null,
|
||||
untitledLastIndex: 0,
|
||||
predefinedQueries: []
|
||||
predefinedQueries: [],
|
||||
db: null
|
||||
}
|
||||
|
||||
export const mutations = {
|
||||
setDb (state, db) {
|
||||
if (state.db) {
|
||||
state.db.shutDown()
|
||||
}
|
||||
state.db = db
|
||||
},
|
||||
saveSchema (state, { dbName, schema }) {
|
||||
state.dbName = dbName
|
||||
state.schema = schema
|
||||
@@ -73,19 +80,18 @@ export const mutations = {
|
||||
|
||||
export const actions = {
|
||||
async addTab ({ state }, data) {
|
||||
let tab
|
||||
const tab = data ? JSON.parse(JSON.stringify(data)) : {}
|
||||
// If no data then create a new blank one...
|
||||
if (!data) {
|
||||
tab = {
|
||||
id: nanoid(),
|
||||
name: null,
|
||||
tempName: state.untitledLastIndex
|
||||
// No data.id means to create new tab, but not blank,
|
||||
// e.g. with 'select * from csv_import' query after csv import
|
||||
if (!data || !data.id) {
|
||||
tab.id = nanoid()
|
||||
tab.name = null
|
||||
tab.tempName = state.untitledLastIndex
|
||||
? `Untitled ${state.untitledLastIndex}`
|
||||
: 'Untitled',
|
||||
isUnsaved: true
|
||||
}
|
||||
: 'Untitled'
|
||||
tab.isUnsaved = true
|
||||
} else {
|
||||
tab = JSON.parse(JSON.stringify(data))
|
||||
tab.isUnsaved = false
|
||||
}
|
||||
|
||||
|
||||
36
src/time.js
Normal file
36
src/time.js
Normal file
@@ -0,0 +1,36 @@
|
||||
export default {
|
||||
getPeriod (start, end) {
|
||||
let diff = end.getTime() - start.getTime()
|
||||
let result = ''
|
||||
|
||||
const days = Math.floor(diff / (1000 * 60 * 60 * 24))
|
||||
diff -= days * (1000 * 60 * 60 * 24)
|
||||
if (days) {
|
||||
result += days + ' d '
|
||||
}
|
||||
|
||||
const hours = Math.floor(diff / (1000 * 60 * 60))
|
||||
diff -= hours * (1000 * 60 * 60)
|
||||
if (hours) {
|
||||
result += hours + ' h '
|
||||
}
|
||||
|
||||
const mins = Math.floor(diff / (1000 * 60))
|
||||
diff -= mins * (1000 * 60)
|
||||
if (mins) {
|
||||
result += mins + ' m '
|
||||
}
|
||||
|
||||
const seconds = Math.floor(diff / (1000))
|
||||
diff -= seconds * (1000)
|
||||
if (seconds) {
|
||||
result += seconds + ' s '
|
||||
}
|
||||
|
||||
if (diff) {
|
||||
result += diff + ' ms '
|
||||
}
|
||||
|
||||
return result.replace(/\s$/, '')
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div id="dbloader-container">
|
||||
<db-upload illustrated />
|
||||
<db-upload type="illustrated" />
|
||||
<div id="note">
|
||||
Sqliteviz is fully client-side. Your database never leaves your computer.
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import Vuex from 'vuex'
|
||||
import { shallowMount } from '@vue/test-utils'
|
||||
import DbUpload from '@/components/DbUpload.vue'
|
||||
import fu from '@/fileUtils'
|
||||
import database from '@/database.js'
|
||||
|
||||
describe('DbUploader.vue', () => {
|
||||
afterEach(() => {
|
||||
@@ -24,7 +25,10 @@ describe('DbUploader.vue', () => {
|
||||
|
||||
// mock db loading
|
||||
const schema = {}
|
||||
const $db = { loadDb: sinon.stub().resolves(schema) }
|
||||
const db = {
|
||||
loadDb: sinon.stub().resolves(schema)
|
||||
}
|
||||
database.getNewDatabase = sinon.stub().returns(db)
|
||||
|
||||
// mock router
|
||||
const $router = { push: sinon.stub() }
|
||||
@@ -33,12 +37,12 @@ describe('DbUploader.vue', () => {
|
||||
// mount the component
|
||||
const wrapper = shallowMount(DbUpload, {
|
||||
store,
|
||||
mocks: { $db, $router, $route }
|
||||
mocks: { $router, $route }
|
||||
})
|
||||
|
||||
await wrapper.find('.drop-area').trigger('click')
|
||||
expect($db.loadDb.calledOnceWith(file)).to.equal(true)
|
||||
await $db.loadDb.returnValues[0]
|
||||
expect(db.loadDb.calledOnceWith(file)).to.equal(true)
|
||||
await db.loadDb.returnValues[0]
|
||||
expect(mutations.saveSchema.calledOnceWith(state, schema)).to.equal(true)
|
||||
expect($router.push.calledOnceWith('/editor')).to.equal(true)
|
||||
})
|
||||
@@ -53,7 +57,10 @@ describe('DbUploader.vue', () => {
|
||||
|
||||
// mock db loading
|
||||
const schema = {}
|
||||
const $db = { loadDb: sinon.stub().resolves(schema) }
|
||||
const db = {
|
||||
loadDb: sinon.stub().resolves(schema)
|
||||
}
|
||||
database.getNewDatabase = sinon.stub().returns(db)
|
||||
|
||||
// mock router
|
||||
const $router = { push: sinon.stub() }
|
||||
@@ -62,7 +69,7 @@ describe('DbUploader.vue', () => {
|
||||
// mount the component
|
||||
const wrapper = shallowMount(DbUpload, {
|
||||
store,
|
||||
mocks: { $db, $router, $route }
|
||||
mocks: { $router, $route }
|
||||
})
|
||||
|
||||
// mock a file dropped by a user
|
||||
@@ -74,8 +81,8 @@ describe('DbUploader.vue', () => {
|
||||
})
|
||||
|
||||
await wrapper.find('.drop-area').trigger('drop', dropData)
|
||||
expect($db.loadDb.calledOnceWith(file)).to.equal(true)
|
||||
await $db.loadDb.returnValues[0]
|
||||
expect(db.loadDb.calledOnceWith(file)).to.equal(true)
|
||||
await db.loadDb.returnValues[0]
|
||||
expect(mutations.saveSchema.calledOnceWith(state, schema)).to.equal(true)
|
||||
expect($router.push.calledOnceWith('/editor')).to.equal(true)
|
||||
})
|
||||
@@ -94,7 +101,10 @@ describe('DbUploader.vue', () => {
|
||||
|
||||
// mock db loading
|
||||
const schema = {}
|
||||
const $db = { loadDb: sinon.stub().resolves(schema) }
|
||||
const db = {
|
||||
loadDb: sinon.stub().resolves(schema)
|
||||
}
|
||||
database.getNewDatabase = sinon.stub().returns(db)
|
||||
|
||||
// mock router
|
||||
const $router = { push: sinon.stub() }
|
||||
@@ -103,11 +113,11 @@ describe('DbUploader.vue', () => {
|
||||
// mount the component
|
||||
const wrapper = shallowMount(DbUpload, {
|
||||
store,
|
||||
mocks: { $db, $router, $route }
|
||||
mocks: { $router, $route }
|
||||
})
|
||||
|
||||
await wrapper.find('.drop-area').trigger('click')
|
||||
await $db.loadDb.returnValues[0]
|
||||
await db.loadDb.returnValues[0]
|
||||
expect($router.push.called).to.equal(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -184,7 +184,6 @@ describe('MainMenu.vue', () => {
|
||||
|
||||
it('Ctrl R calls currentTab.execute if running is enabled and route.path is "/editor"',
|
||||
async () => {
|
||||
console.log('ctrl r')
|
||||
const state = {
|
||||
currentTab: {
|
||||
query: 'SELECT * FROM foo',
|
||||
|
||||
@@ -4,7 +4,6 @@ import { mount, createLocalVue } from '@vue/test-utils'
|
||||
import Vuex from 'vuex'
|
||||
import Schema from '@/components/Schema.vue'
|
||||
import TableDescription from '@/components/TableDescription.vue'
|
||||
import fu from '@/fileUtils.js'
|
||||
|
||||
const localVue = createLocalVue()
|
||||
localVue.use(Vuex)
|
||||
@@ -99,75 +98,4 @@ describe('Schema.vue', () => {
|
||||
expect(tables.at(1).vm.name).to.equal('bar')
|
||||
expect(tables.at(2).vm.name).to.equal('foobar')
|
||||
})
|
||||
|
||||
it('Change DB', async () => {
|
||||
// mock store state and mutations
|
||||
const mutations = {
|
||||
saveSchema: sinon.stub()
|
||||
}
|
||||
|
||||
const state = {
|
||||
dbName: 'fooDB',
|
||||
schema: [
|
||||
{
|
||||
name: 'foo',
|
||||
columns: [
|
||||
{ name: 'foo_id', type: 'INTEGER' },
|
||||
{ name: 'foo_title', type: 'NVARCHAR(24)' }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'foo_prices',
|
||||
columns: [
|
||||
{ name: 'foo_id', type: 'INTEGER' },
|
||||
{ name: 'foo_price', type: 'INTEGER' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const store = new Vuex.Store({ state, mutations })
|
||||
|
||||
// stub getFileFromUser
|
||||
const file = { file: 'hello' }
|
||||
sinon.stub(fu, 'getFileFromUser').resolves(file)
|
||||
|
||||
// mock $db.loadDb()
|
||||
const newSchema = {
|
||||
dbName: 'barDB',
|
||||
schema: [
|
||||
{
|
||||
name: 'bar',
|
||||
columns: [
|
||||
{ name: 'bar_id', type: 'INTEGER' },
|
||||
{ name: 'bar_title', type: 'NVARCHAR(24)' }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'bar_prices',
|
||||
columns: [
|
||||
{ name: 'bar_id', type: 'INTEGER' },
|
||||
{ name: 'bar_price', type: 'INTEGER' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
const $db = {
|
||||
loadDb: sinon.stub().resolves(newSchema)
|
||||
}
|
||||
|
||||
// mount the component
|
||||
const wrapper = mount(Schema, { store, localVue, mocks: { $db } })
|
||||
|
||||
// trigger the event
|
||||
await wrapper.find('#db-edit').trigger('click')
|
||||
|
||||
expect(fu.getFileFromUser.calledOnceWith('.db,.sqlite,.sqlite3')).to.equal(true)
|
||||
|
||||
await fu.getFileFromUser.returnValues[0]
|
||||
expect($db.loadDb.calledOnceWith(file)).to.equal(true)
|
||||
|
||||
await $db.loadDb.returnValues[0]
|
||||
expect(mutations.saveSchema.calledOnceWith(state, newSchema)).to.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -145,18 +145,17 @@ describe('Tab.vue', () => {
|
||||
it('Shows .result-in-progress message when executing query', (done) => {
|
||||
// mock store state
|
||||
const state = {
|
||||
currentTabId: 1
|
||||
currentTabId: 1,
|
||||
db: {
|
||||
execute () { return new Promise(() => {}) }
|
||||
}
|
||||
}
|
||||
|
||||
const store = new Vuex.Store({ state, mutations })
|
||||
const $db = {
|
||||
execute () { return new Promise(() => {}) }
|
||||
}
|
||||
// mount the component
|
||||
const wrapper = mount(Tab, {
|
||||
store,
|
||||
stubs: ['chart'],
|
||||
mocks: { $db },
|
||||
propsData: {
|
||||
id: 1,
|
||||
initName: 'foo',
|
||||
@@ -178,18 +177,17 @@ describe('Tab.vue', () => {
|
||||
it('Shows error when executing query ends with error', async () => {
|
||||
// mock store state
|
||||
const state = {
|
||||
currentTabId: 1
|
||||
currentTabId: 1,
|
||||
db: {
|
||||
execute () { return Promise.reject(new Error('There is no table foo')) }
|
||||
}
|
||||
}
|
||||
|
||||
const store = new Vuex.Store({ state, mutations })
|
||||
const $db = {
|
||||
execute () { return Promise.reject(new Error('There is no table foo')) }
|
||||
}
|
||||
// mount the component
|
||||
const wrapper = mount(Tab, {
|
||||
store,
|
||||
stubs: ['chart'],
|
||||
mocks: { $db },
|
||||
propsData: {
|
||||
id: 1,
|
||||
initName: 'foo',
|
||||
@@ -210,7 +208,10 @@ describe('Tab.vue', () => {
|
||||
it('Passes result to sql-table component', async () => {
|
||||
// mock store state
|
||||
const state = {
|
||||
currentTabId: 1
|
||||
currentTabId: 1,
|
||||
db: {
|
||||
execute () { return Promise.resolve(result) }
|
||||
}
|
||||
}
|
||||
|
||||
const store = new Vuex.Store({ state, mutations })
|
||||
@@ -221,14 +222,11 @@ describe('Tab.vue', () => {
|
||||
[2, 'bar']
|
||||
]
|
||||
}
|
||||
const $db = {
|
||||
execute () { return Promise.resolve(result) }
|
||||
}
|
||||
|
||||
// mount the component
|
||||
const wrapper = mount(Tab, {
|
||||
store,
|
||||
stubs: ['chart'],
|
||||
mocks: { $db },
|
||||
propsData: {
|
||||
id: 1,
|
||||
initName: 'foo',
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
import { expect } from 'chai'
|
||||
import chai from 'chai'
|
||||
import chaiAsPromised from 'chai-as-promised'
|
||||
import initSqlJs from 'sql.js'
|
||||
import db from '@/database.js'
|
||||
const config = {
|
||||
locateFile: filename => 'js/sql-wasm.wasm'
|
||||
}
|
||||
import database from '@/database.js'
|
||||
chai.use(chaiAsPromised)
|
||||
const expect = chai.expect
|
||||
chai.should()
|
||||
|
||||
const db = database.getNewDatabase()
|
||||
const getSQL = initSqlJs()
|
||||
|
||||
describe('database.js', () => {
|
||||
it('creates schema', () => {
|
||||
return initSqlJs(config)
|
||||
.then(SQL => {
|
||||
const database = new SQL.Database()
|
||||
database.run(`
|
||||
CREATE TABLE test (
|
||||
it('creates schema', async () => {
|
||||
const SQL = await getSQL
|
||||
const tempDb = new SQL.Database()
|
||||
tempDb.run(`CREATE TABLE test (
|
||||
col1,
|
||||
col2 integer,
|
||||
col3 decimal(5,2),
|
||||
col4 varchar(30)
|
||||
)
|
||||
`)
|
||||
)`)
|
||||
|
||||
const data = database.export()
|
||||
const data = tempDb.export()
|
||||
const buffer = new Blob([data])
|
||||
return db.loadDb(buffer)
|
||||
})
|
||||
.then(({ dbName, schema }) => {
|
||||
|
||||
const { schema } = await db.loadDb(buffer)
|
||||
expect(schema).to.have.lengthOf(1)
|
||||
expect(schema[0].name).to.equal('test')
|
||||
expect(schema[0].columns[0].name).to.equal('col1')
|
||||
@@ -35,37 +35,32 @@ describe('database.js', () => {
|
||||
expect(schema[0].columns[3].name).to.equal('col4')
|
||||
expect(schema[0].columns[3].type).to.equal('varchar(30)')
|
||||
})
|
||||
})
|
||||
|
||||
it('creates schema with virtual table', () => {
|
||||
return initSqlJs(config)
|
||||
.then(SQL => {
|
||||
const database = new SQL.Database()
|
||||
database.run(`
|
||||
it('creates schema with virtual table', async () => {
|
||||
const SQL = await getSQL
|
||||
const tempDb = new SQL.Database()
|
||||
tempDb.run(`
|
||||
CREATE VIRTUAL TABLE test_virtual USING fts4(
|
||||
col1, col2,
|
||||
notindexed=col1, notindexed=col2,
|
||||
tokenize=unicode61 "tokenchars=.+#")
|
||||
`)
|
||||
|
||||
const data = database.export()
|
||||
const data = tempDb.export()
|
||||
const buffer = new Blob([data])
|
||||
return db.loadDb(buffer)
|
||||
})
|
||||
.then(({ dbName, schema }) => {
|
||||
|
||||
const { schema } = await db.loadDb(buffer)
|
||||
expect(schema[0].name).to.equal('test_virtual')
|
||||
expect(schema[0].columns[0].name).to.equal('col1')
|
||||
expect(schema[0].columns[0].type).to.equal('N/A')
|
||||
expect(schema[0].columns[1].name).to.equal('col2')
|
||||
expect(schema[0].columns[1].type).to.equal('N/A')
|
||||
})
|
||||
})
|
||||
|
||||
it('returns a query result', () => {
|
||||
return initSqlJs(config)
|
||||
.then(SQL => {
|
||||
const database = new SQL.Database()
|
||||
database.run(`
|
||||
it('returns a query result', async () => {
|
||||
const SQL = await getSQL
|
||||
const tempDb = new SQL.Database()
|
||||
tempDb.run(`
|
||||
CREATE TABLE test (
|
||||
id integer,
|
||||
name varchar(100),
|
||||
@@ -77,14 +72,11 @@ describe('database.js', () => {
|
||||
( 2, 'Draco Malfoy', 'Slytherin');
|
||||
`)
|
||||
|
||||
const data = database.export()
|
||||
const data = tempDb.export()
|
||||
const buffer = new Blob([data])
|
||||
return db.loadDb(buffer)
|
||||
})
|
||||
.then(({ dbName, schema }) => {
|
||||
return db.execute('SELECT * from test')
|
||||
})
|
||||
.then(result => {
|
||||
|
||||
await db.loadDb(buffer)
|
||||
const result = await db.execute('SELECT * from test')
|
||||
expect(result.columns).to.have.lengthOf(3)
|
||||
expect(result.columns[0]).to.equal('id')
|
||||
expect(result.columns[1]).to.equal('name')
|
||||
@@ -97,13 +89,11 @@ describe('database.js', () => {
|
||||
expect(result.values[1][1]).to.equal('Draco Malfoy')
|
||||
expect(result.values[1][2]).to.equal('Slytherin')
|
||||
})
|
||||
})
|
||||
|
||||
it('returns an error', () => {
|
||||
return initSqlJs(config)
|
||||
.then(SQL => {
|
||||
const database = new SQL.Database()
|
||||
database.run(`
|
||||
it('returns an error', async () => {
|
||||
const SQL = await getSQL
|
||||
const tempDb = new SQL.Database()
|
||||
tempDb.run(`
|
||||
CREATE TABLE test (
|
||||
id integer,
|
||||
name varchar(100),
|
||||
@@ -115,15 +105,9 @@ describe('database.js', () => {
|
||||
( 2, 'Draco Malfoy', 'Slytherin');
|
||||
`)
|
||||
|
||||
const data = database.export()
|
||||
const data = tempDb.export()
|
||||
const buffer = new Blob([data])
|
||||
return db.loadDb(buffer)
|
||||
})
|
||||
.then(() => {
|
||||
return db.execute('SELECT * from foo')
|
||||
})
|
||||
.catch(result => {
|
||||
expect(result).to.equal('no such table: foo')
|
||||
})
|
||||
await db.loadDb(buffer)
|
||||
await expect(db.execute('SELECT * from foo')).to.be.rejectedWith(/^no such table: foo$/)
|
||||
})
|
||||
})
|
||||
|
||||
17
tests/unit/dbUtils.spec.js
Normal file
17
tests/unit/dbUtils.spec.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import { expect } from 'chai'
|
||||
import dbUtils from '@/dbUtils.js'
|
||||
|
||||
describe('dbUtils.js', () => {
|
||||
it('generator', () => {
|
||||
const arr = ['1', '2', '3', '4', '5']
|
||||
const size = 2
|
||||
const chunks = dbUtils.generateChunks(arr, size)
|
||||
const output = []
|
||||
for (const chunk of chunks) {
|
||||
output.push(chunk)
|
||||
}
|
||||
expect(output[0]).to.eql(['1', '2'])
|
||||
expect(output[1]).to.eql(['3', '4'])
|
||||
expect(output[2]).to.eql(['5'])
|
||||
})
|
||||
})
|
||||
@@ -8,7 +8,6 @@ module.exports = {
|
||||
// This wasm file will be fetched dynamically when we initialize sql.js
|
||||
// It is important that we do not change its name, and that it is in the same folder as the js
|
||||
{ from: 'node_modules/sql.js/dist/sql-wasm.wasm', to: 'js/' },
|
||||
{ from: 'node_modules/sql.js/dist/worker.sql-wasm.js', to: 'js/' },
|
||||
{ from: 'LICENSE', to: './' }
|
||||
])
|
||||
]
|
||||
@@ -22,5 +21,14 @@ module.exports = {
|
||||
.options({
|
||||
limit: 10000
|
||||
})
|
||||
|
||||
config.module
|
||||
.rule('worker')
|
||||
.test(/\.worker\.js$/)
|
||||
.use('worker-loader')
|
||||
.loader('worker-loader')
|
||||
.end()
|
||||
|
||||
config.module.rule('js').exclude.add(/\.worker\.js$/)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user