@freckleface/golembase-tables
Version:
SQL-like table implementation on top of Golem Base
242 lines (241 loc) • 14.6 kB
JavaScript
import { transformAnnotationsToPOJO } from "@freckleface/golembase-js-transformations";
import { createClient, Tagged } from "golem-base-sdk";
import { buildFkQueries, CreateTableObjToGBCreate, filterObjectBySelect, groupSqlIntoBatches, InsertObjToGBCreate, parseForeignKeyString, parseSql, SQLCreateTableToGBCreate, SQLInsertToGBCreate } from "./sql_to_object.js";
import { readFileSync } from "fs";
/**
* TODO:
* Convert all tablenames to lowercase, every time. The system is case-sensitive and if the users do create table departments and SELECT * FROM DEPARTMENTS, they won't match up.
* Check if table name already exists; if so, update rather than create (or perhaps issue exception?)
* Need to get lists of values like the older indexer
* doSQL needs a path to the private key file, or by default use the ~/.config business. Presently it's just looking in the working directory.
* SELECT seems to be ignoring the app name? When I run it with a new app name, I'm getting twice the departments etc, including other app names.
*/
const encoder = new TextEncoder();
const decoder = new TextDecoder();
const keyBytes = readFileSync('./private.key');
const key = new Tagged("privatekey", keyBytes);
export const client = await createClient(1337, key, 'http://localhost:8545', 'ws://localhost:8545');
export const doSQL = async (app, big_sql) => {
const delimiterRegex = /\s*;\s*/;
const trailingSemicolonRegex = /\s*;\s*$/; // Regex for removing final semicolon
// Remove whitespace at beginning and end via trim() and then remove final semicolon if present and then split on semicolons with optional whitespace around the semicolons
const sqls = big_sql.trim().replace(trailingSemicolonRegex, '').split(delimiterRegex);
console.log(sqls);
// Parse each one and get back a data block
let parsed = [];
for (let sql of sqls) {
let parsed_next = parseSql(sql);
if (parsed_next) {
parsed.push(parsed_next);
}
}
// Group together contiguous CREATE TABLES and INSERTS, as we can send them across in a single transaction
let batches = groupSqlIntoBatches(parsed);
console.log(batches);
// Now go through each batch and build the golem stuff!
console.log('WORKING THROUGH BATCHES...');
let output = [];
for (let batch of batches) {
if (batch.length > 0) { // This should always be true, but just in case
// Check the first item in the batch. If it's create-table or insert, we can combine all of these into a single transaction.
if (batch[0].sqlType == 'create table' || batch[0].sqlType == 'insert') {
let golemBatch = [];
for (let createOrInsert of batch) {
if (createOrInsert.sqlType == 'create table') {
golemBatch.push(CreateTableObjToGBCreate(app, createOrInsert));
output.push(`TABLE CREATED: ${createOrInsert.data?.tablename}`);
}
else {
golemBatch.push(InsertObjToGBCreate(app, createOrInsert));
output.push(`DATA INSERTED: ${createOrInsert.data?.tablename}`);
}
}
//console.log(util.inspect(golemBatch, { depth: null }));
// TODO: Run them NOW as a single transaction
const receipts = await client.createEntities(golemBatch);
console.log(receipts);
}
else {
// There is always only one select per batch, so grab it
let selectObj = batch[0].data;
const select_tables = await client.queryEntities(`app="${app}" && type="table" && tablename="${selectObj.tablename}"`);
let FKs = {};
if (select_tables.length > 0) {
// Grab the table's metadata
const table_metadata = await client.getEntityMetaData(select_tables[0]?.entityKey);
for (let pair of table_metadata.stringAnnotations) {
const foundFk = parseForeignKeyString(pair.value);
if (foundFk) {
const keyname = foundFk.localKey;
FKs[keyname] = foundFk;
}
}
}
selectObj.where = `app="${app}" && ${selectObj.where}`;
const result2 = await client.queryEntities(selectObj.where);
for (let item of result2) {
const metadata = await client.getEntityMetaData(item.entityKey);
// Convert to a data object
const obj = transformAnnotationsToPOJO(metadata);
let final = filterObjectBySelect(selectObj.select, obj);
// Now for the foreign key view-as (if present)
const builtFKs = buildFkQueries(final, FKs);
if (builtFKs?.length > 0) {
for (let fk of builtFKs) {
// Query for the foreign key's item
// 1. Query
// 2. Grab metadata
// 3. Convert to POJO
const query_fk = await client.queryEntities(fk.queryString);
// Grab the keyname to use (same as "view as") and store its value locally with the same name.
// Should only return one, but just in case, just grab first
if (query_fk && query_fk.length > 0) {
const fk_metadata = await client.getEntityMetaData(query_fk[0].entityKey);
const fk_pojo = transformAnnotationsToPOJO(fk_metadata);
final[fk.viewKey] = fk_pojo[fk.viewKey];
}
}
}
//console.log('===========RESULTS OF SELECT===========')
//console.log(final);
output.push(JSON.stringify(final));
}
}
}
}
//console.log('SENDING BACK OUTPUT:');
//console.log(output);
return output;
};
export const test1 = async () => {
let creates = [
SQLCreateTableToGBCreate('GOLEM-SQLTEST-v0.1', `CREATE TABLE users (
user_id INTEGER,
username TEXT,
dept_id INTEGER,
building TEXT,
phone_number TEXT,
CONSTRAINT fk__view_as__department_name
FOREIGN KEY (dept_id) REFERENCES departments(dept_id),
INDEX idx_username (username),
INDEX idx_dept_id (dept_id)
)`),
SQLCreateTableToGBCreate('GOLEM-SQLTEST-v0.1', `CREATE TABLE departments (
dept_id INTEGER,
department_name TEXT,
city TEXT,
INDEX idx_dept_id (dept_id),
INDEX idx_department_name (department_name)
)`),
SQLInsertToGBCreate('GOLEM-SQLTEST-v0.1', "INSERT INTO departments (dept_id, department_name, city) values ('ACCT', 'Accounting', 'New York')"),
SQLInsertToGBCreate('GOLEM-SQLTEST-v0.1', "INSERT INTO departments (dept_id, department_name, city) values ('IT', 'Information Technology', 'New York')"),
SQLInsertToGBCreate('GOLEM-SQLTEST-v0.1', "INSERT INTO departments (dept_id, department_name, city) values ('MGT', 'Management', 'Boston')"),
SQLInsertToGBCreate('GOLEM-SQLTEST-v0.1', "INSERT INTO departments (dept_id, department_name, city) values ('HR', 'Human Resources', 'Chicago')"),
SQLInsertToGBCreate("GOLEM-SQLTEST-v0.1", "INSERT INTO users (user_id, username, dept_id, building, phone_number) VALUES (101, 'asmith', 'ACCT', 'Main', '555-0101');"),
SQLInsertToGBCreate("GOLEM-SQLTEST-v0.1", "INSERT INTO users (user_id, username, dept_id, building, phone_number) VALUES (102, 'bjones', 'MGT', 'West Wing', '555-0102');"),
SQLInsertToGBCreate("GOLEM-SQLTEST-v0.1", "INSERT INTO users (user_id, username, dept_id, building, phone_number) VALUES (103, 'cwilliams', 'ACCT', 'Main', '555-0103');"),
SQLInsertToGBCreate("GOLEM-SQLTEST-v0.1", "INSERT INTO users (user_id, username, dept_id, building, phone_number) VALUES (104, 'davis_r', 'HR', 'Annex', '555-0104');"),
SQLInsertToGBCreate("GOLEM-SQLTEST-v0.1", "INSERT INTO users (user_id, username, dept_id, building, phone_number) VALUES (105, 'emiller', 'MGT', 'West Wing', '555-0105');"),
SQLInsertToGBCreate("GOLEM-SQLTEST-v0.1", "INSERT INTO users (user_id, username, dept_id, building, phone_number) VALUES (106, 'fgarcia', 'IT', 'South Tower', '555-0106');"),
SQLInsertToGBCreate("GOLEM-SQLTEST-v0.1", "INSERT INTO users (user_id, username, dept_id, building, phone_number) VALUES (107, 'h.chen', 'ACCT', 'Main', '555-0107');"),
SQLInsertToGBCreate("GOLEM-SQLTEST-v0.1", "INSERT INTO users (user_id, username, dept_id, building, phone_number) VALUES (108, 'ijackson', 'HR', 'Annex', '555-0108');"),
SQLInsertToGBCreate("GOLEM-SQLTEST-v0.1", "INSERT INTO users (user_id, username, dept_id, building, phone_number) VALUES (109, 'kim_s', 'IT', 'South Tower', '555-0109');"),
SQLInsertToGBCreate("GOLEM-SQLTEST-v0.1", "INSERT INTO users (user_id, username, dept_id, building, phone_number) VALUES (110, 'l.taylor', 'MGT', 'West Wing', '555-0110');"),
];
// Joins happen automatically; they're built in via view-as constraints on foreign keys
//const receipts = await client.createEntities(creates);
//console.log(receipts);
// Here's a good example of why we probably want to make calls into the library at some level
// The call happens after prepping the SQL and before selecting out the fields
//const selectSql2 = "select username, dept_id from users where building = 'Main'";
const selectSql = "select username, dept_id from users where building = 'West Wing'";
const selectSqlObj = parseSql(selectSql);
// Grab the tablename and grab its metadata
// TODO: Store table's hash as well so we can grab it directly rather than query?
const select_tables = await client.queryEntities(`app="GOLEM-SQLTEST-v0.1" && type="table" && tablename="${selectSqlObj?.data?.tablename}"`);
let FKs = {};
if (select_tables.length > 0) {
// Grab the table's metadata
const table_metadata = await client.getEntityMetaData(select_tables[0]?.entityKey);
for (let pair of table_metadata.stringAnnotations) {
const foundFk = parseForeignKeyString(pair.value);
if (foundFk) {
const keyname = foundFk.localKey;
FKs[keyname] = foundFk;
}
}
}
const result2 = await client.queryEntities(selectSqlObj?.data?.where);
for (let item of result2) {
const metadata = await client.getEntityMetaData(item.entityKey);
// Convert to a data object
const obj = transformAnnotationsToPOJO(metadata);
let final = filterObjectBySelect(selectSqlObj?.data?.select, obj);
// Now for the foreign key view-as (if present)
const builtFKs = buildFkQueries(final, FKs);
if (builtFKs?.length > 0) {
for (let fk of builtFKs) {
// Query for the foreign key's item
// 1. Query
// 2. Grab metadata
// 3. Convert to POJO
const query_fk = await client.queryEntities(fk.queryString);
// Grab the keyname to use (same as "view as") and store its value locally with the same name.
// Should only return one, but just in case, just grab first
if (query_fk && query_fk.length > 0) {
const fk_metadata = await client.getEntityMetaData(query_fk[0].entityKey);
const fk_pojo = transformAnnotationsToPOJO(fk_metadata);
final[fk.viewKey] = fk_pojo[fk.viewKey];
}
}
}
console.log(final);
}
};
async function testCreateInsertSelect() {
let output = await doSQL("GOLEM-SQLTEST-v1.0", `
CREATE TABLE users (
user_id INTEGER,
username TEXT,
dept_id INTEGER,
building TEXT,
phone_number TEXT,
CONSTRAINT fk__view_as__department_name
FOREIGN KEY (dept_id) REFERENCES departments(dept_id),
INDEX idx_username (username),
INDEX idx_dept_id (dept_id)
);
CREATE TABLE departments (
dept_id INTEGER,
department_name TEXT,
city TEXT,
INDEX idx_dept_id (dept_id),
INDEX idx_department_name (department_name)
);
INSERT INTO departments (dept_id, department_name, city) values ('ACCT', 'Accounting', 'New York');
INSERT INTO departments (dept_id, department_name, city) values ('IT', 'Information Technology', 'New York');
INSERT INTO departments (dept_id, department_name, city) values ('MGT', 'Management', 'Boston');
INSERT INTO departments (dept_id, department_name, city) values ('HR', 'Human Resources', 'Chicago');
INSERT INTO users (user_id, username, dept_id, building, phone_number) VALUES (101, 'asmith', 'ACCT', 'Main', '555-0101');
INSERT INTO users (user_id, username, dept_id, building, phone_number) VALUES (102, 'bjones', 'MGT', 'West Wing', '555-0102');
INSERT INTO users (user_id, username, dept_id, building, phone_number) VALUES (103, 'cwilliams', 'ACCT', 'Main', '555-0103');
INSERT INTO users (user_id, username, dept_id, building, phone_number) VALUES (104, 'davis_r', 'HR', 'Annex', '555-0104');
INSERT INTO users (user_id, username, dept_id, building, phone_number) VALUES (105, 'emiller', 'MGT', 'West Wing', '555-0105');
INSERT INTO users (user_id, username, dept_id, building, phone_number) VALUES (106, 'fgarcia', 'IT', 'South Tower', '555-0106');
INSERT INTO users (user_id, username, dept_id, building, phone_number) VALUES (107, 'h.chen', 'ACCT', 'Main', '555-0107');
INSERT INTO users (user_id, username, dept_id, building, phone_number) VALUES (108, 'ijackson', 'HR', 'Annex', '555-0108');
INSERT INTO users (user_id, username, dept_id, building, phone_number) VALUES (109, 'kim_s', 'IT', 'South Tower', '555-0109');
INSERT INTO users (user_id, username, dept_id, building, phone_number) VALUES (110, 'l.taylor', 'MGT', 'West Wing', '555-0110');
select username, dept_id from users where building = 'West Wing';
select username, dept_id from users where building = 'South Tower';
select dept_id, department_name from departments;
`);
console.log(output);
}
async function testSelect() {
let output = await doSQL("GOLEM-SQLTEST-v1.0", `
select username, dept_id from users where username = "asmith";
`);
console.log(output);
}
//await testSelect();