1
0
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:
lana-k
2021-04-09 16:43:20 +02:00
parent c2864b4308
commit b30eeb6788
23 changed files with 1084 additions and 357 deletions

View File

@@ -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: [
{

View File

@@ -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
View File

@@ -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",

View File

@@ -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"
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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
View 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 })
})
}
}

View File

@@ -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
View 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
View 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
}
}

View File

@@ -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)
})
}
}

View 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,

View File

@@ -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
View 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$/, '')
}
}

View File

@@ -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>

View File

@@ -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)
})
})

View File

@@ -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',

View File

@@ -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)
})
})

View File

@@ -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',

View File

@@ -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$/)
})
})

View 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'])
})
})

View File

@@ -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$/)
}
}