From eca7250cf08acc44cc4420f2e87dc1257e1ea0a3 Mon Sep 17 00:00:00 2001 From: Roy Goode Date: Mon, 2 Jul 2018 10:11:34 +0100 Subject: [PATCH 01/18] Fixed malformed XML comments --- src/SQLite.cs | 3 ++- src/SQLiteAsync.cs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/SQLite.cs b/src/SQLite.cs index c276066f..65c06d91 100644 --- a/src/SQLite.cs +++ b/src/SQLite.cs @@ -1406,7 +1406,8 @@ public void RunInTransaction (Action action) /// /// /// An of the objects to insert. - /// + /// + /// /// A boolean indicating if the inserts should be wrapped in a transaction. /// /// diff --git a/src/SQLiteAsync.cs b/src/SQLiteAsync.cs index 2823748a..1b2cf2b3 100644 --- a/src/SQLiteAsync.cs +++ b/src/SQLiteAsync.cs @@ -911,7 +911,8 @@ public Task ExecuteAsync (string query, params object[] args) /// /// /// An of the objects to insert. - /// + /// + /// /// A boolean indicating if the inserts should be wrapped in a transaction. /// /// From aab9d06cc31dfa277dc787029003419d9b7a72f0 Mon Sep 17 00:00:00 2001 From: Roy Goode Date: Mon, 2 Jul 2018 11:03:16 +0100 Subject: [PATCH 02/18] TableMapping.Column changed to ColumnMapping This is necessary to allow for ColumnMapping and TableMapping to have derived classes in the next step of Fluent API development --- src/SQLite.cs | 148 +++++++++++++++++++++++++------------------------- 1 file changed, 74 insertions(+), 74 deletions(-) diff --git a/src/SQLite.cs b/src/SQLite.cs index 65c06d91..7ab14928 100644 --- a/src/SQLite.cs +++ b/src/SQLite.cs @@ -74,7 +74,7 @@ public static SQLiteException New (SQLite3.Result r, string message) public class NotNullConstraintViolationException : SQLiteException { - public IEnumerable Columns { get; protected set; } + public IEnumerable Columns { get; protected set; } protected NotNullConstraintViolationException (SQLite3.Result r, string message) : this (r, message, null, null) @@ -762,7 +762,7 @@ public List GetTableInfo (string tableName) void MigrateTable (TableMapping map, List existingCols) { - var toBeAdded = new List (); + var toBeAdded = new List (); foreach (var p in map.Columns) { var found = false; @@ -2177,17 +2177,17 @@ public class TableMapping public bool WithoutRowId { get; private set; } - public Column[] Columns { get; private set; } + public ColumnMapping[] Columns { get; private set; } - public Column PK { get; private set; } + public ColumnMapping PK { get; private set; } public string GetByPrimaryKeySql { get; private set; } public CreateFlags CreateFlags { get; private set; } - readonly Column _autoPk; - readonly Column[] _insertColumns; - readonly Column[] _insertOrReplaceColumns; + readonly ColumnMapping _autoPk; + readonly ColumnMapping[] _insertColumns; + readonly ColumnMapping[] _insertOrReplaceColumns; public TableMapping (Type type, CreateFlags createFlags = CreateFlags.None) { @@ -2225,11 +2225,11 @@ from p in ti.DeclaredProperties baseType = ti.BaseType; } - var cols = new List (); + var cols = new List (); foreach (var p in props) { var ignore = p.IsDefined (typeof (IgnoreAttribute), true); if (!ignore) { - cols.Add (new Column (p, createFlags)); + cols.Add (new ColumnMapping (p, createFlags)); } } Columns = cols.ToArray (); @@ -2265,105 +2265,105 @@ public void SetAutoIncPK (object obj, long id) } } - public Column[] InsertColumns { + public ColumnMapping[] InsertColumns { get { return _insertColumns; } } - public Column[] InsertOrReplaceColumns { + public ColumnMapping[] InsertOrReplaceColumns { get { return _insertOrReplaceColumns; } } - public Column FindColumnWithPropertyName (string propertyName) + public ColumnMapping FindColumnWithPropertyName (string propertyName) { var exact = Columns.FirstOrDefault (c => c.PropertyName == propertyName); return exact; } - public Column FindColumn (string columnName) + public ColumnMapping FindColumn (string columnName) { var exact = Columns.FirstOrDefault (c => c.Name.ToLower () == columnName.ToLower ()); return exact; } + } - public class Column - { - PropertyInfo _prop; + public class ColumnMapping + { + PropertyInfo _prop; - public string Name { get; private set; } + public string Name { get; private set; } - public PropertyInfo PropertyInfo => _prop; + public PropertyInfo PropertyInfo => _prop; - public string PropertyName { get { return _prop.Name; } } + public string PropertyName { get { return _prop.Name; } } - public Type ColumnType { get; private set; } + public Type ColumnType { get; private set; } - public string Collation { get; private set; } + public string Collation { get; private set; } - public bool IsAutoInc { get; private set; } - public bool IsAutoGuid { get; private set; } + public bool IsAutoInc { get; private set; } + public bool IsAutoGuid { get; private set; } - public bool IsPK { get; private set; } + public bool IsPK { get; private set; } - public IEnumerable Indices { get; set; } + public IEnumerable Indices { get; set; } - public bool IsNullable { get; private set; } + public bool IsNullable { get; private set; } - public int? MaxStringLength { get; private set; } + public int? MaxStringLength { get; private set; } - public bool StoreAsText { get; private set; } + public bool StoreAsText { get; private set; } - public Column (PropertyInfo prop, CreateFlags createFlags = CreateFlags.None) - { - var colAttr = prop.CustomAttributes.FirstOrDefault (x => x.AttributeType == typeof (ColumnAttribute)); - - _prop = prop; - Name = (colAttr != null && colAttr.ConstructorArguments.Count > 0) ? - colAttr.ConstructorArguments[0].Value?.ToString () : - prop.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 (prop.PropertyType) ?? prop.PropertyType; - Collation = Orm.Collation (prop); - - IsPK = Orm.IsPK (prop) || - (((createFlags & CreateFlags.ImplicitPK) == CreateFlags.ImplicitPK) && - string.Compare (prop.Name, Orm.ImplicitPkName, StringComparison.OrdinalIgnoreCase) == 0); - - var isAuto = Orm.IsAutoInc (prop) || (IsPK && ((createFlags & CreateFlags.AutoIncPK) == CreateFlags.AutoIncPK)); - IsAutoGuid = isAuto && ColumnType == typeof (Guid); - IsAutoInc = isAuto && !IsAutoGuid; - - Indices = Orm.GetIndices (prop); - if (!Indices.Any () - && !IsPK - && ((createFlags & CreateFlags.ImplicitIndex) == CreateFlags.ImplicitIndex) - && Name.EndsWith (Orm.ImplicitIndexSuffix, StringComparison.OrdinalIgnoreCase) - ) { - Indices = new IndexedAttribute[] { new IndexedAttribute () }; - } - IsNullable = !(IsPK || Orm.IsMarkedNotNull (prop)); - MaxStringLength = Orm.MaxStringLength (prop); + public ColumnMapping (PropertyInfo prop, CreateFlags createFlags = CreateFlags.None) + { + var colAttr = prop.CustomAttributes.FirstOrDefault (x => x.AttributeType == typeof (ColumnAttribute)); - StoreAsText = prop.PropertyType.GetTypeInfo ().CustomAttributes.Any (x => x.AttributeType == typeof (StoreAsTextAttribute)); - } + _prop = prop; + Name = (colAttr != null && colAttr.ConstructorArguments.Count > 0) ? + colAttr.ConstructorArguments[0].Value?.ToString () : + prop.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 (prop.PropertyType) ?? prop.PropertyType; + Collation = Orm.Collation (prop); - public void SetValue (object obj, object val) - { - if (val != null && ColumnType.GetTypeInfo ().IsEnum) { - _prop.SetValue (obj, Enum.ToObject (ColumnType, val)); - } - else { - _prop.SetValue (obj, val, null); - } + IsPK = Orm.IsPK (prop) || + (((createFlags & CreateFlags.ImplicitPK) == CreateFlags.ImplicitPK) && + String.Compare (prop.Name, Orm.ImplicitPkName, StringComparison.OrdinalIgnoreCase) == 0); + + var isAuto = Orm.IsAutoInc (prop) || (IsPK && ((createFlags & CreateFlags.AutoIncPK) == CreateFlags.AutoIncPK)); + IsAutoGuid = isAuto && ColumnType == typeof (Guid); + IsAutoInc = isAuto && !IsAutoGuid; + + Indices = Orm.GetIndices (prop); + if (!Indices.Any () + && !IsPK + && ((createFlags & CreateFlags.ImplicitIndex) == CreateFlags.ImplicitIndex) + && Name.EndsWith (Orm.ImplicitIndexSuffix, StringComparison.OrdinalIgnoreCase) + ) { + Indices = new IndexedAttribute[] { new IndexedAttribute () }; } + IsNullable = !(IsPK || Orm.IsMarkedNotNull (prop)); + MaxStringLength = Orm.MaxStringLength (prop); - public object GetValue (object obj) - { - return _prop.GetValue (obj, null); + StoreAsText = prop.PropertyType.GetTypeInfo ().CustomAttributes.Any (x => x.AttributeType == typeof (StoreAsTextAttribute)); + } + + public void SetValue (object obj, object val) + { + if (val != null && ColumnType.GetTypeInfo ().IsEnum) { + _prop.SetValue (obj, Enum.ToObject (ColumnType, val)); } + else { + _prop.SetValue (obj, val, null); + } + } + + public object GetValue (object obj) + { + return _prop.GetValue (obj, null); } } @@ -2433,7 +2433,7 @@ public static Type GetType (object obj) return obj.GetType (); } - public static string SqlDecl (TableMapping.Column p, bool storeDateTimeAsTicks) + public static string SqlDecl (ColumnMapping p, bool storeDateTimeAsTicks) { string decl = "\"" + p.Name + "\" " + SqlType (p, storeDateTimeAsTicks) + " "; @@ -2453,7 +2453,7 @@ public static string SqlDecl (TableMapping.Column p, bool storeDateTimeAsTicks) return decl; } - public static string SqlType (TableMapping.Column p, bool storeDateTimeAsTicks) + public static string SqlType (ColumnMapping p, bool storeDateTimeAsTicks) { var clrType = p.ColumnType; if (clrType == typeof (Boolean) || clrType == typeof (Byte) || clrType == typeof (UInt16) || clrType == typeof (SByte) || clrType == typeof (Int16) || clrType == typeof (Int32) || clrType == typeof (UInt32) || clrType == typeof (Int64)) { @@ -2657,7 +2657,7 @@ public IEnumerable ExecuteDeferredQuery (TableMapping map) var stmt = Prepare (); try { - var cols = new TableMapping.Column[SQLite3.ColumnCount (stmt)]; + var cols = new ColumnMapping[SQLite3.ColumnCount (stmt)]; for (int i = 0; i < cols.Length; i++) { var name = SQLite3.ColumnName16 (stmt, i); From 2aea54cbc89ae928f8c1103049f232c25329d3c6 Mon Sep 17 00:00:00 2001 From: Roy Goode Date: Mon, 2 Jul 2018 11:13:21 +0100 Subject: [PATCH 03/18] Introduced ColumnMappingFromAttributes Implements the original functionality of TableMapping.Column for building column definitions from classes decorated with attributes, to allow the functionality to be kept separate from the new Fluent API --- src/SQLite.cs | 63 ++++++++++++++++++++++++++++----------------------- 1 file changed, 35 insertions(+), 28 deletions(-) diff --git a/src/SQLite.cs b/src/SQLite.cs index 7ab14928..5689b970 100644 --- a/src/SQLite.cs +++ b/src/SQLite.cs @@ -2229,7 +2229,7 @@ from p in ti.DeclaredProperties foreach (var p in props) { var ignore = p.IsDefined (typeof (IgnoreAttribute), true); if (!ignore) { - cols.Add (new ColumnMapping (p, createFlags)); + cols.Add (new ColumnMappingFromAttributes (p, createFlags)); } } Columns = cols.ToArray (); @@ -2292,36 +2292,58 @@ public ColumnMapping FindColumn (string columnName) public class ColumnMapping { - PropertyInfo _prop; + readonly PropertyInfo _prop; - public string Name { get; private set; } + public string Name { get; protected set; } public PropertyInfo PropertyInfo => _prop; public string PropertyName { get { return _prop.Name; } } - public Type ColumnType { get; private set; } + public Type ColumnType { get; protected set; } - public string Collation { get; private set; } + public string Collation { get; protected set; } - public bool IsAutoInc { get; private set; } - public bool IsAutoGuid { get; private set; } + public bool IsAutoInc { get; protected set; } + public bool IsAutoGuid { get; protected set; } - public bool IsPK { get; private set; } + public bool IsPK { get; protected set; } public IEnumerable Indices { get; set; } - public bool IsNullable { get; private set; } + public bool IsNullable { get; protected set; } - public int? MaxStringLength { get; private set; } + public int? MaxStringLength { get; protected set; } - public bool StoreAsText { get; private set; } + public bool StoreAsText { get; protected set; } + + public ColumnMapping (PropertyInfo prop) + { + _prop = prop; + } + + public void SetValue (object obj, object val) + { + if (val != null && ColumnType.GetTypeInfo ().IsEnum) { + _prop.SetValue (obj, Enum.ToObject (ColumnType, val)); + } + else { + _prop.SetValue (obj, val, null); + } + } + + public object GetValue (object obj) + { + return _prop.GetValue (obj, null); + } + } - public ColumnMapping (PropertyInfo prop, CreateFlags createFlags = CreateFlags.None) + class ColumnMappingFromAttributes : ColumnMapping + { + internal ColumnMappingFromAttributes (PropertyInfo prop, CreateFlags createFlags = CreateFlags.None) : base(prop) { var colAttr = prop.CustomAttributes.FirstOrDefault (x => x.AttributeType == typeof (ColumnAttribute)); - _prop = prop; Name = (colAttr != null && colAttr.ConstructorArguments.Count > 0) ? colAttr.ConstructorArguments[0].Value?.ToString () : prop.Name; @@ -2350,21 +2372,6 @@ public ColumnMapping (PropertyInfo prop, CreateFlags createFlags = CreateFlags.N StoreAsText = prop.PropertyType.GetTypeInfo ().CustomAttributes.Any (x => x.AttributeType == typeof (StoreAsTextAttribute)); } - - public void SetValue (object obj, object val) - { - if (val != null && ColumnType.GetTypeInfo ().IsEnum) { - _prop.SetValue (obj, Enum.ToObject (ColumnType, val)); - } - else { - _prop.SetValue (obj, val, null); - } - } - - public object GetValue (object obj) - { - return _prop.GetValue (obj, null); - } } class EnumCacheInfo From 89b86d2c00b9d06eabdbc85906d4850856b1c017 Mon Sep 17 00:00:00 2001 From: Roy Goode Date: Mon, 2 Jul 2018 11:25:22 +0100 Subject: [PATCH 04/18] Introduced TableMappingFromAttributes Implements the original functionality of TableMapping for building table definitions from classes decorated with attributes, to allow the functionality to be kept separate from the new Fluent API --- src/SQLite.cs | 99 +++++++++++++++++++++++++++------------------------ 1 file changed, 53 insertions(+), 46 deletions(-) diff --git a/src/SQLite.cs b/src/SQLite.cs index 5689b970..635fc647 100644 --- a/src/SQLite.cs +++ b/src/SQLite.cs @@ -400,12 +400,12 @@ public TableMapping GetMapping (Type type, CreateFlags createFlags = CreateFlags lock (_mappings) { if (_mappings.TryGetValue (key, out map)) { if (createFlags != CreateFlags.None && createFlags != map.CreateFlags) { - map = new TableMapping (type, createFlags); + map = new TableMappingFromAttributes (type, createFlags); _mappings[key] = map; } } else { - map = new TableMapping (type, createFlags); + map = new TableMappingFromAttributes (type, createFlags); _mappings.Add (key, map); } } @@ -2171,27 +2171,67 @@ public class StoreAsTextAttribute : Attribute public class TableMapping { - public Type MappedType { get; private set; } + public Type MappedType { get; } - public string TableName { get; private set; } + public string TableName { get; protected set; } - public bool WithoutRowId { get; private set; } + public bool WithoutRowId { get; protected set; } - public ColumnMapping[] Columns { get; private set; } + public ColumnMapping[] Columns { get; protected set; } - public ColumnMapping PK { get; private set; } + public ColumnMapping PK { get; protected set; } - public string GetByPrimaryKeySql { get; private set; } + public string GetByPrimaryKeySql { get; protected set; } - public CreateFlags CreateFlags { get; private set; } + public CreateFlags CreateFlags { get; protected set; } - readonly ColumnMapping _autoPk; - readonly ColumnMapping[] _insertColumns; - readonly ColumnMapping[] _insertOrReplaceColumns; + protected ColumnMapping _autoPk; + protected ColumnMapping[] _insertColumns; + protected ColumnMapping[] _insertOrReplaceColumns; + + public bool HasAutoIncPK { get; protected set; } - public TableMapping (Type type, CreateFlags createFlags = CreateFlags.None) + public void SetAutoIncPK (object obj, long id) + { + if (_autoPk != null) { + _autoPk.SetValue (obj, Convert.ChangeType (id, _autoPk.ColumnType, null)); + } + } + + public ColumnMapping[] InsertColumns { + get { + return _insertColumns; + } + } + + public ColumnMapping[] InsertOrReplaceColumns { + get { + return _insertOrReplaceColumns; + } + } + + public ColumnMapping FindColumnWithPropertyName (string propertyName) + { + var exact = Columns.FirstOrDefault (c => c.PropertyName == propertyName); + return exact; + } + + public ColumnMapping FindColumn (string columnName) + { + var exact = Columns.FirstOrDefault (c => c.Name.ToLower () == columnName.ToLower ()); + return exact; + } + + public TableMapping (Type type) { MappedType = type; + } + } + + class TableMappingFromAttributes : TableMapping + { + internal TableMappingFromAttributes (Type type, CreateFlags createFlags = CreateFlags.None) : base(type) + { CreateFlags = createFlags; var typeInfo = type.GetTypeInfo (); @@ -2255,39 +2295,6 @@ from p in ti.DeclaredProperties _insertColumns = Columns.Where (c => !c.IsAutoInc).ToArray (); _insertOrReplaceColumns = Columns.ToArray (); } - - public bool HasAutoIncPK { get; private set; } - - public void SetAutoIncPK (object obj, long id) - { - if (_autoPk != null) { - _autoPk.SetValue (obj, Convert.ChangeType (id, _autoPk.ColumnType, null)); - } - } - - public ColumnMapping[] InsertColumns { - get { - return _insertColumns; - } - } - - public ColumnMapping[] InsertOrReplaceColumns { - get { - return _insertOrReplaceColumns; - } - } - - public ColumnMapping FindColumnWithPropertyName (string propertyName) - { - var exact = Columns.FirstOrDefault (c => c.PropertyName == propertyName); - return exact; - } - - public ColumnMapping FindColumn (string columnName) - { - var exact = Columns.FirstOrDefault (c => c.Name.ToLower () == columnName.ToLower ()); - return exact; - } } public class ColumnMapping From 521d7800da40f9aac55060b38c52c8de6679174f Mon Sep 17 00:00:00 2001 From: Roy Goode Date: Mon, 2 Jul 2018 11:33:20 +0100 Subject: [PATCH 05/18] Introduced ColumnIndex class and IColumnIndex interface This provides a common API for indices created by the original attribute-based implementation and the new Fluent API implementation so they can be used interchangeably --- src/SQLite.cs | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/SQLite.cs b/src/SQLite.cs index 635fc647..11a2d57d 100644 --- a/src/SQLite.cs +++ b/src/SQLite.cs @@ -2055,6 +2055,13 @@ public SQLiteConnectionString (string databasePath, bool storeDateTimeAsTicks) } } + public interface IColumnIndex + { + string Name { get; set; } + int Order { get; set; } + bool Unique { get; set; } + } + [AttributeUsage (AttributeTargets.Class)] public class TableAttribute : Attribute { @@ -2095,7 +2102,7 @@ public class AutoIncrementAttribute : Attribute } [AttributeUsage (AttributeTargets.Property)] - public class IndexedAttribute : Attribute + public class IndexedAttribute : Attribute, IColumnIndex { public string Name { get; set; } public int Order { get; set; } @@ -2169,6 +2176,13 @@ public class StoreAsTextAttribute : Attribute { } + public class ColumnIndex : IColumnIndex + { + public string Name { get; set; } + public int Order { get; set; } + public bool Unique { get; set; } + } + public class TableMapping { public Type MappedType { get; } @@ -2316,7 +2330,7 @@ public class ColumnMapping public bool IsPK { get; protected set; } - public IEnumerable Indices { get; set; } + public IEnumerable Indices { get; set; } public bool IsNullable { get; protected set; } @@ -2372,7 +2386,7 @@ internal ColumnMappingFromAttributes (PropertyInfo prop, CreateFlags createFlags && ((createFlags & CreateFlags.ImplicitIndex) == CreateFlags.ImplicitIndex) && Name.EndsWith (Orm.ImplicitIndexSuffix, StringComparison.OrdinalIgnoreCase) ) { - Indices = new IndexedAttribute[] { new IndexedAttribute () }; + Indices = new IColumnIndex[] { new IndexedAttribute () }; } IsNullable = !(IsPK || Orm.IsMarkedNotNull (prop)); MaxStringLength = Orm.MaxStringLength (prop); From b5d33658b37ba16a53d1b8e8067adb21815ef8e8 Mon Sep 17 00:00:00 2001 From: Roy Goode Date: Mon, 2 Jul 2018 11:45:51 +0100 Subject: [PATCH 06/18] Added UseMapping method This will be used by the Fluent API version(s) of the CreateTable method(s) to add or replace mappings created without using attributes --- src/SQLite.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/SQLite.cs b/src/SQLite.cs index 11a2d57d..8bc7a1a6 100644 --- a/src/SQLite.cs +++ b/src/SQLite.cs @@ -426,6 +426,23 @@ public TableMapping GetMapping (CreateFlags createFlags = CreateFlags.None) { return GetMapping (typeof (T), createFlags); } + + /// + /// Adds or replaces a table mapping in the collection. + /// + /// The table mapping to add or replace. + void UseMapping (TableMapping tableMapping) + { + var key = tableMapping.MappedType.FullName; + lock (_mappings) { + if (_mappings.ContainsKey(key)) { + _mappings[key] = tableMapping; + } + else { + _mappings.Add (key, tableMapping); + } + } + } private struct IndexedColumn { From b843b2f0b3808639a302ff49060ac8852cb52d7b Mon Sep 17 00:00:00 2001 From: Roy Goode Date: Mon, 2 Jul 2018 11:55:15 +0100 Subject: [PATCH 07/18] Added TableMappingBuilderExtensions This will be used for parsing properties from expressions in the Fluent API --- src/SQLite.cs | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/SQLite.cs b/src/SQLite.cs index 8bc7a1a6..703aee15 100644 --- a/src/SQLite.cs +++ b/src/SQLite.cs @@ -2412,6 +2412,50 @@ internal ColumnMappingFromAttributes (PropertyInfo prop, CreateFlags createFlags } } + 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; + } + } + class EnumCacheInfo { public EnumCacheInfo (Type type) From b6d099ce4b260297b2e9be9aa747e91f72ee64c7 Mon Sep 17 00:00:00 2001 From: Roy Goode Date: Mon, 2 Jul 2018 12:32:12 +0100 Subject: [PATCH 08/18] Added TableMappingBuilder class Allows building table mappings using a Fluent API --- src/SQLite.cs | 280 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 263 insertions(+), 17 deletions(-) diff --git a/src/SQLite.cs b/src/SQLite.cs index 703aee15..350f249a 100644 --- a/src/SQLite.cs +++ b/src/SQLite.cs @@ -2208,19 +2208,24 @@ public class TableMapping public bool WithoutRowId { get; protected set; } - public ColumnMapping[] Columns { get; protected set; } + public ColumnMapping[] Columns { get; internal set; } - public ColumnMapping PK { get; protected set; } + public ColumnMapping PK { get; internal set; } - public string GetByPrimaryKeySql { get; protected set; } + public string GetByPrimaryKeySql { get; internal set; } public CreateFlags CreateFlags { get; protected set; } protected ColumnMapping _autoPk; protected ColumnMapping[] _insertColumns; protected ColumnMapping[] _insertOrReplaceColumns; - - public bool HasAutoIncPK { get; protected set; } + + internal ColumnMapping AutoIncPK { + get { return _autoPk; } + set { _autoPk = value; } + } + + public bool HasAutoIncPK => _autoPk != null; public void SetAutoIncPK (object obj, long id) { @@ -2253,9 +2258,10 @@ public ColumnMapping FindColumn (string columnName) return exact; } - public TableMapping (Type type) + public TableMapping (Type type, string tableName = null) { MappedType = type; + TableName = tableName ?? type.Name; } } @@ -2313,8 +2319,6 @@ from p in ti.DeclaredProperties } } - HasAutoIncPK = _autoPk != null; - if (PK != null) { GetByPrimaryKeySql = string.Format ("select * from \"{0}\" where \"{1}\" = ?", TableName, PK.Name); } @@ -2332,28 +2336,28 @@ public class ColumnMapping { readonly PropertyInfo _prop; - public string Name { get; protected set; } + public string Name { get; internal set; } public PropertyInfo PropertyInfo => _prop; public string PropertyName { get { return _prop.Name; } } - public Type ColumnType { get; protected set; } + public Type ColumnType { get; internal set; } - public string Collation { get; protected set; } + public string Collation { get; internal set; } - public bool IsAutoInc { get; protected set; } - public bool IsAutoGuid { get; protected set; } + public bool IsAutoInc { get; internal set; } + public bool IsAutoGuid { get; internal set; } - public bool IsPK { get; protected set; } + public bool IsPK { get; internal set; } public IEnumerable Indices { get; set; } - public bool IsNullable { get; protected set; } + public bool IsNullable { get; internal set; } - public int? MaxStringLength { get; protected set; } + public int? MaxStringLength { get; internal set; } - public bool StoreAsText { get; protected set; } + public bool StoreAsText { get; internal set; } public ColumnMapping (PropertyInfo prop) { @@ -2455,6 +2459,248 @@ internal static void AddProperties (this List list, Expre return defaultValue; } } + + public class TableMappingBuilder + { + string _tableName; + PropertyInfo _primaryKey; + + 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); + + public TableMappingBuilder TableName(string name) + { + _tableName = name; + 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 ColumnIndex { 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 ToMapping () + { + var tableMapping = new TableMapping (MappedType, _tableName ?? MappedType.Name); + + 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 ColumnMapping (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.AutoIncPK = c; + } + + 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; + } + } class EnumCacheInfo { From 94b29decdf51225a73226352ae1a0521ebdccd53 Mon Sep 17 00:00:00 2001 From: Roy Goode Date: Mon, 2 Jul 2018 12:39:22 +0100 Subject: [PATCH 09/18] Added TableMapping.Build() method Call this method to create a new instance of TableMappingBuilder to use the Fluent API to build table mappings --- src/SQLite.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/SQLite.cs b/src/SQLite.cs index 350f249a..290793e6 100644 --- a/src/SQLite.cs +++ b/src/SQLite.cs @@ -2263,6 +2263,17 @@ public TableMapping (Type type, string tableName = null) MappedType = type; TableName = tableName ?? type.Name; } + + /// + /// Returns a TableMappingBuilder for constructing table mappings with a Fluent API. + /// Please note: the SQLite attributes on the type's properties will be ignored (by design) if this method is used. + /// + /// The entity type to build a table mapping for. + /// The table mapping builder. + public static TableMappingBuilder Build() + { + return new TableMappingBuilder(); + } } class TableMappingFromAttributes : TableMapping From aeb96a8a282a9a946c196ff932876d5048d07b99 Mon Sep 17 00:00:00 2001 From: Roy Goode Date: Mon, 2 Jul 2018 12:47:58 +0100 Subject: [PATCH 10/18] Added CreateTableFromMapping() This will be used internally to create tables in the database based on given table mappings --- src/SQLite.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/SQLite.cs b/src/SQLite.cs index 290793e6..428a82f5 100644 --- a/src/SQLite.cs +++ b/src/SQLite.cs @@ -507,9 +507,14 @@ public CreateTableResult CreateTable (Type ty, CreateFlags createFlags = CreateF { var map = GetMapping (ty, createFlags); + return CreateTableFromMapping (map, createFlags); + } + + CreateTableResult CreateTableFromMapping (TableMapping map, CreateFlags createFlags) + { // 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.FullName)); } // Check if the table exists @@ -518,7 +523,6 @@ public CreateTableResult CreateTable (Type ty, CreateFlags createFlags = CreateF // Create or migrate it if (existingCols.Count == 0) { - // Facilitate virtual tables a.k.a. full-text search. bool fts3 = (createFlags & CreateFlags.FullTextSearch3) != 0; bool fts4 = (createFlags & CreateFlags.FullTextSearch4) != 0; @@ -532,7 +536,7 @@ public CreateTableResult CreateTable (Type ty, CreateFlags createFlags = CreateF var decl = string.Join (",\n", decls.ToArray ()); query += decl; query += ")"; - if(map.WithoutRowId) { + if (map.WithoutRowId) { query += " without rowid"; } From fa9d10a2fcc95e8bb9839d21114ca395ef9e41ba Mon Sep 17 00:00:00 2001 From: Roy Goode Date: Mon, 2 Jul 2018 13:04:01 +0100 Subject: [PATCH 11/18] Added public CreateTable methods for the Fluent API CreateTable(), CreateTables(), CreateTableAsync(), CreateTablesAsync() Used for creating tables based on the provided table mapping, generated from TableMappingBuilder --- src/SQLite.cs | 32 ++++++++++++++++++++++++++++++++ src/SQLiteAsync.cs | 26 ++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/src/SQLite.cs b/src/SQLite.cs index 428a82f5..873fecda 100644 --- a/src/SQLite.cs +++ b/src/SQLite.cs @@ -580,6 +580,21 @@ CreateTableResult CreateTableFromMapping (TableMapping map, CreateFlags createFl return result; } + + /// + /// 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 CreateTableResult CreateTable (TableMapping map, CreateFlags createFlags = CreateFlags.None) + { + UseMapping (map); + return CreateTableFromMapping (map, createFlags); + } /// /// Executes a "create table if not exists" on the database for each type. It also @@ -670,6 +685,23 @@ public CreateTablesResult CreateTables (CreateFlags createFlags = CreateFlags.No 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) { + var aResult = CreateTable (mapping, createFlags); + result.Results[mapping.MappedType] = aResult; + } + return result; + } + /// /// Creates an index for the specified table and columns. /// diff --git a/src/SQLiteAsync.cs b/src/SQLiteAsync.cs index 1b2cf2b3..badb3c7d 100644 --- a/src/SQLiteAsync.cs +++ b/src/SQLiteAsync.cs @@ -278,6 +278,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 @@ -362,6 +376,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. /// From 2e9c7cbac9bcb632dbcaca0be94ac1c2afffb63f Mon Sep 17 00:00:00 2001 From: Roy Goode Date: Mon, 2 Jul 2018 13:14:39 +0100 Subject: [PATCH 12/18] Added TableMapping.From() method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit So you can use the new CreateTable(TableMapping…) methods with the original attribute-based implementation of table mappings if you really want to --- src/SQLite.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/SQLite.cs b/src/SQLite.cs index 873fecda..2c2f3ea3 100644 --- a/src/SQLite.cs +++ b/src/SQLite.cs @@ -2310,6 +2310,17 @@ public static TableMappingBuilder Build() { return new TableMappingBuilder(); } + + /// + /// Returns a TableMapping by retrieving the attributes of the given type using reflection. + /// + /// Optional flags allowing implicit PK and indexes based on naming conventions. + /// The type to reflect to create the table mapping. + /// The table mapping for the reflected type. + public static TableMapping From (CreateFlags createFlags = CreateFlags.None) + { + return new TableMappingFromAttributes (typeof(T), createFlags); + } } class TableMappingFromAttributes : TableMapping From dee0e599b1e6f0e90be87e8476fc2c21bcd6249b Mon Sep 17 00:00:00 2001 From: Roy Goode Date: Mon, 2 Jul 2018 13:51:17 +0100 Subject: [PATCH 13/18] Made UseMapping() public Because this method is useful for supplying table mappings for query results, if you really need to specify column names without decorating those classes with attributes --- src/SQLite.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SQLite.cs b/src/SQLite.cs index 2c2f3ea3..90b80ea4 100644 --- a/src/SQLite.cs +++ b/src/SQLite.cs @@ -431,7 +431,7 @@ public TableMapping GetMapping (CreateFlags createFlags = CreateFlags.None) /// Adds or replaces a table mapping in the collection. /// /// The table mapping to add or replace. - void UseMapping (TableMapping tableMapping) + public void UseMapping (TableMapping tableMapping) { var key = tableMapping.MappedType.FullName; lock (_mappings) { From 49095e209cba0206dbfc7f1b017a2b4224a015ab Mon Sep 17 00:00:00 2001 From: Roy Goode Date: Mon, 2 Jul 2018 14:25:35 +0100 Subject: [PATCH 14/18] Added TableMapping-friendly query overloads Useful for querying the database with strongly-typed results for those that use the Fluent API to create table mappings --- src/SQLite.cs | 172 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 168 insertions(+), 4 deletions(-) diff --git a/src/SQLite.cs b/src/SQLite.cs index 90b80ea4..5650126d 100644 --- a/src/SQLite.cs +++ b/src/SQLite.cs @@ -1019,6 +1019,30 @@ public List Query (TableMapping map, string query, params object[] args) var cmd = CreateCommand (query, args); return cmd.ExecuteQuery (map); } + + /// + /// Creates a SQLiteCommand given the command text (SQL) with arguments. Place a '?' + /// in the command text for each of the arguments and then executes that command. + /// It returns each row of the result using the specified mapping. + /// + /// + /// A to use to convert the resulting rows + /// into objects. + /// + /// + /// The fully escaped SQL. + /// + /// + /// Arguments to substitute for the occurences of '?' in the query. + /// + /// + /// An enumerable with one result for each row returned by the query. + /// + public List Query (TableMapping map, string query, params object[] args) + { + var cmd = CreateCommand (query, args); + return cmd.ExecuteQuery (map); + } /// /// Creates a SQLiteCommand given the command text (SQL) with arguments. Place a '?' @@ -1048,6 +1072,33 @@ public IEnumerable DeferredQuery (TableMapping map, string query, params var cmd = CreateCommand (query, args); return cmd.ExecuteDeferredQuery (map); } + + /// + /// Creates a SQLiteCommand given the command text (SQL) with arguments. Place a '?' + /// in the command text for each of the arguments and then executes that command. + /// It returns each row of the result using the specified mapping. + /// + /// + /// A to use to convert the resulting rows + /// into objects. + /// + /// + /// The fully escaped SQL. + /// + /// + /// Arguments to substitute for the occurences of '?' in the query. + /// + /// + /// An enumerable with one result for each row returned by the query. + /// The enumerator (retrieved by calling GetEnumerator() on the result of this method) + /// will call sqlite3_step on each call to MoveNext, so the database + /// connection must remain open for the lifetime of the enumerator. + /// + public IEnumerable DeferredQuery (TableMapping map, string query, params object[] args) + { + var cmd = CreateCommand (query, args); + return cmd.ExecuteDeferredQuery (map); + } /// /// Returns a queryable interface to the table represented by the given type. @@ -1060,11 +1111,24 @@ public IEnumerable DeferredQuery (TableMapping map, string query, params { return new TableQuery (this); } + + /// + /// Returns a queryable interface to the table represented by the given type. + /// + /// The table mapping to use. + /// + /// A queryable object that is able to translate Where, OrderBy, and Take + /// queries into native SQL. + /// + public TableQuery Table (TableMapping map) where T : new() + { + return new TableQuery (this, map); + } /// /// Attempts to retrieve an object with the given primary key from the table /// associated with the specified type. Use of this method requires that - /// the given type have a designated PrimaryKey (using the PrimaryKeyAttribute). + /// the given type have a designated PrimaryKey. /// /// /// The primary key. @@ -1082,7 +1146,7 @@ public IEnumerable DeferredQuery (TableMapping map, string query, params /// /// Attempts to retrieve an object with the given primary key from the table /// associated with the specified type. Use of this method requires that - /// the given type have a designated PrimaryKey (using the PrimaryKeyAttribute). + /// the given type have a designated PrimaryKey. /// /// /// The primary key. @@ -1098,6 +1162,26 @@ public object Get (object pk, TableMapping map) { return Query (map, map.GetByPrimaryKeySql, pk).First (); } + + /// + /// Attempts to retrieve an object with the given primary key from the table + /// associated with the specified type. Use of this method requires that + /// the given type have a designated PrimaryKey. + /// + /// + /// The TableMapping used to identify the table. + /// + /// + /// The primary key. + /// + /// + /// The object with the given primary key. Throws a not found exception + /// if the object is not found. + /// + public T Get (TableMapping map, object pk) where T : new() + { + return Query (map, map.GetByPrimaryKeySql, pk).First (); + } /// /// Attempts to retrieve the first object that matches the predicate from the table @@ -1114,11 +1198,30 @@ public object Get (object pk, TableMapping map) { return Table ().Where (predicate).First (); } + + /// + /// Attempts to retrieve the first object that matches the predicate from the table + /// associated with the specified type. + /// + /// + /// The TableMapping used to identify the table. + /// + /// + /// A predicate for which object to find. + /// + /// + /// The object that matches the given predicate. Throws a not found exception + /// if the object is not found. + /// + public T Get (TableMapping map, Expression> predicate) where T : new() + { + return Table (map).Where (predicate).First (); + } /// /// Attempts to retrieve an object with the given primary key from the table /// associated with the specified type. Use of this method requires that - /// the given type have a designated PrimaryKey (using the PrimaryKeyAttribute). + /// the given type have a designated PrimaryKey. /// /// /// The primary key. @@ -1132,11 +1235,31 @@ public object Get (object pk, TableMapping map) var map = GetMapping (typeof (T)); return Query (map.GetByPrimaryKeySql, pk).FirstOrDefault (); } + + /// + /// Attempts to retrieve an object with the given primary key from the table + /// associated with the specified type. Use of this method requires that + /// the given type have a designated PrimaryKey. + /// + /// + /// The TableMapping used to identify the table. + /// + /// + /// The primary key. + /// + /// + /// The object with the given primary key or null + /// if the object is not found. + /// + public T Find (TableMapping map, object pk) where T : new() + { + return Query (map, map.GetByPrimaryKeySql, pk).FirstOrDefault (); + } /// /// Attempts to retrieve an object with the given primary key from the table /// associated with the specified type. Use of this method requires that - /// the given type have a designated PrimaryKey (using the PrimaryKeyAttribute). + /// the given type have a designated PrimaryKey. /// /// /// The primary key. @@ -1168,6 +1291,25 @@ public object Find (object pk, TableMapping map) { return Table ().Where (predicate).FirstOrDefault (); } + + /// + /// Attempts to retrieve the first object that matches the predicate from the table + /// associated with the specified type. + /// + /// + /// The TableMapping used to identify the table. + /// + /// + /// A predicate for which object to find. + /// + /// + /// The object that matches the given predicate or null + /// if the object is not found. + /// + public T Find (TableMapping map, Expression> predicate) where T : new() + { + return Table (map).Where (predicate).FirstOrDefault (); + } /// /// Attempts to retrieve the first object that matches the query from the table @@ -1187,6 +1329,28 @@ public object Find (object pk, TableMapping map) { return Query (query, args).FirstOrDefault (); } + + /// + /// Attempts to retrieve the first object that matches the query from the table + /// associated with the specified type. + /// + /// + /// The TableMapping used to identify the table. + /// + /// + /// The fully escaped SQL. + /// + /// + /// Arguments to substitute for the occurences of '?' in the query. + /// + /// + /// The object that matches the given predicate or null + /// if the object is not found. + /// + public T FindWithQuery (TableMapping map, string query, params object[] args) where T : new() + { + return Query (map, query, args).FirstOrDefault (); + } /// /// Attempts to retrieve the first object that matches the query from the table From e60092dcd37624533aa6060618e24515a09cbbf5 Mon Sep 17 00:00:00 2001 From: Roy Goode Date: Mon, 2 Jul 2018 14:34:07 +0100 Subject: [PATCH 15/18] Added support for WithoutRowId() in TableMappingBuilder This implementation was missing from the Fluent API but was available via TableAttribute --- src/SQLite.cs | 146 +++++++++++++++++++++++++++----------------------- 1 file changed, 79 insertions(+), 67 deletions(-) diff --git a/src/SQLite.cs b/src/SQLite.cs index 5650126d..3b54c5aa 100644 --- a/src/SQLite.cs +++ b/src/SQLite.cs @@ -2406,7 +2406,7 @@ public class TableMapping public string TableName { get; protected set; } - public bool WithoutRowId { get; protected set; } + public bool WithoutRowId { get; internal set; } public ColumnMapping[] Columns { get; internal set; } @@ -2681,166 +2681,176 @@ internal static void AddProperties (this List list, Expre return defaultValue; } } - + public class TableMappingBuilder { string _tableName; PropertyInfo _primaryKey; + bool _withoutRowId; - readonly List _ignore = new List(); - readonly List _autoInc = new List(); - readonly List _notNull = new List(); - readonly List _storeAsText = new List(); + 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>(); + 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); - public TableMappingBuilder TableName(string name) + public TableMappingBuilder TableName (string name) { _tableName = name; return this; } - public TableMappingBuilder ColumnName(Expression> property, string name) + public TableMappingBuilder WithoutRowId (bool value = true) { - _columnNames.AddPropertyValue(property, name); + _withoutRowId = value; return this; } - public TableMappingBuilder MaxLength(Expression> property, int maxLength) + public TableMappingBuilder ColumnName (Expression> property, string name) { - _maxLengths.AddPropertyValue(property, maxLength); + _columnNames.AddPropertyValue (property, name); return this; } - public TableMappingBuilder Collation(Expression> property, string collation) + public TableMappingBuilder MaxLength (Expression> property, int maxLength) { - _collations.AddPropertyValue(property, collation); + _maxLengths.AddPropertyValue (property, maxLength); return this; } - public TableMappingBuilder Index(Expression> property, bool unique = false, string indexName = null, int order = 0) + public TableMappingBuilder Collation (Expression> property, string collation) { - var prop = property.AsPropertyInfo(); - if (!_indices.ContainsKey(prop)) - { - _indices[prop] = new List(); + _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 ColumnIndex { Name = indexName, Order = order, Unique = unique }); + _indices[prop].Add (new ColumnIndex { + Name = indexName, + Order = order, + Unique = unique + }); return this; } - public TableMappingBuilder Index(string indexName, Expression> property, bool unique = false, int order = 0) + public TableMappingBuilder Index (string indexName, Expression> property, bool unique = false, int order = 0) { - return Index(property, unique, indexName, order); + return Index (property, unique, indexName, order); } - public TableMappingBuilder Unique(Expression> property, string indexName = null, int order = 0) + public TableMappingBuilder Unique (Expression> property, string indexName = null, int order = 0) { - return Index(property, true, indexName, order); + return Index (property, true, indexName, order); } - public TableMappingBuilder Unique(string indexName, Expression> property, int order = 0) + public TableMappingBuilder Unique (string indexName, Expression> property, int order = 0) { - return Index(property, true, indexName, order); + return Index (property, true, indexName, order); } - public TableMappingBuilder Index(params Expression>[] properties) + public TableMappingBuilder Index (params Expression>[] properties) { - for (int i = 0; i < properties.Length; i++) - { - Index(properties[i], false, null, i); + for (int i = 0; i < properties.Length; i++) { + Index (properties[i], false, null, i); } + return this; } - public TableMappingBuilder Index(string indexName, params Expression>[] properties) + public TableMappingBuilder Index (string indexName, params Expression>[] properties) { - for (int i = 0; i < properties.Length; i++) - { - Index(properties[i], false, indexName, i); + for (int i = 0; i < properties.Length; i++) { + Index (properties[i], false, indexName, i); } + return this; } - public TableMappingBuilder Unique(params Expression>[] properties) + public TableMappingBuilder Unique (params Expression>[] properties) { - for (int i = 0; i < properties.Length; i++) - { - Index(properties[i], true, null, i); + for (int i = 0; i < properties.Length; i++) { + Index (properties[i], true, null, i); } + return this; } - public TableMappingBuilder Unique(string indexName, params Expression>[] properties) + public TableMappingBuilder Unique (string indexName, params Expression>[] properties) { - for (int i = 0; i < properties.Length; i++) - { - Index(properties[i], true, indexName, i); + for (int i = 0; i < properties.Length; i++) { + Index (properties[i], true, indexName, i); } + return this; } - public TableMappingBuilder PrimaryKey(Expression> property, bool autoIncrement = false) + public TableMappingBuilder PrimaryKey (Expression> property, bool autoIncrement = false) { - _primaryKey = property.AsPropertyInfo(); - if (autoIncrement) - { - _autoInc.Add(_primaryKey); + _primaryKey = property.AsPropertyInfo (); + if (autoIncrement) { + _autoInc.Add (_primaryKey); } + return this; } - public TableMappingBuilder Ignore(Expression> property) + public TableMappingBuilder Ignore (Expression> property) { - _ignore.AddProperty(property); + _ignore.AddProperty (property); return this; } - public TableMappingBuilder Ignore(params Expression>[] properties) + public TableMappingBuilder Ignore (params Expression>[] properties) { - _ignore.AddProperties(properties); + _ignore.AddProperties (properties); return this; } - public TableMappingBuilder AutoIncrement(Expression> property) + public TableMappingBuilder AutoIncrement (Expression> property) { - _autoInc.AddProperty(property); + _autoInc.AddProperty (property); return this; } - public TableMappingBuilder AutoIncrement(params Expression>[] properties) + public TableMappingBuilder AutoIncrement (params Expression>[] properties) { - _autoInc.AddProperties(properties); + _autoInc.AddProperties (properties); return this; } - public TableMappingBuilder NotNull(Expression> property) + public TableMappingBuilder NotNull (Expression> property) { - _notNull.AddProperty(property); + _notNull.AddProperty (property); return this; } - public TableMappingBuilder NotNull(params Expression>[] properties) + public TableMappingBuilder NotNull (params Expression>[] properties) { - _notNull.AddProperties(properties); + _notNull.AddProperties (properties); return this; } - public TableMappingBuilder StoreAsText(Expression> property) + public TableMappingBuilder StoreAsText (Expression> property) { - _storeAsText.AddProperty(property); + _storeAsText.AddProperty (property); return this; } - public TableMappingBuilder StoreAsText(params Expression>[] properties) + public TableMappingBuilder StoreAsText (params Expression>[] properties) { - _storeAsText.AddProperties(properties); + _storeAsText.AddProperties (properties); return this; } @@ -2850,7 +2860,9 @@ public TableMappingBuilder StoreAsText(params Expression>[] p /// The table mapping as created by the builder. public TableMapping ToMapping () { - var tableMapping = new TableMapping (MappedType, _tableName ?? MappedType.Name); + var tableMapping = new TableMapping (MappedType, _tableName ?? MappedType.Name) { + WithoutRowId = _withoutRowId + }; var props = new List (); var baseType = MappedType; From 91b03d63db8764a4abb36782479028fb5653d532 Mon Sep 17 00:00:00 2001 From: Roy Goode Date: Mon, 2 Jul 2018 14:34:51 +0100 Subject: [PATCH 16/18] Tweak for TableMapping-friendly TableQuery TableQuery constructor that accepts TableMapping --- src/SQLite.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SQLite.cs b/src/SQLite.cs index 3b54c5aa..0e438a63 100644 --- a/src/SQLite.cs +++ b/src/SQLite.cs @@ -3643,12 +3643,12 @@ public class TableQuery : BaseTableQuery, IEnumerable Expression _selector; - TableQuery (SQLiteConnection conn, TableMapping table) + public TableQuery (SQLiteConnection conn, TableMapping table) { Connection = conn; Table = table; } - + public TableQuery (SQLiteConnection conn) { Connection = conn; From 0e61fbe7dba11a5a2650a493f09428c6583e8620 Mon Sep 17 00:00:00 2001 From: Roy Goode Date: Mon, 2 Jul 2018 14:47:01 +0100 Subject: [PATCH 17/18] Added Fluent API CreateTable tests Based on previous CreateTable tests --- tests/CreateTableFluentTest.cs | 232 +++++++++++++++++++++++++++++++++ tests/SQLite.Tests.csproj | 1 + 2 files changed, 233 insertions(+) create mode 100644 tests/CreateTableFluentTest.cs diff --git a/tests/CreateTableFluentTest.cs b/tests/CreateTableFluentTest.cs new file mode 100644 index 00000000..2baae89e --- /dev/null +++ b/tests/CreateTableFluentTest.cs @@ -0,0 +1,232 @@ +using System; +using System.Linq; + +#if NETFX_CORE +using Microsoft.VisualStudio.TestPlatform.UnitTestFramework; +using SetUp = Microsoft.VisualStudio.TestPlatform.UnitTestFramework.TestInitializeAttribute; +using TestFixture = Microsoft.VisualStudio.TestPlatform.UnitTestFramework.TestClassAttribute; +using Test = Microsoft.VisualStudio.TestPlatform.UnitTestFramework.TestMethodAttribute; +#else +using NUnit.Framework; +#endif + + +namespace SQLite.Tests +{ + [TestFixture] + public class CreateTableFluentTest + { + class NoPropObject + { + } + + [Test, ExpectedException] + public void CreateTypeWithNoProps () + { + var db = new TestDb (); + + var mapping = TableMapping.Build ().ToMapping (); + + db.CreateTable (mapping); + } + + class DbSchema + { + public TableMapping Products { get; } + public TableMapping Orders { get; } + public TableMapping OrderLines { get; } + public TableMapping OrderHistory { get; } + + public DbSchema () + { + Products = TableMapping.Build () + .TableName("Product") + .PrimaryKey (x => x.Id, autoIncrement: true) + .ToMapping (); + + Orders = TableMapping.Build () + .TableName("Order") + .PrimaryKey (x => x.Id, autoIncrement: true) + .ToMapping (); + + OrderLines = TableMapping.Build () + .TableName("OrderLine") + .PrimaryKey (x => x.Id, autoIncrement: true) + .Index ("IX_OrderProduct", x => x.OrderId, x => x.ProductId) + .ToMapping (); + + OrderHistory = TableMapping.Build () + .TableName("OrderHistory") + .PrimaryKey (x => x.Id, autoIncrement: true) + .ToMapping (); + } + + public TableMapping[] Tables => new[] {Products, Orders, OrderLines, OrderHistory}; + } + + [Test] + public void CreateThem () + { + var db = new TestDb (); + var schema = new DbSchema (); + + db.CreateTables (CreateFlags.None, schema.Tables); + + VerifyCreations (db); + } + + [Test] + public void CreateTwice () + { + var db = new TestDb (); + + var product = TableMapping.Build () + .TableName("Product") + .PrimaryKey (x => x.Id, autoIncrement: true) + .ToMapping (); + + var order = TableMapping.Build() + .TableName("Order") + .PrimaryKey (x => x.Id, autoIncrement: true) + .ToMapping (); + + var orderLine = TableMapping.Build () + .TableName("OrderLine") + .PrimaryKey (x => x.Id, autoIncrement: true) + .Index ("IX_OrderProduct", x => x.OrderId, x => x.ProductId) + .ToMapping (); + + var orderHistory = TableMapping.Build() + .TableName("OrderHistory") + .PrimaryKey (x => x.Id, autoIncrement: true) + .ToMapping (); + + db.CreateTable (product); + db.CreateTable (order); + db.CreateTable (orderLine); + db.CreateTable (orderHistory); + + VerifyCreations(db); + } + + private static void VerifyCreations(TestDb db) + { + var orderLine = db.GetMapping(typeof(OrderLinePoco)); + Assert.AreEqual(6, orderLine.Columns.Length); + + var l = new OrderLine() + { + Status = OrderLineStatus.Shipped + }; + db.Insert(l); + var lo = db.Table().First(x => x.Status == OrderLineStatus.Shipped); + Assert.AreEqual(lo.Id, l.Id); + } + + class Issue115_MyObject + { + public string UniqueId { get; set; } + public byte OtherValue { get; set; } + } + + [Test] + public void Issue115_MissingPrimaryKey () + { + using (var conn = new TestDb ()) { + var mapping = TableMapping.Build () + .PrimaryKey (x => x.UniqueId) + .ToMapping (); + conn.CreateTable (mapping); + conn.InsertAll (from i in Enumerable.Range (0, 10) + select new Issue115_MyObject { + UniqueId = i.ToString (), + OtherValue = (byte)(i * 10), + }); + + var query = conn.Table (mapping); + foreach (var itm in query) { + itm.OtherValue++; + Assert.AreEqual (1, conn.Update (itm, typeof(Issue115_MyObject))); + } + } + } + + class WantsNoRowId + { + public int Id { get; set; } + public string Name { get; set; } + } + + class SqliteMaster + { + public string Type { get; set; } + public string Name { get; set; } + public string TableName { get; set; } + public int RootPage { get; set; } + public string Sql { get; set; } + } + + [Test] + public void WithoutRowId () + { + using (var conn = new TestDb ()) { + var master = TableMapping.Build () + .TableName("sqlite_master") + .ColumnName (x => x.Type, "type") + .ColumnName (x => x.Name, "name") + .ColumnName (x => x.TableName, "tbl_name") + .ColumnName (x => x.RootPage, "rootpage") + .ColumnName (x => x.Sql, "sql") + .ToMapping (); + + var wantsNoRowId = TableMapping.Build () + .PrimaryKey (x => x.Id) + .WithoutRowId () + .ToMapping (); + + var orderLine = TableMapping.Build () + .TableName("OrderLine") + .PrimaryKey (x => x.Id, autoIncrement: true) + .Index ("IX_OrderProduct", x => x.OrderId, x => x.ProductId) + .ToMapping (); + + conn.CreateTable (orderLine); + var info = conn.Table (master).Where (m => m.TableName == "OrderLine").First (); + Assert.That (!info.Sql.Contains ("without rowid")); + + conn.CreateTable (wantsNoRowId); + info = conn.Table (master).Where (m => m.TableName == "WantsNoRowId").First (); + Assert.That (info.Sql.Contains ("without rowid")); + } + } + } + + public class ProductPoco + { + public int Id { get; set; } + public string Name { get; set; } + public decimal Price { get; set; } + + public uint TotalSales { get; set; } + } + public class OrderPoco + { + public int Id { get; set; } + public DateTime PlacedTime { get; set; } + } + public class OrderHistoryPoco { + public int Id { get; set; } + public int OrderId { get; set; } + public DateTime Time { get; set; } + public string Comment { get; set; } + } + public class OrderLinePoco + { + public int Id { get; set; } + public int OrderId { get; set; } + public int ProductId { get; set; } + public int Quantity { get; set; } + public decimal UnitPrice { get; set; } + public OrderLineStatus Status { get; set; } + } +} diff --git a/tests/SQLite.Tests.csproj b/tests/SQLite.Tests.csproj index 674f1cbe..a664dc99 100644 --- a/tests/SQLite.Tests.csproj +++ b/tests/SQLite.Tests.csproj @@ -44,6 +44,7 @@ + From f29756507de2868029b6635360b1e835ed9e74fb Mon Sep 17 00:00:00 2001 From: Roy Goode Date: Mon, 2 Jul 2018 15:25:49 +0100 Subject: [PATCH 18/18] Standardized InsertColumns / InsertOrReplaceColumns Between Fluent API and Attribute API (Fixes bug that failed new unit tests for Fluent API) --- src/SQLite.cs | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/src/SQLite.cs b/src/SQLite.cs index 0e438a63..36035394 100644 --- a/src/SQLite.cs +++ b/src/SQLite.cs @@ -2417,8 +2417,6 @@ public class TableMapping public CreateFlags CreateFlags { get; protected set; } protected ColumnMapping _autoPk; - protected ColumnMapping[] _insertColumns; - protected ColumnMapping[] _insertOrReplaceColumns; internal ColumnMapping AutoIncPK { get { return _autoPk; } @@ -2433,18 +2431,9 @@ public void SetAutoIncPK (object obj, long id) _autoPk.SetValue (obj, Convert.ChangeType (id, _autoPk.ColumnType, null)); } } - - public ColumnMapping[] InsertColumns { - get { - return _insertColumns; - } - } - - public ColumnMapping[] InsertOrReplaceColumns { - get { - return _insertOrReplaceColumns; - } - } + + public ColumnMapping[] InsertColumns => Columns.Where (c => !c.IsAutoInc).ToArray (); + public ColumnMapping[] InsertOrReplaceColumns => Columns.ToArray (); public ColumnMapping FindColumnWithPropertyName (string propertyName) { @@ -2548,9 +2537,6 @@ from p in ti.DeclaredProperties // 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 (); } }