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);
}
//-------------------------------------------------------------------
}
//=======================================================================
}