diff --git a/package-lock.json b/package-lock.json
index 31d0c5e..65c6954 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,17 +1,16 @@
{
"name": "sqliteviz",
- "version": "1.0.0",
+ "version": "0.13.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "sqliteviz",
- "version": "1.0.0",
+ "version": "0.13.0",
"license": "Apache-2.0",
"dependencies": {
"codemirror": "^5.57.0",
"core-js": "^3.6.5",
- "debounce": "^1.2.0",
"nanoid": "^3.1.12",
"papaparse": "^5.3.0",
"plotly.js": "^1.58.4",
@@ -6748,11 +6747,6 @@
"integrity": "sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0=",
"dev": true
},
- "node_modules/debounce": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.0.tgz",
- "integrity": "sha512-mYtLl1xfZLi1m4RtQYlZgJUNQjl4ZxVnHzIR8nLLgi4q1YT8o/WM+MK/f8yfcc9s5Ir5zRaPZyZU6xs1Syoocg=="
- },
"node_modules/debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
@@ -29304,11 +29298,6 @@
"integrity": "sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0=",
"dev": true
},
- "debounce": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.0.tgz",
- "integrity": "sha512-mYtLl1xfZLi1m4RtQYlZgJUNQjl4ZxVnHzIR8nLLgi4q1YT8o/WM+MK/f8yfcc9s5Ir5zRaPZyZU6xs1Syoocg=="
- },
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
diff --git a/package.json b/package.json
index 4b9b7c0..39b4114 100644
--- a/package.json
+++ b/package.json
@@ -12,7 +12,6 @@
"dependencies": {
"codemirror": "^5.57.0",
"core-js": "^3.6.5",
- "debounce": "^1.2.0",
"nanoid": "^3.1.12",
"papaparse": "^5.3.0",
"plotly.js": "^1.58.4",
diff --git a/src/assets/styles/tables.css b/src/assets/styles/tables.css
index f577ad3..5511f00 100644
--- a/src/assets/styles/tables.css
+++ b/src/assets/styles/tables.css
@@ -1,5 +1,5 @@
.rounded-bg {
- padding: 40px 5px 5px;
+ padding: 35px 5px 5px;
background-color: white;
border-radius: 5px;
position: relative;
@@ -36,7 +36,7 @@
}
table {
min-width: 100%;
- margin-top: -40px;
+ margin-top: -35px;
border-collapse: collapse;
}
thead th, .fixed-header {
@@ -56,7 +56,7 @@ tbody td {
border-right: 1px solid var(--color-border-light);
}
td, th, .fixed-header {
- padding: 12px 24px;
+ padding: 8px 24px;
white-space: nowrap;
}
diff --git a/src/components/DbUploader/DelimiterSelector/ascii.js b/src/components/CsvImport/DelimiterSelector/ascii.js
similarity index 100%
rename from src/components/DbUploader/DelimiterSelector/ascii.js
rename to src/components/CsvImport/DelimiterSelector/ascii.js
diff --git a/src/components/DbUploader/DelimiterSelector/index.vue b/src/components/CsvImport/DelimiterSelector/index.vue
similarity index 100%
rename from src/components/DbUploader/DelimiterSelector/index.vue
rename to src/components/CsvImport/DelimiterSelector/index.vue
diff --git a/src/components/DbUploader/csv.js b/src/components/CsvImport/csv.js
similarity index 100%
rename from src/components/DbUploader/csv.js
rename to src/components/CsvImport/csv.js
diff --git a/src/components/CsvImport/index.vue b/src/components/CsvImport/index.vue
new file mode 100644
index 0000000..26ff7db
--- /dev/null
+++ b/src/components/CsvImport/index.vue
@@ -0,0 +1,381 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
No data
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/DbUploader.vue b/src/components/DbUploader.vue
new file mode 100644
index 0000000..c6bb051
--- /dev/null
+++ b/src/components/DbUploader.vue
@@ -0,0 +1,259 @@
+
+
+
+
+
+
+ Drop the database or CSV file here or click to choose a file from your computer.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/DbUploader/index.vue b/src/components/DbUploader/index.vue
deleted file mode 100644
index bd515c4..0000000
--- a/src/components/DbUploader/index.vue
+++ /dev/null
@@ -1,558 +0,0 @@
-
-
-
-
-
-
- Drop the database or CSV file here or click to choose a file from your computer.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
No data
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/lib/database/_sql.js b/src/lib/database/_sql.js
index 9f95e17..0dfa5ae 100644
--- a/src/lib/database/_sql.js
+++ b/src/lib/database/_sql.js
@@ -39,13 +39,15 @@ export default class Sql {
return this.db.exec(sql, params)
}
- import (columns, values, progressCounterId, progressCallback, chunkSize = 1500) {
- this.createDb()
- this.db.exec(dbUtils.getCreateStatement(columns, values))
+ import (tabName, columns, values, progressCounterId, progressCallback, chunkSize = 1500) {
+ if (this.db === null) {
+ this.createDb()
+ }
+ this.db.exec(dbUtils.getCreateStatement(tabName, columns, values))
const chunks = dbUtils.generateChunks(values, chunkSize)
const chunksAmount = Math.ceil(values.length / chunkSize)
let count = 0
- const insertStr = dbUtils.getInsertStmt(columns)
+ const insertStr = dbUtils.getInsertStmt(tabName, columns)
const insertStmt = this.db.prepare(insertStr)
progressCallback({ progress: 0, id: progressCounterId })
diff --git a/src/lib/database/_statements.js b/src/lib/database/_statements.js
index b4365a6..158a1fe 100644
--- a/src/lib/database/_statements.js
+++ b/src/lib/database/_statements.js
@@ -1,3 +1,5 @@
+import sqliteParser from 'sqlite-parser'
+
export default {
* generateChunks (arr, size) {
const count = Math.ceil(arr.length / size)
@@ -9,14 +11,14 @@ export default {
}
},
- getInsertStmt (columns) {
+ getInsertStmt (tabName, columns) {
const colList = `"${columns.join('", "')}"`
const params = columns.map(() => '?').join(', ')
- return `INSERT INTO csv_import (${colList}) VALUES (${params});`
+ return `INSERT INTO "${tabName}" (${colList}) VALUES (${params});`
},
- getCreateStatement (columns, values) {
- let result = 'CREATE table csv_import('
+ getCreateStatement (tabName, columns, values) {
+ let result = `CREATE table "${tabName}"(`
columns.forEach((col, index) => {
// Get the first row of values to determine types
const value = values[0][index]
@@ -40,5 +42,49 @@ export default {
})
result = result.replace(/,\s$/, ');')
return result
+ },
+
+ getAst (sql) {
+ // There is a bug is sqlite-parser
+ // It throws an error if tokenizer has an arguments:
+ // https://github.com/codeschool/sqlite-parser/issues/59
+ const fixedSql = sql
+ .replace(/(tokenize=[^,]+)"tokenchars=.+?"/, '$1')
+ .replace(/(tokenize=[^,]+)"remove_diacritics=.+?"/, '$1')
+ .replace(/(tokenize=[^,]+)"separators=.+?"/, '$1')
+ .replace(/tokenize=.+?(,|\))/, 'tokenize=unicode61$1')
+
+ return sqliteParser(fixedSql)
+ },
+
+ /*
+ * Return an array of columns with name and type. E.g.:
+ * [
+ * { name: 'id', type: 'INTEGER' },
+ * { name: 'title', type: 'NVARCHAR(30)' },
+ * ]
+ */
+ getColumns (sql) {
+ const columns = []
+ const ast = this.getAst(sql)
+
+ const columnDefinition = ast.statement[0].format === 'table'
+ ? ast.statement[0].definition
+ : ast.statement[0].result.args.expression // virtual table
+
+ columnDefinition.forEach(item => {
+ if (item.variant === 'column' && ['identifier', 'definition'].includes(item.type)) {
+ let type = item.datatype ? item.datatype.variant : 'N/A'
+ if (item.datatype && item.datatype.args) {
+ type = type + '(' + item.datatype.args.expression[0].value
+ if (item.datatype.args.expression.length === 2) {
+ type = type + ', ' + item.datatype.args.expression[1].value
+ }
+ type = type + ')'
+ }
+ columns.push({ name: item.name, type: type })
+ }
+ })
+ return columns
}
}
diff --git a/src/lib/database/_worker.js b/src/lib/database/_worker.js
index 057fde8..7aa3877 100644
--- a/src/lib/database/_worker.js
+++ b/src/lib/database/_worker.js
@@ -8,10 +8,18 @@ function processMsg (sql) {
switch (data && data.action) {
case 'open':
return sql.open(data.buffer)
+ case 'reopen':
+ return sql.open(sql.export())
case 'exec':
return sql.exec(data.sql, data.params)
case 'import':
- return sql.import(data.columns, data.values, data.progressCounterId, postMessage)
+ return sql.import(
+ data.tabName,
+ data.columns,
+ data.values,
+ data.progressCounterId,
+ postMessage
+ )
case 'export':
return sql.export()
case 'close':
diff --git a/src/lib/database/index.js b/src/lib/database/index.js
index b6d8570..00006d8 100644
--- a/src/lib/database/index.js
+++ b/src/lib/database/index.js
@@ -1,4 +1,4 @@
-import sqliteParser from 'sqlite-parser'
+import stms from './_statements'
import fu from '@/lib/utils/fileIo'
// We can import workers like so because of worker-loader:
// https://webpack.js.org/loaders/worker-loader/
@@ -20,6 +20,8 @@ export default {
let progressCounterIds = 0
class Database {
constructor (worker) {
+ this.dbName = null
+ this.schema = null
this.worker = worker
this.pw = new PromiseWorker(worker)
@@ -50,19 +52,20 @@ class Database {
delete this.importProgresses[id]
}
- async importDb (name, data, progressCounterId) {
+ async addTableFromCsv (tabName, data, progressCounterId) {
const result = await this.pw.postMessage({
action: 'import',
columns: data.columns,
values: data.values,
- progressCounterId
+ progressCounterId,
+ tabName
})
if (result.error) {
throw new Error(result.error)
}
-
- return await this.getSchema(name)
+ this.dbName = this.dbName || 'database'
+ this.refreshSchema()
}
async loadDb (file) {
@@ -73,11 +76,11 @@ class Database {
throw new Error(res.error)
}
- const dbName = file ? file.name.replace(/\.[^.]+$/, '') : 'database'
- return this.getSchema(dbName)
+ this.dbName = file ? fu.getFileName(file) : 'database'
+ this.refreshSchema()
}
- async getSchema (name) {
+ async refreshSchema () {
const getSchemaSql = `
SELECT name, sql
FROM sqlite_master
@@ -90,19 +93,17 @@ class Database {
result.values.forEach(item => {
parsedSchema.push({
name: item[0],
- columns: getColumns(item[1])
+ columns: stms.getColumns(item[1])
})
})
}
- // Return db name and schema
- return {
- dbName: name,
- schema: parsedSchema
- }
+ // Refresh schema
+ this.schema = parsedSchema
}
async execute (commands) {
+ await this.pw.postMessage({ action: 'reopen' })
const results = await this.pw.postMessage({ action: 'exec', sql: commands })
if (results.error) {
@@ -120,48 +121,27 @@ class Database {
}
fu.exportToFile(data, fileName)
}
-}
-function getAst (sql) {
- // There is a bug is sqlite-parser
- // It throws an error if tokenizer has an arguments:
- // https://github.com/codeschool/sqlite-parser/issues/59
- const fixedSql = sql
- .replace(/(tokenize=[^,]+)"tokenchars=.+?"/, '$1')
- .replace(/(tokenize=[^,]+)"remove_diacritics=.+?"/, '$1')
- .replace(/(tokenize=[^,]+)"separators=.+?"/, '$1')
- .replace(/tokenize=.+?(,|\))/, 'tokenize=unicode61$1')
-
- return sqliteParser(fixedSql)
-}
+ async validateTableName (name) {
+ if (name.startsWith('sqlite_')) {
+ throw new Error("Table name can't start with sqlite_")
+ }
-/*
- * Return an array of columns with name and type. E.g.:
- * [
- * { name: 'id', type: 'INTEGER' },
- * { name: 'title', type: 'NVARCHAR(30)' },
- * ]
-*/
-function getColumns (sql) {
- const columns = []
- const ast = getAst(sql)
-
- const columnDefinition = ast.statement[0].format === 'table'
- ? ast.statement[0].definition
- : ast.statement[0].result.args.expression // virtual table
-
- columnDefinition.forEach(item => {
- if (item.variant === 'column' && ['identifier', 'definition'].includes(item.type)) {
- let type = item.datatype ? item.datatype.variant : 'N/A'
- if (item.datatype && item.datatype.args) {
- type = type + '(' + item.datatype.args.expression[0].value
- if (item.datatype.args.expression.length === 2) {
- type = type + ', ' + item.datatype.args.expression[1].value
- }
- type = type + ')'
- }
- columns.push({ name: item.name, type: type })
+ if (/[^\w]/.test(name)) {
+ throw new Error('Table name can contain only letters, digits and underscores')
}
- })
- return columns
+
+ if (/^(\d)/.test(name)) {
+ throw new Error("Table name can't start with a digit")
+ }
+
+ await this.execute(`BEGIN; CREATE TABLE "${name}"(id); ROLLBACK;`)
+ }
+
+ sanitizeTableName (tabName) {
+ return tabName
+ .replace(/[^\w]/g, '_') // replace everything that is not letter, digit or _ with _
+ .replace(/^(\d)/, '_$1') // add _ at beginning if starts with digit
+ .replace(/_{2,}/g, '_') // replace multiple _ with one _
+ }
}
diff --git a/src/lib/utils/fileIo.js b/src/lib/utils/fileIo.js
index 28802ff..5c227be 100644
--- a/src/lib/utils/fileIo.js
+++ b/src/lib/utils/fileIo.js
@@ -6,6 +6,10 @@ export default {
: /\.(db|sqlite(3)?)+$/.test(file.name)
},
+ getFileName (file) {
+ return file.name.replace(/\.[^.]+$/, '')
+ },
+
exportToFile (str, fileName, type = 'octet/stream') {
// Create downloader
const downloader = document.createElement('a')
diff --git a/src/lib/utils/time.js b/src/lib/utils/time.js
index 3d276b1..5858d15 100644
--- a/src/lib/utils/time.js
+++ b/src/lib/utils/time.js
@@ -3,5 +3,13 @@ export default {
const diff = end.getTime() - start.getTime()
const seconds = diff / 1000
return seconds.toFixed(3) + 's'
+ },
+
+ debounce (func, ms) {
+ let timeout
+ return function () {
+ clearTimeout(timeout)
+ timeout = setTimeout(() => func.apply(this, arguments), ms)
+ }
}
}
diff --git a/src/store/mutations.js b/src/store/mutations.js
index 51f2e35..adb8e70 100644
--- a/src/store/mutations.js
+++ b/src/store/mutations.js
@@ -7,10 +7,6 @@ export default {
}
state.db = db
},
- saveSchema (state, { dbName, schema }) {
- state.dbName = dbName
- state.schema = schema
- },
updateTab (state, { index, name, id, query, chart, isUnsaved }) {
const tab = state.tabs[index]
diff --git a/src/store/state.js b/src/store/state.js
index 2c2754a..5769379 100644
--- a/src/store/state.js
+++ b/src/store/state.js
@@ -1,7 +1,4 @@
export default {
- schema: null,
- dbFile: null,
- dbName: null,
tabs: [],
currentTab: null,
currentTabId: null,
diff --git a/src/views/Main/Editor/Schema/index.vue b/src/views/Main/Editor/Schema/index.vue
index a863bee..bf0588a 100644
--- a/src/views/Main/Editor/Schema/index.vue
+++ b/src/views/Main/Editor/Schema/index.vue
@@ -10,6 +10,7 @@
+
+
+
+