New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

@linked-db/linked-ql

Package Overview
Dependencies
Maintainers
0
Versions
125
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@linked-db/linked-ql - npm Package Compare versions

Comparing version 0.2.82 to 0.3.0

src/query/alter/abstracts/AbstractStatementNode.js

2

package.json

@@ -14,3 +14,3 @@ {

"icon": "https://webqit.io/icon.svg",
"version": "0.2.82",
"version": "0.3.0",
"license": "MIT",

@@ -17,0 +17,0 @@ "repository": {

import { _isObject } from '@webqit/util/js/index.js';
import CreateDatabase from '../../query/create/CreateDatabase.js';

@@ -9,15 +8,12 @@ import AlterDatabase from '../../query/alter/AlterDatabase.js';

import DropTable from '../../query/drop/DropTable.js';
import Select from '../../query/select/Select.js';
import Insert from '../../query/insert/Insert.js';
import Update from '../../query/update/Update.js';
import Delete from '../../query/delete/Delete.js';
import Node from '../../query/abstracts/Node.js';
import Parser from '../../query/Parser.js';
import Savepoint from './Savepoint.js';
const objInternals = {
infoSchemaDB: 'obj_information_schema',
instances: new Set,
schemas: new Map,
};
export default class AbstractClient {
/**
* @property String
*/
static get OBJ_INFOSCHEMA_DB() { return 'obj_information_schema'; }

@@ -28,18 +24,6 @@ /**

constructor(driver, params = {}) {
if (!this.constructor.kind) throw new Error(`Subclasses of Objective SQL Client must implement a static "kind" property.`);
if (!objInternals.schemas.has(this.constructor.kind)) { objInternals.schemas.set(this.constructor.kind, new Map); }
objInternals.instances.add(this);
Object.defineProperty(this, '$', { value: {
driver,
schemas: objInternals.schemas.get(this.constructor.kind),
params,
}});
Object.defineProperty(this, '$', { value: { driver, params }});
}
/**
* @property String
*/
static get OBJ_INFOSCHEMA_DB() { return objInternals.infoSchemaDB; }
/**
* @property Driver

@@ -55,436 +39,184 @@ */

/**
* Sets or returns default database.
* Returns a database instance.
*
* @param Array args
* @param String name
* @param Object params
*
* @return String
* @return Database
*/
async searchPath(...args) { return this.searchPathCallback(() => {}, ...arguments); }
/**
* Returns internal schemas object.
*
* @return Map
*/
async schemas() {
if (!this.$.schemas.size) await this.databases();
return this.$.schemas;
database(name, params = {}) {
return new this.constructor.Database(this, ...arguments);
}
/**
* Returns list of databases.
* Returns all available databases.
*
* @param Object params
*
* @return Array
*/
async databases(params = {}) { return this.databasesCallback(() => ([]), ...arguments); }
async databases() { return []; }
/**
* Creates a database.
* Tells whether a database exists.
*
* @param String dbName
* @param Object params
* @param String name
*
* @return Bool
*/
async createDatabase(dbName, params = {}) { return this.createDatabaseCallback(...arguments); }
/**
* Forwards to: createDatabase().
* @with: params.ifNotExixts = true
*/
async createDatabaseIfNotExists(dbName, params = {}) { return this.createDatabase(dbName, { ...params, ifNotExists: true }); }
/**
* Returns a database instance.
*
* @param String dbName
* @param Function editCallback
* @param Object params
*
* @return Bool
*/
async alterDatabase(dbName, editCallback, params = {}) { return this.alterDatabaseCallback(...arguments); }
/**
* Drops a database.
*
* @param String dbName
* @param Object params
*
* @return Bool
*/
async dropDatabase(dbName, params = {}) { return this.dropDatabaseCallback(...arguments); }
/**
* @forwardsTo: dropDatabase().
* @with: params.ifExixts = true
*/
async dropDatabaseIfExists(dbName, params = {}) { return this.dropDatabase(dbName, { ...params, ifNotExists: true }); }
/**
* Returns a database instance.
*
* @param String dbName
* @param Object params
*
* @return Database
*/
database(dbName, params = {}) {
const schemasMap = this.$.schemas;
if (!schemasMap.has(dbName)) {
schemasMap.set(dbName, {
name: dbName,
tables: new Map,
hiddenAs: 'inmemory',
});
}
return new this.constructor.Database(this, ...arguments);
async hasDatabase(name) {
return (await this.databases()).includes(name);
}
/**
* BASE LOGICS
*/
/**
* Base logic for the searchPath() method.
* Returns a JSON representation of a database and its tables.
*
* @param Function callback
* @param Array path
* @param String name
* @param Array tables
* @param Object params
*
* @return String
* @return Object
*/
async searchPathCallback(callback, ...path) {
if (path.length) {
const returnValue = await callback(path);
this.$.searchPath = path;
return returnValue;
}
if (!this.$.searchPath) { this.$.searchPath = await callback(); }
return this.$.searchPath;
async describeDatabase(name, tables = [], params = {}) {
return { name, tables: await this.database(name).describeTable(tables, params), };
}
/**
* Base logic for the searchPath() method.
* Composes a CREATE DATABASE query from descrete inputs
*
* @param String tblName
* @param Object|String dbSchema
* @param Object params
*
* @return String
* @return Savepoint
*/
async getBasename(tblName) {
const searchPath = await this.searchPath();
return searchPath.reduce(async (prev, dbName) => (await prev) || (await this.database(dbName).tables({ name: tblName })).length ? dbName : null, null);
async createDatabase(dbSchema, params = {}) {
if (typeof dbSchema === 'string') { dbSchema = { name: dbSchema }; }
else if (typeof dbSchema?.name !== 'string') throw new Error(`createDatabase() called with invalid arguments.`);
// -- Compose an schemaInstamce from request
const schemaInstamce = CreateDatabase.fromJson(this, dbSchema);
if (params.ifNotExists) schemaInstamce.withFlag('IF_NOT_EXISTS');
return await this.query(schemaInstamce, params);
}
/**
* Base logic for the databases() method.
* Composes an ALTER DATABASE query from descrete inputs
*
* @param Function callback
* @param Object filter
* @param Array standardExclusions
* @param Object|String altRequest
* @param Function callback
* @param Object params
*
* @return Array
* @return Savepoint
*/
async databasesCallback(callback, filter = {}, standardExclusions = []) {
const schemasMap = this.$.schemas;
if (!schemasMap._touched || filter.force) {
schemasMap._touched = true;
for (let db of await callback()) {
if (typeof db === 'string') { db = { name: db }; }
if (schemasMap.has(db.name)) {
delete schemasMap.get(db.name).hiddenAs;
} else { schemasMap.set(db.name, { ...db, tables: new Map }); }
}
}
let dbList = [...schemasMap.values()].filter(db => !db.hiddenAs).map(db => db.name);
if (filter.name) {
dbList = dbList.filter(dbName => dbName === filter.name);
} else if (!filter.includeStandardExclusions) {
const OBJ_INFOSCHEMA_DB = this.constructor.OBJ_INFOSCHEMA_DB;
const standardExclusionsRe = new RegExp(`^${ standardExclusions.concat(OBJ_INFOSCHEMA_DB).join('|') }$`, 'i');
dbList = dbList.filter(dbName => !standardExclusionsRe.test(dbName));
}
return dbList;
async alterDatabase(altRequest, callback, params = {}) {
if (typeof callback !== 'function') throw new Error(`alterDatabase() called with invalid arguments.`);
if (typeof altRequest === 'string') { altRequest = { name: altRequest }; }
else if (typeof altRequest?.name !== 'string') throw new Error(`alterDatabase() called with invalid arguments.`);
// -- Compose an altInstance from request
const schemaJson = await this.describeDatabase(altRequest.name, altRequest.tables);
const schemaInstance = CreateDatabase.fromJson(this, schemaJson).status('UP', 'UP');
await callback(schemaInstance);
const altInstance = schemaInstance.getAlt().with({ resultSchema: schemaInstance });
if (!altInstance.ACTIONS.length) return;
return await this.query(altInstance, params);
}
/**
* Base logic for describeTable()
* Composes a DROP DATABASE query from descrete inputs
*
* @param Function callback
* @param String|Object|CreateDatabase dbSchema
* @param String dbName
* @param Object params
*
* @return Object
* @return Savepoint
*/
async createDatabaseCallback(callback, dbSchema, params = {}) {
let dbCreateInstance;
if (dbSchema instanceof CreateDatabase) {
dbCreateInstance = dbSchema;
dbSchema = dbCreateInstance.toJson();
} else {
if (typeof dbSchema === 'string') { dbSchema = { name: dbSchema }; }
if (typeof dbSchema !== 'object' || !dbSchema.name) throw new Error(`Invalid argument#1 to createDatabase().`);
// First we validate operation
const dbFound = (await this.databases(dbSchema))[0];
if (dbFound) {
if (params.ifNotExists) return;
throw new Error(`Database ${ dbSchema.name } already exists.`);
}
// Then forward the operation for execution
dbCreateInstance = CreateDatabase.fromJson(this/*IMPORTANT: not db API*/, dbSchema);
if (params.ifNotExists) dbCreateInstance.withFlag('IF_NOT_EXISTS');
}
// ------
// Must be before db changes below
const dbApi = this.database(dbSchema.name, params);
const schemasMap = await this.schemas(), tablesSavepoints = new Set;
// DB changes now
let onAfterCreateCalled;
const onAfterCreate = async () => {
onAfterCreateCalled = true;
delete schemasMap.get(dbSchema.name).hiddenAs; // This does really exist now
schemasMap.get(dbSchema.name).schemaEdit = { get tablesSavepoints() { return tablesSavepoints; } };
for (const tblSchema of dbSchema.tables || []) {
await dbApi.createTable(tblSchema, params);
}
delete schemasMap.get(dbSchema.name).schemaEdit;
};
await callback(dbCreateInstance, onAfterCreate, params);
// AFTER WE NOW EXISTS
if (!onAfterCreateCalled) await onAfterCreate();
// Create savepoint?
let savepointCreation = true;
if (params.noCreateSavepoint || (new RegExp(`^${ this.constructor.OBJ_INFOSCHEMA_DB }$`)).test(dbSchema.name)) {
savepointCreation = false;
}
if (savepointCreation) {
await this.createSavepoint({
savepoint_desc: params.savepointDesc || 'Database create',
// Current state
name_snapshot: null, // How we know created
// New state
current_name: dbSchema.name,
}, tablesSavepoints);
}
return dbApi;
async dropDatabase(dbName, params = {}) {
if (typeof dbName !== 'string') throw new Error(`dropDatabase() called with an invalid name: ${ dbName }.`);
// -- Compose an dropInstamce from request
const dropInstamce = DropDatabase.fromJson(this, { name: dbName });
if (params.ifExists) dropInstamce.withFlag('IF_EXISTS');
if (params.cascade) dropInstamce.withFlag('CASCADE');
return await this.query(dropInstamce, params);
}
/**
* Base logic for alterDatabase()
* Base logic for dropDatabase()
*
* @param Function callback
* @param String|Object|AlterDatabase dbAlterRequest
* @param Function editCallback
* @param Object params
* @param Function handler
* @param String query
* @param Object params
*
* @return Object
*/
async alterDatabaseCallback(callback, dbAlterRequest, editCallback, params = {}) {
const schemasMap = await this.schemas(), tablesSavepoints = new Set;
let dbAlterInstance, dbName, dbSchema;
let onAfterAfterCalled, onAfterAlter = () => {};
if (dbAlterRequest instanceof AlterDatabase) {
// Remap arguments
dbAlterInstance = dbAlterRequest;
dbName = dbAlterInstance.NAME;
params = editCallback || {};
// Create savepount data
dbSchema = schemasMap.get(dbName);
} else if (typeof editCallback === 'function') {
let tablesAlterRequest = [];
if (typeof dbAlterRequest === 'object' && dbAlterRequest) {
if (Array.isArray(dbAlterRequest.tables)) { tablesAlterRequest = dbAlterRequest.tables; }
dbName = dbAlterRequest.name;
} else { dbName = dbAlterRequest; }
if (typeof dbName !== 'string') throw new Error(`Invalid argument#1 to alterDatabase().`);
// First we validate operation
const dbFound = (await this.databases({ name: dbName }))[0];
if (!dbFound) {
if (params.ifExists) return;
throw new Error(`Database ${ dbName } does not exist.`);
async queryCallback(handler, query, params = {}) {
if (typeof query === 'string') query = Parser.parse(this, query.trim()/* Important */, null, { log: false });
else if (!(query instanceof Node)) throw new Error(`query() called with invalid arguments.`);
const instanceOf = (o, classes) => classes.some(c => o instanceof c);
// -- Generate resultSchema for AlterDatabase and DropDatabase? We'll need it for savepoint creation or per driver's request for it (params.$resultSchema === 'always')
const scope = {};
const resultSchemaRequired = dbName => dbName && !(new RegExp(dbName, 'i')).test(this.constructor.OBJ_INFOSCHEMA_DB) && (!params.noCreateSavepoint || params.$resultSchema === 'always');
if (instanceOf(query, [CreateDatabase,AlterDatabase,DropDatabase]) && resultSchemaRequired(query.name())) {
if (query instanceof DropDatabase) {
const resultSchema = CreateDatabase.fromJson(this, await this.describeDatabase(query.name(), '*')).status('DOWN');
query.with({ resultSchema });
} else if (query instanceof AlterDatabase && !query.resultSchema) {
const tablesList = query.ACTIONS.filter(a => ['ALTER','DROP'].includes(a.TYPE)).map(x => x.NAME);
const resultSchema = CreateDatabase.fromJson(this, await this.describeDatabase(query.name(), tablesList)).status('UP', 'UP').alterWith(query); // Simulate edits;
query.with({ resultSchema });
} else if (query instanceof CreateDatabase) query.with({ resultSchema: query });
// -- And that's what we'll use as snapshot
scope.savepoint = query.resultSchema;
} else if (instanceOf(query, [CreateTable,AlterTable,DropTable])) {
const basename = query.basename() || await this.basenameGet(query.name(), true);
if (resultSchemaRequired(basename)) {
const dbApi = this.database(basename);
if (query instanceof DropTable && basename) {
const resultSchema = CreateTable.fromJson(dbApi, await dbApi.describeTable(query.name())).status('DOWN');
query.with({ resultSchema });
} else if (query instanceof AlterTable && !query.resultSchema && basename) {
const resultSchema = CreateTable.fromJson(dbApi, await dbApi.describeTable(query.name())).status('UP', 'UP').alterWith(query); // Simulate edits;
query.with({ resultSchema });
} else if (query instanceof CreateTable && basename) query.with({ resultSchema: query });
// -- But this is what we'll use as snapshot
if (!params.noCreateSavepoint && basename) {
scope.savepoint = CreateDatabase.fromJson(this, {
name: dbApi.name,
tables: [query.resultSchema]
}).status('UP');
}
}
// Singleton DB schema
dbSchema = schemasMap.get(dbName);
// For recursive operations
if (dbSchema.schemaEdit) return await editCallback(dbSchema.schemaEdit);
// On to snapshots; before the database changes below
const dbApi = this.database(dbName, params);
// On to editing work; but first load all necessary table schemas into memory
const dbSchemaEdit = CreateDatabase.cloneJson(dbSchema);
const tableSchemas = await dbApi.describeTable(tablesAlterRequest, params);
Object.defineProperty(dbSchemaEdit, 'tables', { value: tableSchemas.map(tableSchema => CreateTable.cloneJson(tableSchema)) });
Object.defineProperties(dbSchemaEdit.tables, {
get: { value: name => dbSchemaEdit.tables.find(x => x.name === name), configurable: true },
has: { value: name => dbSchemaEdit.tables.get(name) ? true : false, configurable: true },
delete: { value: name => dbSchemaEdit.tables.splice(dbSchemaEdit.tables.findIndex(x => x.name === name), 1), configurable: true },
});
Object.defineProperty(dbSchemaEdit, 'tablesSavepoints', { get() { return tablesSavepoints; } });
// Call for editing
dbSchema.schemaEdit = dbSchemaEdit;
await editCallback(dbSchemaEdit);
// Diff into a AlterDatabase instance
dbAlterInstance = AlterDatabase.fromDiffing(this/*IMPORTANT: not db API*/, dbSchema, dbSchemaEdit);
// Handle tableSchema edits
onAfterAlter = async ($dbName = dbName) => {
onAfterAfterCalled = true;
const tableDiffs = AlterTable.fromDiffing2d(dbApi/*IMPORTANT: not client API*/, tableSchemas, dbSchemaEdit.tables);
for (const diff of tableDiffs) {
if (diff.type === 'DROP') { await dbApi.dropTable(diff.argument, params); }
if (diff.type === 'ADD') { await dbApi.createTable(diff.argument, params); }
if (diff.type === 'ALTER') { await dbApi.alterTable(diff.argument, params); }
}
delete dbSchema.schemaEdit; // Cleanup
};
} else {
throw new Error(`Alter database "${ dbName }" called without a valid callback function.`);
}
// ------
// DB changes now
await callback(dbAlterInstance, onAfterAlter, params);
const newDbName = dbAlterInstance.ACTIONS.find(action => action.TYPE === 'RENAME' && !action.REFERENCE)?.ARGUMENT;
if (newDbName) {
// Modify original schema to immediately reflect the db changes
dbSchema.name = newDbName;
schemasMap.delete(dbName);
schemasMap.set(dbSchema.name, dbSchema);
// -- Execute...
const returnValue = await handler(query, params);
// -- Generate savepoint?
if (!params.noCreateSavepoint && scope.savepoint) {
scope.savepoint.status(scope.savepoint.status(), true);
return await this.createSavepoint(scope.savepoint, params.savepointDesc);
}
// ------
// AFTER WE NOW Executed ALTER
if (!onAfterAfterCalled) await onAfterAlter(newDbName || dbName);
// ------
// Create savepoint
let savepoint, savepointCreation = dbAlterInstance.ACTIONS.length || tablesSavepoints.size;
if (params.noCreateSavepoint || (new RegExp(`^${ this.constructor.OBJ_INFOSCHEMA_DB }$`)).test(dbName)) {
savepointCreation = false;
}
if (savepointCreation) {
savepoint = await this.createSavepoint({
savepoint_desc: params.savepointDesc || 'Database alter',
// Current state
name_snapshot: dbName, // Old name
// New state
current_name: newDbName || dbName,
}, tablesSavepoints);
}
// ------
// Done
return savepoint;
return returnValue;
}
/**
* Base logic for dropDatabase()
* Sets or returns the search path for resolving unqualified table references.
*
* @param Function callback
* @param String dbName
* @param Object params
* @param Array|String resolutionPath
*
* @return Object
* @return Array
*/
async dropDatabaseCallback(callback, dbName, params = {}) {
let dbDropInstance;
if (dbName instanceof DropDatabase) {
dbDropInstance = dbName;
dbName = dbDropInstance.NAME;
} else {
// First we validate operation
const dbFound = (await this.databases({ name: dbName }))[0];
if (!dbFound) {
if (params.ifExists) return;
throw new Error(`Database ${ dbName } does not exist.`);
}
// Then forward the operation for execution
dbDropInstance = new DropDatabase(this/*IMPORTANT: not db API*/, dbName);
if (params.ifExists) dbDropInstance.withFlag('IF_EXISTS');
if (params.cascade) dbDropInstance.withFlag('CASCADE');
}
const schemasMap = await this.schemas();
const dbSchema = schemasMap.get(dbName);
if (dbSchema.schemaEdit) throw new Error(`Cannot delete database when already in edit mode.`);
// -----------------
// Must be before db changes below
let savepointCreation = true, tablesSavepoints;
if (params.noCreateSavepoint || (new RegExp(`^${ this.constructor.OBJ_INFOSCHEMA_DB }$`)).test(dbSchema.name)) {
savepointCreation = false;
}
if (savepointCreation) {
const dbApi = this.database(dbName, params);
tablesSavepoints = new Set((await dbApi.describeTable('*')).map(tblSchema => ({
// Snapshot
name_snapshot: tblSchema.name,
columns_snapshot: JSON.stringify(tblSchema.columns),
constraints_snapshot: JSON.stringify(tblSchema.constraints),
indexes_snapshot: JSON.stringify(tblSchema.indexes),
// New state
current_name: null, // How we know deleted
})));
}
// -----------------
// DB changes now
await callback(dbDropInstance, params);
// -----------------
// Then update records
dbSchema.hiddenAs = 'dropped';
//dbSchema.tables.clear();
for (const [ , tblSchema ] of dbSchema.tables) { tblSchema.hiddenAs = 'dropped'; }
// -----------------
// Main savepoints
if (savepointCreation) {
return this.createSavepoint({
savepoint_desc: params.savepointDesc || 'Database drop',
// Current state
name_snapshot: dbSchema.name,
// New state
current_name: null, // How we know deleted
}, tablesSavepoints);
}
async basenameResolution(resolutionPath = []) {
if (arguments.length) { return (this.$.resolutionPath = [].concat(resolutionPath), this); }
return new BasenameResolutor(this.$.basenameResolution);
}
/**
* Base logic for dropDatabase()
* Resolving unqualified table reference.
*
* @param Function callback
* @param String|Object|Function ...query
* @param String tblName
* @param Bool withDefaultBasename
*
* @return Object
* @returns String
*/
async queryCallback(callback, ...args) {
const Types = [ Insert, Update, Delete, Select, DropDatabase, DropTable, CreateDatabase, CreateTable, AlterDatabase, AlterTable ];
const params = typeof args[args.length - 1] === 'object' ? args.pop() : {};
const exec = async instance => {
if (instance instanceof CreateDatabase) return await this.createDatabase(instance, params);
if (instance instanceof AlterDatabase) return await this.alterDatabase(instance, params);
if (instance instanceof DropDatabase) return await this.dropDatabase(instance, params);
let basename = instance.BASENAME;
if (!basename) {
const searchPath = await this.searchPath();
basename = searchPath.find(s => !s.startsWith('$')) || searchPath[0];
}
if (instance instanceof CreateTable) return await this.database(basename).createTable(instance, params);
if (instance instanceof AlterTable) return await this.database(basename).alterTable(instance, params);
if (instance instanceof DropTable) return await this.database(basename).dropTable(instance, params);
// For Insert, Update, Delete, Select queries...
return await callback(instance, params);
};
if (typeof args[0] === 'string' && typeof args[1] === 'function') {
const $queryType = args.shift(), queryType = $queryType.toLowerCase().replace(/^\w|_./g, m => m.toUpperCase().replace('_', ''));
const Type = Types.find(Type => Type.name === queryType);
if (!Type) throw new Error(`Unknown query type: ${ $queryType }.`);
const instance = new Type(this);
args.forEach((arg, i) => {
if (typeof arg !== 'function') throw new Error(`Invalid argument at #${ i }.`);
arg(instance);
});
return await exec(instance);
}
if (args.length > 1) throw new Error(`Invalid argument count.`);
if (typeof args[0] === 'object' && args[0]) {
const instance = Types.reduce((prev, Type) => prev || Type.fromJson(this, args[0]), null);
if (instance) return await exec(instance);
}
if (typeof args[0] === 'string') {
const instance = Parser.parse(this, args[0]);
return await exec(instance);
}
throw new Error(`Invalid arguments.`);
async basenameGet(tblName, withDefaultBasename = false) {
const basenames = await this.basenameResolution();
return (
await basenames.reduce(async (prev, dbName) => {
return (await prev) || (await this.database(dbName).hasTable(tblName)) ? dbName : null;
}, null)
) || (withDefaultBasename ? basenames.find(s => !s.startsWith('$')) || basenames[0] : null);
}

@@ -495,69 +227,60 @@

*
* @param Object entry
* @param Set tblEntires
* @param CreateDatabase schemaInstamce
* @param String savepointDescription
*
* @return Object
*/
async createSavepoint(entry, tblEntries = new Set) {
// Commit to DB
async createSavepoint(schemaInstamce, savepointDesc = null) {
// -- Create schema?
const OBJ_INFOSCHEMA_DB = this.constructor.OBJ_INFOSCHEMA_DB;
const infoSchemaDB = this.database(OBJ_INFOSCHEMA_DB);
if (!(await this.databases({ name: OBJ_INFOSCHEMA_DB }))[0]) {
await this.createDatabase(OBJ_INFOSCHEMA_DB);
await infoSchemaDB.createTable({
name: 'database_savepoints',
columns: [
{ name: 'id', type: 'uuid', primaryKey: true, default: { expr: 'gen_random_uuid()' } },
{ name: 'name_snapshot', type: 'varchar' },
{ name: 'savepoint_desc', type: 'varchar' },
{ name: 'savepoint_date', type: 'timestamp' },
{ name: 'rollback_date', type: 'timestamp' },
{ name: 'current_name', type: 'varchar' },
],
});
await infoSchemaDB.createTable({
name: 'table_savepoints',
columns: [
{ name: 'savepoint_id', type: 'uuid', references: { table: 'database_savepoints', columns: ['id'], deleteRule: 'cascade' } },
{ name: 'name_snapshot', type: 'varchar' },
{ name: 'columns_snapshot', type: 'json' },
{ name: 'constraints_snapshot', type: 'json' },
{ name: 'indexes_snapshot', type: 'json' },
{ name: 'current_name', type: 'varchar' },
],
});
if (!(await this.hasDatabase(OBJ_INFOSCHEMA_DB))) {
await this.createDatabase({
name: OBJ_INFOSCHEMA_DB,
tables: [{
name: 'database_savepoints',
columns: [
{ name: 'id', type: 'uuid', primaryKey: true, default: { expr: 'gen_random_uuid()' } },
// Actual snapshot
{ name: 'name', type: 'varchar', notNull: true },
{ name: '$name', type: 'varchar' },
{ name: 'tables', type: 'json' },
{ name: 'status', type: 'varchar' },
// Meta data
{ name: 'savepoint_description', type: 'varchar' },
{ name: 'database_tag', type: 'varchar', notNull: true },
{ name: 'version_tag', type: 'int', notNull: true },
{ name: 'savepoint_date', type: 'timestamp', notNull: true },
{ name: 'rollback_date', type: 'timestamp' },
],
}],
}, { noCreateSavepoint: true });
}
// ------------------
// Resolve forward histories before creating new one
const dbName = [OBJ_INFOSCHEMA_DB,'database_savepoints'];
let where = x => x.in( y => y.literal(entry.name_snapshot || entry.current_name), ['active','name_snapshot'], ['active','current_name']);
while(where) {
const rolledbackSavepoints = await this.query('select', q => {
q.select(['active','id'], x => x.name(['following','id']).as('id_following'));
q.from(dbName).as('active');
q.leftJoin(dbName).as('following').on( x => x.equals(['following','name_snapshot'], ['active','current_name']) );
q.where( where );
q.where( x => x.isNotNull(['active','rollback_date']) );
q.orderBy(['active','savepoint_date']).withFlag('ASC');
q.limit(1);
});
if (rolledbackSavepoints[0]?.id) {
await this.query('delete', q => {
q.from(dbName);
q.where( x => x.equals('id', y => y.literal(rolledbackSavepoints[0].id) ) );
});
// -- Savepoint JSON
const savepointJson = {
database_tag: null,
...schemaInstamce.toJson(),
savepoint_description: savepointDesc,
version_tag: null,
savepoint_date: new String('now()'),
};
// -- Find a match first
const currentSavepoint = await this.database(schemaInstamce.name()).savepoint();
if (currentSavepoint) {
const tblName = [OBJ_INFOSCHEMA_DB,'database_savepoints'].join('.');
// -- Apply id and tag from lookup
savepointJson.database_tag = currentSavepoint.databaseTag;
savepointJson.version_tag = (await this.query(`SELECT max(version_tag) + 1 AS next_tag FROM ${ tblName } WHERE database_tag = '${ currentSavepoint.databaseTag }'`))[0].next_tag;
// -- Delete forward records
if (savepointJson.version_tag - 1 !== currentSavepoint.versionTag) {
await this.query(`DELETE FROM ${ tblName } WHERE database_tag = '${ currentSavepoint.databaseTag }' AND rollback_date IS NOT NULL`);
}
if (rolledbackSavepoints[0]?.id_following) { where = x => x.equals(['active','id'], y => y.literal(rolledbackSavepoints[0].id_following) ); }
else { where = null; }
} else {
// -- Generate tag and version as fresh
savepointJson.database_tag = `db:${ ( 0 | Math.random() * 9e6 ).toString( 36 ) }`;
savepointJson.version_tag = 1;
}
// ------------------
// Create savepoint
const insertResult = await infoSchemaDB.table('database_savepoints').add({ ...entry, savepoint_date: 'now()' });
const savepoint = new Savepoint(this, { ...insertResult.toJson(), id_active: null });
if (tblEntries.size) {
tblEntries = [ ...tblEntries ].map(tblEntry => ({ ...tblEntry, savepoint_id: savepoint.id }));
await infoSchemaDB.table('table_savepoints').addAll(tblEntries);
}
return savepoint;
// -- Create record
const insertResult = await this.database(OBJ_INFOSCHEMA_DB).table('database_savepoints').add(savepointJson);
return new Savepoint(this, { ...insertResult.toJson(), id_following: null });
}
}
import CreateTable from '../../query/create/CreateTable.js';
import AlterTable from '../../query/alter/AlterTable.js';
import DropTable from '../../query/drop/DropTable.js';

@@ -13,7 +12,3 @@ import Savepoint from './Savepoint.js';

constructor(client, dbName, params = {}) {
this.$ = {
client,
schema: client.$.schemas.get(dbName),
params
};
Object.defineProperty(this, '$', { value: { client, name: dbName, params }});
}

@@ -29,3 +24,3 @@

*/
get name() { return this.$.schema.name; }
get name() { return this.$.name; }

@@ -38,5 +33,12 @@ /**

/**
* @property Bool
* Returns a table instance.
*
* @param String name
* @param Object params
*
* @return Table
*/
get dropped() { return this.$.schema.hiddenAs === 'dropped'; }
table(name, params = {}) {
return new this.constructor.Table(this, ...arguments);
}

@@ -46,23 +48,21 @@ /**

*
* @param Object params
*
* @return Array
*/
async tables(params = {}) { return this.tablesCallback(() => ([]), ...arguments); }
async tables() { return []; }
/**
* Describes table.
* Tells whether a table exists.
*
* @param String tblName
* @param Object params
* @param String name
*
* @return Object
* @return Bool
*/
async describeTable(tblName, params = {}) { return this.describeTableCallback((tblName, params) => {}, ...arguments); }
async hasTable(name) {
return (await this.tables()).includes(name);
}
/**
* Creates table.
* Base logic for describeTable()
*
* @param String tblName
* @param Object tblSchema
* @param String|Array tblName_s
* @param Object params

@@ -72,39 +72,48 @@ *

*/
async createTable(tblName, tblSchema = {}, params = {}) { return this.createTableCallback(() => ([]), ...arguments); }
async describeTable(tblName_s, params = {}) {
const tblNames = [].concat(tblName_s);
const isSingle = !Array.isArray(tblName_s) && tblName_s !== '*';
const isAll = tblNames.length === 1 && tblNames[0] === '*';
return isSingle ? null : [];
}
/**
* Forwards to: createTable().
* @with: params.ifNotExixts = true
*/
async createTableIfNotExists(tblName, tblSchema = {}, params = {}) { return this.createTable(tblName, tblSchema, { ...params, ifNotExists: true }); }
/**
* Alters table.
* Composes a CREATE TABLE query from descrete inputs
*
* @param String tblName
* @param Object tblSchema
* @param Object params
*
* @return Bool
* @return Savepoint
*/
async alterTable(tblName, tblSchema, params = {}) { return this.alterTableCallback((tblName, tblSchema, params) => {}, ...arguments); }
async createTable(tblSchema, params = {}) {
if (typeof tblSchema?.name !== 'string') throw new Error(`createTable() called with invalid arguments.`);
// -- Compose an schemaInstamce from request
const schemaInstamce = CreateTable.fromJson(this, tblSchema);
if (params.ifNotExists) schemaInstamce.withFlag('IF_NOT_EXISTS');
return this.client.query(schemaInstamce, params);
}
/**
* Drops table.
* Composes an ALTER TABLE query from descrete inputs
*
* @param String tblName
* @param Function callback
* @param Object params
*
* @return Bool
* @return Savepoint
*/
async dropTable(tblName, params = {}) { return this.dropTableCallback((tblName, params) => {}, ...arguments); }
async alterTable(tblName, callback, params = {}) {
if (typeof callback !== 'function' || typeof tblName !== 'string') throw new Error(`alterTable() called with invalid arguments.`);
// -- Compose an altInstance from request
const schemaJson = await this.describeTable(tblName);
const schemaInstance = CreateTable.fromJson(this, schemaJson).status('UP', 'UP');
await callback(schemaInstance);
const altInstance = schemaInstance.getAlt().with({ resultSchema: schemaInstance });
if (!altInstance.ACTIONS.length) return;
if (params.ifExists) altInstance.withFlag('IF_EXISTS');
return this.client.query(altInstance, params);
}
/**
* Forwards to: dropTable().
* @with: params.ifExixts = true
*/
async dropTableIfExists(tblName, params = {}) { return this.dropTable(tblName, { ...params, ifNotExists: true }); }
/**
* Returns a table instance.
* Composes a DROP TABLE query from descrete inputs
*

@@ -114,13 +123,11 @@ * @param String tblName

*
* @return Bool
* @return Savepoint
*/
table(tblName, params = {}) {
const tablesMap = this.$.schema.tables;
if (!tablesMap.has(tblName)) {
tablesMap.set(tblName, {
name: tblName,
hiddenAs: 'inmemory',
});
}
return new this.constructor.Table(this, ...arguments);
async dropTable(tblName, params = {}) {
if (typeof tblName !== 'string') throw new Error(`dropTable() called with invalid arguments.`);
// -- Compose an dropInstamce from request
const dropInstamce = DropTable.fromJson(this, { name: tblName });
if (params.ifExists) dropInstamce.withFlag('IF_EXISTS');
if (params.cascade) dropInstamce.withFlag('CASCADE');
return this.client.query(dropInstamce, params);
}

@@ -137,255 +144,19 @@

const OBJ_INFOSCHEMA_DB = this.client.constructor.OBJ_INFOSCHEMA_DB;
if ((await this.client.databases({ name: OBJ_INFOSCHEMA_DB }))[0]) {
const forward = params.direction === 'forward';
const dbName = [OBJ_INFOSCHEMA_DB,'database_savepoints'];
const result = await this.client.query('select', q => {
q.from(dbName).as(forward ? 'active' : 'preceding');
if (forward) {
q.select( ['following','*'], f => f.name(['active','id']).as('id_active') );
q.rightJoin(dbName).as('following').on( x => x.equals(['following','name_snapshot'], ['active','current_name']) );
q.where( x => x.in( x => x.literal(this.name), ['active','name_snapshot'], ['active','current_name'] ), x => x.isNotNull(['active','rollback_date']) );
q.orderBy(['active','savepoint_date']).withFlag('ASC');
} else {
q.select( ['preceding','*'], f => f.name(['active','id']).as('id_active') );
q.leftJoin(dbName).as('active').on( x => x.equals(['active','name_snapshot'], ['preceding','current_name']) );
q.where( x => x.in( x => x.literal(this.name), ['preceding','name_snapshot'], ['preceding','current_name'] ), x => x.isNull(['preceding','rollback_date']) );
q.orderBy(['preceding','savepoint_date']).withFlag('DESC');
}
q.limit(1);
});
return result[0] && new Savepoint(this.client, result[0], params.direction);
}
if (!(await this.client.hasDatabase(OBJ_INFOSCHEMA_DB))) return;
const tblName = [OBJ_INFOSCHEMA_DB,'database_savepoints'].join('.');
const result = params.direction === 'forward'
? await this.client.query(`
SELECT savepoint.*, preceding.id AS id_preceding FROM ${ tblName } AS savepoint
LEFT JOIN ${ tblName } AS preceding ON preceding.database_tag = savepoint.database_tag AND COALESCE(preceding."$name", preceding.name) = savepoint.name AND preceding.version_tag < savepoint.version_tag
WHERE COALESCE(savepoint.name, savepoint."$name") = '${ this.name }' AND savepoint.rollback_date IS NOT NULL AND (preceding.id IS NULL OR preceding.rollback_date IS NULL)
ORDER BY savepoint.version_tag ASC LIMIT 1
`)
: await this.client.query(`
SELECT savepoint.*, following.id AS id_following FROM ${ tblName } AS savepoint
LEFT JOIN ${ tblName } AS following ON following.database_tag = savepoint.database_tag AND following.name = COALESCE(savepoint."$name", savepoint.name) AND following.version_tag > savepoint.version_tag
WHERE COALESCE(savepoint."$name", savepoint.name) = '${ this.name }' AND savepoint.rollback_date IS NULL AND (following.id IS NULL OR following.rollback_date IS NOT NULL)
ORDER BY savepoint.version_tag DESC LIMIT 1
`);
return result[0] && new Savepoint(this.client, result[0], params.direction);
}
/**
* Base logic for the tables() method.
*
* @param Function callback
* @param Object filter
*
* @return Array
*/
async tablesCallback(callback, filter = {}) {
const tablesMap = this.$.schema.tables;
if (!tablesMap._touched || filter.force) {
tablesMap._touched = true;
for (let tbl of await callback()) {
if (typeof tbl === 'string') { tbl = { name: tbl }; }
if (tablesMap.has(tbl.name)) {
delete tablesMap.get(tbl.name).hiddenAs;
} else { tablesMap.set(tbl.name, { ...tbl }); }
}
}
let tblList = [...tablesMap.values()].filter(tbl => !tbl.hiddenAs).map(tbl => tbl.name);
if (filter.name) { tblList = tblList.filter(tblName => tblName === filter.name); }
return tblList;
}
/**
* Base logic for describeTable()
*
* @param Function callback
* @param String|Array tblName_s
* @param Object params
*
* @return Object
*/
async describeTableCallback(callback, tblName_s, params = {}) {
const isMultiple = Array.isArray(tblName_s);
const tblNames = isMultiple ? tblName_s : [tblName_s];
const isAll = tblNames.length === 1 && tblNames[0] === '*';
if (this.dropped) return isAll || isMultiple ? [] : undefined;
const tablesMap = this.$.schema.tables;
const requestList = isAll ? ['*'] : tblNames;//TODO.filter(tblName => !tablesMap.get(tblName)?.columns && !tablesMap.get(tblName)?.hiddenAs);
if (requestList.length) {
const tblSchemas = await callback(requestList, params); // Describe should always add constraint names
for (const tblSchema of tblSchemas) {
if (tablesMap.has(tblSchema.name)) {
delete tablesMap.get(tblSchema.name).hiddenAs;
Object.assign(tablesMap.get(tblSchema.name), tblSchema);
} else { tablesMap.set(tblSchema.name, tblSchema); }
}
}
if (isAll) return [...tablesMap.values()].filter(tbl => !tbl.hiddenAs);
if (isMultiple) return tblNames.map(tblName => tablesMap.get(tblName)).filter(tbl => !tbl.hiddenAs);
return !tablesMap.get(tblName_s)?.hiddenAs ? tablesMap.get(tblName_s) : undefined;
}
/**
* Base logic for createTable()
*
* @param Function callback
* @param Object tblSchema
* @param Object params
*/
async createTableCallback(callback, tblSchema, params = {}) {
await this.client.alterDatabase(this.name, async dbSchemaEdit => {
let tblCreateRequest;
if (tblSchema instanceof CreateTable) {
tblCreateRequest = tblSchema;
tblSchema = tblCreateRequest.toJson();
} else {
const tblFound = (await this.tables({ name: tblSchema.name }))[0];
if (tblFound) {
if (params.ifNotExists) return;
throw new Error(`Table ${ tblSchema.name } already exists.`);
}
if (tblSchema.basename && tblSchema.basename !== this.name) {
throw new Error(`A table schema of database ${ tblSchema.basename } is being passed to ${ this.name }.`);
}
tblCreateRequest = CreateTable.fromJson(this.client/*IMPORTANT: client API*/, tblSchema);
if (params.ifNotExists) tblCreateRequest.withFlag('IF_NOT_EXISTS');
}
// Important:
tblCreateRequest.name([this.name,tblCreateRequest.NAME]);
// Create savepoint
dbSchemaEdit.tablesSavepoints.add({
// Snapshot
name_snapshot: null,
columns_snapshot: JSON.stringify([]),
constraints_snapshot: JSON.stringify([]),
indexes_snapshot: JSON.stringify([]),
// New state
current_name: tblSchema.name
});
await callback(tblCreateRequest, params);
// Update original objects in place
const tablesMap = this.$.schema.tables;
if (tablesMap.get(tblSchema.name)?.hiddenAs) {
delete tablesMap.get(tblSchema.name).hiddenAs; // This does really exist now
} else {
tablesMap.set(tblSchema.name, { name: tblSchema.name });
}
}, { savepointDesc: 'Table create', ...params });
return this.table(tblSchema.name, params);
}
/**
* Base logic for alterTable()
*
* @param Function callback
* @param String tblName
* @param Function editCallback
* @param Object params
*/
async alterTableCallback(callback, tblName, editCallback, params = {}) {
return this.client.alterDatabase(this.name, async dbSchemaEdit => {
let tblAlterRequest, tblSchema;
if (tblName instanceof AlterTable) {
// Remap arguments
tblAlterRequest = tblName;
tblName = tblAlterRequest.NAME;
params = editCallback || {};
// Create savepount data
tblSchema = tblAlterRequest.JSON_BEFORE?.columns ? tblAlterRequest.JSON_BEFORE : await this.describeTable(tblName, params);
} else if (typeof editCallback === 'function') {
// First we validate operation
const tblFound = (await this.tables({ name: tblName }))[0];
if (!tblFound) {
if (params.ifExists) return;
throw new Error(`Table ${ tblName } does not exist.`);
}
// Singleton TBL schema
tblSchema = await this.describeTable(tblName, params);
// For recursive edits
if (tblSchema.schemaEdit) return await editCallback(tblSchema.schemaEdit);
// Fresh edit
tblSchema.schemaEdit = CreateTable.cloneJson(tblSchema); // One global object
// ------
// Call for modification
await editCallback(tblSchema.schemaEdit);
// Diff into a AlterTable instance
tblAlterRequest = AlterTable.fromDiffing(this.client/*IMPORTANT: client API*/, tblSchema, tblSchema.schemaEdit);
if (params.ifExists) tblAlterRequest.withFlag('IF_EXISTS');
delete tblSchema.schemaEdit;
} else {
throw new Error(`Alter table "${ tblName }" called with invalid arguments.`);
}
// Important:
tblAlterRequest.name([this.name,tblAlterRequest.NAME]);
const newTblName = tblAlterRequest.ACTIONS.find(action => action.TYPE === 'RENAME' && !action.REFERENCE)?.ARGUMENT;
const newTblLocation = tblAlterRequest.ACTIONS.find(action => action.TYPE === 'RELOCATE')?.ARGUMENT;
if (tblAlterRequest.ACTIONS.length) {
// Create savepoint
for (const action of tblAlterRequest.ACTIONS) {
if (action.TYPE === 'RENAME' && action.REFERENCE) {
const listName = action.REFERENCE.type === 'CONSTRAINT' ? 'constraints' : (action.REFERENCE.type === 'INDEX' ? 'indexes' : 'columns');
const nameKey = listName === 'constraints' ? 'constraintName' : (listName === 'indexes' ? 'indexName' : 'name');
tblSchema[listName].find(obj => obj[nameKey] === action.REFERENCE.name)[`$${ nameKey }`] = action.ARGUMENT;
}
}
dbSchemaEdit.tablesSavepoints.add({
// Snapshot
name_snapshot: tblSchema.name,
columns_snapshot: JSON.stringify(tblSchema.columns),
constraints_snapshot: JSON.stringify(tblSchema.constraints || []),
indexes_snapshot: JSON.stringify(tblSchema.indexes || []),
// New state
current_name: newTblName || tblName,
});
// Effect changes
await callback(tblAlterRequest, params);
}
// Update original schema object in place
// This lets describeTable() know to lookup remote db
const tablesMap = this.$.schema.tables;
delete tablesMap.get(tblName).columns;
if (newTblName) { tblSchema.name = newTblName; }
if (newTblLocation) {
tblSchema.basename = newTblLocation;
tablesMap.delete(tblName);
}
}, { savepointDesc: 'Table alter', ...params });
}
/**
* Base logic for dropTable()
*
* @param Function callback
* @param String tblName
* @param Object params
*
* @return Object
*/
async dropTableCallback(callback, tblName, params = {}) {
return this.client.alterDatabase(this.name, async dbSchemaEdit => {
let tblDropRequest;
if (tblName instanceof DropTable) {
tblDropRequest = tblName;
tblName = tblDropRequest.NAME;
} else {
// First we validate operation
const tblFound = (await this.tables({ name: tblName }))[0];
if (!tblFound) {
if (params.ifExists) return;
throw new Error(`Table ${ tblName } does not exist.`);
}
// Then forward the operation for execution
tblDropRequest = new DropTable(this.client/*IMPORTANT: client API*/, tblName, this.name);
if (params.ifExists) tblDropRequest.withFlag('IF_EXISTS');
if (params.cascade) tblDropRequest.withFlag('CASCADE');
}
// Important:
tblDropRequest.name([this.name,tblDropRequest.NAME]);
// Create savepoint
const tblSchema = await this.describeTable(tblName, params);
if (tblSchema.schemaEdit) throw new Error(`Cannot delete table when already in edit mode.`);
dbSchemaEdit.tablesSavepoints.add({
// Snapshot
name_snapshot: tblSchema.name,
columns_snapshot: JSON.stringify(tblSchema.columns),
constraints_snapshot: JSON.stringify(tblSchema.constraints),
indexes_snapshot: JSON.stringify(tblSchema.indexes),
// New state
current_name: null, // How we know deleted
});
await callback(tblDropRequest, params);
// Then update original schema object in place
const tablesMap = this.$.schema.tables;
tablesMap.get(tblName).hiddenAs = 'dropped';
delete tablesMap.get(tblName).columns;
delete tablesMap.get(tblName).constraints;
delete tablesMap.get(tblName).indexes;
}, { savepointDesc: 'Table drop', ...params });
}
}

@@ -44,3 +44,3 @@

} else {
var uniqueColumns = this.table.columnsForConstraint('UNIQUE');
var uniqueColumns = this.table.columnsForConstraint('UNIQUE_KEY');
keyPaths = uniqueColumns.filter(keyPath => _any(keyPath, columnName => this.columns.includes(columnName)));

@@ -47,0 +47,0 @@ wheres.by = 'uniqueKeys';

@@ -15,3 +15,3 @@

database,
schema: database.$.schema.tables.get(tblName),
name: tblName,
params

@@ -24,3 +24,3 @@ };

*/
get name() { return this.$.schema.name; }
get name() { return this.$.name; }

@@ -47,36 +47,2 @@ /**

/**
* Returns the table's current savepoint.
*
* @param Object params
*
* @returns Object
*/
async savepoint(params = {}) {
const OBJ_INFOSCHEMA_DB = this.database.client.constructor.OBJ_INFOSCHEMA_DB;
if ((await this.database.client.databases({ name: OBJ_INFOSCHEMA_DB }))[0]) {
const forward = params.direction === 'forward';
const dbName = [OBJ_INFOSCHEMA_DB,'database_savepoints'];
const tblName = [OBJ_INFOSCHEMA_DB,'table_savepoints'];
const tblFields = ['name_snapshot', 'columns_snapshot', 'constraints_snapshot', 'indexes_snapshot', 'current_name'];
const dbFields = ['id', 'name_snapshot', 'savepoint_desc', 'savepoint_date', 'rollback_date', 'current_name'];
const result = await this.database.client.query('select', q => {
q.from(tblName).as('tbl');
q.select( ...tblFields.map(name => ['tbl',name]) );
q.select( ...dbFields.map(name => f => f.name(['db',name]).as(`db_${ name }`)) );
q.rightJoin(dbName).as('db').on( x => x.equals(['db','id'], ['tbl','savepoint_id']), x => x.in( y => y.literal(this.database.name), ['db','name_snapshot'], ['db','current_name'] ), x => x[forward ? 'isNotNull' : 'isNull'](['db','rollback_date']) );
q.where( x => x.in( x => x.literal(this.name), ['tbl','name_snapshot'], ['tbl','current_name'] ) );
q.orderBy(['db','savepoint_date']).withFlag(forward ? 'ASC' : 'DESC');
q.limit(1);
});
if (!result[0]) return;
const [ tblDetails, dbDetails ] = Object.keys(result[0]).reduce(([tblDetails, dbDetails], key) => {
if (key.startsWith('db_')) return [tblDetails, { ...dbDetails, [key.replace('db_', '')]: result[0][key] }];
return [{ ...tblDetails, [key]: result[0][key] }, dbDetails];
}, [{}, {}]);
const context = new Savepoint(this.database.client, dbDetails, params.direction);
return Object.defineProperty(tblDetails, 'context', { get: () => context, });
}
}
/**

@@ -104,3 +70,3 @@ * ----------

const schema = await this.database.describeTable(this.name);
const inlineConstraintTypesMap = { 'PRIMARY_KEY': 'primaryKey', 'UNIQUE': 'uniqueKey', 'CHECK': 'check', 'FOREIGN_KEY': 'references' };
const inlineConstraintTypesMap = { 'PRIMARY_KEY': 'primaryKey', 'UNIQUE_KEY': 'uniqueKey', 'CHECK': 'check', 'FOREIGN_KEY': 'references' };
let columns = !(constraintType in inlineConstraintTypesMap) ? [] : schema.columns.filter(col => col[inlineConstraintTypesMap[constraintType]]).map(col => [col.name]);

@@ -156,3 +122,3 @@ if (schema.constraints.length) { columns = columns.concat(schema.constraints.filter(cnst => cnst.type === constraintType).reduce((cols, cnst) => cols.concat([cnst.columns]))); }

const primaryKeyColumns = await this.primaryKeyColumns();
const uniqueKeyColumns = await this.columnsForConstraint('UNIQUE');
const uniqueKeyColumns = await this.columnsForConstraint('UNIQUE_KEY');
primaryKeyColumns.concat(uniqueKeyColumns).map(columns => {

@@ -159,0 +125,0 @@ return `(${ columns.map(col => `${ this.quote(obj[col]) } IN (${ columns.join(',') })`).join(' AND ') })`;

@@ -41,3 +41,3 @@

} else {
var uniqueColumns = this.table.columnsForConstraint('UNIQUE');
var uniqueColumns = this.table.columnsForConstraint('UNIQUE_KEY');
keyPaths = uniqueColumns.filter(keyPath => _any(keyPath, columnName => this.columns.includes(columnName)));

@@ -44,0 +44,0 @@ wheres.by = 'uniqueKeys';

import CreateDatabase from "../../query/create/CreateDatabase.js";
export default class Savepoint {

@@ -7,6 +9,6 @@

*/
constructor(client, details, direction = 'backward') {
constructor(client, json, direction = 'backward') {
Object.defineProperty(this, '$', { value: {
client,
details,
json,
direction,

@@ -17,3 +19,3 @@ }});

/**
* @property Driver
* @returns Driver
*/

@@ -23,40 +25,43 @@ get client() { return this.$.client; }

/**
* @property String
* @returns String
*/
get id() { return this.$.details.id; }
get direction() { return this.$.direction; }
/**
* @property String
* @returns String
*/
get name_snapshot() { return this.$.details.name_snapshot; }
get id() { return this.$.json.id; }
/**
* @property String
* @returns String
*/
get savepoint_desc() { return this.$.details.savepoint_desc; }
get description() { return this.$.json.savepoint_description; }
/**
* @property Date
* @returns String
*/
get savepoint_date() { return this.$.details.savepoint_date; }
get databaseTag() { return this.$.json.database_tag; }
/**
* @property Date
* @returns Number
*/
get rollback_date() { return this.$.details.rollback_date; }
get versionTag() { return this.$.json.version_tag; }
/**
* @property String
* @returns Date
*/
get current_name() { return this.$.details.current_name; }
get savepointDate() { return this.$.json.savepoint_date; }
/**
* @property Bool
* @returns Date
*/
get id_active() { return 'id_active' in this.$.details ? this.$.details.id_active : undefined }
get rollbackDate() { return this.$.json.rollback_date; }
/**
* @property String
* @returns Object
*/
get direction() { return this.$.direction; }
schema() {
const { name, $name, tables = [], status } = this.$.json;
return { name, ...($name ? { $name } : {}), tables, status };
}

@@ -66,3 +71,3 @@ /**

*/
toJson() { return { ...this.$.details }; }
toJson() { return { ...this.$.json }; }

@@ -72,104 +77,33 @@ /**

*/
async status() {
const currentSavepointInDb = (await this.client.database(this.current_name || this.name_snapshot).savepoint({ direction: this.direction })) || {};
if (currentSavepointInDb.id === this.id) {
this.$.details.rollback_date = currentSavepointInDb.rollback_date;
this.$.details.id_active = currentSavepointInDb.id_active;
return { canRollback: true };
}
return { canRollback: false };
async canRollback() {
const dbName = this.$.json.rollback_date ? this.$.json.name : this.$.json.$name || this.$.json.name;
const currentSavepoint = (await this.client.database(dbName).savepoint({ direction: this.direction })) || {};
return currentSavepoint.id === this.$.json.id;
}
/**
* Returns tables associated with current savepoint.
*
* @return Array
*/
async getAssociatedSnapshots() {
const OBJ_INFOSCHEMA_DB = this.client.constructor.OBJ_INFOSCHEMA_DB;
return this.client.query('select', q => {
q.select('*');
q.from([OBJ_INFOSCHEMA_DB,'table_savepoints']);
q.where( c => c.equals('savepoint_id', q => q.literal(this.id)) );
});
}
/**
* Method for restoring db schema to an identified savepoint.
*
* @param Object params
*
* @return Object
* @return Void
*/
async rollback(params = {}) {
// Validate instance
if (!this.current_name && !this.name_snapshot) throw new Error(`Invalid savepoint; null record.`);
if (!(await this.status()).canRollback) throw new Error(`Invalid rollback order.`);
// Validated
const getTableSnapshots = async () => {
const tableSnapshots = await this.getAssociatedSnapshots();
return tableSnapshots.map(tableSnapshot => ({
// Identity
name: tableSnapshot.name_snapshot,
$name: tableSnapshot.current_name,
database: this.current_name,
// Lists
columns: tableSnapshot.columns_snapshot.map(col => ({
...col,
...(this.direction === 'forward' && col.$name ? { name: col.$name, $name: col.name } : {}),
})),
constraints: tableSnapshot.constraints_snapshot.map(cnst => ({
...cnst,
...(this.direction === 'forward' && cnst.$constraintName ? { constraintName: cnst.$constraintName, $constraintName: cnst.constraintName } : {}),
})),
indexes: tableSnapshot.indexes_snapshot.map(ndx => ({
...ndx,
...(this.direction === 'forward' && ndx.$indexName ? { indexName: ndx.$indexName, $indexName: ndx.indexName } : {}),
})),
}));
};
const errors = {}, noCreateSavepoint = this.direction === 'forward' || this.id_active;
if (!this.name_snapshot) {
// We are at db's creation point. Drop database - to non existence.
if (params.allowMutateDB) {
await this.client.dropDatabase(this.current_name, { cascade: true, noCreateSavepoint });
} else { errors.noMutateDB = true; }
} else if (!this.current_name) {
// We are at db's drop point. Recreate database - back to existence.
if (params.allowMutateDB) {
await this.client.createDatabase({ name: this.name_snapshot, tables: await getTableSnapshots() }, { noCreateSavepoint });
} else { errors.noMutateDB = true; }
} else {
const tables = await getTableSnapshots();
await this.client.alterDatabase({ name: this.current_name, tables: tables.map(tbl => tbl.$name/*if tbl is in db*/).filter(tblName => tblName) }, dbSchemaEdit => {
dbSchemaEdit.name = this.name_snapshot;
dbSchemaEdit.tables.splice(0);
dbSchemaEdit.tables.push(...tables.filter(tbl => tbl.name/*if tbl isn't in db*/));
}, { noCreateSavepoint });
async rollback() {
if (!(await this.canRollback())) throw new Error(`Invalid rollback order.`);
const schemaInstance = CreateDatabase.fromJson(this.client, this.schema());
if (!this.$.json.rollback_date) {
schemaInstance.reverseAlt(true);
schemaInstance.status(schemaInstance.status(), true);
}
if (Object.keys(errors).length) return false;
// Update records now
const OBJ_INFOSCHEMA_DB = this.client.constructor.OBJ_INFOSCHEMA_DB;
const dbName = [OBJ_INFOSCHEMA_DB,'database_savepoints'];
if (this.direction === 'forward') {
this.$.details.rollback_date = null;
await this.client.query('update', q => {
q.table(dbName);
q.set('rollback_date', null);
q.where( x => x.equals('current_name', y => y.literal(this.name_snapshot)), x => x.isNotNull('rollback_date') );
});
} else {
this.$.details.rollback_date = new Date;
await this.client.query('update', q => {
q.table(dbName);
q.set('rollback_date', x => x.call('now'));
q.where( x => x.or(
y => y.equals('id', z => z.literal(this.id)),
// the following is faulty. forces unrolled back savepoints to a rolled back state
y => y.and( z => z.equals('name_snapshot', z => z.literal(this.current_name) ), z => z.isNull('rollback_date') )
) );
});
}
// Execute rollback
if (schemaInstance.status() === 'DOWN') {
this.client.dropDatabase(schemaInstance.name(), { cascade: true, noCreateSavepoint: true });
} else if (schemaInstance.status() === 'UP') {
const altInstance = schemaInstance.getAlt().with({ resultSchema: schemaInstance });
this.client.query(altInstance, { noCreateSavepoint: true });
} else this.client.query(schemaInstance, { noCreateSavepoint: true });
// Update record
const tblName = [this.client.constructor.OBJ_INFOSCHEMA_DB,'database_savepoints'].join('.');
await this.client.query(`UPDATE ${ tblName } SET rollback_date = ${ this.$.json.rollback_date ? 'NULL' : 'now()' } WHERE id = '${ this.$.json.id }'`);
this.$.json.rollback_date = this.$.json.rollback_date ? null : Date.now();
return true;
}
}

@@ -34,48 +34,3 @@

/**
* List: system database.
*
* @var Array
*/
static systemDBs = [ 'information_schema', 'mysql', 'performance_schema', 'sys', 'pg_catalog', 'pg_toast' ];
/**
* Sets default database.
*
* @param String dbName
* @param Object params
*
* @return String|Null
*/
async searchPath(...args) {
return this.searchPathCallback(path => {
return new Promise((resolve, reject) => {
const driver = this.driver;
if (path) {
path = path.map(name => Identifier.fromJson(this, name));
const sql = this.params.dialect === 'mysql' ? `USE ${ path[0] }` : `SET SEARCH_PATH TO ${ path.join(',') }`;
return driver.query(sql, (err, result) => {
if (err) return reject(err);
resolve(result);
});
}
let sql, key;
if (this.params.dialect === 'mysql') {
sql = 'SELECT database() AS default_db', key = 'default_db';
} else {
// Here, what we need is SHOW SEARCH_PATH not SELECT current_database()
sql = `SHOW SEARCH_PATH`, key = 'search_path';
sql = `SELECT current_setting('SEARCH_PATH')`, key = 'current_setting';
}
return driver.query(sql, (err, result) => {
if (err) return reject(err);
const rows = result.rows || result;
const value = (rows[0] || {})[key];
resolve(Lexer.split(value, [',']).map(s => Identifier.parseIdent(this, s.trim())[0]));
});
});
}, ...args);
}
/**
* Returns a list of databases.

@@ -87,30 +42,21 @@ *

*/
async databases(params = {}) {
return this.databasesCallback(() => {
return new Promise((resolve, reject) => {
const sql = `SELECT schema_name FROM information_schema.schemata`;
return this.driver.query(sql, (err, result) => {
if (err) return reject(err);
resolve((result.rows || result).map(row => row.schema_name));
});
});
}, params, this.constructor.systemDBs);
async databases() {
const sql = `SELECT schema_name FROM information_schema.schemata`;
const result = await this.driver.query(sql);
return (result.rows || result).map(row => row.schema_name);
}
/**
* Creates a database.
* Runs a query.
*
* @param String dbName
* @param Object params
* @param String query
* @param Object params
*
* @return Bool
* @return Any
*/
async createDatabase(dbName, params = {}) {
return this.createDatabaseCallback((dbCreateInstance, handleTables, params) => {
return new Promise((resolve, reject) => {
return this.driver.query(dbCreateInstance.toString(), (err, result) => {
if (err) return reject(err);
resolve(result);
});
});
async query(query, params = {}) {
return await this.queryCallback(async (queryInstance, params) => {
if (queryInstance.expandable) await queryInstance.expand(true);
const result = await this.driver.query(queryInstance.toString(), params.params || []);
return result.rows || result;
}, ...arguments);

@@ -120,62 +66,25 @@ }

/**
* Alters a database.
* Sets or returns the search path for resolving unqualified table references.
*
* @param String dbName
* @param Function schemaCallback
* @param Object params
* @param Array|String resolutionPath
*
* @return Bool
* @return Array
*/
async alterDatabase(dbName, schemaCallback, params = {}) {
return this.alterDatabaseCallback(async (dbAlterInstance, handleTables, params) => {
if (!dbAlterInstance.ACTIONS.length) return;
await handleTables(); // Handle tables before rename DB
return new Promise((resolve, reject) => {
return this.driver.query(dbAlterInstance.toString(), (err, result) => {
if (err) return reject(err);
resolve(result);
});
});
}, ...arguments);
async basenameResolution(resolutionPath = []) {
if (arguments.length) {
resolutionPath = [].concat(resolutionPath).map(name => Identifier.fromJson(this, name));
const sql = this.params.dialect === 'mysql' ? `USE ${ resolutionPath[0] }` : `SET SEARCH_PATH TO ${ resolutionPath.join(',') }`;
return await this.driver.query(sql);
}
let sql, key;
if (this.params.dialect === 'mysql') {
sql = 'SELECT database() AS default_db', key = 'default_db';
} else {
sql = `SHOW SEARCH_PATH`, key = 'search_path'; // Can't remember what happens here
sql = `SELECT current_setting('SEARCH_PATH')`, key = 'current_setting';
}
const result = await this.driver.query(sql);
const value = ((result.rows || result)[0] || {})[key];
return Lexer.split(value, [',']).map(s => Identifier.parseIdent(this, s.trim())[0]);
}
/**
* Drops a database.
*
* @param String dbName
* @param Object params
*
* @return Bool
*/
async dropDatabase(dbName, params = {}) {
return this.dropDatabaseCallback((dbDropInstance, params) => {
return new Promise((resolve, reject) => {
return this.driver.query(dbDropInstance.toString(), (err, result) => {
if (err) return reject(err);
resolve(result);
});
});
}, ...arguments);
}
/**
* ---------
* QUERY
* ---------
*/
/**
* @inheritdoc
*/
async query(...query) {
return this.queryCallback(async (query, params) => {
if (query.expandable) await query.expand(true);
return new Promise((resolve, reject) => {
this.driver.query(`${ query }`, params.params || [], (err, result) => {
if (err) return reject(err);
resolve(result.rows || result);
});
});
}, ...query);
}
}

@@ -7,8 +7,2 @@

/**
* ---------------------------
* SQLDatabase class
* ---------------------------
*/
export default class SQLDatabase extends AbstractDatabase {

@@ -26,16 +20,8 @@

*
* @param Object params
*
* @return Array
*/
async tables(params = {}) {
return this.tablesCallback(() => {
return new Promise((resolve, reject) => {
const sql = `SELECT table_name FROM information_schema.tables WHERE table_schema = '${ this.name }'`;
return this.client.driver.query(sql, (err, result) => {
if (err) return reject(err);
resolve((result.rows || result).map(row => row.table_name));
});
});
}, ...arguments);
async tables() {
const sql = `SELECT table_name FROM information_schema.tables WHERE table_schema = '${ this.name }'`;
const result = await this.client.driver.query(sql);
return (result.rows || result).map(row => row.table_name);
}

@@ -49,81 +35,15 @@

*
* @return Object
* @return Object|Array
*/
describeTable(tblName_s, params = {}) {
return this.describeTableCallback((tblNames, params) => {
return new Promise((resolve, reject) => {
const driver = this.client.driver;
const [ sql0, sql1 ] = this.getDescribeTableSql(tblNames);
return driver.query(sql0, (err, columns) => {
if (err) return reject(err);
return driver.query(sql1, (err, constraints) => {
if (err) return reject(err);
const tblSchemas = this.formatDescribeTableResult(tblNames, (columns.rows || columns), (constraints.rows || constraints), []);
resolve(tblSchemas);
});
});
});
}, ...arguments);
async describeTable(tblName_s, params = {}) {
const isSingle = !Array.isArray(tblName_s) && tblName_s !== '*';
const tblNames = [].concat(tblName_s);
const [ sql0, sql1 ] = this.getDescribeTableSql(tblNames);
const columns = await this.client.driver.query(sql0);
const constraints = await this.client.driver.query(sql1);
const schemas = this.formatDescribeTableResult(tblNames, (columns.rows || columns), (constraints.rows || constraints), []);
return isSingle ? schemas[0] : schemas;
}
/**
* Creates table.
*
* @param Object tblSchema
* @param Object params
*
* @return Object
*/
async createTable(tblSchema, params = {}) {
return this.createTableCallback((tblCreateRequest, params) => {
return new Promise((resolve, reject) => {
return this.client.driver.query(tblCreateRequest.toString(), (err, result) => {
if (err) return reject(err);
resolve(this.formatSideEffectResult(result));
});
});
}, ...arguments);
}
/**
* Alters table.
*
* @param String tblName
* @param Function schemaCallback
* @param Object params
*
* @return Bool
*/
async alterTable(tblName, schemaCallback, params = {}) {
return this.alterTableCallback((tblAlterRequest, params) => {
if (!tblAlterRequest.ACTIONS.length) return;
return new Promise((resolve, reject) => {
return this.client.driver.query(tblAlterRequest.toString(), (err, result) => {
if (err) return reject(err);
resolve(this.formatSideEffectResult(result));
});
});
}, ...arguments);
}
/**
* Drops table.
*
* @param String tblName
* @param Object params
*
* @return Bool
*/
async dropTable(tblName, params = {}) {
return this.dropTableCallback((tblDropRequest, params) => {
return new Promise((resolve, reject) => {
return this.client.driver.query(tblDropRequest.toString(), (err, result) => {
if (err) return reject(err);
resolve(this.formatSideEffectResult(result));
});
});
}, ...arguments);
}
/**
* Composes the SQL for a SHOW TABLE operation.

@@ -236,6 +156,5 @@ *

const formatRelation = (key, tableScope = false) => ({
...(!tableScope ? { constraintName: key.constraint_name } : {}),
basename: key.referenced_table_schema,
table: key.referenced_table_name,
columns: key.referenced_column_name.split(',').map(s => s.trim()),
...(!tableScope ? { name: key.constraint_name } : {}),
targetTable: [key.referenced_table_schema,key.referenced_table_name],
targetColumns: key.referenced_column_name.split(',').map(s => s.trim()),
...(key.match_rule !== 'NONE' ? { matchRule: key.match_rule } : {}),

@@ -267,3 +186,2 @@ updateRule: key.update_rule,

name: tblName,
basename: this.name,
columns: $columns.reduce((cols, col) => {

@@ -275,6 +193,6 @@ const temp = {};

...(primaryKey.length === 1 && primaryKey[0].column_name === col.column_name && (temp.pKeys = primaryKey.pop()) ? {
primaryKey: { constraintName: temp.pKeys.constraint_name }
primaryKey: { name: temp.pKeys.constraint_name }
} : {}),
...((temp.uKeys = uniqueKeys.filter(key => key.column_name === col.column_name)).length === 1 && (uniqueKeys = uniqueKeys.filter(key => key !== temp.uKeys[0])) ? {
uniqueKey: { constraintName: temp.uKeys[0].constraint_name }
uniqueKey: { name: temp.uKeys[0].constraint_name }
} : {}),

@@ -285,3 +203,3 @@ ...((temp.fKeys = foreignKeys.filter(key => key.column_name === col.column_name)).length === 1 && (foreignKeys = foreignKeys.filter(key => key !== temp.fKeys[0])) ? {

...((temp.cKeys = checks.filter(key => key.check_constraint_level !== 'Table' && key.columns.length === 1 && key.columns[0] === col.column_name)).length === 1 && (checks = checks.filter(key => key !== temp.cKeys[0])) ? {
check: { constraintName: temp.cKeys[0].constraint_name, expr: temp.cKeys[0].check_clause }
check: { name: temp.cKeys[0].constraint_name, expr: temp.cKeys[0].check_clause }
} : {}),

@@ -298,3 +216,3 @@ ...(col.is_identity !== 'NO' ? {

...(col.default ? {
default: col.default
default: { expr: col.default }
} : {}),

@@ -307,4 +225,4 @@ });

schema.constraints.push(...[...primaryKey, ...uniqueKeys, ...foreignKeys].map(key => ({
constraintName: key.constraint_name,
type: key.constraint_type,
name: key.constraint_name,
type: key.constraint_type === 'UNIQUE' ? 'UNIQUE_KEY' : key.constraint_type,
columns: key.column_name.split(',').map(col => col.trim()),

@@ -314,3 +232,3 @@ ...(key.constraint_type === 'FOREIGN KEY' ? { references: formatRelation(key, true) } : {}),

schema.constraints.push(...checks.map(key => ({
constraintName: key.constraint_name,
name: key.constraint_name,
type: key.constraint_type,

@@ -323,11 +241,2 @@ columns: key.columns,

}
/**
* Standardizes the return value of a side-effect query.
*
* @param Array|Object result
*
* @returns Object
*/
formatSideEffectResult(result) { return result; }
}

@@ -5,11 +5,6 @@

import SQLDeleteQueryInspector from './SQLDeleteQueryInspector.js';
import Identifier from '../../query/select/Identifier.js';
import AbstractTable from '../abstracts/AbstractTable.js';
import SQLCursor from './SQLCursor.js';
/**
* ---------------------------
* SQLTable class
* ---------------------------
*/
export default class SQLTable extends AbstractTable {

@@ -101,3 +96,3 @@

return new Promise((resolve, reject) => {
let insertSql = `INSERT INTO ${ this.database.name }.${ this.name }\n\t(${ Object.keys(rowObj).join(',') })\n\t`;
let insertSql = `INSERT INTO ${ this.database.name }.${ this.name }\n\t(${ Object.keys(rowObj).map(key => Identifier.fromJson(this, key)).join(', ') })\n\t`;
insertSql += `VALUES\n\t${ formatAddRow(Object.values(rowObj), this.database.client.params.dialect) }\n\t`;

@@ -196,6 +191,11 @@ insertSql += 'RETURNING *';

}
return _isNumeric(val) ? val : (_isNull(val) ? 'NULL' : (dialect === 'mysql' ? `'${ val.replace(/'/g, `\\'`) }'` : `'${ val.replace(/'/g, `''`) }'`));
if (val instanceof String) return val + '';
if (_isNumeric(val)) return val;
if (_isNull(val)) return 'NULL';
if (Array.isArray(val) || _isObject(val)) (val = JSON.stringify(val));
if (typeof val === 'string') return `'${ val.replace(/'/g, `''`) }'`;
throw new Error(`Couldn't serialize payload.`);
};
const formatAddRow = (values, dialect) => '(' + values.map(val => formatVal(val, dialect)).join(',') + ')';
const formatAssignments = (rowObj, dialect) => Object.keys(rowObj).map(key => `${ key } = ${ formatVal(rowObj[key], dialect) }`).join(',');
const formatAssignments = (rowObj, dialect) => Object.keys(rowObj).map(key => `${ Identifier.fromJson(this, key) } = ${ formatVal(rowObj[key], dialect) }`).join(',');
const formatPutRow = (rowObj, dialect) => {

@@ -202,0 +202,0 @@ const assignments = formatAssignments(rowObj, dialect);

@@ -40,3 +40,3 @@ import Lexer from '../Lexer.js';

*/
get rootNode() { return this.CONTEXT && this.CONTEXT instanceof Node ? this.CONTEXT.rootNode : this; }
get rootNode() { return this.CONTEXT instanceof Node ? this.CONTEXT.rootNode : this; }

@@ -46,3 +46,3 @@ /**

*/
get statementNode() { return this.CONTEXT && this.CONTEXT instanceof Node ? this.CONTEXT.statementNode : this; }
get statementNode() { return this.CONTEXT instanceof Node ? this.CONTEXT.statementNode : null; }

@@ -54,2 +54,19 @@ /**

*/
/**
* @property Array
*/
get quoteChars() { return this.constructor.getQuoteChars(this); }
/**
* A Quote helper
*
* @param String string
*
* @returns String
*/
quote(string) {
const quoteChar = this.quoteChars[0];
return `${ quoteChar }${ string.replace(new RegExp(quoteChar, 'g'), quoteChar.repeat(2)) }${ quoteChar }`;
}

@@ -65,28 +82,23 @@ /**

const dialect = (asInputDialect && context?.params?.inputDialect) || context?.params?.dialect;
return dialect === 'mysql' && !context.params.ansiQuotes ? ['"', "'"] : ["'"];
return dialect === 'mysql' && !context.params.ansiQuotes ? ["'", '"'] : ["'"];
}
/**
* @property Array
* @property String
*/
get quoteChars() { return this.constructor.getQuoteChars(this); }
get escChar() { return this.constructor.getEscChar(this); }
/**
* Determines the proper escape character for the active SQL dialect ascertained from context.
* An Escape helper
*
* @param Node|AbstractClient context
* @param String|Array string_s
*
* @returns String
*/
static getEscChar(context, asInputDialect = false) {
const dialect = (asInputDialect && context?.params?.inputDialect) || context?.params?.dialect;
return dialect === 'mysql' && !context.params.ansiQuotes ? '`' : '"';
autoEsc(string_s) {
const $strings = (Array.isArray(string_s) ? string_s : [string_s]).map(s => s && !/^(\*|[\w]+)$/.test(s) ? `${ this.escChar }${ s.replace(new RegExp(this.escChar, 'g'), this.escChar.repeat(2)) }${ this.escChar }` : s );
return Array.isArray(string_s) ? $strings : $strings[0];
}
/**
* @property String
*/
get escChar() { return this.constructor.getEscChar(this); }
/**
* @inheritdoc

@@ -98,2 +110,14 @@ */

}
/**
* Determines the proper escape character for the active SQL dialect ascertained from context.
*
* @param Node|AbstractClient context
*
* @returns String
*/
static getEscChar(context, asInputDialect = false) {
const dialect = (asInputDialect && context?.params?.inputDialect) || context?.params?.dialect;
return dialect === 'mysql' && !context.params.ansiQuotes ? '`' : '"';
}

@@ -113,14 +137,2 @@ /**

/**
* An Escape helper
*
* @param String|Array string_s
*
* @returns String
*/
autoEsc(string_s) {
const $strings = (Array.isArray(string_s) ? string_s : [string_s]).map(s => s && !/^(\*|[\w]+)$/.test(s) ? `${ this.escChar }${ s.replace(new RegExp(this.escChar, 'g'), this.escChar.repeat(2)) }${ this.escChar }` : s );
return Array.isArray(string_s) ? $strings : $strings[0];
}
/**
* -----------

@@ -151,3 +163,14 @@ * QUERY BUILDER

withFlag(...flags) {
this.FLAGS.push(...flags.filter(f => f).map(flag => flag.toUpperCase()));
flags = new Set(flags.filter(f => f));
this.FLAGS = this.FLAGS.reduce(($flags, $flag) => {
const a = $flag.split(':');
for (const flag of flags) {
const b = flag.split(':');
if (b[0] === a[0]) {
$flag = [...(new Set([...a, ...b]))].join(':');
flags.delete(flag);
}
}
return $flags.concat($flag);
}, []).concat(...flags);
return this;

@@ -161,5 +184,20 @@ }

*
* @return String
*/
getFlag(flag) {
const b = flag.toUpperCase().split(':');
return this.FLAGS.find($flag => {
const a = $flag.split(':');
return b[0] === a[0] && b.every(f => a.includes(f));
});
}
/**
* Helper for inspecting flags on the instance.
*
* @params String flag
*
* @return Bool
*/
hasFlag(flag) { return this.FLAGS.includes(flag.toUpperCase()); }
hasFlag(flag) { return !!this.getFlag(flag); }

@@ -233,3 +271,3 @@ /**

}
throw new Error(`Arguments must be of type ${ Types.map(Type => Type.name).join(', ') } or a JSON equivalent. Recieved: ${ typeof arg }`);
throw new Error(`Arguments must be of type ${ Types.map(Type => Type.name).join(', ') } or a JSON equivalent. Recieved: ${ typeof arg === 'object' && arg ? arg.constructor.name : typeof arg }`);
}

@@ -236,0 +274,0 @@ }

import Node from '../abstracts/Node.js';
import TableLevelConstraint from '../create/TableLevelConstraint.js';
import ColumnLevelConstraint from '../create/ColumnLevelConstraint.js';
import CreateTable from '../create/CreateTable.js';
import DataType from '../create/DataType.js';

@@ -25,3 +24,3 @@ import Column from '../create/Column.js';

*/
renameTo(newName) {
rename(newName) {
this.TYPE = 'RENAME';

@@ -33,3 +32,3 @@ this.ARGUMENT = newName;

/**
* Adds a "RELOCATE" action to the instance,
* Adds a "MOVE" action to the instance,
*

@@ -40,4 +39,4 @@ * @param String newName

*/
relocateTo(newDb) {
this.TYPE = 'RELOCATE';
move(newDb) {
this.TYPE = 'MOVE';
this.ARGUMENT = newDb;

@@ -67,4 +66,4 @@ return this;

*/
add(argument) {
this.TYPE = 'ADD';
new(argument) {
this.TYPE = 'NEW';
this.ARGUMENT = argument;

@@ -91,2 +90,3 @@ return this;

* @param Object reference
* @param Any argument
*

@@ -102,2 +102,30 @@ * @returns this

/**
* Adds a "CHANGE" action to the instance,
*
* @param Object argument
* @param Column argument
*
* @returns this
*/
change(reference, argument) {
this.TYPE = 'CHANGE';
this.REFERENCE = reference;
this.ARGUMENT = argument;
return this;
}
/**
* Adds a "OWNER" action to the instance,
*
* @param Column argument
*
* @returns this
*/
owner(argument) {
this.TYPE = 'OWNER';
this.ARGUMENT = argument;
return this;
}
/**

@@ -111,3 +139,3 @@ * @inheritdoc

argument: typeof this.ARGUMENT?.toJson === 'function' ? this.ARGUMENT.toJson() : this.ARGUMENT,
flags: this.FLAGS,
...(this.FLAGS.length ? { flags: this.FLAGS } : {}),
}

@@ -122,11 +150,14 @@ }

const instance = (new this(context)).withFlag(...(json.flags || []));
// RENAME/RELOCATE
if (['RENAME','RELOCATE'].includes(json.type)) {
instance[json.type === 'RENAME' ? 'renameTo' : 'relocateTo'](json.argument);
// RENAME/MOVE
if (['RENAME','MOVE'].includes(json.type)) {
instance[json.type === 'RENAME' ? 'rename' : 'move'](json.argument);
return instance;
}
// DROP/ADD
if (['DROP','ADD','SET'].includes(json.type)) {
const argument = [TableLevelConstraint,Index,Column].reduce((prev, Class) => prev || Class.fromJson(context, json.argument), null);
instance[json.type === 'DROP' ? 'drop' : (json.type === 'SET' ? 'set' : 'add')](argument);
if (['DROP','NEW','SET'].includes(json.type)) {
let Classes = [];
if (['NEW','SET'].includes(json.type)) Classes = [...Column.CONSTRAINT_TYPES,DataType];
if (json.type === 'NEW') Classes = [...CreateTable.CONSTRAINT_TYPES.concat(Classes),Index,Column];
const argument = Classes.reduce((prev, Class) => prev || Class.fromJson(context, json.argument), null) || json.argument;
instance[json.type.toLowerCase()](argument);
return instance;

@@ -138,14 +169,13 @@ }

const { reference, argument: subAction } = json;
let arg = subAction.argument;
if (reference.kind === 'COLUMN') {
arg = [ColumnLevelConstraint,DataType].reduce((prev, Class) => prev || Class.fromJson(context, arg), null) || arg;
} else {
const Class = reference.kind === 'CONSTRAINT' ? TableLevelConstraint : Index;
arg = Class.fromJson(context, arg) || arg;
}
const methodName = subAction.type.toLowerCase() + (['RENAME', 'RELOCATE'].includes(subAction.type) ? 'To' : '');
instance.alter(reference, a => a[methodName](arg));
instance.alter(reference, this.fromJson(instance, subAction));
return instance;;
}
// ALTER
if (json.type === 'CHANGE') {
// Handle columns specially
const { reference, argument } = json;
instance.change(reference, Column.fromJson(instance, argument));
return instance;;
}
}
}
import Lexer from '../Lexer.js';
import StatementNode from '../abstracts/StatementNode.js';
import AbstractStatementNode from './abstracts/AbstractStatementNode.js';
import Action from './Action.js';
export default class AlterDatabase extends StatementNode {
/**
* Instance properties
*/
NAME = '';
ACTIONS = [];
export default class AlterDatabase extends AbstractStatementNode {
/**
* @constructor
*/
constructor(context, name) {
super(context);
this.NAME = name;
}
/**
* Sets the name
* Adds a "OWNER TO" action to the instance.
*
* @param String name
* @param String newOwner
*
* @returns Void
*/
name(name) { this.NAME = name; }
/**
* Adds a "RENAME" action to the instance,
*
* @param String newName
*
* @returns Action
*/
renameTo(newName) { return this.build('ACTIONS', [newName], Action, 'renameTo'); }
/**
* @inheritdoc
*/
toJson() {
return {
name: this.NAME,
actions: this.ACTIONS.map(action => action.toJson()),
flags: this.FLAGS,
};
}
/**
* @inheritdoc
*/
static fromJson(context, json) {
if (typeof json?.name !== 'string') return;
const instance = (new this(context, json.name)).withFlag(...(json.flags || []));
for (const action of json.actions) {
instance.ACTIONS.push(Action.fromJson(context, action));
}
return instance;
}
addOwner(newOwner) { return this.build('ACTIONS', [newOwner], Action, 'owner'); }

@@ -67,5 +21,38 @@ /**

stringify() {
const newDbName = this.ACTIONS.find(action => action.TYPE === 'RENAME' && !action.REFERENCE)?.ARGUMENT;
if (!newDbName) return '';
return `ALTER SCHEMA${ this.hasFlag('IF_EXISTS') ? ' IF EXISTS' : '' } ${ this.autoEsc(this.NAME) } RENAME TO ${ this.autoEsc(newDbName) }`;
if (!this.ACTIONS.length) return '';
let stmts = [], rename0, move0;
for (const action of this.ACTIONS) {
// RENAME TO...
if (action.TYPE === 'RENAME') {
rename0 = `RENAME TO ${ this.autoEsc(action.ARGUMENT) }`;
continue;
}
// MOVE TO...
if (action.TYPE === 'MOVE') {
move0 = `SET TABLESPACE ${ this.autoEsc(action.ARGUMENT) }`;
continue;
}
// DROP
if (action.TYPE === 'DROP') {
// All flags are postgres'
const ifExists = action.hasFlag('IF_EXISTS');
const restrictOrCascadeFlag = action.getFlag('RESTRICT') || action.getFlag('CASCADE');
stmts.push(`DROP TABLE${ ifExists ? ' IF EXISTS' : '' } ${ this.autoEsc([].concat(action.ARGUMENT.name)).join('.') }${ restrictOrCascadeFlag ? ` ${ restrictOrCascadeFlag }` : '' }`);
continue;
}
// ADD
if (action.TYPE === 'NEW') {
stmts.push(action.ARGUMENT+'');
continue;
}
// ALTER
if (action.TYPE === 'ALTER') {
const { REFERENCE: reference, ARGUMENT: subAction } = action;
stmts.push(subAction.ARGUMENT+'');
}
}
const sql = [ ...stmts ];
if (rename0) sql.push(`ALTER SCHEMA ${ this.autoEsc(this.NAME) }\n\t${ rename0 }`);
if (move0) sql.push(`ALTER SCHEMA ${ this.autoEsc(rename0 ? this.ACTIONS.find(action => action.TYPE === 'RENAME').ARGUMENT : this.NAME) }\n\t${ move0 }`);
return sql.join(';\n');
}

@@ -77,28 +64,39 @@

static parse(context, expr) {
const [ match, ifExists, rest ] = /^ALTER\s+DATABASE\s+(IF\s+EXISTS\s+)?([\s\S]+)$/i.exec(expr) || [];
const [ match, rest ] = /^ALTER\s+DATABASE\s+([\s\S]+)$/i.exec(expr) || [];
if (!match) return;
const [ name1Part, name2Part ] = Lexer.split(rest, ['RENAME\\s+TO'], { useRegex: 'i' });
const [name1] = this.parseIdent(context, name1Part.trim(), true) || [];
const [name2] = this.parseIdent(context, name2Part.trim(), true) || [];
if (!name1 || !name2) return;
const instance = new this(context, name1);
if (ifExists) instance.withFlag('IF_EXISTS');
instance.renameTo(name2);
return instance;
}
/**
* @inheritdoc
*/
static fromDiffing(context, jsonA, jsonB, flags = []) {
if (!jsonA.name) throw new Error(`Could not assertain database1 name or database1 name invalid.`);
if (!jsonB.name) throw new Error(`Could not assertain database2 name or database2 name invalid.`);
const instance = (new this(context, jsonA.name)).withFlag(...flags);
// RENAME TO...
if (jsonB.name !== jsonA.name) {
instance.renameTo(jsonB.name);
const [ namePart, bodyPart ] = Lexer.split(rest, ['\\s+'], { useRegex: true, limit: 1 });
const [ dbName ] = this.parseIdent(context, namePart.trim(), true) || [];
if (!dbName) return;
const instance = (new this(context)).name(dbName);
// ----------
const regex = name => new RegExp(`${ this[ name ].source }`, 'i');
// RENAME ... TO ...
const [ renameMatch, newNodeNameUnescaped_a, /*esc*/, newNodeNameEscaped_a ] = regex('renameRe').exec(bodyPart) || [];
if (renameMatch) {
const newNodeName = newNodeNameUnescaped_a || this.autoUnesc(instance, newNodeNameEscaped_a);
instance.addRename(newNodeName);
return instance;
}
// MOVE ... TO ...
const [ moveMatch, newSchemaUnescaped, /*esc*/, newSchemaEscaped ] = regex('moveRe').exec(bodyPart) || [];
if (moveMatch) {
instance.addMove(newSchemaUnescaped || this.autoUnesc(instance, newSchemaEscaped));
return instance;
}
// OWNER ... TO ...
const [ ownerMatch, newOwnerUnescaped, /*esc*/, newOwnerEscaped ] = regex('ownerRe').exec(bodyPart) || [];
if (ownerMatch) {
instance.addOwner(newOwnerUnescaped || this.autoUnesc(instance, newOwnerEscaped));
return instance;
}
return instance;
}
/**
* @property RegExp
*/
static renameRe = /^RENAME\s+TO\s+(?:(\w+)|([`"])((?:\2\2|[^\2])+)\2)$/;
static moveRe = /^SET\s+TABLESPACE\s+(?:(\w+)|([`"])((?:\2\2|[^\2])+)\2)$/;
static ownerRe = /^OWNER\s+TO\s+(?:(\w+)|([`"])((?:\2\2|[^\2])+)\2)$/;
}
import Lexer from '../Lexer.js';
import { _isObject, _isFunction } from '@webqit/util/js/index.js';
import StatementNode from '../abstracts/StatementNode.js';
import Action from './Action.js';
import Column from '../create/Column.js';
import CreateTable from '../create/CreateTable.js';
import TableLevelConstraint from '../create/TableLevelConstraint.js';
import ColumnLevelConstraint from '../create/ColumnLevelConstraint.js';
import AbstractStatementNode from './abstracts/AbstractStatementNode.js';
import DataType from '../create/DataType.js';
import Column from '../create/Column.js';
import Index from '../create/Index.js';
import Action from './Action.js';
export default class AlterTable extends StatementNode {
export default class AlterTable extends AbstractStatementNode {
/**
* Instance properties
*/
NAME = '';
BASENAME = '';
JSON_BEFORE = {};
ACTIONS = [];
/**
* @constructor
*/
constructor(context, name, basename = null, jsonBefore = {}) {
super(context);
this.NAME = name;
this.BASENAME = basename;
this.JSON_BEFORE = jsonBefore;
}
/**
* Sets the name
* Adds a "CHANGE" action to the instance.
*
* @param Array|String name
*
* @returns Void
*/
name(name) {
const nameParts = Array.isArray(name) ? [...name] : [name];
this.NAME = nameParts.pop();
this.BASENAME = nameParts.pop();
if (nameParts.length) throw new Error(`Idents can be maximum of two parts. Recieved: ${ nameParts.reverse().join('.') }.${ this.BASENAME }.${ this.NAME }`);
}
/**
* Adds a "RENAME" action to the instance,
*
* @param String newName
*
* @returns Action
*/
renameTo(newName) { return this.build('ACTIONS', [newName], Action, 'renameTo'); }
/**
* Adds a "RELOCATE" action to the instance,
*
* @param String newName
*
* @returns Action
*/
relocateTo(newDb) { return this.build('ACTIONS', [newDb], Action, 'relocateTo'); }
/**
* Adds a "DROP" action to the instance,
*
* @param Object argument
*
* @returns Action
*/
drop(argument) { return this.build('ACTIONS', [argument], Action, 'drop'); }
/**
* Adds a "ADD" action to the instance,
*
* @param Object argument
*
* @returns this
*/
add(argument) { return this.build('ACTIONS', [argument], Action, 'add'); }
/**
* Adds a "ALTER" action to the instance,
*
* @param Object reference
* @param Column argument
*
* @returns Action
*/
alter(reference, argument) { return this.build('ACTIONS', [reference, argument], Action, 'alter'); }
/**
* @inheritdoc
*/
toJson() {
return {
name: this.NAME,
basename: this.BASENAME,
jsonBefore: this.JSON_BEFORE,
actions: this.ACTIONS.map(action => action.toJson()),
flags: this.FLAGS,
};
}
/**
* @inheritdoc
*/
static fromJson(context, json) {
if (typeof json?.name !== 'string' || !Array.isArray(json.actions)) return;
const instance = (new this(context, json.name, json.basename, json.jsonBefore)).withFlag(...(json.flags || []));
for (const action of json.actions) {
instance.ACTIONS.push(Action.fromJson(instance, action));
}
return instance;
}
addChange(reference, argument) { return this.build('ACTIONS', [reference, argument], Action, 'change'); }

@@ -122,12 +27,12 @@ /**

if (!this.ACTIONS.length) return '';
const stmts0 = [], stmts1 = [];
let stmts = [], rename0, move0;
for (const action of this.ACTIONS) {
// RENAME TO...
if (action.TYPE === 'RENAME') {
stmts0.push(`RENAME TO ${ this.autoEsc(action.ARGUMENT) }`);
rename0 = `RENAME TO ${ this.autoEsc(action.ARGUMENT) }`;
continue;
}
// RELOCATE TO...
if (action.TYPE === 'RELOCATE') {
stmts0.push(`SET SCHEMA ${ this.autoEsc(action.ARGUMENT) }`);
// MOVE TO...
if (action.TYPE === 'MOVE') {
move0 = `SET SCHEMA ${ this.autoEsc(action.ARGUMENT) }`;
continue;

@@ -137,101 +42,68 @@ }

if (action.TYPE === 'DROP') {
// All flags are postgres'
const ifExists = action.hasFlag('IF_EXISTS');
const $flags = (action.FLAGS?.join(' ') || '').match(/RESTRICT|CASCADE/i) || [];
const nodeKind = action.ARGUMENT instanceof TableLevelConstraint ? 'CONSTRAINT' : (action.ARGUMENT instanceof Index ? 'INDEX' : 'COLUMN');
if (this.params.dialect === 'mysql' && nodeKind === 'CONSTRAINT' && action.ARGUMENT.CONSTRAINT_NAME === 'PRIMARY') {
stmts1.push(`DROP PRIMARY KEY`);
} else {
const nameKey = nodeKind === 'CONSTRAINT' ? 'CONSTRAINT_NAME' : (nodeKind === 'INDEX' ? 'INDEX_NAME' : 'NAME');
stmts1.push(`DROP ${ this.params.dialect === 'mysql' && nodeKind === 'CONSTRAINT' && action.ARGUMENT.TYPE/* being a table-level constraint */ === 'FOREIGN_KEY' ? 'FOREIGN KEY' : nodeKind }${ ifExists ? ' IF EXISTS' : '' } ${ this.autoEsc(action.ARGUMENT[nameKey]) }${ $flags.length ? ` ${ $flags[0] }` : '' }`);
}
const restrictOrCascadeFlag = action.getFlag('RESTRICT') || action.getFlag('CASCADE');
if (this.params.dialect === 'mysql' && ['PRIMARY_KEY', 'FOREIGN_KEY'].includes(action.ARGUMENT.kind)) {
if (action.ARGUMENT.kind === 'PRIMARY_KEY') stmts.push(`DROP PRIMARY KEY`);
else stmts.push(`DROP FOREIGN KEY ${ this.autoEsc(action.ARGUMENT.name) }`);
} else stmts.push(`DROP ${ action.ARGUMENT.kind === 'COLUMN' ? 'COLUMN' : 'CONSTRAINT' }${ ifExists ? ' IF EXISTS' : '' } ${ this.autoEsc(action.ARGUMENT.name) }${ restrictOrCascadeFlag ? ` ${ restrictOrCascadeFlag }` : '' }`);
continue;
}
// ADD
if (action.TYPE === 'ADD') {
if (action.TYPE === 'NEW') {
const ifNotExists = action.hasFlag('IF_NOT_EXISTS');
const [ , first, afterCol ] = /(FIRST)|AFTER\s+(\w+)/i.exec(action.FLAGS?.join(' ') || '') || [];
stmts1.push(`ADD ${ action.ARGUMENT instanceof Column ? `COLUMN ` : '' }${ ifNotExists ? 'IF NOT EXISTS ' : '' }${ action.ARGUMENT }${ first ? ' FIRST' : (afterCol ? ` AFTER ${ afterCol.toLowerCase() }` : '') }`);
const firstFlag = action.hasFlag('FIRST');
const afterFlag = action.getFlag('AFTER')?.replace('AFTER:', '');
stmts.push(`ADD ${ action.ARGUMENT instanceof Column ? `COLUMN ` : '' }${ ifNotExists ? `IF NOT EXISTS ` : '' }${ action.ARGUMENT }${ firstFlag ? ` FIRST` : (afterFlag ? ` AFTER ${ this.autoEsc([afterFlag]) }` : '') }`);
if (this.params.dialect === 'mysql' && action.ARGUMENT instanceof Column) {
const constraint = action.ARGUMENT.CONSTRAINTS.find(c => c.TYPE === 'FOREIGN_KEY');
if (constraint) stmts1.push(`ADD ${ TableLevelConstraint.fromColumnLevelConstraint(constraint, action.ARGUMENT.NAME) }`);
const constraint = action.ARGUMENT.foreignKey();
if (constraint) stmts.push(`ADD ${ ForeignKey2.fromJson(instance, constraint.toJson()).columns([action.ARGUMENT.name()]) }`);
}
continue;
}
// CHANGE
if (action.TYPE === 'CHANGE') {
const firstFlag = action.hasFlag('FIRST');
const afterFlag = action.getFlag('AFTER')?.replace('AFTER:', '');
stmts.push(`CHANGE COLUMN ${ this.autoEsc(action.REFERENCE.name) } ${ action.ARGUMENT }${ firstFlag ? ` FIRST` : (afterFlag ? ` AFTER ${ this.autoEsc([afterFlag]) }` : '') }`);
continue;
}
// ALTER
if (action.TYPE === 'ALTER') {
// Handle columns specially
const { REFERENCE:reference, ARGUMENT:subAction } = action;
const { REFERENCE: reference, ARGUMENT: subAction } = action;
// RENAME
if (subAction.TYPE === 'RENAME') {
stmts1.push(`RENAME ${ reference.kind } ${ this.autoEsc(reference.name) } TO ${ this.autoEsc(subAction.ARGUMENT) }`);
stmts.push(`RENAME ${ reference.kind } ${ this.autoEsc(reference.name) } TO ${ this.autoEsc(subAction.ARGUMENT) }`);
continue;
}
if (reference.kind === 'COLUMN') {
const asTableLevelConstraint = () => {
if (subAction.TYPE === 'ADD') {
stmts1.push(`ADD ${ TableLevelConstraint.fromColumnLevelConstraint(subAction.ARGUMENT, reference.name) }`);
} else {
let dropStatement = dropTarget => `DROP CONSTRAINT ${ this.autoEsc(dropTarget.CONSTRAINT_NAME) }`;
if (this.params.dialect === 'mysql' && ['PRIMARY_KEY', 'FOREIGN_KEY'].includes(dropTarget.TYPE)) {
dropStatement = dropTarget => dropTarget.TYPE === 'PRIMARY_KEY' ? `DROP PRIMARY KEY` : `DROP FOREIGN KEY ${ this.autoEsc(dropTarget.CONSTRAINT_NAME) }`;
}
if (subAction.TYPE === 'DROP') {
stmts1.push(dropStatement(subAction.ARGUMENT));
} else if (subAction.TYPE === 'SET') {
stmts1.push(dropStatement({ TYPE: subAction.ARGUMENT.TYPE, CONSTRAINT_NAME: reference.name })); // We process DROP first, then ADD
stmts1.push(`ADD ${ TableLevelConstraint.fromColumnLevelConstraint(subAction.ARGUMENT, reference.name) }`);
}
}
};
const asLiterals = () => {
stmts1.push(`ALTER COLUMN ${ this.autoEsc(reference.name) } ${ subAction.TYPE } ${ subAction.ARGUMENT }`);
};
if (this.params.dialect === 'mysql') {
if (subAction.ARGUMENT instanceof ColumnLevelConstraint) {
if (subAction.ARGUMENT.TYPE === 'DEFAULT') {
stmts1.push(`ALTER COLUMN ${ this.autoEsc(reference.name) } ${ subAction.TYPE === 'DROP' ? 'DROP' : 'SET' } ${ subAction.ARGUMENT }`);
} else if (['PRIMARY_KEY', 'FOREIGN_KEY', 'UNIQUE'].includes(subAction.ARGUMENT.TYPE)) {
asTableLevelConstraint();
} else {
asLiterals();
}
} else {
asLiterals();
}
} else {
if (subAction.ARGUMENT instanceof DataType) {
stmts1.push(`ALTER COLUMN ${ this.autoEsc(reference.name) } SET DATA TYPE ${ subAction.ARGUMENT }`);
} else if (subAction.ARGUMENT instanceof ColumnLevelConstraint) {
if (['IDENTITY', 'EXPRESSION', 'DEFAULT', 'NOT_NULL'].includes(subAction.ARGUMENT.TYPE)) {
if (subAction.TYPE === 'DROP' || (subAction.ARGUMENT.TYPE === 'IDENTITY' && subAction.TYPE === 'SET')) {
stmts1.push(`ALTER COLUMN ${ this.autoEsc(reference.name) } DROP ${ subAction.ARGUMENT.TYPE.replace(/_/, ' ') }${ subAction.TYPE === 'DROP' && ['IDENTITY', 'EXPRESSION'].includes(subAction.ARGUMENT.TYPE) && action.FLAGS?.includes('IF_EXISTS') ? ` IF EXISTS` : '' }`);
}
if (['ADD', 'SET'].includes(subAction.TYPE) && subAction.ARGUMENT.TYPE !== 'EXPRESSION'/* Can't add a generated expression to a column after definition */) {
stmts1.push(`ALTER COLUMN ${ this.autoEsc(reference.name) } ${ subAction.ARGUMENT.TYPE === 'IDENTITY' ? 'ADD' : 'SET' } ${ subAction.ARGUMENT }`);
}
} else if (['PRIMARY_KEY', 'FOREIGN_KEY', 'UNIQUE', 'CHECK'].includes(subAction.ARGUMENT.TYPE)) {
asTableLevelConstraint();
} else {
asLiterals();
}
} else {
asLiterals();
}
}
continue;
// Typically: SET TYPE // SET|DROP IDENTITY|EXPRESSION|DEFAULT|NOT_NULL
if (subAction.TYPE === 'SET' && subAction.ARGUMENT instanceof DataType) {
stmts.push(`ALTER COLUMN ${ this.autoEsc(reference.name) } SET DATA TYPE ${ subAction.ARGUMENT }`);
} else if (subAction.TYPE === 'DROP') {
const ifExists = ['IDENTITY', 'EXPRESSION'].includes(subAction.ARGUMENT) && action.hasFlag('IF_EXISTS');
stmts.push(`ALTER COLUMN ${ this.autoEsc(reference.name) } DROP ${ subAction.ARGUMENT.replace(/_/, ' ') }${ ifExists ? ` IF EXISTS` : '' }`);
} else if (reference.kind === 'COLUMN') {
const verb = subAction.ARGUMENT.TYPE === 'IDENTITY' ? 'ADD' : 'SET';
stmts.push(`ALTER COLUMN ${ this.autoEsc(reference.name) } ${ verb } ${ subAction.ARGUMENT }`);
}
if (typeof subAction.ARGUMENT === 'string') {
stmts1.push(`ALTER ${ reference.kind } ${ this.autoEsc(reference.name) } ${ subAction.ARGUMENT }`);
continue;
// Constraints section
if (['CONSTRAINT','INDEX'].includes(reference.kind)) {
stmts.push(`ALTER ${ reference.kind } ${ this.autoEsc(reference.name) } ${ subAction.ARGUMENT }`);
}
// From constraints diffing
let dropStatement = `DROP ${ reference.kind } ${ this.autoEsc(reference.name) }`;
if (this.params.dialect === 'mysql' && ['PRIMARY_KEY', 'FOREIGN_KEY'].includes(subAction.ARGUMENT.TYPE/* being a table-level constraint */)) {
dropStatement = subAction.ARGUMENT.TYPE === 'PRIMARY_KEY' ? `DROP PRIMARY KEY` : `DROP FOREIGN KEY ${ this.autoEsc(reference.name) }`;
}
stmts1.push(dropStatement, `ADD ${ subAction.ARGUMENT }`);
continue;
}
}
return `ALTER TABLE${ this.hasFlag('IF_EXISTS') ? ' IF EXISTS' : '' } ${ this.autoEsc([this.BASENAME, this.NAME].filter(s => s)).join('.') }\n\t${ [...stmts1, ...stmts0].join(',\n\t') }`;
let renames = [];
if (this.params.dialect !== 'mysql') {
[ stmts, renames ] = stmts.reduce(([stmts, renames], stmt) => {
if (stmt.startsWith('RENAME')) return [stmts, renames.concat(stmt)];
return [stmts.concat(stmt), renames];
}, [[], []]);
}
const sql = [];
const basename = this.BASENAME || (this.CONTEXT instanceof this.constructor.Node ? this.CONTEXT.NAME/* DB won't have actually been renamed */ : this.CONTEXT?.name);
if (stmts.length) sql.push(`ALTER TABLE${ this.hasFlag('IF_EXISTS') ? ' IF EXISTS' : '' } ${ this.autoEsc([basename, this.NAME].filter(s => s)).join('.') }\n\t${ stmts.join(',\n\t') }`);
for (const stmt of renames) sql.push(`ALTER TABLE${ this.hasFlag('IF_EXISTS') ? ' IF EXISTS' : '' } ${ this.autoEsc([basename, this.NAME].filter(s => s)).join('.') }\n\t${ stmt }`);
if (rename0) sql.push(`ALTER TABLE${ this.hasFlag('IF_EXISTS') ? ' IF EXISTS' : '' } ${ this.autoEsc([basename, this.NAME].filter(s => s)).join('.') }\n\t${ rename0 }`);
if (move0) sql.push(`ALTER TABLE${ this.hasFlag('IF_EXISTS') ? ' IF EXISTS' : '' } ${ this.autoEsc([basename, rename0 ? this.ACTIONS.find(action => action.TYPE === 'RENAME').ARGUMENT : this.NAME].filter(s => s)).join('.') }\n\t${ move0 }`);
return sql.join(';\n');
}

@@ -246,5 +118,7 @@

const [ namePart, bodyPart ] = Lexer.split(rest, ['\\s+'], { useRegex: true, limit: 1 });
const [tblName, dbName] = this.parseIdent(context, namePart.trim(), true) || [];
const [ tblName, dbName ] = this.parseIdent(context, namePart.trim(), true) || [];
if (!tblName) return;
const instance = new this(context, tblName, dbName || context/*Database*/?.name);
const instance = (new this(context))
.name(tblName)
.basename(dbName);
if (ifExists) instance.withFlag('IF_EXISTS');

@@ -263,12 +137,12 @@ // ----------

const reference = { kind: nodeKind, name: nodeName };
instance.alter(reference, a => a.renameTo(newNodeName));
instance.addAlt(reference, a => a.rename(newNodeName));
} else {
instance.renameTo(newNodeName);
instance.addRename(newNodeName);
}
continue;
}
// RELOCATE ... TO ...
const [ relocateMatch, newSchemaUnescaped, /*esc*/, newSchemaEscaped ] = regex('relocateRe').exec(stmt) || [];
if (relocateMatch) {
instance.relocateTo(newSchemaUnescaped || this.autoUnesc(instance, newSchemaEscaped));
// MOVE ... TO ...
const [ moveMatch, newSchemaUnescaped, /*esc*/, newSchemaEscaped ] = regex('moveRe').exec(stmt) || [];
if (moveMatch) {
instance.addMove(newSchemaUnescaped || this.autoUnesc(instance, newSchemaEscaped));
continue;

@@ -279,9 +153,5 @@ }

if (dropMatch) {
const nodeKind = /CONSTRAINT|PRIMARY\s+KEY|FOREIGN\s+KEY|CHECK/i.test(nodeKind_b) ? 'CONSTRAINT' : (/INDEX|KEY/i.test(nodeKind_b) ? 'INDEX' : 'COLUMN');
const nodeName = nodeNameUnescaped_b || this.autoUnesc(instance, nodeNameEscaped_b) || nodeKind_b.trim().replace(/\s+KEY/i, '').toUpperCase()/* when, in mysql, it's just: drop PRIMARY KEY */;
const argument = nodeKind === 'CONSTRAINT' ? new TableLevelConstraint(instance, nodeName, nodeKind_b.trim().toUpperCase(), []/*columns*/, null) : (
nodeKind === 'INDEX' ? new Index(instance, nodeName, nodeKind_b.trim().toUpperCase(), []/*columns*/) : new Column(instance, nodeName, null, [])
);
const flags = [ifExists_b, flags_b].filter(s => s).map(s => s.trim().replace(/\s+/g, '_').toUpperCase());
instance.drop(argument).withFlag(...flags);
instance.addDrop({ kind: nodeKind_b.trim().replace(/\s+/g, '_').toUpperCase(), name: nodeName }).withFlag(...flags);
continue;

@@ -292,35 +162,38 @@ }

if (addMatch) {
const [ , $spec, $flags ] = spec_c.match(/([\s\S]+)\s+(FIRST|AFTER\s+.+)$/i) || [ , spec_c ];
const argument = parseCallback(instance, $spec.trim(), columnKeyword_c ? [Column] : [TableLevelConstraint,Index,Column]); // Note that Column must come last
const flags = [ifColumnNotExists_c, $flags].filter(s => s).map(s => s.trim().replace(/\s+/g, '_').toUpperCase());
instance.add(argument).withFlag(...flags);
const [ , $spec, $first, $afterRef ] = spec_c.match(/([\s\S]+)\s+(?:(FIRST)|AFTER\s+(.+))$/i) || [ , spec_c ];
const argument = parseCallback(instance, $spec.trim(), columnKeyword_c ? [Column] : [...CreateTable.CONSTRAINT_TYPES,Index,Column]); // Note that Column must come last
const flags = [ifColumnNotExists_c, $first].filter(s => s).map(s => s.trim().replace(/\s+/g, '_').toUpperCase()).concat($afterRef ? `AFTER:${ $afterRef }` : []);
instance.addNew(argument).withFlag(...flags);
continue;
}
// CHANGE
const [ changeMatch, verb_d, nodeNameUnescaped_d, /*esc*/, nodeNameEscaped_d, spec_d ] = regex('changeRe').exec(stmt) || [];
if (changeMatch) {
const nodeName = nodeNameUnescaped_d || this.autoUnesc(instance, nodeNameEscaped_d);
const [ , $spec, $first, $afterRef ] = spec_d.match(/([\s\S]+)\s+(?:(FIRST)|AFTER\s+(.+))$/i) || [ , spec_d ];
const argument = parseCallback(instance, /MODIFY/i.test(verb_d) ? `${ nodeName } ${ $spec }` : $spec, [Column]);
const flags = [ifColumnNotExists_c, $first].filter(s => s).map(s => s.trim().replace(/\s+/g, '_').toUpperCase()).concat($afterRef ? `AFTER:${ $afterRef }` : []);
instance.addChange({ kind: 'COLUMN', name: nodeName }, argument).withFlag(...flags);
continue;
}
// ALTER
const [ alterMatch, nodeKind_d, nodeNameUnescaped_d, /*esc*/, nodeNameEscaped_d, subAction_d = '', argument_d = '', ifNodeExits_d, constraintOrIndexAttr_d ] = regex('alterRe').exec(stmt) || [];
const [ alterMatch, nodeKind_e, nodeNameUnescaped_e, /*esc*/, nodeNameEscaped_e, subAction_e = '', argument_e = '', ifNodeExits_e, constraintOrIndexAttr_e ] = regex('alterRe').exec(stmt) || [];
if (alterMatch) {
const nodeName = nodeNameUnescaped_d || this.autoUnesc(instance, nodeNameEscaped_d);
const nodeKind = /CONSTRAINT|CHECK/i.test(nodeKind_d) ? 'CONSTRAINT' : (/INDEX|KEY/i.test(nodeKind_d) ? 'INDEX' : 'COLUMN');
let subAction = subAction_d.toUpperCase() || 'SET', flags = ifNodeExits_d ? ['IF_EXISTS'] : [], $ = {};
let argumentNew;
// Is column data type?
if (subAction.endsWith('TYPE')) {
argumentNew = parseCallback(instance, argument_d, [DataType]);
const nodeName = nodeNameUnescaped_e || this.autoUnesc(instance, nodeNameEscaped_e);
const nodeKind = /CONSTRAINT|CHECK/i.test(nodeKind_e) ? 'CONSTRAINT' : (/INDEX|KEY/i.test(nodeKind_e) ? 'INDEX' : 'COLUMN');
let argumentNew, subAction = subAction_e.toUpperCase() || 'SET', flags = ifNodeExits_e ? ['IF_EXISTS'] : [], $ = {};
if (subAction === 'DROP') {
argumentNew = argument_e;
} else if (subAction.endsWith('TYPE')) {
argumentNew = parseCallback(instance, argument_e, [DataType]);
subAction = 'SET';
}
// Is column constraint?
else if ($.argument = parseCallback(instance, argument_d, [ColumnLevelConstraint], { assert: false })) {
} else if ($.argument = parseCallback(instance, argument_e, Column.CONSTRAINT_TYPES, { assert: false })) {
argumentNew = $.argument;
} else if (subAction_e/*NOTE: original*/) {
argumentNew = argument_e;
} else {
argumentNew = constraintOrIndexAttr_e;
}
// Is SET|DROP|ADD flag?
else if (subAction_d/*NOTE: original*/) {
argumentNew = argument_d;
}
// Is just flag?
else {
argumentNew = constraintOrIndexAttr_d;
}
// Push
const reference = { kind: nodeKind, name: nodeName };
instance.alter(reference, a => a[subAction.toLowerCase()](argumentNew)).withFlag(...flags);
instance.addAlt(reference, a => a[subAction.toLowerCase()](argumentNew)).withFlag(...flags);
continue;

@@ -333,135 +206,11 @@ }

static fromDiffing(context, jsonA, jsonB, flags = []) {
if (!jsonA?.name) throw new Error(`Could not assertain table1 name or table1 name invalid.`);
if (!jsonB?.name) throw new Error(`Could not assertain table2 name or table2 name invalid.`);
const instance = (new this(context, jsonA.name, jsonA.basename, jsonA)).withFlag(...flags);
// RENAME TO...
if (jsonB.name !== jsonA.name) {
instance.renameTo(jsonB.name);
}
// RELOCATE ... TO ...
if (jsonB.basename !== jsonA.basename) {
instance.relocateTo(jsonB.basename);
}
// DIFF STRUCTURE
for (const listName of ['columns', 'constraints', 'indexes']) {
const nameKey = listName === 'constraints' ? 'constraintName' : (listName === 'indexes' ? 'indexName' : 'name');
const nodeKind = listName === 'constraints' ? 'CONSTRAINT' : (listName === 'indexes' ? 'INDEX' : 'COLUMN');
const NodeClass = nodeKind === 'CONSTRAINT' ? TableLevelConstraint : (nodeKind === 'INDEX' ? Index : Column);
const [ namesA, namesB, namesAll ] = makeSets(jsonA[listName], jsonB[listName], nameKey);
// --------
for (const nodeName of namesAll) {
const nodeA = jsonA[listName].find(node => node[nameKey] === nodeName);
const nodeB = jsonB[listName].find(node => (`$${ nameKey }` in node ? node[`$${ nameKey }`] : node[nameKey]) === nodeName);
const reference = { kind: nodeKind, name: nodeName };
if (namesA.has(nodeName) && !namesB.has(nodeName)) {
// DROP
instance.drop(NodeClass.fromJson(instance, nodeA));
} else if (!namesA.has(nodeName) && namesB.has(nodeName)) {
// ADD
instance.add(NodeClass.fromJson(instance, nodeB));
} else if (namesA.has(nodeName) && namesB.has(nodeName)) {
// ALTER
if (nodeKind === 'COLUMN') {
const [ propsA, propsB, propsAll ] = makeSets(nodeA, nodeB);
for (const property of propsAll) {
const createArg = node => {
const attrEquivalent = ColumnLevelConstraint.attrEquivalents[property];
if (attrEquivalent) {
const { constraintName, ...detail } = node[property];
return ColumnLevelConstraint.fromJson(instance, { constraintName, type: attrEquivalent, detail });
}
throw new Error(`Unkown attribute: ${ property }.`);
};
if ((propsA.has(property) && nodeA[property]) && (!propsB.has(property) || !nodeB[property])) {
// Drop
instance.alter(reference, a => a.drop(createArg(nodeA)));
} else if ((!propsA.has(property) || !nodeA[property]) && (propsB.has(property) && nodeB[property])) {
// Add
instance.alter(reference, a => a.add(createArg(nodeB)));
} else if (propsA.has(property) && propsB.has(property) && !isSame(nodeA[property], nodeB[property])) {
// Rename/alter
if (property === 'name') {
// Column rename
instance.alter(reference, a => a.renameTo(nodeB[property]));
} else if (property === 'type') {
// Change data type
instance.alter(reference, a => a.set(DataType.fromJson(instance, nodeB[property])));
} else {
instance.alter(reference, a => a.set(createArg(nodeB)));
}
}
}
} else if (!isSame(nodeA, nodeB)) {
// Alter constraint/index
instance.alter(reference, a => a.set(NodeClass.fromJson(instance, nodeB)));
}
}
}
}
return instance;
}
/**
* @inheritdoc
*/
static fromDiffing2d(context, jsonsA, jsonsB, flags = []) {
const nameKey = 'name';
const actions = [], [ namesA, namesB, namesAll ] = makeSets(jsonsA, jsonsB, nameKey);
for (const nodeName of namesAll) {
if (namesA.has(nodeName) && !namesB.has(nodeName)) {
// DROP
actions.push({ type: 'DROP', argument: nodeName });
} else if (!namesA.has(nodeName) && namesB.has(nodeName)) {
// ADD
const nodeB = jsonsB.find(tblSchema => (`$${ nameKey }` in tblSchema ? tblSchema[`$${ nameKey }`] : tblSchema[nameKey]) === nodeName);
actions.push({ type: 'ADD', argument: CreateTable.fromJson(context, nodeB, flags) });
} else if (namesA.has(nodeName) && namesB.has(nodeName)) {
// ALTER
const nodeA = jsonsA.find(tblSchema => tblSchema[nameKey] === nodeName);
const nodeB = jsonsB.find(tblSchema => ( tblSchema[`$${ nameKey }`] || tblSchema[nameKey]) === nodeName);
const tblAlterInstance = this.fromDiffing(context, nodeA, nodeB, flags);
if (tblAlterInstance.ACTIONS.length) {
actions.push({ type: 'ALTER', argument: tblAlterInstance });
}
}
}
return actions;
}
/**
* @property RegExp
*/
static renameRe = /^RENAME\s+(?:(?:(COLUMN|CONSTRAINT|INDEX|KEY)\s+)?(?:(\w+)|([`"])((?:\3\3|[^\3])+)\3)\s+)?(?:TO|AS)\s+(?:(\w+)|([`"])([^\6]+)\6)$/;
static relocateRe = /^SET\s+SCHEMA\s+(?:(\w+)|([`"])((?:\2\2|[^\3])+)\2)$/;
static renameRe = /^RENAME\s+(?:(?:(COLUMN|CONSTRAINT|INDEX|KEY)\s+)?(?:(\w+)|([`"])((?:\3\3|[^\3])+)\3)\s+)?(?:TO|AS)\s+(?:(\w+)|([`"])((?:\6\6|[^\6])+)\6)$/;
static moveRe = /^SET\s+SCHEMA\s+(?:(\w+)|([`"])((?:\2\2|[^\2])+)\2)$/;
static dropRe = /^DROP\s+(COLUMN\s+|CONSTRAINT\s+|PRIMARY\s+KEY|FOREIGN\s+KEY\s+|CHECK\s+|INDEX\s+|KEY\s+)?(IF\s+EXISTS\s+)?(?:(\w+)|([`"])((?:\4\4|[^\3])+)\4)?(?:\s+(RESTRICT|CASCADE))?$/;
static addRe = /^ADD\s+(COLUMN\s+)?(IF\s+NOT\s+EXISTS\s+)?([\s\S]+)$/;
static changeRe = /^(CHANGE|MODIFY)\s+COLUMN\s+(?:(\w+)|([`"])((?:\3\3|[^\3])+?)\3)\s+([\s\S]+)$/;
static alterRe = /^ALTER\s+(?:(COLUMN|CONSTRAINT|CHECK|INDEX|KEY)\s+)?(?:(\w+)|([`"])((?:\3\3|[^\3])+?)\3)\s+(?:(ADD|DROP|(?:SET\s+DATA\s+)?TYPE|SET)\s+(.+)(IF\s+EXISTS)?$|(VISIBLE|(?:NOT\s+)?INVISIBLE|NOT\s+ENFORCED|ENFORCED|DEFERRABLE|NOT\s+DEFERRABLE|INITIALLY\s+DEFERRED|INITIALLY\s+IMMEDIATE))/;
}
function makeSets(a, b, nameKey) {
if (Array.isArray(a)) {
a = a.map(x => x[nameKey]);
b = b.map(x => `$${ nameKey }` in x ? x[`$${ nameKey }`] : x[nameKey]);
} else {
a = Object.keys(a);
b = Object.keys(b).filter(s => !s.startsWith('$'));
}
a = new Set(a);
b = new Set(b);
const ab = new Set([ ...a, ...b ]);
return [ a, b, ab ];
}
function isSame(a, b) {
if (a === b) return true;
if (Array.isArray(a) && Array.isArray(b) && a.length === b.length) {
const $b = b.slice(0).sort();
return a.slice(0).sort().every((x, i) => isSame(x, $b[i]));
}
const temp = {};
if (typeof a === 'object' && a && typeof b === 'object' && b && (temp.keys_a = Object.keys(a)).length === (temp.keys_b = Object.keys(b)).length) {
return temp.keys_a.reduce((prev, k) => prev && isSame(a[k], b[k]), true);
}
return false;
}
import Lexer from '../Lexer.js';
import { _after, _before, _unwrap, _toCamel } from '@webqit/util/str/index.js';
import ColumnLevelConstraint from './ColumnLevelConstraint.js';
import { _toCamel, _fromCamel } from '@webqit/util/str/index.js';
import AbstractNode from './abstracts/AbstractNode.js';
import AutoIncrement from './constraints/AutoIncrement.js';
import Identity from './constraints/Identity.js';
import Expression from './constraints/Expression.js';
import Default from './constraints/Default.js';
import NotNull from './constraints/NotNull.js';
import PrimaryKey1 from './constraints/PrimaryKey1.js';
import ForeignKey1 from './constraints/ForeignKey1.js';
import UniqueKey1 from './constraints/UniqueKey1.js';
import Check from './constraints/Check.js';
import DataType from './DataType.js';
import Node from '../abstracts/Node.js';
export default class Column extends Node {
export default class Column extends AbstractNode {

@@ -13,14 +21,17 @@ /**

*/
NAME = '';
TYPE = null;
TYPE;
$TYPE;
CONSTRAINTS = [];
/**
* @constructor
*/
constructor(context, name) {
super(context);
this.NAME = name;
}
* @var Array
*/
static get WRITABLE_PROPS() { return ['TYPE'].concat(super.WRITABLE_PROPS); }
static get SUBTREE_PROPS() { return ['CONSTRAINTS']; }
/**
* @var Array
*/
static CONSTRAINT_TYPES = [AutoIncrement,Identity,Expression,Default,NotNull,PrimaryKey1,ForeignKey1,UniqueKey1,Check];
/**

@@ -33,12 +44,74 @@ * Sets the column type,

*/
type(value) { return this.build('TYPE', [value], DataType); }
type(value) {
if (!arguments.length) return this[this.smartKey('TYPE')];
return (this.build(this.smartKey('TYPE', true), [value], DataType), this);
}
/**
* AUTO_INCREMENT
*/
autoIncrement(trueFalse = null) { return this.constraint('AUTO_INCREMENT', ...arguments); }
/**
* IDENTITY
*/
identity(trueFalse = null) { return this.constraint('IDENTITY', ...arguments); }
/**
* EXPRESSION
*/
expression(trueFalse = null) { return this.constraint('EXPRESSION', ...arguments); }
/**
* DEFAULT
*/
default(trueFalse = null) { return this.constraint('DEFAULT', ...arguments); }
/**
* NOT_NULL
*/
notNull(trueFalse = null) { return this.constraint('NOT_NULL', ...arguments); }
/**
* PRIMARY_KEY
*/
primaryKey(trueFalse = null) { return this.constraint('PRIMARY_KEY', ...arguments); }
/**
* FOREIGN_KEY
*/
foreignKey(trueFalse = null) { return this.constraint('FOREIGN_KEY', ...arguments); }
/**
* UNIQUE_KEY
*/
uniqueKey(trueFalse = null) { return this.constraint('UNIQUE_KEY', ...arguments); }
/**
* CHECK
*/
check(trueFalse = null) { return this.constraint('CHECK', ...arguments); }
/**
* Adds a column-level constraint to the column,
*
* @param ColumnLevelConstraint constraint
* @param String type
* @param Bool setting
*
* @returns this
*/
constraint(...constraints) { return this.build('CONSTRAINTS', constraints, ColumnLevelConstraint); }
constraint(type, setting = null) {
const existing = this.CONSTRAINTS.find(cons => cons.TYPE === type);
if (arguments.length === 1) return existing;
if (setting) {
if (existing) {
if (setting === true || !Object.keys(setting).length) return;
throw new Error(`${ type } already exists in column. Granular modification of a constraint must be done on an instance of the contraint itself.`);
}
this.build('CONSTRAINTS', [{ type, ...(typeof setting === 'object' ? setting : {}) }], this.constructor.CONSTRAINT_TYPES);
return this.constraint(type);
}
if (existing) existing.status('DOWN');
return this;
}

@@ -50,11 +123,14 @@ /**

let json = {
name: this.NAME,
type: this.TYPE?.toJson(),
type: this.TYPE.toJson(),
...(this.$TYPE ? { $type: this.$TYPE.toJson() } : {}),
};
for (const constraint of this.CONSTRAINTS) {
const { constraintName, type, detail } = constraint.toJson();
const equivProperty = Object.keys(ColumnLevelConstraint.attrEquivalents).find(prop => ColumnLevelConstraint.attrEquivalents[prop] === type);
json = { ...json, [ equivProperty ]: { constraintName, ...detail } };
for (const cons of this.CONSTRAINTS) {
const { type, ...constraintDef } = cons.toJson();
const propName = type === 'FOREIGN_KEY' ? 'references' : _toCamel(type.toLowerCase().replace('_', ' '));
const props = Object.keys(constraintDef);
const lonePropValue = props.length === 1 ? constraintDef[props[0]] : null;
const propValue = !props.length ? true : (lonePropValue === 'DOWN' ? false : (props.length === 1 && props[0] === 'expr' ? lonePropValue : constraintDef));
json = { ...json, [ propName ]: propValue };
}
return json;
return { ...json, ...super.toJson()/** Status */ };
}

@@ -66,14 +142,28 @@

static fromJson(context, json) {
if (typeof json?.name !== 'string') return;
const instance = new this(context, json.name);
// Constraints
for (const property in ColumnLevelConstraint.attrEquivalents) {
if (!json[property]) continue;
const { constraintName, ...detail } = json[property];
const type = ColumnLevelConstraint.attrEquivalents[property];
instance.constraint(ColumnLevelConstraint.fromJson(instance, { constraintName, type, detail }));
}
// An instance with just the name is used in AlterTable.fromJson() for DROP col_name
if (json.type) instance.type(DataType.fromJson(instance, json.type));
return instance;
const { type, $type, name: _, $name: __, status: ___, ...constraints } = json;
if (!DataType.fromJson({}, type)) return;
return super.fromJson(context, json, () => {
const instance = new this(context);
instance.type(DataType.fromJson(instance, type));
instance.hardSet($type, val => instance.type(DataType.fromJson(instance, val)));
const constraintsNormalized = Object.entries(constraints).reduce((normalized, [name, value]) => {
if (!['boolean','number','string'].includes(typeof value) && !(typeof value === 'object' && value)) {
throw new Error(`Invalid value for constraint "${ name }"`);
}
let cons = { ...(value === false ? { status: 'DOWN' } : (value === true ? {} : (['number','string'].includes(typeof value) ? { expr: value } : value))) };
if (name.startsWith('$')) {
cons = Object.fromEntries(Object.entries(cons).map(([name, val]) => [`$${ name }`, val]));
name = name.slice(1);
}
if (name === 'references') name = 'foreignKey';
if (name in normalized) Object.assign(normalized[name], cons);
else normalized[name] = cons;
return normalized;
}, {});
// Constraints
for (const name in constraintsNormalized) {
instance.constraint(_fromCamel(name, '_').toUpperCase(), constraintsNormalized[name]);
}
return instance;
});
}

@@ -85,6 +175,5 @@

stringify() {
// Render constraints in the order of ColumnLevelConstraint.attrEquivalents;
let constraints = Object.values(ColumnLevelConstraint.attrEquivalents).map(type => this.CONSTRAINTS.find(cnst => cnst.TYPE === type)).filter(c => c);
let constraints = this.CONSTRAINTS;
if (this.params.dialect === 'mysql') { constraints = constraints.filter(c => c.TYPE !== 'FOREIGN_KEY'); }
return `${ this.autoEsc(this.NAME) } ${ this.TYPE }${ constraints.length ? ` ${ constraints.join(' ') }` : '' }`;
return `${ this.autoEsc(this.name()) } ${ this.type() }${ constraints.length ? ` ${ constraints.join(' ') }` : '' }`;
}

@@ -99,3 +188,3 @@

if (!name) return;
const instance = new this(context, name);
const instance = (new this(context)).name(name);
// Parse into "type" and constraints

@@ -113,3 +202,4 @@ const qualifier = '(CONSTRAINT\\s+.+?\\s+)?';

for (const constraint of tokens) {
instance.constraint(parseCallback(instance, constraint, [ColumnLevelConstraint]));
const cons = parseCallback(instance, constraint, this.CONSTRAINT_TYPES);
instance.build('CONSTRAINTS', [cons], this.CONSTRAINT_TYPES);
}

@@ -116,0 +206,0 @@ return instance;

import StatementNode from '../abstracts/StatementNode.js';
import AlterDatabase from '../alter/AlterDatabase.js';
import AbstractStatementNode from './abstracts/AbstractStatementNode.js';
import CreateTable from './CreateTable.js';
export default class CreateDatabase extends StatementNode {
/**
* Instance properties
export default class CreateDatabase extends AbstractStatementNode {
/**
* Lists
*/
NAME = '';
TABLES = [];
/**
* @constructor
* @inheritdoc
*/
constructor(context, name) {
super(context);
this.NAME = name;
static get SUBTREE_PROPS() { return ['TABLES']; }
/**
* Returns a table or adds a table to the schema,
*
* @param String|CreateTable table
*
* @returns Any
*/
table(table) {
if (typeof table === 'string') return this.TABLES.find(tbl => tbl.name() === table);
return (this.build('TABLES', [table], CreateTable), this);
}
/**
* Sets the name
* Apply changes to this schema.
*
* @param String name
* @param AlterDatabase altInstance
*
* @returns Void
* @returns this
*/
name(name) { this.NAME = name; }
alterWith(altInstance) {
// -----
const getTable = (name, ifExists = false) => {
const node = this.table(name);
if (!node && !ifExists) throw new Error(`TABLE ${ name } does not exist.`);
return node;
}
// -----
for (const action of altInstance.ACTIONS) {
if (action.TYPE === 'RENAME') {
this.name(action.ARGUMENT);
} else if (action.TYPE === 'MOVE') {
this.basename(action.ARGUMENT);
} else if (action.TYPE === 'DROP') {
const node = getTable(action.ARGUMENT, action.hasFlag('IF_EXISTS'));
node?.status('DOWN');
} else if (action.TYPE === 'NEW') {
if (!action.hasFlag('IF_NOT_EXISTS') || !getTable(action.ARGUMENT.name(), true)) {
this.table(action.ARGUMENT.toJson());
}
} else if (action.TYPE === 'ALTER') {
const node = getTable(action.REFERENCE, action.hasFlag('IF_EXISTS'));
if (!node) continue;
node.alter(action.ARGUMENT);
}
}
}

@@ -31,3 +68,21 @@ /**

*/
toJson() { return { name: this.NAME, flags: this.FLAGS }; }
getAlt() {
const instance = (new AlterDatabase(this.CONTEXT)).name(this.NAME).basename(this.BASENAME);
if (this.$NAME && this.NAME && this.$NAME !== this.NAME) {
instance.addRename(this.$NAME);
}
if (this.$BASENAME && this.BASENAME && this.$BASENAME !== this.BASENAME) {
instance.addMove(this.$BASENAME);
}
for (const tbl of this.TABLES) {
if (tbl.status() === 'UP') {
instance.addAlt({ name: tbl.NAME, kind: 'TABLE' }, a => a.set(tbl.getAlt()));
} else if (tbl.status() === 'DOWN') {
instance.addDrop({ name: [tbl.BASENAME || this.NAME, tbl.NAME], kind: 'TABLE' });
} else {
instance.addNew(tbl.clone());
}
}
return instance;
}

@@ -37,11 +92,49 @@ /**

*/
/**
* @inheritdoc
*/
cascadeAlt() {
// Normalize subtree statuses
this.status(this.status(), true);
// We've been dropped or renamed?
const altType = this.dropped() ? 'DOWN' : (this.$NAME && this.$NAME !== this.NAME ? 'RENAME' : null);
if (altType) {
// TODO: Check with all tables and call updateDatabaseReferences() on them
}
// Ask tables to also cascadeAlt()
for (const tbl of this.TABLES) tbl.cascadeAlt();
this.altsCascaded = true;
return this;
}
/**
* @inheritdoc
*/
toJson() {
return {
...super.toJson(),
tables: this.TABLES.map(table => table.toJson()),
}
}
/**
* @inheritdoc
*/
static fromJson(context, json) {
if (typeof json?.name !== 'string') return;
return (new this(context, json.name)).withFlag(...(json.flags || []));
if (['tables'].some(key => key in json && !Array.isArray(json[key]))) return;
return super.fromJson(context, json, () => {
const instance = new this(context);
for (const tbl of json.tables || []) instance.table(tbl);
return instance;
});
}
/**
* @inheritdoc
*/
stringify() { return `CREATE SCHEMA${ this.hasFlag('IF_NOT_EXISTS') ? ' IF NOT EXISTS' : '' } ${ this.autoEsc(this.NAME) }`; }
stringify() {
const sql = [`CREATE SCHEMA${ this.hasFlag('IF_NOT_EXISTS') ? ' IF NOT EXISTS' : '' } ${ this.autoEsc(this.name()) }`];
return [ ...sql, ...this.TABLES ].join(';\n');
}

@@ -51,3 +144,3 @@ /**

*/
static parse(context, expr) {
static parse(context, expr, parseCallback) {
const [ match, ifNotExists, namePart ] = /^CREATE\s+DATABASE\s+(IF\s+NOT\s+EXISTS\s+)?(.+)$/i.exec(expr) || [];

@@ -57,19 +150,6 @@ if (!match) return;

if (!name) return;
const instance = new this(context, name, params);
const instance = (new this(context)).name(name);
if (ifNotExists) instance.withFlag('IF_NOT_EXISTS');
return instance;
}
/**
* @inheritdoc
*/
static cloneJson(json) {
const jsonClone = { name: json.name };
const rebase = (obj, key) => {
const value = obj[key];
Object.defineProperty(obj, `$${ key }`, { get: () => value });
};
rebase(jsonClone, 'name');
return jsonClone;
}
}
import Lexer from '../Lexer.js';
import { _unwrap } from '@webqit/util/str/index.js';
import StatementNode from '../abstracts/StatementNode.js';
import TableLevelConstraint from './TableLevelConstraint.js';
import AlterTable from '../alter/AlterTable.js';
import AbstractStatementNode from './abstracts/AbstractStatementNode.js';
import AbstractConstraint from './constraints/AbstractConstraint.js';
import ForeignKey1 from './constraints/ForeignKey1.js';
import PrimaryKey2 from './constraints/PrimaryKey2.js';
import ForeignKey2 from './constraints/ForeignKey2.js';
import UniqueKey2 from './constraints/UniqueKey2.js';
import Check from './constraints/Check.js';
import Column from './Column.js';
import Index from './Index.js';
export default class CreateTable extends StatementNode {
export default class CreateTable extends AbstractStatementNode {
/**
* Instance properties
* Instance props.
*/
NAME = '';
BASENAME = '';
COLUMNS = [];

@@ -21,50 +25,143 @@ CONSTRAINTS = [];

/**
* @constructor
* Other props
*/
constructor(context, name, basename) {
super(context);
this.NAME = name;
this.BASENAME = basename;
NODES = new Set;
/**
* @inheritdoc
*/
static get WRITABLE_PROPS() { return ['BASENAME'].concat(super.WRITABLE_PROPS); }
static get SUBTREE_PROPS() { return ['COLUMNS', 'CONSTRAINTS', 'INDEXES']; }
/**
* @var Array
*/
static CONSTRAINT_TYPES = [PrimaryKey2,ForeignKey2,UniqueKey2,Check];
/**
* @inheritdoc
*/
connectedNodeCallback(node) {
if ([Column, AbstractConstraint, Index].some(x => node instanceof x)) this.NODES.add(node);
}
/**
* PRIMARY_KEY
*/
primaryKey() {
return this.CONSTRAINTS.find(cons => cons.TYPE === 'PRIMARY_KEY')
|| this.COLUMNS.reduce((pk, col) => pk || col.primaryKey(), null);
}
/**
* Sets the name
* Returns a column or adds a column to the schema,
*
* @param Array|String name
* @param String|Column column
*
* @returns Void
* @returns Any
*/
name(name) {
const nameParts = Array.isArray(name) ? [...name] : [name];
this.NAME = nameParts.pop();
this.BASENAME = nameParts.pop();
if (nameParts.length) throw new Error(`Idents can be maximum of two parts. Recieved: ${ nameParts.reverse().join('.') }.${ this.BASENAME }.${ this.NAME }`);
column(column) {
if (typeof column === 'string') return this.COLUMNS.find(col => col.name() === column);
return (this.build('COLUMNS', [column], Column), this);
}
/**
* Adds a column to the schema,
* Returns a constraint or adds a constraint to the schema,
*
* @param Column column
* @param String|PrimaryKey2,ForeignKey2,UniqueKey2,Check constraint
*
* @returns this
* @returns Any
*/
column(...columns) { return this.build('COLUMNS', columns, Column); }
constraint(constraint) {
if (typeof constraint === 'string') return this.CONSTRAINTS.find(cons => cons.name() === constraint);
return (this.build('CONSTRAINTS', [constraint], this.constructor.CONSTRAINT_TYPES), this);
}
/**
* Adds a table-level constraint to the schema,
* Returns a constraint or adds a constraint to the schema,
*
* @param TableLevelConstraint constraint
* @param String|Index index
*
* @returns this
* @returns Any
*/
constraint(...constraints) { return this.build('CONSTRAINTS', constraints, TableLevelConstraint); }
index(index) {
if (typeof index === 'string') return this.INDEXES.find(idx => idx.name() === index);
return (this.build('INDEXES', [index], Index), this);
}
/**
* Adds an index index to the schema,
* Apply changes to this schema.
*
* @param Index constraint
* @param AlterTable altInstance
*
* @returns this
*/
index(...indexes) { return this.build('INDEXES', indexes, Index); }
alterWith(altInstance) {
// -----
const getNode = (reference, ifExists = false) => {
const node = this.NODES.find(node => {
return (reference.kind === 'COLUMN' ? node instanceof Column : (reference.kind === 'CONSTRAINT' ? node instanceof AbstractConstraint : node.TYPE === reference.kind/* constraint or index */))
&& (!reference.name ? reference.kind === 'PRIMARY_KEY'/* mysql only */ : node.NAME === reference.name)
});
if (!node && !ifExists) throw new Error(`${ reference.kind }${ reference.name ? ` "${ reference.name }"` : '' } does not exist.`);
return node;
}
// -----
for (const action of altInstance.ACTIONS) {
if (action.TYPE === 'RENAME') {
this.name(action.ARGUMENT);
} else if (action.TYPE === 'MOVE') {
this.basename(action.ARGUMENT);
} else if (action.TYPE === 'DROP') {
const node = getNode(action.ARGUMENT, action.hasFlag('IF_EXISTS'));
node?.status('DOWN');
} else if (action.TYPE === 'NEW') {
if (action.ARGUMENT instanceof AbstractConstraint) {
if (action.ARGUMENT.COLUMNS.length === 1) {
getNode({ kind: 'COLUMN', name: action.ARGUMENT.COLUMNS[0] }).constraint(action.ARGUMENT.TYPE, action.ARGUMENT.toJson());
} else this.constraint(action.ARGUMENT.toJson());
} else if (action.ARGUMENT instanceof Index) {
this.index(action.ARGUMENT.toJson());
} else if (!action.hasFlag('IF_NOT_EXISTS') || !getNode({ kind: 'COLUMN', name: action.ARGUMENT.NAME }, true)) {
this.column(action.ARGUMENT.toJson());
}
} else if (action.TYPE === 'CHANGE') {
const node = getNode(action.REFERENCE);
node.status('UP', () => {
if (action.ARGUMENT.name() !== node.name()) node.name(action.ARGUMENT.name());
node.type(action.ARGUMENT.type().toJson());
});
for (const cons of action.ARGUMENT.CONSTRAINTS.filter(cons => !['PRIMARY_KEY', 'FOREIGN_KEY', 'UNIQUE_KEY', 'CHECK'].includes(cons.TYPE))) {
const existing = node.constraint(cons.TYPE);
if (existing) {
existing.detail(cons.DETAIL);
} else node.constraint(cons.toJson());
}
} else if (action.TYPE === 'ALTER') {
const { REFERENCE: reference, ARGUMENT: subAction } = action;
const node = getNode(reference, action.hasFlag('IF_EXISTS'));
if (!node) continue;
if (subAction.TYPE === 'RENAME') {
node.name(subAction.ARGUMENT);
} else if (subAction.TYPE === 'SET' && subAction.ARGUMENT instanceof DataType) {
node.type(subAction.ARGUMENT.toJson());
} else if (Column.CONSTRAINT_TYPES.some(Type => subAction.ARGUMENT instanceof Type)) {
const existing = node.constraint(subAction.ARGUMENT.TYPE);
if (subAction.ARGUMENT.TYPE === 'IDENTITY') {
if (subAction.TYPE === 'SET' && !existing) throw new Error(`IDENTITY constraint has not been created in ${ node.NAME }`);
if (subAction.TYPE === 'NEW' && existing) throw new Error(`IDENTITY constraint already exists in ${ node.NAME }`);
} else if (subAction.ARGUMENT.TYPE === 'EXPRESSION' && subAction.TYPE !== 'DROP') {
throw new Error(`Cannot add EXPRESSION constraint after column creation`);
}
if (existing) {
existing.detail(subAction.ARGUMENT.DETAIL);
} else node.constraint(subAction.ARGUMENT.toJson());
} else if (subAction.TYPE === 'DROP' && ['IDENTITY', 'EXPRESSION', 'DEFAULT', 'NOT_NULL'].includes(subAction.ARGUMENT)) {
const existing = node.constraint(subAction.ARGUMENT);
if (existing) existing.status('DOWN');
else if(!action.hasFlag('IF_EXISTS')/* Postgres IDENTITY|EXPRESSION */) throw new Error(`Cannot drop ${ subAction.ARGUMENT }; does not exist.`);
} else if (['CONSTRAINT','INDEX'].includes(reference.kind)) ;
}
}
}

@@ -74,12 +171,166 @@ /**

*/
getAlt() {
const instance = (new AlterTable(this.CONTEXT)).name(this.NAME).basename(this.BASENAME);
if (this.$NAME && this.NAME && this.$NAME !== this.NAME) {
instance.addRename(this.$NAME);
}
if (this.$BASENAME && this.BASENAME && this.$BASENAME !== this.BASENAME) {
instance.addMove(this.$BASENAME);
}
const constraintDirty = (cons, includingName = false) => (cons.status() !== 'UP' || ['$EXPR','$ALWAYS','$TARGET_TABLE','$TARGET_COLUMNS','$MATCH_RULE','$UPDATE_RULE','$DELETE_RULE'].concat(includingName ? '$NAME' : []).some(k => /*exists*/k in cons && /*not empty*/(Array.isArray(cons[k]) ? cons[k].length : ![undefined, null].includes(cons[k])) && /*different*/!isSame(cons[k.slice(1)], cons[k])));
for (const col of this.COLUMNS) {
const columnRef = { kind: 'COLUMN', name: col.NAME };
if (col.status() === 'UP') {
if (this.params.dialect === 'mysql') {
// // Column name or type changed, or these attrs changed? Use MySQL CHANGE clause?
if ((col.$TYPE && !isSame(col.$TYPE.toJson(), col.TYPE.toJson()))
|| (col.CONSTRAINTS.some(cons => ['AUTO_INCREMENT', 'EXPRESSION', 'NOT_NULL'].includes(cons.TYPE) && constraintDirty(cons, true)))) {
const columnClone = col.clone();
columnClone.CONSTRAINTS = columnClone.CONSTRAINTS.filter(cons => ['AUTO_INCREMENT', 'EXPRESSION', 'NOT_NULL', 'DEFAULT'].includes(cons.TYPE));
instance.addChange(columnRef, columnClone);
} else {
const consDefault = col.CONSTRAINTS.find(cons => cons.TYPE === 'DEFAULT' && constraintDirty(cons, true));
if (consDefault) instance.addAlt(columnRef, a => consDefault.status() === 'DOWN' ? a.drop(consDefault.TYPE) : a.set(consDefault));
// Column rename? Must come last!!!
if (col.$NAME && col.$NAME !== col.NAME) {
instance.addAlt({ kind: 'COLUMN', name: col.NAME }, a => a.rename(col.$NAME) );
}
}
} else {
// Column type change?
if (col.$TYPE && !isSame(col.$TYPE.toJson(), col.TYPE.toJson())) {
instance.addAlt(columnRef, a => a.set(col.$TYPE) );
}
// Constraints level1 changed?
const constraints1 = col.CONSTRAINTS.filter(cons => ['IDENTITY', 'EXPRESSION', 'NOT_NULL', 'DEFAULT'].includes(cons.TYPE) && constraintDirty(cons, true));
for (const cons of constraints1) {
if (cons.status() === 'UP' && cons.TYPE === 'IDENTITY') instance.addAlt(columnRef, a => a.drop('IDENTITY'));
if (cons.status() !== 'DOWN' && cons.TYPE === 'EXPRESSION') throw new Error('EXPRESSION constraints cannot be added or modified after column creation.');
instance.addAlt(columnRef, a => cons.status() === 'DOWN' ? a.drop(cons.TYPE) : a[cons.TYPE === 'IDENTITY' ? 'new' : 'set'](cons));
}
// Column rename? Must come last!!!
if (col.$NAME && col.$NAME !== col.NAME) {
instance.addAlt({ kind: 'COLUMN', name: col.NAME }, a => a.rename(col.$NAME) );
}
}
// Constraints level2 changed?
const constraints2 = col.CONSTRAINTS.filter(cons => ['PRIMARY_KEY', 'FOREIGN_KEY', 'UNIQUE_KEY', 'CHECK'].includes(cons.TYPE));
for (const cons of constraints2) {
if (constraintDirty(cons)) {
if (['UP', 'DOWN'].includes(cons.status())) instance.addDrop({ kind: cons.TYPE, name: cons.NAME });
if (cons.status() !== 'DOWN') instance.addNew(CreateTable.CONSTRAINT_TYPES.find(Type => Type.TYPE === cons.TYPE).fromJson(instance, { ...cons.toJson(), columns: [col.statementNode.altsCascaded ? col.name() : col.NAME] }));
} else if (cons.status() === 'UP' && cons.$NAME && cons.$NAME !== cons.NAME) {
instance.addAlt({ kind: 'CONSTRAINT', name: cons.NAME }, a => a.rename(cons.$NAME) );
}
}
continue;
}
// DROP COLUMN?
if (col.status() === 'DOWN') {
instance.addDrop(columnRef);
continue;
}
// ADD COLUMN
instance.addNew(Column.fromJson(instance, col.toJson()));
}
const tableLevlConstraintDirty = cons => constraintDirty(cons) || (cons.$COLUMNS?.length && !isSame(cons.$COLUMNS, cons.COLUMNS));
for (const cons of this.CONSTRAINTS) {
if (tableLevlConstraintDirty(cons)) {
if (['UP', 'DOWN'].includes(cons.status())) instance.addDrop({ kind: cons.TYPE, name: cons.NAME });
if (cons.status() !== 'DOWN') instance.addNew(CreateTable.CONSTRAINT_TYPES.find(Type => Type.TYPE === cons.TYPE).fromJson(instance, cons.toJson()));
} else if (cons.status() === 'UP' && cons.$NAME && cons.$NAME !== cons.NAME) {
instance.addAlt({ kind: 'CONSTRAINT', name: cons.NAME }, a => a.rename(cons.$NAME) );
}
}
return instance;
}
/**
* @inheritdoc
*/
cascadeAlt() {
// Normalize subtree statuses
this.status(this.status(), true);
const getAltType = node => node.dropped() ? 'DOWN' : (node.$NAME && node.$NAME !== node.NAME ? 'RENAME' : null);
// We've been dropped or renamed?
const altType = getAltType(this);
if (altType) {
// TODO: Check with all tables and call updateTableReferences() on them
}
// A column in here was dropped or renamed?
for (const col of this.COLUMNS) {
const altType = getAltType(col);
if (!altType) continue;
// Check with our own references to columns
for (const cons of this.CONSTRAINTS) {
if (cons instanceof Check) continue;
const targetList = cons.$COLUMNS.length ? cons.$COLUMNS : cons.COLUMNS;
const index = targetList.indexOf(col.NAME);
if (index > -1) {
if (altType === 'DOWN') targetList.splice(index, 1);
else if (altType === 'RENAME') targetList[index] = col.$NAME;
};
}
// TODO: Check with all tables and call updateColumnReferences() on them
}
this.altsCascaded = true;
return this;
}
/**
* @inheritdoc
*/
updateDatabaseReferences(db, altType) {
// A database was dropped or renamed. We check with our own references to databases
for (const node of this.NODES) {
if (!(node instanceof ForeignKey1)) continue;
if (node.targetTable().basename() !== db.NAME) continue;
if (altType === 'DOWN') node.status('DOWN');
else if (altType === 'RENAME') node.targetTable().basename(db.$NAME);
}
}
/**
* @inheritdoc
*/
updateTableReferences(tbl, altType) {
// A table was dropped or renamed. We check with our own references to tables
for (const node of this.NODES) {
if (!(node instanceof ForeignKey1)) continue;
if (node.targetTable().basename() && tbl.basename() && node.targetTable().basename() !== tbl.basename()) continue;
if (node.targetTable().name() === tbl.NAME) {
if (altType === 'DOWN') node.status('DOWN');
else if (altType === 'RENAME') node.targetTable().name(tbl.$NAME);
};
}
}
/**
* @inheritdoc
*/
updateColumnReferences(col, altType) {
// A column somewhere was dropped or renamed. We check with our own references to columns
for (const node of this.NODES) {
if (!(node instanceof ForeignKey1)) continue;
if (node.targetTable().basename() && col.statementNode/* tbl */.basename() && node.targetTable().basename() !== col.statementNode/* tbl */.basename()) continue;
if (node.targetTable().name() !== col.statementNode/* tbl */.name()) continue;
const targetList = cons.$TARGET_COLUMNS.length ? cons.$TARGET_COLUMNS : cons.TARGET_COLUMNS;
const index = targetList.indexOf(col.NAME);
if (index > -1) {
if (altType === 'DOWN') targetList.splice(index, 1);
else if (altType === 'RENAME') targetList[index] = col.$NAME;
};
}
}
/**
* @inheritdoc
*/
toJson() {
const json = {
name: this.NAME,
basename: this.BASENAME,
return {
columns: this.COLUMNS.map(column => column.toJson()),
constraints: this.CONSTRAINTS.map(constraint => constraint.toJson()),
indexes: this.INDEXES.map(index => index.toJson()),
flags: this.FLAGS,
...super.toJson(),
}
return json;
}

@@ -91,10 +342,10 @@

static fromJson(context, json) {
if (typeof json?.name !== 'string' || !Array.isArray(json.columns)) return;
const instance = (new this(context, json.name, json.basename)).withFlag(...(json.flags || []));
// Lists
instance.column(...json.columns);
if (json.constraints?.length) instance.constraint(...json.constraints);
if (json.indexes?.length) instance.index(...json.indexes);
// Instance
return instance;
if (!Array.isArray(json?.columns) || ['constraints', 'indexes'].some(key => key in json && !Array.isArray(json[key]))) return;
return super.fromJson(context, json, () => {
const instance = new this(context);
for (const col of json.columns) instance.column(col);
for (const cons of (json.constraints || [])) instance.constraint(cons);
for (const idx of (json.indexes || [])) instance.index(idx);
return instance;
});
}

@@ -108,6 +359,7 @@

const constraints = this.CONSTRAINTS.slice(0);
const indexes = this.INDEXES.slice(0);
if (this.params.dialect === 'mysql') {
constraints.push(...this.COLUMNS.reduce((constraints, col) => {
const constraint = col.CONSTRAINTS.find(c => c.TYPE === 'FOREIGN_KEY');
if (constraint) return constraints.concat(TableLevelConstraint.fromColumnLevelConstraint(constraint, col.NAME));
const constraint = col.foreignKey();
if (constraint) return constraints.concat(ForeignKey2.fromJson(this, constraint.toJson()).columns([col.name()]));
return constraints;

@@ -117,4 +369,5 @@ }, []));

if (constraints.length) { defs.push(constraints.map(cnst => cnst.stringify()).join(',\n\t')); }
if (this.INDEXES.length) { defs.push(this.INDEXES.map(ndx => ndx.stringify()).join(',\n\t')); }
return `CREATE TABLE${ this.hasFlag('IF_NOT_EXISTS') ? ' IF NOT EXISTS' : '' } ${ this.autoEsc([this.BASENAME, this.NAME].filter(s => s)).join('.') } (\n\t${ defs.join(',\n\t') }\n)`;
if (indexes.length) { defs.push(indexes.map(ndx => ndx.stringify()).join(',\n\t')); }
const basename = this.basename() || (this.CONTEXT instanceof this.constructor.Node ? this.CONTEXT.NAME/* DB won't have actually been renamed */ : this.CONTEXT?.name);
return `CREATE TABLE${ this.hasFlag('IF_NOT_EXISTS') ? ' IF NOT EXISTS' : '' } ${ this.autoEsc([basename, this.name()].filter(s => s)).join('.') } (\n\t${ defs.join(',\n\t') }\n)`;
}

@@ -131,49 +384,35 @@

if (!tblName) return;
const instance = new this(context, tblName, dbName || context/*Database*/?.name);
const instance = (new this(context))
.name(tblName)
.basename(dbName);
if (ifNotExists) instance.withFlag('IF_NOT_EXISTS');
const defs = Lexer.split(_unwrap(bodyPart, '(', ')'), [',']).map(def => {
return parseCallback(instance, def.trim(), [TableLevelConstraint,Index,Column]); // Note that Column must come last
return parseCallback(instance, def.trim(), [PrimaryKey2,ForeignKey2,UniqueKey2,Check,Index,Column]); // Note that Column must come last
});
for (const def of defs) {
if (def instanceof TableLevelConstraint) instance.constraint(def);
if (def instanceof Column) instance.column(def);
else if (def instanceof Index) instance.index(def);
else instance.column(def);
else instance.constraint(def);
}
return instance;
}
/**
* @inheritdoc
*/
static cloneJson(json) {
const jsonClone = structuredClone(json);
// ----------------
const rebase = (obj, key) => {
const value = obj[key];
Object.defineProperty(obj, `$${ key }`, { get: () => value, configurable: true });
};
rebase(jsonClone, 'name');
for (const column of jsonClone.columns || []) {
for (const type of ['primaryKey', 'references', 'uniqueKey', 'check']) { column[type] && rebase(column[type], 'constraintName'); }
rebase(column, 'name');
}
for (const constraint of jsonClone.constraints || []) { rebase(constraint, 'constraintName'); }
for (const index of jsonClone.indexes || []) { rebase(index, 'indexName'); }
// ----------------
const redefine = (obj, key, nameKey) => {
const arr = obj[key];
Object.defineProperty(obj, key, { get() { return arr; } });
Object.defineProperties(arr, {
get: { value: name => arr.find(x => x[nameKey] === name), configurable: true },
has: { value: name => arr.get(name) ? true : false, configurable: true },
delete: { value: name => arr.splice(arr.findIndex(x => x[nameKey] === name), 1), configurable: true },
});
};
redefine(jsonClone, 'columns', 'name');
redefine(jsonClone, 'constraints', 'constraintName');
redefine(jsonClone, 'indexes', 'indexName');
// ----------------
return jsonClone;
}
/**
*
* @param Any a
* @param Any b
* @returns
*/
function isSame(a, b) {
if (a === b) return true;
if (Array.isArray(a) && Array.isArray(b) && a.length === b.length) {
const $b = b.slice(0).sort();
return a.slice(0).sort().every((x, i) => isSame(x, $b[i]));
}
const temp = {};
if (typeof a === 'object' && a && typeof b === 'object' && b && (temp.keys_a = Object.keys(a)).length === (temp.keys_b = Object.keys(b)).length) {
return temp.keys_a.reduce((prev, k) => prev && isSame(a[k], b[k]), true);
}
return false;
}

@@ -10,4 +10,4 @@

*/
NAME = '';
PRECISION = 0;
NAME;
PRECISION;

@@ -30,7 +30,2 @@ /**

}
/**
* @inheritdoc
*/
stringify() { return `${ this.NAME }${ this.PRECISION ? `(${ this.PRECISION })` : `` }`; }

@@ -48,2 +43,7 @@ /**

}
/**
* @inheritdoc
*/
stringify() { return `${ this.NAME }${ this.PRECISION ? `(${ this.PRECISION })` : `` }`; }

@@ -50,0 +50,0 @@ /**

import Lexer from '../Lexer.js';
import { _unwrap } from '@webqit/util/str/index.js';
import Node from '../abstracts/Node.js';
import AbstractNode from './abstracts/AbstractNode.js';
export default class Index extends Node {
export default class Index extends AbstractNode {
TYPE;
$TYPE;
COLUMNS = [];
$COLUMNS = [];
/**
* Instance properties
* @inheritdoc
*/
INDEX_NAME = '';
TYPE = '';
COLUMNS = [];
static get WRITABLE_PROPS() { return ['TYPE', 'COLUMNS'].concat(super.WRITABLE_PROPS); }
/**
* @constructor
/**
* Sets/gets the index type,
*
* @param Void|String value
*
* @returns this
*/
constructor(context, indexName, type, columns) {
super(context);
this.INDEX_NAME = indexName;
this.TYPE = type;
this.COLUMNS = columns;
type(value) {
if (!arguments.length) return this[this.smartKey('TYPE')];
return (this[this.smartKey('TYPE', true)] = value, this);
}
/**
* Sets/gets the index columns,
*
* @param Void|Array columns
*
* @returns this
*/
columns(columns) {
if (!arguments.length) return this[this.smartKey('COLUMNS')];
return (this[this.smartKey('COLUMNS', true)] = [].concat(columns), this);
}
/**
* @inheritdoc

@@ -30,6 +47,23 @@ */

type: this.TYPE,
...(this.$TYPE ? { $type: this.$TYPE } : {}),
columns: this.COLUMNS,
...(this.INDEX_NAME ? { indexName: this.INDEX_NAME } : {})
...(this.$COLUMNS.length ? { $columns: this.$COLUMNS } : {}),
...super.toJson(), // Status
};
}
/**
* @inheritdoc
*/
static fromJson(context, json) {
if (typeof json?.type !== 'string' || !/^(INDEX|KEY|FULLTEXT)$/i.test(json.type) || !json.columns?.length) return;
return super.fromJson(context, json, () => {
const instance = (new this(context))
.columns(json.columns)
.type(json.type);
instance.hardSet(json.$columns, val => instance.columns(val));
instance.hardSet(json.$type, val => instance.type(val));
return instance;
});
}

@@ -39,3 +73,3 @@ /**

*/
stringify() { return `${ this.TYPE }${ this.INDEX_NAME ? ` ${ this.INDEX_NAME }` : '' } (${ this.COLUMNS.join(', ') })`; }
stringify() { return `${ this.type() }${ this.name() ? ` ${ this.name() }` : '' } (${ this.columns().join(', ') })`; }

@@ -53,20 +87,7 @@ /**

});
return new this(context, name, type.toUpperCase(), columns);
return (new this(context))
.type(type.replace(/\s+(INDEX|KEY)/i, '').toUpperCase())
.columns(columns)
.name(name);
}
/**
* @inheritdoc
*/
static fromJson(context, json) {
if (typeof json.indexName !== 'string' && (typeof json?.type !== 'string' || !json.type.match(/INDEX|KEY|FULLTEXT/i))) return;
return new this(context, json.indexName, json.type, json.columns);
}
/**
* @property Object
*/
static attrEquivalents = {
fulltext: 'FULLTEXT',
index: 'INDEX',
};
}

@@ -9,20 +9,15 @@

*/
NAME = '';
NAME;
/**
* @constructor
*/
constructor(context, name) {
super(context);
this.NAME = name;
}
/**
* Sets the name
* Returns name or sets name.
*
* @param String name
* @param Void|String name
*
* @returns Void
* @returns String
*/
name(name) { this.NAME = name; }
name(name) {
if (!arguments.length) return this.NAME;
return (this.NAME = name, this);
}

@@ -39,3 +34,3 @@ /**

if (typeof json?.name !== 'string') return;
return (new this(context, json.name)).withFlag(...(json.flags || []));;
return (new this(context)).name(json.name).withFlag(...(json.flags || []));;
}

@@ -56,3 +51,3 @@

if (!dbName) return;
const instance = new this(context, dbName);
const instance = (new this(context)).name(dbName);
if (ifExists) instance.withFlag('IF_EXISTS');

@@ -59,0 +54,0 @@ if (cascade) instance.withFlag('CASCADE');

@@ -9,26 +9,27 @@

*/
NAME = '';
BASENAME = '';
NAME;
BASENAME;
/**
* @constructor
* Returns name or sets name.
*
* @param Void|String name
*
* @returns String
*/
constructor(context, name, basename) {
super(context);
this.NAME = name;
this.BASENAME = basename;
name(name) {
if (!arguments.length) return this.NAME;
return (this.NAME = name, this);
}
/**
* Sets the name
* Returns basename or sets basename.
*
* @param Array|String name
* @param Void|String name
*
* @returns Void
* @returns String
*/
name(name) {
const nameParts = Array.isArray(name) ? [...name] : [name];
this.NAME = nameParts.pop();
this.BASENAME = nameParts.pop();
if (nameParts.length) throw new Error(`Idents can be maximum of two parts. Recieved: ${ nameParts.reverse().join('.') }.${ this.BASENAME }.${ this.NAME }`);
basename(basename) {
if (!arguments.length) return this.BASENAME;
return (this.BASENAME = basename, this);
}

@@ -46,3 +47,3 @@

if (typeof json?.name !== 'string') return;
return (new this(context, json.name, json.basename)).withFlag(...(json.flags || []));
return (new this(context)).name(json.name).basename(json.basename).withFlag(...(json.flags || []));
}

@@ -63,3 +64,3 @@

if (!tblName) return;
const instance = new this(context, tblName, dbName);
const instance = (new this(context)).name(tblName).basename(dbName);
if (ifExists) instance.withFlag('IF_EXISTS');

@@ -66,0 +67,0 @@ if (cascade) instance.withFlag('CASCADE');

@@ -16,3 +16,2 @@

if (!expr?.length) return;
const $grammar = grammar?.length ? grammar : this.grammar;

@@ -19,0 +18,0 @@ for (const Node of $grammar) {

@@ -114,3 +114,3 @@

if (!exprNode) {
aliasUnescaped = aliasEscaped = null;
$alias = aliasUnescaped = aliasEscaped = null;
$expr = expr; // IMPORTANT

@@ -117,0 +117,0 @@ }

@@ -355,4 +355,4 @@

{ backtest: '^(?!.*~$)', test: '>', regex: '(?<!~)>' },
'((\\s+(?:NOT\\s+)?IS\\s+(?:NOT\\s+)?(TRUE|FALSE|NULL|UNKNOWN|DISTINCT\\s+FROM)\\s+)|\\s+(ISNULL|NOTNULL|IN|ANY|LIKE|(?:NOT\\s+)?BETWEEN(?:\\s+SYMMETRIC)?)\\s+|(?:\\s+)?(=|<=|>=|!=|<>)(?:\\s+)?)',
'((\\s+(?:NOT\\s+)?IS\\s+(?:NOT\\s+)?(TRUE|FALSE|NULL|UNKNOWN|DISTINCT\\s+FROM\\s+))|\\s+(ISNULL|NOTNULL|IN|ANY|LIKE|(?:NOT\\s+)?BETWEEN(?:\\s+SYMMETRIC)?)\\s+|(?:\\s+)?(=|<=|>=|!=|<>)(?:\\s+)?)',
];
}
import Lexer from '../Lexer.js';
import Assertion from './Assertion.js';
import Parens from './Parens.js';
import Node from '../abstracts/Node.js';

@@ -32,3 +33,3 @@

this.LOGIC = 'AND';
return (this.build('ASSERTIONS', assertions, [Condition,Assertion]), this);
return (this.build('ASSERTIONS', assertions, [Condition,Assertion,Parens]), this);
}

@@ -46,3 +47,3 @@

this.LOGIC = 'OR';
return (this.build('ASSERTIONS', assertions, [Condition,Assertion]), this);
return (this.build('ASSERTIONS', assertions, [Condition,Assertion,Parens]), this);
}

@@ -49,0 +50,0 @@

@@ -46,3 +46,3 @@

*/
stringify() { return `${ this.NAME.toUpperCase() }(${ this.ARGS.join(',') })`; }
stringify() { return `${ this.NAME.toUpperCase() }(${ this.ARGS.join(', ') })`; }

@@ -49,0 +49,0 @@ /**

@@ -24,2 +24,3 @@

if (nameParts.length) throw new Error(`Idents can be maximum of two parts. Recieved: ${ nameParts.reverse().join('.') }.${ this.BASENAME }.${ this.NAME }`);
return this;
}

@@ -30,3 +31,6 @@

*/
toJson() { return { name: this.BASENAME ? [this.BASENAME,this.NAME] : this.NAME, flags: this.FLAGS }; }
toJson() {
const name = this.BASENAME ? [this.BASENAME,this.NAME] : this.NAME;
return this.FLAGS.length ? { name, flags: this.FLAGS } : name;
}

@@ -49,3 +53,3 @@ /**

return this.autoEsc([this.BASENAME, this.NAME].filter(s => s)).join('.') + (
this.FLAGS.length ? ` ${ this.FLAGS.map(s => s.replace(/_/g, ' ')).join(' ') }` : ''
''//this.FLAGS.length ? ` ${ this.FLAGS.map(s => s.replace(/_/g, ' ')).join(' ') }` : ''
);

@@ -52,0 +56,0 @@ }

@@ -133,8 +133,8 @@

if (!joinMatch) return;
const { tokens: [ $table, $correlation ], matches } = Lexer.lex(joinSpec, ['ON|USING'], { useRegex:'i' });
const { tokens: [ $table, $correlation ], matches } = Lexer.lex(joinSpec, ['\\s+(?:ON|USING)\\s+'], { useRegex:'i' });
const instance = super.parse(context, $table.trim(), parseCallback);
instance.TYPE = type.trim().toUpperCase() + '_JOIN';
if (/^USING$/i.test(matches[0])) {
if (/USING/i.test(matches[0])) {
instance.using(parseCallback(instance, $correlation.trim(), [Identifier]));
} else if (/^ON$/i.test(matches[0])) {
} else if (/ON/i.test(matches[0])) {
instance.on(parseCallback(instance, $correlation.trim(), [Condition,Assertion]));

@@ -141,0 +141,0 @@ }

import Lexer from '../Lexer.js';
import Identifier from './Identifier.js';
import CreateTable from '../create/CreateTable.js';
import JsonPath from './json/JsonPath.js';

@@ -65,10 +66,10 @@ import Node from '../abstracts/Node.js';

async eval() {
const getPrimaryKey = schema => schema.columns.find(col => col.primaryKey)?.name || schema.constraints.find(cons => cons.type === 'PRIMARY_KEY')?.columns[0];
const getKeyDef = (schema, foreignKey) => schema.columns.find(col => col.name === foreignKey.NAME)?.references || schema.constraints.find(cons => cons.type === 'FOREIGN_KEY' && cons.columns.includes(foreignKey.NAME))?.references;
const getPrimaryKey = schema => schema.columns.find(col => col.primaryKey)?.name || schema.constraints.find(cons => cons.type === 'PRIMARY_KEY')?.targetColumns[0];
const getKeyDef = (schema, foreignKey) => schema.columns.find(col => col.name === foreignKey.NAME)?.references || schema.constraints.find(cons => cons.type === 'FOREIGN_KEY' && cons.targetColumns.includes(foreignKey.NAME));
const getSchema = async (tblName, dbName) => {
const clientApi = this.rootNode.CONTEXT;
const basename = dbName || await clientApi.getBasename(tblName);
const basename = dbName || await clientApi.basenameGet(tblName);
const dbApi = clientApi.database(basename);
if (!(await dbApi.tables({ name: tblName })).length) return;
return await dbApi.describeTable(tblName, { force: true });
if (!(await dbApi.hasTable(tblName))) return;
return await dbApi.describeTable(tblName);
};

@@ -98,3 +99,3 @@ if (!this.rootNode.CONTEXT) throw new Error(`No client API in context.`);

// Get schema_lhs from keyDef
const table_lhs = Identifier.fromJson(this, keyDef_rhs.basename ? [keyDef_rhs.basename,keyDef_rhs.table] : keyDef_rhs.table);
const table_lhs = Identifier.fromJson(this, keyDef_rhs.basename ? [keyDef_rhs.basename,keyDef_rhs.targetTable] : keyDef_rhs.targetTable);
const schema_lhs = await getSchema(table_lhs.NAME, table_lhs.BASENAME);

@@ -127,3 +128,3 @@ if (!schema_lhs) throw new Error(`[${ this }]: The implied table ${ table_lhs } does not exist.`);

// Get schema_rhs from keyDef!
const table_rhs = Identifier.fromJson(this, keyDef_lhs.basename ? [keyDef_lhs.basename,keyDef_lhs.table] : keyDef_lhs.table);
const table_rhs = Identifier.fromJson(this, keyDef_lhs.basename ? [keyDef_lhs.basename,keyDef_lhs.targetTable] : keyDef_lhs.targetTable);
const schema_rhs = await getSchema(table_rhs.NAME, table_rhs.BASENAME || table_lhs.BASENAME);

@@ -130,0 +131,0 @@ if (!schema_rhs) throw new Error(`[${ this }]: The implied table ${ table_rhs } does not exist.`);

@@ -57,4 +57,4 @@

if (!/^(TRUE|FALSE|NULL)$/i.test(expr)) return;
return new this(context, JSON.parse(expr));
return new this(context, JSON.parse(expr.toLowerCase()));
}
}

@@ -6,2 +6,3 @@

import pg from 'pg';
import CreateTable from '../src/query/create/CreateTable.js';
import SQLClient from '../src/api/sql/SQLClient.js';

@@ -15,3 +16,3 @@

let showQuery;
let showQuery = false;
const lqlClient = new SQLClient({

@@ -26,2 +27,3 @@ query() {

console.log('DROP 4', await lqlClient.query('DROP DATABASE if exists obj_information_schema CASCADE'));
console.log('DROP 3', await lqlClient.query('DROP TABLE if exists public.books'));

@@ -55,19 +57,54 @@ console.log('DROP 2', await lqlClient.query('DROP TABLE if exists public.users'));

const savepoint3 = await lqlClient.database('public').savepoint();
console.log('\n\n\n\n\n\ntables---------', await lqlClient.database('public').tables());
/*
console.log('rollback 3', await savepoint3.rollback());
console.log('rollback 2', await savepoint2.rollback());
console.log('rollback 1', await savepoint1.rollback());
*/
await lqlClient.query(`INSERT INTO roles (name, created_time) VALUES ('admin', now()), ('guest', now())`);
await lqlClient.query(`INSERT INTO users (title, name, role, created_time) VALUES ('Mr.', 'Ox-Harris', 1, now()), ('Mrs.', 'Jane', 2, now())`);
await lqlClient.query(`INSERT INTO books (title, content, author, created_time) VALUES ('Rich Dad & Poor Dad', 'content...1', 1, now()), ('Beauty & the Beast', 'content...2', 2, now())`);
console.log('\n\n\n\n\n\current savepoint-----', (await lqlClient.database('public').savepoint()).toJson());
//const ww = await lqlClient.query(`SELECT title, content, author ~> name, author ~> role ~> name role_name FROM books where author ~> role ~> name = 'admin'`);
//const ww = await lqlClient.query(`SELECT name, role <~ author <~ books ~> title FROM roles`);
const ww = await lqlClient.query(`SELECT users.name, roles.name as role_name FROM users LEFT JOIN roles ON roles.id = users.role where roles.name = $1`, { params: ['admin'] });
console.log(ww);
let spliceForwardHistories = false;
if (spliceForwardHistories) {
console.log('.....create publications.....', await lqlClient.query(`CREATE TABLE publications (
id int primary key generated always as identity,
title varchar,
content varchar,
created_time timestamp
)`, { savepointDesc: 'Created publications' }));
const savepoint4 = await lqlClient.database('public').savepoint();
// Should see: 1,2,3,7
console.log('\n\n\n\n\n\nall savepoints-----', ...(await lqlClient.database('obj_information_schema').table('database_savepoints').getAll()));
} else {
// Roll forward
for (let i = 0; i < 3; i ++) {
await (await lqlClient.database('public').savepoint({ direction: 'forward' })).rollback();
}
// Should see: 1,2,3
console.log('\n\n\n\n\n\nall savepoints-----', ...(await lqlClient.database('obj_information_schema').table('database_savepoints').getAll()));
await lqlClient.query(`INSERT INTO roles (name, created_time) VALUES ('admin', now()), ('guest', now())`);
await lqlClient.query(`INSERT INTO users (title, name, role, created_time) VALUES ('Mr.', 'Ox-Harris', 1, now()), ('Mrs.', 'Jane', 2, now())`);
await lqlClient.query(`INSERT INTO books (title, content, author, created_time) VALUES ('Rich Dad & Poor Dad', 'content...1', 1, now()), ('Beauty & the Beast', 'content...2', 2, now())`);
//const ww = await lqlClient.query(`SELECT title, content, author ~> name, author ~> role ~> name role_name FROM books where author ~> role ~> name = 'admin'`);
//const ww = await lqlClient.query(`SELECT name, role <~ author <~ books ~> title FROM roles`);
const ww = await lqlClient.query(`SELECT users.name, roles.name as role_name FROM users LEFT JOIN roles ON roles.id = users.role where roles.name = $1`, { params: ['admin'] });
console.log(ww);
}
// Should see: 6 or 7
console.log('\n\n\n\n\n\current savepoint-----', (await lqlClient.database('public').savepoint()).toJson());
console.log('the end.');
import pg from 'pg';
import mysql from 'mysql2';
import Parser from '../src/Parser.js';
import CreateTable from '../src/statements/schema/CreateTable.js';
import AlterTable from '../src/statements/schema/AlterTable.js';
import SQLClient from '../src/databases/sql/SQLClient.js';
import CreateTable from '../src/query/create/CreateTable.js';
import SQLClient from '../src/api/sql/SQLClient.js';

@@ -13,5 +11,2 @@ // PG Client

port: 5432,
//user: 'oxharris',
//password: '',
//database: 'oxharris',
});

@@ -46,114 +41,15 @@ await pgClient.connect();

const createTableSql = `
CREATE TABLE IF NOT EXISTS test0 (
id int PRIMARY KEY CONSTRAINT genn generated by default as identity,
ref int CONSTRAINT nn not null CONSTRAINT uni_q unique CONSTRAINT fk REFERENCES pretest (id) MATCH FULL ON DELETE RESTRICT ON UPDATE SET NULL,
ref2 int,
rand VARCHAR (11) CHECK (rand IS NOT NULL),
rand2 text,
CONSTRAINT ck CHECK (ref > 10),
CONSTRAINT fk2 FOREIGN KEY (ref2) REFERENCES pretest2 (id),
UNIQUE (rand2,rand)
)`;
const sqlClient = new SQLClient(client, { dialect });
const dbName = 'public2', dbName2 = 'public';
const alterTableSql = `
ALTER TABLE IF EXISTS public.test
RENAME AS new_tbl_name,
RENAME constraint constraint_name1 TO new_constraint_name,
console.log('..............', await sqlClient.databases());
console.log('..............', await sqlClient.database(dbName).tables());
ADD constraint constraint_name2 PRIMARY KEY (col_name1),
ADD constraint fruit_type UNIQUE (hhh),
ADD PRIMARY KEY (col_name2),
ADD CHECK (check_expr),
ADD FULLTEXT INDEX (ft_name),
ADD column new_col varchar(10) FIRST,
ADD column new_col2 int AFTER refColumn,
DROP constraint if exists constraint_name3 cascade,
DROP PRIMARY KEY,
DROP FOREIGN KEY fk_name,
DROP COLUMN if exists col_name,
DROP col_name,
ALTER column column_name set data type varchar(60),
ALTER constraint constraint_name4 INVISIBLE,
ALTER COLUMN column_name8 SET COMPRESSION compression_method,
ALTER constraint constraint_name8 DEFERRABLE
`;
// ALTER column column_name2 set AUTO_INCREMENT,
/*
console.log('');
console.log('');
console.log('Create TABLE pretest');
console.log('');
const sql0 = `
CREATE TABLE IF NOT EXISTS pretest (
id int primary key,
desc0 text not null
)`;
const result0 = await client.query(sql0);
console.log(result0);
// --------------------------------------------
console.log('');
console.log('');
console.log('Create TABLE pretest2');
console.log('');
const sql1 = `
CREATE TABLE IF NOT EXISTS pretest2 (
id int primary key,
desc0 text not null
)`;
const result1 = await client.query(sql1);
console.log(result1);
// --------------------------------------------
console.log('');
console.log('');
console.log('Create TABLE test0');
console.log('');
const result2 = await client.query(createTableSql);
console.log(result2);
// --------------------------------------------
console.log('');
console.log('');
console.log('Parse CREATE TABLE');
console.log('');
const result5 = await Parser.parse(createTableSql);
console.log(JSON.stringify(result5.toJson(), null, 3));
console.log(result5 + '');
console.log(CreateTable.fromJson(result5.toJson()) + '');
*/
// --------------------------------------------
console.log('');
console.log('');
console.log('Show CREATE TABLE');
console.log('');
const sqlClient = new SQLClient(client, { dialect });
const dbName = 'public2', dbName2 = 'public';
const savepoint = await sqlClient.alterDatabase(dbName, dbSchema => {
dbSchema.name = dbName2;
dbSchema.name(dbName);
}, { savepointDesc: 'To public2' });
console.log('............../////////////// alter public db', savepoint.toJson());
console.log('............../////////////// alter public db', savepoint?.toJson());
console.log('ALTER DATABASE public', await savepoint?.rollback());
console.log('ALTER DATABASE public', await savepoint.rollback());

@@ -163,4 +59,3 @@

const publicDb = sqlClient.database(dbName);
const publicDb = sqlClient.database(dbName2);
console.log('public@', (await publicDb.savepoint({ direction: 'forward' }))?.toJson());

@@ -172,10 +67,10 @@

{ name: 'id', type: 'int', primaryKey: true },
{ name: 'author1', type: 'int', references: { table: 'test0', columns: ['id'] }, },
{ name: 'author1', type: 'int', references: { targetTable: 'books', targetColumns: ['id'] }, },
{ name: 'author2', type: 'int', },
{ name: 'content', type: { name: 'varchar', maxLen: 30 }, default: '\'Hello world\'', },
{ name: 'content', type: { name: 'varchar', precision: 30 }, default: '\'Hello world\'', },
{ name: 'isbn', type: 'int', identity: { always: false }, notNull: true },
],
constraints: [
{ type: 'FOREIGN KEY', columns: ['author2'], references: { table: 'test0', columns: ['id'] } },
{ type: 'UNIQUE KEY', columns: ['author2', 'author1'] },
{ type: 'FOREIGN_KEY', columns: ['author2'], targetTable: 'books', targetColumns: ['id'] },
{ type: 'UNIQUE_KEY', columns: ['author2', 'author1'] },
],

@@ -186,12 +81,11 @@ indexes: []

await publicDb.dropTable(booksTbl.name, { ifExists: true });
console.log('HAS?', await publicDb.hasTable('books'), 'THEN, CREATE TABLE books IF NOT EXISTS, then DESCRIBE');
const t = await publicDb.createTable(booksTbl, { ifNotExists: true });
const booksSchema = await publicDb.describeTable('books');
console.log('SCHEMA',booksSchema);
console.log('CREATE TABLE books IF NOT EXISTS, then DESCRIBE');
console.log(CreateTable.fromJson(publicDb, booksSchema) + '');
await publicDb.createTable(booksTbl, { ifNotExists: true });
const booksSchema = await publicDb.describeTable('books');
console.log(CreateTable.fromJson(booksSchema) + '');
const tt = await publicDb.alterTable('books', tblSchema => {
const isbn = tblSchema.columns.find(col => col.name === 'isbn');
delete isbn.identity;
tblSchema.column('isbn').identity(false);
});

@@ -209,3 +103,3 @@

console.log('----------------------------------------------------------------');
console.log('-----------------------------||-----------------------------------');

@@ -220,3 +114,8 @@ // ----------------

{ name: 'id', type: 'int', primaryKey: true },
]
],
constraints: [{
type: 'UNIQUE_KEY',
name: 'uniiiiiiiiiiiiiique',
columns: ['id'],
}]
},

@@ -226,8 +125,15 @@ booksTbl,

const dbApi = await sqlClient.createDatabase(dbCreateRequest);
const savepoint0 = await dbApi.savepoint();
await sqlClient.database('obj_information_schema').table('database_savepoints').deleteAll();
const savepoint0 = await sqlClient.createDatabase(dbCreateRequest);
console.log('-----------------------------|||-----------------------------------');
const dbApi = sqlClient.database(dbCreateRequest.name);
//const savepoint0 = await sqlClient.alterDatabase(dbCreateRequest.name, dbSchema => dbSchema.name = 'some_new_db');
globalParams.showSql = true;
//globalParams.showSql = true;
console.log('---------------------------------------rolling back to CREATION POINT (DROP DB):', savepoint0.toJson());
console.log('---------------------------------------rollback done:', await savepoint0.rollback({ allowMutateDB: true }));
console.log(':::::::::::::::::------------', '' instanceof String, await sqlClient.database('obj_information_schema').table('database_savepoints').getAll());

@@ -261,9 +167,9 @@ //console.log('\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\', savepoint0.toJson());

// Rename DB
dbSchema.name = 'new_db_name';
dbSchema.name('new_db_name');
// Modify column
dbSchema.tables.get('test0').columns.get('id').uniqueKey = true;
dbSchema.table('test0').column('id').identity(true);
// Remove test1 table
dbSchema.tables.delete('test1');
dbSchema.table('test1').drop();
/// Add table test2
dbSchema.tables.push({
dbSchema.table({
name: 'test2',

@@ -276,2 +182,3 @@ columns: [

console.log('````````````````````````````````````````````', (await sqlClient.database('new_db_name').describeTable('test0')).columns);
await savepoint3.rollback();

@@ -278,0 +185,0 @@

@@ -29,4 +29,4 @@

await sqlClient.dropDatabase('new_db_name', { ifExists: true, cascade: true, noCreateSavepoint: true });
await sqlClient.alterDatabase('private', db => db.name = 'public', { ifExists: true, noCreateSavepoint: true });
//console.log('//////////////////////', await sqlClient.databases({ force: true }));
//await sqlClient.alterDatabase('private', db => db.name('public'), { noCreateSavepoint: true });
//console.log('//////////////////////', await sqlClient.databases());
//console.log(await sqlClient.database('public').dropTable('test2', { cascade: true }));

@@ -38,14 +38,13 @@ //console.log(await sqlClient.database('public').tables());

describe(`Rename DB and rollback`, function() {
// Globals
let publicBefore = 'public', publicAfter = 'private', savepoint0;
let publicDb, savepoint0;
it(`DO: ${ desc0 }`, async function() {
savepoint0 = await sqlClient.alterDatabase('public', dbSchema => {
dbSchema.name = 'private';
savepoint0 = await sqlClient.alterDatabase(publicBefore, dbSchema => {
dbSchema.name(publicAfter);
}, {
savepointDesc: 'Rename to private',
});
publicDb = sqlClient.database('public');
const databases = await sqlClient.databases({ force: true });
expect(databases).to.be.an('array').that.includes('private').and.not.includes('public');
const databases = await sqlClient.databases();
expect(databases).to.be.an('array').that.includes(publicAfter).and.not.includes(publicBefore);
});

@@ -57,19 +56,19 @@

const databases = await sqlClient.databases();
expect(databases).to.be.an('array').that.includes('public').and.not.includes('private');
expect(databases).to.be.an('array').that.includes(publicBefore).and.not.includes(publicAfter);
});
it(`ROLLFORWARD: ${ desc0 }`, async function() {
const savepoint = await publicDb.savepoint({ direction: 'forward' });
const savepoint = await sqlClient.database(publicBefore).savepoint({ direction: 'forward' });
const success = await savepoint.rollback();
expect(success).to.be.true;
const databases = await sqlClient.databases();
expect(databases).to.be.an('array').that.includes('private').and.not.includes('public');
expect(databases).to.be.an('array').that.includes(publicAfter).and.not.includes(publicBefore);
});
it(`ROLLBACK (2): ${ desc0 }`, async function() {
const savepoint = await publicDb.savepoint();
const savepoint = await sqlClient.database(publicAfter).savepoint();
const success = await savepoint.rollback();
expect(success).to.be.true;
const databases = await sqlClient.databases();
expect(databases).to.be.an('array').that.includes('public').and.not.includes('private');
expect(databases).to.be.an('array').that.includes(publicBefore).and.not.includes(publicAfter);
});

@@ -84,4 +83,4 @@

};
const tblApi = await publicDb.createTable(tblCreateRequest, { ifNotExists: true });
const tables = await publicDb.tables();
const tblApi = await sqlClient.database(publicBefore).createTable(tblCreateRequest, { ifNotExists: true });
const tables = await sqlClient.database(publicBefore).tables();
expect(tables).to.be.an('array').that.includes('test2');

@@ -104,3 +103,3 @@ });

{ name: 'age', type: 'int' },
{ name: 'parent', type: 'int', references: { table: 'users', columns: ['id'] } },
{ name: 'parent', type: 'int', references: { targetTable: 'users', targetColumns: ['id'] } },
]

@@ -111,10 +110,10 @@ }, {

{ name: 'id', type: 'int', primaryKey: true },
{ name: 'author1', type: 'int', references: { table: 'users', columns: ['id'] }, },
{ name: 'author1', type: 'int', references: { targetTable: 'users', targetColumns: ['id'] }, },
{ name: 'author2', type: 'int', },
{ name: 'content', type: { name: 'varchar', maxLen: 30 }, default: { expr: '\'Hello world\'' }, },
{ name: 'content', type: { name: 'varchar', precision: 30 }, default: { expr: '\'Hello world\'' }, },
{ name: 'isbn', type: 'int', identity: { always: false }, notNull: true },
],
constraints: [
{ type: 'FOREIGN_KEY', columns: ['author2'], references: { table: 'users', columns: ['id'] } },
{ type: 'UNIQUE', columns: ['author2', 'author1'] },
{ type: 'FOREIGN_KEY', columns: ['author2'], targetTable: 'users', targetColumns: ['id'] },
{ type: 'UNIQUE_KEY', columns: ['author2', 'author1'] },
],

@@ -125,6 +124,7 @@ indexes: []

let someDb;
let savepoint0, someDb;
it(`DO: ${ desc1 }`, async function() {
someDb = await sqlClient.createDatabase(dbCreateRequest);
savepoint0 = await sqlClient.createDatabase(dbCreateRequest);
someDb = sqlClient.database(dbCreateRequest.name);
const databases = await sqlClient.databases();

@@ -136,5 +136,3 @@ expect(databases).to.be.an('array').that.includes('some_db');

let savepoint0;
it(`ROLLBACK: ${ desc1 } (BY DROPPING DB)`, async function() {
savepoint0 = await someDb.savepoint();
const success = await savepoint0.rollback({ allowMutateDB: true });

@@ -148,3 +146,3 @@ expect(success).to.be.true;

// Remeber savepoint0? Let's assert that we can't rollback (since it's been rolled back)
expect((await savepoint0.status()).canRollback).to.be.false;
expect(await savepoint0.canRollback()).to.be.false;
const savepoint = await someDb.savepoint({ direction: 'forward' });

@@ -156,5 +154,5 @@ const success = await savepoint.rollback({ allowMutateDB: true });

const tables = await someDb.tables();
expect(tables).to.be.an('array').that.eql(['users','books']);
expect(tables).to.be.an('array').that.have.members(['users','books']);
// Call out savepoint0! Let's assert that now we can rollback (since it's been rolled forward)
expect((await savepoint0.status()).canRollback).to.be.true;
expect(await savepoint0.canRollback()).to.be.true;
});

@@ -177,3 +175,3 @@

const tables = await someDb.tables();
expect(tables).to.be.an('array').that.eql(['users','books']);
expect(tables).to.be.an('array').that.have.members(['users','books']);
});

@@ -188,7 +186,7 @@

};
const tblApi = await someDb.createTable(tblCreateRequest);
const tblSavepoint = await someDb.createTable(tblCreateRequest);
const tables = await someDb.tables();
expect(tables).to.be.an('array').that.eql(['users','books','test1']);
const tblSavepointDetails = await someDb.table('test1').savepoint();
expect(await tblSavepointDetails.context.status()).to.be.an('object').with.property('canRollback', true);
expect(tables).to.be.an('array').that.have.members(['books','test1','users',]);
//TODO:const tblSavepointDetails = await someDb.table('test1').savepoint();
//TODO:expect(await tblSavepointDetails.context.status()).to.be.an('object').with.property('canRollback', true);
});

@@ -203,9 +201,9 @@

// Rename DB
dbSchema.name = 'new_db_name';
dbSchema.name('new_db_name');
// Modify column
dbSchema.tables.get('users').columns.get('id').uniqueKey = true;
dbSchema.table('users').column('id').uniqueKey(true);
// Remove test1 table
dbSchema.tables.delete('test1');
dbSchema.table('test1').drop();
/// Add table test2
dbSchema.tables.push({
dbSchema.table({
name: 'test2',

@@ -217,6 +215,6 @@ columns: [

});
expect(someDb.name).to.eql('new_db_name');
const someDb = sqlClient.database('new_db_name');
const tables = await someDb.tables();
expect(tables).to.be.an('array').that.eql(['users','books','test2']);
const users = await someDb.describeTable('users', { force: true });
expect(tables).to.be.an('array').that.have.members(['users','books','test2']);
const users = await someDb.describeTable('users');
expect(users.columns.find(col => col.name === 'id').uniqueKey).to.be.an('object');

@@ -223,0 +221,0 @@

@@ -23,3 +23,2 @@

const sqlClient = new SQLClient($pgClient, { dialect: 'postgres' });
console.log('\n\n\n\n\nn\n\n;;;;;;;;;;;;;;;;;;;;;;;;', await sqlClient.searchPath(), '\n\n\n\n\n');
// --------------------------

@@ -73,3 +72,3 @@

ADD column new_col varchar(10) references distant_tbl (id) on update restrict on delete cascade FIRST,
MODIFY column new_col varchar(10) references distant_tbl (id) on update restrict on delete cascade FIRST,
ADD column new_col2 int AFTER refColumn,

@@ -84,11 +83,11 @@

ALTER column "column _name" set data type varchar(60),
ALTER constraint constraint_name4 INVISIBLE,
ALTER CONSTRAINT constraint_name4 INVISIBLE,
ALTER COLUMN column_name8 SET COMPRESSION compression_method,
ALTER constraint constraint_name8 DEFERRABLE
`;
const tblAlterInstance1 = await Parser.parse({ name: 'some_database' }, alterTableSql);
const tblAlterInstance1 = Parser.parse({ name: 'some_database' }, alterTableSql);
const tblAlterInstance2 = AlterTable.fromJson(tblAlterInstance1.CONTEXT, tblAlterInstance1.toJson());
const sql1 = tblAlterInstance1 + '';
const sql2 = tblAlterInstance2 + '';
console.log(sql1);
console.log(sql2);
/*

@@ -103,35 +102,31 @@ console.log(sql2);

it(`DO: Diffs 2 schemas into an Alter Table statement`, async function() {
const schemaA = {
const schema = {
name: 'testt',
$name: 'testtttt',
basename: 'public',
columns: [
{ name: 'id', type: { name: 'VARCHAR', maxLen: 30 }, default: 20 },
{ name: 'author', type: { name: 'INT' }, references: { constraintName: 'fkk', table: 'table1', columns: ['col3', 'col4']} },
{ name: 'id', $name: 'iddd', type: { name: 'VARCHAR', precision: 30 }, $type: 'int', default: 20, $default: 9, notNull: true },
{ name: 'author', type: { name: 'INT' }, references: { name: 'fkk', targetTable: 'table1', targetColumns: ['col3', 'col4']}, status: 'UP' },
],
constraints: [
{ type: 'FOREIGN_KEY', columns: ['col1', 'col2'], references: { table: 'table1', columns: ['col3', 'col4']} },
{ type: 'PRIMARY_KEY', columns: 'col5' },
{ type: 'FOREIGN_KEY', name: 'constraint_name1', columns: ['id', 'author'], targetTable: 'testt', targetColumns: ['col5', 'author'] },
{ type: 'PRIMARY_KEY', columns: 'col5', $columns: ['uuu', 'lll'], name: 'pk', $name: 'pk2' },
],
indexes: []
};
const schemaB = {
name: 'testt',
basename: 'public2',
columns: [
{ name: 'id3', $name: 'id', notNull: true, type: { name: 'vARCHAR', maxLen: 70 }, default: 20 },
{ name: 'author', type: { name: 'INT' }, references: { constraintName: 'fkk222', $constraintName: 'fkk', table: 'table1', columns: ['col3', 'col5']} },
],
constraints: [
{ type: 'FOREIGN_KEY', columns: ['col1', 'col2'], references: { table: 'table1', columns: ['col3', 'col4']} },
{ type: 'PRIMARY_KEY', columns: 'col5' },
],
indexes: []
};
const tblAlterInstance1 = AlterTable.fromDiffing({}, schemaA, schemaB);
const schemaInstance = CreateTable.fromJson({}, schema);
//schemaInstance.status('UP', true);
schemaInstance.column('author').status('DOWN');//.name('author2');
//schemaInstance.reverseAlt(true);
schemaInstance.cascadeAlt();
const tblAlterInstance1 = schemaInstance.getAlt();
const tblAlterInstance2 = AlterTable.fromJson(tblAlterInstance1.CONTEXT, tblAlterInstance1.toJson());
const sql1 = tblAlterInstance1 + '';
const sql2 = tblAlterInstance2 + '';
console.log(sql1);
/*
console.log(sql1);
console.log(sql2);
console.log(JSON.stringify(schemaInstance.toJson(), null, 3));
console.log(JSON.stringify(tblAlterInstance1.toJson(), null, 3));

@@ -138,0 +133,0 @@ console.log(JSON.stringify(tblAlterInstance2.toJson(), null, 3));

@@ -28,3 +28,3 @@

it(`"parse()" the expression and stringify to compare with original`, async function() {
const query1 = await Parser.parse({}, expr1, null, { explain: false });
const query1 = await Parser.parse({}, expr1, null, { log: false });
const query2 = Select.fromJson(query1.CONTEXT, query1.toJson());

@@ -31,0 +31,0 @@ const sql1 = query1 + '';

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc