using System.Collections;
using System.Data;
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.POIFS.FileSystem;
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;
}
}
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 SQLiteProvider : IProvider
{
private static object writelock = new();
private bool bForceRebuild;
private bool RebuildTriggers = false;
public SQLiteProvider(string filename)
{
var path = Path.GetDirectoryName(filename);
if (!path.IsNullOrWhiteSpace())
Directory.CreateDirectory(path);
URL = filename;
}
public string URL { get; set; }
public event LogEvent? OnLog;
///
/// An array containing every type in the database.
///
public Type[] Types { get; set; } = [];
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)));
}
using var access = GetWriteAccess();
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())
// OnLog?.Invoke(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))
{
OnLog?.Invoke(LogType.Information, $"Creating Table: {nameof(CustomProperty)}");
CreateTable(access, typeof(CustomProperty), true, []);
}
else
{
CheckFields(access, typeof(CustomProperty), value.Item1, []);
}
var customproperties = Load(); // new Filter(x => x.Class).IsEqualTo(type.EntityName()))
metadata = LoadMetaData();
foreach (var type in ordered)
{
if (type.GetCustomAttribute() == null)
{
table = type.EntityName().Split('.').Last();
if (!metadata.ContainsKey(table))
{
OnLog?.Invoke(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))
{
OnLog?.Invoke(LogType.Information, "Creating Table: " + type.EntityName().Split('.').Last());
CreateTable(access, type, true, customproperties);
}
else
{
CheckFields(access, type, metadata[table].Item1, customproperties);
}
}
}
if (bForceRebuild)
{
ExecuteSQL(access, "VACUUM;");
File.Delete(chkfile);
}
}
public bool IsRelational()
{
return true;
}
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;
OnLog?.Invoke(type, e.Statement + ";");
}
private SQLiteReadAccessor GetReadAccess()
{
var result = new SQLiteReadAccessor(URL);
result.Connection.Trace += Connection_Trace;
return result;
}
private SQLiteWriteAccessor GetWriteAccess()
{
var result = new SQLiteWriteAccessor(URL);
result.Connection.Trace += Connection_Trace;
return result;
}
private int ExecuteSQL(SQLiteWriteAccessor writer, string statement)
{
var result = 0;
try
{
using (var command = writer.CreateCommand())
{
command.CommandText = statement;
result = command.ExecuteNonQuery();
}
}
catch (Exception)
{
throw;
}
return result;
}
#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 = 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())
return "NUM";
if (type.GetInterfaces().Contains(typeof(IPackable)))
return "BLOB";
return "TEXT";
}
private void LoadFields(Type type, Dictionary fields, List? prefixes, CustomProperty[] customproperties)
{
if (prefixes == null)
prefixes = new List();
AutoEntity? view = type.GetCustomAttribute();
Type definition = view?.Generator != null
? view.Generator.Definition
: type;
var properties = CoreUtils.GetInheritedProperties(definition).Where(x =>
x.GetCustomAttribute() == null
// We think this is wrong; but who knows? && x.GetCustomAttribute() == null
&& x.GetCustomAttributes().FirstOrDefault(a => a.GetType().Equals(typeof(AggregateAttribute))) == null
&& x.GetCustomAttributes().FirstOrDefault(a => a.GetType().Equals(typeof(FormulaAttribute))) == null
&& x.GetCustomAttributes().FirstOrDefault(a => a.GetType().Equals(typeof(ConditionAttribute))) == null
&& x.GetCustomAttributes().FirstOrDefault(a => a.GetType().Equals(typeof(ChildEntityAttribute))) == null
&& x.CanWrite
&& x.PropertyType != typeof(UserProperties)
);
foreach (var property in properties)
if (property.PropertyType.GetInterfaces().Contains(typeof(IEnclosedEntity)))
{
LoadFields(property.PropertyType, fields, prefixes.Concat(new[] { property }).ToList(), customproperties);
}
else if (property.PropertyType.GetInterfaces().Contains(typeof(IEntityLink)))
{
var subprop = property.PropertyType.GetProperty("ID")!; // Not-null because IEntityLink has ID
var subname = ColumnName(prefixes.Concat(new[] { property, subprop }).ToArray());
var subtype = ColumnType(subprop.PropertyType);
fields[subname] = subtype;
}
else
{
var colname = ColumnName(prefixes.Concat(new[] { property }).ToArray());
var coltype = ColumnType(property.PropertyType);
fields[colname] = string.Format("{0}{1}", coltype, colname.Equals("ID") ? " PRIMARY KEY" : "");
}
// Now add in the Custom Properties (if any exist)
//CustomProperty[] customprops = Load(new Filter(x=>x.Class).IsEqualTo(type.EntityName()));
foreach (var prop in customproperties.Where(x => x.Class.Equals(type.EntityName())))
fields[prop.Name] = ColumnType(prop.PropertyType);
}
private List LoadIndexes(Type type)
{
var result = new List();
var properties = type.GetProperties().Where(x =>
x.GetCustomAttribute() == null && x.GetCustomAttribute() == null &&
x.GetCustomAttributes().FirstOrDefault(a => a.GetType().IsSubclassOf(typeof(AggregateAttribute))) == null
&& x.GetCustomAttributes().FirstOrDefault(a => a.GetType().Equals(typeof(ChildEntityAttribute))) == null
&& x.CanWrite);
foreach (var property in properties)
if (property.PropertyType.GetInterfaces().Contains(typeof(IEntityLink)))
{
var subprop = property.PropertyType.GetProperty("ID")!; // Not-null because IEntityLink has ID
var subname = ColumnName(property, subprop);
var tablename = type.EntityName().Split('.').Last();
result.Add(string.Format("CREATE INDEX idx{0}{1} ON {0} ([{2}])", tablename, subname.Replace(".", ""), subname));
}
else
{
var index = property.GetCustomAttributes().FirstOrDefault();
if (index != null)
{
var colname = ColumnName(property);
var tablename = type.EntityName().Split('.').Last();
result.Add(string.Format("CREATE INDEX idx{0}{1} ON {0} ([{2}])", tablename, colname.Replace(".", ""), colname));
}
}
return result;
}
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.RootProperties(otherType)
.Where(x => x.IsEntityLink
&& x.PropertyType.GetInterfaceDefinition(typeof(IEntityLink<>))?.GenericTypeArguments[0] == type
&& !x.IsCalculated);
foreach(var prop in props)
{
var fieldname = $"{prop.Name}.ID";
if(prop.GetAttribute() is EntityRelationshipAttribute attr
&& attr.Action == DeleteAction.Cascade)
{
cascadeFields.Add(fieldname);
}
else
{
setNullFields.Add(fieldname);
}
}
cascadeFields.Sort();
setNullFields.Sort();
if(cascadeFields.Count > 0)
{
cascades.Add(new(otherType, cascadeFields));
}
if(setNulls.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 = 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))
{
OnLog?.Invoke(LogType.Information, "Creating Table: " + type.EntityName().Split('.').Last());
using (var access = GetWriteAccess())
{
CreateTable(access, type, true, customproperties);
}
}
else
{
var type_fields = new Dictionary();
LoadFields(view.Generator.Definition, type_fields, null, customproperties);
using (var access = GetWriteAccess())
{
RebuildTable(access, type, metadata[table].Item1, type_fields, customproperties);
}
}
}
}
}
private Dictionary CheckDefaultColumns(IAutoEntityGenerator generator)
{
var viewfields = new Dictionary();
LoadFields(generator.Definition, viewfields, null, new CustomProperty[] { });
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())
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 = Columns.None(table.Entity);
var constants = CheckDefaultColumns(union);
var interfacefields = new Dictionary();
LoadFields(union.Definition, interfacefields, null, new CustomProperty[] { });
var entityfields = new Dictionary();
LoadFields(table.Entity, entityfields, null, new CustomProperty[] { });
foreach (var field in interfacefields.Keys)
{
if (entityfields.ContainsKey(field))
columns.Add(field);
else
{
var constant = table.Constants.FirstOrDefault(x => String.Equals(x.Mapping.Property, field));
if (constant != null)
constants[field] = constant.Value;
else
constants[field] = null;
}
}
var query = 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($"{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 = PrepareSelectNonGeneric(table.Type, new SQLiteCommand(), 'A',
table.Filter, table.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($"{EscapeValue(constant.Constant)} as [{constant.Mapping.Property}]");
foreach (var constant in CheckDefaultColumns(cartesian))
fields.Add($"{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, null, customproperties);
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
{
ExecuteSQL(access, statement);
}
catch (Exception e)
{
OnLog?.Invoke(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)
{
OnLog?.Invoke(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())
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))
ExecuteSQL(access, string.Format("DROP {0} {1};", drop, table));
CreateTable(access, type, true, customproperties);
}
catch (Exception e)
{
OnLog?.Invoke(LogType.Error, string.Format("RebuildTable({0}) [VIEW] failed: {1}\n{2}", table, e.Message, e.StackTrace));
throw;
}
}
else
{
OnLog?.Invoke(LogType.Information, "Rebuilding Table: " + table);
try
{
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)
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)
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)
{
ExecuteSQL(access,
string.Format("INSERT INTO {0} ({1}) SELECT {1} FROM _{0}_old;", table,
string.Join(", ", fields)));
ExecuteSQL(access, string.Format("DROP TABLE _{0}_old;", table));
}
transaction.Commit();
}
ExecuteSQL(access, "PRAGMA foreign_keys = on;");
}
catch (Exception e)
{
OnLog?.Invoke(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();
if ((view != null) && (view.Generator != null))
LoadFields(view.Generator.Definition, type_fields, null, customproperties);
else
LoadFields(type, type_fields, null, customproperties);
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);
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]))
ExecuteSQL(access, string.Format("DROP INDEX {0}", index));
foreach (var index in type_indexes)
if (!db_indexes.ContainsValue(index))
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))
ExecuteSQL(access, $"DROP TRIGGER {key}");
if(type_trigger is not null)
{
if (!db_triggers.ContainsValue(type_trigger))
ExecuteSQL(access, type_trigger);
}
//#endif
}
// private void CheckViews(SQLiteWriteAccessor access, Type type, Dictionary db_views)
// {
// var type_view = LoadView(type);
//
//
// if (!type_triggers.Contains(db_triggers[viewname]))
// ExecuteSQL(access, string.Format("DROP TRIGGER {0}", trigger));
//
// if (!db_views.ContainsValue(type_view))
// ExecuteSQL(access, type_view);
// }
#endregion
#region CRUD Operations
private static bool IsNull([NotNullWhen(false)] object? o)
{
return o == null || o is DBNull;
}
public object? Decode(object o, Type type)
{
if (IsNull(o))
return type == typeof(string) ? "" : type.GetDefault();
if (type == typeof(string[]))
{
if (!IsNull(o))
{
if (o is byte[] array)
using (var ms = new MemoryStream(array))
{
#pragma warning disable SYSLIB0011
var deser = new BinaryFormatter().Deserialize(ms);
#pragma warning restore SYSLIB0011
return deser as string[];
}
return Array.Empty();
}
return Array.Empty();
}
if (type.GetInterfaces().Contains(typeof(IPackable)))
{
var packable = (Activator.CreateInstance(type) as IPackable)!; // Not-null because of above check
if (!IsNull(o))
{
if (o is byte[] array)
{
using (var ms = new MemoryStream(array))
packable.Unpack(new BinaryReader(ms));
}
}
return packable;
}
if (type == typeof(DateTime))
return IsNull(o) || string.IsNullOrEmpty(o.ToString()) ? DateTime.MinValue : DateTime.Parse(o.ToString() ?? "");
if (type == typeof(TimeSpan))
{
if (IsNull(o))
return TimeSpan.MinValue;
var oStr = o.ToString() ?? "";
if (double.TryParse(oStr, out var hrs))
return TimeSpan.FromHours(hrs);
if (oStr.Contains(':'))
return TimeSpan.Parse(oStr);
}
if (type == typeof(Guid))
return IsNull(o) ? Guid.Empty : Guid.Parse(o.ToString() ?? "");
if (type == typeof(bool))
{
var oStr = o.ToString() ?? "";
return !IsNull(o) && (oStr.Equals("1") || oStr.ToUpper().Equals("TRUE"));
}
if (type.IsEnum)
return Enum.Parse(type, o.ToString() ?? "");
if (IsNull(o))
return null;
if (type == typeof(long))
return o;
var result = CoreUtils.ChangeType(o, type);
return result;
}
public static object Encode(object? o, Type? type)
{
if (IsNull(o) || type is null)
return DBNull.Value;
if (type == typeof(DateTime) && o.GetType() == typeof(string))
o = DateTime.Parse(o.ToString() ?? "");
if (type == typeof(double) && o.GetType() == typeof(string))
o = double.Parse(o.ToString() ?? "");
if (type == typeof(float) && o.GetType() == typeof(string))
o = float.Parse(o.ToString() ?? "");
if (type == typeof(long) && o.GetType() == typeof(string))
o = long.Parse(o.ToString() ?? "");
if (type.IsEnum && o.GetType() == typeof(string))
o = Enum.Parse(type, o.ToString() ?? "");
if (type == typeof(TimeSpan) && o.GetType() == typeof(string))
{
if (o.ToString()?.Contains(':') == true)
{
if (TimeSpan.TryParse(o.ToString(), out var time))
o = time;
}
else if (double.TryParse(o.ToString(), out var hrs))
{
o = TimeSpan.FromHours(hrs);
}
}
if (type == typeof(bool))
{
if (o.GetType() == typeof(string))
o = bool.Parse(o.ToString() ?? "");
if (o.Equals(false))
return DBNull.Value;
return o;
}
if (type == typeof(string) && string.IsNullOrEmpty(o.ToString()))
return DBNull.Value;
if (type == typeof(Guid) && o.GetType() == typeof(string))
o = Guid.Parse(o.ToString() ?? "");
if (o is string[])
using (var ms = new MemoryStream())
{
#pragma warning disable SYSLIB0011
new BinaryFormatter().Serialize(ms, o);
#pragma warning restore SYSLIB0011
return ms.GetBuffer();
}
if (o is IPackable pack)
{
using var ms = new MemoryStream();
using var writer = new BinaryWriter(ms);
pack.Pack(writer);
return ms.ToArray();
}
if (o.GetType() == typeof(double) && o.Equals(default(double)))
return DBNull.Value;
if (o.GetType() == typeof(float) && o.Equals(default(float)))
return DBNull.Value;
if (o.GetType() == typeof(int) && o.Equals(default(int)))
return DBNull.Value;
if (o.GetType() == typeof(long) && o.Equals(default(long)))
return DBNull.Value;
if (o.GetType() == typeof(DateTime))
{
if ((DateTime)o == DateTime.MinValue)
return DBNull.Value;
return string.Format("{0:yyyy-MM-dd HH:mm:ss.FFFFFFF}", o);
}
if (o.GetType() == typeof(TimeSpan))
{
if (((TimeSpan)o).Ticks == 0L)
return DBNull.Value;
return ((TimeSpan)o).TotalHours;
}
if (o.GetType() == typeof(Guid))
{
if (o.Equals(Guid.Empty))
return DBNull.Value;
return o.ToString() ?? "";
}
if (type == typeof(double) && o.GetType() == typeof(string))
if (double.TryParse((string)o, out var value))
o = value;
if (o.GetType().IsEnum)
return o.ToString() ?? "";
return o;
}
private static readonly Dictionary operators = new()
{
{ Operator.IsEqualTo, "{0} = {1}" },
{ Operator.IsNotEqualTo, "{0} != {1}" },
{ Operator.IsGreaterThan, "{0} > {1}" },
{ Operator.IsGreaterThanOrEqualTo, "{0} >= {1}" },
{ Operator.IsLessThan, "{0} < {1}" },
{ Operator.IsLessThanOrEqualTo, "{0} <= {1}" },
{ Operator.BeginsWith, "{0} LIKE {1} || '%'" },
{ Operator.Contains, "{0} LIKE '%' || {1} || '%'" },
{ Operator.EndsWith, "{0} LIKE '%' || {1}" },
{ Operator.InList, "{0} IN ({1})" },
{ Operator.NotInList, "{0} NOT IN ({1})" },
{ Operator.InQuery, "{0} IN ({1})" },
{ Operator.NotInQuery, "{0} NOT IN ({1})" }
};
private static string EscapeValue(object? value)
{
if (IsNull(value))
return "NULL";
if ((value is string) || (value is Enum) || (value is Guid))
return string.Format("\'" + "{0}" + "\'", value.ToString()?.Replace("\'", "\'\'"));
if (value is string[])
return string.Format("hex({0})", BitConverter.ToString(Encoding.ASCII.GetBytes(value.ToString() ?? "")).Replace("-", ""));
if (value is IColumn col)
return $"[{col.Property}]";
return value.ToString() ?? "";
}
private static string GetFilterConstant(FilterConstant constant)
{
return constant switch
{
FilterConstant.Now => "datetime()",
FilterConstant.Today => "datetime(date())",
FilterConstant.Zero => "0",
// These figures are based around today
FilterConstant.OneWeekAgo => "datetime(date(),'-7 days')",
FilterConstant.OneWeekAhead => "datetime(date(),'+7 days')",
FilterConstant.OneMonthAgo => "datetime(date(),'-1 month')",
FilterConstant.OneMonthAhead => "datetime(date(),'+1 month')",
FilterConstant.ThreeMonthsAgo => "datetime(date(),'-3 months')",
FilterConstant.ThreeMonthsAhead => "datetime(date(),'+3 months')",
FilterConstant.SixMonthsAgo => "datetime(date(),'-6 months')",
FilterConstant.SixMonthsAhead => "datetime(date(),'+6 months')",
FilterConstant.OneYearAgo => "datetime(date(),'-1 year')",
FilterConstant.OneYearAhead => "datetime(date(),'+1 year')",
// Relative Week values run from Monday thru Sunday
FilterConstant.StartOfLastWeek => "datetime(date(), 'weekday 0', '-13 days')",
FilterConstant.EndOfLastWeek => "datetime(date(), 'weekday 0', '-6 days', '-000.0001 seconds')",
FilterConstant.StartOfThisWeek => "datetime(date(), 'weekday 0', '-6 days')",
FilterConstant.EndOfThisWeek => "datetime(date(), 'weekday 0', '+1 day', '-000.0001 seconds')",
FilterConstant.StartOfNextWeek => "datetime(date(), 'weekday 0', '+1 days')",
FilterConstant.EndOfNextWeek => "datetime(date(), 'weekday 0', '+8 days', '-000.0001 seconds')",
FilterConstant.StartOfLastMonth => "datetime(date(), 'start of month', '-1 month')",
FilterConstant.EndOfLastMonth => "datetime(date(), 'start of month', '-000.0001 seconds')",
FilterConstant.StartOfThisMonth => "datetime(date(), 'start of month')",
FilterConstant.EndOfThisMonth => "datetime(date(), 'start of month', '+1 month', '-000.0001 seconds')",
FilterConstant.StartOfNextMonth => "datetime(date(), 'start of month', '+1 month')",
FilterConstant.EndOfNextMonth => "datetime(date(), 'start of month', '+2 months', '-000.0001 seconds')",
FilterConstant.StartOfLastCalendarYear => "datetime(date(), 'start of year', '-1 year')",
FilterConstant.EndOfLastCalendarYear => "datetime(date(), 'start of year', '-000.0001 seconds')",
FilterConstant.StartOfThisCalendarYear => "datetime(date(), 'start of year')",
FilterConstant.EndOfThisCalendarYear => "datetime(date(), 'start of year', '+1 year', '-000.0001 seconds')",
FilterConstant.StartOfNextCalendarYear => "datetime(date(), 'start of year', '+1 year')",
FilterConstant.EndOfNextCalendarYear => "datetime(date(), 'start of year', '+2 years', '-000.0001 seconds')",
FilterConstant.StartOfLastFinancialYear => "datetime(date(), '-18 months', 'start of year', '+6 months')",
FilterConstant.EndOfLastFinancialYear => "datetime(date(), '-18 months', 'start of year', '-18 months', '-000.0001 seconds')",
FilterConstant.StartOfThisFinancialYear => "datetime(date(), '-6 months', 'start of year', '+6 months')",
FilterConstant.EndOfThisFinancialYear => "datetime(date(), '-6 months', 'start of year', '+18 months', '-000.0001 seconds')",
FilterConstant.StartOfNextFinancialYear => "datetime(date(), '+6 months', 'start of year', '+6 months')",
FilterConstant.EndOfNextFinancialYear => "datetime(date(), '+6 months', 'start of year', '+18 months', '-000.0001 seconds')",
_ => throw new Exception($"FilterConstant.{constant} is not implemented!"),
};
}
private string GetFilterClauseNonGeneric(Type T, SQLiteCommand command, char prefix, IFilter? filter, List> tables,
Dictionary fieldmap, List columns, bool useparams)
{
if (filter == null || filter.Expression == null)
return "";
var result = "";
if (filter.Operator == Operator.All)
{
result = filter.IsNot ? "1 = 0" : "1 = 1";
}
else if (filter.Operator == Operator.None)
{
result = filter.IsNot ? "1 = 1" : "1 = 0";
}
else
{
string prop;
if (CoreUtils.TryFindMemberExpression(filter.Expression, out var mexp))
{
prop = CoreUtils.GetFullPropertyName(mexp, ".");
}
else
{
prop = filter.Expression.ToString();
if (prop.Contains("=>"))
prop = string.Join(".", prop.Split('.').Skip(1));
mexp = CoreUtils.GetMemberExpression(T, prop);
}
LoadFieldsandTables(command, T, prefix, fieldmap, tables, columns, prop, useparams);
if (fieldmap.ContainsKey(prop))
prop = fieldmap[prop];
if(filter.Value is CustomFilterValue)
{
throw new Exception("Custom Filter Value not able to be processed server-side!");
}
if (filter.Operator == Operator.InList || filter.Operator == Operator.NotInList)
{
// if, and only if the list contains Guids, we can safely bypass the
// 1000-parameter limit by using building the string ourselves
if (filter.Value is Guid[] list)
{
result = string.Format("(" + operators[filter.Operator] + ")", prop, string.Format("\"{0}\"", string.Join("\",\"", list)));
}
else if (filter.Value is IEnumerable enumerable)
{
var paramlist = new List