diff --git a/extensions/dacpac/src/test/testDacFxService.ts b/extensions/dacpac/src/test/testDacFxService.ts index d87383ff12..16b5969b30 100644 --- a/extensions/dacpac/src/test/testDacFxService.ts +++ b/extensions/dacpac/src/test/testDacFxService.ts @@ -170,4 +170,7 @@ export class DacFxTestService implements mssql.IDacFxService { }; return Promise.resolve(streamingJobValidationResult); } + parseTSqlScript(filePath: string, databaseSchemaProvider: string): Thenable { + return Promise.resolve({ containsCreateTableStatement: true }); + } } diff --git a/extensions/mssql/config.json b/extensions/mssql/config.json index 4013a4f020..4bbd81246b 100644 --- a/extensions/mssql/config.json +++ b/extensions/mssql/config.json @@ -1,6 +1,6 @@ { "downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/{#version#}/microsoft.sqltools.servicelayer-{#fileName#}", - "version": "4.1.0.4", + "version": "4.1.0.9", "downloadFileNames": { "Windows_86": "win-x86-net6.0.zip", "Windows_64": "win-x64-net6.0.zip", diff --git a/extensions/mssql/src/contracts.ts b/extensions/mssql/src/contracts.ts index 34054babe9..e0c415eebc 100644 --- a/extensions/mssql/src/contracts.ts +++ b/extensions/mssql/src/contracts.ts @@ -545,6 +545,11 @@ export interface ValidateStreamingJobParams { createStreamingJobTsql: string } +export interface ParseTSqlScriptParams { + filePath: string; + databaseSchemaProvider: string; +} + export namespace ExportRequest { export const type = new RequestType('dacfx/export'); } @@ -577,6 +582,10 @@ export namespace ValidateStreamingJobRequest { export const type = new RequestType('dacfx/validateStreamingJob'); } +export namespace ParseTSqlScriptRequest { + export const type = new RequestType('dacfx/parseTSqlScript'); +} + // ------------------------------- ------------------------------------ // ------------------------------- ---------------------------------------- diff --git a/extensions/mssql/src/dacfx/dacFxService.ts b/extensions/mssql/src/dacfx/dacFxService.ts index 11634c6089..0f6b7d1a6c 100644 --- a/extensions/mssql/src/dacfx/dacFxService.ts +++ b/extensions/mssql/src/dacfx/dacFxService.ts @@ -130,4 +130,15 @@ export class DacFxService implements mssql.IDacFxService { } ); } + + public async parseTSqlScript(filePath: string, databaseSchemaProvider: string): Promise { + const params: contracts.ParseTSqlScriptParams = { filePath, databaseSchemaProvider }; + try { + const result = await this.client.sendRequest(contracts.ParseTSqlScriptRequest.type, params); + return result; + } catch (e) { + this.client.logFailedRequest(contracts.ParseTSqlScriptRequest.type, e); + throw e; + } + } } diff --git a/extensions/mssql/src/mssql.d.ts b/extensions/mssql/src/mssql.d.ts index 5b6b46e05f..fb8ec23ea1 100644 --- a/extensions/mssql/src/mssql.d.ts +++ b/extensions/mssql/src/mssql.d.ts @@ -391,6 +391,7 @@ declare module 'mssql' { generateDeployPlan(packageFilePath: string, databaseName: string, ownerUri: string, taskExecutionMode: azdata.TaskExecutionMode): Thenable; getOptionsFromProfile(profilePath: string): Thenable; validateStreamingJob(packageFilePath: string, createStreamingJobTsql: string): Thenable; + parseTSqlScript(filePath: string, databaseSchemaProvider: string): Thenable; } export interface DacFxResult extends azdata.ResultStatus { @@ -408,6 +409,10 @@ declare module 'mssql' { export interface ValidateStreamingJobResult extends azdata.ResultStatus { } + export interface ParseTSqlScriptResult { + containsCreateTableStatement: boolean; + } + export interface ExportParams { databaseName: string; packageFilePath: string; diff --git a/extensions/sql-database-projects/src/models/project.ts b/extensions/sql-database-projects/src/models/project.ts index bdfc3a5f4b..ce317aa992 100644 --- a/extensions/sql-database-projects/src/models/project.ts +++ b/extensions/sql-database-projects/src/models/project.ts @@ -10,6 +10,7 @@ import * as utils from '../common/utils'; import * as xmlFormat from 'xml-formatter'; import * as os from 'os'; import * as UUID from 'vscode-languageclient/lib/utils/uuid'; +import * as mssql from 'mssql'; import { Uri, window } from 'vscode'; import { EntryType, IDatabaseReferenceProjectEntry, IProjectEntry, ISqlProject, ItemType, SqlTargetPlatform } from 'sqldbproj'; @@ -247,14 +248,19 @@ export class Project implements ISqlProject { const fileEntries: FileProjectEntry[] = []; for (let f of Array.from(filesSet.values())) { const typeEntry = entriesWithType.find(e => e.relativePath === f); - let containsCreateTableStatement; + let containsCreateTableStatement = false; // read file to check if it has a "Create Table" statement const fullPath = path.join(utils.getPlatformSafeFileEntryPath(this.projectFolderPath), utils.getPlatformSafeFileEntryPath(f)); - if (await utils.exists(fullPath)) { - const fileContents = await fs.readFile(fullPath); - containsCreateTableStatement = fileContents.toString().toLowerCase().includes('create table'); + if (utils.getAzdataApi() && await utils.exists(fullPath)) { + const dacFxService = await utils.getDacFxService() as mssql.IDacFxService; + try { + const result = await dacFxService.parseTSqlScript(fullPath, this.getProjectTargetVersion()); + containsCreateTableStatement = result.containsCreateTableStatement; + } catch (e) { + console.error(utils.getErrorMessage(e)); + } } fileEntries.push(this.createFileProjectEntry(f, EntryType.File, typeEntry ? typeEntry.typeAttribute : undefined, containsCreateTableStatement)); diff --git a/extensions/sql-database-projects/src/test/dialogs/publishDatabaseDialog.test.ts b/extensions/sql-database-projects/src/test/dialogs/publishDatabaseDialog.test.ts index aade45731a..de937e35f7 100644 --- a/extensions/sql-database-projects/src/test/dialogs/publishDatabaseDialog.test.ts +++ b/extensions/sql-database-projects/src/test/dialogs/publishDatabaseDialog.test.ts @@ -39,7 +39,7 @@ describe('Publish Database Dialog', () => { sdkStyle: false }); - const project = new Project(projFilePath); + const project = await Project.openProject(projFilePath); const publishDatabaseDialog = new PublishDatabaseDialog(project); publishDatabaseDialog.openDialog(); should.notEqual(publishDatabaseDialog.publishTab, undefined); diff --git a/extensions/sql-database-projects/src/test/dialogs/publishOptionsDialog.test.ts b/extensions/sql-database-projects/src/test/dialogs/publishOptionsDialog.test.ts index 683c6d00ec..1437774081 100644 --- a/extensions/sql-database-projects/src/test/dialogs/publishOptionsDialog.test.ts +++ b/extensions/sql-database-projects/src/test/dialogs/publishOptionsDialog.test.ts @@ -12,6 +12,7 @@ import * as TypeMoq from 'typemoq'; import { PublishOptionsDialog } from '../../dialogs/publishOptionsDialog'; import { PublishDatabaseDialog } from '../../dialogs/publishDatabaseDialog'; import { Project } from '../../models/project'; +import sinon = require('sinon'); describe('Publish Database Options Dialog', () => { before(async function (): Promise { @@ -19,6 +20,8 @@ describe('Publish Database Options Dialog', () => { }); it('Should open dialog successfully ', async function (): Promise { + const proj = new Project(''); + sinon.stub(proj, 'getProjectTargetVersion').returns('150'); const publishDatabaseDialog = new PublishDatabaseDialog(new Project('')); const optionsDialog = new PublishOptionsDialog(testData.getDeploymentOptions(), publishDatabaseDialog); optionsDialog.openDialog(); diff --git a/extensions/sql-database-projects/src/test/projectController.test.ts b/extensions/sql-database-projects/src/test/projectController.test.ts index f2abc4fd99..1caf7e9027 100644 --- a/extensions/sql-database-projects/src/test/projectController.test.ts +++ b/extensions/sql-database-projects/src/test/projectController.test.ts @@ -366,28 +366,36 @@ describe('ProjectsController', function (): void { let projController = TypeMoq.Mock.ofType(ProjectsController); projController.callBase = true; projController.setup(x => x.getPublishDialog(TypeMoq.It.isAny())).returns(() => publishDialog.object); - - void projController.object.publishProject(new Project('FakePath')); + const proj = new Project('FakePath'); + sinon.stub(proj, 'getProjectTargetVersion').returns('150'); + void projController.object.publishProject(proj); should(opened).equal(true); }); it('Callbacks are hooked up and called from Publish dialog', async function (): Promise { - const projPath = path.dirname(await testUtils.createTestSqlProjFile(baselines.openProjectFileBaseline)); - await testUtils.createTestDataSources(baselines.openDataSourcesBaseline, projPath); - const proj = new Project(projPath); + const projectFile = await testUtils.createTestSqlProjFile(baselines.openProjectFileBaseline) + const projFolder = path.dirname(projectFile); + await testUtils.createTestDataSources(baselines.openDataSourcesBaseline, projFolder); + const proj = await Project.openProject(projectFile); const publishHoller = 'hello from callback for publish()'; const generateHoller = 'hello from callback for generateScript()'; let holler = 'nothing'; - let publishDialog = TypeMoq.Mock.ofType(PublishDatabaseDialog, undefined, undefined, proj); - publishDialog.callBase = true; - publishDialog.setup(x => x.getConnectionUri()).returns(() => Promise.resolve('fake|connection|uri')); + const setupPublishDialog = (): PublishDatabaseDialog => { + const dialog = new PublishDatabaseDialog(proj); + sinon.stub(dialog, 'getConnectionUri').returns(Promise.resolve('fake|connection|uri')); + return dialog; + }; + + let publishDialog = setupPublishDialog(); let projController = TypeMoq.Mock.ofType(ProjectsController); projController.callBase = true; - projController.setup(x => x.getPublishDialog(TypeMoq.It.isAny())).returns(() => publishDialog.object); + projController.setup(x => x.getPublishDialog(TypeMoq.It.isAny())).returns(() => { + return publishDialog; + }); projController.setup(x => x.publishOrScriptProject(TypeMoq.It.isAny(), TypeMoq.It.isAny(), true)).returns(() => { holler = publishHoller; return Promise.resolve(undefined); @@ -397,14 +405,15 @@ describe('ProjectsController', function (): void { holler = generateHoller; return Promise.resolve(undefined); }); - publishDialog.object.publishToExistingServer = true; + publishDialog.publishToExistingServer = true; void projController.object.publishProject(proj); - await publishDialog.object.publishClick(); + await publishDialog.publishClick(); should(holler).equal(publishHoller, 'executionCallback() is supposed to have been setup and called for Publish scenario'); + publishDialog = setupPublishDialog(); void projController.object.publishProject(proj); - await publishDialog.object.generateScriptClick(); + await publishDialog.generateScriptClick(); should(holler).equal(generateHoller, 'executionCallback() is supposed to have been setup and called for GenerateScript scenario'); }); @@ -603,6 +612,11 @@ describe('ProjectsController', function (): void { const dataWorkspaceMock = TypeMoq.Mock.ofType(); dataWorkspaceMock.setup(x => x.getProjectsInWorkspace(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve([vscode.Uri.file(project1.projectFilePath), vscode.Uri.file(project2.projectFilePath)])); sinon.stub(vscode.extensions, 'getExtension').returns({ exports: dataWorkspaceMock.object }); + sinon.stub(utils, 'getDacFxService').returns({ + parseTSqlScript: (_: string, __: string) => { + return Promise.resolve({ containsCreateTableStatement: true }); + } + }); // add project reference from project1 to project2 await projController.addDatabaseReferenceCallback(project1, { @@ -633,7 +647,11 @@ describe('ProjectsController', function (): void { const showErrorMessageSpy = sinon.spy(vscode.window, 'showErrorMessage'); const dataWorkspaceMock = TypeMoq.Mock.ofType(); sinon.stub(vscode.extensions, 'getExtension').returns({ exports: dataWorkspaceMock.object }); - + sinon.stub(utils, 'getDacFxService').returns({ + parseTSqlScript: (_: string, __: string) => { + return Promise.resolve({ containsCreateTableStatement: true }); + } + }); // add dacpac reference to something in the same folder should(project1.databaseReferences.length).equal(0, 'There should not be any database references to start with'); diff --git a/extensions/sql-database-projects/src/test/testContext.ts b/extensions/sql-database-projects/src/test/testContext.ts index 5962c7065e..6012e09f67 100644 --- a/extensions/sql-database-projects/src/test/testContext.ts +++ b/extensions/sql-database-projects/src/test/testContext.ts @@ -140,6 +140,7 @@ export class MockDacFxService implements mssql.IDacFxService { 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); } + public parseTSqlScript(_: string, __: string): Thenable { return Promise.resolve({ containsCreateTableStatement: true }); } } export function createContext(): TestContext { diff --git a/extensions/sql-database-projects/src/test/testUtils.ts b/extensions/sql-database-projects/src/test/testUtils.ts index d4ac849726..34af034b43 100644 --- a/extensions/sql-database-projects/src/test/testUtils.ts +++ b/extensions/sql-database-projects/src/test/testUtils.ts @@ -6,6 +6,7 @@ import * as path from 'path'; import * as os from 'os'; import * as constants from '../common/constants'; +import * as templates from '../templates/templates'; import { promises as fs } from 'fs'; import should = require('should'); @@ -30,6 +31,10 @@ export async function shouldThrowSpecificError(block: Function, expectedMessage: export async function createTestSqlProjFile(contents: string, folderPath?: string): Promise { folderPath = folderPath ?? path.join(await generateTestFolderPath(), 'TestProject'); + const macroDict: Record = { + 'PROJECT_DSP': constants.defaultDSP + }; + contents = templates.macroExpansion(contents, macroDict); return await createTestFile(contents, 'TestProject.sqlproj', folderPath); }