using System; using System.Collections.Generic; using System.Data; using System.Data.Common; using System.Data.Odbc; using Microsoft.Data.SqlClient; using System.Linq; using Sarand.Net.Standard.Base.App; using Sarand.Net.Standard.Base.Core; using Sarand.Net.Standard.Base.Bus; using Sarand.Net.Standard.Base.Dbas; using Sarand.Net.Standard.Dbas.Base; using Sarand.Net.Standard.Base.Utils; using System.Diagnostics; using Sarand.Net.Standard.Base.Security; namespace Sarand.Net.Standard.Dbas.Data { //======================================================================= internal class SacsDatabaseOdbc : SacsDatabase { //------------------------------------------------------------------- // Data members. private String sqlConnectionString = ""; private readonly Dictionary tableDfns = new Dictionary(); private bool tableDfnsPopulated = false; public static readonly String DbCol_CreateOn = "CreateOn"; public static readonly String DbCol_CreateBy = "CreateBy"; public static readonly String DbCol_UpdateOn = "UpdateOn"; public static readonly String DbCol_UpdateBy = "UpdateBy"; //------------------------------------------------------------------- // Construction. public SacsDatabaseOdbc(DbmsType dbms, String databaseName, String connectionString) : base(dbms, databaseName, connectionString) { if (dbms == DbmsType.MsSql) sqlConnectionString = connectionString; } //------------------------------------------------------------------- // Properties. public String SqlConnectionString { get { return sqlConnectionString; } set { if (sqlConnectionString.Equals(value)) return; sqlConnectionString = value; OnConnectionChanged(); } } //------------------------------------------------------------------- // Connection. public override void OnConnectionChanged() { } public override DbConnection CreateConnection(bool forceOdbc = false) { if (!forceOdbc && DbmsType == DbmsType.MsSql && !StringUtil.IsEmpty(sqlConnectionString)) return new SqlConnection(sqlConnectionString); return new OdbcConnection(ConnectionString); } public override DbDataAdapter CreateDataAdapter() { return new OdbcDataAdapter(); } //------------------------------------------------------------------- // I d e n t i t y G e n e r a t i o n //------------------------------------------------------------------- private DbDfnTable nextIdTableDfn = null; public override SysId GetNextId(String objectName, DbUpdateInfo dbUpdateInfo, bool databaseTable = true, String idColumn = "Id") { PopulateNextIdTableDfn(); // Next Id database definitions. String colObject = NextIdTable + "." + NextIdColumn_Object; String colNextId = NextIdTable + "." + NextIdColumn_NextId; String colAliasNextId = "NxtId"; String filter = "(" + colObject + ")='" + objectName + "'"; bool hasCreateCols = (nextIdTableDfn.HasColumnByName(DbCol_CreateOn) && nextIdTableDfn.HasColumnByName(DbCol_CreateBy)); bool hasUpdateCols = (nextIdTableDfn.HasColumnByName(DbCol_UpdateOn) && nextIdTableDfn.HasColumnByName(DbCol_UpdateBy)); // Query the data. int nextIdValue = 1; DbQuery qry = new DbQuery(DbmsType); qry.Tables.Add(NextIdTable); qry.Columns.Add(colNextId, colAliasNextId); qry.Filters.Add(filter); //SaLogger.Debug($"{SysUtil.Fn} Query: {qry.BuildQuery()}"); DbResultSet_DataReader resultsSet = DbResultSet_DataReader.ExecuteQuery(this, qry.BuildQuery()); if (resultsSet != null && resultsSet.NextRecord()) { NamedValue.Map values = resultsSet.GetRecordValues(); nextIdValue = Math.Max(1, (int)values.GetValue(colAliasNextId).AsLong()); // Update the record with the next available value. List updateColumnValues = new List(); updateColumnValues.Add(NextIdColumn_NextId, (nextIdValue + 1).ToString()); if (hasUpdateCols) { updateColumnValues.Add(DbCol_UpdateOn, DbQuery.FormatDateTimeForCondition(DateTime.Now)); updateColumnValues.Add(DbCol_UpdateBy, SysId.IsValid(dbUpdateInfo.UserId) ? SysId.ToString(dbUpdateInfo.UserId) : "0"); } String updateQry = "Update " + NextIdTable + " Set " + updateColumnValues.Concat("=").Concat(", ") + " Where " + filter; //SaLogger.Debug($"{SysUtil.Fn} Update query: {updateQry}"); using (var dbConnection = CreateConnection()) using (var dbCommand = dbConnection.CreateCommand()) { dbConnection.Open(); dbCommand.CommandType = System.Data.CommandType.Text; dbCommand.CommandText = updateQry; dbCommand.ExecuteNonQuery(); } } else { // Object not found on NextId table. // If object is a database table, query maximum value. if (databaseTable) nextIdValue = GetNextIdFromTable(objectName, idColumn); // Construct query to store Next Id value. List insertColumnValues = new List(); insertColumnValues.Add(NextIdColumn_Object, "'" + DbQuery.FormatForCondition(objectName) + "'"); insertColumnValues.Add(NextIdColumn_NextId, (nextIdValue + 1).ToString()); if (hasCreateCols) { insertColumnValues.Add(DbCol_CreateOn, DbQuery.FormatDateTimeForCondition(DateTime.Now)); insertColumnValues.Add(DbCol_CreateBy, SysId.IsValid(dbUpdateInfo.UserId) ? SysId.ToString(dbUpdateInfo.UserId) : "0"); } if (hasUpdateCols) { insertColumnValues.Add(DbCol_UpdateOn, DbQuery.FormatDateTimeForCondition(DateTime.Now)); insertColumnValues.Add(DbCol_UpdateBy, SysId.IsValid(dbUpdateInfo.UserId) ? SysId.ToString(dbUpdateInfo.UserId) : "0"); } String insertQry = "Insert Into " + NextIdTable + " (" + insertColumnValues.GetValues(StringPair.Value.Value1).Concat(",") + ") Values (" + insertColumnValues.GetValues(StringPair.Value.Value2).Concat(",") + ")"; SaLogger.Debug($"{SysUtil.Fn} Insert query: {insertQry}"); using (var dbConnection = CreateConnection()) using (var dbCommand = dbConnection.CreateCommand()) { dbConnection.Open(); dbCommand.CommandType = System.Data.CommandType.Text; dbCommand.CommandText = insertQry; dbCommand.ExecuteNonQuery(); } } if (resultsSet != null) resultsSet.Dispose(); return new SysId(nextIdValue); } public override int GetNextIdFromTable(String objectName, String idColumn = "Id") { DataValue maxId = GetRowValue(SaSecurityDbDfn.DelimitReservedWord(objectName), "Max(" + idColumn + ")", ""); if (maxId != null && maxId.GetDataType() != DataValue.DataType.Unknown) return Math.Max(0, (int)maxId.AsLong()) + 1; return 1; } public override bool SetMinNextId(String objectName, SysId id, DbUpdateInfo dbUpdateInfo, bool databaseTable = true, String idColumn = "Id", bool force = false) { PopulateNextIdTableDfn(); throw new NotSupportedException($"{SysUtil.Fn} Not yet implemented."); //return true; } private void PopulateNextIdTableDfn() { if (nextIdTableDfn != null && nextIdTableDfn.Name.Equals(NextIdTable)) return; nextIdTableDfn = GetTableDfn(NextIdTable); } //------------------------------------------------------------------- // Q u e r y D a t a b a s e //------------------------------------------------------------------- // Query records. public override DbResultSet ExecuteQuery(String query) { ClearErrorState(); Log(SysUtil.Fn, "Query: " + query, SaMessageLevel.Debug); try { return DbResultSet_DataReader.ExecuteQuery(this, query); } catch (Exception exc) { SaLogger.Error($"{SysUtil.Fn} {ExceptionUtil.GetLogMessage(exc)}\nQuery:\n\t{String.Join("\n\t", query.ReplaceAll("\n\r", "\n").Split("\n"))}", true); SetErrorState(exc.Message); } return null; } //------------------------------------------------------------------- /// Execute a non-query statement and return the row count. public override int ExecuteNonQuery(string query) { ClearErrorState(); Log(SysUtil.Fn, "Query: " + query, SaMessageLevel.Debug); using (var dbConnection = CreateConnection()) using (var dbCommand = dbConnection.CreateCommand()) { try { dbConnection.Open(); dbCommand.CommandType = System.Data.CommandType.Text; dbCommand.CommandText = query; return dbCommand.ExecuteNonQuery(); } catch (Exception exc) { SaLogger.Error($"{SysUtil.Fn} {ExceptionUtil.GetLogMessage(exc)}\nQuery:\n\t{String.Join("\n\t", query.ReplaceAll("\n\r", "\n").Split("\n"))}", true); SetErrorState(exc.Message); } } return 0; } //------------------------------------------------------------------- /// Compile and execute an insert statement and return the row count. public override int ExecuteInsert(String tableName, List columnNameValues) { ClearErrorState(); String query = "INSERT INTO " + tableName + " " + "(" + columnNameValues.GetValues(StringPair.Value.Value1).Concat(", ") + ") " + "VALUES (" + columnNameValues.GetValues(StringPair.Value.Value2).Concat(", ") + ")"; Log(SysUtil.Fn, "Query: " + query, SaMessageLevel.Debug); using (var dbConnection = CreateConnection()) using (var dbCommand = dbConnection.CreateCommand()) { try { dbConnection.Open(); dbCommand.CommandType = System.Data.CommandType.Text; dbCommand.CommandText = query; return dbCommand.ExecuteNonQuery(); } catch (Exception exc) { SaLogger.Error($"{SysUtil.Fn} {ExceptionUtil.GetLogMessage(exc)}\nQuery:\n\t{String.Join("\n\t", query.ReplaceAll("\n\r", "\n").Split("\n"))}", true); SetErrorState(exc.Message); } } return 0; } //------------------------------------------------------------------- /// Compile and execute an update statement and return the row count. public override int ExecuteUpdate(String tableName, List columnNameValues, String filter) { ClearErrorState(); String query = "UPDATE " + tableName + " " + "SET " + columnNameValues.Concat("=").Concat(", "); if (!StringUtil.IsEmpty(filter)) query += (String)" WHERE " + filter; Log(SysUtil.Fn, "Query: " + query, SaMessageLevel.Debug); using (var dbConnection = CreateConnection()) using (var dbCommand = dbConnection.CreateCommand()) { try { dbConnection.Open(); dbCommand.CommandType = System.Data.CommandType.Text; dbCommand.CommandText = query; return dbCommand.ExecuteNonQuery(); } catch (Exception exc) { SaLogger.Error($"{SysUtil.Fn} {ExceptionUtil.GetLogMessage(exc)}\nQuery:\n\t{String.Join("\n\t", query.ReplaceAll("\n\r", "\n").Split("\n"))}", true); SetErrorState(exc.Message); } } return 0; } //------------------------------------------------------------------- /// Compile and execute a delete statement and return the row count. public override int ExecuteDelete(String tableName, String filter) { ClearErrorState(); String query = "DELETE FROM " + tableName; if (!StringUtil.IsEmpty(filter)) query += " WHERE " + filter; Log(SysUtil.Fn, "Query: " + query, SaMessageLevel.Debug); using (var dbConnection = CreateConnection()) using (var dbCommand = dbConnection.CreateCommand()) { try { dbConnection.Open(); dbCommand.CommandType = System.Data.CommandType.Text; dbCommand.CommandText = query; return dbCommand.ExecuteNonQuery(); } catch (Exception exc) { SaLogger.Error($"{SysUtil.Fn} {ExceptionUtil.GetLogMessage(exc)}\nQuery:\n\t{String.Join("\n\t", query.ReplaceAll("\n\r", "\n").Split("\n"))}", true); SetErrorState(exc.Message); } } return 0; } //------------------------------------------------------------------- /// Retrieve specific row value. public override DataValue GetRowValue(String tableName, String columnName, String filter) { ClearErrorState(); try { String colAlias = "DataValue"; String query = "SELECT " + columnName + " '" + colAlias + "' FROM " + tableName + (!StringUtil.IsEmpty(filter) ? " WHERE " + filter : ""); using (DbResultSet dbResultSet = DbResultSet_DataReader.ExecuteQuery(this, query)) { if (dbResultSet.NextRecord()) return dbResultSet.GetRecordValues().GetValue(colAlias); else if (dbResultSet.HasErrorState()) SetErrorState(dbResultSet.ErrorStateMessage); } } catch (Exception exc) { SaLogger.Error($"{SysUtil.Fn} {ExceptionUtil.GetLogMessage(exc)}\n\tTable: {tableName}. Column: {columnName}. Filter: " + (!StringUtil.IsEmpty(filter) ? filter : ""), true); SetErrorState(exc.Message); } return null; } //------------------------------------------------------------------- /// Get row count from Table applying optional filter. public override long GetRowCount(String tableName, String filter) { ClearErrorState(); DataValue dataValue = GetRowValue(tableName, "Count(*)", filter); if (dataValue != null) return dataValue.AsLong(); return 0; } //------------------------------------------------------------------- /// Get calculated value from table - Sum, Min or Max. public override DataValue GetRowValue(String tableName, String columnName, String filter, DbGetValueType getValueType) { ClearErrorState(); String fn = ""; if (getValueType == DbGetValueType.eDbValueSum) fn = "SUM"; else if (getValueType == DbGetValueType.eDbValueMin) fn = "MIN"; else if (getValueType == DbGetValueType.eDbValueMax) fn = "MAX"; return GetRowValue(tableName, fn + "(" + columnName + ")", filter); } //------------------------------------------------------------------- // D a t a b a s e S c h e m a //------------------------------------------------------------------- // NB Always use ODBC connection for querying schema. // eg. SQL Connection GetSchema('Indices') doesn't return index columns. public override HashSet GetTableNames(bool refresh = false) { lock (tableDfns) { // Only populate table names if not previously done. if (!tableDfnsPopulated || refresh) { tableDfns.Clear(); using (DbConnection dbConnection = CreateConnection(true)) { dbConnection.Open(); tableDfns.AddRange(GetTableDefinitions(dbConnection).ToDictionary(table => table.Name, table => table)); } } return tableDfns.Values.GetNames().ToHashSet(StringComparer.InvariantCultureIgnoreCase); } } public override List GetTableDfns(bool refresh = false) { lock (tableDfns) { if (refresh) ClearTableDfns(); // Only populate table names and/or table schemas where not previously done. if (!tableDfnsPopulated || tableDfns.Count < 1 || tableDfns.Values.Any(table => !table.HasColumns())) { // Use a single connection for table and schema population. using (DbConnection dbConnection = CreateConnection(true)) { dbConnection.Open(); if (!tableDfnsPopulated || tableDfns.Count < 1) { tableDfns.Clear(); tableDfns.AddRange(GetTableDefinitions(dbConnection).ToDictionary(table => table.Name, table => table)); } tableDfnsPopulated = true; // Populate details for each table. foreach (DbDfnTable table in tableDfns.Values) { if (!table.HasColumns()) { PopulateTableColumns(dbConnection, table); PopulateTableIndices(dbConnection, table); } } } } return new List(tableDfns.Values); } } public void ClearTableDfns() { lock (tableDfns) { tableDfns.Clear(); tableDfnsPopulated = false; } } public override DbDfnTable GetTableDfn(String tableName, bool refresh = false) { lock (tableDfns) { if (!tableDfns.Keys.Contains(tableName) || refresh) { // Remove current table definition. tableDfns.Remove(tableName); } // Only populate the specified table if required. DbDfnTable table = tableDfns.GetValueOrDefault(tableName); if (table == null || !table.HasColumns()) { using (DbConnection dbConnection = CreateConnection(true)) { dbConnection.Open(); if (!tableDfns.ContainsKey(tableName)) tableDfns.AddRange(GetTableDefinitions(dbConnection, tableName).ToDictionary(table => table.Name, table => table)); table = tableDfns.GetValueOrDefault(tableName); if (table != null && (!table.HasColumns() || refresh)) { PopulateTableColumns(dbConnection, table); PopulateTableIndices(dbConnection, table); } } } return tableDfns.GetValueOrDefault(tableName); } } //------------------------------------------------------------------- private static List GetTableDefinitions(DbConnection dbConnection, String tableNameRestriction = null) { List tables = new List(); if (SacsSystem.DebugMode) SaLogger.Debug($"{SysUtil.Fn} Database: {dbConnection.Database}." + (!StringUtil.IsEmpty(tableNameRestriction) ? $" Table: {tableNameRestriction}." : "")); String[] restrictions = new String[] { null, null, tableNameRestriction }; // database, owner, table name, type using (DataTable tableSchema = dbConnection.GetSchema(OdbcMetaDataCollectionNames.Tables, restrictions)) { //DataSet set = tableSchema.DataSet; foreach (DataRow row in tableSchema.Rows) { //----------------------------------------------------------- // Columns are: Name Size Data Type Values // 1. TABLE_CAT 255 12 VarChar // 2. TABLE_SCHEM 128 12 VarChar // 3. TABLE_NAME 128 12 VarChar // 4. TABLE_TYPE 128 12 VarChar 'SYSTEM TABLE' 'TABLE' 'VIEW' // 'GLOBAL TEMPORARY' 'LOCAL TEMPORARY' // 'ALIAS' 'SYNONYM' // 5. REMARKS 254 12 VarChar // TABLE_CATALOG is the SQL alternative to TABLE_CAT // TABLE_QUALIFIER is the ODBC 2.0 alternative to TABLE_CAT (ODBC 3.0). // TABLE_SCHEMA is the SQL alternative to TABLE_SCHEM // TABLE_OWNER is the ODBC 2.0 alternative to TABLE_SCHEM (ODBC 3.0). // Variations: // Sage Line 50: TABLE_QUALIFIER, TABLE_OWNER, TABLE_NAME, TABLE_TYPE, REMARKS. // Sybase Advantage: QUALIFIERNAME, OWNERNAME, TABLENAME, TYPENAME, REMARKS //----------------------------------------------------------- String database = String.Empty, schema = String.Empty, tableName = String.Empty, type = String.Empty, remarks = String.Empty; foreach (DataColumn column in tableSchema.Columns) { String colName = column.ColumnName; String value = row[colName].ToString(); if (colName.EqualsCI("TABLE_CAT") || colName.EqualsCI("TABLE_CATALOG") || colName.EqualsCI("TABLE_QUALIFIER") || colName.EqualsCI("QualifierName")) database = value; else if (colName.EqualsCI("TABLE_SCHEM") || colName.EqualsCI("TABLE_SCHEMA") || colName.EqualsCI("TABLE_OWNER") || colName.EqualsCI("OwnerName")) schema = value; else if (colName.EqualsCI("TABLE_NAME") || colName.EqualsCI("TableName")) tableName = value; else if (colName.EqualsCI("TABLE_TYPE") || colName.EqualsCI("TypeName")) type = value; else if (colName.EqualsCI("REMARKS")) remarks = value; else SaLogger.Warn($"{SysUtil.Fn} Unrecognised column name: '{colName}'"); } // TODO Why is Type sometimes "BASE TABLE"? if (!StringUtil.IsEmpty(tableName) && (type.EqualsCI("Table") || type.EqualsCI("Base Table")) && (!schema.EqualsCI("sys") || !tableName.StartsWithCI("trace_xe_"))) { #if DEBUG //SaLogger.Trace($"{SysUtil.Fn} Database={database}, Schema={schema}, Table={tableName}, Type={type}"); #endif tables.Add(new DbDfnTable(schema, tableName, remarks)); } } } if (SacsSystem.DebugMode && tables.Exists(item => item == null)) Debugger.Break(); return tables.OrderBy(item => item.Name).ToList(); } //------------------------------------------------------------------- private void PopulateTableColumns(DbConnection dbConnection, DbDfnTable table) { //if (SacsSystem.DebugMode) SaLogger.Debug($"{SysUtil.Fn} Table: {table.Name}"); short columnPos = 0; String[] restrictions = new String[] { null, table.Schema, table.Name, null }; // database, owner, table name, column name using (DataTable columnSchema = dbConnection.GetSchema(OdbcMetaDataCollectionNames.Columns, restrictions)) { foreach (DataRow row in columnSchema.Rows) { //----------------------------------------------------------- // Columns are: Name Size Data Type Values // 1. TABLE_CAT 255 12 VarChar // 2. TABLE_SCHEM 128 12 VarChar // 3. TABLE_NAME 128 12 VarChar // 4. COLUMN_NAME 128 12 VarChar // 5. DATA_TYPE 5 5 SmallInt // 6. TYPE_NAME 64 12 VarChar // 7. COLUMN_SIZE 10 4 Integer (Counter) // 8. BUFFER_LENGTH 10 4 Integer (Counter) // 9. DECIMAL_DIGITS 5 5 SmallInt // 10. NUM_PREC_RADIX 5 5 SmallInt 10 or 2. // 11. NULLABLE 5 5 SmallInt SQL_NO_NULLS,SQL_NULLABLE,SQL_NULLABLE_UNKNOWN // 12. REMARKS 254 12 VarChar // 13. COLUMN_DEF 128 12 VarChar Default value. // 14. SQL_DATA_TYPE 5 5 SmallInt // 15. SQL_DATETIME_SUB 5 5 SmallInt // 16. CHAR_OCTET_LENGTH 10 4 Integer (Counter) // 17. ORDINAL_POSITION 10 4 Integer (Counter) // 18. IS_NULLABLE 128 12 VarChar 'YES' or 'NO' // 19. ORDINAL 10 4 Integer (Counter) // Alternative column names: // ODBC 2.0 column ODBC 3.x column // TABLE_QUALIFIER TABLE_CAT // TABLE_OWNER TABLE_SCHEM // PRECISION COLUMN_SIZE // LENGTH BUFFER_LENGTH // SCALE DECIMAL_DIGITS // RADIX NUM_PREC_RADIX // SQL Server columns: // TABLE_CATALOG, TABLE_SCHEMA, ORDINAL_POSITION, COLUMN_DEFAULT // CHARACTER_MAXIMUM_LENGTH, CHARACTER_OCTET_LENGTH // NUMERIC_PRECISION, NUMERIC_PRECISION_RADIX, NUMERIC_SCALE, DATETIME_PRECISION // CHARACTER_SET_CATALOG, CHARACTER_SET_SCHEMA, CHARACTER_SET_NAME, COLLATION_CATALOG // IS_SPARSE, IS_COLUMN_SET, IS_FILESTREAM // SQL Server columns returned by ODBC conection: // NUM_PREC_RADIX, COLUMN_DEF, SQL_DATA_TYPE, SQL_DATETIME_SUB, CHAR_OCTET_LENGTH // SS_IS_SPARSE, SS_IS_COLUMN_SET, SS_IS_COMPUTED, SS_UDT_CATALOG_NAME, SS_UDT_SCHEMA_NAME // SS_UDT_ASSEMBLY_TYPE_NAME, SS_XML_SCHEMACOLLECTION_CATALOG_NAME, SS_XML_SCHEMACOLLECTION_SCHEMA_NAME, SS_XML_SCHEMACOLLECTION_NAME, SS_DATA_TYPE // Sage ODBC data sources quote the following columns: // TABLE_QUALIFIER,TABLE_OWNER,TABLE_NAME,COLUMN_NAME,DATA_TYPE, // TYPE_NAME,PRECISION,LENGTH,SCALE,RADIX,NULLABLE,REMARKS // Sybase Advantage data sources quote the following columns: // QUALIFIERNAME,OWNERNAME,TABLENAME,colName,TYPE // TYPENAME,PRECISION,LENGTH,SCALE,RADIX,NULLABLE // REMARKS,DEFAULT,DATATYPE,DATETIME_SUBTYPE // OCTET_LENGTH,ORDINAL_POSITION,IS_NULLABLE // Notes. // - SQL Connection doesn't show if a column is Identity. // - Unclear how to deterine column size for datetime values between SQL and ODBC. //----------------------------------------------------------- String database = String.Empty, schema = String.Empty, tableName = String.Empty, columnName = String.Empty, nativeDataTypeName = String.Empty, remarks = String.Empty; short nativeDataType = 0, decimalDigits = 0; long columnSize = 0, bufferLen = 0; DbColumnNullable nullable = DbColumnNullable.Unknown; bool isIdentity = false, isNullable = false; foreach (DataColumn column in columnSchema.Columns) { String colName = column.ColumnName; String value = row[colName].ToString(); //if (columnName.EqualsCI("CreateOn")) SaLogger.Debug(GetType().Name+".PopulateTableColumns Schema column name: " + colName + " [" + value + "]"); if (colName.EqualsCI("TABLE_CAT") || colName.EqualsCI("TABLE_CATALOG") || colName.EqualsCI("TABLE_QUALIFIER") || colName.EqualsCI("QualifierName")) database = value; else if (colName.EqualsCI("TABLE_SCHEM") || colName.EqualsCI("TABLE_SCHEMA") || colName.EqualsCI("TABLE_OWNER") || colName.EqualsCI("OwnerName")) schema = value; else if (colName.EqualsCI("TABLE_NAME") || colName.EqualsCI("TableName")) tableName = value; else if (colName.EqualsCI("Column_Name") || colName.EqualsCI("ColumnName")) columnName = value; else if (colName.EqualsCI("Data_Type") || colName.EqualsCI("Type")) { if (value.IsNumeric(true)) nativeDataType = value.ToShort(); else { // SQL returns data type name (non numeric). nativeDataTypeName = value; nativeDataType = OdbcUtil.GetSqlNativeDataType(value); } } else if (colName.EqualsCI("Type_Name") || colName.EqualsCI("TypeName")) { nativeDataTypeName = value; if (value.EndsWith(" identity")) { nativeDataTypeName = value.Left(value.Length - " identity".Length); isIdentity = true; } } else if (colName.EqualsCI("Column_Size") || colName.EqualsCI("CHARACTER_MAXIMUM_LENGTH") || colName.EqualsCI("NUMERIC_PRECISION") || colName.EqualsCI("DATETIME_PRECISION") || colName.EqualsCI("Precision")) { if (!StringUtil.IsEmpty(value)) columnSize = value.ToLong(); if (colName.EqualsCI("CHARACTER_MAXIMUM_LENGTH") && value.Equals("-1")) columnSize = 0; } else if (colName.EqualsCI("Buffer_Length") || colName.EqualsCI("Length")) { if (!StringUtil.IsEmpty(value)) bufferLen = value.ToLong(); } else if (colName.EqualsCI("Decimal_Digits") || colName.EqualsCI("Scale") || colName.EqualsCI("NUMERIC_SCALE")) { if (!StringUtil.IsEmpty(value)) decimalDigits = value.ToShort(); } else if (colName.EqualsCI("Nullable")) { if (!StringUtil.IsEmpty(value)) nullable = (DbColumnNullable)value.ToShort(); } else if (colName.EqualsCI("Is_Nullable")) isNullable = !value.EqualsCI("No"); else if (colName.EqualsCI("REMARKS")) remarks = value; // SqlServer specific. else if (colName.EqualsCI("ORDINAL_POSITION")) { } else if (colName.EqualsCI("COLUMN_DEFAULT")) { } else if (colName.EqualsCI("CHARACTER_OCTET_LENGTH")) { } else if (colName.EqualsCI("NUMERIC_PRECISION_RADIX")) { } else if (colName.EqualsCI("CHARACTER_SET_CATALOG")) { } else if (colName.EqualsCI("CHARACTER_SET_SCHEMA")) { } else if (colName.EqualsCI("CHARACTER_SET_NAME")) { } else if (colName.EqualsCI("COLLATION_CATALOG")) { } else if (colName.EqualsCI("IS_SPARSE")) { } else if (colName.EqualsCI("IS_COLUMN_SET")) { } else if (colName.EqualsCI("IS_FILESTREAM")) { } // SQL Server ODBC. else if (colName.EqualsCI("SS_IS_IDENTITY")) isIdentity = value.ToBool(); else if (colName.EqualsCI("NUM_PREC_RADIX")) { } else if (colName.EqualsCI("COLUMN_DEF")) { } else if (colName.EqualsCI("SQL_DATA_TYPE")) { } else if (colName.EqualsCI("SQL_DATETIME_SUB")) { } else if (colName.EqualsCI("CHAR_OCTET_LENGTH")) { } else if (colName.EqualsCI("SS_IS_SPARSE")) { } else if (colName.EqualsCI("SS_IS_COLUMN_SET")) { } else if (colName.EqualsCI("SS_IS_COMPUTED")) { } else if (colName.EqualsCI("SS_UDT_CATALOG_NAME")) { } else if (colName.EqualsCI("SS_UDT_SCHEMA_NAME")) { } else if (colName.EqualsCI("SS_UDT_ASSEMBLY_TYPE_NAME")) { } else if (colName.EqualsCI("SS_XML_SCHEMACOLLECTION_CATALOG_NAME")) { } else if (colName.EqualsCI("SS_XML_SCHEMACOLLECTION_SCHEMA_NAME")) { } else if (colName.EqualsCI("SS_XML_SCHEMACOLLECTION_NAME")) { } else if (colName.EqualsCI("SS_DATA_TYPE")) { } // Unrecognised columns. else SaLogger.Debug($"{SysUtil.Fn} Unrecognised Schema column name: {colName} [{value}]"); } if (!StringUtil.IsEmpty(columnName)) { //SaLogger.Debug(GetType().Name+".PopulateTableColumns Database=" + database + ", Schema=" + schema + ", Table=" + tableName + ", Column=" + columnName); DbDfnColumn column = new DbDfnColumn(schema, tableName, ++columnPos, columnName); column.DataTypeNative = nativeDataType; column.DataTypeNameNative = nativeDataTypeName; DbColumnDataType dataType = OdbcUtil.OdbcToDataType(nativeDataType); column.DataType = dataType; column.IsIdentity = isIdentity; column.BufferLen = bufferLen; column.Size = columnSize; column.DecimalDigits = decimalDigits; column.Nullable = nullable; column.IsNullable = isNullable; column.Remarks = remarks; table.AddColumn(column); } } } // Additional logic for SQL connections. SqlConnection sqlConnection = dbConnection as SqlConnection; if (sqlConnection != null) { // Retrieve additional information via SqlDataReader. using (SqlCommand sqlCommand = sqlConnection.CreateCommand()) { sqlCommand.CommandText = "Select Top 1 * From [" + table.Name + "]"; using (SqlDataReader sqlDataReader = sqlCommand.ExecuteReader(CommandBehavior.SchemaOnly)) using (DataTable dataTable = sqlDataReader.GetSchemaTable()) { foreach (DataRow row in dataTable.Rows) { String columnName = ""; bool isIdentity = false; foreach (DataColumn col in dataTable.Columns) { String value = row[col].ToString(); if (col.ColumnName.EqualsCI("ColumnName")) columnName = value; else if (col.ColumnName.EqualsCI("IsIdentity")) isIdentity = value.ToBool(); //if (columnName.EqualsCI("CreateOn")) SaLogger.Debug(GetType().Name+".PopulateTableColumns Column name: " + col.ColumnName + " [" + value + "]"); } DbDfnColumn columnDfn = table.Columns.GetColumnByName(columnName); if (columnDfn != null) { if (isIdentity) columnDfn.IsIdentity = isIdentity; } } } } } } //------------------------------------------------------------------- private void PopulateTableIndices(DbConnection dbConnection, DbDfnTable table) { String[] restrictions = new String[] { null, table.Schema, table.Name }; // database, owner, table name using (DataTable columnSchema = dbConnection.GetSchema(OdbcMetaDataCollectionNames.Indexes, restrictions)) { foreach (DataRow row in columnSchema.Rows) { //--------------------------------------------------------- // Columns are: Name Size Data Type Values // 1. TABLE_CAT 255 12 VarChar // Catalog name of the table to which the statistic or index applies. // 2. TABLE_SCHEM 128 12 VarChar // Schema name of the table to which the statistic or index applies. // 3. TABLE_NAME 128 12 VarChar // Table name of the table to which the statistic or index applies. // 4. NON_UNIQUE 5 5 SmallInt // Indicates whether the index prohibits duplicate values (SQL_TRUE or SQL_FALSE). // 5. INDEX_QUALIFIER VarChar // The identifier that is used to qualify the index name doing a DROP INDEX. // 6. INDEX_NAME VarChar Index name. // 7. TYPE SmallInt // Type of information being returned: // 0 SQL_TABLE_STAT indicates a statistic for the table (in the CARDINALITY or PAGES column). // ? SQL_INDEX_BTREE indicates a B-Tree index. // 1 SQL_INDEX_CLUSTERED indicates a clustered index. // ? SQL_INDEX_CONTENT indicates a content index. // 2 SQL_INDEX_HASHED indicates a hashed index. // 3 SQL_INDEX_OTHER indicates another type of index. // 8. ORDINAL_POSITION SmallInt // Column sequence number in index (starting with 1). // 9. COLUMN_NAME VarChar // Column name. If the column is based on an expression, // such as SALARY + BENEFITS, the expression is returned. // 10. ASC_OR_DESC Char(1) // Sort sequence for the column: "A" for ascending; "D" for descending. // 11. CARDINALITY Integer // Number of rows in table if TYPE is SQL_TABLE_STAT. // Number of unique values in the index if TYPE is not SQL_TABLE_STAT. // 12. PAGES Integer // Number of pages for the table if TYPE is SQL_TABLE_STAT. // Number of pages for the index if TYPE is not SQL_TABLE_STAT. // 13. FILTER_CONDITION VarChar // If the index is a filtered index, this is the filter condition, such as 'SALARY > 30000'. // Alternative column names: // ODBC 2.0 column ODBC 3.x column // TABLE_QUALIFIER TABLE_CAT // TABLE_OWNER TABLE_SCHEM // SEQ_IN_INDEX ORDINAL_POSITION // COLLATION ASC_OR_DESC // Sybase Advantage column names: // QUALIFIERNAME,OWNERNAME,TABLENAME,NONUNIQUE // INDEXQUALIFIERNAME,INDEXNAME,TYPE,SEQININDEX // COLUMNNAME,COLLATION,CARDINALITY,PAGES,FILTER //--------------------------------------------------------- String database = String.Empty, schema = String.Empty, tableName = String.Empty, indexQualifier = String.Empty, indexName = String.Empty, columnName = String.Empty, ascOrDesc = String.Empty, filterCondition = String.Empty; bool unique = false, primary = false, clustered = false, nullable = false; int indexType = 0; short ordinalPos = 0; int cardinality = 0, pages = 0; foreach (DataColumn column in columnSchema.Columns) { String colName = column.ColumnName; String value = row[colName].ToString(); if (colName.EqualsCI("TABLE_CAT") || colName.EqualsCI("TABLE_QUALIFIER") || colName.EqualsCI("QualifierName")) database = value; else if (colName.EqualsCI("TABLE_SCHEM") || colName.EqualsCI("TABLE_OWNER") || colName.EqualsCI("OwnerName")) schema = value; else if (colName.EqualsCI("TABLE_NAME") || colName.EqualsCI("TableName")) tableName = value; else if (colName.EqualsCI("NON_UNIQUE") || colName.EqualsCI("NONUNIQUE")) unique = (value.ToShort() == 0); else if (colName.EqualsCI("INDEX_QUALIFIER") || colName.EqualsCI("INDEXQUALIFIERNAME")) indexQualifier = value; else if (colName.EqualsCI("INDEX_NAME") || colName.EqualsCI("INDEXNAME")) indexName = value; else if (colName.EqualsCI("TYPE")) indexType = value.ToInt(); else if (colName.EqualsCI("TYPE_DESC")) { if (value.EqualsCI("CLUSTERED")) clustered = true; } else if (colName.EqualsCI("ORDINAL_POSITION") || colName.EqualsCI("SEQININDEX")) ordinalPos = value.ToShort(); else if (colName.EqualsCI("Column_Name") || colName.EqualsCI("ColumnName")) columnName = value; else if (colName.EqualsCI("ASC_OR_DESC")) ascOrDesc = value; else if (colName.EqualsCI("CARDINALITY")) cardinality = value.ToInt(); else if (colName.EqualsCI("PAGES")) pages = value.ToInt(); else if (colName.EqualsCI("FILTER_CONDITION") || colName.EqualsCI("FILTER")) filterCondition = value; //else SaLogger.Debug(GetType().Name+".PopulateTableIndices Unrecognised Schema column name: "+colName); //SaLogger.Debug(GetType().Name+".PopulateTableIndices Column name: " + colName+", Value="+value); } //------------------------------------------------------- String debugIndex = "PK_SaAttrUpdate"; if (indexName.EqualsCI(debugIndex) && false) { /* StockTracker Address table. SQL: Column: constraint_catalog. Value: SaStockTracker_Solent_STN Column: constraint_schema. Value: dbo Column: constraint_name. Value: PK_Address Column: table_catalog. Value: SaStockTracker_Solent_STN Column: table_schema. Value: dbo Column: table_name. Value: Address Column: index_name. Value: PK_Address Column: type_desc. Value: CLUSTERED ODBC: Column: TABLE_CAT. Value: SaStockTracker_Solent_STN Column: TABLE_SCHEM. Value: dbo Column: TABLE_NAME. Value: Address Column: NON_UNIQUE. Value: 0 Column: INDEX_QUALIFIER. Value: Address Column: INDEX_NAME. Value: PK_Address Column: TYPE. Value: 1 Column: ORDINAL_POSITION. Value: 1 Column: COLUMN_NAME. Value: Id Column: ASC_OR_DESC. Value: A Column: CARDINALITY. Value: 6 Column: PAGES. Value: 1 Column: FILTER_CONDITION. Value: */ foreach (DataColumn column in columnSchema.Columns) { String colName = column.ColumnName; String value = row[colName].ToString(); SaLogger.Debug(debugIndex + " Column: " + colName + ". Value: " + value); } } //------------------------------------------------------- if (!StringUtil.IsEmpty(indexName)) { //SaLogger.Debug(GetType().Name+".PopulateTableIndices Database=" + database + ", Schema=" + schema + ", Table=" + tableName + ", Index="+indexName+", Column=" + columnName); // Retrieve index or populate if not found. DbDfnIndex index = table.GetIndexByName(indexName); if (index == null) { index = new DbDfnIndex(schema, tableName, indexName, unique); index.IndexType = indexType; index.Primary = primary; index.Clustered = clustered; index.Nullable = nullable; index.Pages = pages; index.UniqueValues = cardinality; table.AddIndex(index); } // Add column to index: if (!StringUtil.IsEmpty(columnName)) { DbDfnIndexColumn indexColumn = new DbDfnIndexColumn(ordinalPos, columnName, (!ascOrDesc.EqualsCI("D"))); index.AddColumn(indexColumn); } } } } // Query to list included columns. // Select IndexName = i.Name, ColName = c.Name // From sys.indexes i // Inner Join sys.index_columns ic On ic.object_id = i.object_id And ic.index_id = i.index_id // Inner Join sys.columns c On c.object_id = ic.object_id And c.column_id = ic.column_id // Where ic.is_included_column = 1 // Order By i.Name // Attempt to retrieve included columns (SQL only). SqlConnection sqlConnection = dbConnection as SqlConnection; if (sqlConnection != null) { using (DataTable columnSchema = dbConnection.GetSchema("IndexColumns", restrictions)) { foreach (DataRow row in columnSchema.Rows) { foreach (DataColumn column in columnSchema.Columns) { String colName = column.ColumnName; String value = row[colName].ToString(); String constraintCatalog, constraintSchema, constraintName, tableCatalog, tableSchema, tableName, columnName, ordinalPosition, keyType, indexName; #if DEBUG //SaLogger.Trace(fn+" Column name=" + colName); #endif if (colName.EqualsCI("constraint_catalog")) constraintCatalog = value; else if (colName.EqualsCI("constraint_schema")) constraintSchema = value; else if (colName.EqualsCI("constraint_name")) constraintName = value; else if (colName.EqualsCI("table_catalog")) tableCatalog = value; else if (colName.EqualsCI("table_schema")) tableSchema = value; else if (colName.EqualsCI("table_name")) tableName = value; else if (colName.EqualsCI("column_name")) columnName = value; else if (colName.EqualsCI("ordinal_position")) ordinalPosition = value; else if (colName.EqualsCI("KeyType")) keyType = value; else if (colName.EqualsCI("index_name")) indexName = value; } } } } } //------------------------------------------------------------------- } //======================================================================= }