diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Data/TestDataConnectionSql.Data.cs b/src/Adapter/MSTestAdapter.PlatformServices/Data/TestDataConnectionSql.Data.cs new file mode 100644 index 0000000000..49cea47eca --- /dev/null +++ b/src/Adapter/MSTestAdapter.PlatformServices/Data/TestDataConnectionSql.Data.cs @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#if NETFRAMEWORK +using System.Data; +using System.Data.Common; + +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; + +using StringEx = Microsoft.VisualStudio.TestTools.UnitTesting.StringEx; + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Data; + +internal partial class TestDataConnectionSql +{ + #region Data + + /// + /// Read a table from the connection, into a DataTable + /// Code used to be in UnitTestDataManager. + /// + /// The table name. + /// Columns. + /// new DataTable. + [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Un-tested. Leaving behavior as is.")] + [SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security", Justification = "Not passed in from the user.")] +#pragma warning disable SA1202 // Elements must be ordered by access + public override DataTable ReadTable(string tableName, IEnumerable? columns) +#pragma warning restore SA1202 // Elements must be ordered by access + { + using DbDataAdapter dataAdapter = Factory.CreateDataAdapter(); + using DbCommand command = Factory.CreateCommand(); + + // We need to escape bad characters in table name like [Sheet1$] in Excel. + // But if table name is quoted in terms of provider, don't touch it to avoid e.g. [dbo.tables.etc]. + string quotedTableName = PrepareNameForSql(tableName); + if (PlatformServiceProvider.Instance.AdapterTraceLogger.IsInfoEnabled) + { + PlatformServiceProvider.Instance.AdapterTraceLogger.Info("ReadTable: data driven test: got table name from attribute: {0}", tableName); + PlatformServiceProvider.Instance.AdapterTraceLogger.Info("ReadTable: data driven test: will use table name: {0}", tableName); + } + + command.Connection = Connection; + command.CommandText = string.Format(CultureInfo.InvariantCulture, "select {0} from {1}", GetColumnsSQL(columns), quotedTableName); + + WriteDiagnostics("ReadTable: SQL Query: {0}", command.CommandText); + dataAdapter.SelectCommand = command; + + DataTable table = new() + { + Locale = CultureInfo.InvariantCulture, + }; + dataAdapter.Fill(table); + + table.TableName = tableName; // Make table name in the data set the same as original table name. + return table; + } + + private string GetColumnsSQL(IEnumerable? columns) + { + string? result = null; + if (columns != null) + { + StringBuilder builder = new(); + foreach (string columnName in columns) + { + if (builder.Length > 0) + { + builder.Append(','); + } + + builder.Append(QuoteIdentifier(columnName)); + } + + result = builder.ToString(); + } + + // Return a valid list of columns, or default to * for all columns + return !StringEx.IsNullOrEmpty(result) ? result : "*"; + } + + #endregion +} + +#endif diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Data/TestDataConnectionSql.Helpers.cs b/src/Adapter/MSTestAdapter.PlatformServices/Data/TestDataConnectionSql.Helpers.cs new file mode 100644 index 0000000000..8a99bf5fc0 --- /dev/null +++ b/src/Adapter/MSTestAdapter.PlatformServices/Data/TestDataConnectionSql.Helpers.cs @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#if NETFRAMEWORK +using System.Data; + +using StringEx = Microsoft.VisualStudio.TestTools.UnitTesting.StringEx; + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Data; + +internal partial class TestDataConnectionSql +{ + #region Helpers + +#pragma warning disable SA1202 // Elements must be ordered by access + public bool IsOpen() => _connection is { State: ConnectionState.Open }; + + /// + /// Returns true when given provider (OLEDB or ODBC) is for MSSql. + /// + /// OLEDB or ODBC provider. + /// True if provider is for MSSql. + protected static bool IsMSSql(string providerName) => (!StringEx.IsNullOrEmpty(providerName) && + (providerName.StartsWith(KnownOleDbProviderNames.SqlOleDb, StringComparison.OrdinalIgnoreCase) || + providerName.StartsWith(KnownOleDbProviderNames.MSSqlNative, StringComparison.OrdinalIgnoreCase))) || + string.Equals(providerName, KnownOdbcDrivers.MSSql, StringComparison.OrdinalIgnoreCase); +#pragma warning restore SA1202 // Elements must be ordered by access + + /// + /// Just a helper method to see if a string is in a string array + /// Note that the array can be null, this is treated as an empty array. + /// + /// The string. + /// An array of values. + /// True if string exists in array. + private static bool IsInArray(string? candidate, string[]? values) + { + if (values == null) + { + return false; + } + + foreach (string value in values) + { + if (string.Equals(value, candidate, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + } + + #endregion + + #region Types + + /// + /// When querying for tables, metadata varies quite a bit from DB to DB + /// This struct encapsulates those variations. + /// + protected struct SchemaMetaData + { + // Name of a table containing tables or views + public string? SchemaTable; + + // Column that contains schema names, if null, unused + public string? SchemaColumn; + + // Column that contains the table names + public string? NameColumn; + + // Column that contains a table "type", if null, type is unchecked + public string? TableTypeColumn; + + // If table type is available, it is checked to be one of the values on this list + public string[]? ValidTableTypes; + + // If schema is available, it is checked to not be one of the values on this list + public string[]? InvalidSchemas; + } + + /// + /// Known OLE DB providers for MS SQL and Oracle. + /// + /// + /// How Data Connection dialog maps to different providers: + /// SqlOleDb: Data Source = MS SQL, Provider = OLE DB + /// SqlOleDb.1: Data Source = Other, Provider = Microsoft OLE DB Provider for Sql Server + /// Provider=SQLOLEDB;Data Source=SqlServer;Integrated Security=SSPI;Initial Catalog=DatabaseName + /// SQLNCLI.1: Data Source = Other, Provider = Sql Native Client + /// Provider=SQLNCLI.1;Data Source=SqlServer;Integrated Security=SSPI;Initial Catalog=DatabaseName. + /// + protected static class KnownOleDbProviderNames + { + internal const string SqlOleDb = "SQLOLEDB"; + internal const string MSSqlNative = "SQLNCLI"; + + // Note MSDASQL (OLE DB Provider for ODBC) is not supported by .NET. + } + + /// + /// Known ODBC drivers. + /// + /// + /// sqlsrv32.dll: Driver={SQL Server};Server=SqlServer;Database=DatabaseName;Trusted_Connection=yes + /// msorcl32.dll: Driver={Microsoft ODBC for Oracle};Server=OracleServer;Uid=user;Pwd=password. + /// + protected static class KnownOdbcDrivers + { + internal const string MSSql = "sqlsrv32.dll"; + } + + #endregion +} + +#endif diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Data/TestDataConnectionSql.Names.cs b/src/Adapter/MSTestAdapter.PlatformServices/Data/TestDataConnectionSql.Names.cs new file mode 100644 index 0000000000..cff6d14ab5 --- /dev/null +++ b/src/Adapter/MSTestAdapter.PlatformServices/Data/TestDataConnectionSql.Names.cs @@ -0,0 +1,237 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#if NETFRAMEWORK +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Data; + +internal partial class TestDataConnectionSql +{ + /// + /// Take a possibly qualified name with at least minimal quoting + /// and return a fully quoted string + /// Take care to only convert names that are of a recognized form. + /// + /// The table name. + /// A fully quoted string. + public string PrepareNameForSql(string tableName) + { + string[]? parts = SplitName(tableName); + + if (parts is { Length: > 0 }) + { + // Seems to be well formed, so make sure we end up fully quoted + return JoinAndQuoteName(parts, true); + } + else + { + // Just use what they gave us, literally, since we do not really understand the format + return tableName; + } + } + + /// + /// Take a possibly qualified name and break it down into an + /// array of identifiers unquoting any quoted names. + /// + /// A string. + /// An array of unquoted parts, or null if the name fails to conform. + public string[]? SplitName(string name) + { + List parts = []; + + int here = 0; + int end = name.Length; + char firstDelimiter = ' '; // initialize since code analysis is not smart enough + char currentDelimiter; + char catalogSeparatorChar = CatalogSeparatorChar; + char schemaSeparatorChar = SchemaSeparatorChar; + + while (here < end) + { + int next = FindIdentifierEnd(name, here); + string identifier = name.Substring(here, next - here); + + if (StringEx.IsNullOrEmpty(identifier)) + { + // Not well formed, split failed + return null; + } + + if (identifier.StartsWith(QuotePrefix, StringComparison.Ordinal)) + { + identifier = UnquoteIdentifier(identifier); + } + + parts.Add(identifier); + + if (next < end) + { + currentDelimiter = name[next]; + switch (parts.Count) + { + case 1: + // We infer there will be at least 2 parts + firstDelimiter = currentDelimiter; + if (firstDelimiter != catalogSeparatorChar + && firstDelimiter != schemaSeparatorChar) + { + // Not well formed, split failed + return null; + } + + break; + + case 2: + // We infer there will be at least 3 parts + if (firstDelimiter != catalogSeparatorChar + || currentDelimiter != schemaSeparatorChar) + { + // Not well formed, split failed + return null; + } + + break; + + default: + // We infer there will be at least 4 or more parts + // so not well formed, split failed + return null; + } + + // Skip delimiter + here = next + 1; + } + else + { + // We have found the end + if (parts.Count == 2 && firstDelimiter != schemaSeparatorChar) + { + // Not well formed, split failed + return null; + } + + return [.. parts]; + } + } + + // Ended in a delimiter, or no parts at all, either is invalid + return null; + } + + /// + /// Take a list of unquoted name parts and join them into a + /// qualified name. Either minimally quote (to the extent required + /// to reliably split the name again) or fully quote, therefore made suitable + /// for a database query. + /// + /// Name parts. + /// Should full quote. + /// A qualified name. + public string JoinAndQuoteName(string[] parts, bool fullyQuote) + { + int partCount = parts.Length; + StringBuilder result = new(); + + DebugEx.Assert(partCount is > 0 and < 4, "partCount should be 1,2 or 3."); + + int currentPart = 0; + if (partCount > 2) + { + result.Append(MaybeQuote(parts[currentPart++], fullyQuote)); + result.Append(CommandBuilder.CatalogSeparator); + } + + if (partCount > 1) + { + result.Append(MaybeQuote(parts[currentPart++], fullyQuote)); + result.Append(CommandBuilder.SchemaSeparator); + } + + result.Append(MaybeQuote(parts[currentPart], fullyQuote)); + return result.ToString(); + } + + private string MaybeQuote(string identifier, bool force) => force || FindSeparators(identifier, 0) != -1 ? QuoteIdentifier(identifier) : identifier; + + /// + /// Find the first separator in a string. + /// + /// The string. + /// Index. + /// Location of the separator. + private int FindSeparators(string text, int from) => text.IndexOfAny([SchemaSeparatorChar, CatalogSeparatorChar], from); + + /// + /// Given a string and a position in that string, assumed + /// to be the start of an identifier, find the end of that + /// identifier. Take into account quoting rules. + /// + /// The string. + /// start index. + /// Position in string after end of identifier (may be off end of string). + private int FindIdentifierEnd(string text, int start) + { + // These routine assumes prefixes and suffixes + // are single characters + string prefix = QuotePrefix; + DebugEx.Assert(prefix.Length == 1, "prefix length should be 1."); + char prefixChar = prefix[0]; + + int end = text.Length; + if (text[start] == prefixChar) + { + // Identifier is quoted. Repeatedly look for + // suffix character, until not found, + // the character after is end of string, + // or not another suffix character + + // Skip opening quote + int here = start + 1; + + string suffix = QuoteSuffix; + DebugEx.Assert(suffix.Length == 1, "suffix length should be 1."); + char suffixChar = suffix[0]; + + while (here < end) + { + here = text.IndexOf(suffixChar, here); + if (here == -1) + { + // If this happens the string is malformed, since we had an + // opening quote without a closing one, but we can survive this + break; + } + + // Skip the quote we just found + here++; + + // If this the end? + if (here == end || text[here] != suffixChar) + { + // Well formed end of identifier + return here; + } + + // We have a double quote, skip the second one, then keep looking + here++; + } + + // If we fall off end of loop, + // we didn't find the matching close quote + // Best thing to do is to just return the whole string + return end; + } + else + { + // In the case of an unquoted strings, the processing is much + // simpler... the end is end of string, or the first + // of several possible separators. + int separatorPosition = FindSeparators(text, start); + return separatorPosition == -1 ? end : separatorPosition; + } + } +} + +#endif diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Data/TestDataConnectionSql.Quotes.cs b/src/Adapter/MSTestAdapter.PlatformServices/Data/TestDataConnectionSql.Quotes.cs new file mode 100644 index 0000000000..bad02a90ed --- /dev/null +++ b/src/Adapter/MSTestAdapter.PlatformServices/Data/TestDataConnectionSql.Quotes.cs @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#if NETFRAMEWORK +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Data; + +internal partial class TestDataConnectionSql +{ + #region Quotes + + [MemberNotNull(nameof(_quotePrefix))] +#pragma warning disable SA1201 // Elements must appear in the correct order + public virtual string QuotePrefix +#pragma warning restore SA1201 // Elements must appear in the correct order + { + get + { + if (StringEx.IsNullOrEmpty(_quotePrefix)) + { + GetQuoteLiterals(); + } + + return _quotePrefix; + } + + set => _quotePrefix = value; + } + + [MemberNotNull(nameof(_quoteSuffix))] + public virtual string QuoteSuffix + { + get + { + if (StringEx.IsNullOrEmpty(_quoteSuffix)) + { + GetQuoteLiterals(); + } + + return _quoteSuffix; + } + + set => _quoteSuffix = value; + } + + private char CatalogSeparatorChar + { + get + { + if (CommandBuilder != null) + { + string catalogSeparator = CommandBuilder.CatalogSeparator; + if (!StringEx.IsNullOrEmpty(catalogSeparator)) + { + DebugEx.Assert(catalogSeparator.Length == 1, "catalogSeparator should have 1 element."); + return catalogSeparator[0]; + } + } + + return '.'; + } + } + + private char SchemaSeparatorChar + { + get + { + if (CommandBuilder != null) + { + string schemaSeparator = CommandBuilder.SchemaSeparator; + if (!StringEx.IsNullOrEmpty(schemaSeparator)) + { + DebugEx.Assert(schemaSeparator.Length == 1, "schemaSeparator should have 1 element."); + return schemaSeparator[0]; + } + } + + return '.'; + } + } + + /// + /// Note that for Oledb and Odbc CommandBuilder.QuotePrefix/Suffix is empty. + /// So we use GetQuoteLiterals for those. For all others we use CommandBuilder.QuotePrefix/Suffix. + /// + [MemberNotNull(nameof(_quotePrefix), nameof(_quoteSuffix))] + public virtual void GetQuoteLiterals() + { + _quotePrefix = CommandBuilder.QuotePrefix; + _quoteSuffix = CommandBuilder.QuoteSuffix; + } + + protected virtual string QuoteIdentifier(string identifier) + { + DebugEx.Assert(!StringEx.IsNullOrEmpty(identifier), "identifier should not be null."); + return CommandBuilder.QuoteIdentifier(identifier); + } + + protected virtual string UnquoteIdentifier(string identifier) + { + DebugEx.Assert(!StringEx.IsNullOrEmpty(identifier), "identifier should not be null."); + return CommandBuilder.UnquoteIdentifier(identifier); + } + + [MemberNotNull(nameof(_quotePrefix), nameof(_quoteSuffix), nameof(QuotePrefix), nameof(QuoteSuffix))] + protected void GetQuoteLiteralsHelper() + { + // Try to get quote chars by hand for those providers that for some reason return empty QuotePrefix/Suffix. + string s = "abcdefgh"; + string quoted = QuoteIdentifier(s); + string[] parts = quoted.Split([s], StringSplitOptions.None); + + DebugEx.Assert(parts is { Length: 2 }, "TestDataConnectionSql.GetQuotesLiteralHelper: Failure when trying to quote an identifier!"); + DebugEx.Assert(!StringEx.IsNullOrEmpty(parts[0]), "TestDataConnectionSql.GetQuotesLiteralHelper: Trying to set empty value for QuotePrefix!"); + DebugEx.Assert(!StringEx.IsNullOrEmpty(parts[1]), "TestDataConnectionSql.GetQuotesLiteralHelper: Trying to set empty value for QuoteSuffix!"); + + QuotePrefix = parts[0]; + QuoteSuffix = parts[1]; + } + + #endregion +} + +#endif diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Data/TestDataConnectionSql.Schema.cs b/src/Adapter/MSTestAdapter.PlatformServices/Data/TestDataConnectionSql.Schema.cs new file mode 100644 index 0000000000..cf5eeb4431 --- /dev/null +++ b/src/Adapter/MSTestAdapter.PlatformServices/Data/TestDataConnectionSql.Schema.cs @@ -0,0 +1,294 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#if NETFRAMEWORK +using System.Data; +using System.Data.Common; +using System.Data.Odbc; +using System.Data.OleDb; +using System.Data.SqlClient; + +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Data; + +internal partial class TestDataConnectionSql +{ + #region Schema + +#pragma warning disable SA1202 // Elements must be ordered by access + + /// + /// Returns default database schema, can be null if there is no default schema like for Excel. + /// Can throw. + /// + /// The default database schema. + public virtual string? GetDefaultSchema() => null; + +#pragma warning restore SA1202 // Elements must be ordered by access + + /// + /// Returns list of data tables and views. Sorted. + /// Any errors, return an empty list. + /// + /// List of sorted tables and views. + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Requirement is to handle all kinds of user exceptions and message appropriately.")] + public override List GetDataTablesAndViews() + { + WriteDiagnostics("GetDataTablesAndViews"); + List tableNames = []; + try + { + string? defaultSchema = GetDefaultSchema(); + WriteDiagnostics("Default schema is {0}", defaultSchema); + + SchemaMetaData[] metadatas = GetSchemaMetaData(); + + // TODO: may be find better way to enumerate tables/views. + foreach (SchemaMetaData metadata in metadatas) + { + DataTable? dataTable = null; + try + { + WriteDiagnostics("Getting schema table {0}", metadata.SchemaTable); + dataTable = Connection.GetSchema(metadata.SchemaTable); + } + catch (Exception ex) + { + WriteDiagnostics("Failed to get schema table"); + + // This can be normal case as some providers do not support views. + if (PlatformServiceProvider.Instance.AdapterTraceLogger.IsWarningEnabled) + { + PlatformServiceProvider.Instance.AdapterTraceLogger.Warning("DataUtil.GetDataTablesAndViews: exception (can be normal case as some providers do not support views): " + ex); + } + + continue; + } + + DebugEx.Assert(dataTable != null, "Failed to get data table that contains metadata about tables!"); + + foreach (DataRow row in dataTable.Rows) + { + WriteDiagnostics("Row: {0}", row); + string? tableSchema = null; + bool isDefaultSchema = false; + + // Check the table type for validity + if (metadata.TableTypeColumn != null) + { + if (row[metadata.TableTypeColumn] != DBNull.Value) + { + string? tableType = row[metadata.TableTypeColumn] as string; + if (!IsInArray(tableType, metadata.ValidTableTypes)) + { + WriteDiagnostics("Table type {0} is not acceptable", tableType); + + // Not a valid table type, get the next row + continue; + } + } + } + + // Get the schema name, and filter bad schemas + if (row[metadata.SchemaColumn] != DBNull.Value) + { + tableSchema = row[metadata.SchemaColumn] as string; + + if (IsInArray(tableSchema, metadata.InvalidSchemas)) + { + WriteDiagnostics("Schema {0} is not acceptable", tableSchema); + + // A table in a schema we do not want to see, get the next row + continue; + } + + isDefaultSchema = string.Equals(tableSchema, defaultSchema, StringComparison.OrdinalIgnoreCase); + } + + string? tableName = row[metadata.NameColumn] as string; + WriteDiagnostics("Table {0}{1} found", tableSchema != null ? tableSchema + "." : string.Empty, tableName); + + // If schema is defined and is not equal to default, prepend table schema in front of the table. + string? qualifiedTableName = tableName; + qualifiedTableName = isDefaultSchema + ? FormatTableNameForDisplay(null, tableName) + : FormatTableNameForDisplay(tableSchema, tableName); + + WriteDiagnostics("Adding Table {0}", qualifiedTableName); + tableNames.Add(qualifiedTableName); + } + + tableNames.Sort(StringComparer.OrdinalIgnoreCase); + } + } + catch (Exception e) + { + WriteDiagnostics("Failed to fetch tables for {0}, exception: {1}", Connection.ConnectionString, e); + + // OK to fall through and return whatever we do have... + } + + return tableNames; + } + + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Requirement is to handle all kinds of user exceptions and message appropriately.")] + public override List? GetColumns(string tableName) + { + WriteDiagnostics("GetColumns for {0}", tableName); + try + { + SplitTableName(tableName, out string? targetSchema, out string? targetName); + + // This lets us specifically query for columns from the appropriate table name + // but assumes all databases have the same restrictions on all the column + // schema tables + string?[] restrictions = + [ + null, // Catalog (don't care) + targetSchema, // Table schema + targetName, // Table name + null, + ]; // Column name (don't care) + + DataTable? columns = null; + try + { + columns = Connection.GetSchema("Columns", restrictions); + } + catch (NotSupportedException e) + { + WriteDiagnostics("GetColumns for {0} failed to get column metadata, exception {1}", tableName, e); + } + + if (columns != null) + { + List result = []; + + // Add all the columns + foreach (DataRow columnRow in columns.Rows) + { + WriteDiagnostics("Column info: {0}", columnRow); + result.Add(columnRow["COLUMN_NAME"].ToString()); + } + + // Now we are done, since for any particular table or view, all the columns + // must be found in a single metadata collection + return result; + } + else + { + WriteDiagnostics("Column metadata is null"); + } + } + catch (Exception e) + { + WriteDiagnostics("GetColumns for {0}, failed {1}", tableName, e); + } + + return null; // Some problem occurred + } + + /// + /// Split a table name into schema and table name, providing default + /// schema if available. + /// + /// The name. + /// The schema name output. + /// The table name output. + protected void SplitTableName(string name, out string? schemaName, out string tableName) + { + // Split the name because we need to separately look for + // tableSchema and tableName + string[]? parts = SplitName(name); + + DebugEx.Assert(parts?.Length > 0, "parts should have more than one element."); + + // Right now this processing ignores any three part names (where the catalog is specified) + // We use the default schema if the name does not specify one explicitly + schemaName = parts.Length > 1 ? parts[parts.Length - 2] : GetDefaultSchema(); + tableName = parts[parts.Length - 1]; + } + + /// + /// Returns qualified data table name, formatted for display in Data Table list or use in + /// code or test files. Note that this may not return a suitable string for SQL. + /// + /// Schema part of qualified table name. Quoted or not quoted. + /// Table name. Quoted or not quoted. + /// Qualified data table name. + protected string FormatTableNameForDisplay(string? tableSchema, string? tableName) + { + // Note: schema can be null/empty, that is OK + DebugEx.Assert(!StringEx.IsNullOrEmpty(tableName), "FormatDataTableNameForDisplay should be called only when table name is not empty."); + + return StringEx.IsNullOrEmpty(tableSchema) + ? JoinAndQuoteName([tableName], false) + : JoinAndQuoteName([tableSchema, tableName], false); + } + + /// + /// Classify a table schema as being hidden from the user + /// This helps to hide system tables such as INFORMATION_SCHEMA.COLUMNS. + /// + /// A candidate table schema. + /// True always. + protected virtual bool IsUserSchema(string tableSchema) => + // Default is to allow all schemas + true; + + /// + /// Returns default database schema. Returns null for error + /// this.Connection must be already opened. + /// + /// The default db schema. + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Requirement is to handle all kinds of user exceptions and message appropriately.")] + protected string? GetDefaultSchemaMSSql() + { + DebugEx.Assert(Connection != null, "Connection should not be null."); + + try + { + var oleDbConnection = Connection as OleDbConnection; + var odbcConnection = Connection as OdbcConnection; + DebugEx.Assert( + Connection is SqlConnection || + (oleDbConnection != null && IsMSSql(oleDbConnection.Provider)) || + (odbcConnection != null && IsMSSql(odbcConnection.Driver)), + "GetDefaultSchemaMSSql should be called only for MS SQL (either native or Ole Db or Odbc)."); + + DebugEx.Assert(IsOpen(), "The connection must already be open!"); + DebugEx.Assert(!StringEx.IsNullOrEmpty(Connection.ServerVersion), "GetDefaultSchema: the ServerVersion is null or empty!"); + + int index = Connection.ServerVersion.IndexOf(".", StringComparison.Ordinal); + DebugEx.Assert(index > 0, "GetDefaultSchema: index should be 0"); + + string versionString = Connection.ServerVersion.Substring(0, index); + DebugEx.Assert(!StringEx.IsNullOrEmpty(versionString), "GetDefaultSchema: version string is not present!"); + + int version = int.Parse(versionString, CultureInfo.InvariantCulture); + + // For Yukon (9.0) there are non-default schemas, for MSSql schema is the same as user name. + string sql = version >= 9 ? + "select default_schema_name from sys.database_principals where name = user_name()" : + "select user_name()"; + + using DbCommand cmd = Connection.CreateCommand(); + cmd.CommandText = sql; + string? defaultSchema = cmd.ExecuteScalar() as string; + return defaultSchema; + } + catch (Exception e) + { + // Any problems, at least return null, which says there is no default + WriteDiagnostics("Got an exception trying to determine default schema: {0}", e); + } + + return null; + } + + #endregion +} + +#endif diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Data/TestDataConnectionSql.cs b/src/Adapter/MSTestAdapter.PlatformServices/Data/TestDataConnectionSql.cs index b4c98f23ee..30e6bba365 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Data/TestDataConnectionSql.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Data/TestDataConnectionSql.cs @@ -1,14 +1,9 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. #if NETFRAMEWORK -using System.Data; using System.Data.Common; -using System.Data.Odbc; -using System.Data.OleDb; -using System.Data.SqlClient; -using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Data; @@ -16,7 +11,7 @@ namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Dat /// /// Data connections based on direct DB implementations all derive from this one. /// -internal class TestDataConnectionSql : TestDataConnection +internal partial class TestDataConnectionSql : TestDataConnection { private readonly DbConnection _connection; private string? _quoteSuffix; @@ -106,729 +101,6 @@ protected virtual SchemaMetaData[] GetSchemaMetaData() return [data]; } - #region Quotes - -#pragma warning disable SA1201 // Elements must appear in the correct order - [MemberNotNull(nameof(_quotePrefix))] - public virtual string QuotePrefix -#pragma warning restore SA1201 // Elements must appear in the correct order - { - get - { - if (StringEx.IsNullOrEmpty(_quotePrefix)) - { - GetQuoteLiterals(); - } - - return _quotePrefix; - } - - set => _quotePrefix = value; - } - - [MemberNotNull(nameof(_quoteSuffix))] - public virtual string QuoteSuffix - { - get - { - if (StringEx.IsNullOrEmpty(_quoteSuffix)) - { - GetQuoteLiterals(); - } - - return _quoteSuffix; - } - - set => _quoteSuffix = value; - } - - private char CatalogSeparatorChar - { - get - { - if (CommandBuilder != null) - { - string catalogSeparator = CommandBuilder.CatalogSeparator; - if (!StringEx.IsNullOrEmpty(catalogSeparator)) - { - DebugEx.Assert(catalogSeparator.Length == 1, "catalogSeparator should have 1 element."); - return catalogSeparator[0]; - } - } - - return '.'; - } - } - - private char SchemaSeparatorChar - { - get - { - if (CommandBuilder != null) - { - string schemaSeparator = CommandBuilder.SchemaSeparator; - if (!StringEx.IsNullOrEmpty(schemaSeparator)) - { - DebugEx.Assert(schemaSeparator.Length == 1, "schemaSeparator should have 1 element."); - return schemaSeparator[0]; - } - } - - return '.'; - } - } - - /// - /// Take a possibly qualified name with at least minimal quoting - /// and return a fully quoted string - /// Take care to only convert names that are of a recognized form. - /// - /// The table name. - /// A fully quoted string. - public string PrepareNameForSql(string tableName) - { - string[]? parts = SplitName(tableName); - - if (parts is { Length: > 0 }) - { - // Seems to be well formed, so make sure we end up fully quoted - return JoinAndQuoteName(parts, true); - } - else - { - // Just use what they gave us, literally, since we do not really understand the format - return tableName; - } - } - - /// - /// Take a possibly qualified name and break it down into an - /// array of identifiers unquoting any quoted names. - /// - /// A string. - /// An array of unquoted parts, or null if the name fails to conform. - public string[]? SplitName(string name) - { - List parts = []; - - int here = 0; - int end = name.Length; - char firstDelimiter = ' '; // initialize since code analysis is not smart enough - char currentDelimiter; - char catalogSeparatorChar = CatalogSeparatorChar; - char schemaSeparatorChar = SchemaSeparatorChar; - - while (here < end) - { - int next = FindIdentifierEnd(name, here); - string identifier = name.Substring(here, next - here); - - if (StringEx.IsNullOrEmpty(identifier)) - { - // Not well formed, split failed - return null; - } - - if (identifier.StartsWith(QuotePrefix, StringComparison.Ordinal)) - { - identifier = UnquoteIdentifier(identifier); - } - - parts.Add(identifier); - - if (next < end) - { - currentDelimiter = name[next]; - switch (parts.Count) - { - case 1: - // We infer there will be at least 2 parts - firstDelimiter = currentDelimiter; - if (firstDelimiter != catalogSeparatorChar - && firstDelimiter != schemaSeparatorChar) - { - // Not well formed, split failed - return null; - } - - break; - - case 2: - // We infer there will be at least 3 parts - if (firstDelimiter != catalogSeparatorChar - || currentDelimiter != schemaSeparatorChar) - { - // Not well formed, split failed - return null; - } - - break; - - default: - // We infer there will be at least 4 or more parts - // so not well formed, split failed - return null; - } - - // Skip delimiter - here = next + 1; - } - else - { - // We have found the end - if (parts.Count == 2 && firstDelimiter != schemaSeparatorChar) - { - // Not well formed, split failed - return null; - } - - return [.. parts]; - } - } - - // Ended in a delimiter, or no parts at all, either is invalid - return null; - } - - /// - /// Take a list of unquoted name parts and join them into a - /// qualified name. Either minimally quote (to the extent required - /// to reliably split the name again) or fully quote, therefore made suitable - /// for a database query. - /// - /// Name parts. - /// Should full quote. - /// A qualified name. - public string JoinAndQuoteName(string[] parts, bool fullyQuote) - { - int partCount = parts.Length; - StringBuilder result = new(); - - DebugEx.Assert(partCount is > 0 and < 4, "partCount should be 1,2 or 3."); - - int currentPart = 0; - if (partCount > 2) - { - result.Append(MaybeQuote(parts[currentPart++], fullyQuote)); - result.Append(CommandBuilder.CatalogSeparator); - } - - if (partCount > 1) - { - result.Append(MaybeQuote(parts[currentPart++], fullyQuote)); - result.Append(CommandBuilder.SchemaSeparator); - } - - result.Append(MaybeQuote(parts[currentPart], fullyQuote)); - return result.ToString(); - } - - /// - /// Note that for Oledb and Odbc CommandBuilder.QuotePrefix/Suffix is empty. - /// So we use GetQuoteLiterals for those. For all others we use CommandBuilder.QuotePrefix/Suffix. - /// - [MemberNotNull(nameof(_quotePrefix), nameof(_quoteSuffix))] - public virtual void GetQuoteLiterals() - { - _quotePrefix = CommandBuilder.QuotePrefix; - _quoteSuffix = CommandBuilder.QuoteSuffix; - } - - protected virtual string QuoteIdentifier(string identifier) - { - DebugEx.Assert(!StringEx.IsNullOrEmpty(identifier), "identifier should not be null."); - return CommandBuilder.QuoteIdentifier(identifier); - } - - protected virtual string UnquoteIdentifier(string identifier) - { - DebugEx.Assert(!StringEx.IsNullOrEmpty(identifier), "identifier should not be null."); - return CommandBuilder.UnquoteIdentifier(identifier); - } - - [MemberNotNull(nameof(_quotePrefix), nameof(_quoteSuffix), nameof(QuotePrefix), nameof(QuoteSuffix))] - protected void GetQuoteLiteralsHelper() - { - // Try to get quote chars by hand for those providers that for some reason return empty QuotePrefix/Suffix. - string s = "abcdefgh"; - string quoted = QuoteIdentifier(s); - string[] parts = quoted.Split([s], StringSplitOptions.None); - - DebugEx.Assert(parts is { Length: 2 }, "TestDataConnectionSql.GetQuotesLiteralHelper: Failure when trying to quote an identifier!"); - DebugEx.Assert(!StringEx.IsNullOrEmpty(parts[0]), "TestDataConnectionSql.GetQuotesLiteralHelper: Trying to set empty value for QuotePrefix!"); - DebugEx.Assert(!StringEx.IsNullOrEmpty(parts[1]), "TestDataConnectionSql.GetQuotesLiteralHelper: Trying to set empty value for QuoteSuffix!"); - - QuotePrefix = parts[0]; - QuoteSuffix = parts[1]; - } - - private string MaybeQuote(string identifier, bool force) => force || FindSeparators(identifier, 0) != -1 ? QuoteIdentifier(identifier) : identifier; - - /// - /// Find the first separator in a string. - /// - /// The string. - /// Index. - /// Location of the separator. - private int FindSeparators(string text, int from) => text.IndexOfAny([SchemaSeparatorChar, CatalogSeparatorChar], from); - - /// - /// Given a string and a position in that string, assumed - /// to be the start of an identifier, find the end of that - /// identifier. Take into account quoting rules. - /// - /// The string. - /// start index. - /// Position in string after end of identifier (may be off end of string). - private int FindIdentifierEnd(string text, int start) - { - // These routine assumes prefixes and suffixes - // are single characters - string prefix = QuotePrefix; - DebugEx.Assert(prefix.Length == 1, "prefix length should be 1."); - char prefixChar = prefix[0]; - - int end = text.Length; - if (text[start] == prefixChar) - { - // Identifier is quoted. Repeatedly look for - // suffix character, until not found, - // the character after is end of string, - // or not another suffix character - - // Skip opening quote - int here = start + 1; - - string suffix = QuoteSuffix; - DebugEx.Assert(suffix.Length == 1, "suffix length should be 1."); - char suffixChar = suffix[0]; - - while (here < end) - { - here = text.IndexOf(suffixChar, here); - if (here == -1) - { - // If this happens the string is malformed, since we had an - // opening quote without a closing one, but we can survive this - break; - } - - // Skip the quote we just found - here++; - - // If this the end? - if (here == end || text[here] != suffixChar) - { - // Well formed end of identifier - return here; - } - - // We have a double quote, skip the second one, then keep looking - here++; - } - - // If we fall off end of loop, - // we didn't find the matching close quote - // Best thing to do is to just return the whole string - return end; - } - else - { - // In the case of an unquoted strings, the processing is much - // simpler... the end is end of string, or the first - // of several possible separators. - int separatorPosition = FindSeparators(text, start); - return separatorPosition == -1 ? end : separatorPosition; - } - } - - #endregion - - #region Schema - -#pragma warning disable SA1202 // Elements must be ordered by access - - /// - /// Returns default database schema, can be null if there is no default schema like for Excel. - /// Can throw. - /// - /// The default database schema. - public virtual string? GetDefaultSchema() => null; - -#pragma warning restore SA1202 // Elements must be ordered by access - - /// - /// Returns list of data tables and views. Sorted. - /// Any errors, return an empty list. - /// - /// List of sorted tables and views. - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Requirement is to handle all kinds of user exceptions and message appropriately.")] - public override List GetDataTablesAndViews() - { - WriteDiagnostics("GetDataTablesAndViews"); - List tableNames = []; - try - { - string? defaultSchema = GetDefaultSchema(); - WriteDiagnostics("Default schema is {0}", defaultSchema); - - SchemaMetaData[] metadatas = GetSchemaMetaData(); - - // TODO: may be find better way to enumerate tables/views. - foreach (SchemaMetaData metadata in metadatas) - { - DataTable? dataTable = null; - try - { - WriteDiagnostics("Getting schema table {0}", metadata.SchemaTable); - dataTable = Connection.GetSchema(metadata.SchemaTable); - } - catch (Exception ex) - { - WriteDiagnostics("Failed to get schema table"); - - // This can be normal case as some providers do not support views. - if (PlatformServiceProvider.Instance.AdapterTraceLogger.IsWarningEnabled) - { - PlatformServiceProvider.Instance.AdapterTraceLogger.Warning("DataUtil.GetDataTablesAndViews: exception (can be normal case as some providers do not support views): " + ex); - } - - continue; - } - - DebugEx.Assert(dataTable != null, "Failed to get data table that contains metadata about tables!"); - - foreach (DataRow row in dataTable.Rows) - { - WriteDiagnostics("Row: {0}", row); - string? tableSchema = null; - bool isDefaultSchema = false; - - // Check the table type for validity - if (metadata.TableTypeColumn != null) - { - if (row[metadata.TableTypeColumn] != DBNull.Value) - { - string? tableType = row[metadata.TableTypeColumn] as string; - if (!IsInArray(tableType, metadata.ValidTableTypes)) - { - WriteDiagnostics("Table type {0} is not acceptable", tableType); - - // Not a valid table type, get the next row - continue; - } - } - } - - // Get the schema name, and filter bad schemas - if (row[metadata.SchemaColumn] != DBNull.Value) - { - tableSchema = row[metadata.SchemaColumn] as string; - - if (IsInArray(tableSchema, metadata.InvalidSchemas)) - { - WriteDiagnostics("Schema {0} is not acceptable", tableSchema); - - // A table in a schema we do not want to see, get the next row - continue; - } - - isDefaultSchema = string.Equals(tableSchema, defaultSchema, StringComparison.OrdinalIgnoreCase); - } - - string? tableName = row[metadata.NameColumn] as string; - WriteDiagnostics("Table {0}{1} found", tableSchema != null ? tableSchema + "." : string.Empty, tableName); - - // If schema is defined and is not equal to default, prepend table schema in front of the table. - string? qualifiedTableName = tableName; - qualifiedTableName = isDefaultSchema - ? FormatTableNameForDisplay(null, tableName) - : FormatTableNameForDisplay(tableSchema, tableName); - - WriteDiagnostics("Adding Table {0}", qualifiedTableName); - tableNames.Add(qualifiedTableName); - } - - tableNames.Sort(StringComparer.OrdinalIgnoreCase); - } - } - catch (Exception e) - { - WriteDiagnostics("Failed to fetch tables for {0}, exception: {1}", Connection.ConnectionString, e); - - // OK to fall through and return whatever we do have... - } - - return tableNames; - } - - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Requirement is to handle all kinds of user exceptions and message appropriately.")] - public override List? GetColumns(string tableName) - { - WriteDiagnostics("GetColumns for {0}", tableName); - try - { - SplitTableName(tableName, out string? targetSchema, out string? targetName); - - // This lets us specifically query for columns from the appropriate table name - // but assumes all databases have the same restrictions on all the column - // schema tables - string?[] restrictions = - [ - null, // Catalog (don't care) - targetSchema, // Table schema - targetName, // Table name - null, - ]; // Column name (don't care) - - DataTable? columns = null; - try - { - columns = Connection.GetSchema("Columns", restrictions); - } - catch (NotSupportedException e) - { - WriteDiagnostics("GetColumns for {0} failed to get column metadata, exception {1}", tableName, e); - } - - if (columns != null) - { - List result = []; - - // Add all the columns - foreach (DataRow columnRow in columns.Rows) - { - WriteDiagnostics("Column info: {0}", columnRow); - result.Add(columnRow["COLUMN_NAME"].ToString()); - } - - // Now we are done, since for any particular table or view, all the columns - // must be found in a single metadata collection - return result; - } - else - { - WriteDiagnostics("Column metadata is null"); - } - } - catch (Exception e) - { - WriteDiagnostics("GetColumns for {0}, failed {1}", tableName, e); - } - - return null; // Some problem occurred - } - - /// - /// Split a table name into schema and table name, providing default - /// schema if available. - /// - /// The name. - /// The schema name output. - /// The table name output. - protected void SplitTableName(string name, out string? schemaName, out string tableName) - { - // Split the name because we need to separately look for - // tableSchema and tableName - string[]? parts = SplitName(name); - - DebugEx.Assert(parts?.Length > 0, "parts should have more than one element."); - - // Right now this processing ignores any three part names (where the catalog is specified) - // We use the default schema if the name does not specify one explicitly - schemaName = parts.Length > 1 ? parts[parts.Length - 2] : GetDefaultSchema(); - tableName = parts[parts.Length - 1]; - } - - /// - /// Returns qualified data table name, formatted for display in Data Table list or use in - /// code or test files. Note that this may not return a suitable string for SQL. - /// - /// Schema part of qualified table name. Quoted or not quoted. - /// Table name. Quoted or not quoted. - /// Qualified data table name. - protected string FormatTableNameForDisplay(string? tableSchema, string? tableName) - { - // Note: schema can be null/empty, that is OK - DebugEx.Assert(!StringEx.IsNullOrEmpty(tableName), "FormatDataTableNameForDisplay should be called only when table name is not empty."); - - return StringEx.IsNullOrEmpty(tableSchema) - ? JoinAndQuoteName([tableName], false) - : JoinAndQuoteName([tableSchema, tableName], false); - } - - /// - /// Just a helper method to see if a string is in a string array - /// Note that the array can be null, this is treated as an empty array. - /// - /// The string. - /// An array of values. - /// True if string exists in array. - private static bool IsInArray(string? candidate, string[]? values) - { - if (values == null) - { - return false; - } - - foreach (string value in values) - { - if (string.Equals(value, candidate, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - - return false; - } - - #endregion - - #region Helpers - -#pragma warning disable SA1202 // Elements must be ordered by access - public bool IsOpen() => _connection is { State: ConnectionState.Open }; - - /// - /// Returns true when given provider (OLEDB or ODBC) is for MSSql. - /// - /// OLEDB or ODBC provider. - /// True if provider is for MSSql. - protected static bool IsMSSql(string providerName) => (!StringEx.IsNullOrEmpty(providerName) && - (providerName.StartsWith(KnownOleDbProviderNames.SqlOleDb, StringComparison.OrdinalIgnoreCase) || - providerName.StartsWith(KnownOleDbProviderNames.MSSqlNative, StringComparison.OrdinalIgnoreCase))) || - string.Equals(providerName, KnownOdbcDrivers.MSSql, StringComparison.OrdinalIgnoreCase); - - /// - /// Classify a table schema as being hidden from the user - /// This helps to hide system tables such as INFORMATION_SCHEMA.COLUMNS. - /// - /// A candidate table schema. - /// True always. - protected virtual bool IsUserSchema(string tableSchema) => - // Default is to allow all schemas - true; - - /// - /// Returns default database schema. Returns null for error - /// this.Connection must be already opened. - /// - /// The default db schema. - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Requirement is to handle all kinds of user exceptions and message appropriately.")] - protected string? GetDefaultSchemaMSSql() - { - DebugEx.Assert(Connection != null, "Connection should not be null."); - - try - { - var oleDbConnection = Connection as OleDbConnection; - var odbcConnection = Connection as OdbcConnection; - DebugEx.Assert( - Connection is SqlConnection || - (oleDbConnection != null && IsMSSql(oleDbConnection.Provider)) || - (odbcConnection != null && IsMSSql(odbcConnection.Driver)), - "GetDefaultSchemaMSSql should be called only for MS SQL (either native or Ole Db or Odbc)."); - - DebugEx.Assert(IsOpen(), "The connection must already be open!"); - DebugEx.Assert(!StringEx.IsNullOrEmpty(Connection.ServerVersion), "GetDefaultSchema: the ServerVersion is null or empty!"); - - int index = Connection.ServerVersion.IndexOf(".", StringComparison.Ordinal); - DebugEx.Assert(index > 0, "GetDefaultSchema: index should be 0"); - - string versionString = Connection.ServerVersion.Substring(0, index); - DebugEx.Assert(!StringEx.IsNullOrEmpty(versionString), "GetDefaultSchema: version string is not present!"); - - int version = int.Parse(versionString, CultureInfo.InvariantCulture); - - // For Yukon (9.0) there are non-default schemas, for MSSql schema is the same as user name. - string sql = version >= 9 ? - "select default_schema_name from sys.database_principals where name = user_name()" : - "select user_name()"; - - using DbCommand cmd = Connection.CreateCommand(); - cmd.CommandText = sql; - string? defaultSchema = cmd.ExecuteScalar() as string; - return defaultSchema; - } - catch (Exception e) - { - // Any problems, at least return null, which says there is no default - WriteDiagnostics("Got an exception trying to determine default schema: {0}", e); - } - - return null; - } - - #endregion - - #region Data - - /// - /// Read a table from the connection, into a DataTable - /// Code used to be in UnitTestDataManager. - /// - /// The table name. - /// Columns. - /// new DataTable. - [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Un-tested. Leaving behavior as is.")] - [SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security", Justification = "Not passed in from the user.")] -#pragma warning disable SA1202 // Elements must be ordered by access - public override DataTable ReadTable(string tableName, IEnumerable? columns) -#pragma warning restore SA1202 // Elements must be ordered by access - { - using DbDataAdapter dataAdapter = Factory.CreateDataAdapter(); - using DbCommand command = Factory.CreateCommand(); - - // We need to escape bad characters in table name like [Sheet1$] in Excel. - // But if table name is quoted in terms of provider, don't touch it to avoid e.g. [dbo.tables.etc]. - string quotedTableName = PrepareNameForSql(tableName); - if (PlatformServiceProvider.Instance.AdapterTraceLogger.IsInfoEnabled) - { - PlatformServiceProvider.Instance.AdapterTraceLogger.Info("ReadTable: data driven test: got table name from attribute: {0}", tableName); - PlatformServiceProvider.Instance.AdapterTraceLogger.Info("ReadTable: data driven test: will use table name: {0}", tableName); - } - - command.Connection = Connection; - command.CommandText = string.Format(CultureInfo.InvariantCulture, "select {0} from {1}", GetColumnsSQL(columns), quotedTableName); - - WriteDiagnostics("ReadTable: SQL Query: {0}", command.CommandText); - dataAdapter.SelectCommand = command; - - DataTable table = new() - { - Locale = CultureInfo.InvariantCulture, - }; - dataAdapter.Fill(table); - - table.TableName = tableName; // Make table name in the data set the same as original table name. - return table; - } - - private string GetColumnsSQL(IEnumerable? columns) - { - string? result = null; - if (columns != null) - { - StringBuilder builder = new(); - foreach (string columnName in columns) - { - if (builder.Length > 0) - { - builder.Append(','); - } - - builder.Append(QuoteIdentifier(columnName)); - } - - result = builder.ToString(); - } - - // Return a valid list of columns, or default to * for all columns - return !StringEx.IsNullOrEmpty(result) ? result : "*"; - } - - #endregion - [SuppressMessage("Microsoft.Usage", "CA2215:Dispose methods should call base class dispose", Justification = "Un-tested. Just preserving behavior.")] #pragma warning disable SA1202 // Elements must be ordered by access public override void Dispose() @@ -840,66 +112,6 @@ public override void Dispose() GC.SuppressFinalize(this); } - - #region Types - - /// - /// When querying for tables, metadata varies quite a bit from DB to DB - /// This struct encapsulates those variations. - /// - protected struct SchemaMetaData - { - // Name of a table containing tables or views - public string? SchemaTable; - - // Column that contains schema names, if null, unused - public string? SchemaColumn; - - // Column that contains the table names - public string? NameColumn; - - // Column that contains a table "type", if null, type is unchecked - public string? TableTypeColumn; - - // If table type is available, it is checked to be one of the values on this list - public string[]? ValidTableTypes; - - // If schema is available, it is checked to not be one of the values on this list - public string[]? InvalidSchemas; - } - - /// - /// Known OLE DB providers for MS SQL and Oracle. - /// - /// - /// How Data Connection dialog maps to different providers: - /// SqlOleDb: Data Source = MS SQL, Provider = OLE DB - /// SqlOleDb.1: Data Source = Other, Provider = Microsoft OLE DB Provider for Sql Server - /// Provider=SQLOLEDB;Data Source=SqlServer;Integrated Security=SSPI;Initial Catalog=DatabaseName - /// SQLNCLI.1: Data Source = Other, Provider = Sql Native Client - /// Provider=SQLNCLI.1;Data Source=SqlServer;Integrated Security=SSPI;Initial Catalog=DatabaseName. - /// - protected static class KnownOleDbProviderNames - { - internal const string SqlOleDb = "SQLOLEDB"; - internal const string MSSqlNative = "SQLNCLI"; - - // Note MSDASQL (OLE DB Provider for ODBC) is not supported by .NET. - } - - /// - /// Known ODBC drivers. - /// - /// - /// sqlsrv32.dll: Driver={SQL Server};Server=SqlServer;Database=DatabaseName;Trusted_Connection=yes - /// msorcl32.dll: Driver={Microsoft ODBC for Oracle};Server=OracleServer;Uid=user;Pwd=password. - /// - protected static class KnownOdbcDrivers - { - internal const string MSSql = "sqlsrv32.dll"; - } - - #endregion } #endif