diff --git a/README.md b/README.md index 5658e3a..6318449 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,19 @@ [![NPM Version](https://badge.fury.io/js/sql-source-control.svg)](https://badge.fury.io/js/sql-source-control) + [![Build Status](https://travis-ci.org/justinlettau/sql-source-control.svg?branch=master)](https://travis-ci.org/justinlettau/sql-source-control) + [![Build status](https://ci.appveyor.com/api/projects/status/a92idr95kkly8lgt/branch/master?svg=true)](https://ci.appveyor.com/project/justinlettau/sql-source-control/branch/master) + [![Dependency Status](https://david-dm.org/justinlettau/sql-source-control.svg)](https://david-dm.org/justinlettau/sql-source-control) + [![Dev Dependency Status](https://david-dm.org/justinlettau/sql-source-control/dev-status.svg)](https://david-dm.org/justinlettau/sql-source-control?type=dev) # SQL Source Control + CLI for scripting SQL objects into a flat file structure for use with source control systems. # Table of Contents + - [Features](#features) - [Installation](#installation) - [Usage](#usage) @@ -22,16 +28,19 @@ CLI for scripting SQL objects into a flat file structure for use with source con - [Development](#development) # Features + - Works with **any source control system** like Git, SVN, Mercurial, etc. - Supports all recent version of **Microsoft SQL Server**. - **Free and open source**! # Installation + ```bash npm install -g sql-source-control ``` # Usage + Commands are directory specific, so run all commands in the directory you want the scripts created in. ```bash @@ -42,6 +51,7 @@ ssc --help If TCP/IP is not enabled, you may receive a "failed to connect" error on commands. ### `ssc init` + This will ask you a bunch of questions, and then write a config file for you. If the current directory contains a `Web.config` file with the `connectionStrings` property, the @@ -51,21 +61,23 @@ specified with the `--webconfig` flag. Options: | Option | Alias | Type | Description | Default | -|---------------|-------|-----------|-------------------------------------------------|---------| +| ------------- | ----- | --------- | ----------------------------------------------- | ------- | | `--force` | `-f` | `boolean` | Overwrite an existing config file, if present. | n/a | | `--skip` | `-s` | `boolean` | Use defaults and not prompt you for any options | n/a | | `--webconfig` | `-w` | `string` | Relative path to a `Web.config` file. | n/a | ### `ssc list` + List all available connections specified in the configuration file. Options: | Option | Alias | Type | Description | Default | -|------------|-------|----------|-------------------------------|------------| +| ---------- | ----- | -------- | ----------------------------- | ---------- | | `--config` | `-c` | `string` | Relative path to config file. | `ssc.json` | ### `ssc pull [conn]` + Generate SQL files for all tables, stored procedures, functions, etc. All scripts will be put in the `output.root` directory and SQL scripts will be organized into subdirectories (based on config file). @@ -80,13 +92,13 @@ only include static data tables, like lookup tables, in the `data` configuration Arguments: | Argument | Description | Default | -|----------|-----------------------------------------|-----------------------------------------| +| -------- | --------------------------------------- | --------------------------------------- | | `conn` | Optional name of the connection to use. | First available connection from config. | Options: | Option | Alias | Type | Description | Default | -|------------|-------|----------|-------------------------------|------------| +| ---------- | ----- | -------- | ----------------------------- | ---------- | | `--config` | `-c` | `string` | Relative path to config file. | `ssc.json` | Example output (see [here](https://github.com/justinlettau/sql-source-control-example) for full example): @@ -100,6 +112,9 @@ Example output (see [here](https://github.com/justinlettau/sql-source-control-ex dbo.complex-math.sql dbo.awesome-table-function.sql ... + ./jobs + amazing-things.sql + ... ./schemas dbo.sql ... @@ -118,29 +133,31 @@ Example output (see [here](https://github.com/justinlettau/sql-source-control-ex ``` ### `ssc push [conn]` + Execute all local scripts against the requested database. Arguments: | Argument | Description | Default | -|----------|-----------------------------------------|-----------------------------------------| +| -------- | --------------------------------------- | --------------------------------------- | | `conn` | Optional name of the connection to use. | First available connection from config. | Options: | Option | Alias | Type | Description | Default | -|------------|-------|-----------|-------------------------------|------------| +| ---------- | ----- | --------- | ----------------------------- | ---------- | | `--config` | `-c` | `string` | Relative path to config file. | `ssc.json` | | `--skip` | `-s` | `boolean` | Skip user warning prompt. | `false` | # Configuration + Configuration options are stored in a `ssc.json` file. The following properties are supported: **connections** (`object[]`, `string`): Relative path to a `Web.config` file with `connectionStrings`, a `ssc-connections.json` file with an array of connections, or an array of connections with the following properties: | Property | Type | Description | Default | -|------------|----------|------------------|---------| +| ---------- | -------- | ---------------- | ------- | | `name` | `string` | Connection name. | n/a | | `server` | `string` | Server name. | n/a | | `database` | `string` | Database name. | n/a | @@ -157,10 +174,11 @@ includes none. properties are supported: | Property | Type | Description | Default | -|-------------|----------|--------------------------------------------------------|-----------------------| +| ----------- | -------- | ------------------------------------------------------ | --------------------- | | `root` | `string` | Directory for scripted files, relative to config file. | `./_sql-database` | | `data` | `string` | Subdirectory for data files. | `./data` | | `functions` | `string` | Subdirectory for function files. | `./functions` | +| `jobs` | `string` | Subdirectory for jobs files. | `./jobs` | | `procs` | `string` | Subdirectory for stored procedure files. | `./stored-procedures` | | `schemas` | `string` | Subdirectory for schema files. | `./schemas` | | `tables` | `string` | Subdirectory for table files. | `./tables` | @@ -172,22 +190,25 @@ properties are supported: following properties are supported. | Property | Type | Description | Default | -|-------------|--------------|-----------------------------------------------------|------------------| +| ----------- | ------------ | --------------------------------------------------- | ---------------- | | `data` | `string` (2) | Idempotency for data files. | `truncate` | | `functions` | `string` (1) | Idempotency for function files. | `if-exists-drop` | +| `jobs` | `string` (1) | Idempotency for jobs files. | `if-exists-drop` | | `procs` | `string` (1) | Idempotency for stored procedure files. | `if-exists-drop` | | `tables` | `string` (1) | Idempotency for table files. | `if-not-exists` | -| `triggers` | `string` (1) | Idempotency for triggers files. | `if-exists-drop` | +| `triggers` | `string` (1) | Idempotency for trigger files. | `if-exists-drop` | | `types` | `string` (1) | Idempotency for user defined table parameter files. | `if-not-exists` | -| `views` | `string` (1) | Idempotency for views files. | `if-exists-drop` | +| `views` | `string` (1) | Idempotency for view files. | `if-exists-drop` | 1. `if-exists-drop`, `if-not-exists`, or `false`. -2. `delete-and-ressed`, `delete`, `truncate`, or `false`. +2. `delete-and-reseed`, `delete`, `truncate`, or `false`. # Examples ### Connections + Basic connections. + ```json { "connections": [ @@ -204,6 +225,7 @@ Basic connections. ``` Connections stored in `Web.config` file. + ```json { "connections": "./Web.config" @@ -211,7 +233,8 @@ Connections stored in `Web.config` file. ``` Connections stored in separate JSON file. Storing connections in a separate JSON can be used in conjunction with a -`.gitignore` entry to prevent user connections or sensitive data from being commited. +`.gitignore` entry to prevent user connections or sensitive data from being committed. + ```json { "connections": "./ssc-connections.json" @@ -219,7 +242,9 @@ Connections stored in separate JSON file. Storing connections in a separate JSON ``` ### Files + Only include certain files. + ```js { // ... @@ -228,6 +253,7 @@ Only include certain files. ``` Exclude certain files. + ```js { // ... @@ -236,7 +262,9 @@ Exclude certain files. ``` ### Data + Only include certain tales. + ```js { // ... @@ -245,6 +273,7 @@ Only include certain tales. ``` Exclude certain tables. + ```js { // ... @@ -253,7 +282,9 @@ Exclude certain tables. ``` ### Output + Override default options. + ```js { // ... @@ -266,7 +297,9 @@ Override default options. ``` ### Idempotency + Override default options. + ```js { // ... @@ -278,7 +311,9 @@ Override default options. ``` ### Defaults + Default configuration values. + ```json { "connections": [], @@ -288,6 +323,7 @@ Default configuration values. "root": "./_sql-database", "data": "./data", "functions": "./functions", + "jobs": "./jobs", "procs": "./stored-procedures", "schemas": "./schemas", "tables": "./tables", @@ -298,6 +334,7 @@ Default configuration values. "idempotency": { "data": "truncate", "functions": "if-exists-drop", + "jobs": "if-exists-drop", "procs": "if-exists-drop", "tables": "if-not-exists", "triggers": "if-exists-drop", @@ -308,6 +345,7 @@ Default configuration values. ``` # Development + Clone the repo and run the following commands in the `sql-source-control` directory: ```bash diff --git a/src/commands/pull.ts b/src/commands/pull.ts index b50842b..a1ef7c5 100644 --- a/src/commands/pull.ts +++ b/src/commands/pull.ts @@ -4,7 +4,6 @@ import * as multimatch from 'multimatch'; import * as ora from 'ora'; import Config from '../common/config'; -import Connection from '../common/connection'; import FileUtility from '../common/file-utility'; import MSSQLGenerator from '../generators/mssql'; import { @@ -12,6 +11,9 @@ import { SqlDataResult, SqlForeignKey, SqlIndex, + SqlJob, + SqlJobSchedule, + SqlJobStep, SqlObject, SqlPrimaryKey, SqlTable, @@ -21,6 +23,9 @@ import { columnsRead, foreignKeysRead, indexesRead, + jobSchedulesRead, + jobsRead, + jobStepsRead, objectsRead, primaryKeysRead, tablesRead, @@ -39,7 +44,7 @@ export default class Pull { /** * Invoke action. */ - invoke(): void { + invoke() { const config = new Config(this.options.config); const conn = config.getConnection(this.name); @@ -56,7 +61,10 @@ export default class Pull { pool.request().query(primaryKeysRead), pool.request().query(foreignKeysRead), pool.request().query(indexesRead), - pool.request().query(typesRead) + pool.request().query(typesRead), + pool.request().query(jobsRead(conn.database)), + pool.request().query(jobStepsRead(conn.database)), + pool.request().query(jobSchedulesRead(conn.database)) ]) .then(results => { const tables: sql.IRecordSet = results[1].recordset; @@ -107,7 +115,10 @@ export default class Pull { const foreignKeys: SqlForeignKey[] = results[4].recordset; const indexes: SqlIndex[] = results[5].recordset; const types: SqlType[] = results[6].recordset; - const data: SqlDataResult[] = results.slice(7); + const jobs: SqlJob[] = results[7].recordset; + const jobSteps: SqlJobStep[] = results[8].recordset; + const jobSchedules: SqlJobSchedule[] = results[9].recordset; + const data: SqlDataResult[] = results.slice(10); const generator = new MSSQLGenerator(config); const file = new FileUtility(config); @@ -200,6 +211,16 @@ export default class Pull { file.write(config.output.data, name, content); }); + // jobs + jobs.forEach(item => { + const steps = jobSteps.filter(x => x.job_id === item.job_id); + const schedules = jobSchedules.filter(x => x.job_id === item.job_id); + const name = `${item.name}.sql`; + const content = generator.job(item, steps, schedules); + + file.write(config.output.jobs, name, content); + }); + const msg = file.finalize(); this.spinner.succeed(msg); } diff --git a/src/commands/push.ts b/src/commands/push.ts index 687a662..2f17720 100644 --- a/src/commands/push.ts +++ b/src/commands/push.ts @@ -21,7 +21,7 @@ export default class Push { /** * Invoke actions. */ - invoke(): void { + invoke() { const config = new Config(this.options.config); const conn = config.getConnection(this.name); @@ -29,7 +29,7 @@ export default class Push { .prompt([ { message: [ - 'WARNING! All local SQL files will be executed against the requested database.', + `${chalk.yellow('WARNING!')} All local SQL files will be executed against the requested database.`, 'This can not be undone!', 'Make sure to backup your database first.', EOL, @@ -56,7 +56,7 @@ export default class Push { * @param config Configuration used to execute commands. * @param conn Connection used to execute commands. */ - private batch(config: Config, conn: Connection): Promise { + private batch(config: Config, conn: Connection) { const files = this.getFilesOrdered(config); let promise = new sql.ConnectionPool(conn).connect(); @@ -94,7 +94,8 @@ export default class Push { config.output.functions, config.output.procs, config.output.triggers, - config.output.data + config.output.data, + config.output.jobs ]; directories.forEach(dir => { diff --git a/src/common/config.ts b/src/common/config.ts index 87fde15..9d39a27 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -120,6 +120,7 @@ export default class Config implements IConfig { output: OutputConfig = { data: './data', functions: './functions', + jobs: './jobs', procs: './stored-procedures', root: './_sql-database', schemas: './schemas', @@ -135,6 +136,7 @@ export default class Config implements IConfig { idempotency: IdempotencyConfig = { data: 'truncate', functions: 'if-exists-drop', + jobs: 'if-exists-drop', procs: 'if-exists-drop', tables: 'if-not-exists', triggers: 'if-exists-drop', diff --git a/src/common/helpers.ts b/src/common/helpers.ts new file mode 100644 index 0000000..fa4daea --- /dev/null +++ b/src/common/helpers.ts @@ -0,0 +1,23 @@ +/** + * Common helper functions. + */ +export class Helpers { + /** + * Group a collection of objects by a specific key. + * + * @param items Collection of items to group. + * @param key Property name to group by. + */ + static groupByName(items: T[], key: string): { [key: string]: T[] } { + return items.reduce( + (prev, cur) => { + const prop = cur[key]; + const group = (prev[prop] = prev[prop] || []); + group.push(cur); + + return prev; + }, + {} as { [key: string]: T[] } + ); + } +} diff --git a/src/common/interfaces.ts b/src/common/interfaces.ts index 1860dfe..886363f 100644 --- a/src/common/interfaces.ts +++ b/src/common/interfaces.ts @@ -48,6 +48,7 @@ export interface IConnection { export interface IdempotencyConfig { data?: IdempotencyData; functions?: IdempotencyObject; + jobs?: IdempotencyObject; procs?: IdempotencyObject; tables?: IdempotencyObject; triggers?: IdempotencyObject; @@ -62,6 +63,7 @@ export interface OutputConfig { root?: string; data?: string | false; functions?: string | false; + jobs?: string | false; procs?: string | false; schemas?: string | false; tables?: string | false; diff --git a/src/generators/mssql.ts b/src/generators/mssql.ts index 78b64fb..35c5ba7 100644 --- a/src/generators/mssql.ts +++ b/src/generators/mssql.ts @@ -3,18 +3,21 @@ import { EOL } from 'os'; import { isBoolean, isDate, isNull, isString } from 'ts-util-is'; import Config from '../common/config'; +import { Helpers } from '../common/helpers'; import { SqlColumn, SqlDataResult, SqlForeignKey, SqlIndex, + SqlJob, + SqlJobSchedule, + SqlJobStep, SqlObject, SqlPrimaryKey, SqlSchema, SqlTable, SqlType } from '../queries/interfaces'; -import { GroupedObjects } from './interfaces'; /** * MSSQL generator. @@ -207,7 +210,7 @@ export default class MSSQLGenerator { foreignKeys = foreignKeys.filter(x => x.object_id === item.object_id); indexes = indexes.filter(x => x.object_id === item.object_id); - const groupedKeys = this.groupByName(primaryKeys); + const groupedKeys = Helpers.groupByName(primaryKeys, 'name'); Object.keys(groupedKeys).forEach(name => { output += this.primaryKey(groupedKeys[name]); output += EOL; @@ -225,7 +228,7 @@ export default class MSSQLGenerator { output += EOL; }); - const groupedIndexes = this.groupByName(indexes); + const groupedIndexes = Helpers.groupByName(indexes, 'name'); Object.keys(groupedIndexes).forEach(name => { output += this.index(groupedIndexes[name]); output += EOL; @@ -413,20 +416,39 @@ export default class MSSQLGenerator { } /** - * Group a collection of items by name. + * Get job file content. * - * @param items Collection of items to group. + * @param steps Steps from query. + * @param schedules Schedules from query. */ - private groupByName(items: T[]) { - return items.reduce( - (prev, cur) => { - const group = (prev[cur.name] = prev[cur.name] || []); - group.push(cur); - - return prev; - }, - {} as GroupedObjects - ); + job(job: SqlJob, steps: SqlJobStep[], schedules: SqlJobSchedule[]) { + let output = ''; + + switch (this.config.idempotency.views) { + case 'if-exists-drop': + output += `IF EXISTS (SELECT 1 FROM msdb.dbo.sysjobs WHERE name = '${job.name}')`; + output += EOL; + output += `EXEC msdb.dbo.sp_delete_job @job_name = '${job.name}'`; + output += EOL; + output += 'GO'; + output += EOL; + output += EOL; + output += this.addJob(job, steps, schedules); + output += EOL; + break; + case 'if-not-exists': + output += `IF NOT EXISTS (SELECT 1 FROM msdb.dbo.sysjobs WHERE name = '${job.name}')`; + output += EOL; + output += 'BEGIN'; + output += EOL; + output += this.addJob(job, steps, schedules); + output += EOL; + output += 'END'; + output += EOL; + break; + } + + return output; } /** @@ -677,6 +699,148 @@ export default class MSSQLGenerator { return `[${item.column}] ${direction}`; } + /** + * Get job file content. + * + * @param steps Steps from query. + * @param schedules Schedules from query. + */ + private addJob(job: SqlJob, steps: SqlJobStep[], schedules: SqlJobSchedule[]) { + let output = ''; + + // job + output += 'EXEC msdb.dbo.sp_add_job '; + output += EOL; + output += this.indent() + `@job_name = N'${job.name}',`; + output += EOL; + output += this.indent() + `@enabled = ${job.enabled},`; + output += EOL; + output += this.indent() + `@description = N'${job.description}',`; + output += EOL; + output += this.indent() + `@notify_level_eventlog = ${job.notify_level_eventlog},`; + output += EOL; + output += this.indent() + `@notify_level_email = ${job.notify_level_email},`; + output += EOL; + output += this.indent() + `@notify_level_netsend = ${job.notify_level_netsend},`; + output += EOL; + output += this.indent() + `@notify_level_page = ${job.notify_level_page},`; + output += EOL; + output += this.indent() + `@delete_level = ${job.delete_level};`; + output += EOL; + output += 'GO'; + output += EOL; + output += EOL; + + // steps + steps.forEach(step => { + output += 'EXEC msdb.dbo.sp_add_jobstep'; + output += EOL; + output += this.indent() + `@job_name = N'${step.job_name}',`; + output += EOL; + output += this.indent() + `@step_name = N'${step.step_name}',`; + output += EOL; + output += this.indent() + `@subsystem = N'${step.subsystem}',`; + output += EOL; + output += this.indent() + `@command = N'${step.command}',`; + output += EOL; + + if (step.additional_parameters) { + output += this.indent() + `@additional_parameters = N'${step.additional_parameters}',`; + output += EOL; + } + + output += this.indent() + `@cmdexec_success_code = ${step.cmdexec_success_code},`; + output += EOL; + output += this.indent() + `@on_success_action = ${step.on_success_action},`; + output += EOL; + output += this.indent() + `@on_success_step_id = ${step.on_success_step_id},`; + output += EOL; + output += this.indent() + `@on_fail_action = ${step.on_fail_action},`; + output += EOL; + output += this.indent() + `@on_fail_step_id = ${step.on_fail_step_id},`; + output += EOL; + output += this.indent() + `@database_name = N'${step.database_name}',`; + output += EOL; + + if (step.database_user_name) { + output += this.indent() + `@database_user_name = N'${step.database_user_name}',`; + output += EOL; + } + + output += this.indent() + `@retry_attempts = ${step.retry_attempts},`; + output += EOL; + output += this.indent() + `@retry_interval = ${step.retry_interval},`; + output += EOL; + output += this.indent() + `@os_run_priority = ${step.os_run_priority},`; + output += EOL; + output += this.indent() + `@flags = ${step.flags};`; + output += EOL; + output += 'GO'; + output += EOL; + output += EOL; + }); + + // schedule + if (schedules.length) { + output += 'EXEC msdb.dbo.sp_add_schedule'; + output += EOL; + + schedules.forEach(schedule => { + output += this.indent() + `@schedule_name = N'${schedule.schedule_name}',`; + output += EOL; + output += this.indent() + `@enabled = ${schedule.enabled},`; + output += EOL; + output += this.indent() + `@freq_type = ${schedule.freq_type},`; + output += EOL; + output += this.indent() + `@freq_interval = ${schedule.freq_interval},`; + output += EOL; + output += this.indent() + `@freq_subday_type = ${schedule.freq_subday_type},`; + output += EOL; + output += this.indent() + `@freq_subday_interval = ${schedule.freq_subday_interval},`; + output += EOL; + output += this.indent() + `@freq_relative_interval = ${schedule.freq_relative_interval},`; + output += EOL; + output += this.indent() + `@freq_recurrence_factor = ${schedule.freq_recurrence_factor},`; + output += EOL; + output += this.indent() + `@active_start_date = ${schedule.active_start_date},`; + output += EOL; + output += this.indent() + `@active_end_date = ${schedule.active_end_date},`; + output += EOL; + output += this.indent() + `@active_start_time = ${schedule.active_start_time},`; + output += EOL; + output += this.indent() + `@active_end_time = ${schedule.active_end_time};`; + output += EOL; + }); + + output += 'GO'; + output += EOL; + output += EOL; + + // attach + const scheduleName = schedules[0].schedule_name; + + output += 'EXEC msdb.dbo.sp_attach_schedule'; + output += EOL; + output += this.indent() + `@job_name = N'${job.name}',`; + output += EOL; + output += this.indent() + `@schedule_name = N'${scheduleName}';`; + output += EOL; + output += 'GO'; + output += EOL; + output += EOL; + } + + // job server + output += 'EXEC msdb.dbo.sp_add_jobserver'; + output += EOL; + output += this.indent() + `@job_name = N'${job.name}';`; + output += EOL; + output += 'GO'; + output += EOL; + + return output; + } + /** * Generate indentation spacing. * diff --git a/src/queries/interfaces.ts b/src/queries/interfaces.ts index 4b3896e..511dc1a 100644 --- a/src/queries/interfaces.ts +++ b/src/queries/interfaces.ts @@ -115,3 +115,63 @@ export interface SqlIndex { export interface SqlObject extends AbstractSqlObject { text: string; } + +/** + * SQL job. + */ +export interface SqlJob { + job_id: string; + enabled: boolean; + name: string; + description: string; + notify_level_eventlog: number; + notify_level_email: number; + notify_level_netsend: number; + notify_level_page: number; + delete_level: number; +} + +/** + * SQL job step. + */ +export interface SqlJobStep { + job_id: string; + job_name: string; + step_uid: string; + step_number: number; + step_name: string; + subsystem: string; + command: string; + additional_parameters: string; + cmdexec_success_code: number; + on_success_action: number; + on_success_step_id: number; + on_fail_action: number; + on_fail_step_id: number; + database_name: string; + database_user_name: string; + retry_attempts: number; + retry_interval: number; + os_run_priority: number; + flags: number; +} + +/** + * SQL job schedule. + */ +export interface SqlJobSchedule { + job_id: string; + schedule_uid: string; + schedule_name: string; + enabled: boolean; + freq_type: number; + freq_interval: number; + freq_subday_type: number; + freq_subday_interval: number; + freq_relative_interval: number; + freq_recurrence_factor: number; + active_start_date: number; + active_end_date: number; + active_start_time: number; + active_end_time: number; +} diff --git a/src/queries/mssql.ts b/src/queries/mssql.ts index 9c7f133..17725a1 100644 --- a/src/queries/mssql.ts +++ b/src/queries/mssql.ts @@ -197,7 +197,90 @@ export const objectsRead = ` INNER JOIN syscomments sc ON sc.id = so.object_id AND so.type in ('P', 'V', 'TF', 'IF', 'FN', 'TR') INNER JOIN sys.schemas s ON s.schema_id = so.schema_id GROUP BY - so.name - ,s.name - ,so.type + so.name, + s.name, + so.type +`; + +/** + * Get SQL information for jobs. + */ +export const jobsRead = (database: string) => ` + SELECT DISTINCT + j.job_id, + j.name, + j.enabled, + j.description, + j.notify_level_eventlog, + j.notify_level_email, + j.notify_level_netsend, + j.notify_level_page, + j.delete_level + FROM + msdb.dbo.sysjobs j + LEFT JOIN msdb.dbo.sysjobsteps s ON s.job_id = j.job_id + WHERE + s.database_name = '${database}' + ORDER BY + j.name +`; + +/** + * Get SQL information for jobs. + */ +export const jobStepsRead = (database: string) => ` + SELECT + s.job_id, + j.name as [job_name], + s.step_uid, + s.step_id AS step_number, + s.step_name, + s.subsystem, + s.command, + s.additional_parameters, + s.cmdexec_success_code, + s.on_success_action, + s.on_success_step_id, + s.on_fail_action, + s.on_fail_step_id, + s.database_name, + s.database_user_name, + s.retry_attempts, + s.retry_interval, + s.os_run_priority, + s.flags + FROM + msdb.dbo.sysjobsteps s + INNER JOIN msdb.dbo.sysjobs j ON j.job_id = s.job_id + WHERE + s.database_name = '${database}' + ORDER BY + s.job_id, + s.step_id +`; + +/** + * Get SQL information for job schedules. + */ +export const jobSchedulesRead = (database: string) => ` + SELECT + s.schedule_uid, + s.name AS [schedule_name], + s.enabled, + s.freq_type, + s.freq_interval, + s.freq_subday_type, + s.freq_subday_interval, + s.freq_relative_interval, + s.freq_recurrence_factor, + s.active_start_date, + s.active_end_date, + s.active_start_time, + s.active_end_time, + js.job_id + FROM + msdb.dbo.sysschedules s + INNER JOIN msdb.dbo.sysjobschedules js ON js.schedule_id = s.schedule_id + ORDER BY + s.name `;