mirror of
https://github.com/lana-k/sqliteviz.git
synced 2025-12-07 02:28:54 +08:00
Compare commits
7 Commits
0.24.1
...
378b9fb580
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
378b9fb580 | ||
|
|
244ba9eb08 | ||
|
|
53e5194295 | ||
|
|
04274ef19a | ||
|
|
3893a66f4e | ||
|
|
1b6b7c71e9 | ||
|
|
3f6427ff0e |
4
.github/workflows/main.yml
vendored
4
.github/workflows/main.yml
vendored
@@ -14,10 +14,10 @@ jobs:
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 10.x
|
||||
node-version: 16.x
|
||||
|
||||
- name: Update npm
|
||||
run: npm install -g npm@7
|
||||
run: npm install -g npm@8
|
||||
|
||||
- name: npm install and build
|
||||
run: |
|
||||
|
||||
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 10.x
|
||||
node-version: 16.x
|
||||
- name: Install browsers
|
||||
run: |
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
sudo apt-get install -y chromium-browser firefox
|
||||
|
||||
- name: Update npm
|
||||
run: npm install -g npm@7
|
||||
run: npm install -g npm@8
|
||||
|
||||
- name: Install the project
|
||||
run: npm install
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# docker build -t sqliteviz/test -f Dockerfile.test .
|
||||
#
|
||||
|
||||
FROM node:12
|
||||
FROM node:12.22-buster
|
||||
|
||||
RUN set -ex; \
|
||||
apt update; \
|
||||
|
||||
@@ -4,11 +4,12 @@
|
||||
|
||||
# sqliteviz
|
||||
|
||||
Sqliteviz is a single-page offline-first PWA for fully client-side visualisation of SQLite databases or CSV files.
|
||||
Sqliteviz is a single-page offline-first PWA for fully client-side visualisation
|
||||
of SQLite databases, CSV, JSON or NDJSON files.
|
||||
|
||||
With sqliteviz you can:
|
||||
- run SQL queries against a SQLite database and create [Plotly][11] charts and pivot tables based on the result sets
|
||||
- import a CSV file into a SQLite database and visualize imported data
|
||||
- import a CSV/JSON/NDJSON file into a SQLite database and visualize imported data
|
||||
- export result set to CSV file
|
||||
- manage inquiries and run them against different databases
|
||||
- import/export inquiries from/to a JSON file
|
||||
|
||||
@@ -45,6 +45,8 @@ SQLite 3rd party extensions included:
|
||||
1. [pivot_vtab][5] -- a pivot virtual table
|
||||
2. `pearson` correlation coefficient function extension from [sqlean][21]
|
||||
(which is part of [squib][20])
|
||||
3. [sqlitelua][22] -- a virtual table `luafunctions` which allows to define custom scalar,
|
||||
aggregate and table-valued functions in Lua
|
||||
|
||||
To ease the step to have working clone locally, the build is committed into
|
||||
the repository.
|
||||
@@ -103,3 +105,4 @@ described in [this message from SQLite Forum][12]:
|
||||
[19]: https://github.com/lana-k/sqliteviz/blob/master/tests/lib/database/sqliteExtensions.spec.js
|
||||
[20]: https://github.com/mrwilson/squib/blob/master/pearson.c
|
||||
[21]: https://github.com/nalgeon/sqlean/blob/incubator/src/pearson.c
|
||||
[22]: https://github.com/kev82/sqlitelua
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
FROM node:14-bullseye
|
||||
FROM node:20.14-bookworm
|
||||
|
||||
RUN set -ex; \
|
||||
echo 'deb http://deb.debian.org/debian unstable main' \
|
||||
> /etc/apt/sources.list.d/unstable.list; \
|
||||
apt-get update; \
|
||||
apt-get install -y -t unstable firefox; \
|
||||
apt-get install -y firefox-esr; \
|
||||
apt-get install -y chromium
|
||||
|
||||
WORKDIR /tmp/build
|
||||
|
||||
@@ -69,6 +69,19 @@
|
||||
],
|
||||
"metadata": {}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"!du -b lib | head -n 2"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"collapsed": false,
|
||||
"outputHidden": false,
|
||||
"inputHidden": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
@@ -176,7 +189,7 @@
|
||||
},
|
||||
"language_info": {
|
||||
"name": "python",
|
||||
"version": "3.10.7",
|
||||
"version": "3.10.14",
|
||||
"mimetype": "text/x-python",
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
|
||||
@@ -24,6 +24,7 @@ cflags = (
|
||||
# Compile-time optimisation
|
||||
'-Os', # reduces the code size about in half comparing to -O2
|
||||
'-flto',
|
||||
'-Isrc', '-Isrc/lua',
|
||||
)
|
||||
emflags = (
|
||||
# Base
|
||||
@@ -61,6 +62,15 @@ def build(src: Path, dst: Path):
|
||||
'-c', src / 'extension-functions.c',
|
||||
'-o', out / 'extension-functions.o',
|
||||
])
|
||||
logging.info('Building LLVM bitcode for SQLite Lua extension')
|
||||
subprocess.check_call([
|
||||
'emcc',
|
||||
*cflags,
|
||||
'-shared',
|
||||
*(src / 'lua').glob('*.c'),
|
||||
*(src / 'sqlitelua').glob('*.c'),
|
||||
'-o', out / 'sqlitelua.o',
|
||||
])
|
||||
|
||||
logging.info('Building WASM from bitcode')
|
||||
subprocess.check_call([
|
||||
@@ -68,6 +78,7 @@ def build(src: Path, dst: Path):
|
||||
*emflags,
|
||||
out / 'sqlite3.o',
|
||||
out / 'extension-functions.o',
|
||||
out / 'sqlitelua.o',
|
||||
'-o', out / 'sql-wasm.js',
|
||||
])
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import logging
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
import zipfile
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
@@ -30,8 +32,14 @@ extension_urls = (
|
||||
# =====================
|
||||
('https://github.com/jakethaw/pivot_vtab/raw/9323ef93/pivot_vtab.c', 'sqlite3_pivotvtab_init'),
|
||||
('https://github.com/nalgeon/sqlean/raw/95e8d21a/src/pearson.c', 'sqlite3_pearson_init'),
|
||||
# Third-party extension with own dependencies
|
||||
# ===========================================
|
||||
('https://github.com/kev82/sqlitelua/raw/db479510/src/main.c', 'sqlite3_luafunctions_init'),
|
||||
)
|
||||
|
||||
lua_url = 'http://www.lua.org/ftp/lua-5.3.5.tar.gz'
|
||||
sqlitelua_url = 'https://github.com/kev82/sqlitelua/archive/db479510.zip'
|
||||
|
||||
sqljs_url = 'https://github.com/sql-js/sql.js/archive/refs/tags/v1.7.0.zip'
|
||||
|
||||
|
||||
@@ -59,6 +67,38 @@ def _get_amalgamation(tgt: Path):
|
||||
shutil.copyfileobj(fr, fw)
|
||||
|
||||
|
||||
def _get_lua(tgt: Path):
|
||||
# Library definitions from lua/Makefile
|
||||
lib_str = '''
|
||||
CORE_O= lapi.o lcode.o lctype.o ldebug.o ldo.o ldump.o lfunc.o lgc.o llex.o \
|
||||
lmem.o lobject.o lopcodes.o lparser.o lstate.o lstring.o ltable.o \
|
||||
ltm.o lundump.o lvm.o lzio.o
|
||||
LIB_O= lauxlib.o lbaselib.o lbitlib.o lcorolib.o ldblib.o liolib.o \
|
||||
lmathlib.o loslib.o lstrlib.o ltablib.o lutf8lib.o loadlib.o linit.o
|
||||
LUA_O= lua.o
|
||||
'''
|
||||
header_only_files = {'lprefix', 'luaconf', 'llimits', 'lualib'}
|
||||
lib_names = set(re.findall(r'(\w+)\.o', lib_str)) | header_only_files
|
||||
|
||||
logging.info('Downloading and extracting Lua %s', lua_url)
|
||||
archive = tarfile.open(fileobj=BytesIO(request.urlopen(lua_url).read()))
|
||||
(tgt / 'lua').mkdir()
|
||||
for tarinfo in archive:
|
||||
tarpath = Path(tarinfo.name)
|
||||
if tarpath.match('src/*') and tarpath.stem in lib_names:
|
||||
with (tgt / 'lua' / tarpath.name).open('wb') as fw:
|
||||
shutil.copyfileobj(archive.extractfile(tarinfo), fw)
|
||||
|
||||
logging.info('Downloading and extracting SQLite Lua extension %s', sqlitelua_url)
|
||||
archive = zipfile.ZipFile(BytesIO(request.urlopen(sqlitelua_url).read()))
|
||||
archive_root_dir = zipfile.Path(archive, archive.namelist()[0])
|
||||
(tgt / 'sqlitelua').mkdir()
|
||||
for zpath in (archive_root_dir / 'src').iterdir():
|
||||
if zpath.name != 'main.c':
|
||||
with zpath.open() as fr, (tgt / 'sqlitelua' / zpath.name).open('wb') as fw:
|
||||
shutil.copyfileobj(fr, fw)
|
||||
|
||||
|
||||
def _get_contrib_functions(tgt: Path):
|
||||
request.urlretrieve(contrib_functions_url, tgt / 'extension-functions.c')
|
||||
|
||||
@@ -70,6 +110,7 @@ def _get_extensions(tgt: Path):
|
||||
for url, init_fn in extension_urls:
|
||||
logging.info('Downloading and appending to amalgamation %s', url)
|
||||
with request.urlopen(url) as resp:
|
||||
f.write(b'\n')
|
||||
shutil.copyfileobj(resp, f)
|
||||
init_functions.append(init_fn)
|
||||
|
||||
@@ -90,6 +131,7 @@ def _get_sqljs(tgt: Path):
|
||||
def configure(tgt: Path):
|
||||
_get_amalgamation(tgt)
|
||||
_get_contrib_functions(tgt)
|
||||
_get_lua(tgt)
|
||||
_get_extensions(tgt)
|
||||
_get_sqljs(tgt)
|
||||
|
||||
|
||||
2
lib/sql-js/dist/sql-wasm.js
vendored
2
lib/sql-js/dist/sql-wasm.js
vendored
File diff suppressed because one or more lines are too long
BIN
lib/sql-js/dist/sql-wasm.wasm
vendored
BIN
lib/sql-js/dist/sql-wasm.wasm
vendored
Binary file not shown.
7057
package-lock.json
generated
7057
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
12
package.json
12
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sqliteviz",
|
||||
"version": "0.24.1",
|
||||
"version": "0.25.0",
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@@ -10,7 +10,7 @@
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"codemirror": "^5.57.0",
|
||||
"codemirror": "^5.65.18",
|
||||
"core-js": "^3.6.5",
|
||||
"dataurl-to-blob": "^0.0.1",
|
||||
"html2canvas": "^1.1.4",
|
||||
@@ -18,11 +18,11 @@
|
||||
"nanoid": "^3.1.12",
|
||||
"papaparse": "^5.4.1",
|
||||
"pivottable": "^2.23.0",
|
||||
"plotly.js": "^1.58.4",
|
||||
"plotly.js": "^2.35.2",
|
||||
"promise-worker": "^2.0.1",
|
||||
"react": "^16.13.1",
|
||||
"react-chart-editor": "^0.45.0",
|
||||
"react-dom": "^16.13.1",
|
||||
"react": "^16.14.0",
|
||||
"react-chart-editor": "^0.46.1",
|
||||
"react-dom": "^16.14.0",
|
||||
"sql.js": "file:./lib/sql-js",
|
||||
"vue": "^2.6.11",
|
||||
"vue-codemirror": "^4.0.6",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"background_color": "white",
|
||||
"description": "Sqliteviz is a single-page application for fully client-side visualisation of SQLite databases or CSV.",
|
||||
"description": "Sqliteviz is a single-page application for fully client-side visualisation of SQLite databases, CSV, JSON or NDJSON.",
|
||||
"display": "fullscreen",
|
||||
"icons": [
|
||||
{
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
:clickToClose="false"
|
||||
>
|
||||
<div class="dialog-header">
|
||||
CSV import
|
||||
<close-icon @click="cancelCsvImport" :disabled="disableDialog"/>
|
||||
{{ typeName }} import
|
||||
<close-icon @click="cancelImport" :disabled="disableDialog"/>
|
||||
</div>
|
||||
<div class="dialog-body">
|
||||
<text-field
|
||||
@@ -18,15 +18,15 @@
|
||||
width="484px"
|
||||
:disabled="disableDialog"
|
||||
:error-msg="tableNameError"
|
||||
id="csv-table-name"
|
||||
id="csv-json-table-name"
|
||||
/>
|
||||
<div class="chars">
|
||||
<div v-if="!isJson && !isNdJson" class="chars">
|
||||
<delimiter-selector
|
||||
v-model="delimiter"
|
||||
width="210px"
|
||||
class="char-input"
|
||||
@input="previewCsv"
|
||||
:disabled="disableDialog"
|
||||
@input="preview"
|
||||
/>
|
||||
<text-field
|
||||
label="Quote char"
|
||||
@@ -36,6 +36,7 @@
|
||||
:disabled="disableDialog"
|
||||
class="char-input"
|
||||
id="quote-char"
|
||||
@input="preview"
|
||||
/>
|
||||
<text-field
|
||||
label="Escape char"
|
||||
@@ -49,52 +50,52 @@
|
||||
:disabled="disableDialog"
|
||||
class="char-input"
|
||||
id="escape-char"
|
||||
@input="preview"
|
||||
/>
|
||||
</div>
|
||||
<check-box
|
||||
@click="header = $event"
|
||||
:init="true"
|
||||
v-if="!isJson && !isNdJson"
|
||||
:init="header"
|
||||
label="Use first row as column headers"
|
||||
:disabled="disableDialog"
|
||||
@click="changeHeaderDisplaying"
|
||||
/>
|
||||
<sql-table
|
||||
v-if="previewData
|
||||
&& (previewData.rowCount > 0 || Object.keys(previewData).length > 0)
|
||||
"
|
||||
v-if="previewData && previewData.rowCount > 0"
|
||||
:data-set="previewData"
|
||||
class="preview-table"
|
||||
:preview="true"
|
||||
class="preview-table"
|
||||
/>
|
||||
<div v-else class="no-data">No data</div>
|
||||
<logs
|
||||
class="import-csv-errors"
|
||||
:messages="importCsvMessages"
|
||||
class="import-errors"
|
||||
:messages="importMessages"
|
||||
/>
|
||||
</div>
|
||||
<div class="dialog-buttons-container">
|
||||
<button
|
||||
class="secondary"
|
||||
:disabled="disableDialog"
|
||||
@click="cancelCsvImport"
|
||||
id="csv-cancel"
|
||||
@click="cancelImport"
|
||||
id="import-cancel"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
v-show="!importCsvCompleted"
|
||||
v-show="!importCompleted"
|
||||
class="primary"
|
||||
:disabled="disableDialog"
|
||||
@click="loadFromCsv(file)"
|
||||
id="csv-import"
|
||||
:disabled="disableDialog || disableImport"
|
||||
@click="loadToDb(file)"
|
||||
id="import-start"
|
||||
>
|
||||
Import
|
||||
</button>
|
||||
<button
|
||||
v-show="importCsvCompleted"
|
||||
v-show="importCompleted"
|
||||
class="primary"
|
||||
:disabled="disableDialog"
|
||||
@click="finish"
|
||||
id="csv-finish"
|
||||
id="import-finish"
|
||||
>
|
||||
Finish
|
||||
</button>
|
||||
@@ -115,7 +116,7 @@ import fIo from '@/lib/utils/fileIo'
|
||||
import events from '@/lib/utils/events'
|
||||
|
||||
export default {
|
||||
name: 'CsvImport',
|
||||
name: 'CsvJsonImport',
|
||||
components: {
|
||||
CloseIcon,
|
||||
TextField,
|
||||
@@ -124,33 +125,50 @@ export default {
|
||||
SqlTable,
|
||||
Logs
|
||||
},
|
||||
props: ['file', 'db', 'dialogName'],
|
||||
props: {
|
||||
file: File,
|
||||
db: Object,
|
||||
dialogName: String
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
disableDialog: false,
|
||||
disableImport: false,
|
||||
tableName: '',
|
||||
delimiter: '',
|
||||
quoteChar: '"',
|
||||
escapeChar: '"',
|
||||
header: true,
|
||||
importCsvCompleted: false,
|
||||
importCsvMessages: [],
|
||||
importCompleted: false,
|
||||
importMessages: [],
|
||||
previewData: null,
|
||||
addedTable: null,
|
||||
tableNameError: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isJson () {
|
||||
return fIo.isJSON(this.file)
|
||||
},
|
||||
isNdJson () {
|
||||
return fIo.isNDJSON(this.file)
|
||||
},
|
||||
typeName () {
|
||||
return this.isJson || this.isNdJson ? 'JSON' : 'CSV'
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
quoteChar () {
|
||||
this.previewCsv()
|
||||
isJson () {
|
||||
if (this.isJson) {
|
||||
this.delimiter = '\u001E'
|
||||
this.header = false
|
||||
}
|
||||
},
|
||||
|
||||
escapeChar () {
|
||||
this.previewCsv()
|
||||
},
|
||||
|
||||
header () {
|
||||
this.previewCsv()
|
||||
isNdJson () {
|
||||
if (this.isNdJson) {
|
||||
this.delimiter = '\u001E'
|
||||
this.header = false
|
||||
}
|
||||
},
|
||||
tableName: time.debounce(function () {
|
||||
this.tableNameError = ''
|
||||
@@ -164,7 +182,11 @@ export default {
|
||||
}, 400)
|
||||
},
|
||||
methods: {
|
||||
cancelCsvImport () {
|
||||
changeHeaderDisplaying (e) {
|
||||
this.header = e
|
||||
this.preview()
|
||||
},
|
||||
cancelImport () {
|
||||
if (!this.disableDialog) {
|
||||
if (this.addedTable) {
|
||||
this.db.execute(`DROP TABLE "${this.addedTable}"`)
|
||||
@@ -175,14 +197,15 @@ export default {
|
||||
}
|
||||
},
|
||||
reset () {
|
||||
this.header = true
|
||||
this.header = !this.isJson && !this.isNdJson
|
||||
this.quoteChar = '"'
|
||||
this.escapeChar = '"'
|
||||
this.delimiter = ''
|
||||
this.delimiter = !this.isJson && !this.isNdJson ? '' : '\u001E'
|
||||
this.tableName = ''
|
||||
this.disableDialog = false
|
||||
this.importCsvCompleted = false
|
||||
this.importCsvMessages = []
|
||||
this.disableImport = false
|
||||
this.importCompleted = false
|
||||
this.importMessages = []
|
||||
this.previewData = null
|
||||
this.addedTable = null
|
||||
this.tableNameError = ''
|
||||
@@ -191,39 +214,69 @@ export default {
|
||||
this.tableName = this.db.sanitizeTableName(fIo.getFileName(this.file))
|
||||
this.$modal.show(this.dialogName)
|
||||
},
|
||||
async previewCsv () {
|
||||
this.importCsvCompleted = false
|
||||
async preview () {
|
||||
this.disableImport = false
|
||||
if (!this.file) {
|
||||
return
|
||||
}
|
||||
this.importCompleted = false
|
||||
const config = {
|
||||
preview: 3,
|
||||
quoteChar: this.quoteChar || '"',
|
||||
escapeChar: this.escapeChar,
|
||||
header: this.header,
|
||||
delimiter: this.delimiter
|
||||
delimiter: this.delimiter,
|
||||
columns: !this.isJson && !this.isNdJson ? null : ['doc']
|
||||
}
|
||||
try {
|
||||
const start = new Date()
|
||||
const parseResult = await csv.parse(this.file, config)
|
||||
const parseResult = this.isJson
|
||||
? await this.getJsonParseResult(this.file)
|
||||
: await csv.parse(this.file, config)
|
||||
const end = new Date()
|
||||
this.previewData = parseResult.data
|
||||
this.previewData.rowCount = parseResult.rowCount
|
||||
this.delimiter = parseResult.delimiter
|
||||
|
||||
// In parseResult.messages we can get parse errors
|
||||
this.importCsvMessages = parseResult.messages || []
|
||||
this.importMessages = parseResult.messages || []
|
||||
|
||||
if (this.previewData.rowCount === 0) {
|
||||
this.disableImport = true
|
||||
this.importMessages.push({
|
||||
type: 'info',
|
||||
message: 'No rows to import.'
|
||||
})
|
||||
}
|
||||
|
||||
if (!parseResult.hasErrors) {
|
||||
this.importCsvMessages.push({
|
||||
this.importMessages.push({
|
||||
message: `Preview parsing is completed in ${time.getPeriod(start, end)}.`,
|
||||
type: 'success'
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
this.importCsvMessages = [{
|
||||
console.error(err)
|
||||
this.importMessages = [{
|
||||
message: err,
|
||||
type: 'error'
|
||||
}]
|
||||
}
|
||||
},
|
||||
async loadFromCsv (file) {
|
||||
async getJsonParseResult (file) {
|
||||
const jsonContent = await fIo.getFileContent(file)
|
||||
const isEmpty = !jsonContent.trim()
|
||||
return {
|
||||
data: {
|
||||
columns: ['doc'],
|
||||
values: { doc: !isEmpty ? [jsonContent] : [] }
|
||||
},
|
||||
hasErrors: false,
|
||||
messages: [],
|
||||
rowCount: +(!isEmpty)
|
||||
}
|
||||
},
|
||||
async loadToDb (file) {
|
||||
if (!this.tableName) {
|
||||
this.tableNameError = "Table name can't be empty"
|
||||
return
|
||||
@@ -234,17 +287,18 @@ export default {
|
||||
quoteChar: this.quoteChar || '"',
|
||||
escapeChar: this.escapeChar,
|
||||
header: this.header,
|
||||
delimiter: this.delimiter
|
||||
delimiter: this.delimiter,
|
||||
columns: !this.isJson && !this.isNdJson ? null : ['doc']
|
||||
}
|
||||
const parseCsvMsg = {
|
||||
message: 'Parsing CSV...',
|
||||
const parsingMsg = {
|
||||
message: `Parsing ${this.typeName}...`,
|
||||
type: 'info'
|
||||
}
|
||||
this.importCsvMessages.push(parseCsvMsg)
|
||||
const parseCsvLoadingIndicator = setTimeout(() => { parseCsvMsg.type = 'loading' }, 1000)
|
||||
this.importMessages.push(parsingMsg)
|
||||
const parsingLoadingIndicator = setTimeout(() => { parsingMsg.type = 'loading' }, 1000)
|
||||
|
||||
const importMsg = {
|
||||
message: 'Importing CSV into a SQLite database...',
|
||||
message: `Importing ${this.typeName} into a SQLite database...`,
|
||||
type: 'info'
|
||||
}
|
||||
let importLoadingIndicator = null
|
||||
@@ -256,27 +310,30 @@ export default {
|
||||
|
||||
try {
|
||||
let start = new Date()
|
||||
const parseResult = await csv.parse(this.file, config)
|
||||
const parseResult = this.isJson
|
||||
? await this.getJsonParseResult(file)
|
||||
: await csv.parse(this.file, config)
|
||||
|
||||
let end = new Date()
|
||||
|
||||
if (!parseResult.hasErrors) {
|
||||
const rowCount = parseResult.rowCount
|
||||
let period = time.getPeriod(start, end)
|
||||
parseCsvMsg.type = 'success'
|
||||
parsingMsg.type = 'success'
|
||||
|
||||
if (parseResult.messages.length > 0) {
|
||||
this.importCsvMessages = this.importCsvMessages.concat(parseResult.messages)
|
||||
parseCsvMsg.message = `${rowCount} rows are parsed in ${period}.`
|
||||
this.importMessages = this.importMessages.concat(parseResult.messages)
|
||||
parsingMsg.message = `${rowCount} rows are parsed in ${period}.`
|
||||
} else {
|
||||
// Inform about csv parsing success
|
||||
parseCsvMsg.message = `${rowCount} rows are parsed successfully in ${period}.`
|
||||
// Inform about parsing success
|
||||
parsingMsg.message = `${rowCount} rows are parsed successfully in ${period}.`
|
||||
}
|
||||
|
||||
// Loading indicator for csv parsing is not needed anymore
|
||||
clearTimeout(parseCsvLoadingIndicator)
|
||||
// Loading indicator for parsing is not needed anymore
|
||||
clearTimeout(parsingLoadingIndicator)
|
||||
|
||||
// Add info about import start
|
||||
this.importCsvMessages.push(importMsg)
|
||||
this.importMessages.push(importMsg)
|
||||
|
||||
// Show import progress after 1 second
|
||||
importLoadingIndicator = setTimeout(() => {
|
||||
@@ -291,52 +348,108 @@ export default {
|
||||
this.addedTable = this.tableName
|
||||
// Inform about import success
|
||||
period = time.getPeriod(start, end)
|
||||
importMsg.message = `Importing CSV into a SQLite database is completed in ${period}.`
|
||||
importMsg.message = `Importing ${this.typeName} ` +
|
||||
`into a SQLite database is completed in ${period}.`
|
||||
importMsg.type = 'success'
|
||||
|
||||
// Loading indicator for import is not needed anymore
|
||||
clearTimeout(importLoadingIndicator)
|
||||
|
||||
this.importCsvCompleted = true
|
||||
this.importCompleted = true
|
||||
} else {
|
||||
parseCsvMsg.message = 'Parsing ended with errors.'
|
||||
parseCsvMsg.type = 'info'
|
||||
this.importCsvMessages = this.importCsvMessages.concat(parseResult.messages)
|
||||
parsingMsg.message = 'Parsing ended with errors.'
|
||||
parsingMsg.type = 'info'
|
||||
this.importMessages = this.importMessages.concat(parseResult.messages)
|
||||
}
|
||||
} catch (err) {
|
||||
if (parseCsvMsg.type === 'loading') {
|
||||
parseCsvMsg.type = 'info'
|
||||
console.error(err)
|
||||
if (parsingMsg.type === 'loading') {
|
||||
parsingMsg.type = 'info'
|
||||
}
|
||||
|
||||
if (importMsg.type === 'loading') {
|
||||
importMsg.type = 'info'
|
||||
}
|
||||
|
||||
this.importCsvMessages.push({
|
||||
this.importMessages.push({
|
||||
message: err,
|
||||
type: 'error'
|
||||
})
|
||||
}
|
||||
|
||||
clearTimeout(parseCsvLoadingIndicator)
|
||||
clearTimeout(parsingLoadingIndicator)
|
||||
clearTimeout(importLoadingIndicator)
|
||||
this.db.deleteProgressCounter(progressCounterId)
|
||||
this.disableDialog = false
|
||||
},
|
||||
async finish () {
|
||||
this.$modal.hide(this.dialogName)
|
||||
const stmt = [
|
||||
const stmt = this.getQueryExample()
|
||||
const tabId = await this.$store.dispatch('addTab', { query: stmt })
|
||||
this.$store.commit('setCurrentTabId', tabId)
|
||||
this.importCompleted = false
|
||||
this.$emit('finish')
|
||||
events.send('inquiry.create', null, { auto: true })
|
||||
},
|
||||
getQueryExample () {
|
||||
return this.isNdJson ? this.getNdJsonQueryExample()
|
||||
: this.isJson ? this.getJsonQueryExample()
|
||||
: [
|
||||
'/*',
|
||||
` * Your CSV file has been imported into ${this.addedTable} table.`,
|
||||
' * You can run this SQL query to make all CSV records available for charting.',
|
||||
' */',
|
||||
`SELECT * FROM "${this.addedTable}"`
|
||||
].join('\n')
|
||||
const tabId = await this.$store.dispatch('addTab', { query: stmt })
|
||||
this.$store.commit('setCurrentTabId', tabId)
|
||||
this.importCsvCompleted = false
|
||||
this.$emit('finish')
|
||||
events.send('inquiry.create', null, { auto: true })
|
||||
},
|
||||
getNdJsonQueryExample () {
|
||||
try {
|
||||
const firstRowJson = JSON.parse(this.previewData.values.doc[0])
|
||||
const firstKey = Object.keys(firstRowJson)[0]
|
||||
return [
|
||||
'/*',
|
||||
` * Your NDJSON file has been imported into ${this.addedTable} table.`,
|
||||
` * Run this SQL query to get values of property ${firstKey} ` +
|
||||
'and make them available for charting.',
|
||||
' */',
|
||||
`SELECT doc->>'${firstKey}'`,
|
||||
`FROM "${this.addedTable}"`
|
||||
].join('\n')
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return [
|
||||
'/*',
|
||||
` * Your NDJSON file has been imported into ${this.addedTable} table.`,
|
||||
' */',
|
||||
'SELECT *',
|
||||
`FROM "${this.addedTable}"`
|
||||
].join('\n')
|
||||
}
|
||||
},
|
||||
getJsonQueryExample () {
|
||||
try {
|
||||
const firstRowJson = JSON.parse(this.previewData.values.doc[0])
|
||||
const firstKey = Object.keys(firstRowJson)[0]
|
||||
return [
|
||||
'/*',
|
||||
` * Your JSON file has been imported into ${this.addedTable} table.`,
|
||||
` * Run this SQL query to get values of property ${firstKey} ` +
|
||||
'and make them available for charting.',
|
||||
' */',
|
||||
'SELECT *',
|
||||
`FROM "${this.addedTable}"`,
|
||||
`JOIN json_each(doc, '$.${firstKey}')`
|
||||
].join('\n')
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return [
|
||||
'/*',
|
||||
` * Your NDJSON file has been imported into ${this.addedTable} table.`,
|
||||
' */',
|
||||
'SELECT *',
|
||||
`FROM "${this.addedTable}"`
|
||||
].join('\n')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -347,10 +460,14 @@ export default {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
#csv-json-table-name {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.chars {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
margin: 24px 0 20px;
|
||||
margin: 0 0 20px;
|
||||
}
|
||||
.char-input {
|
||||
margin-right: 44px;
|
||||
@@ -359,7 +476,7 @@ export default {
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.import-csv-errors {
|
||||
.import-errors {
|
||||
height: 136px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
@@ -10,7 +10,8 @@
|
||||
@click="browse"
|
||||
>
|
||||
<div class="text">
|
||||
Drop the database or CSV file here or click to choose a file from your computer.
|
||||
Drop the database, CSV, JSON or NDJSON file here
|
||||
or click to choose a file from your computer.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -41,13 +42,13 @@
|
||||
</div>
|
||||
<div id="error" class="error"></div>
|
||||
|
||||
<!--Parse csv dialog -->
|
||||
<csv-import
|
||||
ref="addCsv"
|
||||
<!--Parse csv or json dialog -->
|
||||
<csv-json-import
|
||||
ref="addCsvJson"
|
||||
:file="file"
|
||||
:db="newDb"
|
||||
dialog-name="importFromCsv"
|
||||
@cancel="cancelCsvImport"
|
||||
dialog-name="importFromCsvJson"
|
||||
@cancel="cancelImport"
|
||||
@finish="finish"
|
||||
/>
|
||||
</div>
|
||||
@@ -57,7 +58,7 @@
|
||||
import fIo from '@/lib/utils/fileIo'
|
||||
import ChangeDbIcon from '@/components/svg/changeDb'
|
||||
import database from '@/lib/database'
|
||||
import CsvImport from '@/components/CsvImport'
|
||||
import CsvJsonImport from '@/components/CsvJsonImport'
|
||||
import events from '@/lib/utils/events'
|
||||
|
||||
export default {
|
||||
@@ -79,7 +80,7 @@ export default {
|
||||
},
|
||||
components: {
|
||||
ChangeDbIcon,
|
||||
CsvImport
|
||||
CsvJsonImport
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
@@ -102,7 +103,7 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
cancelCsvImport () {
|
||||
cancelImport () {
|
||||
if (this.newDb) {
|
||||
this.newDb.shutDown()
|
||||
this.newDb = null
|
||||
@@ -128,21 +129,22 @@ export default {
|
||||
if (fIo.isDatabase(file)) {
|
||||
this.loadDb(file)
|
||||
} else {
|
||||
const isJson = fIo.isJSON(file) || fIo.isNDJSON(file)
|
||||
events.send('database.import', file.size, {
|
||||
from: 'csv',
|
||||
from: isJson ? 'json' : 'csv',
|
||||
new_db: true
|
||||
})
|
||||
|
||||
this.file = file
|
||||
await this.$nextTick()
|
||||
const csvImport = this.$refs.addCsv
|
||||
csvImport.reset()
|
||||
return Promise.all([csvImport.previewCsv(), this.animationPromise])
|
||||
.then(csvImport.open)
|
||||
const csvJsonImportModal = this.$refs.addCsvJson
|
||||
csvJsonImportModal.reset()
|
||||
return Promise.all([csvJsonImportModal.preview(), this.animationPromise])
|
||||
.then(csvJsonImportModal.open)
|
||||
}
|
||||
},
|
||||
browse () {
|
||||
fIo.getFileFromUser('.db,.sqlite,.sqlite3,.csv')
|
||||
fIo.getFileFromUser('.db,.sqlite,.sqlite3,.csv,.json,.ndjson')
|
||||
.then(this.checkFile)
|
||||
},
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ export default {
|
||||
components: { Pager },
|
||||
props: {
|
||||
dataSet: Object,
|
||||
time: String,
|
||||
time: [String, Number],
|
||||
pageSize: {
|
||||
type: Number,
|
||||
default: 20
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
</defs>
|
||||
</svg>
|
||||
<span class="icon-tooltip" :style="tooltipStyle" ref="tooltip">
|
||||
Add new table from CSV
|
||||
Add new table from CSV, JSON or NDJSON
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
/>
|
||||
</svg>
|
||||
<span class="icon-tooltip" :style="tooltipStyle" ref="tooltip">
|
||||
Load another database or CSV
|
||||
Load another database, CSV, JSON or NDJSON
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -7,9 +7,9 @@ const hintsByCode = {
|
||||
}
|
||||
|
||||
export default {
|
||||
getResult (source) {
|
||||
getResult (source, columns) {
|
||||
const result = {
|
||||
columns: []
|
||||
columns: columns || []
|
||||
}
|
||||
const values = {}
|
||||
if (source.meta.fields) {
|
||||
@@ -24,8 +24,18 @@ export default {
|
||||
return value
|
||||
})
|
||||
})
|
||||
} else if (columns) {
|
||||
columns.forEach((col, i) => {
|
||||
values[col] = source.data.map(row => {
|
||||
let value = row[i]
|
||||
if (value instanceof Date) {
|
||||
value = value.toISOString()
|
||||
}
|
||||
return value
|
||||
})
|
||||
})
|
||||
} else {
|
||||
for (let i = 0; i <= source.data[0].length - 1; i++) {
|
||||
for (let i = 0; source.data[0] && i <= source.data[0].length - 1; i++) {
|
||||
const colName = `col${i + 1}`
|
||||
result.columns.push(colName)
|
||||
values[colName] = source.data.map(row => {
|
||||
@@ -76,7 +86,7 @@ export default {
|
||||
let res
|
||||
try {
|
||||
res = {
|
||||
data: this.getResult(results),
|
||||
data: this.getResult(results, config.columns),
|
||||
delimiter: results.meta.delimiter,
|
||||
hasErrors: false,
|
||||
rowCount: results.data.length
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
export default {
|
||||
isJSON (file) {
|
||||
return file && file.type === 'application/json'
|
||||
},
|
||||
isNDJSON (file) {
|
||||
return file && file.name.endsWith('.ndjson')
|
||||
},
|
||||
isDatabase (file) {
|
||||
const dbTypes = ['application/vnd.sqlite3', 'application/x-sqlite3']
|
||||
return file.type
|
||||
@@ -51,16 +57,17 @@ export default {
|
||||
},
|
||||
|
||||
importFile () {
|
||||
const reader = new FileReader()
|
||||
|
||||
return this.getFileFromUser('.json')
|
||||
.then(file => {
|
||||
return new Promise((resolve, reject) => {
|
||||
reader.onload = e => {
|
||||
resolve(e.target.result)
|
||||
}
|
||||
reader.readAsText(file)
|
||||
return this.getFileContent(file)
|
||||
})
|
||||
},
|
||||
|
||||
getFileContent (file) {
|
||||
const reader = new FileReader()
|
||||
return new Promise(resolve => {
|
||||
reader.onload = e => resolve(e.target.result)
|
||||
reader.readAsText(file)
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</div>
|
||||
<db-uploader id="db-edit" type="small" />
|
||||
<export-icon tooltip="Export database" @click="exportToFile"/>
|
||||
<add-table-icon @click="addCsv"/>
|
||||
<add-table-icon @click="addCsvJson"/>
|
||||
</div>
|
||||
<div v-show="schemaVisible" class="schema">
|
||||
<table-description
|
||||
@@ -21,12 +21,12 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!--Parse csv dialog -->
|
||||
<csv-import
|
||||
ref="addCsv"
|
||||
<!--Parse csv or json dialog -->
|
||||
<csv-json-import
|
||||
ref="addCsvJson"
|
||||
:file="file"
|
||||
:db="$store.state.db"
|
||||
dialog-name="addCsv"
|
||||
dialog-name="addCsvJson"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -40,7 +40,7 @@ import TreeChevron from '@/components/svg/treeChevron'
|
||||
import DbUploader from '@/components/DbUploader'
|
||||
import ExportIcon from '@/components/svg/export'
|
||||
import AddTableIcon from '@/components/svg/addTable'
|
||||
import CsvImport from '@/components/CsvImport'
|
||||
import CsvJsonImport from '@/components/CsvJsonImport'
|
||||
|
||||
export default {
|
||||
name: 'Schema',
|
||||
@@ -51,7 +51,7 @@ export default {
|
||||
DbUploader,
|
||||
ExportIcon,
|
||||
AddTableIcon,
|
||||
CsvImport
|
||||
CsvJsonImport
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
@@ -80,16 +80,17 @@ export default {
|
||||
exportToFile () {
|
||||
this.$store.state.db.export(`${this.dbName}.sqlite`)
|
||||
},
|
||||
async addCsv () {
|
||||
this.file = await fIo.getFileFromUser('.csv')
|
||||
async addCsvJson () {
|
||||
this.file = await fIo.getFileFromUser('.csv,.json,.ndjson')
|
||||
await this.$nextTick()
|
||||
const csvImport = this.$refs.addCsv
|
||||
csvImport.reset()
|
||||
await csvImport.previewCsv()
|
||||
csvImport.open()
|
||||
const csvJsonImportModal = this.$refs.addCsvJson
|
||||
csvJsonImportModal.reset()
|
||||
await csvJsonImportModal.preview()
|
||||
csvJsonImportModal.open()
|
||||
|
||||
const isJson = fIo.isJSON(this.file) || fIo.isNDJSON(this.file)
|
||||
events.send('database.import', this.file.size, {
|
||||
from: 'csv',
|
||||
from: isJson ? 'json' : 'csv',
|
||||
new_db: false
|
||||
})
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
<script>
|
||||
import plotly from 'plotly.js'
|
||||
import 'react-chart-editor/lib/react-chart-editor.min.css'
|
||||
import 'react-chart-editor/lib/react-chart-editor.css'
|
||||
|
||||
import PlotlyEditor from 'react-chart-editor'
|
||||
import chartHelper from '@/lib/chartHelper'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<main-menu />
|
||||
<keep-alive include="Workspace">
|
||||
<keep-alive include="Workspace,Inquiries">
|
||||
<router-view id="main-view" />
|
||||
</keep-alive>
|
||||
</div>
|
||||
|
||||
@@ -2,10 +2,10 @@ import { expect } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import Vuex from 'vuex'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import CsvImport from '@/components/CsvImport'
|
||||
import CsvJsonImport from '@/components/CsvJsonImport'
|
||||
import csv from '@/lib/csv'
|
||||
|
||||
describe('CsvImport.vue', () => {
|
||||
describe('CsvJsonImport.vue', () => {
|
||||
let state = {}
|
||||
let actions = {}
|
||||
let mutations = {}
|
||||
@@ -13,7 +13,7 @@ describe('CsvImport.vue', () => {
|
||||
let clock
|
||||
let wrapper
|
||||
const newTabId = 1
|
||||
const file = { name: 'my data.csv' }
|
||||
const file = new File([], 'my data.csv')
|
||||
|
||||
beforeEach(() => {
|
||||
clock = sinon.useFakeTimers()
|
||||
@@ -40,11 +40,11 @@ describe('CsvImport.vue', () => {
|
||||
}
|
||||
|
||||
// mount the component
|
||||
wrapper = mount(CsvImport, {
|
||||
wrapper = mount(CsvJsonImport, {
|
||||
store,
|
||||
propsData: {
|
||||
file,
|
||||
dialogName: 'addCsv',
|
||||
dialogName: 'addCsvJson',
|
||||
db
|
||||
}
|
||||
})
|
||||
@@ -74,11 +74,12 @@ describe('CsvImport.vue', () => {
|
||||
}]
|
||||
})
|
||||
|
||||
wrapper.vm.previewCsv()
|
||||
wrapper.vm.preview()
|
||||
await wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.find('[data-modal="addCsv"]').exists()).to.equal(true)
|
||||
expect(wrapper.find('#csv-table-name input').element.value).to.equal('my_data')
|
||||
expect(wrapper.find('[data-modal="addCsvJson"]').exists()).to.equal(true)
|
||||
expect(wrapper.find('.dialog-header').text()).to.equal('CSV import')
|
||||
expect(wrapper.find('#csv-json-table-name input').element.value).to.equal('my_data')
|
||||
expect(wrapper.findComponent({ name: 'delimiter-selector' }).vm.value).to.equal('|')
|
||||
expect(wrapper.find('#quote-char input').element.value).to.equal('"')
|
||||
expect(wrapper.find('#escape-char input').element.value).to.equal('"')
|
||||
@@ -93,8 +94,36 @@ describe('CsvImport.vue', () => {
|
||||
.to.include('Information about row 0. Comma was used as a standart delimiter.')
|
||||
expect(wrapper.findComponent({ name: 'logs' }).text())
|
||||
.to.include('Preview parsing is completed in')
|
||||
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
|
||||
expect(wrapper.find('#import-finish').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('#import-start').isVisible()).to.equal(true)
|
||||
expect(wrapper.find('#import-start').attributes().disabled).to.equal(undefined)
|
||||
})
|
||||
|
||||
it('disables import if no rows found', async () => {
|
||||
sinon.stub(csv, 'parse').resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['col2', 'col1'],
|
||||
values: {
|
||||
col1: [],
|
||||
col2: []
|
||||
}
|
||||
},
|
||||
rowCount: 0,
|
||||
messages: []
|
||||
})
|
||||
|
||||
await wrapper.vm.preview()
|
||||
await wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
const rows = wrapper.findAll('tbody tr')
|
||||
expect(rows).to.have.lengthOf(0)
|
||||
expect(wrapper.findComponent({ name: 'logs' }).text())
|
||||
.to.include('No rows to import.')
|
||||
expect(wrapper.find('.no-data').isVisible()).to.equal(true)
|
||||
expect(wrapper.find('#import-finish').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('#import-start').isVisible()).to.equal(true)
|
||||
expect(wrapper.find('#import-start').attributes().disabled).to.equal('disabled')
|
||||
})
|
||||
|
||||
it('reparses when parameters changes', async () => {
|
||||
@@ -111,7 +140,7 @@ describe('CsvImport.vue', () => {
|
||||
rowCount: 1
|
||||
})
|
||||
|
||||
wrapper.vm.previewCsv()
|
||||
wrapper.vm.preview()
|
||||
wrapper.vm.open()
|
||||
await csv.parse.returnValues[0]
|
||||
await wrapper.vm.$nextTick()
|
||||
@@ -231,20 +260,32 @@ describe('CsvImport.vue', () => {
|
||||
col2: ['foo']
|
||||
}
|
||||
},
|
||||
rowCount: 1
|
||||
rowCount: 1,
|
||||
messages: []
|
||||
})
|
||||
|
||||
wrapper.vm.previewCsv()
|
||||
wrapper.vm.preview()
|
||||
wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
let resolveParsing
|
||||
parse.onCall(1).returns(new Promise(resolve => {
|
||||
resolveParsing = resolve
|
||||
resolveParsing = () => resolve({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['col1', 'col2'],
|
||||
values: {
|
||||
col1: [1],
|
||||
col2: ['foo']
|
||||
}
|
||||
},
|
||||
rowCount: 1,
|
||||
messages: []
|
||||
})
|
||||
}))
|
||||
|
||||
await wrapper.find('#csv-table-name input').setValue('foo')
|
||||
await wrapper.find('#csv-import').trigger('click')
|
||||
await wrapper.find('#csv-json-table-name input').setValue('foo')
|
||||
await wrapper.find('#import-start').trigger('click')
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
// "Parsing CSV..." in the logs
|
||||
@@ -262,11 +303,11 @@ describe('CsvImport.vue', () => {
|
||||
expect(wrapper.find('#quote-char input').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#escape-char input').element.disabled).to.equal(true)
|
||||
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(true)
|
||||
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#csv-finish').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#import-cancel').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#import-finish').element.disabled).to.equal(true)
|
||||
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(true)
|
||||
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
|
||||
expect(wrapper.find('#import-finish').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('#import-start').isVisible()).to.equal(true)
|
||||
await resolveParsing()
|
||||
await parse.returnValues[1]
|
||||
|
||||
@@ -306,7 +347,7 @@ describe('CsvImport.vue', () => {
|
||||
messages: []
|
||||
})
|
||||
|
||||
wrapper.vm.previewCsv()
|
||||
wrapper.vm.preview()
|
||||
wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
@@ -315,8 +356,8 @@ describe('CsvImport.vue', () => {
|
||||
resolveImport = resolve
|
||||
}))
|
||||
|
||||
await wrapper.find('#csv-table-name input').setValue('foo')
|
||||
await wrapper.find('#csv-import').trigger('click')
|
||||
await wrapper.find('#csv-json-table-name input').setValue('foo')
|
||||
await wrapper.find('#import-start').trigger('click')
|
||||
await csv.parse.returnValues[1]
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
@@ -329,11 +370,11 @@ describe('CsvImport.vue', () => {
|
||||
expect(wrapper.find('#quote-char input').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#escape-char input').element.disabled).to.equal(true)
|
||||
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(true)
|
||||
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#csv-finish').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#import-cancel').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#import-finish').element.disabled).to.equal(true)
|
||||
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(true)
|
||||
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
|
||||
expect(wrapper.find('#import-finish').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('#import-start').isVisible()).to.equal(true)
|
||||
await resolveImport()
|
||||
})
|
||||
|
||||
@@ -377,12 +418,12 @@ describe('CsvImport.vue', () => {
|
||||
resolveImport = resolve
|
||||
}))
|
||||
|
||||
wrapper.vm.previewCsv()
|
||||
wrapper.vm.preview()
|
||||
wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-table-name input').setValue('foo')
|
||||
await wrapper.find('#csv-import').trigger('click')
|
||||
await wrapper.find('#csv-json-table-name input').setValue('foo')
|
||||
await wrapper.find('#import-start').trigger('click')
|
||||
await csv.parse.returnValues[1]
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
@@ -397,11 +438,11 @@ describe('CsvImport.vue', () => {
|
||||
expect(wrapper.find('#quote-char input').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#escape-char input').element.disabled).to.equal(true)
|
||||
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(true)
|
||||
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#csv-finish').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#import-cancel').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#import-finish').element.disabled).to.equal(true)
|
||||
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(true)
|
||||
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
|
||||
expect(wrapper.find('#import-finish').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('#import-start').isVisible()).to.equal(true)
|
||||
await resolveImport()
|
||||
})
|
||||
|
||||
@@ -440,12 +481,12 @@ describe('CsvImport.vue', () => {
|
||||
}]
|
||||
})
|
||||
|
||||
wrapper.vm.previewCsv()
|
||||
wrapper.vm.preview()
|
||||
wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-table-name input').setValue('foo')
|
||||
await wrapper.find('#csv-import').trigger('click')
|
||||
await wrapper.find('#csv-json-table-name input').setValue('foo')
|
||||
await wrapper.find('#import-start').trigger('click')
|
||||
await csv.parse.returnValues[1]
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
@@ -460,11 +501,11 @@ describe('CsvImport.vue', () => {
|
||||
expect(wrapper.find('#quote-char input').element.disabled).to.equal(false)
|
||||
expect(wrapper.find('#escape-char input').element.disabled).to.equal(false)
|
||||
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(false)
|
||||
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(false)
|
||||
expect(wrapper.find('#csv-finish').element.disabled).to.equal(false)
|
||||
expect(wrapper.find('#import-cancel').element.disabled).to.equal(false)
|
||||
expect(wrapper.find('#import-finish').element.disabled).to.equal(false)
|
||||
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(false)
|
||||
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
|
||||
expect(wrapper.find('#import-finish').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('#import-start').isVisible()).to.equal(true)
|
||||
})
|
||||
|
||||
it('has proper state before import is completed', async () => {
|
||||
@@ -501,12 +542,12 @@ describe('CsvImport.vue', () => {
|
||||
wrapper.vm.db.addTableFromCsv = sinon.stub()
|
||||
.resolves(new Promise(resolve => { resolveImport = resolve }))
|
||||
|
||||
wrapper.vm.previewCsv()
|
||||
wrapper.vm.preview()
|
||||
wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-table-name input').setValue('foo')
|
||||
await wrapper.find('#csv-import').trigger('click')
|
||||
await wrapper.find('#csv-json-table-name input').setValue('foo')
|
||||
await wrapper.find('#import-start').trigger('click')
|
||||
await csv.parse.returnValues[1]
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
@@ -525,11 +566,11 @@ describe('CsvImport.vue', () => {
|
||||
expect(wrapper.find('#quote-char input').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#escape-char input').element.disabled).to.equal(true)
|
||||
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(true)
|
||||
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#csv-finish').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#import-cancel').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#import-finish').element.disabled).to.equal(true)
|
||||
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(true)
|
||||
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
|
||||
expect(wrapper.find('#import-finish').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('#import-start').isVisible()).to.equal(true)
|
||||
expect(wrapper.vm.db.addTableFromCsv.getCall(0).args[0]).to.equal('foo') // table name
|
||||
|
||||
// After resolving - loading indicator is not shown
|
||||
@@ -570,12 +611,12 @@ describe('CsvImport.vue', () => {
|
||||
messages: []
|
||||
})
|
||||
|
||||
wrapper.vm.previewCsv()
|
||||
wrapper.vm.preview()
|
||||
wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-table-name input').setValue('foo')
|
||||
await wrapper.find('#csv-import').trigger('click')
|
||||
await wrapper.find('#csv-json-table-name input').setValue('foo')
|
||||
await wrapper.find('#import-start').trigger('click')
|
||||
await csv.parse.returnValues[1]
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
@@ -589,10 +630,10 @@ describe('CsvImport.vue', () => {
|
||||
expect(wrapper.find('#quote-char input').element.disabled).to.equal(false)
|
||||
expect(wrapper.find('#escape-char input').element.disabled).to.equal(false)
|
||||
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(false)
|
||||
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(false)
|
||||
expect(wrapper.find('#csv-finish').element.disabled).to.equal(false)
|
||||
expect(wrapper.find('#import-cancel').element.disabled).to.equal(false)
|
||||
expect(wrapper.find('#import-finish').element.disabled).to.equal(false)
|
||||
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(false)
|
||||
expect(wrapper.find('#csv-finish').isVisible()).to.equal(true)
|
||||
expect(wrapper.find('#import-finish').isVisible()).to.equal(true)
|
||||
})
|
||||
|
||||
it('import fails', async () => {
|
||||
@@ -627,12 +668,12 @@ describe('CsvImport.vue', () => {
|
||||
|
||||
wrapper.vm.db.addTableFromCsv = sinon.stub().rejects(new Error('fail'))
|
||||
|
||||
wrapper.vm.previewCsv()
|
||||
wrapper.vm.preview()
|
||||
wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-table-name input').setValue('foo')
|
||||
await wrapper.find('#csv-import').trigger('click')
|
||||
await wrapper.find('#csv-json-table-name input').setValue('foo')
|
||||
await wrapper.find('#import-start').trigger('click')
|
||||
await csv.parse.returnValues[1]
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
@@ -647,10 +688,10 @@ describe('CsvImport.vue', () => {
|
||||
expect(wrapper.find('#quote-char input').element.disabled).to.equal(false)
|
||||
expect(wrapper.find('#escape-char input').element.disabled).to.equal(false)
|
||||
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(false)
|
||||
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(false)
|
||||
expect(wrapper.find('#csv-finish').element.disabled).to.equal(false)
|
||||
expect(wrapper.find('#import-cancel').element.disabled).to.equal(false)
|
||||
expect(wrapper.find('#import-finish').element.disabled).to.equal(false)
|
||||
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(false)
|
||||
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('#import-finish').isVisible()).to.equal(false)
|
||||
})
|
||||
|
||||
it('import finish', async () => {
|
||||
@@ -668,19 +709,19 @@ describe('CsvImport.vue', () => {
|
||||
messages: []
|
||||
})
|
||||
|
||||
wrapper.vm.previewCsv()
|
||||
wrapper.vm.preview()
|
||||
wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-import').trigger('click')
|
||||
await wrapper.find('#import-start').trigger('click')
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-finish').trigger('click')
|
||||
await wrapper.find('#import-finish').trigger('click')
|
||||
|
||||
expect(actions.addTab.calledOnce).to.equal(true)
|
||||
await actions.addTab.returnValues[0]
|
||||
expect(mutations.setCurrentTabId.calledOnceWith(state, newTabId)).to.equal(true)
|
||||
expect(wrapper.find('[data-modal="addCsv"]').exists()).to.equal(false)
|
||||
expect(wrapper.find('[data-modal="addCsvJson"]').exists()).to.equal(false)
|
||||
expect(wrapper.emitted('finish')).to.have.lengthOf(1)
|
||||
})
|
||||
|
||||
@@ -699,47 +740,525 @@ describe('CsvImport.vue', () => {
|
||||
messages: []
|
||||
})
|
||||
|
||||
await wrapper.vm.previewCsv()
|
||||
await wrapper.vm.preview()
|
||||
await wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-import').trigger('click')
|
||||
await wrapper.find('#import-start').trigger('click')
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-cancel').trigger('click')
|
||||
await wrapper.find('#import-cancel').trigger('click')
|
||||
|
||||
expect(wrapper.find('[data-modal="addCsv"]').exists()).to.equal(false)
|
||||
expect(wrapper.find('[data-modal="addCsvJson"]').exists()).to.equal(false)
|
||||
expect(wrapper.vm.db.execute.calledOnceWith('DROP TABLE "my_data"')).to.equal(true)
|
||||
expect(wrapper.vm.db.refreshSchema.calledOnce).to.equal(true)
|
||||
expect(wrapper.emitted('cancel')).to.have.lengthOf(1)
|
||||
})
|
||||
|
||||
it('checks table name', async () => {
|
||||
sinon.stub(csv, 'parse').resolves()
|
||||
await wrapper.vm.previewCsv()
|
||||
sinon.stub(csv, 'parse').resolves({
|
||||
data: {},
|
||||
hasErrors: false,
|
||||
messages: []
|
||||
})
|
||||
await wrapper.vm.preview()
|
||||
await wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-table-name input').setValue('foo')
|
||||
await wrapper.find('#csv-json-table-name input').setValue('foo')
|
||||
await clock.tick(400)
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.find('#csv-table-name .text-field-error').text()).to.equal('')
|
||||
expect(wrapper.find('#csv-json-table-name .text-field-error').text()).to.equal('')
|
||||
|
||||
wrapper.vm.db.validateTableName = sinon.stub().rejects(new Error('this is a bad table name'))
|
||||
await wrapper.find('#csv-table-name input').setValue('bar')
|
||||
await wrapper.find('#csv-json-table-name input').setValue('bar')
|
||||
await clock.tick(400)
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.find('#csv-table-name .text-field-error').text())
|
||||
expect(wrapper.find('#csv-json-table-name .text-field-error').text())
|
||||
.to.equal('this is a bad table name. Try another table name.')
|
||||
|
||||
await wrapper.find('#csv-table-name input').setValue('')
|
||||
await wrapper.find('#csv-json-table-name input').setValue('')
|
||||
await clock.tick(400)
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.find('#csv-table-name .text-field-error').text()).to.equal('')
|
||||
expect(wrapper.find('#csv-json-table-name .text-field-error').text()).to.equal('')
|
||||
|
||||
await wrapper.find('#csv-import').trigger('click')
|
||||
expect(wrapper.find('#csv-table-name .text-field-error').text())
|
||||
await wrapper.find('#import-start').trigger('click')
|
||||
expect(wrapper.find('#csv-json-table-name .text-field-error').text())
|
||||
.to.equal("Table name can't be empty")
|
||||
expect(wrapper.vm.db.addTableFromCsv.called).to.equal(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('CsvJsonImport.vue - json', () => {
|
||||
let state = {}
|
||||
let actions = {}
|
||||
let mutations = {}
|
||||
let store = {}
|
||||
let clock
|
||||
let wrapper
|
||||
const newTabId = 1
|
||||
const file = new File(
|
||||
[new Blob(
|
||||
[JSON.stringify({ foo: [1, 2, 3] }, null, 2)],
|
||||
{ type: 'application/json' }
|
||||
)],
|
||||
'my data.json',
|
||||
{ type: 'application/json' })
|
||||
|
||||
beforeEach(() => {
|
||||
clock = sinon.useFakeTimers()
|
||||
|
||||
// mock store state and mutations
|
||||
state = {}
|
||||
mutations = {
|
||||
setDb: sinon.stub(),
|
||||
setCurrentTabId: sinon.stub()
|
||||
}
|
||||
actions = {
|
||||
addTab: sinon.stub().resolves(newTabId)
|
||||
}
|
||||
store = new Vuex.Store({ state, mutations, actions })
|
||||
|
||||
const db = {
|
||||
sanitizeTableName: sinon.stub().returns('my_data'),
|
||||
addTableFromCsv: sinon.stub().resolves(),
|
||||
createProgressCounter: sinon.stub().returns(1),
|
||||
deleteProgressCounter: sinon.stub(),
|
||||
validateTableName: sinon.stub().resolves(),
|
||||
execute: sinon.stub().resolves(),
|
||||
refreshSchema: sinon.stub().resolves()
|
||||
}
|
||||
|
||||
// mount the component
|
||||
wrapper = mount(CsvJsonImport, {
|
||||
store,
|
||||
propsData: {
|
||||
file,
|
||||
dialogName: 'addCsvJson',
|
||||
db
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
it('previews', async () => {
|
||||
await wrapper.vm.preview()
|
||||
await wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.find('[data-modal="addCsvJson"]').exists()).to.equal(true)
|
||||
expect(wrapper.find('.dialog-header').text()).to.equal('JSON import')
|
||||
expect(wrapper.find('#csv-json-table-name input').element.value).to.equal('my_data')
|
||||
expect(wrapper.findComponent({ name: 'delimiter-selector' }).exists()).to.equal(false)
|
||||
expect(wrapper.find('#quote-char input').exists()).to.equal(false)
|
||||
expect(wrapper.find('#escape-char input').exists()).to.equal(false)
|
||||
expect(wrapper.findComponent({ name: 'check-box' }).exists()).to.equal(false)
|
||||
const rows = wrapper.findAll('tbody tr')
|
||||
expect(rows).to.have.lengthOf(1)
|
||||
expect(rows.at(0).findAll('td').at(0).text()).to.equal([
|
||||
'{',
|
||||
' "foo": [',
|
||||
' 1,',
|
||||
' 2,',
|
||||
' 3',
|
||||
' ]',
|
||||
'}'
|
||||
].join('\n')
|
||||
)
|
||||
expect(wrapper.findComponent({ name: 'logs' }).text())
|
||||
.to.include('Preview parsing is completed in')
|
||||
expect(wrapper.find('#import-finish').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('#import-start').isVisible()).to.equal(true)
|
||||
})
|
||||
|
||||
it('has proper state before parsing is complete', async () => {
|
||||
const getJsonParseResult = sinon.stub(wrapper.vm, 'getJsonParseResult')
|
||||
getJsonParseResult.onCall(0).returns({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['doc'],
|
||||
values: {
|
||||
doc: ['{ "foo": [ 1, 2, 3 ] }']
|
||||
}
|
||||
},
|
||||
rowCount: 1,
|
||||
hasErrors: false,
|
||||
messages: []
|
||||
})
|
||||
|
||||
let resolveParsing
|
||||
getJsonParseResult.onCall(1).returns(new Promise(resolve => {
|
||||
resolveParsing = () => resolve({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['doc'],
|
||||
values: {
|
||||
doc: ['{ "foo": [ 1, 2, 3 ] }']
|
||||
}
|
||||
},
|
||||
rowCount: 1,
|
||||
hasErrors: false,
|
||||
messages: []
|
||||
})
|
||||
}))
|
||||
|
||||
await wrapper.vm.preview()
|
||||
await wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-json-table-name input').setValue('foo')
|
||||
await wrapper.find('#import-start').trigger('click')
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
// "Parsing JSON..." in the logs
|
||||
expect(wrapper.findComponent({ name: 'logs' }).findAll('.msg').at(1).text())
|
||||
.to.equal('Parsing JSON...')
|
||||
|
||||
// After 1 second - loading indicator is shown
|
||||
await clock.tick(1000)
|
||||
expect(
|
||||
wrapper.findComponent({ name: 'logs' }).findComponent({ name: 'LoadingIndicator' }).exists()
|
||||
).to.equal(true)
|
||||
|
||||
// All the dialog controls are disabled
|
||||
expect(wrapper.find('#import-cancel').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#import-finish').element.disabled).to.equal(true)
|
||||
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(true)
|
||||
expect(wrapper.find('#import-finish').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('#import-start').isVisible()).to.equal(true)
|
||||
await resolveParsing()
|
||||
await getJsonParseResult.returnValues[1]
|
||||
|
||||
// Loading indicator is not shown when parsing is compete
|
||||
expect(
|
||||
wrapper.findComponent({ name: 'logs' }).findComponent({ name: 'LoadingIndicator' }).exists()
|
||||
).to.equal(false)
|
||||
})
|
||||
|
||||
it('has proper state before import is completed', async () => {
|
||||
const getJsonParseResult = sinon.spy(wrapper.vm, 'getJsonParseResult')
|
||||
|
||||
let resolveImport = sinon.stub()
|
||||
wrapper.vm.db.addTableFromCsv = sinon.stub()
|
||||
.resolves(new Promise(resolve => { resolveImport = resolve }))
|
||||
|
||||
await wrapper.vm.preview()
|
||||
await wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-json-table-name input').setValue('foo')
|
||||
await wrapper.find('#import-start').trigger('click')
|
||||
await getJsonParseResult.returnValues[1]
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
// Parsing success in the logs
|
||||
expect(wrapper.findComponent({ name: 'logs' }).findAll('.msg').at(2).text())
|
||||
.to.equal('Importing JSON into a SQLite database...')
|
||||
|
||||
// After 1 second - loading indicator is shown
|
||||
await clock.tick(1000)
|
||||
expect(
|
||||
wrapper.findComponent({ name: 'logs' }).findComponent({ name: 'LoadingIndicator' }).exists()
|
||||
).to.equal(true)
|
||||
|
||||
// All the dialog controls are disabled
|
||||
expect(wrapper.find('#import-cancel').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#import-finish').element.disabled).to.equal(true)
|
||||
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(true)
|
||||
expect(wrapper.find('#import-finish').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('#import-start').isVisible()).to.equal(true)
|
||||
expect(wrapper.vm.db.addTableFromCsv.getCall(0).args[0]).to.equal('foo') // table name
|
||||
|
||||
// After resolving - loading indicator is not shown
|
||||
await resolveImport()
|
||||
await wrapper.vm.db.addTableFromCsv.returnValues[0]
|
||||
expect(
|
||||
wrapper.findComponent({ name: 'logs' }).findComponent({ name: 'LoadingIndicator' }).exists()
|
||||
).to.equal(false)
|
||||
})
|
||||
|
||||
it('import success', async () => {
|
||||
const getJsonParseResult = sinon.spy(wrapper.vm, 'getJsonParseResult')
|
||||
|
||||
await wrapper.vm.preview()
|
||||
await wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-json-table-name input').setValue('foo')
|
||||
await wrapper.find('#import-start').trigger('click')
|
||||
await getJsonParseResult.returnValues[1]
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
// Import success in the logs
|
||||
const logs = wrapper.findComponent({ name: 'logs' }).findAll('.msg')
|
||||
expect(logs).to.have.lengthOf(3)
|
||||
expect(logs.at(2).text()).to.contain('Importing JSON into a SQLite database is completed in')
|
||||
|
||||
// All the dialog controls are enabled
|
||||
expect(wrapper.find('#import-cancel').element.disabled).to.equal(false)
|
||||
expect(wrapper.find('#import-finish').element.disabled).to.equal(false)
|
||||
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(false)
|
||||
expect(wrapper.find('#import-finish').isVisible()).to.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('CsvJsonImport.vue - ndjson', () => {
|
||||
let state = {}
|
||||
let actions = {}
|
||||
let mutations = {}
|
||||
let store = {}
|
||||
let clock
|
||||
let wrapper
|
||||
const newTabId = 1
|
||||
const file = new File([], 'my data.ndjson')
|
||||
|
||||
beforeEach(() => {
|
||||
clock = sinon.useFakeTimers()
|
||||
|
||||
// mock store state and mutations
|
||||
state = {}
|
||||
mutations = {
|
||||
setDb: sinon.stub(),
|
||||
setCurrentTabId: sinon.stub()
|
||||
}
|
||||
actions = {
|
||||
addTab: sinon.stub().resolves(newTabId)
|
||||
}
|
||||
store = new Vuex.Store({ state, mutations, actions })
|
||||
|
||||
const db = {
|
||||
sanitizeTableName: sinon.stub().returns('my_data'),
|
||||
addTableFromCsv: sinon.stub().resolves(),
|
||||
createProgressCounter: sinon.stub().returns(1),
|
||||
deleteProgressCounter: sinon.stub(),
|
||||
validateTableName: sinon.stub().resolves(),
|
||||
execute: sinon.stub().resolves(),
|
||||
refreshSchema: sinon.stub().resolves()
|
||||
}
|
||||
|
||||
// mount the component
|
||||
wrapper = mount(CsvJsonImport, {
|
||||
store,
|
||||
propsData: {
|
||||
file,
|
||||
dialogName: 'addCsvJson',
|
||||
db
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
it('previews', async () => {
|
||||
sinon.stub(csv, 'parse').resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['doc'],
|
||||
values: {
|
||||
doc: ['{ "foo": [ 1, 2, 3 ] }']
|
||||
}
|
||||
},
|
||||
rowCount: 1,
|
||||
messages: []
|
||||
})
|
||||
|
||||
wrapper.vm.preview()
|
||||
await wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.find('[data-modal="addCsvJson"]').exists()).to.equal(true)
|
||||
expect(wrapper.find('.dialog-header').text()).to.equal('JSON import')
|
||||
expect(wrapper.find('#csv-json-table-name input').element.value).to.equal('my_data')
|
||||
expect(wrapper.findComponent({ name: 'delimiter-selector' }).exists()).to.equal(false)
|
||||
expect(wrapper.find('#quote-char input').exists()).to.equal(false)
|
||||
expect(wrapper.find('#escape-char input').exists()).to.equal(false)
|
||||
expect(wrapper.findComponent({ name: 'check-box' }).exists()).to.equal(false)
|
||||
const rows = wrapper.findAll('tbody tr')
|
||||
expect(rows).to.have.lengthOf(1)
|
||||
expect(rows.at(0).findAll('td').at(0).text()).to.equal('{ "foo": [ 1, 2, 3 ] }')
|
||||
expect(wrapper.findComponent({ name: 'logs' }).text())
|
||||
.to.include('Preview parsing is completed in')
|
||||
expect(wrapper.find('#import-finish').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('#import-start').isVisible()).to.equal(true)
|
||||
})
|
||||
|
||||
it('has proper state before parsing is complete', async () => {
|
||||
const parse = sinon.stub(csv, 'parse')
|
||||
parse.onCall(0).resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['doc'],
|
||||
values: {
|
||||
doc: ['{ "foo": [ 1, 2, 3 ] }']
|
||||
}
|
||||
},
|
||||
rowCount: 1
|
||||
})
|
||||
|
||||
wrapper.vm.preview()
|
||||
wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
let resolveParsing
|
||||
parse.onCall(1).returns(new Promise(resolve => {
|
||||
resolveParsing = () => resolve({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['doc'],
|
||||
values: {
|
||||
doc: ['{ "foo": [ 1, 2, 3 ] }']
|
||||
}
|
||||
},
|
||||
rowCount: 1,
|
||||
messages: []
|
||||
})
|
||||
}))
|
||||
|
||||
await wrapper.find('#csv-json-table-name input').setValue('foo')
|
||||
await wrapper.find('#import-start').trigger('click')
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
// "Parsing JSON..." in the logs
|
||||
expect(wrapper.findComponent({ name: 'logs' }).findAll('.msg').at(1).text())
|
||||
.to.equal('Parsing JSON...')
|
||||
|
||||
// After 1 second - loading indicator is shown
|
||||
await clock.tick(1000)
|
||||
expect(
|
||||
wrapper.findComponent({ name: 'logs' }).findComponent({ name: 'LoadingIndicator' }).exists()
|
||||
).to.equal(true)
|
||||
|
||||
// All the dialog controls are disabled
|
||||
expect(wrapper.find('#import-cancel').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#import-finish').element.disabled).to.equal(true)
|
||||
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(true)
|
||||
expect(wrapper.find('#import-finish').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('#import-start').isVisible()).to.equal(true)
|
||||
await resolveParsing()
|
||||
await parse.returnValues[1]
|
||||
|
||||
// Loading indicator is not shown when parsing is compete
|
||||
expect(
|
||||
wrapper.findComponent({ name: 'logs' }).findComponent({ name: 'LoadingIndicator' }).exists()
|
||||
).to.equal(false)
|
||||
})
|
||||
|
||||
it('has proper state before import is completed', async () => {
|
||||
const parse = sinon.stub(csv, 'parse')
|
||||
parse.onCall(0).resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['doc'],
|
||||
values: {
|
||||
doc: ['{ "foo": [ 1, 2, 3 ] }']
|
||||
}
|
||||
},
|
||||
rowCount: 1,
|
||||
hasErrors: false,
|
||||
messages: []
|
||||
})
|
||||
|
||||
parse.onCall(1).resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['doc'],
|
||||
values: {
|
||||
doc: ['{ "foo": [ 1, 2, 3 ] }']
|
||||
}
|
||||
},
|
||||
rowCount: 1,
|
||||
hasErrors: false,
|
||||
messages: []
|
||||
})
|
||||
|
||||
let resolveImport = sinon.stub()
|
||||
wrapper.vm.db.addTableFromCsv = sinon.stub()
|
||||
.resolves(new Promise(resolve => { resolveImport = resolve }))
|
||||
|
||||
wrapper.vm.preview()
|
||||
wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-json-table-name input').setValue('foo')
|
||||
await wrapper.find('#import-start').trigger('click')
|
||||
await csv.parse.returnValues[1]
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
// Parsing success in the logs
|
||||
expect(wrapper.findComponent({ name: 'logs' }).findAll('.msg').at(2).text())
|
||||
.to.equal('Importing JSON into a SQLite database...')
|
||||
|
||||
// After 1 second - loading indicator is shown
|
||||
await clock.tick(1000)
|
||||
expect(
|
||||
wrapper.findComponent({ name: 'logs' }).findComponent({ name: 'LoadingIndicator' }).exists()
|
||||
).to.equal(true)
|
||||
|
||||
// All the dialog controls are disabled
|
||||
expect(wrapper.find('#import-cancel').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#import-finish').element.disabled).to.equal(true)
|
||||
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(true)
|
||||
expect(wrapper.find('#import-finish').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('#import-start').isVisible()).to.equal(true)
|
||||
expect(wrapper.vm.db.addTableFromCsv.getCall(0).args[0]).to.equal('foo') // table name
|
||||
|
||||
// After resolving - loading indicator is not shown
|
||||
await resolveImport()
|
||||
await wrapper.vm.db.addTableFromCsv.returnValues[0]
|
||||
expect(
|
||||
wrapper.findComponent({ name: 'logs' }).findComponent({ name: 'LoadingIndicator' }).exists()
|
||||
).to.equal(false)
|
||||
})
|
||||
|
||||
it('import success', async () => {
|
||||
const parse = sinon.stub(csv, 'parse')
|
||||
parse.onCall(0).resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['doc'],
|
||||
values: {
|
||||
doc: ['{ "foo": [ 1, 2, 3 ] }']
|
||||
}
|
||||
},
|
||||
rowCount: 1,
|
||||
hasErrors: false,
|
||||
messages: []
|
||||
})
|
||||
// we need to separate calles because messages will mutate
|
||||
parse.onCall(1).resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['doc'],
|
||||
values: {
|
||||
doc: ['{ "foo": [ 1, 2, 3 ] }']
|
||||
}
|
||||
},
|
||||
rowCount: 2,
|
||||
hasErrors: false,
|
||||
messages: []
|
||||
})
|
||||
|
||||
wrapper.vm.preview()
|
||||
wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-json-table-name input').setValue('foo')
|
||||
await wrapper.find('#import-start').trigger('click')
|
||||
await csv.parse.returnValues[1]
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
// Import success in the logs
|
||||
const logs = wrapper.findComponent({ name: 'logs' }).findAll('.msg')
|
||||
expect(logs).to.have.lengthOf(3)
|
||||
expect(logs.at(2).text()).to.contain('Importing JSON into a SQLite database is completed in')
|
||||
|
||||
// All the dialog controls are enabled
|
||||
expect(wrapper.find('#import-cancel').element.disabled).to.equal(false)
|
||||
expect(wrapper.find('#import-finish').element.disabled).to.equal(false)
|
||||
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(false)
|
||||
expect(wrapper.find('#import-finish').isVisible()).to.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from 'chai'
|
||||
import { mount, shallowMount } from '@vue/test-utils'
|
||||
import DelimiterSelector from '@/components/CsvImport/DelimiterSelector'
|
||||
import DelimiterSelector from '@/components/CsvJsonImport/DelimiterSelector'
|
||||
|
||||
describe('DelimiterSelector', async () => {
|
||||
it('shows the name of value', async () => {
|
||||
|
||||
@@ -31,7 +31,7 @@ describe('DbUploader.vue', () => {
|
||||
|
||||
it('loads db on click and redirects to /workspace', async () => {
|
||||
// mock getting a file from user
|
||||
const file = { name: 'test.db' }
|
||||
const file = new File([], 'test.db')
|
||||
sinon.stub(fu, 'getFileFromUser').resolves(file)
|
||||
|
||||
// mock db loading
|
||||
@@ -85,7 +85,7 @@ describe('DbUploader.vue', () => {
|
||||
})
|
||||
|
||||
// mock a file dropped by a user
|
||||
const file = { name: 'test.db' }
|
||||
const file = new File([], 'test.db')
|
||||
const dropData = { dataTransfer: new DataTransfer() }
|
||||
Object.defineProperty(dropData.dataTransfer, 'files', {
|
||||
value: [file],
|
||||
@@ -103,7 +103,7 @@ describe('DbUploader.vue', () => {
|
||||
|
||||
it("doesn't redirect if already on /workspace", async () => {
|
||||
// mock getting a file from user
|
||||
const file = { name: 'test.db' }
|
||||
const file = new File([], 'test.db')
|
||||
sinon.stub(fu, 'getFileFromUser').resolves(file)
|
||||
|
||||
// mock db loading
|
||||
@@ -136,7 +136,7 @@ describe('DbUploader.vue', () => {
|
||||
|
||||
it('shows parse dialog if gets csv file', async () => {
|
||||
// mock getting a file from user
|
||||
const file = { name: 'test.csv' }
|
||||
const file = new File([], 'test.csv')
|
||||
sinon.stub(fu, 'getFileFromUser').resolves(file)
|
||||
|
||||
// mock router
|
||||
@@ -153,24 +153,92 @@ describe('DbUploader.vue', () => {
|
||||
}
|
||||
})
|
||||
|
||||
const CsvImport = wrapper.vm.$refs.addCsv
|
||||
const CsvImport = wrapper.vm.$refs.addCsvJson
|
||||
sinon.stub(CsvImport, 'reset')
|
||||
sinon.stub(CsvImport, 'previewCsv').resolves()
|
||||
sinon.stub(CsvImport, 'preview').resolves()
|
||||
sinon.stub(CsvImport, 'open')
|
||||
|
||||
await wrapper.find('.drop-area').trigger('click')
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(CsvImport.reset.calledOnce).to.equal(true)
|
||||
await wrapper.vm.animationPromise
|
||||
expect(CsvImport.previewCsv.calledOnce).to.equal(true)
|
||||
expect(CsvImport.preview.calledOnce).to.equal(true)
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(CsvImport.open.calledOnce).to.equal(true)
|
||||
wrapper.destroy()
|
||||
})
|
||||
|
||||
it('deletes temporary db if CSV import is canceled', async () => {
|
||||
it('shows parse dialog if gets json file', async () => {
|
||||
// mock getting a file from user
|
||||
const file = { name: 'test.csv' }
|
||||
const file = new File([], 'test.json', { type: 'application/json' })
|
||||
sinon.stub(fu, 'getFileFromUser').resolves(file)
|
||||
|
||||
// mock router
|
||||
const $router = { push: sinon.stub() }
|
||||
const $route = { path: '/workspace' }
|
||||
|
||||
// mount the component
|
||||
const wrapper = mount(DbUploader, {
|
||||
attachTo: place,
|
||||
store,
|
||||
mocks: { $router, $route },
|
||||
propsData: {
|
||||
type: 'illustrated'
|
||||
}
|
||||
})
|
||||
|
||||
const JsonImport = wrapper.vm.$refs.addCsvJson
|
||||
sinon.stub(JsonImport, 'reset')
|
||||
sinon.stub(JsonImport, 'preview').resolves()
|
||||
sinon.stub(JsonImport, 'open')
|
||||
|
||||
await wrapper.find('.drop-area').trigger('click')
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(JsonImport.reset.calledOnce).to.equal(true)
|
||||
await wrapper.vm.animationPromise
|
||||
expect(JsonImport.preview.calledOnce).to.equal(true)
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(JsonImport.open.calledOnce).to.equal(true)
|
||||
wrapper.destroy()
|
||||
})
|
||||
|
||||
it('shows parse dialog if gets ndjson file', async () => {
|
||||
// mock getting a file from user
|
||||
const file = new File([], 'test.ndjson')
|
||||
sinon.stub(fu, 'getFileFromUser').resolves(file)
|
||||
|
||||
// mock router
|
||||
const $router = { push: sinon.stub() }
|
||||
const $route = { path: '/workspace' }
|
||||
|
||||
// mount the component
|
||||
const wrapper = mount(DbUploader, {
|
||||
attachTo: place,
|
||||
store,
|
||||
mocks: { $router, $route },
|
||||
propsData: {
|
||||
type: 'illustrated'
|
||||
}
|
||||
})
|
||||
|
||||
const JsonImport = wrapper.vm.$refs.addCsvJson
|
||||
sinon.stub(JsonImport, 'reset')
|
||||
sinon.stub(JsonImport, 'preview').resolves()
|
||||
sinon.stub(JsonImport, 'open')
|
||||
|
||||
await wrapper.find('.drop-area').trigger('click')
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(JsonImport.reset.calledOnce).to.equal(true)
|
||||
await wrapper.vm.animationPromise
|
||||
expect(JsonImport.preview.calledOnce).to.equal(true)
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(JsonImport.open.calledOnce).to.equal(true)
|
||||
wrapper.destroy()
|
||||
})
|
||||
|
||||
it('deletes temporary db if import is canceled', async () => {
|
||||
// mock getting a file from user
|
||||
const file = new File([], 'test.csv')
|
||||
sinon.stub(fu, 'getFileFromUser').resolves(file)
|
||||
|
||||
// mock router
|
||||
@@ -186,9 +254,9 @@ describe('DbUploader.vue', () => {
|
||||
}
|
||||
})
|
||||
|
||||
const CsvImport = wrapper.vm.$refs.addCsv
|
||||
const CsvImport = wrapper.vm.$refs.addCsvJson
|
||||
sinon.stub(CsvImport, 'reset')
|
||||
sinon.stub(CsvImport, 'previewCsv').resolves()
|
||||
sinon.stub(CsvImport, 'preview').resolves()
|
||||
sinon.stub(CsvImport, 'open')
|
||||
|
||||
await wrapper.find('.drop-area').trigger('click')
|
||||
|
||||
@@ -28,7 +28,26 @@ describe('csv.js', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('getResult without fields', () => {
|
||||
it('getResult without fields but with columns', () => {
|
||||
const source = {
|
||||
data: [
|
||||
[1, 'foo', new Date('2021-06-30T14:10:24.717Z')],
|
||||
[2, 'bar', new Date('2021-07-30T14:10:15.717Z')]
|
||||
],
|
||||
meta: {}
|
||||
}
|
||||
const columns = ['id', 'name', 'date']
|
||||
expect(csv.getResult(source, columns)).to.eql({
|
||||
columns: ['id', 'name', 'date'],
|
||||
values: {
|
||||
id: [1, 2],
|
||||
name: ['foo', 'bar'],
|
||||
date: ['2021-06-30T14:10:24.717Z', '2021-07-30T14:10:15.717Z']
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('getResult without fields and columns', () => {
|
||||
const source = {
|
||||
data: [
|
||||
[1, 'foo', new Date('2021-06-30T14:10:24.717Z')],
|
||||
|
||||
@@ -455,4 +455,85 @@ describe('SQLite extensions', function () {
|
||||
xx: [1], xy: [0], xz: [1], yx: [0], yy: [1], yz: [1], zx: [1], zy: [1], zz: [1]
|
||||
})
|
||||
})
|
||||
|
||||
it('supports simple Lua functions', async function () {
|
||||
const actual = await db.execute(`
|
||||
INSERT INTO
|
||||
luafunctions(name, src)
|
||||
VALUES
|
||||
('lua_inline', 'return {"arg"}, {"rv"}, "simple", function(arg) return arg + 1 end'),
|
||||
('lua_full', '
|
||||
local input = {"arg"}
|
||||
local output = {"rv"}
|
||||
|
||||
local function func(x)
|
||||
return math.sin(math.pi) + x
|
||||
end
|
||||
|
||||
return input, output, "simple", func
|
||||
');
|
||||
|
||||
SELECT lua_inline(1), lua_full(1) - 1 < 0.000001;
|
||||
`)
|
||||
expect(actual.values).to.eql({ 'lua_inline(1)': [2], 'lua_full(1) - 1 < 0.000001': [1] })
|
||||
})
|
||||
|
||||
it('supports aggregate Lua functions', async function () {
|
||||
const actual = await db.execute(`
|
||||
INSERT INTO
|
||||
luafunctions(name, src)
|
||||
VALUES
|
||||
('lua_sum', '
|
||||
local inputs = {"item"}
|
||||
local outputs = {"sum"}
|
||||
|
||||
local function func(item)
|
||||
if aggregate_now(item) then
|
||||
return item
|
||||
end
|
||||
|
||||
local sum = 0
|
||||
while true do
|
||||
if aggregate_now(item) then
|
||||
break
|
||||
end
|
||||
sum = sum + item
|
||||
item = coroutine.yield()
|
||||
end
|
||||
|
||||
return sum
|
||||
end
|
||||
|
||||
return inputs, outputs, "aggregate", func
|
||||
');
|
||||
|
||||
SELECT SUM(value), lua_sum(value) FROM generate_series(1, 10);
|
||||
`)
|
||||
expect(actual.values).to.eql({ 'SUM(value)': [55], 'lua_sum(value)': [55] })
|
||||
})
|
||||
|
||||
it('supports table-valued Lua functions', async function () {
|
||||
const actual = await db.execute(`
|
||||
INSERT INTO
|
||||
luafunctions(name, src)
|
||||
VALUES
|
||||
('lua_match', '
|
||||
local inputs = {"pattern", "s"}
|
||||
local outputs = {"idx", "elm"}
|
||||
|
||||
local function func(pattern, s)
|
||||
local i = 1
|
||||
for k in s:gmatch(pattern) do
|
||||
coroutine.yield(i, k)
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
|
||||
return inputs, outputs, "table", func
|
||||
');
|
||||
|
||||
SELECT * FROM lua_match('%w+', 'hello world from Lua');
|
||||
`)
|
||||
expect(actual.values).to.eql({ idx: [1, 2, 3, 4], elm: ['hello', 'world', 'from', 'Lua'] })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -106,10 +106,65 @@ describe('fileIo.js', () => {
|
||||
await expect(fIo.readAsArrayBuffer(blob)).to.be.rejectedWith('Problem parsing input file.')
|
||||
})
|
||||
|
||||
it('isJSON', () => {
|
||||
let file = { type: 'application/json' }
|
||||
expect(fIo.isJSON(file)).to.equal(true)
|
||||
|
||||
file = { type: 'application/x-sqlite3' }
|
||||
expect(fIo.isJSON(file)).to.equal(false)
|
||||
|
||||
file = { type: '', name: 'test.db' }
|
||||
expect(fIo.isJSON(file)).to.equal(false)
|
||||
|
||||
file = { type: '', name: 'test.sqlite' }
|
||||
expect(fIo.isJSON(file)).to.equal(false)
|
||||
|
||||
file = { type: '', name: 'test.sqlite3' }
|
||||
expect(fIo.isJSON(file)).to.equal(false)
|
||||
|
||||
file = { type: '', name: 'test.csv' }
|
||||
expect(fIo.isJSON(file)).to.equal(false)
|
||||
|
||||
file = { type: '', name: 'test.ndjson' }
|
||||
expect(fIo.isJSON(file)).to.equal(false)
|
||||
|
||||
file = { type: 'text', name: 'test.db' }
|
||||
expect(fIo.isJSON(file)).to.equal(false)
|
||||
})
|
||||
|
||||
it('isNDJSON', () => {
|
||||
let file = { type: 'application/json', name: 'test.json' }
|
||||
expect(fIo.isNDJSON(file)).to.equal(false)
|
||||
|
||||
file = { type: 'application/x-sqlite3', name: 'test.sqlite3' }
|
||||
expect(fIo.isNDJSON(file)).to.equal(false)
|
||||
|
||||
file = { type: '', name: 'test.db' }
|
||||
expect(fIo.isNDJSON(file)).to.equal(false)
|
||||
|
||||
file = { type: '', name: 'test.sqlite' }
|
||||
expect(fIo.isNDJSON(file)).to.equal(false)
|
||||
|
||||
file = { type: '', name: 'test.sqlite3' }
|
||||
expect(fIo.isNDJSON(file)).to.equal(false)
|
||||
|
||||
file = { type: '', name: 'test.csv' }
|
||||
expect(fIo.isNDJSON(file)).to.equal(false)
|
||||
|
||||
file = { type: '', name: 'test.ndjson' }
|
||||
expect(fIo.isNDJSON(file)).to.equal(true)
|
||||
|
||||
file = { type: 'text', name: 'test.db' }
|
||||
expect(fIo.isNDJSON(file)).to.equal(false)
|
||||
})
|
||||
|
||||
it('isDatabase', () => {
|
||||
let file = { type: 'application/vnd.sqlite3' }
|
||||
expect(fIo.isDatabase(file)).to.equal(true)
|
||||
|
||||
file = { type: 'application/json' }
|
||||
expect(fIo.isDatabase(file)).to.equal(false)
|
||||
|
||||
file = { type: 'application/x-sqlite3' }
|
||||
expect(fIo.isDatabase(file)).to.equal(true)
|
||||
|
||||
@@ -125,6 +180,9 @@ describe('fileIo.js', () => {
|
||||
file = { type: '', name: 'test.csv' }
|
||||
expect(fIo.isDatabase(file)).to.equal(false)
|
||||
|
||||
file = { type: '', name: 'test.ndjson' }
|
||||
expect(fIo.isDatabase(file)).to.equal(false)
|
||||
|
||||
file = { type: 'text', name: 'test.db' }
|
||||
expect(fIo.isDatabase(file)).to.equal(false)
|
||||
})
|
||||
|
||||
@@ -29,12 +29,12 @@ describe('time.js', () => {
|
||||
})
|
||||
|
||||
it('sleep resolves after n ms', async () => {
|
||||
let before = Date.now()
|
||||
let before = performance.now()
|
||||
await time.sleep(10)
|
||||
expect(Date.now() - before).to.be.least(10)
|
||||
expect(performance.now() - before).to.be.least(10)
|
||||
|
||||
before = Date.now()
|
||||
before = performance.now()
|
||||
await time.sleep(30)
|
||||
expect(Date.now() - before).to.be.least(30)
|
||||
expect(performance.now() - before).to.be.least(30)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -125,7 +125,7 @@ describe('Schema.vue', () => {
|
||||
})
|
||||
|
||||
it('adds table', async () => {
|
||||
const file = { name: 'test.csv' }
|
||||
const file = new File([], 'test.csv')
|
||||
sinon.stub(fIo, 'getFileFromUser').resolves(file)
|
||||
|
||||
sinon.stub(csv, 'parse').resolves({
|
||||
@@ -152,20 +152,20 @@ describe('Schema.vue', () => {
|
||||
|
||||
const store = new Vuex.Store({ state, actions, mutations })
|
||||
const wrapper = mount(Schema, { store, localVue })
|
||||
sinon.spy(wrapper.vm.$refs.addCsv, 'previewCsv')
|
||||
sinon.spy(wrapper.vm, 'addCsv')
|
||||
sinon.spy(wrapper.vm.$refs.addCsv, 'loadFromCsv')
|
||||
sinon.spy(wrapper.vm.$refs.addCsvJson, 'preview')
|
||||
sinon.spy(wrapper.vm, 'addCsvJson')
|
||||
sinon.spy(wrapper.vm.$refs.addCsvJson, 'loadToDb')
|
||||
|
||||
await wrapper.findComponent({ name: 'add-table-icon' }).find('svg').trigger('click')
|
||||
await wrapper.vm.$refs.addCsv.previewCsv.returnValues[0]
|
||||
await wrapper.vm.addCsv.returnValues[0]
|
||||
await wrapper.vm.$refs.addCsvJson.preview.returnValues[0]
|
||||
await wrapper.vm.addCsvJson.returnValues[0]
|
||||
await wrapper.vm.$nextTick()
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.find('[data-modal="addCsv"]').exists()).to.equal(true)
|
||||
await wrapper.find('#csv-import').trigger('click')
|
||||
await wrapper.vm.$refs.addCsv.loadFromCsv.returnValues[0]
|
||||
await wrapper.find('#csv-finish').trigger('click')
|
||||
expect(wrapper.find('[data-modal="addCsv"]').exists()).to.equal(false)
|
||||
expect(wrapper.find('[data-modal="addCsvJson"]').exists()).to.equal(true)
|
||||
await wrapper.find('#import-start').trigger('click')
|
||||
await wrapper.vm.$refs.addCsvJson.loadToDb.returnValues[0]
|
||||
await wrapper.find('#import-finish').trigger('click')
|
||||
expect(wrapper.find('[data-modal="addCsvJson"]').exists()).to.equal(false)
|
||||
await state.db.refreshSchema.returnValues[0]
|
||||
|
||||
expect(wrapper.vm.$store.state.db.schema).to.eql([
|
||||
|
||||
Reference in New Issue
Block a user