using System; using System.Collections.Generic; using System.Data.Common; using System.Linq; using System.Runtime.InteropServices; using System.Text; using Microsoft.Win32; using Sarand.Net.Standard.Base.Core; using Sarand.Net.Standard.Base.Bus; using Sarand.Net.Standard.Base.Dbas; using Sarand.Net.Standard.Base.Utils; using Sarand.Net.Standard.Base.App; using Sarand.Net.Standard.Dbas.Data; namespace Sarand.Net.Standard.Dbas.Base { //======================================================================= public abstract class SacsDatabase : SacsStateItem, ISacsDatabase { //------------------------------------------------------------------- // Data members. //------------------------------------------------------------------- // Construction. protected SacsDatabase(DbmsType dbms, String databaseName, String connectionString) { DbmsType = dbms; DatabaseName = databaseName; ConnectionString = connectionString; } //------------------------------------------------------------------- // Database properties. public String ConnectionString { get; } public DbmsType DbmsType { get; } public String DatabaseName { get; } public SelectLockMode SelectLockMode { get; set; } = SelectLockMode.Default; public bool DisplayErrors { get; set; } = true; public abstract void OnConnectionChanged(); public abstract DbConnection CreateConnection(bool forceOdbc = false); public abstract DbDataAdapter CreateDataAdapter(); // Profile properties. public static String ProfileSection_DbUtilities { get; } = "DbUtilities"; public static String ProfileValue_DbExportFile { get; } = "DbExportFile"; public static String ProfileValue_DbExportDsn { get; } = "DbExportDsn"; public static String ProfileValue_DbExportFmt { get; } = "DbExportFmt"; public static String ProfileValue_SecondArchivePath { get; } = "SecondaryArchivePath"; public static String ProfileValue_SecondArchive { get; } = "SecondaryArchive"; public static String ProfileValue_CompareSourceDbDsn { get; } = "CompareDbSourceDsn"; public static String ProfileValue_CompareTargetDbDsn { get; } = "CompareDbTargetDsn"; public static String ProfileValue_DlgPos_DbView { get; } = "DlgPos:DbView"; public static String ProfileValue_DlgPos_DbCompare { get; } = "DlgPos:DbCompare"; //------------------------------------------------------------------- // I d e n t i t y G e n e r a t i o n //------------------------------------------------------------------- protected String NextIdTable { get; set; } = "NextId"; protected String NextIdColumn_Object { get; set; } = "Object"; protected String NextIdColumn_NextId { get; set; } = "NextId"; public abstract SysId GetNextId(String objectName, DbUpdateInfo dbUpdateInfo, bool databaseTable = true, String idColumn = "Id"); public abstract int GetNextIdFromTable(string objectName, string idColumn = "Id"); public abstract bool SetMinNextId(String objectName, SysId id, DbUpdateInfo dbUpdateInfo, bool databaseTable = true, String idColumn = "Id", bool force = false); //------------------------------------------------------------------- // Password utilities. //------------------------------------------------------------------- /// /// Generate password hash using the specified method. /// - Returns hex notation as a string. /// /// /// String of hex digits corresponding to encrypted password. public String EncodePassword_Hex(String password, ISacsDatabase.PasswordHashMethod method = ISacsDatabase.PasswordHashMethod.MsSql) { ClearErrorState(); if (method == ISacsDatabase.PasswordHashMethod.MsSql) { if (DbmsType == DbmsType.MsSql) { try { String colAlias = "DataValue"; String query = $"Select {colAlias}=Convert(VarChar(2000),PwdEncrypt('{DbQuery.FormatForCondition(password)}'),2);"; using (DbResultSet dbResultSet = DbResultSet_DataReader.ExecuteQuery(this, query)) { if (dbResultSet.NextRecord()) return dbResultSet.GetRecordValues().GetValue(colAlias).ToString(); else if (dbResultSet.HasErrorState()) SetErrorState(dbResultSet.ErrorStateMessage); } } catch (Exception exc) { SaLogger.Error($"{SysUtil.Fn} {ExceptionUtil.GetLogMessage(exc, "hashing password")}", true); SetErrorState(exc.Message); } } else SaLogger.Warn($"{SysUtil.Fn} Unable to encode password by method 'SQL'. Database is '{DbmsType}'."); } else SaLogger.Warn($"{SysUtil.Fn} Unable to encode password. Unrecognised method: '{method}'."); return ""; } /// /// Compare the password to the encrypted hash using the specified method. /// /// /// /// True if the password matches the encrypted hash value. public bool ComparePassword_Hex(String password, String passwordHashHex, ISacsDatabase.PasswordHashMethod method = ISacsDatabase.PasswordHashMethod.MsSql) { ClearErrorState(); if (method == ISacsDatabase.PasswordHashMethod.MsSql) { if (DbmsType == DbmsType.MsSql) { try { String colAlias = "PwdCompare"; String query = $"Select {colAlias}=PwdCompare('{DbQuery.FormatForCondition(password)}', Convert(VarBinary(1000),'{DbQuery.FormatForCondition(passwordHashHex)}',2));"; using (DbResultSet dbResultSet = DbResultSet_DataReader.ExecuteQuery(this, query)) { if (dbResultSet.NextRecord()) return StringUtil.ToBool(dbResultSet.GetRecordValues().GetValue(colAlias).ToString()); else if (dbResultSet.HasErrorState()) SetErrorState(dbResultSet.ErrorStateMessage); } } catch (Exception exc) { SaLogger.Error($"{SysUtil.Fn} {ExceptionUtil.GetLogMessage(exc, "comparing password")}", true); SetErrorState(exc.Message); } } else SaLogger.Warn($"{SysUtil.Fn} Unable to compare password by method 'SQL'. Database is '{DbmsType}'."); } else SaLogger.Warn($"{SysUtil.Fn} Unable to compare password. Unrecognised method: '{method}'."); return false; } //------------------------------------------------------------------- // O D B C D r i v e r s //------------------------------------------------------------------- [DllImport("odbccp32.dll", CharSet = CharSet.Unicode, SetLastError = true)] private static extern bool SQLGetInstalledDriversW(char[] lpszBuf, ushort cbufMax, out ushort pcbBufOut); /// /// Gets the ODBC driver names from the SQLGetInstalledDrivers function. /// Ref. https://stackoverflow.com/questions/6457973/odbc-driver-list-from-net /// /// List of ODBC driver names, if successfull. public static List GetOdbcDriverNames() { try { // ToDo Switch to SQLDrivers to get name and attributes. // Ref. https://social.msdn.microsoft.com/Forums/en-US/e98d3beb-35d5-4899-b277-0f7c568c6215/list-of-odbc-drivers-installed?forum=visualfoxprogeneral. // P/Invoke to SQLGetInstalledDriversW. // NB Only returns names and not attributes. char[] driverNamesBuffer = new char[ushort.MaxValue]; ushort size; bool succeeded = SQLGetInstalledDriversW(driverNamesBuffer, ushort.MaxValue, out size); if (succeeded == true) { char[] driverNames = new char[size - 1]; Array.Copy(driverNamesBuffer, driverNames, size - 1); return new List(new string(driverNames).Split('\0')).OrderBy(item => item).ToList(); } } catch (Exception) { } try { #pragma warning disable CA1416 // Alternatively, use the registry. // Validate platform compatibility using (RegistryKey localMachineHive = Registry.LocalMachine) using (RegistryKey odbcDriversKey = localMachineHive.OpenSubKey(@"SOFTWARE\ODBC\ODBCINST.INI\ODBC Drivers")) { if (odbcDriversKey != null) return new List(odbcDriversKey.GetValueNames()).OrderBy(item => item).ToList(); } // Validate platform compatibility // 64-BitConverter machines. using (RegistryKey localMachineHive = Registry.LocalMachine) using (RegistryKey odbcDriversKey = localMachineHive.OpenSubKey(@"SOFTWARE\Wow6432Node\ODBC\ODBCINST.INI\ODBC Drivers")) { if (odbcDriversKey != null) return new List(odbcDriversKey.GetValueNames()).OrderBy(item => item).ToList(); } #pragma warning restore CA1416 } catch (Exception) { } return new List(); } //------------------------------------------------------------------- // O D B C D a t a S o u r c e s //------------------------------------------------------------------- [DllImport("odbc32.dll")] public static extern int SQLDataSources(int EnvHandle, int Direction, StringBuilder ServerName, int ServerNameBufferLenIn, ref int ServerNameBufferLenOut, StringBuilder Driver, int DriverBufferLenIn, ref int DriverBufferLenOut); [DllImport("odbc32.dll")] public static extern int SQLAllocEnv(ref int EnvHandle); public static List GetOdbcDatasources() { List dataSources = new List(); int envHandle = 0; const int SQL_FETCH_NEXT = 1; const int SQL_FETCH_FIRST_SYSTEM = 32; if (SQLAllocEnv(ref envHandle) != -1) { int ret; StringBuilder serverName = new StringBuilder(1024); StringBuilder driverName = new StringBuilder(1024); int snLen = 0; int driverLen = 0; ret = SQLDataSources(envHandle, SQL_FETCH_FIRST_SYSTEM, serverName, serverName.Capacity, ref snLen, driverName, driverName.Capacity, ref driverLen); while (ret == 0) { SaDbDfnOdbcDataSource dataSource = new SaDbDfnOdbcDataSource(serverName.ToString(), driverName.ToString()); dataSources.Add(dataSource); ret = SQLDataSources(envHandle, SQL_FETCH_NEXT, serverName, serverName.Capacity, ref snLen, driverName, driverName.Capacity, ref driverLen); } } return dataSources.OrderBy(item => item.Name).ToList(); } //------------------------------------------------------------------- // Q u e r y D a t a b a s e //------------------------------------------------------------------- public String GetSelectQueryPrefix() { // Query prefix. switch (SelectLockMode) { case SelectLockMode.ReadCommitted: if (DbmsType == DbmsType.MsSql) { // Recommended for SQL2005+ to allow committed reads the same visibility as uncommitted reads: // -- Enable row versioning for the default Read Committed isolation level // ALTER DATABASE DevSchoolOneLMS SET READ_COMMITTED_SNAPSHOT ON; // -- Enable row versioning as a separate Snapshot isolation level // ALTER DATABASE DevSchoolOneLMS SET ALLOW_SNAPSHOT_ISOLATION ON; // Enabling row versioning allows the default READ COMMITTED isolation level to see a consistant // version of the row without resorting to SHARE locks that block UPDATE statements. return "Set Transaction Isolation Level Read Committed; "; } break; case SelectLockMode.ReadUncommitted: if (DbmsType == DbmsType.MsSql) return "Set Transaction Isolation Level Read Uncommitted; "; break; case SelectLockMode.RepeatableRead: if (DbmsType == DbmsType.MsSql) return "Set Transaction Isolation Level Repeatable Read; "; break; case SelectLockMode.Serializable: if (DbmsType == DbmsType.MsSql) return "Set Transaction Isolation Level Serializable; "; break; } return ""; } public abstract DbResultSet ExecuteQuery(String query); public abstract int ExecuteNonQuery(String query); public abstract int ExecuteInsert(String tableName, List columnNameValues); public abstract int ExecuteUpdate(String tableName, List columnNameValues, String filter); public abstract int ExecuteDelete(String tableName, String filter); /// /// Add value pair - used to build value lists for insert and update. /// Caters for delimiting text values and uses 'NULL' for empty text values. /// public static void AddValuePair(List columnNameValues, String columnName, String value, bool delimitString = false) { if (delimitString) { if (!StringUtil.IsEmpty(value)) value = "'" + value.Duplicate('\'') + "'"; else value = "NULL"; } columnNameValues.Add(columnName, value); } /// Add SysId value pair - used to build value lists for insert and update. public static void AddValuePair(List columnNameValues, String columnName, SysId value) { columnNameValues.Add(columnName, SysId.ToInt(value).ToString()); } /// Add numeric value pair - used to build value lists for insert and update. public static void AddValuePair(List columnNameValues, String columnName, long value) { columnNameValues.Add(columnName, value.ToString()); } /// Add decimal value pair - used to build value lists for insert and update. public static void AddValuePair(List columnNameValues, String columnName, decimal value) { columnNameValues.Add(columnName, value.ToString()); } /// Add date/time value pair - used to build value lists for insert and update. public static void AddValuePair(List columnNameValues, String columnName, DateTime value) { AddValuePair(columnNameValues, columnName, DateUtil.Format(value, "yyyy/MM/dd HH:mm:ss"), true); } // Specific queries. public abstract long GetRowCount(String tableName, String filter); public abstract DataValue GetRowValue(String tableName, String columnName, String filter); public abstract DataValue GetRowValue(String tableName, String columnName, String filter, DbGetValueType getValueType); //------------------------------------------------------------------- // D a t a b a s e S c h e m a //------------------------------------------------------------------- public abstract List GetTableNames(bool refresh = false); public abstract List GetTableDfns(bool refresh = false); public abstract DbDfnTable GetTableDfn(String name, bool refresh = false); public bool TableExists(String tableName) { if (StringUtil.IsEmpty(tableName)) return false; return GetTableNames().Exists(table => table.EqualsCI(tableName)); } public bool ColumnExists(String tableName, String columnName) { if (StringUtil.IsEmpty(tableName) || StringUtil.IsEmpty(columnName)) return false; DbDfnTable table = GetTableDfn(tableName); if (table != null) return table.HasColumnByName(columnName); return false; } public bool IndexExists(String tableName, String indexName) { if (StringUtil.IsEmpty(tableName) || StringUtil.IsEmpty(indexName)) return false; DbDfnTable table = GetTableDfn(tableName); if (table != null) return table.HasIndexByName(indexName); return false; } //------------------------------------------------------------------- // Database info - schema summary. public IDatabaseInfo CreateDbInfo() { return new DbDataItem.DatabaseInfo(this); } //------------------------------------------------------------------- // Logging. public static SaMessageLevel LoggingLevel { get; set; } = SaMessageLevel.None; public static String LoggingPrefix { get; set; } = " ~~~ "; protected static void Log(String fn, String message, SaMessageLevel level = SaMessageLevel.Debug) { if ((int)level > (int)LoggingLevel) return; SaLogger.Log((fn + "\n\t" + message).Trim(), level); } //------------------------------------------------------------------- } //======================================================================= }