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);
}
//-------------------------------------------------------------------
}
//=======================================================================
}