using System;
using System.Collections.Generic;
using System.Linq;
using Sarand.Net.Standard.Base.Core;
using Sarand.Net.Standard.Base.Bus;
using Sarand.Net.Standard.Base.Dbas;
using Sarand.Net.Standard.Dbas.Data;
using Sarand.Net.Standard.Base.App;
using Sarand.Net.Standard.Base.Utils;
using System.Text.RegularExpressions;
namespace Sarand.Net.Standard.Dbas.Base
{
//=======================================================================
public class SacsDatabaseConsole : SacsDisposableStateItem
{
//-------------------------------------------------------------------
// Database configuration
public class OdbcConfig
{
public DbmsType Dbms = DbmsType.General;
public String ConnectionString; // Raw connection string.
public String Location, User, Password; // Connection properties.
/// Database name populated when connection string is generated if not done previously.
public String DatabaseName { get; set; }
}
//-------------------------------------------------------------------
// Data members.
//-------------------------------------------------------------------
// Construction.
//-------------------------------------------------------------------
// Utilities.
protected void LogSql(String fn, String sql)
{
SaLogger.Debug($"{fn} SQL: {sql}");
}
//-------------------------------------------------------------------
// Database configuration.
//-------------------------------------------------------------------
public static DbmsType GetDbmsType(OdbcConfig config)
{
if (config.Dbms == DbmsType.MsSql || config.Location.Left(6).EqualsCI("MsSql:"))
return DbmsType.MsSql;
return config.Dbms;
}
public static String GenerateConnectionString(OdbcConfig config, bool dbmsSpecificConnectionString = false)
{
// Construct connection string.
String connectionString = String.Empty;
DbmsType dbms = GetDbmsType(config);
// SQL connection.
if (dbms == DbmsType.MsSql)
{
// MsSql:Server:Port\Instance.Database
dbms = DbmsType.MsSql;
String sqlDriver = OdbcUtil.GetOdbcSqlDriverName();
String sqlLocation = config.Location.Left(6).EqualsCI("MsSql:") ? config.Location.Substring(6) : config.Location;
String sqlServer = "."; /* Assume local host */
String sqlInstance = "";
String sqlPort = "";
String sqlDatabase = sqlLocation;
if (!String.IsNullOrEmpty(sqlLocation) && sqlLocation.IndexOf('\\') >= 0)
{
// Server=[protocol:]Server;
String[] locationSplit = sqlLocation.Split('\\');
sqlServer = locationSplit[0];
sqlDatabase = locationSplit[1];
}
if (!String.IsNullOrEmpty(sqlServer) && sqlServer.IndexOf(':') > 0)
{
// Server=[protocol:]Server[,port];
String[] serverSplit = sqlServer.Split(':');
sqlServer = serverSplit[0];
sqlPort = serverSplit[1];
}
if (!String.IsNullOrEmpty(sqlDatabase) && sqlDatabase.IndexOf('.') > 0)
{
// Server=[protocol:]Server[\instancename];
String[] databaseSplit = sqlDatabase.Split('.');
sqlInstance = databaseSplit[0];
sqlDatabase = databaseSplit[1];
}
if (!StringUtil.IsEmpty(sqlPort)) sqlServer += "," + sqlPort;
if (!StringUtil.IsEmpty(sqlInstance)) sqlServer += "\\" + sqlInstance;
if (!dbmsSpecificConnectionString)
{
// ODBC connection string for SQL Server.
// Ref. https://docs.microsoft.com/en-us/sql/relational-databases/native-client/applications/using-connection-string-keywords-with-sql-server-native-client
// Address Network address of the server running an instance of SQL Server.
// Driver=;
// Description=;
// UID=;
// APP=;
// WsId=;
// Server=[protocol:]Server[,port][\instancename];
// Database=;
// Trusted_Connection=; [or Integrated Security=SSPI;]
// UseProcForPrepare=2
connectionString = "Driver={" + sqlDriver + "};";
connectionString += "Server=" + sqlServer + ";";
connectionString += "Database=" + sqlDatabase + ";";
if (!config.User.IsEmpty())
{
connectionString += "UID=" + config.User + ";";
connectionString += "PWD=" + config.Password + ";";
}
connectionString += "Trusted_Connection=" + (config.User.IsEmpty() ? "Yes" : "No") + ";";
// Mar24 Added to avoid 'The certificate chain was issued by an authority that is not trusted'.
connectionString += "TrustServerCertificate=Yes;";
// Multiple Active Result Sets.
connectionString += "MARS_Connection=Yes;";
//if (iQueryLogTime > 0)
// connectionString += "QueryLogTime="+String(iQueryLogTime)+";";
//if (iCommandTimeout > 0)
// connectionString += "CommandTimeout=" + String(iCommandTimeout) + ";";
}
else
{
// SQL Connection string.
connectionString = "";
connectionString += "Server=" + sqlServer + ";";
connectionString += "Database=" + sqlDatabase + ";";
if (!config.User.IsEmpty())
{
connectionString += "User Id=" + config.User + ";";
connectionString += "Password=" + config.Password + ";";
}
connectionString += "Trusted_Connection=" + (config.User.IsEmpty() ? "Yes" : "No") + ";";
// Mar24 Added to avoid 'The certificate chain was issued by an authority that is not trusted'.
connectionString += "TrustServerCertificate=Yes;";
}
// Populate configuration values if blank.
if (config.Dbms <= 0)
config.Dbms = dbms;
if (StringUtil.IsEmpty(config.DatabaseName))
config.DatabaseName = sqlDatabase;
//SaLogger.Debug($"{SysUtil.Fn} Driver: " + sqlDriver + ". Server: " + sqlServer + ". Database: " + sqlDatabase + ". Connection: " + connectionString);
}
return connectionString;
}
//-------------------------------------------------------------------
// O p e n O D B C D a t a b a s e
//-------------------------------------------------------------------
public static SacsDatabase OpenOdbcDatabase(OdbcConfig config)
{
// Populate configuration.
config.Dbms = GetDbmsType(config);
// Connection strings.
String connectionString = config.ConnectionString;
if (StringUtil.IsEmpty(connectionString))
connectionString = GenerateConnectionString(config);
String sqlConnectionString = GenerateConnectionString(config, true);
// Open database.
DbGlobalDbms.Type = config.Dbms;
SacsDatabaseOdbc database = new SacsDatabaseOdbc(config.Dbms, config.DatabaseName, connectionString);
database.SelectLockMode = SelectLockMode.ReadUncommitted;
database.SqlConnectionString = sqlConnectionString;
return database;
}
//-------------------------------------------------------------------
// D a t a b a s e I n d i c e s
//-------------------------------------------------------------------
// Database Index stats.
public class IndexStats
{
public int PrimaryIndicesCreated { get; set; }
public int IndicesCreated { get; set; }
public int IndicesDropped { get; set; }
public List Stats
{
get
{
List stats = new List();
if (PrimaryIndicesCreated > 0)
stats.Add("Primary indices created: " + PrimaryIndicesCreated.ToString());
if (IndicesCreated > 0)
stats.Add("Indices created: " + IndicesCreated.ToString());
if (IndicesDropped > 0)
stats.Add("Indices dropped: " + IndicesDropped.ToString());
return stats;
}
}
}
//-------------------------------------------------------------------
protected bool ColumnsExist(String fn, DbDfnTable dfnTable, HashSet columnNames)
{
HashSet missingColumnNames = new HashSet();
foreach (String columnName in columnNames)
{
if (!dfnTable.HasColumnByName(columnName))
missingColumnNames.Add(columnName);
}
if (missingColumnNames.Any())
{
SaLogger.Warn($"{fn} {dfnTable.Name} Table is missing columns for index creation: {missingColumnNames.Concat(", ")}.");
return false;
}
return true;
}
//-------------------------------------------------------------------
// Create primary index.
protected bool CreatePrimaryDbIndex(ISacsDatabase database, IndexStats indexStats, String tableName, String columnsCsv, bool dropIndicesIfNotExist = true)
{
if (database == null || HasErrorState()) return false;
DbDfnTable dfnTable = database.GetTableDfn(tableName, true);
if (dfnTable == null) return true;
HashSet columnNames = new HashSet(Regex.Split(columnsCsv, "[\\s,;\\-+]+").Where(s => !StringUtil.IsEmpty(s)));
if (!ColumnsExist(SysUtil.Fn, dfnTable, columnNames)) return false;
String indexName = "PK_" + tableName;
if (!dfnTable.HasIndexByName(indexName))
{
if (dropIndicesIfNotExist)
{
// All existing indices are dropped
foreach (DbDfnIndex indexDfn in dfnTable.Indices)
DropDbIndex(database, indexStats, tableName, indexDfn.Name);
}
if (!HasErrorState())
{
// Examples:
// MS Access: ALTER TABLE ADD PRIMARY KEY ()
// 1. ALTER TABLE Customer ADD PRIMARY KEY (SID);
// 2. ALTER TABLE [Sales].[Individual]
// ADD CONSTRAINT [PK_Individual_CustomerID]
// PRIMARY KEY CLUSTERED
// ([CustomerID] ASC)
String query = $"ALTER TABLE [{tableName}] ADD CONSTRAINT [{indexName}] PRIMARY KEY ({columnNames.Concat(",", "[", "]")})";
if (database.DbmsType == DbmsType.MsSql)
query = $"ALTER TABLE [{tableName}] ADD CONSTRAINT [{indexName}] PRIMARY KEY CLUSTERED ({columnNames.Concat(",", "[", "]")})";
LogSql(SysUtil.Fn, query);
if (!StringUtil.IsEmpty(query))
{
database.ExecuteNonQuery(query);
if (!database.HasErrorState())
indexStats.PrimaryIndicesCreated++;
else
{
SetErrorState($"Failed to create primary index {indexName} on {tableName} - {database.ErrorStateMessage.TrimEnd('.')}.");
SaLogger.Error(ErrorStateMessage);
}
}
}
}
return OK;
}
//-------------------------------------------------------------------
// Create database index.
protected bool CreateDbIndex(ISacsDatabase database, IndexStats indexStats, String tableName, String indexName, String columnsCsv, String includeColumnsCsv = "", bool unique = false)
{
if (database == null || HasErrorState()) return false;
DbDfnTable dfnTable = database.GetTableDfn(tableName, true);
if (dfnTable == null) return true;
HashSet columnNames = new HashSet(Regex.Split(columnsCsv, "[\\s,;\\-+]+").Where(s => !StringUtil.IsEmpty(s)));
if (!ColumnsExist(SysUtil.Fn, dfnTable, columnNames)) return false;
HashSet includeColumnNames = new HashSet(Regex.Split(includeColumnsCsv, "[\\s,;\\-+]+").Where(s => !StringUtil.IsEmpty(s)));
if (!ColumnsExist(SysUtil.Fn, dfnTable, includeColumnNames)) return false;
// Construct the index name.
String indexNamePrefix = "IDX_" + tableName + "_";
if (!indexName.StartsWith(indexNamePrefix)) indexName = indexNamePrefix + indexName;
// Should also check the columns and their sequence in the index.
if (!dfnTable.HasIndexByName(indexName))
{
String query = $"CREATE " + (unique ? "UNIQUE " : "") + $"INDEX [{indexName}] ON [{tableName}] ({columnNames.Concat(",", "[", "]")})";
if (!includeColumnNames.IsEmpty())
query += $" INCLUDE ({includeColumnNames.Concat(",", "[", "]")})";
LogSql(SysUtil.Fn, query);
if (!StringUtil.IsEmpty(query))
{
database.ExecuteNonQuery(query);
if (!database.HasErrorState())
indexStats.IndicesCreated++;
else
{
SetErrorState($"Failed to create index {indexName} on {tableName} - {database.ErrorStateMessage.TrimEnd('.')}.");
SaLogger.Error(ErrorStateMessage);
}
}
}
return OK;
}
//-------------------------------------------------------------------
// Drop index.
protected bool DropDbIndex(ISacsDatabase database, IndexStats indexStats, String tableName, String indexName)
{
if (database == null || HasErrorState()) return false;
DbDfnTable dfnTable = database.GetTableDfn(tableName, true);
if (dfnTable == null) return true;
DbDfnIndex dfnIndex = dfnTable.GetIndexByName(indexName);
if (dfnIndex == null)
{
SaLogger.Info($"{SysUtil.Fn} Index '{indexName}' does not exist on table {tableName}.");
return true;
}
if (!dfnIndex.Primary)
{
// Drop index:
// MS Access: DROP INDEX index_name ON table_name
// MS SQL Server: DROP INDEX table_name.index_name
// DB2/Oracle: DROP INDEX index_name
// MySQL: ALTER TABLE table_name DROP INDEX index_name
String query = $"DROP INDEX [{indexName}] ON [{tableName}]";
LogSql(SysUtil.Fn, query);
if (database.DbmsType == DbmsType.MsSql)
query = $"DROP INDEX [{tableName}].[{indexName}]";
if (!StringUtil.IsEmpty(query))
{
database.ExecuteNonQuery(query);
if (!database.HasErrorState())
indexStats.IndicesDropped++;
else
{
SetErrorState($"Failed to drop index {indexName} on {tableName} - {database.ErrorStateMessage.TrimEnd('.')}.");
SaLogger.Error(ErrorStateMessage);
}
}
}
else
{
// Drop Primary Key.
String query = $"ALTER TABLE [{tableName}] DROP CONSTRAINT [{indexName}]";
LogSql(SysUtil.Fn, query);
//if (database.DbmsType() == eSaDbmsMsSql)
// sQry = "DROP INDEX ["+tableName+"].["+indexName+"]";
if (!StringUtil.IsEmpty(query))
{
database.ExecuteNonQuery(query);
if (!database.HasErrorState())
indexStats.IndicesDropped++;
else
{
SetErrorState($"Failed to drop primary key {indexName} on {tableName} - {database.ErrorStateMessage.TrimEnd('.')}.");
SaLogger.Error(ErrorStateMessage);
}
}
}
return OK;
}
//-------------------------------------------------------------------
}
//=======================================================================
}