You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

339 lines
12 KiB

import React from 'react';
export const DBTypeEnum = {
ALASQL_SQLITE: 1,
ALASQL_INDEXEDDB: 2,
ALASQL_NATIVE: 3,
SQLJS_SQLITE: 4
};
export const DBSourceEnum = {
ATTACHFILE: 1,
CREATE: 2
}
export function alasql_async_queries(alasql_object, queries) {
var p = Promise.resolve(null);
for (let i = 0; i < queries.length; i++) {
p = p.then(() => {
return alasql_object.promise(queries[i]);
});
}
return p;
}
export class DB {
state = {
db_type: false,
db_object: false,
db_name: false,
}
// TODO random database names
constructor(obj, name, type, library) {
this.state.db_object = obj;
this.state.db_name = name;
this.state.db_type = type;
}
// Perform a chain of asynchronous queries, each one waiting for the last before
// starting. Not supported on all database types.
queries_async(s) {
if (this.state.db_type === DBTypeEnum.ALASQL_SQLITE ||
this.state.db_type === DBTypeEnum.ALASQL_INDEXEDDB ||
this.state.db_type === DBTypeEnum.ALASQL_NATIVE) {
var self = this;
return alasql_async_queries(self.state.db_object, s);
}
throw new Error("Unsupported database type for async operations: " + this.state.db_type);
}
// Perform a series of queries sequentially, then return the last result.
// Note that synchronous operation is not supported for all database types.
queries_sync(s) {
if (this.state.db_type === DBTypeEnum.ALASQL_SQLITE ||
this.state.db_type === DBTypeEnum.ALASQL_NATIVE) {
for (var i = 0; i < (s.length - 1); i++) {
this.state.db_object(s[i]);
}
return this.state.db_object(s[s.length - 1]);
}
if (this.state.db_type === DBTypeEnum.SQLJS_SQLITE) {
for (var i = 0; i < (s.length - 1); i++) {
this.state.db_object.exec(s[i]);
}
return this.state.db_object.exec(s[s.length - 1]);
}
throw new Error("Unsupported database type for sync operations: " + this.state.db_type);
}
migrate_async(db_type, db_name) {
var self = this;
if (this.state.db_type === DBTypeEnum.ALASQL_SQLITE && db_type === DBTypeEnum.ALASQL_INDEXEDDB) {
return new Promise(function (resolve, reject) {
var tables = self.queries_sync(["SHOW TABLES;"]);
var queries = [];
queries.push("DROP INDEXEDDB DATABASE IF EXISTS " + db_name + ";");
queries.push("CREATE INDEXEDDB DATABASE IF NOT EXISTS " + db_name + ";");
queries.push("ATTACH INDEXEDDB DATABASE " + db_name + ";");
tables.forEach(elem => {
queries.push("CREATE TABLE " + db_name + "." + elem.tableid + ";");
queries.push("SELECT * INTO " + db_name + "." + elem.tableid +
" FROM " + self.state.db_name + "." + elem.tableid + ";");
});
queries.push("DETACH DATABASE " + self.state.db_name + ";");
queries.push("USE " + db_name + ";");
self.queries_async(queries).then(() => {
self.state.db_type = db_type;
self.state.db_name = db_name;
resolve();
});
});
} else if (this.state.db_type === DBTypeEnum.ALASQL_SQLITE && db_type === DBTypeEnum.ALASQL_NATIVE) {
return new Promise(function (resolve, reject) {
var tables = self.queries_sync(["SHOW TABLES;"]);
var queries = [];
queries.push("DROP DATABASE IF EXISTS " + db_name + ";");
queries.push("CREATE DATABASE IF NOT EXISTS " + db_name + ";");
tables.forEach(elem => {
queries.push("CREATE TABLE " + db_name + "." + elem.tableid + ";");
queries.push("SELECT * INTO " + db_name + "." + elem.tableid +
" FROM " + self.state.db_name + "." + elem.tableid + ";");
});
queries.push("DETACH DATABASE " + self.state.db_name + ";");
queries.push("USE " + db_name + ";");
self.queries_async(queries).then(() => {
self.state.db_type = db_type;
self.state.db_name = db_name;
resolve();
});
});
} else if (this.state.db_type === DBTypeEnum.SQLJS_SQLITE && db_type === DBTypeEnum.ALASQL_NATIVE) {
return new Promise(function (resolve, reject) {
var tables = self.queries_sync(["SELECT * FROM sqlite_master WHERE type='table';"])[0].values;
// Get all table definition lines.
var table_definitions = [];
var table_names = [];
tables.forEach(elem => {
table_definitions.push(elem[4]);
table_names.push(elem[1]);
});
// We want to create all these tables in the AlaSQL database as well. However, that will require
// us to escape the table names because AlaSQL may break on colums with names equal to keywords
// (see https://github.com/agershun/alasql/issues/1155).
// Here we take a dirty shortcut: explicitly escape all occurrences of "value", "query" and "matrix".
var new_table_definitions = [];
table_definitions.forEach(elem => {
var newelem = elem
.replace(/value/g, "`value`")
.replace(/query/g, "`query`")
.replace(/matrix/g, "`matrix`");
new_table_definitions.push(newelem);
});
var asql = require('alasql');
var queries = [];
queries.push("CREATE DATABASE IF NOT EXISTS " + db_name + ";");
queries.push("USE " + db_name + ";");
new_table_definitions.forEach(elem => {
queries.push(elem); // Creates the table
});
alasql_async_queries(asql, queries)
.then(() => {
// Fill the tables by passing through a JS dictionary object.
table_names.forEach(elem => {
var res = self.queries_sync(["SELECT * FROM " + elem + ";"])[0];
if (res) {
var structured = [];
res.values.forEach(vals => {
var row = {};
var i = 0;
res.columns.forEach(column => {
row[column] = vals[i];
i++;
})
structured.push(row);
});
asql("SELECT * INTO " + elem + " FROM ?", [structured]);
}
});
})
.then(() => {
self.state.db_type = db_type;
self.state.db_name = db_name;
self.state.db_object = asql;
resolve();
});
});
}
throw new Error("Unsupported copy creation from db_type " + this.state.db_type + " to " + db_type);
}
}
// TODO random database names
function fetch_db_from_sqlite(filename, db_name) {
return new Promise(function (resolve, reject) {
var initSqlJs = require('sql.js');
initSqlJs({ locateFile: filename => `/sql.js/dist/${filename}` })
.then(SQL => {
var asql = require('alasql');
asql.promise("ATTACH SQLITE DATABASE " + db_name + "(\"" + filename + "\");")
.then(res => { asql.promise("USE " + db_name + ";"); })
.then(res => {
var imported_db = new DB(asql, db_name, DBTypeEnum.ALASQL_SQLITE);
resolve(imported_db);
});
});
});
}
function fetch_sqljs_db_from_sqlite(filename, db_name) {
return new Promise(function (resolve, reject) {
var initSqlJs = require('sql.js');
initSqlJs({ locateFile: filename => `/sql.js/dist/${filename}` })
.then(SQL => {
fetch(filename)
.then(res => {
if (!res.ok) {
throw new Error(res.status);
}
return res.arrayBuffer();
})
.then(data => {
var array = new Uint8Array(data);
var sqljs_db = new SQL.Database(array);
var imported_db = new DB(sqljs_db, db_name, DBTypeEnum.SQLJS_SQLITE);
resolve(imported_db);
})
});
});
}
function create_indexed_db(name) {
return new Promise(function (resolve, reject) {
var asql = require('alasql');
asql.promise("DROP INDEXEDDB DATABASE " + name + ";")
.then(res => { asql.promise("CREATE INDEXEDDB DATABASE IF NOT EXISTS " + name + ";"); })
.then(res => { asql.promise("ATTACH INDEXEDDB DATABASE " + name + ";"); })
//.then(res => { asql.promise("USE " + name + ";"); }) // TODO why does this break things?
.then(res => {
var created_db = new DB(asql, name, DBTypeEnum.ALASQL_INDEXEDDB);
resolve(created_db);
});
});
}
function create_native_db(name) {
return new Promise(function (resolve, reject) {
var asql = require('alasql');
asql.promise("DROP DATABASE " + name + ";")
.then(res => { asql.promise("CREATE DATABASE IF NOT EXISTS " + name + ";"); })
.then(res => { asql.promise("ATTACH DATABASE " + name + ";"); })
//.then(res => { asql.promise("USE " + name + ";"); }) // TODO why does this break things?
.then(res => {
var created_db = new DB(asql, name, DBTypeEnum.ALASQL_INDEXEDDB);
resolve(created_db);
});
});
}
export class ProvideDB extends React.Component {
state = {
loading: true,
error: false,
done: false,
db: false
};
componentDidMount() {
if (this.props.db_source_type === DBTypeEnum.ALASQL_SQLITE && this.props.db_source === DBSourceEnum.ATTACHFILE) {
fetch_db_from_sqlite(this.props.db_file, this.props.db_source_name)
.then(db => {
if (this.props.db_target_type !== this.props.db_source_type) {
db.migrate_async(this.props.db_target_type, this.props.db_target_name)
.then(() => { this.setState({ loading: false, done: true, db: db }) });
} else { this.setState({ loading: false, done: true, db: db }) };
})
.catch(error => this.setState({ loading: false, done: false, error }));
} else if (this.props.db_source_type === DBTypeEnum.ALASQL_INDEXEDDB && this.props.db_source === DBSourceEnum.CREATE) {
create_indexed_db(this.props.db_source_name)
.then(db => {
if (this.props.db_target_type !== this.props.db_source_type) {
db.migrate_async(this.props.db_target_type, this.props.db_target_name)
.then(() => { this.setState({ loading: false, done: true, db: db }) });
} else { this.setState({ loading: false, done: true, db: db }) };
});
} else if (this.props.db_source_type === DBTypeEnum.ALASQL_NATIVE && this.props.db_source === DBSourceEnum.CREATE) {
create_native_db(this.props.db_source_name)
.then(db => {
if (this.props.db_target_type !== this.props.db_source_type) {
db.migrate_async(this.props.db_target_type, this.props.db_target_name)
.then(() => { this.setState({ loading: false, done: true, db: db }) });
} else { this.setState({ loading: false, done: true, db: db }) };
});
} else if (this.props.db_source_type === DBTypeEnum.SQLJS_SQLITE && this.props.db_source === DBSourceEnum.ATTACHFILE) {
fetch_sqljs_db_from_sqlite(this.props.db_file, this.props.db_source_name)
.then(db => {
if (this.props.db_target_type !== this.props.db_source_type) {
db.migrate_async(this.props.db_target_type, this.props.db_target_name)
.then(() => { this.setState({ loading: false, done: true, db: db }) });
} else { this.setState({ loading: false, done: true, db: db }) };
})
.catch(error => this.setState({ loading: false, done: false, error }));
} else {
throw new Error("Unsupported ProvideDB configuration: from source " + this.props.db_source +
" with source type " + this.props.db_source_type + " to target type " + this.props.db_target_type + ".");
}
}
render() {
return this.props.children(this.state);
}
}
export class DBQueryBar extends React.Component {
onChangeHandler = (event) => {
if(this.props.onChange) { this.props.onChange(event.target.value) };
}
render() {
return (
<form>
<input type="text" onChange={this.onChangeHandler} />
</form>
);
}
}
export class DBQueryConsole extends React.Component {
state = {
processing: false,
result: false,
query: "",
}
onQueryChangeHandler = query => { this.setState({ query: query }); }
onQuerySubmitHandler = () => {
this.setState({ processing: true, result: false });
this.props.database.queries_async([this.state.query])
.then(result => {
this.setState({ processing: false, result: JSON.stringify(result) });
});
}
render() {
return (
<>
<div>
<DBQueryBar onChange={this.onQueryChangeHandler} />
<button onClick={this.onQuerySubmitHandler}>Submit</button>
<p>Result:</p>
{this.state.result && <p>{this.state.result}</p>}
</div>
</>
);
}
}