362 lines
14 KiB
C#
362 lines
14 KiB
C#
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.
|
|
/// <summary>Database name populated when connection string is generated if not done previously.</summary>
|
|
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=<OdbcDriver>;
|
|
// Description=<DSN Caption>;
|
|
// UID=<User>;
|
|
// APP=<ApplicationCaption>;
|
|
// WsId=<WorkstationId>;
|
|
// Server=[protocol:]Server[,port][\instancename];
|
|
// Database=<Default database>;
|
|
// Trusted_Connection=<Yes|No>; [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<String> Stats
|
|
{
|
|
get
|
|
{
|
|
List<String> stats = new List<string>();
|
|
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<String> columnNames)
|
|
{
|
|
HashSet<String> missingColumnNames = new HashSet<string>();
|
|
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<String> columnNames = new HashSet<String>(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 <table> ADD PRIMARY KEY (<ColumnList>)
|
|
// 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<String> columnNames = new HashSet<String>(Regex.Split(columnsCsv, "[\\s,;\\-+]+").Where(s => !StringUtil.IsEmpty(s)));
|
|
if (!ColumnsExist(SysUtil.Fn, dfnTable, columnNames)) return false;
|
|
HashSet<String> includeColumnNames = new HashSet<String>(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;
|
|
}
|
|
|
|
//-------------------------------------------------------------------
|
|
}
|
|
//=======================================================================
|
|
}
|