diff --git a/extensions/dacpac/src/test/testDacFxService.ts b/extensions/dacpac/src/test/testDacFxService.ts index 62ff028f42..517d7dbf1b 100644 --- a/extensions/dacpac/src/test/testDacFxService.ts +++ b/extensions/dacpac/src/test/testDacFxService.ts @@ -10,8 +10,9 @@ export const deployOperationId = 'deploy dacpac'; export const extractOperationId = 'extract dacpac'; export const exportOperationId = 'export bacpac'; export const importOperationId = 'import bacpac'; -export const generateScript = 'genenrate script'; -export const generateDeployPlan = 'genenrate deploy plan'; +export const generateScript = 'generate script'; +export const generateDeployPlan = 'generate deploy plan'; +export const validateStreamingJob = 'validate streaming job'; export class DacFxTestService implements mssql.IDacFxService { dacfxResult: mssql.DacFxResult = { @@ -22,31 +23,31 @@ export class DacFxTestService implements mssql.IDacFxService { constructor() { } - exportBacpac(databaseName: string, packageFilePath: string, ownerUri: string, taskExecutionMode: azdata.TaskExecutionMode): Thenable { + exportBacpac(databaseName: string, packageFilePath: string, ownerUri: string, taskExecutionMode: azdata.TaskExecutionMode): Promise { this.dacfxResult.operationId = exportOperationId; return Promise.resolve(this.dacfxResult); } - importBacpac(packageFilePath: string, databaseName: string, ownerUri: string, taskExecutionMode: azdata.TaskExecutionMode): Thenable { + importBacpac(packageFilePath: string, databaseName: string, ownerUri: string, taskExecutionMode: azdata.TaskExecutionMode): Promise { this.dacfxResult.operationId = importOperationId; return Promise.resolve(this.dacfxResult); } - extractDacpac(databaseName: string, packageFilePath: string, applicationName: string, applicationVersion: string, ownerUri: string, taskExecutionMode: azdata.TaskExecutionMode): Thenable { + extractDacpac(databaseName: string, packageFilePath: string, applicationName: string, applicationVersion: string, ownerUri: string, taskExecutionMode: azdata.TaskExecutionMode): Promise { this.dacfxResult.operationId = extractOperationId; return Promise.resolve(this.dacfxResult); } - importDatabaseProject(databaseName: string, targetFilePath: string, applicationName: string, applicationVersion: string, ownerUri: string, extractTarget: mssql.ExtractTarget, taskExecutionMode: azdata.TaskExecutionMode): Thenable { + importDatabaseProject(databaseName: string, targetFilePath: string, applicationName: string, applicationVersion: string, ownerUri: string, extractTarget: mssql.ExtractTarget, taskExecutionMode: azdata.TaskExecutionMode): Promise { this.dacfxResult.operationId = importOperationId; return Promise.resolve(this.dacfxResult); } - deployDacpac(packageFilePath: string, databaseName: string, upgradeExisting: boolean, ownerUri: string, taskExecutionMode: azdata.TaskExecutionMode, sqlCommandVariableValues?: Record): Thenable { + deployDacpac(packageFilePath: string, databaseName: string, upgradeExisting: boolean, ownerUri: string, taskExecutionMode: azdata.TaskExecutionMode, sqlCommandVariableValues?: Record): Promise { this.dacfxResult.operationId = deployOperationId; return Promise.resolve(this.dacfxResult); } - generateDeployScript(packageFilePath: string, databaseName: string, ownerUri: string, taskExecutionMode: azdata.TaskExecutionMode, sqlCommandVariableValues?: Record): Thenable { + generateDeployScript(packageFilePath: string, databaseName: string, ownerUri: string, taskExecutionMode: azdata.TaskExecutionMode, sqlCommandVariableValues?: Record): Promise { this.dacfxResult.operationId = generateScript; return Promise.resolve(this.dacfxResult); } - generateDeployPlan(packageFilePath: string, databaseName: string, ownerUri: string, taskExecutionMode: azdata.TaskExecutionMode): Thenable { + generateDeployPlan(packageFilePath: string, databaseName: string, ownerUri: string, taskExecutionMode: azdata.TaskExecutionMode): Promise { this.dacfxResult.operationId = generateDeployPlan; const deployPlan: mssql.GenerateDeployPlanResult = { operationId: generateDeployPlan, @@ -56,7 +57,7 @@ export class DacFxTestService implements mssql.IDacFxService { }; return Promise.resolve(deployPlan); } - getOptionsFromProfile(profilePath: string): Thenable { + getOptionsFromProfile(profilePath: string): Promise { const optionsResult: mssql.DacFxOptionsResult = { success: true, errorMessage: '', @@ -144,4 +145,12 @@ export class DacFxTestService implements mssql.IDacFxService { return Promise.resolve(optionsResult); } + validateStreamingJob(packageFilePath: string, createStreamingJobTsql: string): Promise { + this.dacfxResult.operationId = validateStreamingJob; + const streamingJobValidationResult: mssql.ValidateStreamingJobResult = { + success: true, + errorMessage: '' + }; + return Promise.resolve(streamingJobValidationResult); + } } diff --git a/extensions/mssql/src/contracts.ts b/extensions/mssql/src/contracts.ts index 2d0bdce4d3..b20c7360b9 100644 --- a/extensions/mssql/src/contracts.ts +++ b/extensions/mssql/src/contracts.ts @@ -476,6 +476,12 @@ export interface GenerateDeployPlanParams { export interface GetOptionsFromProfileParams { profilePath: string; } + +export interface ValidateStreamingJobParams { + packageFilePath: string, + createStreamingJobTsql: string +} + export namespace ExportRequest { export const type = new RequestType('dacfx/export'); } @@ -503,7 +509,12 @@ export namespace GenerateDeployPlanRequest { export namespace GetOptionsFromProfileRequest { export const type = new RequestType('dacfx/getOptionsFromProfile'); } -// ------------------------------- < DacFx > ------------------------------------ + +export namespace ValidateStreamingJobRequest { + export const type = new RequestType('dacfx/validateStreamingJob'); +} + +// ------------------------------- ------------------------------------ // ------------------------------- ---------------------------------------- diff --git a/extensions/mssql/src/dacfx/dacFxService.ts b/extensions/mssql/src/dacfx/dacFxService.ts index 90ac6c8bf7..d53e761d1e 100644 --- a/extensions/mssql/src/dacfx/dacFxService.ts +++ b/extensions/mssql/src/dacfx/dacFxService.ts @@ -119,4 +119,15 @@ export class DacFxService implements mssql.IDacFxService { } ); } + + public validateStreamingJob(packageFilePath: string, createStreamingJobTsql: string): Thenable { + const params: contracts.ValidateStreamingJobParams = { packageFilePath: packageFilePath, createStreamingJobTsql: createStreamingJobTsql }; + return this.client.sendRequest(contracts.ValidateStreamingJobRequest.type, params).then( + undefined, + e => { + this.client.logFailedRequest(contracts.ValidateStreamingJobRequest.type, e); + return Promise.resolve(undefined); + } + ); + } } diff --git a/extensions/mssql/src/mssql.d.ts b/extensions/mssql/src/mssql.d.ts index 66f10b325c..07a6d9f1c0 100644 --- a/extensions/mssql/src/mssql.d.ts +++ b/extensions/mssql/src/mssql.d.ts @@ -339,6 +339,7 @@ export interface IDacFxService { generateDeployScript(packageFilePath: string, databaseName: string, ownerUri: string, taskExecutionMode: azdata.TaskExecutionMode, sqlCommandVariableValues?: Record, deploymentOptions?: DeploymentOptions): Thenable; generateDeployPlan(packageFilePath: string, databaseName: string, ownerUri: string, taskExecutionMode: azdata.TaskExecutionMode): Thenable; getOptionsFromProfile(profilePath: string): Thenable; + validateStreamingJob(packageFilePath: string, createStreamingJobTsql: string): Thenable; } export interface DacFxResult extends azdata.ResultStatus { @@ -353,6 +354,9 @@ export interface DacFxOptionsResult extends azdata.ResultStatus { deploymentOptions: DeploymentOptions; } +export interface ValidateStreamingJobResult extends azdata.ResultStatus { +} + export interface ExportParams { databaseName: string; packageFilePath: string; diff --git a/extensions/sql-database-projects/package.json b/extensions/sql-database-projects/package.json index 4daa9bf3d7..297c113fca 100644 --- a/extensions/sql-database-projects/package.json +++ b/extensions/sql-database-projects/package.json @@ -95,6 +95,11 @@ "title": "%sqlDatabaseProjects.newView%", "category": "%sqlDatabaseProjects.displayName%" }, + { + "command": "sqlDatabaseProjects.newExternalStreamingJob", + "title": "%sqlDatabaseProjects.newExternalStreamingJob%", + "category": "%sqlDatabaseProjects.displayName%" + }, { "command": "sqlDatabaseProjects.newStoredProcedure", "title": "%sqlDatabaseProjects.newStoredProcedure%", @@ -151,6 +156,11 @@ "title": "%sqlDatabaseProjects.addDatabaseReference%", "category": "%sqlDatabaseProjects.displayName%" }, + { + "command": "sqlDatabaseProjects.validateExternalStreamingJob", + "title": "%sqlDatabaseProjects.validateExternalStreamingJob%", + "category": "%sqlDatabaseProjects.displayName%" + }, { "command": "sqlDatabaseProjects.openContainingFolder", "title": "%sqlDatabaseProjects.openContainingFolder%", @@ -199,6 +209,10 @@ "command": "sqlDatabaseProjects.newView", "when": "false" }, + { + "command": "sqlDatabaseProjects.newExternalStreamingJob", + "when": "false" + }, { "command": "sqlDatabaseProjects.newStoredProcedure", "when": "false" @@ -238,6 +252,10 @@ "command": "sqlDatabaseProjects.addDatabaseReference", "when": "false" }, + { + "command": "sqlDatabaseProjects.validateExternalStreamingJob", + "when": "false" + }, { "command": "sqlDatabaseProjects.openContainingFolder", "when": "false" @@ -308,6 +326,11 @@ "when": "view =~ /^(sqlDatabaseProjectsView|dataworkspace.views.main)$/ && viewItem == databaseProject.itemType.project || viewItem == databaseProject.itemType.folder", "group": "3_dbProjects_newItem@3" }, + { + "command": "sqlDatabaseProjects.newExternalStreamingJob", + "when": "view =~ /^(sqlDatabaseProjectsView|dataworkspace.views.main)$/ && viewItem == databaseProject.itemType.project || viewItem == databaseProject.itemType.folder", + "group": "3_dbProjects_newItem@4" + }, { "command": "sqlDatabaseProjects.newScript", "when": "view =~ /^(sqlDatabaseProjectsView|dataworkspace.views.main)$/ && viewItem == databaseProject.itemType.project || viewItem == databaseProject.itemType.folder", @@ -328,14 +351,19 @@ "when": "view =~ /^(sqlDatabaseProjectsView|dataworkspace.views.main)$/ && viewItem == databaseProject.itemType.referencesRoot", "group": "4_dbProjects_addDatabaseReference" }, + { + "command": "sqlDatabaseProjects.validateExternalStreamingJob", + "when": "view =~ /^(sqlDatabaseProjectsView|dataworkspace.views.main)$/ && viewItem == databaseProject.itemType.file.externalStreamingJob", + "group": "5_dbProjects_streamingJob" + }, { "command": "sqlDatabaseProjects.exclude", - "when": "view =~ /^(sqlDatabaseProjectsView|dataworkspace.views.main)$/ && viewItem == databaseProject.itemType.folder || viewItem == databaseProject.itemType.file", + "when": "view =~ /^(sqlDatabaseProjectsView|dataworkspace.views.main)$/ && viewItem == databaseProject.itemType.folder || viewItem =~ /^databaseProject.itemType.file/", "group": "9_dbProjectsLast@1" }, { "command": "sqlDatabaseProjects.delete", - "when": "view =~ /^(sqlDatabaseProjectsView|dataworkspace.views.main)$/ && viewItem == databaseProject.itemType.folder || viewItem == databaseProject.itemType.file || viewItem == databaseProject.itemType.reference", + "when": "view =~ /^(sqlDatabaseProjectsView|dataworkspace.views.main)$/ && viewItem == databaseProject.itemType.folder || viewItem =~ /^databaseProject.itemType.file/ || viewItem == databaseProject.itemType.reference", "group": "9_dbProjectsLast@2" }, { diff --git a/extensions/sql-database-projects/package.nls.json b/extensions/sql-database-projects/package.nls.json index 230823fb51..1c4ef4c48a 100644 --- a/extensions/sql-database-projects/package.nls.json +++ b/extensions/sql-database-projects/package.nls.json @@ -13,6 +13,7 @@ "sqlDatabaseProjects.schemaCompare": "Schema Compare", "sqlDatabaseProjects.delete": "Delete", "sqlDatabaseProjects.exclude": "Exclude from project", + "sqlDatabaseProjects.validateExternalStreamingJob": "Validate External Streaming Job", "sqlDatabaseProjects.newScript": "Add Script", "sqlDatabaseProjects.newPreDeploymentScript": "Add Pre-Deployment Script", @@ -20,6 +21,7 @@ "sqlDatabaseProjects.newTable": "Add Table", "sqlDatabaseProjects.newView": "Add View", "sqlDatabaseProjects.newStoredProcedure": "Add Stored Procedure", + "sqlDatabaseProjects.newExternalStreamingJob": "Add External Streaming Job", "sqlDatabaseProjects.newItem": "Add Item...", "sqlDatabaseProjects.newFolder": "Add Folder", diff --git a/extensions/sql-database-projects/resources/templates/newTsqlExternalStreamingJobTemplate.sql b/extensions/sql-database-projects/resources/templates/newTsqlExternalStreamingJobTemplate.sql new file mode 100644 index 0000000000..d6924df8ef --- /dev/null +++ b/extensions/sql-database-projects/resources/templates/newTsqlExternalStreamingJobTemplate.sql @@ -0,0 +1,7 @@ +EXEC sys.sp_create_streaming_job @NAME = '@@OBJECT_NAME@@', @STATEMENT = 'INSERT INTO SqlOutputStream SELECT + timeCreated, + machine.temperature as machine_temperature, + machine.pressure as machine_pressure, + ambient.temperature as ambient_temperature, + ambient.humidity as ambient_humidity +FROM EdgeHubInputStream' diff --git a/extensions/sql-database-projects/src/common/constants.ts b/extensions/sql-database-projects/src/common/constants.ts index 1afdeda920..dae78ae355 100644 --- a/extensions/sql-database-projects/src/common/constants.ts +++ b/extensions/sql-database-projects/src/common/constants.ts @@ -149,6 +149,7 @@ export const ousiderFolderPath = localize('outsideFolderPath', "Items with absol export const parentTreeItemUnknown = localize('parentTreeItemUnknown', "Cannot access parent of provided tree item"); export const prePostDeployCount = localize('prePostDeployCount', "To successfully build, update the project to have one pre-deployment script and/or one post-deployment script"); export const invalidProjectReload = localize('invalidProjectReload', "Cannot access provided database project. Only valid, open database projects can be reloaded."); +export const externalStreamingJobValidationPassed = localize('externalStreamingJobValidationPassed', "Validation of external streaming job passed."); export function projectAlreadyOpened(path: string) { return localize('projectAlreadyOpened', "Project '{0}' is already opened.", path); } export function projectAlreadyExists(name: string, path: string) { return localize('projectAlreadyExists', "A project named {0} already exists in {1}.", name, path); } export function noFileExist(fileName: string) { return localize('noFileExist', "File {0} doesn't exist", fileName); } @@ -184,6 +185,7 @@ export const scriptFriendlyName = localize('scriptFriendlyName', "Script"); export const tableFriendlyName = localize('tableFriendlyName', "Table"); export const viewFriendlyName = localize('viewFriendlyName', "View"); export const storedProcedureFriendlyName = localize('storedProcedureFriendlyName', "Stored Procedure"); +export const externalStreamingJobFriendlyName = localize('externalStreamingJobFriendlyName', "External Streaming Job"); export const preDeployScriptFriendlyName = localize('preDeployScriptFriendlyName', "Script.PreDeployment"); export const postDeployScriptFriendlyName = localize('postDeployScriptFriendlyName', "Script.PostDeployment"); @@ -223,6 +225,8 @@ export const True = 'True'; export const False = 'False'; export const Private = 'Private'; export const ProjectGuid = 'ProjectGuid'; +export const Type = 'Type'; +export const ExternalStreamingJob: string = 'ExternalStreamingJob'; // SqlProj File targets export const NetCoreTargets = '$(NETCoreTargetsPath)\\Microsoft.Data.Tools.Schema.SqlTasks.targets'; @@ -270,6 +274,7 @@ export enum DatabaseProjectItemType { project = 'databaseProject.itemType.project', folder = 'databaseProject.itemType.folder', file = 'databaseProject.itemType.file', + externalStreamingJob = 'databaseProject.itemType.file.externalStreamingJob', referencesRoot = 'databaseProject.itemType.referencesRoot', reference = 'databaseProject.itemType.reference', dataSourceRoot = 'databaseProject.itemType.dataSourceRoot', diff --git a/extensions/sql-database-projects/src/controllers/mainController.ts b/extensions/sql-database-projects/src/controllers/mainController.ts index 681f967812..b10073ea4e 100644 --- a/extensions/sql-database-projects/src/controllers/mainController.ts +++ b/extensions/sql-database-projects/src/controllers/mainController.ts @@ -16,7 +16,7 @@ import { ProjectsController } from './projectController'; import { BaseProjectTreeItem } from '../models/tree/baseTreeItem'; import { NetCoreTool } from '../tools/netcoreTool'; import { Project } from '../models/project'; -import { FileNode, FolderNode } from '../models/tree/fileFolderTreeItem'; +import { ExternalStreamingJobFileNode, FileNode, FolderNode } from '../models/tree/fileFolderTreeItem'; import { IconPathHelper } from '../common/iconHelper'; import { IProjectProvider } from 'dataworkspace'; import { SqlDatabaseProjectProvider } from '../projectProvider/projectProvider'; @@ -70,6 +70,7 @@ export default class MainController implements vscode.Disposable { vscode.commands.registerCommand('sqlDatabaseProjects.newTable', async (node: BaseProjectTreeItem) => { await this.projectsController.addItemPromptFromNode(node, templates.table); }); vscode.commands.registerCommand('sqlDatabaseProjects.newView', async (node: BaseProjectTreeItem) => { await this.projectsController.addItemPromptFromNode(node, templates.view); }); vscode.commands.registerCommand('sqlDatabaseProjects.newStoredProcedure', async (node: BaseProjectTreeItem) => { await this.projectsController.addItemPromptFromNode(node, templates.storedProcedure); }); + vscode.commands.registerCommand('sqlDatabaseProjects.newExternalStreamingJob', async (node: BaseProjectTreeItem) => { await this.projectsController.addItemPromptFromNode(node, templates.externalStreamingJob); }); vscode.commands.registerCommand('sqlDatabaseProjects.newItem', async (node: BaseProjectTreeItem) => { await this.projectsController.addItemPromptFromNode(node); }); vscode.commands.registerCommand('sqlDatabaseProjects.newFolder', async (node: BaseProjectTreeItem) => { await this.projectsController.addFolderPrompt(node); }); @@ -79,6 +80,7 @@ export default class MainController implements vscode.Disposable { vscode.commands.registerCommand('sqlDatabaseProjects.delete', async (node: BaseProjectTreeItem) => { await this.projectsController.delete(node); }); vscode.commands.registerCommand('sqlDatabaseProjects.exclude', async (node: FileNode | FolderNode) => { await this.projectsController.exclude(node); }); vscode.commands.registerCommand('sqlDatabaseProjects.changeTargetPlatform', async (node: BaseProjectTreeItem) => { await this.projectsController.changeTargetPlatform(node); }); + vscode.commands.registerCommand('sqlDatabaseProjects.validateExternalStreamingJob', async (node: ExternalStreamingJobFileNode) => { await this.projectsController.validateExternalStreamingJob(node); }); IconPathHelper.setExtensionContext(this.extensionContext); diff --git a/extensions/sql-database-projects/src/controllers/projectController.ts b/extensions/sql-database-projects/src/controllers/projectController.ts index a0940d8968..e5d33fec64 100644 --- a/extensions/sql-database-projects/src/controllers/projectController.ts +++ b/extensions/sql-database-projects/src/controllers/projectController.ts @@ -19,7 +19,7 @@ import { promises as fs } from 'fs'; import { PublishDatabaseDialog } from '../dialogs/publishDatabaseDialog'; import { Project, reservedProjectFolders, FileProjectEntry, SqlProjectReferenceProjectEntry, IDatabaseReferenceProjectEntry } from '../models/project'; import { SqlDatabaseProjectTreeViewProvider } from './databaseProjectTreeViewProvider'; -import { FolderNode, FileNode } from '../models/tree/fileFolderTreeItem'; +import { FolderNode, FileNode, ExternalStreamingJobFileNode } from '../models/tree/fileFolderTreeItem'; import { IPublishSettings, IGenerateScriptSettings } from '../models/IPublishSettings'; import { BaseProjectTreeItem } from '../models/tree/baseTreeItem'; import { ProjectRootTreeItem } from '../models/tree/projectTreeItem'; @@ -204,7 +204,7 @@ export class ProjectsController { try { await this.netCoreTool.runDotnetCommand(options); - return path.join(project.projectFolderPath, 'bin', 'Debug', `${project.projectFileName}.dacpac`); + return project.dacpacOutputPath; } catch (err) { vscode.window.showErrorMessage(constants.projBuildFailed(utils.getErrorMessage(err))); @@ -577,6 +577,31 @@ export class ProjectsController { } } + + public async validateExternalStreamingJob(node: ExternalStreamingJobFileNode): Promise { + const project: Project = this.getProjectFromContext(node); + + let dacpacPath: string = project.dacpacOutputPath; + + if (!await utils.exists(dacpacPath)) { + dacpacPath = await this.buildProject(node); + } + + const streamingJobDefinition: string = (await fs.readFile(node.fileSystemUri.fsPath)).toString(); + + const dacFxService = await this.getDaxFxService(); + const result: mssql.ValidateStreamingJobResult = await dacFxService.validateStreamingJob(dacpacPath, streamingJobDefinition); + + if (result.success) { + vscode.window.showInformationMessage(constants.externalStreamingJobValidationPassed); + } + else { + vscode.window.showErrorMessage(result.errorMessage); + } + + return result; + } + //#region Helper methods public getPublishDialog(project: Project): PublishDatabaseDialog { diff --git a/extensions/sql-database-projects/src/models/project.ts b/extensions/sql-database-projects/src/models/project.ts index 1a92e59405..48f8803fe9 100644 --- a/extensions/sql-database-projects/src/models/project.ts +++ b/extensions/sql-database-projects/src/models/project.ts @@ -32,6 +32,10 @@ export class Project { public postDeployScripts: FileProjectEntry[] = []; public noneDeployScripts: FileProjectEntry[] = []; + public get dacpacOutputPath(): string { + return path.join(this.projectFolderPath, 'bin', 'Debug', `${this.projectFileName}.dacpac`); + } + public get projectFolderPath() { return Uri.file(path.dirname(this.projectFilePath)).fsPath; } @@ -71,7 +75,7 @@ export class Project { const buildElements = itemGroup.getElementsByTagName(constants.Build); for (let b = 0; b < buildElements.length; b++) { - this.files.push(this.createFileProjectEntry(buildElements[b].getAttribute(constants.Include), EntryType.File)); + this.files.push(this.createFileProjectEntry(buildElements[b].getAttribute(constants.Include), EntryType.File, buildElements[b].getAttribute(constants.Type))); } const folderElements = itemGroup.getElementsByTagName(constants.Folder); @@ -291,7 +295,13 @@ export class Project { this.files.push(fileEntry); } - await this.addToProjFile(fileEntry, xmlTag); + const attributes = new Map(); + + if (itemType === templates.externalStreamingJob) { + attributes.set(constants.Type, constants.ExternalStreamingJob); + } + + await this.addToProjFile(fileEntry, xmlTag, attributes); return fileEntry; } @@ -454,9 +464,9 @@ export class Project { await this.addToProjFile(sqlCmdVariableEntry); } - public createFileProjectEntry(relativePath: string, entryType: EntryType): FileProjectEntry { + public createFileProjectEntry(relativePath: string, entryType: EntryType, sqlObjectType?: string): FileProjectEntry { let platformSafeRelativePath = utils.getPlatformSafeFileEntryPath(relativePath); - return new FileProjectEntry(Uri.file(path.join(this.projectFolderPath, platformSafeRelativePath)), relativePath, entryType); + return new FileProjectEntry(Uri.file(path.join(this.projectFolderPath, platformSafeRelativePath)), relativePath, entryType, sqlObjectType); } private findOrCreateItemGroup(containedTag?: string, prePostScriptExist?: { scriptExist: boolean; }): any { @@ -480,6 +490,7 @@ export class Project { if (!outputItemGroup) { outputItemGroup = this.projFileXmlDoc.createElement(constants.ItemGroup); this.projFileXmlDoc.documentElement.appendChild(outputItemGroup); + if (prePostScriptExist) { prePostScriptExist.scriptExist = false; } @@ -488,12 +499,13 @@ export class Project { return outputItemGroup; } - private addFileToProjFile(path: string, xmlTag: string): void { + private addFileToProjFile(path: string, xmlTag: string, attributes?: Map): void { let itemGroup; if (xmlTag === constants.PreDeploy || xmlTag === constants.PostDeploy) { let prePostScriptExist = { scriptExist: true }; itemGroup = this.findOrCreateItemGroup(xmlTag, prePostScriptExist); + if (prePostScriptExist.scriptExist === true) { window.showInformationMessage(constants.deployScriptExists(xmlTag)); xmlTag = constants.None; // Add only one pre-deploy and post-deploy script. All additional ones get added in the same item group with None tag @@ -504,7 +516,15 @@ export class Project { } const newFileNode = this.projFileXmlDoc.createElement(xmlTag); + newFileNode.setAttribute(constants.Include, utils.convertSlashesForSqlProj(path)); + + if (attributes) { + for (const key of attributes.keys()) { + newFileNode.setAttribute(key, attributes.get(key)); + } + } + itemGroup.appendChild(newFileNode); } @@ -530,12 +550,14 @@ export class Project { private removeNode(includeString: string, nodes: any): boolean { for (let i = 0; i < nodes.length; i++) { const parent = nodes[i].parentNode; + if (nodes[i].getAttribute(constants.Include) === utils.convertSlashesForSqlProj(includeString)) { parent.removeChild(nodes[i]); // delete ItemGroup if this was the only entry // only want element nodes, not text nodes const otherChildren = Array.from(parent.childNodes).filter((c: any) => c.childNodes); + if (otherChildren.length === 0) { parent.parentNode.removeChild(parent); } @@ -809,10 +831,10 @@ export class Project { } } - private async addToProjFile(entry: ProjectEntry, xmlTag?: string): Promise { + private async addToProjFile(entry: ProjectEntry, xmlTag?: string, attributes?: Map): Promise { switch (entry.type) { case EntryType.File: - this.addFileToProjFile((entry).relativePath, xmlTag ? xmlTag : constants.Build); + this.addFileToProjFile((entry).relativePath, xmlTag ? xmlTag : constants.Build, attributes); break; case EntryType.Folder: this.addFolderToProjFile((entry).relativePath); @@ -901,11 +923,13 @@ export class FileProjectEntry extends ProjectEntry { */ fsUri: Uri; relativePath: string; + sqlObjectType: string | undefined; - constructor(uri: Uri, relativePath: string, type: EntryType) { - super(type); + constructor(uri: Uri, relativePath: string, entryType: EntryType, sqlObjectType?: string) { + super(entryType); this.fsUri = uri; this.relativePath = relativePath; + this.sqlObjectType = sqlObjectType; } public toString(): string { diff --git a/extensions/sql-database-projects/src/models/tree/fileFolderTreeItem.ts b/extensions/sql-database-projects/src/models/tree/fileFolderTreeItem.ts index cab4cf05fb..a25838eccd 100644 --- a/extensions/sql-database-projects/src/models/tree/fileFolderTreeItem.ts +++ b/extensions/sql-database-projects/src/models/tree/fileFolderTreeItem.ts @@ -71,6 +71,15 @@ export class FileNode extends BaseProjectTreeItem { } } +export class ExternalStreamingJobFileNode extends FileNode { + public get treeItem(): vscode.TreeItem { + const treeItem = super.treeItem; + treeItem.contextValue = DatabaseProjectItemType.externalStreamingJob; + + return treeItem; + } +} + /** * Compares two folder/file tree nodes so that folders come before files, then alphabetically * @param a a folder or file tree node diff --git a/extensions/sql-database-projects/src/models/tree/projectTreeItem.ts b/extensions/sql-database-projects/src/models/tree/projectTreeItem.ts index 2c02b2e54e..3aff573639 100644 --- a/extensions/sql-database-projects/src/models/tree/projectTreeItem.ts +++ b/extensions/sql-database-projects/src/models/tree/projectTreeItem.ts @@ -11,7 +11,7 @@ import * as fileTree from './fileFolderTreeItem'; import { Project, EntryType, FileProjectEntry } from '../project'; import * as utils from '../../common/utils'; import { DatabaseReferencesTreeItem } from './databaseReferencesTreeItem'; -import { DatabaseProjectItemType, RelativeOuterPath } from '../../common/constants'; +import { DatabaseProjectItemType, RelativeOuterPath, ExternalStreamingJob } from '../../common/constants'; import { IconPathHelper } from '../../common/iconHelper'; /** @@ -76,7 +76,13 @@ export class ProjectRootTreeItem extends BaseProjectTreeItem { switch (entry.type) { case EntryType.File: - newNode = new fileTree.FileNode(entry.fsUri, parentNode); + if (entry.sqlObjectType === ExternalStreamingJob) { + newNode = new fileTree.ExternalStreamingJobFileNode(entry.fsUri, parentNode); + } + else { + newNode = new fileTree.FileNode(entry.fsUri, parentNode); + } + break; case EntryType.Folder: newNode = new fileTree.FolderNode(entry.fsUri, parentNode); diff --git a/extensions/sql-database-projects/src/templates/templates.ts b/extensions/sql-database-projects/src/templates/templates.ts index 31ebc9f52e..2427083bbd 100644 --- a/extensions/sql-database-projects/src/templates/templates.ts +++ b/extensions/sql-database-projects/src/templates/templates.ts @@ -15,6 +15,8 @@ export const script: string = 'script'; export const table: string = 'table'; export const view: string = 'view'; export const storedProcedure: string = 'storedProcedure'; +export const externalStreamingJob: string = 'externalStreamingJob'; + export const folder: string = 'folder'; export const preDeployScript: string = 'preDeployScript'; export const postDeployScript: string = 'postDeployScript'; @@ -49,7 +51,8 @@ export async function loadTemplates(templateFolderPath: string) { loadObjectTypeInfo(view, constants.viewFriendlyName, templateFolderPath, 'newTsqlViewTemplate.sql'), loadObjectTypeInfo(storedProcedure, constants.storedProcedureFriendlyName, templateFolderPath, 'newTsqlStoredProcedureTemplate.sql'), loadObjectTypeInfo(preDeployScript, constants.preDeployScriptFriendlyName, templateFolderPath, 'newTsqlPreDeployScriptTemplate.sql'), - loadObjectTypeInfo(postDeployScript, constants.postDeployScriptFriendlyName, templateFolderPath, 'newTsqlPostDeployScriptTemplate.sql') + loadObjectTypeInfo(postDeployScript, constants.postDeployScriptFriendlyName, templateFolderPath, 'newTsqlPostDeployScriptTemplate.sql'), + loadObjectTypeInfo(externalStreamingJob, constants.externalStreamingJobFriendlyName, templateFolderPath, 'newTsqlExternalStreamingJobTemplate.sql') ]); for (const scriptType of scriptTypes) { diff --git a/extensions/sql-database-projects/src/test/baselines/openSqlProjectBaseline.xml b/extensions/sql-database-projects/src/test/baselines/openSqlProjectBaseline.xml index 149e8d1dd8..463e70c0a7 100644 --- a/extensions/sql-database-projects/src/test/baselines/openSqlProjectBaseline.xml +++ b/extensions/sql-database-projects/src/test/baselines/openSqlProjectBaseline.xml @@ -74,6 +74,9 @@ + + + MyProdDatabase diff --git a/extensions/sql-database-projects/src/test/baselines/openSqlProjectWithAdditionalSqlCmdVariablesBaseline.xml b/extensions/sql-database-projects/src/test/baselines/openSqlProjectWithAdditionalSqlCmdVariablesBaseline.xml index 28559b2a0c..e6c341211a 100644 --- a/extensions/sql-database-projects/src/test/baselines/openSqlProjectWithAdditionalSqlCmdVariablesBaseline.xml +++ b/extensions/sql-database-projects/src/test/baselines/openSqlProjectWithAdditionalSqlCmdVariablesBaseline.xml @@ -74,6 +74,9 @@ + + + MyBackupDatabase diff --git a/extensions/sql-database-projects/src/test/project.test.ts b/extensions/sql-database-projects/src/test/project.test.ts index 9124980fae..492a62dfc5 100644 --- a/extensions/sql-database-projects/src/test/project.test.ts +++ b/extensions/sql-database-projects/src/test/project.test.ts @@ -31,12 +31,14 @@ describe('Project: sqlproj content operations', function (): void { const project: Project = await Project.openProject(projFilePath); // Files and folders - should(project.files.filter(f => f.type === EntryType.File).length).equal(5); + should(project.files.filter(f => f.type === EntryType.File).length).equal(6); should(project.files.filter(f => f.type === EntryType.Folder).length).equal(4); should(project.files.find(f => f.type === EntryType.Folder && f.relativePath === 'Views\\User')).not.equal(undefined); // mixed ItemGroup folder should(project.files.find(f => f.type === EntryType.File && f.relativePath === 'Views\\User\\Profile.sql')).not.equal(undefined); // mixed ItemGroup file should(project.files.find(f => f.type === EntryType.File && f.relativePath === '..\\Test\\Test.sql')).not.equal(undefined); // mixed ItemGroup file + should(project.files.find(f => f.type === EntryType.File && f.relativePath === 'MyExternalStreamingJob.sql')).not.equal(undefined); // entry with custom attribute + // SqlCmdVariables should(Object.keys(project.sqlCmdVariables).length).equal(2); @@ -94,20 +96,26 @@ describe('Project: sqlproj content operations', function (): void { const project = await Project.openProject(projFilePath); const folderPath = 'Stored Procedures'; - const filePath = path.join(folderPath, 'Fake Stored Proc.sql'); - const fileContents = 'SELECT \'This is not actually a stored procedure.\''; + const scriptPath = path.join(folderPath, 'Fake Stored Proc.sql'); + const scriptContents = 'SELECT \'This is not actually a stored procedure.\''; + + const scriptPathTagged = path.join(folderPath, 'Fake External Streaming Job.sql'); + const scriptContentsTagged = 'EXEC sys.sp_create_streaming_job \'job\', \'SELECT 7\''; await project.addFolderItem(folderPath); - await project.addScriptItem(filePath, fileContents); + await project.addScriptItem(scriptPath, scriptContents); + await project.addScriptItem(scriptPathTagged, scriptContentsTagged, templates.externalStreamingJob); const newProject = await Project.openProject(projFilePath); should(newProject.files.find(f => f.type === EntryType.Folder && f.relativePath === convertSlashesForSqlProj(folderPath))).not.equal(undefined); - should(newProject.files.find(f => f.type === EntryType.File && f.relativePath === convertSlashesForSqlProj(filePath))).not.equal(undefined); + should(newProject.files.find(f => f.type === EntryType.File && f.relativePath === convertSlashesForSqlProj(scriptPath))).not.equal(undefined); + should(newProject.files.find(f => f.type === EntryType.File && f.relativePath === convertSlashesForSqlProj(scriptPathTagged))).not.equal(undefined); + should(newProject.files.find(f => f.type === EntryType.File && f.relativePath === convertSlashesForSqlProj(scriptPathTagged))?.sqlObjectType).equal(constants.ExternalStreamingJob); - const newFileContents = (await fs.readFile(path.join(newProject.projectFolderPath, filePath))).toString(); + const newScriptContents = (await fs.readFile(path.join(newProject.projectFolderPath, scriptPath))).toString(); - should(newFileContents).equal(fileContents); + should(newScriptContents).equal(scriptContents); }); it('Should add Folder and Build entries to sqlproj with pre-existing scripts on disk', async function (): Promise { diff --git a/extensions/sql-database-projects/src/test/projectController.test.ts b/extensions/sql-database-projects/src/test/projectController.test.ts index bc08dc3bfb..e2904624ac 100644 --- a/extensions/sql-database-projects/src/test/projectController.test.ts +++ b/extensions/sql-database-projects/src/test/projectController.test.ts @@ -85,7 +85,7 @@ describe('ProjectsController', function (): void { const project = await projController.openProject(vscode.Uri.file(sqlProjPath)); - should(project.files.length).equal(9); // detailed sqlproj tests in their own test file + should(project.files.length).equal(10); // detailed sqlproj tests in their own test file should(project.dataSources.length).equal(3); // detailed datasources tests in their own test file }); diff --git a/extensions/sql-database-projects/src/test/templates.test.ts b/extensions/sql-database-projects/src/test/templates.test.ts index d5de3a9732..9557d87aff 100644 --- a/extensions/sql-database-projects/src/test/templates.test.ts +++ b/extensions/sql-database-projects/src/test/templates.test.ts @@ -23,7 +23,7 @@ describe('Templates: loading templates from disk', function (): void { // check expected counts - const numScriptObjectTypes = 6; + const numScriptObjectTypes = 7; should(templates.projectScriptTypes().length).equal(numScriptObjectTypes); should(Object.keys(templates.projectScriptTypes()).length).equal(numScriptObjectTypes); diff --git a/extensions/sql-database-projects/src/test/testContext.ts b/extensions/sql-database-projects/src/test/testContext.ts index 6e5d4d7556..877a409258 100644 --- a/extensions/sql-database-projects/src/test/testContext.ts +++ b/extensions/sql-database-projects/src/test/testContext.ts @@ -115,6 +115,7 @@ export class MockDacFxService implements mssql.IDacFxService { public generateDeployScript(_: string, __: string, ___: string, ____: azdata.TaskExecutionMode, ______?: Record): Thenable { return Promise.resolve(mockDacFxResult); } public generateDeployPlan(_: string, __: string, ___: string, ____: azdata.TaskExecutionMode): Thenable { return Promise.resolve(mockDacFxResult); } public getOptionsFromProfile(_: string): Thenable { return Promise.resolve(mockDacFxOptionsResult); } + public validateStreamingJob(_: string, __: string): Thenable { return Promise.resolve(mockDacFxResult); } } export function createContext(): TestContext {