Add sqlproj property to trace the origin of the project. (#18670)

* Add sqlproj property to trace the origin of the project.

As part of the database migration process (schema conversion, in particular) we want to be able to tell when converted schemas are being built/deployed to the actual database server. Given that we rely on the SQL Database Projects ADS extension for the compilation/deployment, we don't have too many options other than updating the said extension.

The suggested approach is to make the following changes:
1) Add new property to the sqlproj file (called `DatabaseSource`), which will maintain the origin(s) of the project. The property can contain multiple values (separated by semicolon), in case same project contains objects produced by multiple sources (extract schema, convert from another database, etc.).
2) During build and deploy actions, send the well-known values from the newly added property to the telemetry. We don't want to send any random value of the property, as it may raise some privacy concerns. Instead we define a list of the well-known values that we know do not carry any personal information and send those, if they are specified.

This change adds all necessary APIs to the SQl Database projects extension which will be consumed by our migration extensions to populate new `DatabaseSource` property.

* Use `undefined` instead of `null`

Co-authored-by: Kim Santiago <kisantia@microsoft.com>
This commit is contained in:
Alexander Ivanov
2022-04-18 11:38:10 -07:00
committed by GitHub
parent 20a291334e
commit 0f598dd30b
6 changed files with 374 additions and 3 deletions

View File

@@ -14,7 +14,7 @@ import * as constants from '../common/constants';
import { promises as fs } from 'fs';
import { Project } from '../models/project';
import { exists, convertSlashesForSqlProj } from '../common/utils';
import { exists, convertSlashesForSqlProj, getWellKnownDatabaseSources } from '../common/utils';
import { Uri, window } from 'vscode';
import { IDacpacReferenceSettings, IProjectReferenceSettings, ISystemDatabaseReferenceSettings } from '../models/IDatabaseReferenceSettings';
import { SqlTargetPlatform } from 'sqldbproj';
@@ -1558,6 +1558,118 @@ describe('Project: properties', function (): void {
should(() => project.getDatabaseDefaultCollation())
.throw('Invalid value specified for the property \'DefaultCollation\' in .sqlproj file');
});
it('Should add database source to project property', async function (): Promise<void> {
projFilePath = await testUtils.createTestSqlProjFile(baselines.sqlProjectInvalidCollationBaseline);
const project = await Project.openProject(projFilePath);
// Should add a single database source
await project.addDatabaseSource('test1');
let databaseSourceItems: string[] = project.getDatabaseSourceValues();
should(databaseSourceItems.length).equal(1);
should(databaseSourceItems[0]).equal('test1');
// Should add multiple database sources
await project.addDatabaseSource('test2');
await project.addDatabaseSource('test3');
databaseSourceItems = project.getDatabaseSourceValues();
should(databaseSourceItems.length).equal(3);
should(databaseSourceItems[0]).equal('test1');
should(databaseSourceItems[1]).equal('test2');
should(databaseSourceItems[2]).equal('test3');
// Should not add duplicate database sources
await project.addDatabaseSource('test1');
await project.addDatabaseSource('test2');
await project.addDatabaseSource('test3');
should(databaseSourceItems.length).equal(3);
should(databaseSourceItems[0]).equal('test1');
should(databaseSourceItems[1]).equal('test2');
should(databaseSourceItems[2]).equal('test3');
});
it('Should remove database source from project property', async function (): Promise<void> {
projFilePath = await testUtils.createTestSqlProjFile(baselines.sqlProjectInvalidCollationBaseline);
const project = await Project.openProject(projFilePath);
await project.addDatabaseSource('test1');
await project.addDatabaseSource('test2');
await project.addDatabaseSource('test3');
await project.addDatabaseSource('test4');
let databaseSourceItems: string[] = project.getDatabaseSourceValues();
should(databaseSourceItems.length).equal(4);
// Should remove database sources
await project.removeDatabaseSource('test2');
await project.removeDatabaseSource('test1');
await project.removeDatabaseSource('test4');
databaseSourceItems = project.getDatabaseSourceValues();
should(databaseSourceItems.length).equal(1);
should(databaseSourceItems[0]).equal('test3');
// Should remove database source tag when last database source is removed
await project.removeDatabaseSource('test3');
databaseSourceItems = project.getDatabaseSourceValues();
should(databaseSourceItems.length).equal(0);
});
it('Should add and remove values from project properties according to specified case sensitivity', async function (): Promise<void> {
projFilePath = await testUtils.createTestSqlProjFile(baselines.sqlProjectInvalidCollationBaseline);
const project = await Project.openProject(projFilePath);
const propertyName = 'TestProperty';
// Should add value to collection
await project['addValueToCollectionProjectProperty'](propertyName, 'test');
should(project['evaluateProjectPropertyValue'](propertyName)).equal('test');
// Should not allow duplicates of different cases when comparing case insitively
await project['addValueToCollectionProjectProperty'](propertyName, 'TEST');
should(project['evaluateProjectPropertyValue'](propertyName)).equal('test');
// Should allow duplicates of differnt cases when comparing case sensitively
await project['addValueToCollectionProjectProperty'](propertyName, 'TEST', true);
should(project['evaluateProjectPropertyValue'](propertyName)).equal('test;TEST');
// Should remove values case insesitively
await project['removeValueFromCollectionProjectProperty'](propertyName, 'Test');
should(project['evaluateProjectPropertyValue'](propertyName)).equal('TEST');
// Should remove values case sensitively
await project['removeValueFromCollectionProjectProperty'](propertyName, 'Test', true);
should(project['evaluateProjectPropertyValue'](propertyName)).equal('TEST');
await project['removeValueFromCollectionProjectProperty'](propertyName, 'TEST', true);
should(project['evaluateProjectPropertyValue'](propertyName)).equal(undefined);
});
it('Should only return well known database strings when getWellKnownDatabaseSourceString function is called', async function (): Promise<void> {
projFilePath = await testUtils.createTestSqlProjFile(baselines.sqlProjectInvalidCollationBaseline);
const project = await Project.openProject(projFilePath);
await project.addDatabaseSource('test1');
await project.addDatabaseSource('test2');
await project.addDatabaseSource('test3');
await project.addDatabaseSource(constants.WellKnownDatabaseSources[0]);
should(getWellKnownDatabaseSources(project.getDatabaseSourceValues()).length).equal(1);
should(getWellKnownDatabaseSources(project.getDatabaseSourceValues())[0]).equal(constants.WellKnownDatabaseSources[0]);
});
it('Should throw error when adding or removing database source that contains semicolon', async function (): Promise<void> {
projFilePath = await testUtils.createTestSqlProjFile(baselines.sqlProjectInvalidCollationBaseline);
const project = await Project.openProject(projFilePath);
const semicolon = ';';
await testUtils.shouldThrowSpecificError(
async () => await project.addDatabaseSource(semicolon),
constants.invalidProjectPropertyValueProvided(semicolon));
await testUtils.shouldThrowSpecificError(
async () => await project.removeDatabaseSource(semicolon),
constants.invalidProjectPropertyValueProvided(semicolon));
});
});
describe('Project: round trip updates', function (): void {