using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Data.Common; using System.Linq; using Sarand.Net.Standard.Base.Core; using Sarand.Net.Standard.Base.Dbas; using Sarand.Net.Standard.Dbas.Base; using Microsoft.Data.SqlClient; using Sarand.Net.Standard.Base.Utils; using Sarand.Net.Standard.Base.App; namespace Sarand.Net.Standard.Dbas.Data { //======================================================================= public abstract class DbDataItem : IDbDataItem { //------------------------------------------------------------------- // D a t a b a s e I n f o //------------------------------------------------------------------- // Definitions. /// /// Database information - object supplied by main application. /// NB Application may have multiple databases with schema differences. /// Each instance of a DbDataItem must be database specific. /// public class DatabaseInfo : IDatabaseInfo { // Data members. private readonly List tables; private static readonly String defaultSchemaName = "dbo"; // Construction. public DatabaseInfo (ISacsDatabase db) { this.DatabaseName = db.DatabaseName; this.tables = db.GetTableDfns(); } // Properties. public String DatabaseName { get; } public String Context { get { return StringUtil.OnEmpty(DatabaseName, "UnknownDb"); } } // Database schema. public DbDfnTable GetTableDefinition (String schemaName, String tableName) { foreach (DbDfnTable table in tables.FindAll(item => item.Name.EqualsCI(tableName))) { if (StringUtil.OnEmpty(table.Schema, defaultSchemaName).EqualsCI(StringUtil.OnEmpty(schemaName, defaultSchemaName))) return table; } return null; } public bool TableExists (String schemaName, String tableName) { return GetTableDefinition(schemaName, tableName) != null; } public bool ColumnExists (String schemaName, String tableName, String columnName) { DbDfnTable table = GetTableDefinition(schemaName, tableName); if (table == null) return false; return table.HasColumnByName(columnName); } public List GetColumnsForTable (String schemaName, String tableName) { DbDfnTable table = GetTableDefinition(schemaName, tableName); if (table != null) return table.Columns; return new List(); } } //------------------------------------------------------------------- // C o l u m n D a t a //------------------------------------------------------------------- /// Column data - column definition for DbDataItem. public class ColumnData { protected ColumnData (String name, bool identity = false) { Name = name; Identity = identity; } public String Name { get; } public bool Identity { get; } } public class ColumnDataString : ColumnData { public ColumnDataString (String name, bool identity = false) : base(name, identity) { } public String Value { get; set; } = String.Empty; } public class ColumnDataBoolean : ColumnData { public ColumnDataBoolean (String name, bool identity = false) : base(name, identity) { } public Boolean Value { get; set; } = false; } public class ColumnDataByte : ColumnData { public ColumnDataByte (String name, bool identity = false) : base(name, identity) { } public Byte Value { get; set; } = 0; } public class ColumnDataInt16 : ColumnData { public ColumnDataInt16 (String name, bool identity = false) : base(name, identity) { } public Int16 Value { get; set; } = 0; } public class ColumnDataInt32 : ColumnData { public ColumnDataInt32 (String name, bool identity = false) : base(name, identity) { } public Int32 Value { get; set; } = 0; } public class ColumnDataInt64 : ColumnData { public ColumnDataInt64 (String name, bool identity = false) : base(name, identity) { } public Int64 Value { get; set; } = 0; } public class ColumnDataSingle : ColumnData { public ColumnDataSingle (String name, bool identity = false) : base(name, identity) { } public Single Value { get; set; } = 0; } public class ColumnDataDouble : ColumnData { public ColumnDataDouble (String name, bool identity = false) : base(name, identity) { } public Double Value { get; set; } = 0; } public class ColumnDataDecimal : ColumnData { public ColumnDataDecimal (String name, bool identity = false) : base(name, identity) { } public Decimal Value { get; set; } = 0; } public class ColumnDataDateTime : ColumnData { public ColumnDataDateTime (String name, bool identity = false) : base(name, identity) { } public DateTime Value { get; set; } = DateTime.MinValue; } public class ColumnDataByteArray : ColumnData { public ColumnDataByteArray (String name, bool identity = false) : base(name, identity) { } public Byte[] Value { get; set; } = null; } //------------------------------------------------------------------- // D b D a t a I t e m //------------------------------------------------------------------- // Data members. private List existingColumnNamesFull = null; //------------------------------------------------------------------- // Construction. protected DbDataItem (IDatabaseInfo dbInfo = null) { DbInfo = dbInfo; } //------------------------------------------------------------------- // Properties. protected IDatabaseInfo DbInfo { get; } public abstract String SchemaName { get; } public abstract String TableName { get; } public virtual List AlternativeTableNames { get; } = new List(); public static List MissingColumns { get { columnsMissing.Sort(); return new List(columnsMissing); } } //------------------------------------------------------------------- /// Determine the table name used on the database which can differ if alternative names are available. protected String GetActualTableName (String tableName) { if (DbInfo != null) { if (DbInfo.TableExists(SchemaName, tableName)) return tableName; foreach (String tableNameAlternate in AlternativeTableNames) if (DbInfo.TableExists(SchemaName, tableNameAlternate)) return tableNameAlternate; } return tableName; } //------------------------------------------------------------------- // Population. public abstract void PopulateFrom (DbCommand dbCommand, String tableAlias, String filter); public abstract void PopulateFrom (DbDataReader dbdataReader); //------------------------------------------------------------------- // Database schema. public bool ColumnExists (String columnName) { if (DbInfo != null) return DbInfo.ColumnExists(SchemaName, TableName, columnName); return true; } public List GetExistingColumnNames (List columnNames) { List columnNamesExisting = new List(columnNames); if (existingColumnNamesFull == null) { // 'Existing column names (full)' contains the full list of columns confirmed to be on the database. // NB The list will be empty if no DbInfo supplied or the table is not found on the database. if (DbInfo != null) { List existingColumns = DbInfo.GetColumnsForTable(SchemaName,TableName); existingColumnNamesFull = existingColumns.GetNames(); } else existingColumnNamesFull = new List(); } if (existingColumnNamesFull.Count > 0) { // Remove columns that don't exist on the table. columnNamesExisting.RemoveAll(item => !existingColumnNamesFull.ContainsCI(item)); } return columnNamesExisting; } //------------------------------------------------------------------- // Data Reader interface. /// /// Get column index by name. /// Returns -1 if not found and logs message. /// protected int GetColumnOrdinal (DbDataReader dbDataReader, String columnName) { String fn = GetType().Name+".GetColumnOrdinal"; // Identify whether the specified column name is in the list of columns in the results set. // NB Efficient as doesn't load the table schema and avoids logging of exception. // Ref. https://stackoverflow.com/questions/373230/check-for-column-name-in-a-sqldatareader-object bool columnExists = Enumerable.Range(0, dbDataReader.FieldCount).Any(i => string.Equals(dbDataReader.GetName(i), columnName, StringComparison.OrdinalIgnoreCase)); if (columnExists) return dbDataReader.GetOrdinal(columnName); // Invalid column name for result set. if (DbInfo != null && !ColumnExists(columnName)) LogMissingColumn(columnName); else SaLogger.Warn(fn + " Column not found in result set: '" + columnName + "'"); return -1; /* * Dec-2018 Previous logic trapping specific exception. * Works well but automatically logs exception to debug console. try { return dbDataReader.GetOrdinal(columnName); } catch (Exception exc) { if (exc.GetType().Name.EqualsCI("IndexOutOfRangeException")) { // Invalid column name for result set. if (DbInfo != null && !ColumnExists(columnName)) { String columnIdentifier = DbInfo.Context+"."+(!StringUtil.IsEmpty(SchemaName)?SchemaName+".":"")+TableName+"."+columnName; if (!missingColumns.ContainsCI(columnIdentifier)) { missingColumns.Add(columnIdentifier); // Message is only logged on first occurence. SaLogger.Debug(fn + " Column not found on database: " + columnIdentifier); } } else SaLogger.Warn(fn + " Column not found in result set: '" + columnName + "'"); return -1; } } return -1; */ } //------------------------------------------------------------------- // Retrieve field values. public String GetFieldString (DbDataReader dbDataReader, String columnName, String defaultValue) { int columnIdx = GetColumnOrdinal(dbDataReader, columnName); if (columnIdx < 0) return defaultValue; // Check for null value (to avoid throwing exception). if (!dbDataReader.IsDBNull(columnIdx)) { Type dataType = dbDataReader.GetFieldType(columnIdx); // Expected data type. if (dataType == typeof(String)) return dbDataReader.GetString(columnIdx); LogAlternativeTypeColumn(columnName, typeof(String), dataType); } return defaultValue; } public Boolean GetFieldBoolean (DbDataReader dbDataReader, String columnName, Boolean defaultValue) { int columnIdx = GetColumnOrdinal(dbDataReader, columnName); if (columnIdx < 0) return defaultValue; // Check for null value (to avoid throwing exception). if (!dbDataReader.IsDBNull(columnIdx)) { Type dataType = dbDataReader.GetFieldType(columnIdx); // Expected data type. if (dataType == typeof(Boolean)) return dbDataReader.GetBoolean(columnIdx); LogAlternativeTypeColumn(columnName, typeof(Boolean), dataType); } return defaultValue; } public Byte GetFieldByte (DbDataReader dbDataReader, String columnName, Byte defaultValue) { int columnIdx = GetColumnOrdinal(dbDataReader, columnName); if (columnIdx < 0) return defaultValue; // Check for null value (to avoid throwing exception). if (!dbDataReader.IsDBNull(columnIdx)) { Type dataType = dbDataReader.GetFieldType(columnIdx); // Expected data type. if (dataType == typeof(Byte)) return dbDataReader.GetByte(columnIdx); LogAlternativeTypeColumn(columnName, typeof(Byte), dataType); } return defaultValue; } public Int16 GetFieldInt16 (DbDataReader dbDataReader, String columnName, Int16 defaultValue) { int columnIdx = GetColumnOrdinal(dbDataReader, columnName); if (columnIdx < 0) return defaultValue; // Check for null value (to avoid throwing exception). if (!dbDataReader.IsDBNull(columnIdx)) { Type dataType = dbDataReader.GetFieldType(columnIdx); // Expected data type. if (dataType == typeof(Int16)) return dbDataReader.GetInt16(columnIdx); LogAlternativeTypeColumn(columnName, typeof(Int16), dataType); } return defaultValue; } public Int32 GetFieldInt32 (DbDataReader dbDataReader, String columnName, Int32 defaultValue) { int columnIdx = GetColumnOrdinal(dbDataReader, columnName); if (columnIdx < 0) return defaultValue; // Check for null value (to avoid throwing exception). if (!dbDataReader.IsDBNull(columnIdx)) { Type dataType = dbDataReader.GetFieldType(columnIdx); // Expected data type. if (dataType == typeof(Int32)) return dbDataReader.GetInt32(columnIdx); LogAlternativeTypeColumn(columnName, typeof(Int32), dataType); if (dataType == typeof(Int16)) return (Int32)dbDataReader.GetInt16(columnIdx); } return defaultValue; } public Int64 GetFieldInt64 (DbDataReader dbDataReader, String columnName, Int64 defaultValue) { int columnIdx = GetColumnOrdinal(dbDataReader, columnName); if (columnIdx < 0) return defaultValue; // Check for null value (to avoid throwing exception). if (!dbDataReader.IsDBNull(columnIdx)) { Type dataType = dbDataReader.GetFieldType(columnIdx); // Expected data type. if (dataType == typeof(Int64)) return dbDataReader.GetInt64(columnIdx); LogAlternativeTypeColumn(columnName, typeof(Int64), dataType); if (dataType == typeof(Int32)) return (Int64)dbDataReader.GetInt32(columnIdx); if (dataType == typeof(Int16)) return (Int64)dbDataReader.GetInt16(columnIdx); } return defaultValue; } public Single GetFieldSingle (DbDataReader dbDataReader, String columnName, Single defaultValue) { int columnIdx = GetColumnOrdinal(dbDataReader, columnName); if (columnIdx < 0) return defaultValue; // Check for null value (to avoid throwing exception). if (!dbDataReader.IsDBNull(columnIdx)) { Type dataType = dbDataReader.GetFieldType(columnIdx); // Expected data type. if (dataType == typeof(Single)) return dbDataReader.GetFloat(columnIdx); LogAlternativeTypeColumn(columnName, typeof(Single), dataType); } return defaultValue; } public Double GetFieldDouble (DbDataReader dbDataReader, String columnName, Double defaultValue) { int columnIdx = GetColumnOrdinal(dbDataReader, columnName); if (columnIdx < 0) return defaultValue; // Check for null value (to avoid throwing exception). if (!dbDataReader.IsDBNull(columnIdx)) { Type dataType = dbDataReader.GetFieldType(columnIdx); // Expected data type. if (dataType == typeof(Double)) return dbDataReader.GetDouble(columnIdx); LogAlternativeTypeColumn(columnName, typeof(Double), dataType); if (dataType == typeof(decimal)) return (double)dbDataReader.GetDecimal(columnIdx); if (dataType == typeof(float)) return (double)dbDataReader.GetFloat(columnIdx); } return defaultValue; } public Decimal GetFieldDecimal (DbDataReader dbDataReader, String columnName, Decimal defaultValue) { int columnIdx = GetColumnOrdinal(dbDataReader, columnName); if (columnIdx < 0) return defaultValue; // Check for null value (to avoid throwing exception). if (!dbDataReader.IsDBNull(columnIdx)) { // Decimal data values may be alternative data types in result set. // Avoid using try/catch as failures automatically log cast exceptions. // Attempt to read value as decimal. Type dataType = dbDataReader.GetFieldType(columnIdx); // Expected data type. if (dataType == typeof(Decimal)) return dbDataReader.GetDecimal(columnIdx); LogAlternativeTypeColumn(columnName, typeof(Decimal), dataType); // Allow 'double' value. if (dataType == typeof(double)) return (decimal)dbDataReader.GetDouble(columnIdx); // Allow 'float' value. if (dataType == typeof(float)) return (decimal)dbDataReader.GetFloat(columnIdx); } return defaultValue; } public DateTime GetFieldDateTime (DbDataReader dbDataReader, String columnName, DateTime defaultValue) { int columnIdx = GetColumnOrdinal(dbDataReader, columnName); if (columnIdx < 0) return defaultValue; // Check for null value (to avoid throwing exception). if (!dbDataReader.IsDBNull(columnIdx)) { Type dataType = dbDataReader.GetFieldType(columnIdx); // Expected data type. if (dataType == typeof(DateTime)) return dbDataReader.GetDateTime(columnIdx); LogAlternativeTypeColumn(columnName, typeof(DateTime), dataType); } return defaultValue; } public byte[] GetFieldByteArray (DbDataReader dbDataReader, String columnName, byte[] defaultValue) { int columnIdx = GetColumnOrdinal(dbDataReader, columnName); if (columnIdx < 0) return defaultValue; // Check for null value (to avoid throwing exception). if (!dbDataReader.IsDBNull(columnIdx)) { Type dataType = dbDataReader.GetFieldType(columnIdx); // Expected data type. object data = dbDataReader[columnIdx]; if (data is byte[]) return (byte[])data; LogAlternativeTypeColumn(columnName, typeof(Byte[]), dataType); } return defaultValue; } //------------------------------------------------------------------- // Populate command parameters. // Value is passed as a Typed parameter (rather than object) so that it's type is known // even if the value is null, eg. to assign Db type for correct handling of null values, // ref. byte[] -> VarBinary. protected void AddCommandParameter (DbCommand dbCommand, String parameterName, ValueType value) { DbParameter dbParameter = dbCommand.CreateParameter(); dbParameter.ParameterName = parameterName; dbParameter.Value = (Object)value ?? DBNull.Value; if (typeof(ValueType) == typeof(Int16)) { dbParameter.DbType = System.Data.DbType.Int16; } else if (typeof(ValueType) == typeof(Int32)) { dbParameter.DbType = System.Data.DbType.Int32; } else if (typeof(ValueType) == typeof(DateTime)) { if (dbCommand is SqlCommand) dbParameter.DbType = System.Data.DbType.DateTime2; else dbParameter.DbType = System.Data.DbType.DateTime; if ((DateTime)(Object)value == DateTime.MinValue) dbParameter.Value = DBNull.Value; else { DateTime dateTime = (DateTime)dbParameter.Value; // Resolve millisecond precision inconsistency between c# and odbc/sql. //dbParameter.Value = new SqlDateTime((DateTime)dbParameter.Value); dbParameter.Value = dateTime.AddMilliseconds(-dateTime.Millisecond); // .Net 4.5 //dbParameter.Scale = 3; } } else if (typeof(ValueType) == typeof(byte[])) { // Must be explicit with column type to handle null values correctly. dbParameter.DbType = System.Data.DbType.Binary; } dbCommand.Parameters.Add(dbParameter); } //------------------------------------------------------------------- // Database updates via ODBC Command. public abstract bool ExecuteInsertCommand (DbCommand dbCommand); public abstract bool ExecuteUpdateCommand (DbCommand dbCommand); //public abstract bool ExecuteDeleteCommand (DbCommand dbCommand); //------------------------------------------------------------------- // Full list of all columns missing from database over life of application. private static readonly List columnsMissing = new List(); // Missing columns are only logged on their initial discovery. private void LogMissingColumn (String columnName) { String columnIdentifier = DbInfo.Context+"."+(!StringUtil.IsEmpty(SchemaName)?SchemaName+".":"")+TableName+"."+columnName; if (!columnsMissing.ContainsCI(columnIdentifier)) { columnsMissing.Add(columnIdentifier); // Message is only logged on first occurence. SaLogger.Debug(GetType().Name + " Column not found on database: " + columnIdentifier); } } //------------------------------------------------------------------- // Full list of all columns found with alternative data type in database over life of application. private static readonly List columnsAlternativeType = new List(); // Missing columns are only logged on their initial discovery. private void LogAlternativeTypeColumn (String columnName, Type typeExpected, Type typeFound) { String columnIdentifier = DbInfo.Context+"."+(!StringUtil.IsEmpty(SchemaName)?SchemaName+".":"")+TableName+"."+columnName; if (!columnsAlternativeType.ContainsCI(columnIdentifier)) { columnsAlternativeType.Add(columnIdentifier); // Message is only logged on first occurence. SaLogger.Debug(GetType().Name + " Column has inconsistent type: " + columnIdentifier + ". Expected: " + typeExpected.Name + ". Found: " + typeFound.Name); } } //------------------------------------------------------------------- // Logging. public static SaMessageLevel LoggingLevel { get; set; } = SaMessageLevel.None; public static String LoggingPrefix { get; set; } = " ~~~ "; protected void Log (String fn, String message, SaMessageLevel level = SaMessageLevel.Debug) { if ((int)level > (int)LoggingLevel) return; SaLogger.Log((fn + "\n\t" + message).Trim(), level); } //------------------------------------------------------------------- } //======================================================================= }