1
0
mirror of https://github.com/lana-k/sqliteviz.git synced 2025-12-10 03:58:54 +08:00
This commit is contained in:
lana-k
2025-03-20 22:04:15 +01:00
parent 5e2b34a856
commit 0c1b91ab2f
146 changed files with 3317 additions and 2438 deletions

View File

@@ -2,7 +2,7 @@ import * as dereference from 'react-chart-editor/lib/lib/dereference'
import plotly from 'plotly.js'
import { nanoid } from 'nanoid'
export function getOptionsFromDataSources (dataSources) {
export function getOptionsFromDataSources(dataSources) {
if (!dataSources) {
return []
}
@@ -13,7 +13,7 @@ export function getOptionsFromDataSources (dataSources) {
}))
}
export function getOptionsForSave (state, dataSources) {
export function getOptionsForSave(state, dataSources) {
// we don't need to save the data, only settings
// so we modify state.data using dereference
const stateCopy = JSON.parse(JSON.stringify(state))
@@ -25,7 +25,7 @@ export function getOptionsForSave (state, dataSources) {
return stateCopy
}
export async function getImageDataUrl (element, type) {
export async function getImageDataUrl(element, type) {
const chartElement = element.querySelector('.js-plotly-plot')
return await plotly.toImage(chartElement, {
format: type,
@@ -34,7 +34,7 @@ export async function getImageDataUrl (element, type) {
})
}
export function getChartData (element) {
export function getChartData(element) {
const chartElement = element.querySelector('.js-plotly-plot')
return {
data: chartElement.data,
@@ -42,7 +42,7 @@ export function getChartData (element) {
}
}
export function getHtml (options) {
export function getHtml(options) {
const chartId = nanoid()
return `
<script src="https://cdn.plot.ly/plotly-latest.js" charset="UTF-8"></script>

View File

@@ -7,7 +7,7 @@ const hintsByCode = {
}
export default {
getResult (source, columns) {
getResult(source, columns) {
const result = {
columns: columns || []
}
@@ -52,7 +52,7 @@ export default {
return result
},
prepareForExport (resultSet) {
prepareForExport(resultSet) {
const columns = resultSet.columns
const rowCount = resultSet.values[columns[0]].length
const result = {
@@ -61,13 +61,15 @@ export default {
}
for (let rowNumber = 0; rowNumber < rowCount; rowNumber++) {
result.data.push(columns.map(column => resultSet.values[column][rowNumber]))
result.data.push(
columns.map(column => resultSet.values[column][rowNumber])
)
}
return result
},
parse (file, config = {}) {
parse(file, config = {}) {
return new Promise((resolve, reject) => {
const defaultConfig = {
delimiter: '', // auto-detect
@@ -122,7 +124,7 @@ export default {
})
},
serialize (resultSet) {
serialize(resultSet) {
return Papa.unparse(this.prepareForExport(resultSet), { delimiter: '\t' })
}
}

View File

@@ -5,9 +5,11 @@ import wasmUrl from 'sql.js/dist/sql-wasm.wasm?url'
let SQL = null
const sqlModuleReady = initSqlJs({
locateFile: () => wasmUrl
}).then(sqlModule => { SQL = sqlModule })
}).then(sqlModule => {
SQL = sqlModule
})
function _getDataSourcesFromSqlResult (sqlResult) {
function _getDataSourcesFromSqlResult(sqlResult) {
if (!sqlResult) {
return {}
}
@@ -19,31 +21,30 @@ function _getDataSourcesFromSqlResult (sqlResult) {
}
export default class Sql {
constructor () {
constructor() {
this.db = null
}
static build () {
return sqlModuleReady
.then(() => {
return new Sql()
})
static build() {
return sqlModuleReady.then(() => {
return new Sql()
})
}
createDb (buffer) {
createDb(buffer) {
if (this.db != null) this.db.close()
this.db = new SQL.Database(buffer)
return this.db
}
open (buffer) {
open(buffer) {
this.createDb(buffer && new Uint8Array(buffer))
return {
ready: true
}
}
exec (sql, params) {
exec(sql, params) {
if (this.db === null) {
this.createDb()
}
@@ -59,7 +60,7 @@ export default class Sql {
})
}
import (tabName, data, progressCounterId, progressCallback, chunkSize = 1500) {
import(tabName, data, progressCounterId, progressCallback, chunkSize = 1500) {
if (this.db === null) {
this.createDb()
}
@@ -80,7 +81,10 @@ export default class Sql {
}
this.db.exec('COMMIT')
count++
progressCallback({ progress: 100 * (count / chunksAmount), id: progressCounterId })
progressCallback({
progress: 100 * (count / chunksAmount),
id: progressCounterId
})
}
return {
@@ -88,11 +92,11 @@ export default class Sql {
}
}
export () {
export() {
return this.db.export()
}
close () {
close() {
if (this.db) {
this.db.close()
}

View File

@@ -1,8 +1,10 @@
export default {
* generateChunks (data, size) {
*generateChunks(data, size) {
const matrix = Object.keys(data).map(col => data[col])
const [row] = matrix
const transposedMatrix = row.map((value, column) => matrix.map(row => row[column]))
const transposedMatrix = row.map((value, column) =>
matrix.map(row => row[column])
)
const count = Math.ceil(transposedMatrix.length / size)
@@ -13,13 +15,13 @@ export default {
}
},
getInsertStmt (tabName, columns) {
getInsertStmt(tabName, columns) {
const colList = `"${columns.join('", "')}"`
const params = columns.map(() => '?').join(', ')
return `INSERT INTO "${tabName}" (${colList}) VALUES (${params});`
},
getCreateStatement (tabName, data) {
getCreateStatement(tabName, data) {
let result = `CREATE table "${tabName}"(`
for (const col in data) {
// Get the first row of values to determine types
@@ -38,7 +40,8 @@ export default {
type = 'TEXT'
break
}
default: type = 'TEXT'
default:
type = 'TEXT'
}
result += `"${col}" ${type}, `
}

View File

@@ -3,7 +3,7 @@ import Sql from './_sql'
const sqlReady = Sql.build()
function processMsg (sql) {
function processMsg(sql) {
const data = this
switch (data && data.action) {
case 'open':
@@ -28,14 +28,12 @@ function processMsg (sql) {
}
}
function onError (error) {
function onError(error) {
return {
error: error.message
}
}
registerPromiseWorker(data => {
return sqlReady
.then(processMsg.bind(data))
.catch(onError)
return sqlReady.then(processMsg.bind(data)).catch(onError)
})

View File

@@ -6,11 +6,10 @@ import PromiseWorker from 'promise-worker'
import events from '@/lib/utils/events'
function getNewDatabase () {
const worker = new Worker(
new URL('./_worker.js', import.meta.url),
{ type: 'module' }
)
function getNewDatabase() {
const worker = new Worker(new URL('./_worker.js', import.meta.url), {
type: 'module'
})
return new Database(worker)
}
@@ -20,7 +19,7 @@ export default {
let progressCounterIds = 0
class Database {
constructor (worker) {
constructor(worker) {
this.dbName = null
this.schema = null
this.worker = worker
@@ -31,29 +30,33 @@ class Database {
const progress = e.data.progress
if (progress !== undefined) {
const id = e.data.id
this.importProgresses[id].dispatchEvent(new CustomEvent('progress', {
detail: progress
}))
this.importProgresses[id].dispatchEvent(
new CustomEvent('progress', {
detail: progress
})
)
}
})
}
shutDown () {
shutDown() {
this.worker.terminate()
}
createProgressCounter (callback) {
createProgressCounter(callback) {
const id = progressCounterIds++
this.importProgresses[id] = new EventTarget()
this.importProgresses[id].addEventListener('progress', e => { callback(e.detail) })
this.importProgresses[id].addEventListener('progress', e => {
callback(e.detail)
})
return id
}
deleteProgressCounter (id) {
deleteProgressCounter(id) {
delete this.importProgresses[id]
}
async addTableFromCsv (tabName, data, progressCounterId) {
async addTableFromCsv(tabName, data, progressCounterId) {
const result = await this.pw.postMessage({
action: 'import',
data,
@@ -68,9 +71,12 @@ class Database {
this.refreshSchema()
}
async loadDb (file) {
async loadDb(file) {
const fileContent = file ? await fu.readAsArrayBuffer(file) : null
const res = await this.pw.postMessage({ action: 'open', buffer: fileContent })
const res = await this.pw.postMessage({
action: 'open',
buffer: fileContent
})
if (res.error) {
throw new Error(res.error)
@@ -85,7 +91,7 @@ class Database {
})
}
async refreshSchema () {
async refreshSchema() {
const getSchemaSql = `
WITH columns as (
SELECT
@@ -103,7 +109,7 @@ class Database {
this.schema = JSON.parse(result.values.objects[0])
}
async execute (commands) {
async execute(commands) {
await this.pw.postMessage({ action: 'reopen' })
const results = await this.pw.postMessage({ action: 'exec', sql: commands })
@@ -114,7 +120,7 @@ class Database {
return results[results.length - 1]
}
async export (fileName) {
async export(fileName) {
const data = await this.pw.postMessage({ action: 'export' })
if (data.error) {
@@ -124,13 +130,15 @@ class Database {
events.send('database.export', data.byteLength, { to: 'sqlite' })
}
async validateTableName (name) {
async validateTableName(name) {
if (name.startsWith('sqlite_')) {
throw new Error("Table name can't start with sqlite_")
}
if (/[^\w]/.test(name)) {
throw new Error('Table name can contain only letters, digits and underscores')
throw new Error(
'Table name can contain only letters, digits and underscores'
)
}
if (/^(\d)/.test(name)) {
@@ -140,7 +148,7 @@ class Database {
await this.execute(`BEGIN; CREATE TABLE "${name}"(id); ROLLBACK;`)
}
sanitizeTableName (tabName) {
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

View File

@@ -1,5 +1,5 @@
export default {
_migrate (installedVersion, inquiries) {
_migrate(installedVersion, inquiries) {
if (installedVersion === 1) {
inquiries.forEach(inquire => {
inquire.viewType = 'chart'

View File

@@ -7,7 +7,7 @@ const migrate = migration._migrate
export default {
version: 2,
getStoredInquiries () {
getStoredInquiries() {
let myInquiries = JSON.parse(localStorage.getItem('myInquiries'))
if (!myInquiries) {
const oldInquiries = localStorage.getItem('myQueries')
@@ -22,7 +22,7 @@ export default {
return (myInquiries && myInquiries.inquiries) || []
},
duplicateInquiry (baseInquiry) {
duplicateInquiry(baseInquiry) {
const newInquiry = JSON.parse(JSON.stringify(baseInquiry))
newInquiry.name = newInquiry.name + ' Copy'
newInquiry.id = nanoid()
@@ -32,21 +32,28 @@ export default {
return newInquiry
},
isTabNeedName (inquiryTab) {
isTabNeedName(inquiryTab) {
return inquiryTab.isPredefined || !inquiryTab.name
},
updateStorage (inquiries) {
localStorage.setItem('myInquiries', JSON.stringify({ version: this.version, inquiries }))
updateStorage(inquiries) {
localStorage.setItem(
'myInquiries',
JSON.stringify({ version: this.version, inquiries })
)
},
serialiseInquiries (inquiryList) {
serialiseInquiries(inquiryList) {
const preparedData = JSON.parse(JSON.stringify(inquiryList))
preparedData.forEach(inquiry => delete inquiry.isPredefined)
return JSON.stringify({ version: this.version, inquiries: preparedData }, null, 4)
return JSON.stringify(
{ version: this.version, inquiries: preparedData },
null,
4
)
},
deserialiseInquiries (str) {
deserialiseInquiries(str) {
const inquiries = JSON.parse(str)
let inquiryList = []
if (!inquiries.version) {
@@ -59,7 +66,9 @@ export default {
// Generate new ids if they are the same as existing inquiries
inquiryList.forEach(inquiry => {
const allInquiriesIds = this.getStoredInquiries().map(inquiry => inquiry.id)
const allInquiriesIds = this.getStoredInquiries().map(
inquiry => inquiry.id
)
if (allInquiriesIds.includes(inquiry.id)) {
inquiry.id = nanoid()
}
@@ -68,24 +77,23 @@ export default {
return inquiryList
},
importInquiries () {
return fu.importFile()
.then(str => {
const inquires = this.deserialiseInquiries(str)
importInquiries() {
return fu.importFile().then(str => {
const inquires = this.deserialiseInquiries(str)
events.send('inquiry.import', inquires.length)
events.send('inquiry.import', inquires.length)
return inquires
})
return inquires
})
},
export (inquiryList, fileName) {
export(inquiryList, fileName) {
const jsonStr = this.serialiseInquiries(inquiryList)
fu.exportToFile(jsonStr, fileName)
events.send('inquiry.export', inquiryList.length)
},
async readPredefinedInquiries () {
async readPredefinedInquiries() {
const res = await fu.readFile('./inquiries.json')
const data = await res.json()

View File

@@ -3,12 +3,14 @@ import time from '@/lib/utils/time'
import events from '@/lib/utils/events'
export default class Tab {
constructor (state, inquiry = {}) {
constructor(state, inquiry = {}) {
this.id = inquiry.id || nanoid()
this.name = inquiry.id ? inquiry.name : null
this.tempName = inquiry.name || (state.untitledLastIndex
? `Untitled ${state.untitledLastIndex}`
: 'Untitled')
this.tempName =
inquiry.name ||
(state.untitledLastIndex
? `Untitled ${state.untitledLastIndex}`
: 'Untitled')
this.query = inquiry.query
this.viewOptions = inquiry.viewOptions || undefined
this.isPredefined = inquiry.isPredefined
@@ -28,7 +30,7 @@ export default class Tab {
this.state = state
}
async execute () {
async execute() {
this.isGettingResults = true
this.result = null
this.error = null
@@ -39,7 +41,8 @@ export default class Tab {
this.time = time.getPeriod(start, new Date())
if (this.result && this.result.values) {
events.send('resultset.create',
events.send(
'resultset.create',
this.result.values[this.result.columns[0]].length
)
}

View File

@@ -2,14 +2,14 @@ import Lib from 'plotly.js/src/lib'
import dataUrlToBlob from 'dataurl-to-blob'
export default {
async copyText (str, notifyMessage) {
async copyText(str, notifyMessage) {
await navigator.clipboard.writeText(str)
if (notifyMessage) {
Lib.notifier(notifyMessage, 'long')
}
},
async copyImage (source) {
async copyImage(source) {
if (source instanceof HTMLCanvasElement) {
return this._copyCanvas(source)
} else {
@@ -17,24 +17,29 @@ export default {
}
},
async _copyBlob (blob) {
async _copyBlob(blob) {
await navigator.clipboard.write([
new ClipboardItem({ // eslint-disable-line no-undef
new ClipboardItem({
// eslint-disable-line no-undef
[blob.type]: blob
})
])
},
async _copyFromDataUrl (url) {
async _copyFromDataUrl(url) {
const blob = dataUrlToBlob(url)
await this._copyBlob(blob)
Lib.notifier('Image copied to clipboard successfully', 'long')
},
async _copyCanvas (canvas) {
canvas.toBlob(async (blob) => {
await this._copyBlob(blob)
Lib.notifier('Image copied to clipboard successfully', 'long')
}, 'image/png', 1)
async _copyCanvas(canvas) {
canvas.toBlob(
async blob => {
await this._copyBlob(blob)
Lib.notifier('Image copied to clipboard successfully', 'long')
},
'image/png',
1
)
}
}

View File

@@ -1,5 +1,5 @@
export default {
send (name, value, labels) {
send(name, value, labels) {
const event = new CustomEvent('sqliteviz-app-event', {
detail: {
name,

View File

@@ -1,22 +1,22 @@
export default {
isJSON (file) {
isJSON(file) {
return file && file.type === 'application/json'
},
isNDJSON (file) {
isNDJSON(file) {
return file && file.name.endsWith('.ndjson')
},
isDatabase (file) {
isDatabase(file) {
const dbTypes = ['application/vnd.sqlite3', 'application/x-sqlite3']
return file.type
? dbTypes.includes(file.type)
: /\.(db|sqlite(3)?)+$/.test(file.name)
},
getFileName (file) {
getFileName(file) {
return file.name.replace(/\.[^.]+$/, '')
},
downloadFromUrl (url, fileName) {
downloadFromUrl(url, fileName) {
// Create downloader
const downloader = document.createElement('a')
downloader.href = url
@@ -29,7 +29,7 @@ export default {
URL.revokeObjectURL(url)
},
async exportToFile (str, fileName, type = 'octet/stream') {
async exportToFile(str, fileName, type = 'octet/stream') {
const blob = new Blob([str], { type })
const url = URL.createObjectURL(blob)
this.downloadFromUrl(url, fileName)
@@ -40,7 +40,7 @@ export default {
* it will be an unsettled promise. But it's grabbed by
* the garbage collector (tested with FinalizationRegistry).
*/
getFileFromUser (type) {
getFileFromUser(type) {
return new Promise(resolve => {
const uploader = document.createElement('input')
@@ -56,14 +56,13 @@ export default {
})
},
importFile () {
return this.getFileFromUser('.json')
.then(file => {
return this.getFileContent(file)
})
importFile() {
return this.getFileFromUser('.json').then(file => {
return this.getFileContent(file)
})
},
getFileContent (file) {
getFileContent(file) {
const reader = new FileReader()
return new Promise(resolve => {
reader.onload = e => resolve(e.target.result)
@@ -71,11 +70,11 @@ export default {
})
},
readFile (path) {
readFile(path) {
return fetch(path)
},
readAsArrayBuffer (file) {
readAsArrayBuffer(file) {
const fileReader = new FileReader()
return new Promise((resolve, reject) => {

View File

@@ -1,11 +1,11 @@
export default {
getPeriod (start, end) {
getPeriod(start, end) {
const diff = end.getTime() - start.getTime()
const seconds = diff / 1000
return seconds.toFixed(3) + 's'
},
debounce (func, ms) {
debounce(func, ms) {
let timeout
return function () {
clearTimeout(timeout)
@@ -13,9 +13,11 @@ export default {
}
},
sleep (ms) {
sleep(ms) {
return new Promise(resolve => {
setTimeout(() => { resolve() }, ms)
setTimeout(() => {
resolve()
}, ms)
})
}
}