From 4e2fa275351a13b3d1db5d43a4602d294253364c Mon Sep 17 00:00:00 2001 From: Lau Date: Sat, 28 Mar 2020 16:01:37 +0100 Subject: [PATCH 1/5] Fix JetConnection and DbConnectionStringBuilderExtensions issue. --- src/System.Data.Jet/DbConnectionStringBuilderExtensions.cs | 2 +- src/System.Data.Jet/JetConnection.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/System.Data.Jet/DbConnectionStringBuilderExtensions.cs b/src/System.Data.Jet/DbConnectionStringBuilderExtensions.cs index c1306d4e..716d6dbd 100644 --- a/src/System.Data.Jet/DbConnectionStringBuilderExtensions.cs +++ b/src/System.Data.Jet/DbConnectionStringBuilderExtensions.cs @@ -29,7 +29,7 @@ public static void SetProvider(this DbConnectionStringBuilder builder, string va } else if (IsOdbc(builder)) { - builder["driver"] = Regex.Replace(value.Trim(), @"^(?This property can only be set as long as the connection is closed. public DbProviderFactory DataAccessProviderFactory { - get => JetFactory.InnerFactory; + get => JetFactory?.InnerFactory; set { if (value == null) From 1f42ec1f0a453cb6393d4e8c63bc593b896b0847 Mon Sep 17 00:00:00 2001 From: Lau Date: Sat, 28 Mar 2020 16:01:18 +0100 Subject: [PATCH 2/5] Implement automatic driver (ODBC) and provider (OLE DB) inference and detection. --- build/dependencies.props | 5 +- .../JetDbContextOptionsBuilderExtensions.cs | 98 +++++--- src/System.Data.Jet/AdoxWrapper.cs | 9 +- src/System.Data.Jet/InnerConnectionFactory.cs | 2 +- src/System.Data.Jet/JetConfiguration.cs | 26 +-- src/System.Data.Jet/JetConnection.cs | 215 +++++++++++++++--- src/System.Data.Jet/JetDataReader.cs | 131 ++++++----- .../JetStoreDatabaseHandling.cs | 49 +++- src/System.Data.Jet/System.Data.Jet.csproj | 1 + test/EFCore.Jet.FunctionalTests/config.json | 2 +- 10 files changed, 380 insertions(+), 158 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index e7d16ef8..282d031a 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -14,8 +14,9 @@ 1.1.0 1.7.0 4.7.0 - 5.0.0-preview.3.20171.3 - 5.0.0-preview.3.20171.3 + 4.7.0 + 5.0.0-preview.3.20178.1 + 5.0.0-preview.3.20178.1 diff --git a/src/EFCore.Jet/Extensions/JetDbContextOptionsBuilderExtensions.cs b/src/EFCore.Jet/Extensions/JetDbContextOptionsBuilderExtensions.cs index 1fa85f53..ef9719bd 100644 --- a/src/EFCore.Jet/Extensions/JetDbContextOptionsBuilderExtensions.cs +++ b/src/EFCore.Jet/Extensions/JetDbContextOptionsBuilderExtensions.cs @@ -24,147 +24,166 @@ public static class JetDbContextOptionsBuilderExtensions /// Configures the context to connect to a Microsoft Jet database. /// /// The builder being used to configure the context. - /// The connection string of the database to connect to. The underlying data - /// access provider (ODBC or OLE DB) will be inferred from the style of this connection string. + /// The file name or connection string of the database to connect to. + /// If just a file name is supplied, the default data access provider type as defined by + /// `JetConfiguration.DefaultDataAccessProviderType` is being used. If a connection string is supplied, the + /// underlying data access provider (ODBC or OLE DB) will be inferred from the style of the connection string. + /// In case the connection string does not specify an Access driver (ODBC) or ACE/Jet provider (OLE DB), the + /// highest version of all compatible installed ones is being used. /// An optional action to allow additional Jet specific configuration. /// The options builder so that further configuration can be chained. public static DbContextOptionsBuilder UseJet( [NotNull] this DbContextOptionsBuilder optionsBuilder, - [NotNull] string connectionString, + [NotNull] string fileNameOrConnectionString, [CanBeNull] Action jetOptionsAction = null) where TContext : DbContext - => (DbContextOptionsBuilder) UseJet((DbContextOptionsBuilder) optionsBuilder, connectionString, jetOptionsAction); + => (DbContextOptionsBuilder) UseJet((DbContextOptionsBuilder) optionsBuilder, fileNameOrConnectionString, jetOptionsAction); /// /// Configures the context to connect to a Microsoft Jet database. /// /// The builder being used to configure the context. - /// The connection string of the database to connect to. The underlying data - /// access provider (ODBC or OLE DB) will be inferred from the style of this connection string. + /// The file name or connection string of the database to connect to. + /// If just a file name is supplied, the default data access provider type as defined by + /// `JetConfiguration.DefaultDataAccessProviderType` is being used. If a connection string is supplied, the + /// underlying data access provider (ODBC or OLE DB) will be inferred from the style of the connection string. + /// In case the connection string does not specify an Access driver (ODBC) or ACE/Jet provider (OLE DB), the + /// highest version of all compatible installed ones is being used. /// An optional action to allow additional Jet specific configuration. /// The options builder so that further configuration can be chained. public static DbContextOptionsBuilder UseJet( [NotNull] this DbContextOptionsBuilder optionsBuilder, - [NotNull] string connectionString, + [NotNull] string fileNameOrConnectionString, [CanBeNull] Action jetOptionsAction = null) { Check.NotNull(optionsBuilder, nameof(optionsBuilder)); - Check.NotEmpty(connectionString, nameof(connectionString)); + Check.NotEmpty(fileNameOrConnectionString, nameof(fileNameOrConnectionString)); - return UseJetCore(optionsBuilder, connectionString, null, JetConnection.GetDataAccessProviderType(connectionString), jetOptionsAction); + return UseJetCore(optionsBuilder, fileNameOrConnectionString, null, null, jetOptionsAction); } /// /// Configures the context to connect to a Microsoft Jet database. /// /// The builder being used to configure the context. - /// The connection string of the database to connect to. + /// The file name or connection string of the database to connect to. /// An `OdbcFactory` or `OleDbFactory` object to be used for all /// data access operations by the Jet connection. /// An optional action to allow additional Jet specific configuration. /// The options builder so that further configuration can be chained. public static DbContextOptionsBuilder UseJet( [NotNull] this DbContextOptionsBuilder optionsBuilder, - [NotNull] string connectionString, + [NotNull] string fileNameOrConnectionString, [NotNull] DbProviderFactory dataAccessProviderFactory, [CanBeNull] Action jetOptionsAction = null) where TContext : DbContext { Check.NotNull(optionsBuilder, nameof(optionsBuilder)); - Check.NotEmpty(connectionString, nameof(connectionString)); + Check.NotEmpty(fileNameOrConnectionString, nameof(fileNameOrConnectionString)); Check.NotNull(dataAccessProviderFactory, nameof(dataAccessProviderFactory)); - return (DbContextOptionsBuilder) UseJet((DbContextOptionsBuilder) optionsBuilder, connectionString, dataAccessProviderFactory, jetOptionsAction); + return (DbContextOptionsBuilder) UseJet((DbContextOptionsBuilder) optionsBuilder, fileNameOrConnectionString, dataAccessProviderFactory, jetOptionsAction); } /// /// Configures the context to connect to a Microsoft Jet database. /// /// The builder being used to configure the context. - /// The connection string of the database to connect to. + /// The file name or connection string of the database to connect to. /// An `OdbcFactory` or `OleDbFactory` object to be used for all /// data access operations by the Jet connection. /// An optional action to allow additional Jet specific configuration. /// The options builder so that further configuration can be chained. public static DbContextOptionsBuilder UseJet( [NotNull] this DbContextOptionsBuilder optionsBuilder, - [NotNull] string connectionString, + [NotNull] string fileNameOrConnectionString, [NotNull] DbProviderFactory dataAccessProviderFactory, [CanBeNull] Action jetOptionsAction = null) { Check.NotNull(optionsBuilder, nameof(optionsBuilder)); - Check.NotEmpty(connectionString, nameof(connectionString)); + Check.NotEmpty(fileNameOrConnectionString, nameof(fileNameOrConnectionString)); Check.NotNull(dataAccessProviderFactory, nameof(dataAccessProviderFactory)); - return UseJetCore(optionsBuilder, connectionString, dataAccessProviderFactory, null, jetOptionsAction); + return UseJetCore(optionsBuilder, fileNameOrConnectionString, dataAccessProviderFactory, null, jetOptionsAction); } /// /// Configures the context to connect to a Microsoft Jet database. /// /// The builder being used to configure the context. - /// The connection string of the database to connect to. + /// The file name or connection string of the database to connect to. /// The type of the data access provider (`Odbc` or `OleDb`) to be used for all /// data access operations by the Jet connection. /// An optional action to allow additional Jet specific configuration. /// The options builder so that further configuration can be chained. public static DbContextOptionsBuilder UseJet( [NotNull] this DbContextOptionsBuilder optionsBuilder, - [NotNull] string connectionString, + [NotNull] string fileNameOrConnectionString, DataAccessProviderType dataAccessProviderType, [CanBeNull] Action jetOptionsAction = null) where TContext : DbContext { Check.NotNull(optionsBuilder, nameof(optionsBuilder)); - Check.NotEmpty(connectionString, nameof(connectionString)); + Check.NotEmpty(fileNameOrConnectionString, nameof(fileNameOrConnectionString)); - return (DbContextOptionsBuilder) UseJet((DbContextOptionsBuilder)optionsBuilder, connectionString, dataAccessProviderType, jetOptionsAction); + return (DbContextOptionsBuilder) UseJet((DbContextOptionsBuilder)optionsBuilder, fileNameOrConnectionString, dataAccessProviderType, jetOptionsAction); } /// /// Configures the context to connect to a Microsoft Jet database. /// /// The builder being used to configure the context. - /// The connection string of the database to connect to. + /// The file name or connection string of the database to connect to. /// The type of the data access provider (`Odbc` or `OleDb`) to be used for all /// data access operations by the Jet connection. /// An optional action to allow additional Jet specific configuration. /// The options builder so that further configuration can be chained. public static DbContextOptionsBuilder UseJet( [NotNull] this DbContextOptionsBuilder optionsBuilder, - [NotNull] string connectionString, + [NotNull] string fileNameOrConnectionString, DataAccessProviderType dataAccessProviderType, [CanBeNull] Action jetOptionsAction = null) { Check.NotNull(optionsBuilder, nameof(optionsBuilder)); - Check.NotEmpty(connectionString, nameof(connectionString)); + Check.NotEmpty(fileNameOrConnectionString, nameof(fileNameOrConnectionString)); - return UseJetCore(optionsBuilder, connectionString, null, dataAccessProviderType, jetOptionsAction); + return UseJetCore(optionsBuilder, fileNameOrConnectionString, null, dataAccessProviderType, jetOptionsAction); } internal static DbContextOptionsBuilder UseJetWithoutPredefinedDataAccessProvider( [NotNull] this DbContextOptionsBuilder optionsBuilder, - [NotNull] string connectionString, + [NotNull] string fileNameOrConnectionString, [CanBeNull] Action jetOptionsAction = null) - => UseJetCore(optionsBuilder, connectionString, null, null, jetOptionsAction); + => UseJetCore(optionsBuilder, fileNameOrConnectionString, null, null, jetOptionsAction); private static DbContextOptionsBuilder UseJetCore( [NotNull] DbContextOptionsBuilder optionsBuilder, - [NotNull] string connectionString, + [NotNull] string fileNameOrConnectionString, [CanBeNull] DbProviderFactory dataAccessProviderFactory, [CanBeNull] DataAccessProviderType? dataAccessProviderType, Action jetOptionsAction) { Check.NotNull(optionsBuilder, nameof(optionsBuilder)); - Check.NotEmpty(connectionString, nameof(connectionString)); + Check.NotEmpty(fileNameOrConnectionString, nameof(fileNameOrConnectionString)); if (dataAccessProviderFactory == null && dataAccessProviderType == null) { - throw new ArgumentException($"One of the parameters {nameof(dataAccessProviderFactory)} and {nameof(dataAccessProviderType)} must not be null."); + if (JetConnection.IsConnectionString(fileNameOrConnectionString)) + { + dataAccessProviderType = JetConnection.GetDataAccessProviderType(fileNameOrConnectionString); + } + else if (JetConnection.IsFileName(fileNameOrConnectionString)) + { + dataAccessProviderType = JetConfiguration.DefaultDataAccessProviderType; + } + else + { + throw new ArgumentException($"Either {nameof(dataAccessProviderFactory)} or {nameof(dataAccessProviderType)} must not be null, or a file name must be specified for {nameof(fileNameOrConnectionString)}."); + } } var extension = (JetOptionsExtension) GetOrCreateExtension(optionsBuilder) - .WithConnectionString(connectionString); + .WithConnectionString(fileNameOrConnectionString); extension = extension.WithDataAccessProviderFactory( dataAccessProviderFactory ?? JetFactory.Instance.GetDataAccessProviderFactory(dataAccessProviderType.Value)); @@ -231,7 +250,22 @@ public static DbContextOptionsBuilder UseJet( if (jetConnection.DataAccessProviderFactory == null) { - var dataAccessProviderType = JetConnection.GetDataAccessProviderType(jetConnection.ConnectionString); + var fileNameOrConnectionString = jetConnection.ConnectionString; + DataAccessProviderType dataAccessProviderType; + + if (JetConnection.IsConnectionString(fileNameOrConnectionString)) + { + dataAccessProviderType = JetConnection.GetDataAccessProviderType(fileNameOrConnectionString); + } + else if (JetConnection.IsFileName(fileNameOrConnectionString)) + { + dataAccessProviderType = JetConfiguration.DefaultDataAccessProviderType; + } + else + { + throw new ArgumentException($"The data access provider type could not be inferred from the connections {nameof(JetConnection.DataAccessProviderFactory)} or {nameof(JetConnection.ConnectionString)} property and the {nameof(JetConnection.ConnectionString)} property is not a valid file name either."); + } + jetConnection.DataAccessProviderFactory = JetFactory.Instance.GetDataAccessProviderFactory(dataAccessProviderType); jetConnection.Freeze(); } diff --git a/src/System.Data.Jet/AdoxWrapper.cs b/src/System.Data.Jet/AdoxWrapper.cs index fd8c857f..7fff2d2b 100644 --- a/src/System.Data.Jet/AdoxWrapper.cs +++ b/src/System.Data.Jet/AdoxWrapper.cs @@ -1,4 +1,5 @@ using System.Data.Common; +using System.Data.Jet.JetStoreSchemaDefinition; namespace System.Data.Jet { @@ -61,8 +62,10 @@ public static void RenameColumn(string connectionString, string tableName, strin } } - public static string CreateEmptyDatabase(string fileName, DbProviderFactory dataAccessProviderFactory) + public static string CreateEmptyDatabase(string fileNameOrConnectionString, DbProviderFactory dataAccessProviderFactory) { + var fileName = JetStoreDatabaseHandling.ExpandFileName(JetStoreDatabaseHandling.ExtractFileNameFromConnectionString(fileNameOrConnectionString)); + string connectionString = null; using var catalog = GetCatalogInstance(); @@ -77,7 +80,7 @@ public static string CreateEmptyDatabase(string fileName, DbProviderFactory data } catch (Exception e) { - throw new Exception("Cannot create database using the specified connection string: " + connectionString, e); + throw new Exception($"Cannot create database \"{fileName}\" using ADOX with the following connection string: " + connectionString, e); } try @@ -108,7 +111,7 @@ public static string CreateEmptyDatabase(string fileName, DbProviderFactory data } catch (Exception e) { - throw new Exception("Cannot create database using the specified connection string.", e); + throw new Exception($"Cannot setup the newly created database \"{fileName}\" using {Enum.GetName(typeof(DataAccessProviderType), JetConnection.GetDataAccessProviderType(dataAccessProviderFactory))} with the following connection string: " + connectionString, e); } try diff --git a/src/System.Data.Jet/InnerConnectionFactory.cs b/src/System.Data.Jet/InnerConnectionFactory.cs index e891c88b..6cf2bc37 100644 --- a/src/System.Data.Jet/InnerConnectionFactory.cs +++ b/src/System.Data.Jet/InnerConnectionFactory.cs @@ -5,7 +5,7 @@ namespace System.Data.Jet { class InnerConnectionFactory : IDisposable { - public static InnerConnectionFactory Instance = new InnerConnectionFactory(); + public static readonly InnerConnectionFactory Instance = new InnerConnectionFactory(); private InnerConnectionFactory() { diff --git a/src/System.Data.Jet/JetConfiguration.cs b/src/System.Data.Jet/JetConfiguration.cs index 0567a087..01c8d03a 100644 --- a/src/System.Data.Jet/JetConfiguration.cs +++ b/src/System.Data.Jet/JetConfiguration.cs @@ -1,8 +1,4 @@ -using System.Data.Common; - -// ReSharper disable InconsistentNaming - -namespace System.Data.Jet +namespace System.Data.Jet { /// /// Jet configuration @@ -12,10 +8,11 @@ public static class JetConfiguration /// /// The time span offset (Jet does not support timespans) /// - public static DateTime TimeSpanOffset = new DateTime(1899, 12, 30); + public static DateTime TimeSpanOffset { get; set; } = new DateTime(1899, 12, 30); private static object _integerNullValue = Int32.MinValue; + // CHECK: Replace with Nullable /// /// Gets or sets the integer null value returned by queries. This should solve a Jet issue /// that if I do a UNION ALL of null, int and null the Jet raises an error @@ -25,7 +22,7 @@ public static class JetConfiguration /// public static object IntegerNullValue { - get { return _integerNullValue; } + get => _integerNullValue; set { if (!(value is int) && value != null) @@ -33,13 +30,8 @@ public static object IntegerNullValue _integerNullValue = value; } } - - // "Microsoft.ACE.OLEDB.12.0" should have the fewest problems of all versions, as it supports 32 Bit and 64 Bit, - // does not throw an AccessViolationException when connecting through OLE DB without connection pooling enabled, - // has the highest backwards compatibility level of all recent releases and can freely be downloaded from - // Microsoft as "Microsoft Access Database Engine 2010 Redistributable". - public static string OleDbDefaultProvider = "Microsoft.ACE.OLEDB.12.0"; - public static string OdbcDefaultProvider = "Microsoft Access Driver (*.mdb, *.accdb)"; + + public static DataAccessProviderType DefaultDataAccessProviderType { get; set; } = DataAccessProviderType.OleDb; // The SQL statement // @@ -63,7 +55,7 @@ public static object IntegerNullValue /// /// The DUAL table or query /// - public static string DUAL = DUALForAccdb; + public static string DUAL { get; set; } = DUALForAccdb; /// /// The dual table for accdb @@ -81,7 +73,7 @@ public static object IntegerNullValue /// /// true to show SQL statements; otherwise, false. /// - public static bool ShowSqlStatements = false; + public static bool ShowSqlStatements { get; set; } = false; /// /// Gets or sets a value indicating whether the connection pooling should be used @@ -89,6 +81,6 @@ public static object IntegerNullValue /// /// true to use the connection pooling; otherwise, false. /// - public static bool UseConnectionPooling = false; + public static bool UseConnectionPooling { get; set; } = false; } } \ No newline at end of file diff --git a/src/System.Data.Jet/JetConnection.cs b/src/System.Data.Jet/JetConnection.cs index ace62ec7..f6628a0c 100644 --- a/src/System.Data.Jet/JetConnection.cs +++ b/src/System.Data.Jet/JetConnection.cs @@ -1,8 +1,11 @@ +using System.Collections.Generic; using System.Data.Common; using System.Data.Jet.JetStoreSchemaDefinition; +using System.Diagnostics; using System.IO; using System.Linq; using System.Text.RegularExpressions; +using Microsoft.Win32; namespace System.Data.Jet { @@ -27,9 +30,9 @@ public JetConnection() /// /// Initializes a new instance of the class. /// - /// The connection string. - public JetConnection(string connectionString) - : this(connectionString, null) + /// The file name or connection string (either ODBC or OLE DB). + public JetConnection(string fileNameOrConnectionString) + : this(fileNameOrConnectionString, null) { } @@ -46,12 +49,12 @@ public JetConnection(DbProviderFactory dataAccessProviderFactory) /// /// Initializes a new instance of the class. /// - /// The connection string. + /// The file name or connection string (either ODBC or OLE DB). /// The underlying provider factory to use by Jet. Supported are /// `OdbcFactory` and `OleDbFactory`. - public JetConnection(string connectionString, DbProviderFactory dataAccessProviderFactory) + public JetConnection(string fileNameOrConnectionString, DbProviderFactory dataAccessProviderFactory) { - ConnectionString = connectionString; + ConnectionString = fileNameOrConnectionString; if (dataAccessProviderFactory != null) DataAccessProviderFactory = dataAccessProviderFactory; @@ -176,10 +179,10 @@ public override string ConnectionString { if (State != ConnectionState.Closed) throw new InvalidOperationException(Messages.CannotChangePropertyValueInThisConnectionState(nameof(ConnectionString), State)); - + if (_frozen) throw new InvalidOperationException($"Cannot modify \"{nameof(ConnectionString)}\" property after the connection has been frozen."); - + _connectionString = value; } } @@ -216,7 +219,14 @@ public override string Database /// Gets the name of the file to open. /// public override string DataSource - => GetDatabaseFilePath(_connectionString); + { + get + { + var connectionStringBuilder = JetFactory.InnerFactory.CreateConnectionStringBuilder(); + connectionStringBuilder.ConnectionString = _connectionString; + return connectionStringBuilder.GetDataSource(); + } + } /// /// Releases the unmanaged resources used by the and optionally releases the managed resources. @@ -306,16 +316,48 @@ public override void Open() if (State != ConnectionState.Closed) throw new InvalidOperationException(Messages.CannotCallMethodInThisConnectionState(nameof(Open), ConnectionState.Closed, State)); - if (JetFactory == null) + var connectionString = ConnectionString; + var dataAccessProviderFactory = DataAccessProviderFactory; + var dataAccessProviderType = dataAccessProviderFactory == null + ? JetConfiguration.DefaultDataAccessProviderType + : GetDataAccessProviderType(dataAccessProviderFactory); + + dataAccessProviderFactory ??= JetFactory.Instance.GetDataAccessProviderFactory(dataAccessProviderType); + + // If the connection string is just a file path, we need to construct a valid connection string from it + // by using the default data access provider type (ODBC or OLE DB) and retrieving its most recent + // ACE/Jet provider. + if (IsFileName(connectionString)) { - var dataAccessProviderType = GetDataAccessProviderType(ConnectionString); - DataAccessProviderFactory = JetFactory.Instance.GetDataAccessProviderFactory(dataAccessProviderType); + var fileName = connectionString; + connectionString = GetConnectionString(fileName, dataAccessProviderType); } + // It is possible, that a connection string was provided, that left out the actual ACE/Jet provider + // information, but is in a distinctive style (ODBC or OLE DB) anyway. + // We need to retrieving the data access provider type's most recent ACE/Jet provider in that case. + var connectionStringBuilder = dataAccessProviderFactory.CreateConnectionStringBuilder(); + connectionStringBuilder.ConnectionString = connectionString; + + if (string.IsNullOrWhiteSpace(connectionStringBuilder.GetProvider())) + { + var provider = GetMostRecentCompatibleProviders(dataAccessProviderType) + .FirstOrDefault() + .Key; + + if (provider == null) + throw new InvalidOperationException($"Unable to find any compatible {Enum.GetName(typeof(DataAccessProviderType), dataAccessProviderType)} provider for the connection string: {connectionString}"); + + connectionStringBuilder.SetProvider(provider); + connectionString = connectionStringBuilder.ToString(); + } + + DataAccessProviderFactory ??= dataAccessProviderFactory; + try { InnerConnection = InnerConnectionFactory.Instance.OpenConnection( - ExpandDatabaseFilePath(_connectionString), + ExpandDatabaseFilePath(connectionString), JetFactory.InnerFactory); InnerConnection.StateChange += WrappedConnection_StateChange; @@ -437,41 +479,33 @@ public static void ClearAllPools() public void CreateEmptyDatabase() => CreateEmptyDatabase(DataSource, DataAccessProviderFactory); - public static string CreateEmptyDatabase(string fileName, DbProviderFactory dataAccessProviderFactory) - => AdoxWrapper.CreateEmptyDatabase(fileName, dataAccessProviderFactory); + public static string CreateEmptyDatabase(string fileNameOrConnectionString, DbProviderFactory dataAccessProviderFactory) + => AdoxWrapper.CreateEmptyDatabase(fileNameOrConnectionString, dataAccessProviderFactory); public static string GetConnectionString(string fileName, DbProviderFactory dataAccessProviderFactory) => GetConnectionString(fileName, GetDataAccessProviderType(dataAccessProviderFactory)); public static string GetConnectionString(string fileName, DataAccessProviderType dataAccessProviderType) => GetConnectionString( - dataAccessProviderType == DataAccessProviderType.OleDb - ? JetConfiguration.OleDbDefaultProvider - : JetConfiguration.OdbcDefaultProvider, + GetMostRecentCompatibleProviders(dataAccessProviderType).First().Key, fileName, dataAccessProviderType); public static string GetConnectionString(string provider, string fileName, DbProviderFactory dataAccessProviderFactory) => GetConnectionString(provider, fileName, GetDataAccessProviderType(dataAccessProviderFactory)); - + public static string GetConnectionString(string provider, string fileName, DataAccessProviderType dataAccessProviderType) => dataAccessProviderType == DataAccessProviderType.OleDb ? $"Provider={provider};Data Source={fileName}" : $"Driver={{{provider}}};DBQ={fileName}"; - private string GetDatabaseFilePath(string connectionString) - { - var connectionStringBuilder = JetFactory.InnerFactory.CreateConnectionStringBuilder(); - connectionStringBuilder.ConnectionString = connectionString; - return connectionStringBuilder.GetDataSource(); - } - private string ExpandDatabaseFilePath(string connectionString) { var connectionStringBuilder = JetFactory.InnerFactory.CreateConnectionStringBuilder(); connectionStringBuilder.ConnectionString = connectionString; connectionStringBuilder.SetDataSource(JetStoreDatabaseHandling.ExpandFileName(connectionStringBuilder.GetDataSource())); - return connectionStringBuilder.ConnectionString; + + return connectionStringBuilder.ToString(); } public void DropDatabase() @@ -503,14 +537,23 @@ public static bool DatabaseExists(string fileNameOrConnectionString) public static DataAccessProviderType GetDataAccessProviderType(string connectionString) { - var isOleDb = Regex.IsMatch(connectionString, @"Provider\s*=\s*\w+", RegexOptions.IgnoreCase); - var isOdbc = Regex.IsMatch(connectionString, @"Driver\s*=\s*\{?\w+\}?", RegexOptions.IgnoreCase); + var isOleDb = Regex.IsMatch(connectionString, @"^(?:.*;)?\s*Provider\s*=\s*\w+", RegexOptions.IgnoreCase); + var isOdbc = Regex.IsMatch(connectionString, @"^(?:.*;)?Driver\s*=\s*\{?\w+\}?", RegexOptions.IgnoreCase); if (isOdbc && isOleDb) throw new InvalidOperationException("The connection string appears to be for ODBC and OLE DB. Only one distinct style is supported at a time."); if (!isOdbc && !isOleDb) - throw new ArgumentException("The connection string appears to be neither ODBC nor OLE DB compliant.", nameof(connectionString)); + { + isOleDb = Regex.IsMatch(connectionString, @"^(?:.*;)?\s*Data Source\s*=", RegexOptions.IgnoreCase); + isOdbc = Regex.IsMatch(connectionString, @"^(?:.*;)?\s*DBQ\s*=", RegexOptions.IgnoreCase); + + if (isOdbc && isOleDb) + throw new InvalidOperationException("The connection string appears to be for ODBC and OLE DB. Only one distinct style is supported at a time."); + + if (!isOdbc && !isOleDb) + throw new ArgumentException("The connection string appears to be neither ODBC nor OLE DB compliant.", nameof(connectionString)); + } return isOleDb ? DataAccessProviderType.OleDb @@ -547,5 +590,117 @@ public static DataAccessProviderType GetDataAccessProviderType(DbProviderFactory ? DataAccessProviderType.OleDb : DataAccessProviderType.Odbc; } + + public static KeyValuePair[] GetMostRecentCompatibleProviders(DataAccessProviderType dataAccessProviderType) + => dataAccessProviderType == DataAccessProviderType.OleDb + ? _oledbProviders.Value + .Select(s => Regex.Match(s, @"Microsoft\.(?:ACE|Jet)\.OLEDB\.(?\d+\.\d+)", RegexOptions.IgnoreCase)) + .Where(m => m.Success) + .Select( + m => new KeyValuePair( + m.Value, new Version( + m.Groups["Version"] + .Value))) + .OrderByDescending(kvp => kvp.Value) + .ToArray() + : _odbcProviders.Value + .Where( + kvp => kvp.Key.IndexOf("Microsoft Access Driver", StringComparison.OrdinalIgnoreCase) >= 0 && + (kvp.Key.IndexOf("*.mdb", StringComparison.OrdinalIgnoreCase) >= 0 || kvp.Key.IndexOf("*.accdb", StringComparison.OrdinalIgnoreCase) >= 0)) + .OrderByDescending(kvp => kvp.Value) + .ThenByDescending(kvp => kvp.Key.IndexOf("*.accdb", StringComparison.OrdinalIgnoreCase)) + .ToArray(); + + private static readonly Lazy> _odbcProviders = new Lazy>(() => + { + var drivers = new Dictionary(); + + try + { + using (var odbcKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\ODBC\ODBCINST.INI")) + { + if (odbcKey != null) + { + var driverKeyNames = odbcKey.GetSubKeyNames(); + + foreach (var driverKeyName in driverKeyNames) + { + try + { + using (var driverKey = odbcKey.OpenSubKey(driverKeyName)) + { + if (driverKey?.GetValue("Driver") != null) + { + if (driverKey.GetValue("DriverODBCVer") is string versionString) + { + drivers.Add(driverKeyName, new Version(versionString)); + } + } + } + } + catch + { + // ignored + } + } + } + } + } + catch + { + // ignored + } + + return drivers; + }, true); + + private static readonly Lazy _oledbProviders = new Lazy(() => + { + var providers = new List(); + + try + { + using (var clsidKey = Registry.ClassesRoot.OpenSubKey(@"CLSID")) + { + if (clsidKey != null) + { + var clasidSubKeyNames = clsidKey.GetSubKeyNames(); + + foreach (var clsidSubKeyName in clasidSubKeyNames) + { + try + { + using (var clsidSubKey = clsidKey.OpenSubKey(clsidSubKeyName)) + { + if (clsidSubKey?.GetValue("OLEDB_SERVICES") != null) + { + if (clsidSubKey.GetValue(null) is string provider) + { + providers.Add(provider); + } + } + } + } + catch + { + // ignored + } + } + } + } + } + catch + { + // ignored + } + + return providers.ToArray(); + }, true); + + public static bool IsConnectionString(string fileNameOrConnectionString) + => JetStoreDatabaseHandling.IsConnectionString(fileNameOrConnectionString); + + public static bool IsFileName(string fileNameOrConnectionString) + => JetStoreDatabaseHandling.IsFileName(fileNameOrConnectionString); } } \ No newline at end of file diff --git a/src/System.Data.Jet/JetDataReader.cs b/src/System.Data.Jet/JetDataReader.cs index 8c6d6dbd..9695d5b8 100644 --- a/src/System.Data.Jet/JetDataReader.cs +++ b/src/System.Data.Jet/JetDataReader.cs @@ -1,12 +1,10 @@ -using System; -using System.Data.Common; +using System.Data.Common; using System.Text; using System.Threading; -using System.Threading.Tasks; namespace System.Data.Jet { - class JetDataReader : DbDataReader + internal class JetDataReader : DbDataReader { #if DEBUG private static int _activeObjectsCount; @@ -24,15 +22,15 @@ public JetDataReader(DbDataReader dataReader, int topCount, int skipCount) : this(dataReader) { _topCount = topCount; - for (int i = 0; i < skipCount; i++) + for (var i = 0; i < skipCount; i++) { _wrappedDataReader.Read(); } } - private DbDataReader _wrappedDataReader; - private readonly int _topCount = 0; - private int _readCount = 0; + private readonly DbDataReader _wrappedDataReader; + private readonly int _topCount; + private int _readCount; public override void Close() { @@ -43,64 +41,81 @@ public override void Close() } public override int Depth - { - get { return _wrappedDataReader.Depth; } - } + => _wrappedDataReader.Depth; public override int FieldCount - { - get { return _wrappedDataReader.FieldCount; } - } + => _wrappedDataReader.FieldCount; public override bool GetBoolean(int ordinal) { - object booleanObject = _wrappedDataReader.GetValue(ordinal); - if (booleanObject == null) - throw new InvalidOperationException("Cannot cast null to boolean"); - if (booleanObject is bool) - return _wrappedDataReader.GetBoolean(ordinal); - else if (booleanObject is short) - return ((short) booleanObject) != 0; - else - throw new InvalidOperationException(string.Format("Cannot convert {0} to boolean", booleanObject.GetType())); + var value = _wrappedDataReader.GetValue(ordinal); + if (value is bool boolValue) + return boolValue; + + if (value is sbyte sbyteValue) + return sbyteValue != 0; + if (value is byte byteValue) + return byteValue != 0; + if (value is short shortValue) + return shortValue != 0; + if (value is ushort ushortValue) + return ushortValue != 0; + if (value is int intValue) + return intValue != 0; + if (value is uint uintValue) + return uintValue != 0; + if (value is long longValue) + return longValue != 0; + if (value is ulong ulongValue) + return ulongValue != 0; + if (value is decimal decimalValue) + return decimalValue != 0; + + return (bool) value; } public override byte GetByte(int ordinal) { - return Convert.ToByte(_wrappedDataReader.GetValue(ordinal)); + var value = GetValue(ordinal); + if (value is byte byteValue) + return byteValue; + + if (value is sbyte sbyteValue) + return checked((byte) sbyteValue); + if (value is short shortValue) + return checked((byte) shortValue); + if (value is ushort ushortValue) + return checked((byte) ushortValue); + if (value is int intValue) + return checked((byte) intValue); + if (value is uint uintValue) + return checked((byte) uintValue); + if (value is long longValue) + return checked((byte) longValue); + if (value is ulong ulongValue) + return checked((byte) ulongValue); + if (value is decimal decimalValue) + return (byte) decimalValue; + return (byte) value; } public override long GetBytes(int ordinal, long dataOffset, byte[] buffer, int bufferOffset, int length) - { - return _wrappedDataReader.GetBytes(ordinal, dataOffset, buffer, bufferOffset, length); - } + => _wrappedDataReader.GetBytes(ordinal, dataOffset, buffer, bufferOffset, length); public override char GetChar(int ordinal) - { - return _wrappedDataReader.GetChar(ordinal); - } + => _wrappedDataReader.GetChar(ordinal); public override long GetChars(int ordinal, long dataOffset, char[] buffer, int bufferOffset, int length) - { - return _wrappedDataReader.GetChars(ordinal, dataOffset, buffer, bufferOffset, length); - } + => _wrappedDataReader.GetChars(ordinal, dataOffset, buffer, bufferOffset, length); public override string GetDataTypeName(int ordinal) - { - return _wrappedDataReader.GetDataTypeName(ordinal); - } + => _wrappedDataReader.GetDataTypeName(ordinal); public override DateTime GetDateTime(int ordinal) - { - return _wrappedDataReader.GetDateTime(ordinal); - } + => _wrappedDataReader.GetDateTime(ordinal); public virtual TimeSpan GetTimeSpan(int ordinal) - { - TimeSpan timeSpan = GetDateTime(ordinal) - JetConfiguration.TimeSpanOffset; - - return timeSpan; - } + => GetDateTime(ordinal) - JetConfiguration.TimeSpanOffset; public virtual DateTimeOffset GetDateTimeOffset(int ordinal) { @@ -135,7 +150,7 @@ public override float GetFloat(int ordinal) public override Guid GetGuid(int ordinal) { // Fix for discussion https://jetentityframeworkprovider.codeplex.com/discussions/647028 - object value = _wrappedDataReader.GetValue(ordinal); + var value = _wrappedDataReader.GetValue(ordinal); if (value is byte[]) return new Guid((byte[]) value); else @@ -150,11 +165,11 @@ public override short GetInt16(int ordinal) public override int GetInt32(int ordinal) { // Fix for discussion https://jetentityframeworkprovider.codeplex.com/discussions/647028 - object value = _wrappedDataReader.GetValue(ordinal); + var value = _wrappedDataReader.GetValue(ordinal); if (value is string) { - byte[] buffer = Encoding.Unicode.GetBytes((string) value); - int intValue = BitConverter.ToInt32(buffer, 0); + var buffer = Encoding.Unicode.GetBytes((string) value); + var intValue = BitConverter.ToInt32(buffer, 0); return intValue; } else @@ -207,14 +222,10 @@ public override int GetValues(object[] values) } public override bool HasRows - { - get { return _wrappedDataReader.HasRows; } - } + => _wrappedDataReader.HasRows; public override bool IsClosed - { - get { return _wrappedDataReader.IsClosed; } - } + => _wrappedDataReader.IsClosed; public override bool IsDBNull(int ordinal) { @@ -240,18 +251,12 @@ public override bool Read() } public override int RecordsAffected - { - get { return _wrappedDataReader.RecordsAffected; } - } + => _wrappedDataReader.RecordsAffected; public override object this[string name] - { - get { return _wrappedDataReader[name]; } - } + => _wrappedDataReader[name]; public override object this[int ordinal] - { - get { return _wrappedDataReader[ordinal]; } - } + => _wrappedDataReader[ordinal]; } } \ No newline at end of file diff --git a/src/System.Data.Jet/JetStoreSchemaDefinition/JetStoreDatabaseHandling.cs b/src/System.Data.Jet/JetStoreSchemaDefinition/JetStoreDatabaseHandling.cs index 48427aa8..ad6d1dea 100644 --- a/src/System.Data.Jet/JetStoreSchemaDefinition/JetStoreDatabaseHandling.cs +++ b/src/System.Data.Jet/JetStoreSchemaDefinition/JetStoreDatabaseHandling.cs @@ -1,4 +1,6 @@ +using System.Diagnostics; using System.IO; +using System.Linq; using System.Runtime.CompilerServices; using System.Text.RegularExpressions; @@ -9,13 +11,15 @@ static class JetStoreDatabaseHandling private static readonly Regex _regExIsCreateOrDropDatabaseCommand; private static readonly Regex _regExParseCreateDatabaseCommand; private static readonly Regex _regExParseDropDatabaseCommand; + private static readonly Regex _regExIsConnectionString; + private static readonly Regex _regExHasProvider; + private static readonly Regex _regExExtractFilenameFromConnectionString; // TODO: Remove this obsolete block for .NET 5. private static readonly Regex _regExParseObsoleteCreateDatabaseCommand; private static readonly Regex _regExParseObsoleteDropDatabaseCommand; private static readonly Regex _regExParseObsoleteCreateDatabaseCommandFromConnection; private static readonly Regex _regExParseObsoleteDropDatabaseCommandFromConnection; - private static readonly Regex _regExExtractFilenameFromConnectionString; static JetStoreDatabaseHandling() { @@ -33,30 +37,44 @@ static JetStoreDatabaseHandling() @"^\s*drop\s+database\s+'\s*(?(?:''|[^'])*)\s*'\s*(?:;|$)", RegexOptions.IgnoreCase); + // Provider=Microsoft.ACE.OLEDB.12.0 + // Driver=Microsoft.ACE.OLEDB.12.0 + // Data Source=Joe's Database.accdb + // DBQ=Joe's Database.accdb + _regExIsConnectionString = new Regex( + @"^(?:.*;)?\s*(?:provider|driver|data source|dbq)\s*=", + RegexOptions.IgnoreCase); + + // Provider=Microsoft.ACE.OLEDB.12.0 + // Driver=Microsoft.ACE.OLEDB.12.0 + _regExHasProvider = new Regex( + @"^(?:.*;)?\s*(?:provider|driver)\s*=", + RegexOptions.IgnoreCase); + + // Provider=Microsoft.ACE.OLEDB.12.0;Data Source=Joe's Database.accdb; + _regExExtractFilenameFromConnectionString = new Regex( + @"^(?:.*;)?\s*(?:data source|dbq)\s*=\s*(?.*?)\s*(?:;|$)", + RegexOptions.IgnoreCase); + // CREATE DATABASE Joe's Database.accdb; _regExParseObsoleteCreateDatabaseCommand = new Regex( @"^\s*create\s+database\s+(?.*?)\s*(?:;|$)", RegexOptions.IgnoreCase); - // DROP DATABASE 'Joe's Database.accdb'; + // DROP DATABASE Joe's Database.accdb; _regExParseObsoleteDropDatabaseCommand = new Regex( @"^\s*drop\s+database\s+(?.*?)\s*(?:;|$)", RegexOptions.IgnoreCase); - // CREATE DATABASE Provider=Microsoft.ACE.OLEDB.12.0;Data Source=Joe's Database.accdb'; + // CREATE DATABASE Provider=Microsoft.ACE.OLEDB.12.0;Data Source=Joe's Database.accdb; _regExParseObsoleteCreateDatabaseCommandFromConnection = new Regex( @"^\s*create\s+database\s+(?(provider)\s*=\s*.*?)\s*$", RegexOptions.IgnoreCase); - // DROP DATABASE Provider=Microsoft.ACE.OLEDB.12.0;Data Source=Joe's Database.accdb'; + // DROP DATABASE Provider=Microsoft.ACE.OLEDB.12.0;Data Source=Joe's Database.accdb; _regExParseObsoleteDropDatabaseCommandFromConnection = new Regex( @"^\s*drop\s+database\s+(?provider\s*=\s*.*?)\s*$", RegexOptions.IgnoreCase); - - // Provider=Microsoft.ACE.OLEDB.12.0;Data Source=Joe's Database.accdb'; - _regExExtractFilenameFromConnectionString = new Regex( - @"(?:.*;)?\s*(?:data source|dbq)\s*=\s*(?.*?)\s*(?:;|$)", - RegexOptions.IgnoreCase); } public static bool TryDatabaseOperation(JetCommand command) @@ -161,6 +179,19 @@ public static string ExtractFileNameFromConnectionString(string connectionString return fileName; } + public static bool IsConnectionString(string connectionString) + => _regExIsConnectionString.IsMatch(connectionString); + + public static bool IsFileName(string fileName) + => !string.IsNullOrWhiteSpace(fileName) && + !IsConnectionString(fileName) && + !fileName.ToCharArray() + .Intersect(Path.GetInvalidPathChars()) + .Any(); + + public static bool HasProvider(string connectionString) + => _regExHasProvider.IsMatch(connectionString); + public static void DeleteFile(string fileName) { if (!File.Exists(fileName)) diff --git a/src/System.Data.Jet/System.Data.Jet.csproj b/src/System.Data.Jet/System.Data.Jet.csproj index 472c32c6..0004cb70 100644 --- a/src/System.Data.Jet/System.Data.Jet.csproj +++ b/src/System.Data.Jet/System.Data.Jet.csproj @@ -39,6 +39,7 @@ + diff --git a/test/EFCore.Jet.FunctionalTests/config.json b/test/EFCore.Jet.FunctionalTests/config.json index f5fe4f83..6c66070b 100644 --- a/test/EFCore.Jet.FunctionalTests/config.json +++ b/test/EFCore.Jet.FunctionalTests/config.json @@ -1,7 +1,7 @@ { "Test": { "Jet": { - "DefaultConnection": "Driver={Microsoft Access Driver (*.mdb, *.accdb)};DBQ=Jet.accdb;ExtendedAnsiSQL=1" + "DefaultConnection": "DBQ=Jet.accdb" } } } From bdfbafa5f79a72ea151e469d72fc2e1a0c8722b0 Mon Sep 17 00:00:00 2001 From: Lau Date: Sun, 29 Mar 2020 11:20:18 +0200 Subject: [PATCH 3/5] Implicitly add `ExtendedAnsiSQL=1` to ODBC connection strings. --- src/System.Data.Jet/JetConnection.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/System.Data.Jet/JetConnection.cs b/src/System.Data.Jet/JetConnection.cs index f6628a0c..a153f866 100644 --- a/src/System.Data.Jet/JetConnection.cs +++ b/src/System.Data.Jet/JetConnection.cs @@ -351,6 +351,16 @@ public override void Open() connectionStringBuilder.SetProvider(provider); connectionString = connectionStringBuilder.ToString(); } + + // Enable ExtendedAnsiSQL when using ODBC to support ODBC 4.0 statements (like CREATE VIEW). + if (dataAccessProviderType == DataAccessProviderType.Odbc) + { + if (!connectionStringBuilder.ContainsKey("ExtendedAnsiSQL")) + { + connectionStringBuilder["ExtendedAnsiSQL"] = 1; + connectionString = connectionStringBuilder.ToString(); + } + } DataAccessProviderFactory ??= dataAccessProviderFactory; From 0df985e00d749e235761b75801b43531097f16e0 Mon Sep 17 00:00:00 2001 From: Lau Date: Sun, 29 Mar 2020 12:43:26 +0200 Subject: [PATCH 4/5] Fix smaller issues and add TODOs. --- .../Migrations/JetMigrationsSqlGenerator.cs | 6 ++++++ src/System.Data.Jet/AdoxWrapper.cs | 14 ++++++++------ src/System.Data.Jet/ComObject.cs | 2 +- src/System.Data.Jet/JetConnection.cs | 3 +++ .../JetStoreSchemaDefinition/JetRenameHandling.cs | 3 +++ 5 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/EFCore.Jet/Migrations/JetMigrationsSqlGenerator.cs b/src/EFCore.Jet/Migrations/JetMigrationsSqlGenerator.cs index c7644b6c..cc11d0ff 100644 --- a/src/EFCore.Jet/Migrations/JetMigrationsSqlGenerator.cs +++ b/src/EFCore.Jet/Migrations/JetMigrationsSqlGenerator.cs @@ -364,6 +364,12 @@ protected override void Generate( Check.NotNull(operation, nameof(operation)); Check.NotNull(builder, nameof(builder)); + // CHECK: Rename table operations require extensions like ADOX or DAO. + // A native way to do this would be to: + // 1. CREATE TABLE `destination table` + // 2. INSERT INTO ... SELECT ... FROM + // 3. DROP TABLE `source table` + // 4. Recrete indices and references. builder.Append("RENAME TABLE ") .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name)) .Append(" TO ") diff --git a/src/System.Data.Jet/AdoxWrapper.cs b/src/System.Data.Jet/AdoxWrapper.cs index 7fff2d2b..b1f0de43 100644 --- a/src/System.Data.Jet/AdoxWrapper.cs +++ b/src/System.Data.Jet/AdoxWrapper.cs @@ -24,6 +24,8 @@ public static void RenameTable(string connectionString, string tableName, string } catch (Exception e) { + // TODO: Try interating over the _Tables collection instead of using Item["TableName"]. + throw new Exception("Cannot rename table", e); } finally @@ -48,7 +50,8 @@ public static void RenameColumn(string connectionString, string tableName, strin try { using var tables = catalog.Tables; - using var columns = tables[tableName].Columns; + using var table = tables[tableName]; + using var columns = table.Columns; using var column = columns[columnName]; column.Name = newColumnName; } @@ -92,8 +95,7 @@ public static string CreateEmptyDatabase(string fileNameOrConnectionString, DbPr connection.DataAccessProviderFactory = dataAccessProviderFactory; connection.Open(); - string sql = @" -CREATE TABLE `MSysAccessStorage` ( + var sql = @"CREATE TABLE `MSysAccessStorage` ( `DateCreate` DATETIME NULL, `DateUpdate` DATETIME NULL, `Id` COUNTER NOT NULL, @@ -136,9 +138,9 @@ private static dynamic GetCatalogInstanceAndOpen(string errorPrefix, string conn try { - using dynamic cnn = new ComObject("ADODB.Connection"); - cnn.Open(connectionString); - catalog.ActiveConnection = cnn; + using dynamic connection = new ComObject("ADODB.Connection"); + connection.Open(connectionString); + catalog.ActiveConnection = connection; } catch (Exception e) { diff --git a/src/System.Data.Jet/ComObject.cs b/src/System.Data.Jet/ComObject.cs index 4ab5e913..a6c3676f 100644 --- a/src/System.Data.Jet/ComObject.cs +++ b/src/System.Data.Jet/ComObject.cs @@ -110,7 +110,7 @@ private static object WrapIfRequired(object obj) public void Dispose() { - // The RCW is a .NET object and cannot be released from the finalizer anymore, + // The RCW is a .NET object and cannot be released from the finalizer, // because it might not exist anymore. if (_instance != null) { diff --git a/src/System.Data.Jet/JetConnection.cs b/src/System.Data.Jet/JetConnection.cs index a153f866..34690525 100644 --- a/src/System.Data.Jet/JetConnection.cs +++ b/src/System.Data.Jet/JetConnection.cs @@ -489,6 +489,9 @@ public static void ClearAllPools() public void CreateEmptyDatabase() => CreateEmptyDatabase(DataSource, DataAccessProviderFactory); + // TODO: Use the `CREATE_DB` connection string option instead of calling ADOX when using ODBC, to create + // a new database file. + // Alternatively, use DAO in conjunction with ODBC. public static string CreateEmptyDatabase(string fileNameOrConnectionString, DbProviderFactory dataAccessProviderFactory) => AdoxWrapper.CreateEmptyDatabase(fileNameOrConnectionString, dataAccessProviderFactory); diff --git a/src/System.Data.Jet/JetStoreSchemaDefinition/JetRenameHandling.cs b/src/System.Data.Jet/JetStoreSchemaDefinition/JetRenameHandling.cs index a62b8c3c..f9db0e0c 100644 --- a/src/System.Data.Jet/JetStoreSchemaDefinition/JetRenameHandling.cs +++ b/src/System.Data.Jet/JetStoreSchemaDefinition/JetRenameHandling.cs @@ -22,7 +22,10 @@ public static bool TryDatabaseOperation(string connectionString, string commandT { string tableName = match.Groups["tableName"].Value; string newTableName = match.Groups["newTableName"].Value; + + // TODO: Only use ADOX in an OLE DB context. Use DAO in an ODBC context. AdoxWrapper.RenameTable(connectionString, RemoveBrackets(tableName), RemoveBrackets(newTableName)); + return true; } From 6141f1e581da5b93cf4f189e81020fc3fb344c60 Mon Sep 17 00:00:00 2001 From: Lau Date: Sun, 29 Mar 2020 13:38:49 +0200 Subject: [PATCH 5/5] Update still present square bracket identifiers to backticks. --- .../Internal/JetDatabaseModelFactory.cs | 3 +- src/System.Data.Jet/JetCommand.cs | 2 +- src/System.Data.Jet/JetConnection.cs | 3 +- .../JetRenameHandling.cs | 47 +++++++------------ .../JetStoreSchemaDefinitionRetrieve.cs | 8 ++-- .../SystemTableCollection.cs | 8 ++-- src/System.Data.Jet/JetSyntaxHelper.cs | 6 +-- 7 files changed, 33 insertions(+), 44 deletions(-) diff --git a/src/EFCore.Jet/Scaffolding/Internal/JetDatabaseModelFactory.cs b/src/EFCore.Jet/Scaffolding/Internal/JetDatabaseModelFactory.cs index 332f9371..eddc5604 100644 --- a/src/EFCore.Jet/Scaffolding/Internal/JetDatabaseModelFactory.cs +++ b/src/EFCore.Jet/Scaffolding/Internal/JetDatabaseModelFactory.cs @@ -34,7 +34,7 @@ public class JetDatabaseModelFactory : DatabaseModelFactory private Dictionary _tableColumns; private static string ObjectKey([NotNull] string name) - => "[" + name + "]"; + => "`" + name + "`"; private static string TableKey(DatabaseTable table) => TableKey(table.Name); @@ -48,6 +48,7 @@ private static string ColumnKey(DatabaseTable table, string columnName) private static readonly List _tablePatterns = new List { "{table}", + "`{table}`", "[{table}]" }; diff --git a/src/System.Data.Jet/JetCommand.cs b/src/System.Data.Jet/JetCommand.cs index 938337b1..5def4e27 100644 --- a/src/System.Data.Jet/JetCommand.cs +++ b/src/System.Data.Jet/JetCommand.cs @@ -19,7 +19,7 @@ public class JetCommand : DbCommand, ICloneable private int? _rowCount; private static readonly Regex _skipRegularExpression = new Regex(@"\bskip\s(?@.*)\b", RegexOptions.IgnoreCase); - private static readonly Regex _selectRowCountRegularExpression = new Regex(@"^\s*select\s*@@rowcount\s*[;]?\s*$", RegexOptions.IgnoreCase); + private static readonly Regex _selectRowCountRegularExpression = new Regex(@"^\s*select\s*@@rowcount\s*;?\s*$", RegexOptions.IgnoreCase); private static readonly Regex _ifStatementRegex = new Regex(@"^\s*if\s*(?not)?\s*exists\s*\((?.+)\)\s*then\s*(?.*)$", RegexOptions.IgnoreCase); protected JetCommand(JetCommand source) diff --git a/src/System.Data.Jet/JetConnection.cs b/src/System.Data.Jet/JetConnection.cs index 34690525..9c1cdd5f 100644 --- a/src/System.Data.Jet/JetConnection.cs +++ b/src/System.Data.Jet/JetConnection.cs @@ -417,8 +417,7 @@ public bool TableExists(string tableName) try { - var sqlFormat = "select count(*) from [{0}] where 1=2"; - CreateCommand(string.Format(sqlFormat, tableName)) + CreateCommand($"select count(*) from `{tableName}` where 1=2") .ExecuteNonQuery(); tableExists = true; } diff --git a/src/System.Data.Jet/JetStoreSchemaDefinition/JetRenameHandling.cs b/src/System.Data.Jet/JetStoreSchemaDefinition/JetRenameHandling.cs index f9db0e0c..2e76a718 100644 --- a/src/System.Data.Jet/JetStoreSchemaDefinition/JetRenameHandling.cs +++ b/src/System.Data.Jet/JetStoreSchemaDefinition/JetRenameHandling.cs @@ -2,29 +2,26 @@ namespace System.Data.Jet.JetStoreSchemaDefinition { - class JetRenameHandling + internal class JetRenameHandling { - private static Regex _renameTableRegex = new Regex( - $@"^\s*rename\s+table\s+{GetQuotedOrUnquotedNamePattern("tableName")}\s+to\s+{GetQuotedOrUnquotedNamePattern("newTableName")}\s*$", + private static readonly Regex _renameTableRegex = new Regex( + $@"^\s*rename\s+table\s+{GetIdentifierPattern("OldTableName")}\s+to\s+{GetIdentifierPattern("NewTableName")}\s*$", RegexOptions.IgnoreCase); - private static Regex _renameTableColumnRegex = new Regex( - $@"^\s*rename\s+column\s+{GetQuotedOrUnquotedNamePattern("tableName")}\.{GetQuotedOrUnquotedNamePattern("columnName")}\s+to\s+{GetQuotedOrUnquotedNamePattern("newColumnName")}\s*$", + private static readonly Regex _renameTableColumnRegex = new Regex( + $@"^\s*rename\s+column\s+{GetIdentifierPattern("TableName")}\s*\.\s*{GetIdentifierPattern("OldColumnName")}\s+to\s+{GetIdentifierPattern("NewColumnName")}\s*$", RegexOptions.IgnoreCase); public static bool TryDatabaseOperation(string connectionString, string commandText) { - Match match; - - - match = _renameTableRegex.Match(commandText); + var match = _renameTableRegex.Match(commandText); if (match.Success) { - string tableName = match.Groups["tableName"].Value; - string newTableName = match.Groups["newTableName"].Value; + var oldTableName = match.Groups["OldTableName"].Value; + var newTableName = match.Groups["NewTableName"].Value; // TODO: Only use ADOX in an OLE DB context. Use DAO in an ODBC context. - AdoxWrapper.RenameTable(connectionString, RemoveBrackets(tableName), RemoveBrackets(newTableName)); + AdoxWrapper.RenameTable(connectionString, oldTableName, newTableName); return true; } @@ -32,28 +29,20 @@ public static bool TryDatabaseOperation(string connectionString, string commandT match = _renameTableColumnRegex.Match(commandText); if (match.Success) { - string tableName = match.Groups["tableName"].Value; - string columnName = match.Groups["columnName"].Value; - string newColumnName = match.Groups["newColumnName"].Value; - AdoxWrapper.RenameColumn(connectionString, RemoveBrackets(tableName), RemoveBrackets(columnName), RemoveBrackets(newColumnName)); + var tableName = match.Groups["TableName"].Value; + var oldColumnName = match.Groups["OldColumnName"].Value; + var newColumnName = match.Groups["NewColumnName"].Value; + + // TODO: Only use ADOX in an OLE DB context. Use DAO in an ODBC context. + AdoxWrapper.RenameColumn(connectionString, tableName, oldColumnName, newColumnName); + return true; } return false; } - private static string RemoveBrackets(string name) - { - if (name.StartsWith("[") && name.EndsWith("]")) - return name.Substring(1, name.Length - 2); - else - return name; - } - - - static string GetQuotedOrUnquotedNamePattern(string key) - { - return $@"((?<{key}>\S*)|\[(?<{key}>.*)\])"; - } + private static string GetIdentifierPattern(string key) + => $@"(?:`(?<{key}>.*?)`|\[(?<{key}>.*?)\]|(?<{key}>\S*))"; } } diff --git a/src/System.Data.Jet/JetStoreSchemaDefinition/JetStoreSchemaDefinitionRetrieve.cs b/src/System.Data.Jet/JetStoreSchemaDefinition/JetStoreSchemaDefinitionRetrieve.cs index 6aa7fc50..8802f6c8 100644 --- a/src/System.Data.Jet/JetStoreSchemaDefinition/JetStoreSchemaDefinitionRetrieve.cs +++ b/src/System.Data.Jet/JetStoreSchemaDefinition/JetStoreSchemaDefinitionRetrieve.cs @@ -85,8 +85,8 @@ private static DbDataReader GetDbDataReaderFromComplexStatement(DbConnection con if (showStatementPosition == -1) continue; - commandText = commandText.ReplaceCaseInsensitive("\\(\\s*show " + table.Name + "\\s*\\)", "[" + table.TableName + "]"); - commandText = commandText.ReplaceCaseInsensitive("show " + table.Name, "[" + table.TableName + "]"); + commandText = commandText.ReplaceCaseInsensitive("\\(\\s*show " + table.Name + "\\s*\\)", "`" + table.TableName + "`"); + commandText = commandText.ReplaceCaseInsensitive("show " + table.Name, "`" + table.TableName + "`"); tablesToCreate.Add(table); } @@ -174,13 +174,13 @@ private static string GetInsertStatement(string tableName, DataRow row) values += ", "; } - columns += string.Format("[{0}]", column.ColumnName); + columns += string.Format("`{0}`", column.ColumnName); object value = row[column]; values += JetSyntaxHelper.ToSqlStringSwitch(value); } } - return string.Format("INSERT INTO [{0}] ({1}) VALUES ({2})", tableName, columns, values); + return string.Format("INSERT INTO `{0}` ({1}) VALUES ({2})", tableName, columns, values); } [DebuggerStepThrough] diff --git a/src/System.Data.Jet/JetStoreSchemaDefinition/SystemTableCollection.cs b/src/System.Data.Jet/JetStoreSchemaDefinition/SystemTableCollection.cs index 5c7f60f5..3a54e99d 100644 --- a/src/System.Data.Jet/JetStoreSchemaDefinition/SystemTableCollection.cs +++ b/src/System.Data.Jet/JetStoreSchemaDefinition/SystemTableCollection.cs @@ -71,11 +71,11 @@ public void Refresh() foreach (SystemTable table in this) { - table.DropStatement = string.Format("DROP TABLE [{0}]", table.TableName); - table.ClearStatement = string.Format("DELETE FROM [{0}]", table.TableName); + table.DropStatement = string.Format("DROP TABLE `{0}`", table.TableName); + table.ClearStatement = string.Format("DELETE FROM `{0}`", table.TableName); StringBuilder createStatementStringBuilder = new StringBuilder(); - createStatementStringBuilder.AppendFormat("CREATE TABLE [{0}]\r\n", table.TableName); + createStatementStringBuilder.AppendFormat("CREATE TABLE `{0}`\r\n", table.TableName); createStatementStringBuilder.Append("("); bool first = true; foreach (Column column in table.Columns) @@ -85,7 +85,7 @@ public void Refresh() else createStatementStringBuilder.Append(","); createStatementStringBuilder.AppendLine(); - createStatementStringBuilder.AppendFormat(" [{0}] {1}{2} {3}", + createStatementStringBuilder.AppendFormat(" `{0}` {1}{2} {3}", column.Name, column.Type, column.MaxLength == null ? "" : string.Format("({0})", column.MaxLength), diff --git a/src/System.Data.Jet/JetSyntaxHelper.cs b/src/System.Data.Jet/JetSyntaxHelper.cs index 493cb987..66966658 100644 --- a/src/System.Data.Jet/JetSyntaxHelper.cs +++ b/src/System.Data.Jet/JetSyntaxHelper.cs @@ -15,7 +15,7 @@ public static string ReplaceCaseInsensitive(this string s, string oldValue, stri public static string ToSqlString(string value) { // In Jet everything's unicode - return "'" + value.Replace("'", "''") + "'"; + return $"'{value.Replace("'", "''")}'"; } public static string ToSqlString(int value) @@ -84,9 +84,9 @@ public static string ToSqlString(Guid value) internal static string QuoteIdentifier(string name) { if (string.IsNullOrWhiteSpace(name)) - throw new ArgumentNullException("name"); + throw new ArgumentNullException(nameof(name)); - return "[" + name.Replace("]", "]]") + "]"; + return $"`{name}`"; } ///