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 ColumnDataBooleanNullable : ColumnData { public ColumnDataBooleanNullable(String name, bool identity = false) : base(name, identity) { } public Boolean? Value { get; set; } = null; } public class ColumnDataByte : ColumnData { public ColumnDataByte(String name, bool identity = false) : base(name, identity) { } public Byte Value { get; set; } = 0; } public class ColumnDataByteNullable : ColumnData { public ColumnDataByteNullable(String name, bool identity = false) : base(name, identity) { } public Byte? Value { get; set; } = null; } public class ColumnDataInt16 : ColumnData { public ColumnDataInt16(String name, bool identity = false) : base(name, identity) { } public Int16 Value { get; set; } = 0; } public class ColumnDataInt16Nullable : ColumnData { public ColumnDataInt16Nullable(String name, bool identity = false) : base(name, identity) { } public Int16? Value { get; set; } = null; } public class ColumnDataInt32 : ColumnData { public ColumnDataInt32(String name, bool identity = false) : base(name, identity) { } public Int32 Value { get; set; } = 0; } public class ColumnDataInt32Nullable : ColumnData { public ColumnDataInt32Nullable(String name, bool identity = false) : base(name, identity) { } public Int32? Value { get; set; } = null; } public class ColumnDataInt64 : ColumnData { public ColumnDataInt64(String name, bool identity = false) : base(name, identity) { } public Int64 Value { get; set; } = 0; } public class ColumnDataInt64Nullable : ColumnData { public ColumnDataInt64Nullable(String name, bool identity = false) : base(name, identity) { } public Int64? Value { get; set; } = null; } public class ColumnDataSingle : ColumnData { public ColumnDataSingle(String name, bool identity = false) : base(name, identity) { } public Single Value { get; set; } = 0; } public class ColumnDataSingleNullable : ColumnData { public ColumnDataSingleNullable(String name, bool identity = false) : base(name, identity) { } public Single? Value { get; set; } = null; } 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 ColumnDataDecimalNullable : ColumnData { public ColumnDataDecimalNullable(String name, bool identity = false) : base(name, identity) { } public Decimal? Value { get; set; } = null; } public class ColumnDataDateTime : ColumnData { public ColumnDataDateTime(String name, bool identity = false) : base(name, identity) { } public DateTime Value { get; set; } = DateTime.MinValue; } public class ColumnDataDateTimeNullable : ColumnData { public ColumnDataDateTimeNullable(String name, bool identity = false) : base(name, identity) { } public DateTime? Value { get; set; } = null; } public class ColumnDataGuid : ColumnData { public ColumnDataGuid(String name, bool identity = false) : base(name, identity) { } public Guid Value { get; set; } = default(Guid); } public class ColumnDataGuidNullable : ColumnData { public ColumnDataGuidNullable(String name, bool identity = false) : base(name, identity) { } public Guid Value { get; set; } = default(Guid); } public class ColumnDataByteArray : ColumnData { public ColumnDataByteArray(String name, bool identity = false) : base(name, identity) { } public Byte[] Value { get; set; } = null; } public class ColumnDataByteArrayNullable : ColumnData { public ColumnDataByteArrayNullable(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) { // 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($"{SysUtil.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($"{SysUtil.Fn} Column not found on database: {columnIdentifier}"); } } else SaLogger.Warn($"{SysUtil.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) { return GetFieldBooleanNullable(dbDataReader, columnName, defaultValue) ?? defaultValue; } public Boolean? GetFieldBooleanNullable(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) { return GetFieldByteNullable(dbDataReader, columnName, defaultValue) ?? defaultValue; } public Byte? GetFieldByteNullable(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) { return GetFieldInt16Nullable(dbDataReader, columnName, defaultValue) ?? defaultValue; } public Int16? GetFieldInt16Nullable(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) { return GetFieldInt32Nullable(dbDataReader, columnName, defaultValue) ?? defaultValue; } public Int32? GetFieldInt32Nullable(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) { return GetFieldInt64Nullable(dbDataReader, columnName, defaultValue) ?? defaultValue; } public Int64? GetFieldInt64Nullable(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) { return GetFieldSingleNullable(dbDataReader, columnName, defaultValue) ?? defaultValue; } public Single? GetFieldSingleNullable(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) { return GetFieldDoubleNullable(dbDataReader, columnName, defaultValue) ?? defaultValue; } public Double? GetFieldDoubleNullable(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) { return GetFieldDecimalNullable(dbDataReader, columnName, defaultValue) ?? defaultValue; } public Decimal? GetFieldDecimalNullable(DbDataReader dbDataReader, String columnName, Decimal? defaultValue = null) { 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) { return GetFieldDateTimeNullable(dbDataReader, columnName, defaultValue) ?? defaultValue; } public DateTime? GetFieldDateTimeNullable(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 Guid GetFieldGuid(DbDataReader dbDataReader, String columnName, Guid 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 Guid) return (Guid)data; LogAlternativeTypeColumn(columnName, typeof(Byte[]), 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) || typeof(ValueType) == typeof(Int16?)) { dbParameter.DbType = System.Data.DbType.Int16; } else if (typeof(ValueType) == typeof(Int32) || typeof(ValueType) == typeof(Int32?)) { dbParameter.DbType = System.Data.DbType.Int32; } else if (typeof(ValueType) == typeof(DateTime) || typeof(ValueType) == typeof(DateTime?)) { if (dbCommand is SqlCommand) dbParameter.DbType = System.Data.DbType.DateTime2; else dbParameter.DbType = System.Data.DbType.DateTime; if ((Object)value == null || (DateTime)(Object)value == DateTime.MinValue || (DateTime)(Object)value == DateTime.MaxValue) 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 virtual bool ExecuteUpdateCommand(DbCommand dbCommand) { return ExecuteUpdateCommand(dbCommand, null); } public virtual bool ExecuteUpdateCommand(DbCommand dbCommand, List columns = null) { throw new NotImplementedException($"ExecuteUpdateCommand(dbCmd,cols) not overridden. [{GetType().Name}]"); } //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); } //------------------------------------------------------------------- } //======================================================================= }