using System.Collections;
using System.Data;
using System.Data.Common;
using System.Data.SQLite;
using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions;
using System.Reflection;
using System.Resources;
using System.Runtime.Serialization.Formatters.Binary;
using System.Text;
using InABox.Core;
using Microsoft.CodeAnalysis;
using NPOI.SS.UserModel;
namespace InABox.Database.SQLite;
internal abstract class SQLiteAccessor : IDisposable
{
public SQLiteConnection Connection { get; private set; }
protected static int nConnections = 0;
public void Dispose()
{
Connection?.Close();
--nConnections;
}
public SQLiteCommand CreateCommand()
{
return new SQLiteCommand(Connection);
}
protected void Setup(string url)
{
var sb = new SQLiteConnectionStringBuilder();
sb.DataSource = url;
sb.Version = 3;
sb.DateTimeFormat = SQLiteDateFormats.Ticks;
sb.JournalMode = SQLiteJournalModeEnum.Wal;
var conn = sb.ToString();
Connection = new SQLiteConnection(conn);
Connection.BusyTimeout = Convert.ToInt32(TimeSpan.FromMinutes(2).TotalMilliseconds);
Connection.Open();
Connection.SetLimitOption(SQLiteLimitOpsEnum.SQLITE_LIMIT_VARIABLE_NUMBER, 10000);
++nConnections;
}
}
#region Custom Decimal Functions
[SQLiteFunction(Name = "DECIMAL_SUM", Arguments = 1, FuncType = FunctionType.Aggregate)]
public class SQLiteDecimalSum : SQLiteFunction
{
public override void Step(object[] args, int stepNumber, ref object contextData)
{
if (args.Length < 1 || args[0] == DBNull.Value)
return;
decimal d = Convert.ToDecimal(args[0]);
if (contextData != null) d += (decimal)contextData;
contextData = d;
}
public override object Final(object contextData)
{
return contextData;
}
}
[SQLiteFunction(Name = "DECIMAL_ADD", Arguments = -1, FuncType = FunctionType.Scalar)]
public class SQLiteDecimalAdd : SQLiteFunction
{
public override object? Invoke(object[] args)
{
var result = 0.0M;
for(int i = 0; i < args.Length; ++i)
{
var arg = args[i];
if(arg == DBNull.Value)
{
return null;
}
else
{
result += Convert.ToDecimal(arg);
}
}
return result;
}
}
[SQLiteFunction(Name = "DECIMAL_SUB", Arguments = -1, FuncType = FunctionType.Scalar)]
public class SQLiteDecimalSub : SQLiteFunction
{
public override object? Invoke(object[] args)
{
if(args.Length == 0)
{
return 0.0M;
}
else if(args.Length == 1)
{
if (args[0] == DBNull.Value)
{
return null;
}
else
{
return -Convert.ToDecimal(args[0]);
}
}
else
{
if (args[0] == DBNull.Value)
{
return null;
}
var result = Convert.ToDecimal(args[0]);
foreach(var arg in args.Skip(1))
{
if(arg == DBNull.Value)
{
return null;
}
result -= Convert.ToDecimal(arg);
}
return result;
}
}
}
[SQLiteFunction(Name = "DECIMAL_MUL", Arguments = -1, FuncType = FunctionType.Scalar)]
public class SQLiteDecimalMult : SQLiteFunction
{
public override object? Invoke(object[] args)
{
var result = 1.0M;
foreach(var arg in args)
{
if(arg == DBNull.Value)
{
return null;
}
result *= Convert.ToDecimal(arg);
}
return result;
}
}
[SQLiteFunction(Name = "DECIMAL_DIV", Arguments = -1, FuncType = FunctionType.Scalar)]
public class SQLiteDecimalDiv : SQLiteFunction
{
public override object? Invoke(object[] args)
{
if(args.Length == 0)
{
return 1.0M;
}
else if(args.Length == 1)
{
if (args[0] == DBNull.Value)
{
return null;
}
else
{
var denom = Convert.ToDecimal(args[0]);
if(denom == 0M)
{
return new Exception("Attempt to divide by zero.");
}
return 1.0M / denom;
}
}
else
{
if (args[0] == DBNull.Value)
{
return null;
}
var result = Convert.ToDecimal(args[0]);
foreach(var arg in args.Skip(1))
{
if(arg == DBNull.Value)
{
return null;
}
var denom = Convert.ToDecimal(arg);
if(denom == 0M)
{
return new Exception("Attempt to divide by zero.");
}
result /= denom;
}
return result;
}
}
}
#endregion
internal class SQLiteReadAccessor : SQLiteAccessor
{
public SQLiteReadAccessor(string url)
{
Setup(url);
}
}
internal class SQLiteWriteAccessor : SQLiteAccessor
{
private static readonly object writelock = new();
public SQLiteWriteAccessor(string url)
{
if (Monitor.TryEnter(writelock, new TimeSpan(0, 1, 0)))
try
{
Setup(url);
}
catch(Exception e)
{
throw new Exception($"Exception while acquiring write lock! {nConnections} connections open.", e);
}
finally
{
Monitor.Exit(writelock);
}
if (Connection == null)
throw new Exception($"Timeout while aqcuiring write lock! {nConnections} connections open.");
}
public SQLiteTransaction BeginTransaction()
{
return Connection.BeginTransaction();
}
}
public class SQLiteProviderFactory : IProviderFactory
{
private static object writelock = new();
private bool bForceRebuild;
private bool RebuildTriggers = false;
public SQLiteProviderFactory(string filename)
{
var path = Path.GetDirectoryName(filename);
if (!path.IsNullOrWhiteSpace())
Directory.CreateDirectory(path);
URL = filename;
}
public string URL { get; set; }
///
/// An array containing every type in the database.
///
public Type[] Types { get; set; } = [];
private SQLiteProvider MainProvider;
public SQLiteProvider NewProvider(Logger logger) => new SQLiteProvider(this)
{
Logger = logger
};
IProvider IProviderFactory.NewProvider(Logger logger) => NewProvider(logger);
public void Start()
{
var chkfile = Path.ChangeExtension(URL, "chk");
bForceRebuild = File.Exists(chkfile);
if (!File.Exists(URL))
{
SQLiteConnection.CreateFile(URL);
}
else if (bForceRebuild)
{
var i = 0;
while (File.Exists(Path.ChangeExtension(URL, string.Format("{0:D3}", i))))
i++;
File.Copy(URL, Path.ChangeExtension(URL, string.Format("{0:D3}", i)));
}
MainProvider = NewProvider(Logger.Main);
using var access = MainProvider.GetWriteAccess();
MainProvider.ExecuteSQL(access, "PRAGMA journal_mode=WAL;");
//using (var access = GetReadAccess())
//{
// using (var command = access.CreateCommand())
// {
// command.CommandText = "PRAGMA compile_options";
// using (var reader = command.ExecuteReader())
// {
// while (reader.Read())
// Log(LogType.Information,String.Format("{0}",reader.GetValue(0)));
// }
// }
//}
//ExecuteSQL("PRAGMA foreign_keys = on;");
foreach(var type in Types)
{
DatabaseSchema.CheckProperties(type);
}
// Need to arrange the typelist to ensure that foreign keys
// refer to tables that already exist
var ordered = new List();
foreach (var type in Types)
LoadType(type, ordered);
//Load up the metadata
var metadata = LoadMetaData();
var table = typeof(CustomProperty).EntityName().Split('.').Last();
if (!metadata.TryGetValue(table, out var value))
{
Log(LogType.Information, $"Creating Table: {nameof(CustomProperty)}");
CreateTable(access, typeof(CustomProperty), true, []);
}
else
{
CheckFields(access, typeof(CustomProperty), value.Item1, []);
}
var customproperties = MainProvider.Load(); // new Filter(x => x.Class).IsEqualTo(type.EntityName()))
DatabaseSchema.Load(customproperties);
metadata = LoadMetaData();
foreach (var type in ordered)
{
if (type.GetCustomAttribute() == null)
{
table = type.EntityName().Split('.').Last();
if (!metadata.ContainsKey(table))
{
Log(LogType.Information, "Creating Table: " + type.Name);
CreateTable(access, type, true, customproperties);
}
}
}
metadata = LoadMetaData();
foreach (var type in ordered)
{
if (type.GetCustomAttribute() == null)
{
table = type.EntityName().Split('.').Last();
CheckFields(access, type, metadata[table].Item1, customproperties);
}
}
metadata = LoadMetaData();
foreach (var type in ordered)
{
if (type.GetCustomAttribute() == null)
{
table = type.Name;
CheckTriggers(access, type, metadata[table].Item2);
}
}
metadata = LoadMetaData();
foreach (var type in ordered)
{
if (type.GetCustomAttribute() == null)
{
table = type.EntityName().Split('.').Last();
CheckIndexes(access, type, metadata[table].Item3);
}
}
metadata = LoadMetaData();
foreach (var type in ordered)
{
if (type.GetCustomAttribute() != null)
{
table = type.EntityName().Split('.').Last();
if (!metadata.ContainsKey(table))
{
Log(LogType.Information, "Creating Table: " + type.EntityName().Split('.').Last());
CreateTable(access, type, true, customproperties);
}
else
{
CheckFields(access, type, metadata[table].Item1, customproperties);
}
}
}
if (bForceRebuild)
{
MainProvider.ExecuteSQL(access, "VACUUM;");
File.Delete(chkfile);
}
}
public bool IsRelational()
{
return true;
}
#region Database Structure Management
private class MetadataEntry
{
public String Name { get; set; }
public String SQL { get; set; }
}
private class Metadata
{
public List Tables { get; init; }
public List Views { get; init; }
public List Indexes { get; init; }
public List Triggers { get; init; }
public Metadata() : base()
{
Tables = new List();
Views = new List();
Indexes = new List();
Triggers = new List();
}
}
private Dictionary, Dictionary, Dictionary>> LoadMetaData()
{
// Key is table name
// Item1 = Field Name / Field Type map
// Item2 = Trigger Name / Trigger SQL
// Item3 = Index Name / Index SQL
// Item4 = View Name /View SQL
var metadata = new Dictionary, Dictionary, Dictionary>>();
using (var access = MainProvider.GetReadAccess())
{
using (var command = access.CreateCommand())
{
command.CommandText = "SELECT tbl_name, sql, type FROM sqlite_master WHERE type='table' or type='view' ORDER BY type;";
using (var reader = command.ExecuteReader())
{
if (reader.HasRows)
while (reader.Read())
{
var tblinfo = new Tuple, Dictionary, Dictionary>(
new Dictionary(), new Dictionary(), new Dictionary());
var table = reader.GetString(0);
var sql = reader.GetString(1);
bool istable = String.Equals(reader.GetString(2),"table");
if (istable)
{
sql = sql.Replace("\"", "")
.Replace(string.Format("CREATE TABLE {0} (", table), "");
sql = sql.Remove(sql.Length - 1).Trim();
var fields = sql.Replace("\n\t", "").Replace("\t", " ").Replace("\"", "").Trim().Split(',');
var primarykey = "";
foreach (var fld in fields)
{
var field = fld.Trim().Replace("\t", " ").Replace("\"", "").Replace("[", "").Replace("]", "");
//if (field.ToUpper().StartsWith("CONSTRAINT"))
// tblinfo.Item2.Add(field);
if (field.ToUpper().StartsWith("PRIMARY KEY"))
{
primarykey = field.Replace("PRIMARY KEY(", "").Replace(")", "");
}
else
{
var comps = field.Split(' ');
tblinfo.Item1[comps[0]] = string.Format("{0}{1}", comps[1],
field.Contains("PRIMARY KEY") ? " PRIMARY KEY" : "");
}
}
if (!string.IsNullOrEmpty(primarykey))
{
var pkfld = tblinfo.Item1[primarykey];
if (!pkfld.ToUpper().Contains("PRIMARY KEY"))
tblinfo.Item1[primarykey] = string.Format("{0} PRIMARY KEY", pkfld.Trim());
}
}
else
{
sql = sql.Replace("\"", "")
.Replace("DISTINCT ", "");
sql = sql.Split(new String[] { " AS SELECT " }, StringSplitOptions.TrimEntries).Last();
sql = sql.Split(new String[] { " FROM " }, StringSplitOptions.TrimEntries).First();
var fields = sql.Replace("\n\t", "").Replace("\t", " ").Replace("\"", "").Trim().Split(',');
foreach (var fld in fields)
{
var field = fld.Trim()
.Replace("\t", " ")
.Replace("\"", "")
.Replace("[", "").Replace("]", "");
var parts = field.Split(" as ");
if(parts.Length == 1)
{
tblinfo.Item1[field] = "";
}
else if(parts.Length == 2)
{
field = parts[1];
if (parts[0] != "NULL")
{
tblinfo.Item1[field] = "";
}
}
}
}
metadata[table] = tblinfo;
}
reader.Close();
}
// Now Load Up all the indexes
command.CommandText = "select name, tbl_name, sql from sqlite_master where type='index' and sql is not null;";
using (var reader = command.ExecuteReader())
{
if (reader.HasRows)
while (reader.Read())
{
var name = reader.GetString(0);
var table = reader.GetString(1);
if (metadata.ContainsKey(table))
{
var tblInfo = metadata[table];
var sql = reader.GetString(2);
tblInfo.Item3[name] = sql;
}
}
}
// Now Load Up all the triggers
command.CommandText = "select name, tbl_name, sql from sqlite_master where type='trigger';";
using (var reader = command.ExecuteReader())
{
if (reader.HasRows)
while (reader.Read())
{
var name = reader.GetString(0);
var table = reader.GetString(1);
if (metadata.ContainsKey(table))
{
var tblInfo = metadata[table];
var sql = reader.GetString(2);
tblInfo.Item2[name] = sql;
}
}
}
}
}
return metadata;
}
private static void LoadType(Type type, List into)
{
if (into.Contains(type))
return;
var props = type.GetProperties().Where(x => x.PropertyType.GetInterfaces().Contains(typeof(IEntityLink)));
if (!props.Any())
{
into.Insert(0, type);
}
else
{
into.Add(type);
foreach (var prop in props)
{
var subtype = prop.PropertyType.BaseType;
while (subtype != null && !subtype.GetGenericArguments().Any())
subtype = subtype.BaseType;
subtype = subtype?.GetGenericArguments().FirstOrDefault();
if (subtype != null && subtype != type)
LoadType(subtype, into);
}
}
}
private string ColumnName(params PropertyInfo[] properties)
{
var bits = new List();
foreach (var property in properties)
bits.Add(property.Name);
var result = string.Join(".", bits);
return result;
}
private string ColumnType(Type type)
{
if (type.IsOrdinal())
return "INT";
if (type.IsEnum)
return "TEXT";
if (type == typeof(DateTime))
return "TEXT";
if (type == typeof(TimeSpan))
return "NUM";
if (type == typeof(string[]))
return "BLOB";
if (type == typeof(byte[]))
return "BLOB";
if (type.IsFloatingPoint() || type == typeof(decimal))
return "NUM";
if (type.GetInterfaces().Contains(typeof(IPackable)))
return "BLOB";
return "TEXT";
}
private void LoadFields(Type type, Dictionary fields)
{
AutoEntity? view = type.GetCustomAttribute();
Type definition = view?.Generator != null
? view.Generator.Definition
: type;
foreach(var property in DatabaseSchema.Properties(definition).Where(x => x.IsDBColumn))
{
fields[property.Name] =
ColumnType(property.PropertyType)
+ (property.Name.Equals("ID") ? " PRIMARY KEY" : "");
}
}
private List LoadIndexes(Type type)
{
var result = new List();
foreach (var property in DatabaseSchema.Properties(type).Where(x => x.IsDBColumn))
if (property.Parent?.IsEntityLink == true && property.Name.EndsWith(".ID"))
{
var tablename = type.EntityName().Split('.').Last();
result.Add(string.Format("CREATE INDEX idx{0}{1} ON {0} ([{2}])", tablename, property.Name.Replace(".", ""), property.Name));
}
else
{
if (property.HasAttribute())
{
var tablename = type.EntityName().Split('.').Last();
result.Add(string.Format("CREATE INDEX idx{0}{1} ON {0} ([{2}])", tablename, property.Name.Replace(".", ""), property.Name));
}
}
return result;
}
internal Dictionary>>> _cascades = new();
internal Dictionary>>> _setNulls = new();
private void LoadDeletions(Type type)
{
var cascades = new List>>();
var setNulls = new List>>();
foreach(var otherType in Types.Where(x => x.GetCustomAttribute() is null))
{
var setNullFields = new List();
var cascadeFields = new List();
var props = DatabaseSchema.LocalProperties(otherType)
.Where(x => x.Parent?.IsEntityLink == true
&& x.Parent.PropertyType.GetInterfaceDefinition(typeof(IEntityLink<>))?.GenericTypeArguments[0] == type
&& !x.IsCalculated);
foreach(var prop in props)
{
if(prop.Parent?.GetAttribute() is EntityRelationshipAttribute attr
&& attr.Action == DeleteAction.Cascade)
{
cascadeFields.Add(prop.Name);
}
else
{
setNullFields.Add(prop.Name);
}
}
cascadeFields.Sort();
setNullFields.Sort();
if(cascadeFields.Count > 0)
{
cascades.Add(new(otherType, cascadeFields));
}
if(setNullFields.Count > 0)
{
setNulls.Add(new(otherType, setNullFields));
}
}
if(cascades.Count > 0)
{
_cascades[type] = cascades;
}
if(setNulls.Count > 0)
{
_setNulls[type] = setNulls;
}
}
private string? LoadTrigger(Type type)
{
var actions = new List();
if(_setNulls.TryGetValue(type, out var setNulls))
{
foreach(var (otherType, fields) in setNulls)
{
foreach(var field in fields)
{
actions.Add($"UPDATE {otherType.Name} SET [{field}] = NULL WHERE [{field}] = old.ID;");
}
}
}
if(_cascades.TryGetValue(type, out var cascades))
{
foreach(var (otherType, fields) in cascades)
{
foreach(var field in fields)
{
actions.Add($"DELETE FROM {otherType.Name} WHERE [{field}] = old.ID;");
}
}
}
if (actions.Count != 0)
{
return $"CREATE TRIGGER {type.Name}_BEFOREDELETE BEFORE DELETE ON {type.Name} FOR EACH ROW BEGIN {string.Join(' ', actions)} END";
}
else
{
return null;
}
}
public void ForceRecreateViews()
{
var ordered = new List();
foreach (var type in Types)
LoadType(type, ordered);
var customproperties = MainProvider.Load();
var metadata = LoadMetaData();
foreach (var type in ordered)
{
var view = type.GetCustomAttribute();
if (view?.Generator != null)
{
var table = type.EntityName().Split('.').Last();
if (!metadata.ContainsKey(table))
{
Log(LogType.Information, "Creating Table: " + type.EntityName().Split('.').Last());
using (var access = MainProvider.GetWriteAccess())
{
CreateTable(access, type, true, customproperties);
}
}
else
{
var type_fields = new Dictionary();
LoadFields(view.Generator.Definition, type_fields);
using (var access = MainProvider.GetWriteAccess())
{
RebuildTable(access, type, metadata[table].Item1, type_fields, customproperties);
}
}
}
}
}
private Dictionary CheckDefaultColumns(IAutoEntityGenerator generator)
{
var viewfields = new Dictionary();
LoadFields(generator.Definition, viewfields);
Dictionary result = new Dictionary();
if (!viewfields.ContainsKey("ID"))
result["ID"] = null;
if (!viewfields.ContainsKey("Created"))
result["Created"] = null;
if (!viewfields.ContainsKey("CreatedBy"))
result["CreatedBy"] = null;
if (!viewfields.ContainsKey("LastUpdate"))
result["LastUpdate"] = null;
if (!viewfields.ContainsKey("LastUpdateBy"))
result["LastUpdateBy"] = null;
return result;
}
private void CreateTable(SQLiteWriteAccessor access, Type type, bool includeconstraints, CustomProperty[] customproperties)
{
var tablename = type.EntityName().Split('.').Last();
var ddl = new List();
var view = type.GetCustomAttribute();
if (view != null)
{
using (var command = access.CreateCommand())
{
command.CommandText = $"select name from sqlite_master where type='view' and name='{tablename}';";
using (var reader = command.ExecuteReader())
{
if (reader.HasRows)
while (reader.Read())
MainProvider.ExecuteSQL(access,string.Format("DROP VIEW {0}", reader.GetString(0)));
}
}
ddl.Add("CREATE VIEW");
ddl.Add(type.EntityName().Split('.').Last());
ddl.Add("AS");
if (view.Generator is IAutoEntityUnionGenerator union)
{
List queries = new List();
foreach (var table in union.Tables)
{
var columns = new List();
var constants = CheckDefaultColumns(union);
var interfacefields = new Dictionary();
LoadFields(union.Definition, interfacefields);
var entityfields = new Dictionary();
LoadFields(table.Entity, entityfields);
foreach (var field in interfacefields.Keys)
{
var mapping = table.Mappings.FirstOrDefault(x => String.Equals(x.Target.Property, field));
if (mapping != null)
columns.Add(mapping.Source);
else
{
var constant = table.Constants.FirstOrDefault(x => String.Equals(x.Mapping.Property, field));
if (constant != null)
constants[field] = constant.Value;
else
{
if (entityfields.ContainsKey(field))
columns.Add(Column.Create(type,field));
else
constants[field] = null;
}
}
}
var query = MainProvider.PrepareSelectNonGeneric(table.Entity, new SQLiteCommand(), 'A',
table.Filter, columns, null,
null, constants, null, union.Distinct, false);
queries.Add(query);
}
// var viewfields = new Dictionary();
//LoadFields(union.Definition, viewfields, null, new CustomProperty[] { });
// var fields = viewfields.Keys.Select(x => String.Format("[{0}]", x)).ToList();
// if (!fields.Contains("[ID]"))
// fields.Add(String.Format("NULL as [ID]"));
// if (!fields.Contains("[Created]"))
// fields.Add(String.Format("NULL as [Created]"));
// if (!fields.Contains("[CreatedBy]"))
// fields.Add(String.Format("NULL as [CreatedBy]"));
// if (!fields.Contains("[LastUpdate]"))
// fields.Add(String.Format("NULL as [LastUpdate]"));
// if (!fields.Contains("[LastUpdateBy]"))
// fields.Add(String.Format("NULL as [LastUpdateBy]"));
//
// List queries = new List();
// foreach (var entity in union.Entities)
// queries.Add(String.Format("SELECT {0} {1} FROM {2}",
// union.Distinct ? "DISTINCT" : "",
// String.Join(", ", fields),
// entity.EntityName().Split('.').Last())
// );
ddl.Add(String.Join(" UNION ", queries));
}
else if ( view.Generator is IAutoEntityCrossGenerator cross)
{
List constants = new List();
foreach (var constant in CheckDefaultColumns(cross))
constants.Add($"{SQLiteProvider.EscapeValue(constant.Value)} as [{constant.Key}]");
String query = String.Format(
"SELECT {0} {1}.[{2}] as [{3}], {4}.[{5}] as [{6}], {7} FROM {1}, {4} WHERE {1}.[{8}] = {4}.[{9}]",
cross.Distinct ? "DISTINCT" : "",
cross.LeftEntity(),
cross.LeftProperty(),
cross.LeftMapping(),
cross.RightEntity(),
cross.RightProperty(),
cross.RightMapping(),
String.Join(", ", constants),
cross.LeftLink(),
cross.RightLink()
);
ddl.Add(query);
}
else if ( view.Generator is IAutoEntityCartesianGenerator cartesian)
{
List fields = new List();
List tables = new List();
List filters = new List();
int iTable = 0;
foreach (var table in cartesian.Tables)
{
var subQueryText = MainProvider.PrepareSelectNonGeneric(table.Type, new SQLiteCommand(), 'A',
table.Filter, table.Columns.Columns(), null,
null, null, null, cartesian.Distinct, false);
tables.Add($"({subQueryText}) T{iTable}");
foreach (var mapping in table.Mappings)
fields.Add($"T{iTable}.[{mapping.Column.Property}] as [{mapping.Mapping.Property}]");
iTable++;
}
foreach (var constant in cartesian.Constants)
fields.Add($"{SQLiteProvider.EscapeValue(constant.Constant)} as [{constant.Mapping.Property}]");
foreach (var constant in CheckDefaultColumns(cartesian))
fields.Add($"{SQLiteProvider.EscapeValue(constant.Value)} as [{constant.Key}]");
StringBuilder sb = new StringBuilder();
sb.Append("SELECT ");
sb.Append(String.Join(", ", fields));
sb.Append(" FROM ");
sb.Append(String.Join(", ", tables));
if (filters.Any())
sb.Append($" WHERE {String.Join(" AND ", filters)}");
ddl.Add(sb.ToString());
}
ddl.Add(";");
var viewstatement = string.Join(" ", ddl);
}
else
{
ddl.Add("CREATE TABLE");
ddl.Add(type.EntityName().Split('.').Last());
ddl.Add("(");
var fields = new Dictionary();
var constraints = new List();
var indexes = new List();
LoadFields(type, fields);
var defs = new List();
foreach (var key in fields.Keys)
defs.Add(string.Format("[{0}] {1}", key, fields[key]));
if (includeconstraints)
defs.AddRange(constraints);
ddl.Add(string.Join(", ", defs));
ddl.Add(");");
}
var statement = string.Join(" ", ddl);
try
{
MainProvider.ExecuteSQL(access, statement);
}
catch (Exception e)
{
Log(LogType.Error, "Unable to Create Table: " + e.Message);
throw;
}
}
private void RebuildTable(SQLiteWriteAccessor access, Type type, Dictionary table_fields,
Dictionary type_fields,
CustomProperty[] customproperties)
{
var table = type.EntityName().Split('.').Last();
if (type.GetCustomAttribute() != null)
{
Log(LogType.Information, "Recreating View: " + table);
try
{
String drop = "";
using (var command = access.CreateCommand())
{
command.CommandText =
$"select name from sqlite_master where type='trigger' and tbl_name='{table}' and sql is not null;";
using (var reader = command.ExecuteReader())
{
if (reader.HasRows)
while (reader.Read())
MainProvider.ExecuteSQL(access,string.Format("DROP TRIGGER {0}", reader.GetString(0)));
}
command.CommandText = "select type from sqlite_master where name='" + table + "' and sql is not null;";
using (var reader = command.ExecuteReader())
{
if (reader.HasRows)
while (reader.Read())
drop = reader.GetString(0).ToUpper();
}
}
if (!String.IsNullOrWhiteSpace(drop))
MainProvider.ExecuteSQL(access, string.Format("DROP {0} {1};", drop, table));
CreateTable(access, type, true, customproperties);
}
catch (Exception e)
{
Log(LogType.Error, string.Format("RebuildTable({0}) [VIEW] failed: {1}\n{2}", table, e.Message, e.StackTrace));
throw;
}
}
else
{
Log(LogType.Information, "Rebuilding Table: " + table);
try
{
MainProvider.ExecuteSQL(access, "PRAGMA foreign_keys = off;");
// using (var command = access.CreateCommand())
// {
//
// command.CommandText =
// $"select name from sqlite_master where type='trigger' and tbl_name='{table}' and sql is not null;";
// using (var reader = command.ExecuteReader())
// {
// if (reader.HasRows)
// while (reader.Read())
// ExecuteSQL(access,string.Format("DROP TRIGGER {0}", reader.GetString(0)));
// }
// command.CommandText = $"select name from sqlite_master where type='view' and name='{table}';";
// using (var reader = command.ExecuteReader())
// {
// if (reader.HasRows)
// while (reader.Read())
// ExecuteSQL(access,string.Format("DROP VIEW {0}", reader.GetString(0)));
// }
// }
using (var transaction = access.Connection.BeginTransaction())
{
var drops = new List();
using (var command = access.CreateCommand())
{
//command.CommandText = String.Format("select name from sqlite_master where type='trigger' and tbl_name='{0}' and sql is not null;", table);
command.CommandText = "select name from sqlite_master where type='view' and sql is not null;";
using (var reader = command.ExecuteReader())
{
if (reader.HasRows)
while (reader.Read())
drops.Add(string.Format("DROP VIEW {0}", reader.GetString(0)));
}
//command.CommandText = String.Format("select name from sqlite_master where type='trigger' and tbl_name='{0}' and sql is not null;", table);
command.CommandText = "select name from sqlite_master where type='trigger' and sql is not null;";
using (var reader = command.ExecuteReader())
{
if (reader.HasRows)
while (reader.Read())
drops.Add(string.Format("DROP TRIGGER {0}", reader.GetString(0)));
}
command.CommandText = string.Format(
"select name from sqlite_master where type='index' and tbl_name='{0}' and sql is not null;",
table);
using (var reader = command.ExecuteReader())
{
if (reader.HasRows)
while (reader.Read())
drops.Add(string.Format("DROP INDEX {0}", reader.GetString(0)));
}
}
foreach (var drop in drops)
MainProvider.ExecuteSQL(access, drop);
bool existingtable = false;
using (var command = access.CreateCommand())
{
command.CommandText =
$"select name from sqlite_master where type='table' and tbl_name='{table}' and sql is not null;";
using (var reader = command.ExecuteReader())
{
if (reader.HasRows)
existingtable = true;
}
}
if (existingtable)
MainProvider.ExecuteSQL(access, string.Format("ALTER TABLE {0} RENAME TO _{0}_old;", table));
CreateTable(access, type, true, customproperties);
var fields = new List();
foreach (var field in type_fields.Keys)
if (table_fields.ContainsKey(field))
fields.Add("[" + field + "]");
if (existingtable)
{
MainProvider.ExecuteSQL(access,
string.Format("INSERT INTO {0} ({1}) SELECT {1} FROM _{0}_old;", table,
string.Join(", ", fields)));
MainProvider.ExecuteSQL(access, string.Format("DROP TABLE _{0}_old;", table));
}
transaction.Commit();
}
MainProvider.ExecuteSQL(access, "PRAGMA foreign_keys = on;");
}
catch (Exception e)
{
Log(LogType.Error, string.Format("RebuildTable({0}) [TABLE] failed: {1}\n{2}", table, e.Message, e.StackTrace));
throw;
}
}
}
private void CheckFields(SQLiteWriteAccessor access, Type type, Dictionary current_fields, CustomProperty[] customproperties)
{
var type_fields = new Dictionary();
var view = type.GetCustomAttribute();
LoadFields(type, type_fields);
var bRebuild = false;
foreach (var field in type_fields.Keys)
{
try
{
Type tType;
var cprop = customproperties.FirstOrDefault(x => string.Equals(x.Name, field));
if (cprop != null)
tType = cprop.PropertyType;
else
tType = CoreUtils.GetProperty(type, field).PropertyType;
if ((view == null) && (tType == typeof(TimeSpan)) && current_fields.ContainsKey(field) && !current_fields[field].Equals(type_fields[field]))
{
var sql = string.Format(
"update {0} set [{1}] = cast(substr([{1}],1,2) as double) + cast(substr([{1}],4,2) as double)/60 + cast(substr([{1}],7,2) as double)/3600 where [{1}] like \"%:%:%\"",
type.Name, field);
MainProvider.ExecuteSQL(access, sql);
}
}
catch (Exception e)
{
Logger.Send(LogType.Error, "", string.Format("*** Unknown Error: {0}\n{1}", e.Message, e.StackTrace));
}
if (!current_fields.ContainsKey(field) || (!String.IsNullOrWhiteSpace(current_fields[field])) && (current_fields[field] != type_fields[field]))
bRebuild = true;
}
foreach (var field in current_fields.Keys)
if (!type_fields.ContainsKey(field))
bRebuild = true;
if (bForceRebuild || bRebuild)
RebuildTable(access, type, current_fields, type_fields, customproperties);
}
private void CheckIndexes(SQLiteWriteAccessor access, Type type, Dictionary db_indexes)
{
var type_indexes = LoadIndexes(type);
foreach (var index in db_indexes.Keys)
if (!type_indexes.Contains(db_indexes[index]))
MainProvider.ExecuteSQL(access, string.Format("DROP INDEX {0}", index));
foreach (var index in type_indexes)
if (!db_indexes.ContainsValue(index))
MainProvider.ExecuteSQL(access, index);
}
private void CheckTriggers(SQLiteWriteAccessor access, Type type, Dictionary db_triggers)
{
LoadDeletions(type);
/*
#if PURGE
foreach (var trigger in db_triggers.Keys)
ExecuteSQL(access, string.Format("DROP TRIGGER {0}", trigger));
#else*/
var type_trigger = LoadTrigger(type);
foreach (var (key, trigger) in db_triggers)
if (!Equals(type_trigger, trigger))
MainProvider.ExecuteSQL(access, $"DROP TRIGGER {key}");
if(type_trigger is not null)
{
if (!db_triggers.ContainsValue(type_trigger))
MainProvider.ExecuteSQL(access, type_trigger);
}
//#endif
}
#endregion
private void Log(LogType type, string message)
{
Logger.Send(type, "", message);
}
}
public class SQLiteProvider : IProvider
{
public Logger Logger { get; set; }
private SQLiteProviderFactory Factory;
internal SQLiteProvider(SQLiteProviderFactory factory)
{
Factory = factory;
}
private void Connection_Trace(object sender, TraceEventArgs e)
{
var type = LogType.Query;
string[] updates = { "INSERT ", "UPDATE ", "DELETE ", "ALTER ", "DROP ", "CREATE ", "VACUUM" };
foreach (var update in updates)
type = e.Statement.ToUpper().StartsWith(update) ? LogType.Update : type;
Logger.Send(type, "", e.Statement + ";");
}
internal SQLiteReadAccessor GetReadAccess()
{
var result = new SQLiteReadAccessor(Factory.URL);
result.Connection.Trace += Connection_Trace;
return result;
}
internal SQLiteWriteAccessor GetWriteAccess()
{
var result = new SQLiteWriteAccessor(Factory.URL);
result.Connection.Trace += Connection_Trace;
return result;
}
#region Non-Entity Table operations
public static readonly string RANDOMGUID =
"lower(hex(randomblob(4)) " +
"|| '-' || hex(randomblob(2)) " +
"|| '-' || '4' || substr(hex( randomblob(2)), 2) " +
"|| '-' || substr('AB89', 1 + (abs(random()) % 4) , 1) || substr(hex(randomblob(2)), 2) " +
"|| '-' || hex(randomblob(6)))";
internal int ExecuteSQL(SQLiteWriteAccessor writer, string statement)
{
var result = 0;
try
{
using (var command = writer.CreateCommand())
{
command.CommandText = statement;
result = command.ExecuteNonQuery();
}
}
catch
{
throw;
}
return result;
}
public IEnumerable