From 15cda32b91df3da4fee753a53a7fcb0d8c0b1273 Mon Sep 17 00:00:00 2001 From: Justin Lettau Date: Sat, 6 Oct 2018 10:36:34 -0400 Subject: [PATCH] feat: add support for data glob patterns Closes #45 --- README.md | 14 +- src/commands/pull.ts | 304 +++++++--------------- src/commands/push.ts | 32 ++- src/common/file-utility.ts | 112 ++++++++ src/common/utility.ts | 47 ---- src/generators/mssql.ts | 520 +++++++++++++++++++++++++++++++++++++ src/sql/interfaces.ts | 6 +- src/sql/script.ts | 412 ----------------------------- 8 files changed, 774 insertions(+), 673 deletions(-) create mode 100644 src/common/file-utility.ts delete mode 100644 src/common/utility.ts create mode 100644 src/generators/mssql.ts delete mode 100644 src/sql/script.ts diff --git a/README.md b/README.md index 8053c17..916c2e6 100644 --- a/README.md +++ b/README.md @@ -131,7 +131,7 @@ Configuration options are stored in a `ssc.json` file. The following properties **files** (`string[]`): Optional. Glob of files to include/exclude during the `pull` command. Default includes all files. -**data** (`string[]`): Optional. List of table names to include for data scripting during the `pull` command. Default +**data** (`string[]`): Optional. Glob of table names to include for data scripting during the `pull` command. Default includes none. **output** (`object`): Optional. Defines paths where files will be scripted during the `pull` command. The following @@ -217,11 +217,19 @@ Exclude certain files. ``` ### Data -Include data scripting for certain tables. +Only include certain tales. ```js { // ... - "data": ["dbo.LookupTable"] + "data": ["dbo.*"] +} +``` + +Exclude certain tables. +```js +{ + // ... + "data": ["*", "!dbo.*"] } ``` diff --git a/src/commands/pull.ts b/src/commands/pull.ts index 44050dc..6e2d89c 100644 --- a/src/commands/pull.ts +++ b/src/commands/pull.ts @@ -1,15 +1,11 @@ import chalk from 'chalk'; -import * as fs from 'fs-extra'; -import * as glob from 'glob'; import * as sql from 'mssql'; import * as multimatch from 'multimatch'; -import * as path from 'path'; -import { isArray } from 'ts-util-is'; import Config from '../common/config'; import Connection from '../common/connection'; -import { IdempotencyObject } from '../common/types'; -import Utility from '../common/utility'; +import FileUtility from '../common/file-utility'; +import MSSQLGenerator from '../generators/mssql'; import { SqlColumn, SqlDataResult, @@ -17,11 +13,9 @@ import { SqlIndex, SqlObject, SqlPrimaryKey, - SqlSchema, SqlTable, - SqlTableValuedParameter + SqlType } from '../sql/interfaces'; -import * as script from '../sql/script'; import { columnRead, foreignKeyRead, indexRead, objectRead, primaryKeyRead, tableRead, typeRead } from '../sql/sys'; import { PullOptions } from './interfaces'; @@ -31,7 +25,7 @@ export default class Pull { * Invoke action. * * @param name Optional connection name to use. - * @param options. CLI options. + * @param options CLI options. */ public invoke(name: string, options: PullOptions): void { const start: [number, number] = process.hrtime(); @@ -44,25 +38,40 @@ export default class Pull { new sql.ConnectionPool(conn) .connect() .then(pool => { - return Promise.all([ + return Promise.all>([ pool.request().query(objectRead), pool.request().query(tableRead), pool.request().query(columnRead), pool.request().query(primaryKeyRead), pool.request().query(foreignKeyRead), pool.request().query(indexRead), - pool.request().query(typeRead), - ...config.data.map(table => { - return pool.request() - .query(`select * from ${table}`) - .then(result => ({ name: table, type: 'DATA', result })); + pool.request().query(typeRead) + ]) + .then(results => { + const tables: string[] = results[1].recordset + .map(item => `${item.schema}.${item.name}`); + + const matched: string[] = multimatch(tables, config.data); + + if (!matched.length) { + return results; + } + + return Promise.all( + matched.map(item => { + return pool.request() + .query(`select * from ${item}`) + .then(result => ({ name: item, result })); + }) + ) + .then(data => [...results, ...data]); }) - ]).then(results => { - pool.close(); - return results; - }); + .then(results => { + pool.close(); + return results; + }); }) - .then(results => this.scriptFiles(config, results)) + .then(results => this.writeFiles(config, results)) .then(() => { const time: [number, number] = process.hrtime(start); console.log(chalk.green(`Finished after ${time[0]}s!`)); @@ -71,13 +80,12 @@ export default class Pull { } /** - * Write all requested files to the file system based on `results`. + * Write all files to the file system based on `results`. * * @param config Current configuration to use. * @param results Array of data sets from SQL queries. */ - private scriptFiles(config: Config, results: any[]): void { - const existing: string[] = glob.sync(`${config.output.root}/**/*.sql`); + private writeFiles(config: Config, results: any[]): void { // note: array order MUST match query promise array const objects: SqlObject[] = results[0].recordset; @@ -86,202 +94,88 @@ export default class Pull { const primaryKeys: SqlPrimaryKey[] = results[3].recordset; const foreignKeys: SqlForeignKey[] = results[4].recordset; const indexes: SqlIndex[] = results[5].recordset; - const types: SqlTableValuedParameter[] = results[6].recordset; + const types: SqlType[] = results[6].recordset; const data: SqlDataResult[] = results.slice(7); - // get unique schema names - const schemas: SqlSchema[] = tables + const generator: MSSQLGenerator = new MSSQLGenerator(config); + const file: FileUtility = new FileUtility(config); + + // schemas + tables .map(item => item.schema) .filter((value, index, array) => array.indexOf(value) === index) - .map(value => ({ name: value, type: 'SCHEMA' })); - - // write files for schemas - schemas.forEach(item => { - const file: string = Utility.safeFile(`${item.name}.sql`); - - if (!this.include(config.files, file)) { - return; - } - - const content: string = script.schema(item); - const dir: string = this.createFile(config, item, file, content); - this.exclude(config, existing, dir); - }); - - // write files for stored procedures, functions, ect. - objects.forEach(item => { - const file: string = Utility.safeFile(`${item.schema}.${item.name}.sql`); - - if (!this.include(config.files, file)) { - return; - } - - const dir: string = this.createFile(config, item, file, item.text); - this.exclude(config, existing, dir); - }); - - // write files for tables + .map(value => ({ name: value })) + .forEach(item => { + const name: string = `${item.name}.sql`; + const content: string = generator.schema(item); + + file.write(config.output.schemas, name, content); + }); + + // stored procedures + objects + .filter(item => item.type.trim() === 'P') + .forEach(item => { + const name: string = `${item.schema}.${item.name}.sql`; + const content: string = generator.storedProcedure(item); + + file.write(config.output.procs, name, content); + }); + + // views + objects + .filter(item => item.type.trim() === 'V') + .forEach(item => { + const name: string = `${item.schema}.${item.name}.sql`; + const content: string = generator.view(item); + + file.write(config.output.views, name, content); + }); + + // functions + objects + .filter(item => ['TF', 'IF', 'FN'].indexOf(item.type.trim()) !== -1) + .forEach(item => { + const name: string = `${item.schema}.${item.name}.sql`; + const content: string = generator.function(item); + + file.write(config.output.functions, name, content); + }); + + // triggers + objects + .filter(item => item.type.trim() === 'TR') + .forEach(item => { + const name: string = `${item.schema}.${item.name}.sql`; + const content: string = generator.trigger(item); + + file.write(config.output.triggers, name, content); + }); + + // tables tables.forEach(item => { - const file: string = Utility.safeFile(`${item.schema}.${item.name}.sql`); - - if (!this.include(config.files, file)) { - return; - } + const name: string = `${item.schema}.${item.name}.sql`; + const content: string = generator.table(item, columns, primaryKeys, foreignKeys, indexes); - const content: string = script.table(item, columns, primaryKeys, foreignKeys, indexes); - const dir: string = this.createFile(config, item, file, content); - this.exclude(config, existing, dir); + file.write(config.output.tables, name, content); }); - // write files for types + // types types.forEach(item => { - const file: string = Utility.safeFile(`${item.schema}.${item.name}.sql`); - - if (!this.include(config.files, file)) { - return; - } + const name: string = `${item.schema}.${item.name}.sql`; + const content: string = generator.type(item, columns); - const content: string = script.type(item, columns); - const dir: string = this.createFile(config, item, file, content); - this.exclude(config, existing, dir); + file.write(config.output.types, name, content); }); - // write files for data + // data data.forEach(item => { - const file: string = Utility.safeFile(`${item.name}.sql`); + const name: string = `${item.name}.sql`; + const content: string = generator.data(item); - if (!this.include(config.data, item.name)) { - return; - } - - const content: string = script.data(item, config.idempotency.data); - const dir: string = this.createFile(config, item, file, content); - this.exclude(config, existing, dir); + file.write(config.output.data, name, content); }); - // all remaining files in `existing` need deleted - this.removeFiles(existing); - } - - /** - * Write SQL file script to the file system with correct options. - * - * @param config Current configuration to use. - * @param item Row from query. - * @param file Name of file to create. - * @param content Script file contents. - */ - private createFile(config: Config, item: any, file: string, content: string): string { - let dir: string; - let output: string | false; - let type: IdempotencyObject; - - switch (item.type.trim()) { - case 'SCHEMA': // not a real object type - output = config.output.schemas; - type = null; - break; - case 'DATA': // not a real object type - output = config.output.data; - type = null; - break; - case 'U': - output = config.output.tables; - type = config.idempotency.tables; - break; - case 'P': - output = config.output.procs; - type = config.idempotency.procs; - break; - case 'V': - output = config.output.views; - type = config.idempotency.views; - break; - case 'TF': - case 'IF': - case 'FN': - output = config.output.functions; - type = config.idempotency.functions; - break; - case 'TR': - output = config.output.triggers; - type = config.idempotency.triggers; - break; - case 'TT': - output = config.output.types; - type = config.idempotency.types; - break; - default: - output = 'unknown'; - } - - if (output) { - - // get full output path - dir = path.join(config.output.root, output, file); - - // idempotent prefix - content = script.idempotency(item, type) + content; - - // create file - console.log(`Creating ${chalk.cyan(dir)} ...`); - fs.outputFileSync(dir, content.trim()); - } - - return dir; - } - - /** - * Check if a file passes the glob pattern. - * - * @param files Glob pattern to check against. - * @param file File path to check. - */ - private include(files: string[], file: string | string[]): boolean { - if (!files || !files.length) { - return true; - } - - if (!isArray(file)) { - file = [file]; - } - - const results: string[] = multimatch(file, files); - return !!results.length; - } - - /** - * Remove `dir` from `existing` if it exists. - * - * @param config Current configuration to use. - * @param existing Collection of file paths to check against. - * @param dir File path to check. - */ - private exclude(config: Config, existing: string[], dir: string): void { - if (!dir) { - return; - } - - if (config.output.root.startsWith('./') && !dir.startsWith('./')) { - dir = `./${dir}`; - } - - const index: number = existing.indexOf(dir.replace(/\\/g, '/')); - - if (index !== -1) { - existing.splice(index, 1); - } - } - - /** - * Delete all paths in `files`. - * - * @param files Array of file paths to delete. - */ - private removeFiles(files: string[]): void { - files.forEach(file => { - console.log(`Removing ${chalk.cyan(file)} ...`); - fs.removeSync(file); - }); + file.removeRemaining(); } } diff --git a/src/commands/push.ts b/src/commands/push.ts index b8173d9..e1b1839 100644 --- a/src/commands/push.ts +++ b/src/commands/push.ts @@ -1,11 +1,11 @@ import chalk from 'chalk'; import * as fs from 'fs-extra'; +import * as glob from 'glob'; import * as sql from 'mssql'; import { EOL } from 'os'; import Config from '../common/config'; import Connection from '../common/connection'; -import Utility from '../common/utility'; export default class Push { @@ -21,7 +21,7 @@ export default class Push { console.log(`Pushing to ${chalk.magenta(conn.database)} on ${chalk.magenta(conn.server)} ...`); - const files: string[] = Utility.getFilesOrdered(config); + const files: string[] = this.getFilesOrdered(config); let promise: Promise = new sql.ConnectionPool(conn).connect(); files.forEach(file => { @@ -44,4 +44,32 @@ export default class Push { }) .catch(err => console.error(err)); } + + /** + * Get all SQL files in correct execution order. + * + * @param config Config object used to search for connection. + */ + public getFilesOrdered(config: Config): string[] { + const output: string[] = []; + const directories: string[] = [ + config.output.schemas, + config.output.tables, + config.output.types, + config.output.views, + config.output.functions, + config.output.procs, + config.output.triggers, + config.output.data + ] as string[]; + + directories.forEach(dir => { + if (dir) { + const files: string[] = glob.sync(`${config.output.root}/${dir}/**/*.sql`); + output.push(...files); + } + }); + + return output; + } } diff --git a/src/common/file-utility.ts b/src/common/file-utility.ts new file mode 100644 index 0000000..37449f7 --- /dev/null +++ b/src/common/file-utility.ts @@ -0,0 +1,112 @@ +import chalk from 'chalk'; +import * as filenamify from 'filenamify'; +import * as fs from 'fs-extra'; +import * as glob from 'glob'; +import * as multimatch from 'multimatch'; +import * as path from 'path'; +import { isArray } from 'ts-util-is'; + +import Config from './config'; + +/** + * File system interaction and tracking. + */ +export default class FileUtility { + constructor(config: Config) { + this.config = config; + this.loadExisting(); + } + + /** + * Current configuration. + */ + private config: Config; + + /** + * Existing files. + */ + private existing: string[]; + + /** + * Write file to the file system. + * + * @param file Directory to write, relative to root. + * @param file File name to write to. + * @param content File content to write. + */ + public write(dir: string | false, file: string, content: string): void { + if (dir === false) { + return; + } + + // remove unsafe characters + file = filenamify(file); + + if (!this.shouldWrite(file)) { + return; + } + + file = path.join(this.config.output.root, dir, file); + + console.log(`Creating ${chalk.cyan(file)} ...`); + fs.outputFileSync(file, content.trim()); + + this.markAsWritten(file); + } + + /** + * Delete all paths remaining in `existing`. + */ + public removeRemaining(): void { + this.existing.forEach(file => { + console.log(`Removing ${chalk.cyan(file)} ...`); + fs.removeSync(file); + }); + } + + /** + * Check if a file passes the glob pattern. + * + * @param file File path to check. + */ + private shouldWrite(file: string | string[]): boolean { + if (!this.config.files || !this.config.files.length) { + return true; + } + + if (!isArray(file)) { + file = [file]; + } + + const results: string[] = multimatch(file, this.config.files); + return !!results.length; + } + + /** + * Remove `file` from `existing`, if it exists. + * + * @param file File path to check. + */ + private markAsWritten(file: string): void { + if (!file) { + return; + } + + if (this.config.output.root.startsWith('./') && !file.startsWith('./')) { + file = `./${file}`; + } + + const index: number = this.existing.indexOf(file.replace(/\\/g, '/')); + + if (index !== -1) { + this.existing.splice(index, 1); + } + } + + /** + * Load existing files array for comparison. + */ + private loadExisting(): void { + this.existing = glob.sync(`${this.config.output.root}/**/*.sql`); + } +} diff --git a/src/common/utility.ts b/src/common/utility.ts deleted file mode 100644 index 20b5a5f..0000000 --- a/src/common/utility.ts +++ /dev/null @@ -1,47 +0,0 @@ -import * as filenamify from 'filenamify'; -import * as glob from 'glob'; - -import Config from '../common/config'; - -/** - * Helper methods. - */ -export default class Utility { - - /** - * Remove unsafe characters from file name. - * - * @param file Path and file name. - */ - public static safeFile(file: string): string { - return filenamify(file); - } - - /** - * Get all SQL files in correct execution order. - * - * @param config Config object used to search for connection. - */ - public static getFilesOrdered(config: Config): string[] { - const output: string[] = []; - const directories: string[] = [ - config.output.schemas, - config.output.tables, - config.output.types, - config.output.views, - config.output.functions, - config.output.procs, - config.output.triggers, - config.output.data - ] as string[]; - - directories.forEach(dir => { - if (dir) { - const files: string[] = glob.sync(`${config.output.root}/${dir}/**/*.sql`); - output.push(...files); - } - }); - - return output; - } -} diff --git a/src/generators/mssql.ts b/src/generators/mssql.ts new file mode 100644 index 0000000..24da036 --- /dev/null +++ b/src/generators/mssql.ts @@ -0,0 +1,520 @@ + +/* tslint:disable:max-line-length */ +import { EOL } from 'os'; +import { isBoolean, isDate, isString } from 'ts-util-is'; + +import Config from '../common/config'; +import { + SqlColumn, + SqlDataResult, + SqlForeignKey, + SqlIndex, + SqlObject, + SqlPrimaryKey, + SqlSchema, + SqlTable, + SqlType +} from '../sql/interfaces'; + +/** + * MSSQL generator. + */ +export default class MSSQLGenerator { + constructor(config: Config) { + this.config = config; + } + + /** + * Current configuration. + */ + private config: Config; + + /** + * Get data file content. + * + * @param item Row from query. + */ + public data(item: SqlDataResult): string { + let output: string = ''; + + switch (this.config.idempotency.data) { + case 'delete': + output += `delete from ${item.name}` + EOL; + output += EOL; + break; + case 'delete-and-reseed': + output += `delete from ${item.name}`; + output += EOL; + output += `dbcc checkident ('${item.name}', reseed, 0)`; + output += EOL; + break; + case 'truncate': + output += `truncate table ${item.name}`; + output += EOL; + break; + } + + output += EOL; + output += `set identity_insert ${item.name} on`; + output += EOL; + output += EOL; + + item.result.recordset.forEach(row => { + const keys: string[] = Object.keys(row); + const columns: string = keys.join(', '); + const values: string = keys.map(key => this.safeValue(row[key])).join(', '); + + output += `insert into ${item.name} (${columns}) values (${values})`; + output += EOL; + }); + + output += EOL; + output += `set identity_insert ${item.name} off`; + output += EOL; + output += EOL; + + return output; + } + + /** + * Get function file content. + * + * @param item Row from query. + */ + public function(item: SqlObject): string { + const objectId: string = `[${item.schema}].[${item.name}]`; + const type: string = item.type.trim(); + let output: string = ''; + + switch (this.config.idempotency.functions) { + case 'if-exists-drop': + output += `if exists (select * from sys.objects where object_id = object_id('${objectId}') and type = '${type}')`; + output += EOL; + output += `drop function ${objectId}`; + output += EOL; + output += 'go'; + output += EOL; + break; + case 'if-not-exists': + output += `if not exists (select * from sys.objects where object_id = object_id('${objectId}') and type = '${type}')`; + output += EOL; + break; + } + + output += EOL; + output += item.text; + + return output; + } + + /** + * Get stored procedure file content. + * + * @param item Row from query. + */ + public storedProcedure(item: SqlObject): string { + const objectId: string = `[${item.schema}].[${item.name}]`; + const type: string = item.type.trim(); + let output: string = ''; + + switch (this.config.idempotency.procs) { + case 'if-exists-drop': + output += `if exists (select * from sys.objects where object_id = object_id('${objectId}') and type = '${type}')`; + output += EOL; + output += `drop procedure ${objectId}`; + output += EOL; + output += 'go'; + output += EOL; + break; + case 'if-not-exists': + output += `if not exists (select * from sys.objects where object_id = object_id('${objectId}') and type = '${type}')`; + output += EOL; + break; + } + + output += EOL; + output += item.text; + + return output; + } + + /** + * Get schema file content. + * + * @param item Row from query. + */ + public schema(item: SqlSchema): string { + let output: string = ''; + + output += `if not exists (select * from sys.schemas where name = '${item.name}')`; + output += EOL; + output += `exec('create schema ${item.name}')`; + + return output; + } + + /** + * Get table file content. + * + * @param item Row from query. + * @param columns Columns from query. + * @param primaryKeys Primary key from query. + * @param foreignKeys Foreign keys from query. + * @param indexes Indexes from query. + */ + public table( + item: SqlTable, + columns: SqlColumn[], + primaryKeys: SqlPrimaryKey[], + foreignKeys: SqlForeignKey[], + indexes: SqlIndex[] + ): string { + const objectId: string = `[${item.schema}].[${item.name}]`; + const type: string = item.type.trim(); + let output: string = ''; + + switch (this.config.idempotency.tables) { + case 'if-exists-drop': + output += `if exists (select * from sys.objects where object_id = object_id('${objectId}') and type = '${type}')`; + output += EOL; + output += `drop table ${objectId}`; + output += EOL; + output += 'go'; + output += EOL; + break; + case 'if-not-exists': + output += `if not exists (select * from sys.objects where object_id = object_id('${objectId}') and type = '${type}')`; + output += EOL; + break; + } + + output += `create table ${objectId}`; + output += EOL; + output += '('; + output += EOL; + + columns + .filter(x => x.object_id === item.object_id) + .forEach(col => { + output += ' ' + this.column(col) + ','; + output += EOL; + }); + + primaryKeys + .filter(x => x.object_id === item.object_id) + .forEach(pk => { + output += ' ' + this.primaryKey(pk); + output += EOL; + }); + + output += ')'; + output += EOL; + output += EOL; + + foreignKeys + .filter(x => x.object_id === item.object_id) + .forEach(fk => { + output += this.foreignKey(fk); + output += EOL; + }); + + output += EOL; + output += EOL; + + indexes + .filter(x => x.object_id === item.object_id) + .forEach(index => { + output += this.index(index); + output += EOL; + }); + + return output; + } + + /** + * Get trigger file content. + * + * @param item Row from query. + */ + public trigger(item: SqlObject): string { + const objectId: string = `[${item.schema}].[${item.name}]`; + const type: string = item.type.trim(); + let output: string = ''; + + switch (this.config.idempotency.triggers) { + case 'if-exists-drop': + output += `if exists (select * from sys.objects where object_id = object_id('${objectId}') and type = '${type}')`; + output += EOL; + output += `drop trigger ${objectId}`; + output += EOL; + output += 'go'; + output += EOL; + break; + case 'if-not-exists': + output += `if not exists (select * from sys.objects where object_id = object_id('${objectId}') and type = '${type}')`; + output += EOL; + break; + } + + output += EOL; + output += item.text; + + return output; + } + + /** + * Get type file content. + * + * @param item Row from query. + * @param columns Columns from query. + */ + public type(item: SqlType, columns: SqlColumn[]): string { + const objectId: string = `[${item.schema}].[${item.name}]`; + const type: string = item.type.trim(); + let output: string = ''; + + switch (this.config.idempotency.types) { + case 'if-exists-drop': + output += 'if exists ('; + output += EOL; + output += ' select * from sys.table_types as t'; + output += EOL; + output += ' join sys.schemas s on t.schema_id = s.schema_id'; + output += EOL; + output += ` where t.name = '${item.name}' and s.name = '${item.schema}'`; + output += EOL; + output += ')'; + output += EOL; + output += `drop type ${objectId}`; + output += EOL; + output += 'go'; + output += EOL; + break; + case 'if-not-exists': + output += `if exists (select * from sys.objects where object_id = object_id('${objectId}') and type = '${type}')`; + output += EOL; + output += `drop type ${objectId}`; + output += EOL; + output += 'go'; + output += EOL; + break; + } + + output += `create type ${objectId} as table`; + output += EOL; + output += '('; + output += EOL; + + columns + .filter(x => x.object_id === item.object_id) + .forEach((col, idx, array) => { + output += ' ' + this.column(col); + + if (idx !== array.length - 1) { + // not the last column + output += ','; + } + + output += EOL; + }); + + output += ')'; + output += EOL; + output += EOL; + + return output; + } + + /** + * Get view file content. + * + * @param item Row from query. + */ + public view(item: SqlObject): string { + const objectId: string = `[${item.schema}].[${item.name}]`; + const type: string = item.type.trim(); + let output: string = ''; + + switch (this.config.idempotency.views) { + case 'if-exists-drop': + output += `if exists (select * from sys.objects where object_id = object_id('${objectId}') and type = '${type}')`; + output += EOL; + output += `drop view ${objectId}`; + output += EOL; + output += 'go'; + output += EOL; + break; + case 'if-not-exists': + output += `if not exists (select * from sys.objects where object_id = object_id('${objectId}') and type = '${type}')`; + output += EOL; + break; + } + + output += EOL; + output += item.text; + + return output; + } + + /** + * Safely transform SQL value for scripting. + * + * @param value SQL data value. + */ + private safeValue(value: any): any { + if (isString(value)) { + value = value.replace("'", "''"); + return `'${value}'`; + } + + if (isDate(value)) { + value = value.toISOString(); + return `'${value}'`; + } + + if (isBoolean(value)) { + return value ? 1 : 0; + } + + return value; + } + + /** + * Get script for table's column. + * + * @param item Row from query. + */ + private column(item: SqlColumn): string { + let output: string = `[${item.name}]`; + let size: string | number; + + if (item.is_computed) { + output += ` as ${item.formula}`; + return output; + } + + output += ` ${item.datatype}`; + + switch (item.datatype) { + case 'varchar': + case 'char': + case 'varbinary': + case 'binary': + case 'text': + size = (item.max_length === -1 ? 'max' : item.max_length); + output += `(${size})`; + break; + case 'nvarchar': + case 'nchar': + case 'ntext': + size = (item.max_length === -1 ? 'max' : item.max_length / 2); + output += `(${size})`; + break; + case 'datetime2': + case 'time2': + case 'datetimeoffset': + output += `(${item.scale})`; + break; + case 'decimal': + output += `(${item.precision}, ${item.scale})`; + break; + } + + if (item.collation_name) { + output += ` collate ${item.collation_name}`; + } + + output += item.is_nullable ? ' null' : ' not null'; + + if (item.definition) { + output += ` default${item.definition}`; + } + + if (item.is_identity) { + output += ` identity(${item.seed_value || 0}, ${item.increment_value || 1})`; + } + + return output; + } + + /** + * Get script for table's primary key. + * + * @param item Row from query. + */ + private primaryKey(item: SqlPrimaryKey): string { + const direction: string = item.is_descending_key ? 'desc' : 'asc'; + + return `constraint [${item.name}] primary key ([${item.column}] ${direction})`; + } + + /** + * Get script for table's foreign key. + * + * @param item Row from foreignKeys query. + */ + private foreignKey(item: SqlForeignKey): string { + const objectId: string = `[${item.schema}].[${item.table}]`; + const parentObjectId: string = `[${item.parent_schema}].[${item.parent_table}]`; + let output: string = ''; + + output += `alter table ${objectId} with ${item.is_not_trusted ? 'nocheck' : 'check'}`; + output += ` add constraint [${item.name}] foreign key([${item.column}])`; + output += ` references ${parentObjectId} ([${item.reference}])`; + + switch (item.delete_referential_action) { + case 1: + output += ' on delete cascade'; + break; + case 2: + output += ' on delete set null'; + break; + case 3: + output += ' on delete set default'; + break; + } + + switch (item.update_referential_action) { + case 1: + output += ' on update cascade'; + break; + case 2: + output += ' on update set null'; + break; + case 3: + output += ' on update set default'; + break; + } + + output += ` alter table ${objectId} check constraint [${item.name}]`; + + return output; + } + + /** + * Get script for table's indexes. + * + * @param item Row from query. + */ + private index(item: SqlIndex): string { + const objectId: string = `[${item.schema}].[${item.table}]`; + let output: string = ''; + + output += `if not exists (select * from sys.indexes where object_id = object_id('${objectId}') and name = '${item.name}')`; + output += EOL; + output += 'create'; + + if (item.is_unique) { + output += ' unique'; + } + + output += ` nonclustered index [${item.name}] on ${objectId}`; + output += `([${item.column}] ${item.is_descending_key ? 'desc' : 'asc'})`; + + // todo (jbl): includes + + output += EOL; + + return output; + } +} diff --git a/src/sql/interfaces.ts b/src/sql/interfaces.ts index 98b9b38..8d3586c 100644 --- a/src/sql/interfaces.ts +++ b/src/sql/interfaces.ts @@ -15,7 +15,6 @@ export interface AbstractSqlObject { */ export interface SqlSchema { name: string; - type: string; } /** @@ -23,7 +22,6 @@ export interface SqlSchema { */ export interface SqlDataResult { name: string; - type: string; result: sql.IResult; } @@ -34,10 +32,10 @@ export interface SqlDataResult { export interface SqlTable extends AbstractSqlObject { } /** - * SQL Table valued parameter. + * SQL type. */ // tslint:disable-next-line:no-empty-interface -export interface SqlTableValuedParameter extends AbstractSqlObject { } +export interface SqlType extends AbstractSqlObject { } /** * SQL column object. diff --git a/src/sql/script.ts b/src/sql/script.ts deleted file mode 100644 index 7966c85..0000000 --- a/src/sql/script.ts +++ /dev/null @@ -1,412 +0,0 @@ -import { EOL } from 'os'; -import { isBoolean, isDate, isString } from 'ts-util-is'; - -import { IdempotencyData, IdempotencyObject } from '../common/types'; -import { - AbstractSqlObject, - SqlColumn, - SqlDataResult, - SqlForeignKey, - SqlIndex, - SqlPrimaryKey, - SqlSchema, - SqlTable -} from './interfaces'; - -/** - * Get idempotency script prefix. - * - * @param item Row from query. - * @param type Idempotency prefix type. - */ -export function idempotency(item: AbstractSqlObject, type: IdempotencyObject): string { - let obj: string; - const objectId: string = `${item.schema}].[${item.name}`; - - item.type = item.type.trim(); - - // get proper object type for `drop` statement - switch (item.type) { - case 'U': - obj = 'table'; - break; - case 'TT': - obj = 'table'; - break; - case 'P': - obj = 'procedure'; - break; - case 'V': - obj = 'view'; - break; - case 'TF': - case 'IF': - case 'FN': - obj = 'function'; - break; - case 'TR': - obj = 'trigger'; - break; - } - - if (type === 'if-exists-drop') { - // if exists drop - if (item.type === 'TT') { - return [ - 'if exists (', - ' select * from sys.table_types as t', - ' join sys.schemas s on t.schema_id = s.schema_id', - ` where t.name = '${item.name}' and s.name = '${item.schema}'`, - ')', - `drop type [${objectId}]`, - 'go', - EOL - ].join(EOL); - } else { - return [ - `if exists (select * from sys.objects where object_id = object_id('[${objectId}]') and type = '${item.type}')`, - `drop ${obj} [${objectId}]`, - 'go', - EOL - ].join(EOL); - } - } else if (type === 'if-not-exists') { - // if not exists - if (item.type === 'TT') { - return [ - 'if not exists (', - ' select * from sys.table_types as t', - ' join sys.schemas s on t.schema_id = s.schema_id', - ` where t.name = '${item.name}' and s.name = '${item.schema}'`, - ')', - '' - ].join(EOL); - } else { - return [ - // tslint:disable-next-line:max-line-length - `if not exists (select * from sys.objects where object_id = object_id('[${objectId}]') and type = '${item.type}')`, - '' - ].join(EOL); - } - } - - // none - return ''; -} - -/** - * Get script for schema creation. - * - * @param item Object containing schema info. - */ -export function schema(item: SqlSchema): string { - let output: string = ''; - - // idempotency - output += `if not exists (select * from sys.schemas where name = '${item.name}')`; - output += EOL; - - output += `exec('create schema ${item.name}')`; - return output; -} - -/** - * Get script for table's column. - * - * @param item Row from `sys.columns` query. - * @param columns Array of records from `sys.columns` query. - * @param primaryKeys Array of records from `sys.primaryKeys` query. - * @param foreignKeys Array of records from `sys.foreignKeys` query. - * @param indexes Array of records from `sys.indexes` query. - */ -export function table( - item: SqlTable, - columns: SqlColumn[], - primaryKeys: SqlPrimaryKey[], - foreignKeys: SqlForeignKey[], - indexes: SqlIndex[] -): string { - let output: string = `create table [${item.schema}].[${item.name}]`; - output += EOL; - output += '('; - output += EOL; - - // columns - columns - .filter(x => x.object_id === item.object_id) - .forEach(col => { - output += ' ' + column(col) + ','; - output += EOL; - }); - - // primary keys - primaryKeys - .filter(x => x.object_id === item.object_id) - .forEach(pk => { - output += ' ' + primaryKey(pk); - output += EOL; - }); - - output += ')'; - - output += EOL; - output += EOL; - - // foreign keys - foreignKeys - .filter(x => x.object_id === item.object_id) - .forEach(fk => { - output += foreignKey(fk); - output += EOL; - }); - - output += EOL; - output += EOL; - - // indexes - indexes - .filter(x => x.object_id === item.object_id) - .forEach(ix => { - output += index(ix); - output += EOL; - }); - - return output; -} - -/** - * Get script for user defined types. - * - * @param item Row from `sys.columns` query. - * @param columns Array of records from `sys.columns` query. - */ -export function type( - item: SqlTable, - columns: SqlColumn[] -): string { - let output: string = `create type [${item.schema}].[${item.name}] as table`; - output += EOL; - output += '('; - output += EOL; - - // columns - columns - .filter(x => x.object_id === item.object_id) - .forEach((col, idx, array) => { - output += ' ' + column(col); - - // if it is not the last column - if (idx !== array.length - 1) { - output += ','; - } - - output += EOL; - }); - - output += ')'; - output += EOL; - output += EOL; - - return output; -} - -/** - * Get script for for table data. - * - * @param item Results from data query. - * @param idempotency Idempotency option to use. - */ -export function data(item: SqlDataResult, idempotency: IdempotencyData): string { - let output: string = ''; - - if (idempotency === 'truncate') { - output += `truncate table ${item.name}`; - output += EOL; - } else if (idempotency === 'delete') { - output += `delete from ${item.name}`; - output += EOL; - } else { - output += `delete from ${item.name}`; - output += EOL; - output += `dbcc checkident ('${item.name}', reseed, 0)`; - output += EOL; - } - - output += EOL; - output += `set identity_insert ${item.name} on`; - output += EOL; - output += EOL; - - item.result.recordset.forEach(row => { - const keys: string[] = Object.keys(row); - const columns: string = keys.join(', '); - const values: string = keys.map(key => safeValue(row[key])).join(', '); - - output += `insert into ${item.name} (${columns}) values (${values})`; - output += EOL; - }); - - output += EOL; - output += `set identity_insert ${item.name} off`; - - output += EOL; - output += EOL; - return output; -} - -/** - * Safely transform SQL value for scripting. - * - * @param value SQL data value. - */ -function safeValue(value: any): any { - if (isString(value)) { - value = value.replace("'", "''"); - return `'${value}'`; - } - - if (isDate(value)) { - value = value.toISOString(); - return `'${value}'`; - } - - if (isBoolean(value)) { - return value ? 1 : 0; - } - - return value; -} - -/** - * Get script for table's column. - * - * @param item Row from `sys.columns` query. - */ -function column(item: SqlColumn): string { - let output: string = `[${item.name}]`; - - if (item.is_computed) { - output += ` as ${item.formula}`; - return output; - } - - output += ` ${item.datatype}`; - - switch (item.datatype) { - case 'varchar': - case 'char': - case 'varbinary': - case 'binary': - case 'text': - output += '(' + (item.max_length === -1 ? 'max' : item.max_length) + ')'; - break; - case 'nvarchar': - case 'nchar': - case 'ntext': - output += '(' + (item.max_length === -1 ? 'max' : item.max_length / 2) + ')'; - break; - case 'datetime2': - case 'time2': - case 'datetimeoffset': - output += '(' + item.scale + ')'; - break; - case 'decimal': - output += '(' + item.precision + ', ' + item.scale + ')'; - break; - } - - if (item.collation_name) { - output += ` collate ${item.collation_name}`; - } - - output += item.is_nullable ? ' null' : ' not null'; - - if (item.definition) { - output += ` default${item.definition}`; - } - - if (item.is_identity) { - output += ` identity(${item.seed_value || 0}, ${item.increment_value || 1})`; - } - - return output; -} - -/** - * Get script for table's primary key. - * - * @param item Row from `sys.primaryKeys` query. - */ -function primaryKey(item: SqlPrimaryKey): string { - return `constraint [${item.name}] primary key ([${item.column}] ${item.is_descending_key ? 'desc' : 'asc'})`; -} - -/** - * Get script for table's foreign key. - * - * @param item Row from `sys.foreignKeys` query. - */ -function foreignKey(item: SqlForeignKey): string { - const objectId: string = `${item.schema}].[${item.table}`; - const parentObjectId: string = `${item.parent_schema}].[${item.parent_table}`; - - let output: string = `alter table [${objectId}] with ${item.is_not_trusted ? 'nocheck' : 'check'}`; - output += ` add constraint [${item.name}] foreign key([${item.column}])`; - output += ` references [${parentObjectId}] ([${item.reference}])`; - - switch (item.delete_referential_action) { - case 1: - output += ' on delete cascade'; - break; - case 2: - output += ' on delete set null'; - break; - case 3: - output += ' on delete set default'; - break; - } - - switch (item.update_referential_action) { - case 1: - output += ' on update cascade'; - break; - case 2: - output += ' on update set null'; - break; - case 3: - output += ' on update set default'; - break; - } - - output += ` alter table [${objectId}] check constraint [${item.name}]`; - return output; -} - -/** - * Get script for table's indexes. - * - * @param item Row from `sys.indexes` query. - */ -function index(item: SqlIndex): string { - const objectId: string = `${item.schema}].[${item.table}`; - let output: string = ''; - - // idempotency - // tslint:disable-next-line:max-line-length - output += `if not exists (select * from sys.indexes where object_id = object_id('[${objectId}]') and name = '${item.name}')`; - output += EOL; - - output += 'create'; - - if (item.is_unique) { - output += ' unique'; - } - - output += ` nonclustered index [${item.name}] on [${objectId}]`; - output += `([${item.column}] ${item.is_descending_key ? 'desc' : 'asc'})`; - - // todo (jbl): includes - - output += EOL; - return output; -}