1
0
mirror of https://github.com/lana-k/sqliteviz.git synced 2025-12-06 18:18:53 +08:00

CSV import as a table and db connection rework

- Add csv to existing db #32
- [RFE] Simplify working with temporary tables #53
This commit is contained in:
lana-k
2021-05-24 19:40:47 +02:00
parent c96deb5766
commit 99a10225a3
37 changed files with 1362 additions and 1169 deletions

View File

@@ -4,6 +4,9 @@ import { mount, createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex'
import Schema from '@/views/Main/Editor/Schema'
import TableDescription from '@/views/Main/Editor/Schema/TableDescription'
import database from '@/lib/database'
import fIo from '@/lib/utils/fileIo'
import csv from '@/components/CsvImport/csv'
const localVue = createLocalVue()
localVue.use(Vuex)
@@ -16,7 +19,9 @@ describe('Schema.vue', () => {
it('Renders DB name on initial', () => {
// mock store state
const state = {
dbName: 'fooDB'
db: {
dbName: 'fooDB'
}
}
const store = new Vuex.Store({ state })
@@ -31,7 +36,9 @@ describe('Schema.vue', () => {
it('Schema visibility is toggled when click on DB name', async () => {
// mock store state
const state = {
dbName: 'fooDB'
db: {
dbName: 'fooDB'
}
}
const store = new Vuex.Store({ state })
@@ -48,30 +55,32 @@ describe('Schema.vue', () => {
it('Schema filter', async () => {
// mock store state
const state = {
dbName: 'fooDB',
schema: [
{
name: 'foo',
columns: [
{ name: 'id', type: 'INTEGER' },
{ name: 'title', type: 'NVARCHAR(24)' }
]
},
{
name: 'bar',
columns: [
{ name: 'id', type: 'INTEGER' },
{ name: 'price', type: 'INTEGER' }
]
},
{
name: 'foobar',
columns: [
{ name: 'id', type: 'INTEGER' },
{ name: 'price', type: 'INTEGER' }
]
}
]
db: {
dbName: 'fooDB',
schema: [
{
name: 'foo',
columns: [
{ name: 'id', type: 'INTEGER' },
{ name: 'title', type: 'NVARCHAR(24)' }
]
},
{
name: 'bar',
columns: [
{ name: 'id', type: 'INTEGER' },
{ name: 'price', type: 'INTEGER' }
]
},
{
name: 'foobar',
columns: [
{ name: 'id', type: 'INTEGER' },
{ name: 'price', type: 'INTEGER' }
]
}
]
}
}
const store = new Vuex.Store({ state })
@@ -101,15 +110,69 @@ describe('Schema.vue', () => {
it('exports db', async () => {
const state = {
dbName: 'fooDB',
db: {
dbName: 'fooDB',
export: sinon.stub().resolves()
}
}
const store = new Vuex.Store({ state })
const wrapper = mount(Schema, { store, localVue })
await wrapper.findComponent({ name: 'export-icon' }).trigger('click')
await wrapper.findComponent({ name: 'export-icon' }).find('svg').trigger('click')
expect(state.db.export.calledOnceWith('fooDB'))
})
it('adds table', async () => {
const file = { name: 'test.csv' }
sinon.stub(fIo, 'getFileFromUser').resolves(file)
sinon.stub(csv, 'parse').resolves({
delimiter: '|',
data: {
columns: ['col1', 'col2'],
values: [
[1, 'foo']
]
},
hasErrors: false,
messages: []
})
const state = {
db: database.getNewDatabase()
}
state.db.dbName = 'db'
state.db.execute('CREATE TABLE foo(id)')
state.db.refreshSchema()
sinon.spy(state.db, 'refreshSchema')
const store = new Vuex.Store({ state })
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')
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.$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)
await state.db.refreshSchema.returnValues[0]
expect(wrapper.vm.$store.state.db.schema).to.eql([
{ name: 'test', columns: [{ name: 'col1', type: 'real' }, { name: 'col2', type: 'text' }] },
{ name: 'foo', columns: [{ name: 'id', type: 'N/A' }] }
])
const res = await wrapper.vm.$store.state.db.execute('select * from test')
expect(res).to.eql({
columns: ['col1', 'col2'],
values: [[1, 'foo']]
})
})
})

View File

@@ -7,6 +7,5 @@ describe('SqlEditor.vue', () => {
const wrapper = mount(SqlEditor)
await wrapper.findComponent({ name: 'codemirror' }).vm.$emit('input', 'SELECT * FROM foo')
expect(wrapper.emitted('input')[0]).to.eql(['SELECT * FROM foo'])
// Take a pause to keep proper state in debounced '@/views/Main/Editor/Tabs/Tab/SqlEditor/hint'
})
})

View File

@@ -11,22 +11,24 @@ describe('hint.js', () => {
it('Calculates table list for hint', () => {
// mock store state
const schema = [
{
name: 'foo',
columns: [
{ name: 'fooId', type: 'INTEGER' },
{ name: 'name', type: 'NVARCHAR(20)' }
]
},
{
name: 'bar',
columns: [
{ name: 'barId', type: 'INTEGER' }
]
}
]
sinon.stub(state, 'schema').value(schema)
const db = {
schema: [
{
name: 'foo',
columns: [
{ name: 'fooId', type: 'INTEGER' },
{ name: 'name', type: 'NVARCHAR(20)' }
]
},
{
name: 'bar',
columns: [
{ name: 'barId', type: 'INTEGER' }
]
}
]
}
sinon.stub(state, 'db').value(db)
// mock showHint and editor
sinon.stub(CM, 'showHint')
@@ -52,16 +54,18 @@ describe('hint.js', () => {
it('Add default table if there is only one table in schema', () => {
// mock store state
const schema = [
{
name: 'foo',
columns: [
{ name: 'fooId', type: 'INTEGER' },
{ name: 'name', type: 'NVARCHAR(20)' }
]
}
]
sinon.stub(state, 'schema').value(schema)
const db = {
schema: [
{
name: 'foo',
columns: [
{ name: 'fooId', type: 'INTEGER' },
{ name: 'name', type: 'NVARCHAR(20)' }
]
}
]
}
sinon.stub(state, 'db').value(db)
// mock showHint and editor
sinon.stub(CM, 'showHint')
@@ -190,7 +194,7 @@ describe('hint.js', () => {
it('tables is empty object when schema is null', () => {
// mock store state
sinon.stub(state, 'schema').value(null)
sinon.stub(state, 'db').value({ schema: null })
// mock showHint and editor
sinon.stub(CM, 'showHint')

View File

@@ -182,7 +182,8 @@ describe('Tab.vue', () => {
const state = {
currentTabId: 1,
db: {
execute: sinon.stub().rejects(new Error('There is no table foo'))
execute: sinon.stub().rejects(new Error('There is no table foo')),
refreshSchema: sinon.stub().resolves()
}
}
@@ -221,7 +222,7 @@ describe('Tab.vue', () => {
currentTabId: 1,
db: {
execute: sinon.stub().resolves(result),
getSchema: sinon.stub().resolves({ dbName: '', schema: [] })
refreshSchema: sinon.stub().resolves()
}
}
@@ -253,36 +254,17 @@ describe('Tab.vue', () => {
columns: ['id', 'name'],
values: []
}
const newSchema = {
dbName: 'fooDb',
schema: [
{
name: 'foo',
columns: [
{ name: 'id', type: 'INTEGER' },
{ name: 'title', type: 'NVARCHAR(30)' }
]
},
{
name: 'bar',
columns: [
{ name: 'a', type: 'N/A' },
{ name: 'b', type: 'N/A' }
]
}
]
}
// mock store state
const state = {
currentTabId: 1,
dbName: 'fooDb',
db: {
execute: sinon.stub().resolves(result),
getSchema: sinon.stub().resolves(newSchema)
refreshSchema: sinon.stub().resolves()
}
}
sinon.spy(mutations, 'saveSchema')
const store = new Vuex.Store({ state, mutations })
// mount the component
@@ -300,7 +282,6 @@ describe('Tab.vue', () => {
})
await wrapper.vm.execute()
expect(state.db.getSchema.calledOnceWith('fooDb')).to.equal(true)
expect(mutations.saveSchema.calledOnceWith(state, newSchema)).to.equal(true)
expect(state.db.refreshSchema.calledOnce).to.equal(true)
})
})

View File

@@ -20,7 +20,7 @@ describe('MainMenu.vue', () => {
const state = {
currentTab: { query: '', execute: sinon.stub() },
tabs: [{}],
schema: []
db: {}
}
const store = new Vuex.Store({ state })
const $route = { path: '/editor' }
@@ -49,7 +49,7 @@ describe('MainMenu.vue', () => {
const state = {
currentTab: null,
tabs: [{}],
schema: []
db: {}
}
const store = new Vuex.Store({ state })
const $route = { path: '/editor' }
@@ -65,11 +65,11 @@ describe('MainMenu.vue', () => {
expect(wrapper.find('#create-btn').isVisible()).to.equal(true)
})
it('Run is disabled if there is no schema or no query', async () => {
it('Run is disabled if there is no db or no query', async () => {
const state = {
currentTab: { query: 'SELECT * FROM foo', execute: sinon.stub() },
tabs: [{}],
schema: null
db: null
}
const store = new Vuex.Store({ state })
const $route = { path: '/editor' }
@@ -82,7 +82,7 @@ describe('MainMenu.vue', () => {
const vm = wrapper.vm
expect(wrapper.find('#run-btn').element.disabled).to.equal(true)
await vm.$set(state, 'schema', [])
await vm.$set(state, 'db', {})
expect(wrapper.find('#run-btn').element.disabled).to.equal(false)
await vm.$set(state.currentTab, 'query', '')
@@ -97,7 +97,7 @@ describe('MainMenu.vue', () => {
tabIndex: 0
},
tabs: [{ isUnsaved: true }],
schema: null
db: {}
}
const store = new Vuex.Store({ state })
const $route = { path: '/editor' }
@@ -122,7 +122,7 @@ describe('MainMenu.vue', () => {
tabIndex: 0
},
tabs: [{ isUnsaved: true }],
schema: null
db: {}
}
const newQueryId = 1
const actions = {
@@ -156,7 +156,7 @@ describe('MainMenu.vue', () => {
tabIndex: 0
},
tabs: [{ isUnsaved: true }],
schema: null
db: {}
}
const newQueryId = 1
const actions = {
@@ -191,7 +191,7 @@ describe('MainMenu.vue', () => {
tabIndex: 0
},
tabs: [{ isUnsaved: true }],
schema: []
db: {}
}
const store = new Vuex.Store({ state })
const $route = { path: '/editor' }
@@ -212,14 +212,14 @@ describe('MainMenu.vue', () => {
expect(state.currentTab.execute.calledTwice).to.equal(true)
// Running is disabled and route path is editor
await wrapper.vm.$set(state, 'schema', null)
await wrapper.vm.$set(state, 'db', null)
document.dispatchEvent(ctrlR)
expect(state.currentTab.execute.calledTwice).to.equal(true)
document.dispatchEvent(metaR)
expect(state.currentTab.execute.calledTwice).to.equal(true)
// Running is enabled and route path is not editor
await wrapper.vm.$set(state, 'schema', [])
await wrapper.vm.$set(state, 'db', {})
await wrapper.vm.$set($route, 'path', '/my-queries')
document.dispatchEvent(ctrlR)
expect(state.currentTab.execute.calledTwice).to.equal(true)
@@ -236,7 +236,7 @@ describe('MainMenu.vue', () => {
tabIndex: 0
},
tabs: [{ isUnsaved: true }],
schema: []
db: {}
}
const store = new Vuex.Store({ state })
const $route = { path: '/editor' }
@@ -257,14 +257,14 @@ describe('MainMenu.vue', () => {
expect(state.currentTab.execute.calledTwice).to.equal(true)
// Running is disabled and route path is editor
await wrapper.vm.$set(state, 'schema', null)
await wrapper.vm.$set(state, 'db', null)
document.dispatchEvent(ctrlEnter)
expect(state.currentTab.execute.calledTwice).to.equal(true)
document.dispatchEvent(metaEnter)
expect(state.currentTab.execute.calledTwice).to.equal(true)
// Running is enabled and route path is not editor
await wrapper.vm.$set(state, 'schema', [])
await wrapper.vm.$set(state, 'db', {})
await wrapper.vm.$set($route, 'path', '/my-queries')
document.dispatchEvent(ctrlEnter)
expect(state.currentTab.execute.calledTwice).to.equal(true)
@@ -280,7 +280,7 @@ describe('MainMenu.vue', () => {
tabIndex: 0
},
tabs: [{ isUnsaved: true }],
schema: []
db: {}
}
const store = new Vuex.Store({ state })
const $route = { path: '/editor' }
@@ -315,7 +315,7 @@ describe('MainMenu.vue', () => {
tabIndex: 0
},
tabs: [{ isUnsaved: true }],
schema: []
db: {}
}
const store = new Vuex.Store({ state })
const $route = { path: '/editor' }
@@ -360,7 +360,7 @@ describe('MainMenu.vue', () => {
tabIndex: 0
},
tabs: [{ id: 1, name: 'foo', isUnsaved: true }],
schema: []
db: {}
}
const mutations = {
updateTab: sinon.stub()
@@ -411,7 +411,7 @@ describe('MainMenu.vue', () => {
tabIndex: 0
},
tabs: [{ id: 1, name: null, tempName: 'Untitled', isUnsaved: true }],
schema: []
db: {}
}
const mutations = {
updateTab: sinon.stub()
@@ -456,7 +456,7 @@ describe('MainMenu.vue', () => {
tabIndex: 0
},
tabs: [{ id: 1, name: null, tempName: 'Untitled', isUnsaved: true }],
schema: []
db: {}
}
const mutations = {
updateTab: sinon.stub()
@@ -528,7 +528,7 @@ describe('MainMenu.vue', () => {
view: 'chart'
},
tabs: [{ id: 1, name: 'foo', isUnsaved: true, isPredefined: true }],
schema: []
db: {}
}
const mutations = {
updateTab: sinon.stub()
@@ -607,7 +607,7 @@ describe('MainMenu.vue', () => {
tabIndex: 0
},
tabs: [{ id: 1, name: null, tempName: 'Untitled', isUnsaved: true }],
schema: []
db: {}
}
const mutations = {
updateTab: sinon.stub()