diff --git a/docs/4.1.2-release-notes.md b/docs/4.1.2-release-notes.md new file mode 100644 index 0000000..75c8007 --- /dev/null +++ b/docs/4.1.2-release-notes.md @@ -0,0 +1,6 @@ +### Features +- Validate that configure sql schema exists in verify connection + +### Fixes +- Fallback to default schema if empty string is specified +- Fix issue where entityreferences properties were not serialized correctly diff --git a/src/Connector.SqlServer/Connector/ISqlClient.cs b/src/Connector.SqlServer/Connector/ISqlClient.cs index 8ff3a31..f9d34b6 100644 --- a/src/Connector.SqlServer/Connector/ISqlClient.cs +++ b/src/Connector.SqlServer/Connector/ISqlClient.cs @@ -11,6 +11,8 @@ public interface ISqlClient { bool VerifyConnectionProperties(IReadOnlyDictionary config, out ConnectionConfigurationError configurationError); + Task VerifySchemaExists(SqlTransaction transaction, string schema); + Task BeginConnection(IReadOnlyDictionary config); Task GetTableColumns(SqlConnection connection, string tableName, string schema); diff --git a/src/Connector.SqlServer/Connector/SqlClient.cs b/src/Connector.SqlServer/Connector/SqlClient.cs index 7508788..9a791b8 100644 --- a/src/Connector.SqlServer/Connector/SqlClient.cs +++ b/src/Connector.SqlServer/Connector/SqlClient.cs @@ -23,6 +23,9 @@ public string BuildConnectionString(IReadOnlyDictionary config) DataSource = (string)config[SqlServerConstants.KeyName.Host], InitialCatalog = (string)config[SqlServerConstants.KeyName.DatabaseName], Pooling = true, + // Turn off unconditionally for now. Later maybe should be coming from configuration. + // Is needed as new SqlClient library encrypts by default. + Encrypt = false }; // Configure port @@ -93,6 +96,21 @@ public bool VerifyConnectionProperties(IReadOnlyDictionary confi return true; } + public async Task VerifySchemaExists(SqlTransaction transaction, string schema) + { + // INFORMATION_SCHEMA.SCHEMATA contains all the views accessible to the current user in SQL Server. + var schemaQuery = $"SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = '{schema}'"; + + var command = transaction.Connection.CreateCommand(); + command.CommandText = schemaQuery; + command.Transaction = transaction; + + await using (var reader = await command.ExecuteReaderAsync()) + { + return reader.HasRows; + } + } + public async Task BeginConnection(IReadOnlyDictionary config) { var connectionString = BuildConnectionString(config); diff --git a/src/Connector.SqlServer/Connector/SqlServerConnector.cs b/src/Connector.SqlServer/Connector/SqlServerConnector.cs index c976cdc..f350a59 100644 --- a/src/Connector.SqlServer/Connector/SqlServerConnector.cs +++ b/src/Connector.SqlServer/Connector/SqlServerConnector.cs @@ -315,9 +315,31 @@ public override async Task VerifyConnection(Execut await using var connectionAndTransaction = await _client.BeginTransaction(configurationData); var connectionIsOpen = connectionAndTransaction.Connection.State == ConnectionState.Open; + + if (!connectionIsOpen) + { + _logger.LogError("SqlServerConnector connection verification failed, connection could not be opened"); + return new ConnectionVerificationResult(false, "Connection could not be opened"); + } + + var schema = configurationData.GetValue(SqlServerConstants.KeyName.Schema, (string)null); + if (string.IsNullOrEmpty(schema)) + { + schema = SqlTableName.DefaultSchema; + } + + + var schemaExists = await _client.VerifySchemaExists(connectionAndTransaction.Transaction, schema); + await connectionAndTransaction.DisposeAsync(); - return new ConnectionVerificationResult(connectionIsOpen); + if (!schemaExists) + { + _logger.LogError("SqlServerConnector connection verification failed, schema '{schema}' does not exist", schema); + return new ConnectionVerificationResult(false, "Schema does not exist"); + } + + return new ConnectionVerificationResult(true); } catch (Exception e) { diff --git a/src/Connector.SqlServer/Utils/ConnectorConnectionExtensions.cs b/src/Connector.SqlServer/Utils/ConnectorConnectionExtensions.cs index ebb04de..8cc4edb 100644 --- a/src/Connector.SqlServer/Utils/ConnectorConnectionExtensions.cs +++ b/src/Connector.SqlServer/Utils/ConnectorConnectionExtensions.cs @@ -9,14 +9,12 @@ internal static class ConnectorConnectionExtensions /// public static SqlName GetSchema(this IConnectorConnectionV2 config) { - if (config.Authentication.TryGetValue(SqlServerConstants.KeyName.Schema, out var value) && value is string schema) + if (config.Authentication.TryGetValue(SqlServerConstants.KeyName.Schema, out var value) && + value is string schema && + !string.IsNullOrEmpty(schema)) { var sanitizedSchema = schema.ToSanitizedSqlName(); - - if (!string.IsNullOrEmpty(sanitizedSchema)) - { - return SqlName.FromSanitized(schema); - } + return SqlName.FromSanitized(sanitizedSchema); } return SqlName.FromSanitized(SqlTableName.DefaultSchema); diff --git a/src/Connector.SqlServer/Utils/TableDefinitions/MainTableDefinition.cs b/src/Connector.SqlServer/Utils/TableDefinitions/MainTableDefinition.cs index 0fdcb6a..2b55c11 100644 --- a/src/Connector.SqlServer/Utils/TableDefinitions/MainTableDefinition.cs +++ b/src/Connector.SqlServer/Utils/TableDefinitions/MainTableDefinition.cs @@ -94,6 +94,14 @@ public static MainTableColumnDefinition[] GetColumnDefinitions(StreamMode stream return entityTypeValue.ToString(); } + if (propertyValue is EntityReference entityReference) + { + var codeString = entityReference.Code?.ToString(); + return !string.IsNullOrEmpty(codeString) + ? codeString + : entityReference.Name; + } + return propertyValue; }, CanBeNull: true); diff --git a/test/unit/Connector.SqlServer.Test/Connector/SqlClientTests.cs b/test/unit/Connector.SqlServer.Test/Connector/SqlClientTests.cs index e9b2f9f..1b8149f 100644 --- a/test/unit/Connector.SqlServer.Test/Connector/SqlClientTests.cs +++ b/test/unit/Connector.SqlServer.Test/Connector/SqlClientTests.cs @@ -27,7 +27,7 @@ public void BuildConnectionString_Sets_From_Dictionary() var result = _sut.BuildConnectionString(properties); - Assert.Equal("Data Source=host,1433;Initial Catalog=database;User ID=user;Password=password;Pooling=True;Max Pool Size=200;Authentication=SqlPassword", result); + Assert.Equal("Data Source=host,1433;Initial Catalog=database;User ID=user;Password=password;Pooling=True;Max Pool Size=200;Encrypt=False;Authentication=SqlPassword", result); } [Fact] @@ -44,7 +44,7 @@ public void BuildConnectionString_WithPort_Sets_From_Dictionary() var result = _sut.BuildConnectionString(properties); - Assert.Equal("Data Source=host,9499;Initial Catalog=database;User ID=user;Password=password;Pooling=True;Max Pool Size=200;Authentication=SqlPassword", result); + Assert.Equal("Data Source=host,9499;Initial Catalog=database;User ID=user;Password=password;Pooling=True;Max Pool Size=200;Encrypt=False;Authentication=SqlPassword", result); } [Fact] @@ -61,7 +61,7 @@ public void BuildConnectionString_WithStringPort_Sets_From_Dictionary() var result = _sut.BuildConnectionString(properties); - Assert.Equal("Data Source=host,9499;Initial Catalog=database;User ID=user;Password=password;Pooling=True;Max Pool Size=200;Authentication=SqlPassword", result); + Assert.Equal("Data Source=host,9499;Initial Catalog=database;User ID=user;Password=password;Pooling=True;Max Pool Size=200;Encrypt=False;Authentication=SqlPassword", result); } [Fact] @@ -78,7 +78,7 @@ public void BuildConnectionString_WithInvalidPort_Sets_From_Dictionary() var result = _sut.BuildConnectionString(properties); - Assert.Equal("Data Source=host,1433;Initial Catalog=database;User ID=user;Password=password;Pooling=True;Max Pool Size=200;Authentication=SqlPassword", result); + Assert.Equal("Data Source=host,1433;Initial Catalog=database;User ID=user;Password=password;Pooling=True;Max Pool Size=200;Encrypt=False;Authentication=SqlPassword", result); } [Fact] @@ -98,7 +98,7 @@ public void BuildConnectionString_WithConnectionPoolSize_Sets_From_Dictionary() var result = _sut.BuildConnectionString(properties); // assert - Assert.Equal("Data Source=host,1433;Initial Catalog=database;User ID=user;Password=password;Pooling=True;Max Pool Size=10;Authentication=SqlPassword", result); + Assert.Equal("Data Source=host,1433;Initial Catalog=database;User ID=user;Password=password;Pooling=True;Max Pool Size=10;Encrypt=False;Authentication=SqlPassword", result); } [Fact] public void VerifyConnectionProperties_WithValidProperties_ReturnsTrue() diff --git a/test/unit/Connector.SqlServer.Test/Utils/TableDefinitions/MainTableDefinitionTests.cs b/test/unit/Connector.SqlServer.Test/Utils/TableDefinitions/MainTableDefinitionTests.cs index ac62a62..3d892ec 100644 --- a/test/unit/Connector.SqlServer.Test/Utils/TableDefinitions/MainTableDefinitionTests.cs +++ b/test/unit/Connector.SqlServer.Test/Utils/TableDefinitions/MainTableDefinitionTests.cs @@ -204,9 +204,62 @@ public void EntityTypePropertyValues_ShouldBeMadeIntoStrings( var syncColumnDefinitions = MainTableDefinition.GetColumnDefinitions(StreamMode.Sync, properties); // assert - var discoveryDateColumnDefinition = syncColumnDefinitions.Where(column => column.Name == "Type").Should().ContainSingle().And.Subject.First(); - var sqlDateValue = discoveryDateColumnDefinition.GetValueFunc(sqlEntityTypePropertyDate); + var entityTypeColumnDefinition = syncColumnDefinitions.Where(column => column.Name == "Type").Should().ContainSingle().And.Subject.First(); + var sqlDateValue = entityTypeColumnDefinition.GetValueFunc(sqlEntityTypePropertyDate); sqlDateValue.Should().Be("/Person"); } + + [Theory, AutoNData] + public void PersonReferencePropertyValues_ShouldBeMadeIntoStrings( + VersionChangeType versionChangeType, + Guid entityId, + Guid correlationId) + { + // arrange + var properties = new (string, ConnectorPropertyDataType)[] + { + ("LastChangedBy", new EntityPropertyConnectorPropertyDataType(typeof(PersonReference))), + }; + var personReferenceValue = new PersonReference("PersonName"); + + var personReferencePropertyData = new ConnectorPropertyData("LastChangedBy", personReferenceValue, new EntityPropertyConnectorPropertyDataType(typeof(EntityType))); + var connectorEntityData = new ConnectorEntityData(versionChangeType, StreamMode.Sync, entityId, null, null, null, null, new[] { personReferencePropertyData }, Array.Empty(), Array.Empty(), Array.Empty()); + var sqlEntityTypePropertyDate = new SqlConnectorEntityData(connectorEntityData, correlationId, timestamp: DateTimeOffset.Now); + + // act + var syncColumnDefinitions = MainTableDefinition.GetColumnDefinitions(StreamMode.Sync, properties); + + // assert + var personReferenceColumnDefinition = syncColumnDefinitions.Where(column => column.Name == "LastChangedBy").Should().ContainSingle().And.Subject.First(); + var sqlDataValue = personReferenceColumnDefinition.GetValueFunc(sqlEntityTypePropertyDate); + sqlDataValue.Should().Be("PersonName"); + } + + [Theory, AutoNData] + public void EntityReferencePropertyValues_ShouldBeMadeIntoStrings( + VersionChangeType versionChangeType, + Guid entityId, + Guid correlationId) + { + // arrange + var properties = new (string, ConnectorPropertyDataType)[] + { + ("LastChangedBy", new EntityPropertyConnectorPropertyDataType(typeof(EntityReference))), + }; + var entityCode = new EntityCode(EntityType.Person, CodeOrigin.CluedIn, "PersonName"); + var entityReferenceValue = new EntityReference(entityCode); + + var entityReferencePropertyData = new ConnectorPropertyData("LastChangedBy", entityReferenceValue, new EntityPropertyConnectorPropertyDataType(typeof(EntityType))); + var connectorEntityData = new ConnectorEntityData(versionChangeType, StreamMode.Sync, entityId, null, null, null, null, new[] { entityReferencePropertyData }, Array.Empty(), Array.Empty(), Array.Empty()); + var sqlEntityTypePropertyDate = new SqlConnectorEntityData(connectorEntityData, correlationId, timestamp: DateTimeOffset.Now); + + // act + var syncColumnDefinitions = MainTableDefinition.GetColumnDefinitions(StreamMode.Sync, properties); + + // assert + var entityReferenceColumnDefinition = syncColumnDefinitions.Where(column => column.Name == "LastChangedBy").Should().ContainSingle().And.Subject.First(); + var sqlDataValue = entityReferenceColumnDefinition.GetValueFunc(sqlEntityTypePropertyDate); + sqlDataValue.Should().Be("/Person#CluedIn:PersonName"); + } } }