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