From 7e08a18d39e52542222fd48ec7f5b61001dcfff9 Mon Sep 17 00:00:00 2001 From: Lance Contreras Date: Tue, 2 Apr 2024 22:11:30 -0400 Subject: [PATCH] Implemented Fluent Api on the latest master based on @RoyGoode's code --- src/SQLite.cs | 412 ++++++++++++++++++++++++++++++++++++++++++--- src/SQLiteAsync.cs | 26 +++ 2 files changed, 416 insertions(+), 22 deletions(-) diff --git a/src/SQLite.cs b/src/SQLite.cs index 1697fe9f..02571537 100644 --- a/src/SQLite.cs +++ b/src/SQLite.cs @@ -595,6 +595,25 @@ public TableMapping GetMapping (Type type, CreateFlags createFlags = CreateFlags return map; } + /// + /// Add a custom mapping and replace the automatically generated mapping if it exists. + /// Use this method as an alternative to CreateTable with mapping. + /// + /// Table Mapping + /// + public void UseMapping (TableMapping tableMapping) + { + var key = tableMapping.MappedType.FullName; + lock (_mappings) { + if (_mappings.ContainsKey (key)) { + _mappings[key] = tableMapping; + } + else { + _mappings.Add (key, tableMapping); + } + } + } + /// /// Retrieves the mapping that is automatically generated for the given type. /// @@ -672,10 +691,39 @@ public CreateTableResult CreateTable (CreateFlags createFlags = CreateFlags.N public CreateTableResult CreateTable (Type ty, CreateFlags createFlags = CreateFlags.None) { var map = GetMapping (ty, createFlags); + return CreateTableFromMapping (map, createFlags); + } + + /// + /// Executes a "create table if not exists" on the database. It also + /// creates any specified indexes on the columns of the table. It uses + /// a schema automatically generated from the specified type. You can + /// later access this schema by calling GetMapping. + /// + /// + /// Optional flags allowing implicit PK and indexes based on naming conventions. + /// + /// Whether the table was created or migrated. + /// + public CreateTableResult CreateTable (TableMapping map, CreateFlags createFlags = CreateFlags.None) + { + UseMapping (map); + return CreateTableFromMapping (map, createFlags); + } + + /// + /// Creates a table using the mapping that is passed in. + /// + /// The mappint that the table will be created from + /// >Optional flags allowing implicit PK and indexes based on naming conventions. + /// + /// + private CreateTableResult CreateTableFromMapping (TableMapping map, CreateFlags createFlags = CreateFlags.None) + { // Present a nice error if no columns specified if (map.Columns.Length == 0) { - throw new Exception (string.Format ("Cannot create a table without columns (does '{0}' have public properties?)", ty.FullName)); + throw new Exception (string.Format ("Cannot create a table without columns (does '{0}' have public properties?)", map.MappedType)); } // Check if the table exists @@ -743,6 +791,25 @@ public CreateTableResult CreateTable (Type ty, CreateFlags createFlags = CreateF return result; } + + /// + /// Executes a "create table if not exists" on the database for each type. It also + /// creates any specified indexes on the columns of the table. + /// + /// + /// Whether the table was created or migrated for each type. + /// + public CreateTablesResult CreateTables (CreateFlags createFlags = CreateFlags.None, params TableMapping[] mappings) + { + var result = new CreateTablesResult (); + foreach (var mapping in mappings) { + UseMapping (mapping); + var aResult = CreateTableFromMapping (mapping, createFlags); + result.Results[mapping.MappedType] = aResult; + } + return result; + } + /// /// Executes a "create table if not exists" on the database for each type. It also /// creates any specified indexes on the columns of the table. It uses @@ -2553,23 +2620,21 @@ public class TableMapping { public Type MappedType { get; private set; } - public string TableName { get; private set; } + public string TableName { get; internal set; } - public bool WithoutRowId { get; private set; } + public bool WithoutRowId { get; internal set; } - public Column[] Columns { get; private set; } + public Column[] Columns { get; internal set; } - public Column PK { get; private set; } + public Column PK { get; internal set; } - public string GetByPrimaryKeySql { get; private set; } + public string GetByPrimaryKeySql { get; internal set; } public CreateFlags CreateFlags { get; private set; } internal MapMethod Method { get; private set; } = MapMethod.ByName; readonly Column _autoPk; - readonly Column[] _insertColumns; - readonly Column[] _insertOrReplaceColumns; public TableMapping (Type type, CreateFlags createFlags = CreateFlags.None) { @@ -2617,9 +2682,6 @@ public TableMapping (Type type, CreateFlags createFlags = CreateFlags.None) // People should not be calling Get/Find without a PK GetByPrimaryKeySql = string.Format ("select * from \"{0}\" limit 1", TableName); } - - _insertColumns = Columns.Where (c => !c.IsAutoInc).ToArray (); - _insertOrReplaceColumns = Columns.ToArray (); } private IReadOnlyCollection GetPublicMembers(Type type) @@ -2678,13 +2740,13 @@ public void SetAutoIncPK (object obj, long id) public Column[] InsertColumns { get { - return _insertColumns; + return Columns.Where (c => !c.IsAutoInc).ToArray (); } } public Column[] InsertOrReplaceColumns { get { - return _insertOrReplaceColumns; + return Columns.ToArray (); } } @@ -2707,28 +2769,28 @@ public class Column { MemberInfo _member; - public string Name { get; private set; } + public string Name { get; internal set; } public PropertyInfo PropertyInfo => _member as PropertyInfo; public string PropertyName { get { return _member.Name; } } - public Type ColumnType { get; private set; } + public Type ColumnType { get; internal set; } - public string Collation { get; private set; } + public string Collation { get; internal set; } - public bool IsAutoInc { get; private set; } - public bool IsAutoGuid { get; private set; } + public bool IsAutoInc { get; internal set; } + public bool IsAutoGuid { get; internal set; } - public bool IsPK { get; private set; } + public bool IsPK { get; internal set; } public IEnumerable Indices { get; set; } - public bool IsNullable { get; private set; } + public bool IsNullable { get; internal set; } - public int? MaxStringLength { get; private set; } + public int? MaxStringLength { get; internal set; } - public bool StoreAsText { get; private set; } + public bool StoreAsText { get; internal set; } public Column (MemberInfo member, CreateFlags createFlags = CreateFlags.None) { @@ -2820,6 +2882,11 @@ internal enum MapMethod ByName, ByPosition } + + public static TableMappingBuilder Builder () + { + return new TableMappingBuilder (); + } } class EnumCacheInfo @@ -4989,4 +5056,305 @@ public enum ColType : int Null = 5 } } + + internal static class TableMappingBuilderExtensions + { + internal static PropertyInfo AsPropertyInfo (this Expression> property) + { + Expression body = property.Body; + var operand = (body as UnaryExpression)?.Operand as MemberExpression; + if (operand != null) { + body = operand; + } + + return (body as MemberExpression)?.Member as PropertyInfo; + } + + internal static void AddPropertyValue (this Dictionary dict, Expression> property, T value) + { + var prop = AsPropertyInfo (property); + dict[prop] = value; + } + + internal static void AddProperty (this List list, Expression> property) + { + var prop = AsPropertyInfo (property); + if (!list.Contains (prop)) { + list.Add (prop); + } + } + + internal static void AddProperties (this List list, Expression>[] properties) + { + foreach (var property in properties) { + AddProperty (list, property); + } + } + + internal static T GetOrDefault (this Dictionary dict, PropertyInfo key, T defaultValue = default (T)) + { + if (dict.ContainsKey (key)) { + return dict[key]; + } + + return defaultValue; + } + } + + public class TableMappingBuilder + { + string _tableName; + PropertyInfo _primaryKey; + bool _withoutRowId = false; + + readonly List _ignore = new List (); + readonly List _autoInc = new List (); + readonly List _notNull = new List (); + readonly List _storeAsText = new List (); + + readonly Dictionary _columnNames = new Dictionary (); + readonly Dictionary _maxLengths = new Dictionary (); + readonly Dictionary _collations = new Dictionary (); + readonly Dictionary> _indices = new Dictionary> (); + + static Type MappedType => typeof (T); + + internal TableMappingBuilder () { } + + public TableMappingBuilder TableName (string name) + { + _tableName = name; + return this; + } + + public TableMappingBuilder WithoutRowId (bool value = true) + { + _withoutRowId = value; + return this; + } + + public TableMappingBuilder ColumnName (Expression> property, string name) + { + _columnNames.AddPropertyValue (property, name); + return this; + } + + public TableMappingBuilder MaxLength (Expression> property, int maxLength) + { + _maxLengths.AddPropertyValue (property, maxLength); + return this; + } + + public TableMappingBuilder Collation (Expression> property, string collation) + { + _collations.AddPropertyValue (property, collation); + return this; + } + + public TableMappingBuilder Index (Expression> property, bool unique = false, string indexName = null, int order = 0) + { + var prop = property.AsPropertyInfo (); + if (!_indices.ContainsKey (prop)) { + _indices[prop] = new List (); + } + + _indices[prop].Add (new IndexedAttribute { + Name = indexName, + Order = order, + Unique = unique + }); + return this; + } + + public TableMappingBuilder Index (string indexName, Expression> property, bool unique = false, int order = 0) + { + return Index (property, unique, indexName, order); + } + + public TableMappingBuilder Unique (Expression> property, string indexName = null, int order = 0) + { + return Index (property, true, indexName, order); + } + + public TableMappingBuilder Unique (string indexName, Expression> property, int order = 0) + { + return Index (property, true, indexName, order); + } + + public TableMappingBuilder Index (params Expression>[] properties) + { + for (int i = 0; i < properties.Length; i++) { + Index (properties[i], false, null, i); + } + + return this; + } + + public TableMappingBuilder Index (string indexName, params Expression>[] properties) + { + for (int i = 0; i < properties.Length; i++) { + Index (properties[i], false, indexName, i); + } + + return this; + } + + public TableMappingBuilder Unique (params Expression>[] properties) + { + for (int i = 0; i < properties.Length; i++) { + Index (properties[i], true, null, i); + } + + return this; + } + + public TableMappingBuilder Unique (string indexName, params Expression>[] properties) + { + for (int i = 0; i < properties.Length; i++) { + Index (properties[i], true, indexName, i); + } + + return this; + } + + public TableMappingBuilder PrimaryKey (Expression> property, bool autoIncrement = false) + { + _primaryKey = property.AsPropertyInfo (); + if (autoIncrement) { + _autoInc.Add (_primaryKey); + } + + return this; + } + + public TableMappingBuilder Ignore (Expression> property) + { + _ignore.AddProperty (property); + return this; + } + + public TableMappingBuilder Ignore (params Expression>[] properties) + { + _ignore.AddProperties (properties); + return this; + } + + public TableMappingBuilder AutoIncrement (Expression> property) + { + _autoInc.AddProperty (property); + return this; + } + + public TableMappingBuilder AutoIncrement (params Expression>[] properties) + { + _autoInc.AddProperties (properties); + return this; + } + + public TableMappingBuilder NotNull (Expression> property) + { + _notNull.AddProperty (property); + return this; + } + + public TableMappingBuilder NotNull (params Expression>[] properties) + { + _notNull.AddProperties (properties); + return this; + } + + public TableMappingBuilder StoreAsText (Expression> property) + { + _storeAsText.AddProperty (property); + return this; + } + + public TableMappingBuilder StoreAsText (params Expression>[] properties) + { + _storeAsText.AddProperties (properties); + return this; + } + + /// + /// Creates a table mapping based on the expressions provided to the builder. + /// + /// The table mapping as created by the builder. + public TableMapping Build () + { + var tableMapping = new TableMapping (MappedType, CreateFlags.None) { + WithoutRowId = _withoutRowId, + TableName = _tableName + }; + + var props = new List (); + var baseType = MappedType; + var propNames = new HashSet (); + while (baseType != typeof (object)) { + var ti = baseType.GetTypeInfo (); + var newProps = ( + from p in ti.DeclaredProperties + where + !propNames.Contains (p.Name) && + p.CanRead && p.CanWrite && + (p.GetMethod != null) && (p.SetMethod != null) && + (p.GetMethod.IsPublic && p.SetMethod.IsPublic) && + (!p.GetMethod.IsStatic) && (!p.SetMethod.IsStatic) + select p).ToList (); + foreach (var p in newProps) { + propNames.Add (p.Name); + } + + props.AddRange (newProps); + baseType = ti.BaseType; + } + + var cols = new List (); + + foreach (var p in props) { + if (p.CanWrite && !_ignore.Contains (p)) { + var col = new TableMapping.Column (p) { + Name = _columnNames.GetOrDefault (p, p.Name), + //If this type is Nullable then Nullable.GetUnderlyingType returns the T, otherwise it returns null, so get the actual type instead + ColumnType = Nullable.GetUnderlyingType (p.PropertyType) ?? p.PropertyType, + Collation = _collations.GetOrDefault (p, ""), + IsPK = p == _primaryKey + }; + + bool isAuto = _autoInc.Contains (p); + col.IsAutoGuid = isAuto && col.ColumnType == typeof (Guid); + col.IsAutoInc = isAuto && !col.IsAutoGuid; + + col.Indices = _indices.GetOrDefault (p, new List (0)); + + col.IsNullable = !(col.IsPK || _notNull.Contains (p)); + col.MaxStringLength = _maxLengths.GetOrDefault (p, null); + col.StoreAsText = _storeAsText.Contains (p); + + cols.Add (col); + } + } + + tableMapping.Columns = cols.ToArray (); + + foreach (var c in tableMapping.Columns) { + if (c.IsAutoInc && c.IsPK) { + tableMapping.SetAutoIncPK (c, 0); + } + + if (c.IsPK) { + tableMapping.PK = c; + } + } + + if (tableMapping.PK != null) { + tableMapping.GetByPrimaryKeySql = $"select * from \"{tableMapping.TableName}\" where \"{tableMapping.PK.Name}\" = ?"; + } + else { + // People should not be calling Get/Find without a PK + tableMapping.GetByPrimaryKeySql = $"select * from \"{tableMapping.TableName}\" limit 1"; + } + + return tableMapping; + } + } } diff --git a/src/SQLiteAsync.cs b/src/SQLiteAsync.cs index a33f80f4..55bee342 100644 --- a/src/SQLiteAsync.cs +++ b/src/SQLiteAsync.cs @@ -382,6 +382,20 @@ public Task CreateTableAsync (Type ty, CreateFlags createFlag return WriteAsync (conn => conn.CreateTable (ty, createFlags)); } + /// + /// Executes a "create table if not exists" on the database. It also + /// creates any specified indexes on the columns of the table. + /// + /// The table mapping to create the table from. + /// Optional flags allowing implicit PK and indexes based on naming conventions. + /// + /// Whether the table was created or migrated. + /// + public Task CreateTableAsync (TableMapping map, CreateFlags createFlags = CreateFlags.None) + { + return WriteAsync (conn => conn.CreateTable (map, createFlags)); + } + /// /// Executes a "create table if not exists" on the database for each type. It also /// creates any specified indexes on the columns of the table. It uses @@ -466,6 +480,18 @@ public Task CreateTablesAsync (CreateFlags createFlags = Cre return WriteAsync (conn => conn.CreateTables (createFlags, types)); } + /// + /// Executes a "create table if not exists" on the database for each type. It also + /// creates any specified indexes on the columns of the table. + /// + /// + /// Whether the table was created or migrated for each type. + /// + public Task CreateTablesAsync (CreateFlags createFlags = CreateFlags.None, params TableMapping[] mappings) + { + return WriteAsync (conn => conn.CreateTables (createFlags, mappings)); + } + /// /// Executes a "drop table" on the database. This is non-recoverable. ///