diff --git a/src/SQLite.cs b/src/SQLite.cs index 4e77fb4e..9c4bf948 100644 --- a/src/SQLite.cs +++ b/src/SQLite.cs @@ -49,12 +49,16 @@ using Sqlite3BackupHandle = SQLitePCL.sqlite3_backup; using Sqlite3Statement = SQLitePCL.sqlite3_stmt; using Sqlite3 = SQLitePCL.raw; +using System.Data; + + + + #else using Sqlite3DatabaseHandle = System.IntPtr; using Sqlite3BackupHandle = System.IntPtr; using Sqlite3Statement = System.IntPtr; #endif - #pragma warning disable 1591 // XML Doc Comments namespace SQLite @@ -76,7 +80,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) @@ -556,12 +560,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); } } @@ -583,6 +587,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. + public static 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 { public int Order; @@ -645,10 +666,14 @@ 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); + } + 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 @@ -716,6 +741,21 @@ public CreateTableResult CreateTable (Type ty, CreateFlags createFlags = CreateF 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 /// creates any specified indexes on the columns of the table. It uses @@ -805,6 +845,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. /// @@ -923,7 +980,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; @@ -2447,8 +2504,15 @@ public class AutoIncrementAttribute : Attribute { } + public interface IColumnIndex + { + string Name { get; set; } + int Order { get; set; } + bool Unique { get; set; } + } + [AttributeUsage (AttributeTargets.Property)] - public class IndexedAttribute : Attribute + public class IndexedAttribute : Attribute, IColumnIndex { public string Name { get; set; } public int Order { get; set; } @@ -2465,6 +2529,13 @@ public IndexedAttribute (string name, int order) } } + public class ColumnIndex : IColumnIndex + { + public string Name { get; set; } + public int Order { get; set; } + public bool Unique { get; set; } + } + [AttributeUsage (AttributeTargets.Property)] public class IgnoreAttribute : Attribute { @@ -2526,27 +2597,201 @@ public class TableMapping { public Type MappedType { get; private set; } - public string TableName { get; private set; } + public string TableName { get; protected set; } - public bool WithoutRowId { get; private set; } + public bool WithoutRowId { get; protected internal set; } - public Column[] Columns { get; private set; } + public ColumnMapping[] Columns { get; protected internal set; } - public Column PK { get; private set; } + public ColumnMapping PK { get; protected internal set; } - public string GetByPrimaryKeySql { get; private set; } + public string GetByPrimaryKeySql { get; protected internal set; } - public CreateFlags CreateFlags { get; private set; } + public CreateFlags CreateFlags { get; protected set; } - internal MapMethod Method { get; private set; } = MapMethod.ByName; + internal MapMethod Method { get; set; } = MapMethod.ByName; - readonly Column _autoPk; - readonly Column[] _insertColumns; - readonly Column[] _insertOrReplaceColumns; + internal ColumnMapping AutoIncPk { get; set; } - public TableMapping (Type type, CreateFlags createFlags = CreateFlags.None) + public ColumnMapping[] InsertColumns => Columns.Where (c => !c.IsAutoInc).ToArray (); + public ColumnMapping[] InsertOrReplaceColumns => Columns.ToArray (); + + public TableMapping (Type type, string tableName = null) { MappedType = type; + TableName = tableName ?? type.Name; + } + + + public bool HasAutoIncPK => AutoIncPk != null; + + public void SetAutoIncPK (object obj, long id) + { + if (AutoIncPk != null) { + AutoIncPk.SetValue (obj, Convert.ChangeType (id, AutoIncPk.ColumnType, null)); + } + } + + public ColumnMapping FindColumnWithPropertyName (string propertyName) + { + var exact = Columns.FirstOrDefault (c => c.PropertyName == propertyName); + return exact; + } + + public ColumnMapping FindColumn (string columnName) + { + if(Method != MapMethod.ByName) + throw new InvalidOperationException($"This {nameof(TableMapping)} is not mapped by name, but {Method}."); + + var exact = Columns.FirstOrDefault (c => c.Name.ToLower () == columnName.ToLower ()); + return exact; + } + +// public class Column +// { +// MemberInfo _member; + +// public string Name { get; private set; } + +// public PropertyInfo PropertyInfo => _member as PropertyInfo; + +// public string PropertyName { get { return _member.Name; } } + +// public Type ColumnType { get; private set; } + +// public string Collation { get; private set; } + +// public bool IsAutoInc { get; private set; } +// public bool IsAutoGuid { get; private set; } + +// public bool IsPK { get; private set; } + +// public IEnumerable Indices { get; set; } + +// public bool IsNullable { get; private set; } + +// public int? MaxStringLength { get; private set; } + +// public bool StoreAsText { get; private set; } + +// public Column (MemberInfo member, CreateFlags createFlags = CreateFlags.None) +// { +// _member = member; +// var memberType = GetMemberType(member); + +// var colAttr = member.CustomAttributes.FirstOrDefault (x => x.AttributeType == typeof (ColumnAttribute)); +//#if ENABLE_IL2CPP +// var ca = member.GetCustomAttribute(typeof(ColumnAttribute)) as ColumnAttribute; +// Name = ca == null ? member.Name : ca.Name; +//#else +// Name = (colAttr != null && colAttr.ConstructorArguments.Count > 0) ? +// colAttr.ConstructorArguments[0].Value?.ToString () : +// member.Name; +//#endif +// //If this type is Nullable then Nullable.GetUnderlyingType returns the T, otherwise it returns null, so get the actual type instead +// ColumnType = Nullable.GetUnderlyingType (memberType) ?? memberType; +// Collation = Orm.Collation (member); + +// IsPK = Orm.IsPK (member) || +// (((createFlags & CreateFlags.ImplicitPK) == CreateFlags.ImplicitPK) && +// string.Compare (member.Name, Orm.ImplicitPkName, StringComparison.OrdinalIgnoreCase) == 0); + +// var isAuto = Orm.IsAutoInc (member) || (IsPK && ((createFlags & CreateFlags.AutoIncPK) == CreateFlags.AutoIncPK)); +// IsAutoGuid = isAuto && ColumnType == typeof (Guid); +// IsAutoInc = isAuto && !IsAutoGuid; + +// Indices = Orm.GetIndices (member); +// if (!Indices.Any () +// && !IsPK +// && ((createFlags & CreateFlags.ImplicitIndex) == CreateFlags.ImplicitIndex) +// && Name.EndsWith (Orm.ImplicitIndexSuffix, StringComparison.OrdinalIgnoreCase) +// ) { +// Indices = new IndexedAttribute[] { new IndexedAttribute () }; +// } +// IsNullable = !(IsPK || Orm.IsMarkedNotNull (member)); +// MaxStringLength = Orm.MaxStringLength (member); + +// StoreAsText = memberType.GetTypeInfo ().CustomAttributes.Any (x => x.AttributeType == typeof (StoreAsTextAttribute)); +// } + +// public Column (PropertyInfo member, CreateFlags createFlags = CreateFlags.None) +// : this((MemberInfo)member, createFlags) +// { } + +// public void SetValue (object obj, object val) +// { +// if(_member is PropertyInfo propy) +// { +// if (val != null && ColumnType.GetTypeInfo ().IsEnum) +// propy.SetValue (obj, Enum.ToObject (ColumnType, val)); +// else +// propy.SetValue (obj, val); +// } +// else if(_member is FieldInfo field) +// { +// if (val != null && ColumnType.GetTypeInfo ().IsEnum) +// field.SetValue (obj, Enum.ToObject (ColumnType, val)); +// else +// field.SetValue (obj, val); +// } +// else +// throw new InvalidProgramException("unreachable condition"); +// } + +// public object GetValue (object obj) +// { +// if(_member is PropertyInfo propy) +// return propy.GetValue(obj); +// else if(_member is FieldInfo field) +// return field.GetValue(obj); +// else +// throw new InvalidProgramException("unreachable condition"); +// } + +// private static Type GetMemberType(MemberInfo m) +// { +// switch(m.MemberType) +// { +// case MemberTypes.Property: return ((PropertyInfo)m).PropertyType; +// case MemberTypes.Field: return ((FieldInfo)m).FieldType; +// default: throw new InvalidProgramException($"{nameof(TableMapping)} supports properties or fields only."); +// } +// } +// } + + internal enum MapMethod + { + ByName, + ByPosition + } + + /// + /// 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 (); + } + + /// + /// 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 + { + internal TableMappingFromAttributes (Type type, CreateFlags createFlags = CreateFlags.None) : base (type) + { CreateFlags = createFlags; var typeInfo = type.GetTypeInfo (); @@ -2563,26 +2808,26 @@ public TableMapping (Type type, CreateFlags createFlags = CreateFlags.None) TableName = (tableAttr != null && !string.IsNullOrEmpty (tableAttr.Name)) ? tableAttr.Name : MappedType.Name; WithoutRowId = tableAttr != null ? tableAttr.WithoutRowId : false; - var members = GetPublicMembers(type); - var cols = new List(members.Count); - foreach(var m in members) - { - var ignore = m.IsDefined(typeof(IgnoreAttribute), true); - if(!ignore) - cols.Add(new Column(m, createFlags)); + var (members, mappingMethod) = GetPublicMembers (type); + Method = mappingMethod; + + var cols = new List (members.Count); + foreach (var m in members) { + var ignore = m.IsDefined (typeof (IgnoreAttribute), true); + if (!ignore) + cols.Add (new ColumnMappingFromAttributes (m, createFlags)); } Columns = cols.ToArray (); foreach (var c in Columns) { if (c.IsAutoInc && c.IsPK) { - _autoPk = c; + AutoIncPk = c; } + if (c.IsPK) { PK = c; } } - HasAutoIncPK = _autoPk != null; - if (PK != null) { GetByPrimaryKeySql = string.Format ("select * from \"{0}\" where \"{1}\" = ?", TableName, PK.Name); } @@ -2590,208 +2835,579 @@ 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) + protected internal static (IReadOnlyCollection, MapMethod) GetPublicMembers (Type type) { - if(type.Name.StartsWith("ValueTuple`")) - return GetFieldsFromValueTuple(type); + if (type.BaseType == typeof(ValueType)) + return GetFieldsFromValueTuple (type); - var members = new List(); - var memberNames = new HashSet(); - var newMembers = new List(); - do - { - var ti = type.GetTypeInfo(); - newMembers.Clear(); + var members = new List (); + var memberNames = new HashSet (); + var newMembers = new List (); + do { + var ti = type.GetTypeInfo (); + newMembers.Clear (); - newMembers.AddRange( + newMembers.AddRange ( from p in ti.DeclaredProperties - where !memberNames.Contains(p.Name) && + where !memberNames.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); - members.AddRange(newMembers); - foreach(var m in newMembers) - memberNames.Add(m.Name); + members.AddRange (newMembers); + foreach (var m in newMembers) + memberNames.Add (m.Name); type = ti.BaseType; } - while(type != typeof(object)); + while (type != typeof (object)); - return members; + return (members, MapMethod.ByName); } - private IReadOnlyCollection GetFieldsFromValueTuple(Type type) + protected internal static (IReadOnlyCollection, MapMethod) GetFieldsFromValueTuple (Type type) { - Method = MapMethod.ByPosition; - var fields = type.GetFields(); + var fields = type.GetFields (); // https://docs.microsoft.com/en-us/dotnet/api/system.valuetuple-8.rest - if(fields.Length >= 8) - throw new NotSupportedException("ValueTuple with more than 7 members not supported due to nesting; see https://docs.microsoft.com/en-us/dotnet/api/system.valuetuple-8.rest"); + if (fields.Length >= 8) + throw new NotSupportedException ("ValueTuple with more than 7 members not supported due to nesting; see https://docs.microsoft.com/en-us/dotnet/api/system.valuetuple-8.rest"); - return fields; + return (fields, MapMethod.ByPosition); } + } - public bool HasAutoIncPK { get; private set; } + public class ColumnMapping + { + readonly FieldInfo _field; + readonly PropertyInfo _prop; - public void SetAutoIncPK (object obj, long id) + public string Name { get; internal set; } + + public PropertyInfo PropertyInfo => _prop; + public FieldInfo FieldInfo => _field; + + public string PropertyName { get { return _prop?.Name ?? _field?.Name; } } + + public Type ColumnType { get; internal set; } + + public string Collation { get; internal set; } + + public bool IsAutoInc { get; internal set; } + public bool IsAutoGuid { get; internal set; } + + public bool IsPK { get; internal set; } + + public IEnumerable Indices { get; set; } + + public bool IsNullable { get; internal set; } + + public int? MaxStringLength { get; internal set; } + + public bool StoreAsText { get; internal set; } + + public ColumnMapping (MemberInfo prop) { - if (_autoPk != null) { - _autoPk.SetValue (obj, Convert.ChangeType (id, _autoPk.ColumnType, null)); + _prop = prop as PropertyInfo; + _field = prop as FieldInfo; + } + + public void SetValue (object obj, object val) + { + if (val != null && ColumnType.GetTypeInfo ().IsEnum) { + _prop?.SetValue (obj, Enum.ToObject (ColumnType, val)); + _field?.SetValue (obj, Enum.ToObject (ColumnType, val)); + } + else { + _prop?.SetValue (obj, val, null); + _field?.SetValue (obj, val); } } - public Column[] InsertColumns { - get { - return _insertColumns; + public object GetValue (object obj) + { + return _prop?.GetValue (obj, null) + ?? _field?.GetValue(obj); + } + } + + class ColumnMappingFromAttributes : ColumnMapping + { + internal ColumnMappingFromAttributes (MemberInfo prop, CreateFlags createFlags = CreateFlags.None) : base (prop) + { + var colAttr = prop.CustomAttributes.FirstOrDefault (x => x.AttributeType == typeof (ColumnAttribute)); + + Name = (colAttr != null && colAttr.ConstructorArguments.Count > 0) ? + colAttr.ConstructorArguments[0].Value?.ToString () : + prop.Name; + + var propertyType = (prop as PropertyInfo)?.PropertyType + ?? (prop as FieldInfo)?.FieldType; + + //If this type is Nullable then Nullable.GetUnderlyingType returns the T, otherwise it returns null, so get the actual type instead + ColumnType = Nullable.GetUnderlyingType (propertyType) ?? 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 IColumnIndex[] { new IndexedAttribute () }; } + IsNullable = !(IsPK || Orm.IsMarkedNotNull (prop)); + MaxStringLength = Orm.MaxStringLength (prop); + + StoreAsText = propertyType.GetTypeInfo ().CustomAttributes.Any (x => x.AttributeType == typeof (StoreAsTextAttribute)); } + } - public Column[] InsertOrReplaceColumns { - get { - return _insertOrReplaceColumns; + static class TableMappingBuilderExtensions + { + internal static MemberInfo AsMemberInfo (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; } - public Column FindColumnWithPropertyName (string propertyName) + internal static MemberInfo AsMemberInfo (this Expression> property) { - var exact = Columns.FirstOrDefault (c => c.PropertyName == propertyName); - return exact; + Expression body = property.Body; + var operand = (body as UnaryExpression)?.Operand as MemberExpression; + if (operand != null) { + body = operand; + } + + return (body as MemberExpression)?.Member; } - public Column FindColumn (string columnName) + internal static void AddPropertyValue (this Dictionary dict, Expression> property, T value) { - if(Method != MapMethod.ByName) - throw new InvalidOperationException($"This {nameof(TableMapping)} is not mapped by name, but {Method}."); + var prop = AsMemberInfo (property); + dict[prop] = value; + } - var exact = Columns.FirstOrDefault (c => c.Name.ToLower () == columnName.ToLower ()); - return exact; + internal static void AddProperty (this List list, Expression> property) + { + var prop = AsMemberInfo (property); + if (!list.Contains (prop)) { + list.Add (prop); + } } - public class Column + internal static void AddProperties (this List list, Expression>[] properties) { - MemberInfo _member; + foreach (var property in properties) { + AddProperty (list, property); + } + } - public string Name { get; private set; } + internal static T GetOrDefault (this Dictionary dict, MemberInfo key, T defaultValue = default (T)) + { + if (dict.ContainsKey (key)) { + return dict[key]; + } - public PropertyInfo PropertyInfo => _member as PropertyInfo; + return defaultValue; + } + } - public string PropertyName { get { return _member.Name; } } + public class TableMappingBuilder + { + readonly List _columns = new List (); - public Type ColumnType { get; private set; } + public abstract class ColumnMappingBuilder { + public abstract bool Matches (MemberInfo info); + public abstract ColumnMapping ToMapping (); - public string Collation { get; private set; } + public string Collation { get; protected set; } + public string ColumnName { get; protected set; } + public bool IsAutoIncrement { get; protected set; } + public bool IsIgnored { get; protected set; } + public bool IsNotNull { get; protected set; } + public bool IsStoredAsText { get; protected set; } + public int? MaxLength { get; protected set; } + } - public bool IsAutoInc { get; private set; } - public bool IsAutoGuid { get; private set; } + public class ColumnMappingBuilder : ColumnMappingBuilder + { + static Type ColumnType => typeof(TCol); - public bool IsPK { get; private set; } + private MemberInfo _info; + private TableMappingBuilder _table; - public IEnumerable Indices { get; set; } + public ColumnMappingBuilder (TableMappingBuilder table, Expression> property) + { + _info = property.AsMemberInfo (); + ColumnName = _info.Name; - public bool IsNullable { get; private set; } + _table = table; + table._columns.Add (this); + } - public int? MaxStringLength { get; private set; } + public override bool Matches (MemberInfo info) + { + return _info == info; + } - public bool StoreAsText { get; private set; } + public override ColumnMapping ToMapping () + { + if (IsIgnored) { return null; } + + var isPrimaryKey = _table._primaryKeys.Any (x => string.Equals (x, _info.Name, StringComparison.InvariantCulture)); + + return new ColumnMapping (_info) { + Name = ColumnName, + //If this type is Nullable then Nullable.GetUnderlyingType returns the T, otherwise it returns null, so get the actual type instead + ColumnType = Nullable.GetUnderlyingType (ColumnType) ?? ColumnType, + Collation = Collation ?? string.Empty, + IsPK = isPrimaryKey, + Indices = _table._indices.GetOrDefault (_info, new List ()), + IsAutoGuid = IsAutoIncrement && ColumnType == typeof (Guid), + IsAutoInc = IsAutoIncrement, + IsNullable = !(isPrimaryKey || IsNotNull), + MaxStringLength = MaxLength, + StoreAsText = IsStoredAsText + }; + } - public Column (MemberInfo member, CreateFlags createFlags = CreateFlags.None) + public ColumnMappingBuilder HasColumnName (string name) { - _member = member; - var memberType = GetMemberType(member); + ColumnName = name; + return this; + } - var colAttr = member.CustomAttributes.FirstOrDefault (x => x.AttributeType == typeof (ColumnAttribute)); -#if ENABLE_IL2CPP - var ca = member.GetCustomAttribute(typeof(ColumnAttribute)) as ColumnAttribute; - Name = ca == null ? member.Name : ca.Name; -#else - Name = (colAttr != null && colAttr.ConstructorArguments.Count > 0) ? - colAttr.ConstructorArguments[0].Value?.ToString () : - member.Name; -#endif - //If this type is Nullable then Nullable.GetUnderlyingType returns the T, otherwise it returns null, so get the actual type instead - ColumnType = Nullable.GetUnderlyingType (memberType) ?? memberType; - Collation = Orm.Collation (member); - - IsPK = Orm.IsPK (member) || - (((createFlags & CreateFlags.ImplicitPK) == CreateFlags.ImplicitPK) && - string.Compare (member.Name, Orm.ImplicitPkName, StringComparison.OrdinalIgnoreCase) == 0); - - var isAuto = Orm.IsAutoInc (member) || (IsPK && ((createFlags & CreateFlags.AutoIncPK) == CreateFlags.AutoIncPK)); - IsAutoGuid = isAuto && ColumnType == typeof (Guid); - IsAutoInc = isAuto && !IsAutoGuid; - - Indices = Orm.GetIndices (member); - if (!Indices.Any () - && !IsPK - && ((createFlags & CreateFlags.ImplicitIndex) == CreateFlags.ImplicitIndex) - && Name.EndsWith (Orm.ImplicitIndexSuffix, StringComparison.OrdinalIgnoreCase) - ) { - Indices = new IndexedAttribute[] { new IndexedAttribute () }; - } - IsNullable = !(IsPK || Orm.IsMarkedNotNull (member)); - MaxStringLength = Orm.MaxStringLength (member); + public ColumnMappingBuilder HasMaxLength(int maxLength) + { + if (!ColumnType.IsAssignableFrom (typeof (string))) + throw new InvalidOperationException ("Cannot specify max length for non-string types"); - StoreAsText = memberType.GetTypeInfo ().CustomAttributes.Any (x => x.AttributeType == typeof (StoreAsTextAttribute)); + MaxLength = maxLength; + return this; } - public Column (PropertyInfo member, CreateFlags createFlags = CreateFlags.None) - : this((MemberInfo)member, createFlags) - { } + public ColumnMappingBuilder HasCollation(string collation) + { + if (!ColumnType.IsAssignableFrom (typeof (string))) + throw new InvalidOperationException ("Cannot specify collation for non-string types"); + + Collation = collation; + return this; + } - public void SetValue (object obj, object val) + public ColumnMappingBuilder AutoIncrement () { - if(_member is PropertyInfo propy) - { - if (val != null && ColumnType.GetTypeInfo ().IsEnum) - propy.SetValue (obj, Enum.ToObject (ColumnType, val)); - else - propy.SetValue (obj, val); - } - else if(_member is FieldInfo field) - { - if (val != null && ColumnType.GetTypeInfo ().IsEnum) - field.SetValue (obj, Enum.ToObject (ColumnType, val)); - else - field.SetValue (obj, val); - } - else - throw new InvalidProgramException("unreachable condition"); + IsAutoIncrement = true; + return this; } - public object GetValue (object obj) + public ColumnMappingBuilder NotNull () { - if(_member is PropertyInfo propy) - return propy.GetValue(obj); - else if(_member is FieldInfo field) - return field.GetValue(obj); - else - throw new InvalidProgramException("unreachable condition"); + IsNotNull = true; + return this; } - private static Type GetMemberType(MemberInfo m) + public ColumnMappingBuilder StoreAsText () { - switch(m.MemberType) - { - case MemberTypes.Property: return ((PropertyInfo)m).PropertyType; - case MemberTypes.Field: return ((FieldInfo)m).FieldType; - default: throw new InvalidProgramException($"{nameof(TableMapping)} supports properties or fields only."); - } + if (!ColumnType.IsEnum) + throw new InvalidOperationException ("Only enum fields can be stored as text"); + + IsStoredAsText = true; + return this; + } + + public void Ignore() + { + IsIgnored = true; } } - internal enum MapMethod + string _tableName; + + readonly List _primaryKeys = new List (); + bool _withoutRowId; + TableMapping.MapMethod? _method; + + 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 () { - ByName, - ByPosition + _tableName = MappedType.Name; + } + + public TableMappingBuilder MapColumnsByPosition() + { + _method = TableMapping.MapMethod.ByPosition; + return this; + } + + public TableMappingBuilder MapColumnsByName() + { + _method = TableMapping.MapMethod.ByName; + return this; + } + + public TableMappingBuilder HasTableName (string name) + { + _tableName = name; + return this; + } + + public TableMappingBuilder WithoutRowId (bool value = true) + { + _withoutRowId = value; + return this; + } + + public TableMappingBuilder WithColumns(Action> configure) + { + configure (this); + return this; + } + + public ColumnMappingBuilder Column(Expression> property) + { + return new ColumnMappingBuilder (this, property); + } + + /*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 HasIndex (Expression> property, bool unique = false, string indexName = null, int order = 0) + { + var prop = property.AsMemberInfo (); + if (!_indices.ContainsKey (prop)) { + _indices[prop] = new List (); + } + + _indices[prop].Add (new ColumnIndex { + Name = indexName, + Order = order, + Unique = unique + }); + + return this; + } + + public TableMappingBuilder HasIndex (string indexName, Expression> property, bool unique = false, int order = 0) + { + return HasIndex (property, unique, indexName, order); + } + + /*public TableMappingBuilder HasUniqueIndex (Expression> property, string indexName = null, int order = 0) + { + return Index (property, true, indexName, order); + } + + public TableMappingBuilder HasUniqueIndex (string indexName, Expression> property, int order = 0) + { + return Index (property, true, indexName, order); + }*/ + + public TableMappingBuilder HasIndex (params Expression>[] properties) + { + for (int i = 0; i < properties.Length; i++) { + HasIndex (properties[i], false, null, i); + } + + return this; + } + + public TableMappingBuilder HasIndex (string indexName, params Expression>[] properties) + { + for (int i = 0; i < properties.Length; i++) { + HasIndex (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 HasPrimaryKey (Expression> property, bool autoIncrement = false) + { + var propInfo = property.AsMemberInfo (); + + _primaryKeys.Add (propInfo.Name); + if (autoIncrement) { + _autoInc.Add (propInfo.Name); + } + + return this; + } + + public TableMappingBuilder Ignore (Expression> property) + { + _ignore.Add (property.AsMemberInfo ().Name); + return this; + } + + public TableMappingBuilder Ignore (params Expression>[] properties) + { + _ignore.AddRange (properties.Select (p => p.AsMemberInfo ().Name)); + return this; + } + + /*public TableMappingBuilder AutoIncrement (Expression> property) + { + _autoInc.Add (property.AsPropertyInfo ().Name); + return this; + } + + public TableMappingBuilder AutoIncrement (params Expression>[] properties) + { + _autoInc.AddRange (properties.Select (p => p.AsPropertyInfo ().Name)); + return this; + }*/ + + /*public TableMappingBuilder NotNull (Expression> property) + { + _notNull.Add (property.AsPropertyInfo ().Name); + return this; + }*/ + + /*public TableMappingBuilder NotNull (params Expression>[] properties) + { + _notNull.AddRange (properties.Select (p => p.AsPropertyInfo ().Name)); + return this; + }*/ + + /*public TableMappingBuilder StoreAsText (Expression> property) + { + _storeAsText.Add (property.AsPropertyInfo ().Name); + return this; + }*/ + + /*public TableMappingBuilder StoreAsText (params Expression>[] properties) + { + _storeAsText.AddRange (properties.Select (p => p.AsPropertyInfo ().Name)); + 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) { + WithoutRowId = _withoutRowId + }; + + var (props, mappingMethod) = TableMappingFromAttributes.GetPublicMembers(MappedType); + tableMapping.Method = _method ?? mappingMethod; + + var cols = new List (); + + foreach (var p in props) { + // Find + if ((p is FieldInfo || (p as PropertyInfo)?.CanWrite == true) && !_ignore.Contains (p.Name)) { + var colMapping = _columns.FirstOrDefault (x => x.Matches (p)); + if (colMapping?.IsIgnored == true) { continue; } + + var col = colMapping?.ToMapping() + ?? new ColumnMapping (p) { + Name =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 as PropertyInfo)?.PropertyType ?? (p as FieldInfo)?.FieldType) + ?? (p as PropertyInfo)?.PropertyType ?? (p as FieldInfo)?.FieldType, + Collation = string.Empty, + IsPK = _primaryKeys.Any(x => string.Equals(x, p.Name, StringComparison.InvariantCulture)) + }; + + bool isAuto = colMapping?.IsAutoIncrement ?? _autoInc.Contains (p.Name, StringComparer.InvariantCulture); + col.IsAutoGuid = isAuto && col.ColumnType == typeof (Guid); + col.IsAutoInc = isAuto && !col.IsAutoGuid; + + col.Indices = _indices.GetOrDefault (p, new List (0)); + + col.IsNullable = !col.IsPK; + + 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; } } @@ -2870,7 +3486,7 @@ public static Type GetType (object obj) return obj.GetType (); } - public static string SqlDecl (TableMapping.Column p, bool storeDateTimeAsTicks, bool storeTimeSpanAsTicks) + public static string SqlDecl (ColumnMapping p, bool storeDateTimeAsTicks, bool storeTimeSpanAsTicks) { string decl = "\"" + p.Name + "\" " + SqlType (p, storeDateTimeAsTicks, storeTimeSpanAsTicks) + " "; @@ -2890,7 +3506,7 @@ public static string SqlDecl (TableMapping.Column p, bool storeDateTimeAsTicks, return decl; } - public static string SqlType (TableMapping.Column p, bool storeDateTimeAsTicks, bool storeTimeSpanAsTicks) + public static string SqlType (ColumnMapping p, bool storeDateTimeAsTicks, bool storeTimeSpanAsTicks) { 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)) { @@ -3111,7 +3727,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)]; var fastColumnSetters = new Action[SQLite3.ColumnCount (stmt)]; if (map.Method == TableMapping.MapMethod.ByPosition) @@ -3496,7 +4112,7 @@ internal class FastColumnSetter /// /// If no fast setter is available for the requested column (enums in particular cause headache), then this function returns null. /// - internal static Action GetFastSetter (SQLiteConnection conn, TableMapping.Column column) + internal static Action GetFastSetter (SQLiteConnection conn, ColumnMapping column) { Action fastSetter = null; @@ -3655,7 +4271,7 @@ internal static Action GetFastSetter (SQLiteCo /// The column mapping that identifies the target member of the destination object /// A lambda that can be used to retrieve the column value at query-time /// A strongly-typed delegate - private static Action CreateNullableTypedSetterDelegate (TableMapping.Column column, Func getColumnValue) where ColumnMemberType : struct + private static Action CreateNullableTypedSetterDelegate (ColumnMapping column, Func getColumnValue) where ColumnMemberType : struct { var clrTypeInfo = column.PropertyInfo.PropertyType.GetTypeInfo(); bool isNullable = false; @@ -3687,7 +4303,7 @@ private static Action CreateNullableTypedSetterDe /// The column mapping that identifies the target member of the destination object /// A lambda that can be used to retrieve the column value at query-time /// A strongly-typed delegate - private static Action CreateTypedSetterDelegate (TableMapping.Column column, Func getColumnValue) + private static Action CreateTypedSetterDelegate (ColumnMapping column, Func getColumnValue) { var setProperty = (Action)Delegate.CreateDelegate ( typeof (Action), null, diff --git a/src/SQLiteAsync.cs b/src/SQLiteAsync.cs index e3067619..61d95229 100644 --- a/src/SQLiteAsync.cs +++ b/src/SQLiteAsync.cs @@ -71,6 +71,7 @@ Task CreateTablesAsync (CreateFlags creat where T3 : new() where T4 : new() where T5 : new(); + Task CreateTablesAsync (CreateFlags createFlags = CreateFlags.None, params Type[] types); Task> DeferredQueryAsync (string query, params object[] args) where T : new(); Task> DeferredQueryAsync (TableMapping map, string query, params object[] args); @@ -381,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 @@ -465,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. /// diff --git a/tests/SQLite.Tests/ConcurrencyTest.cs b/tests/SQLite.Tests/ConcurrencyTest.cs index ae0b7334..98750e2d 100644 --- a/tests/SQLite.Tests/ConcurrencyTest.cs +++ b/tests/SQLite.Tests/ConcurrencyTest.cs @@ -17,7 +17,7 @@ namespace SQLite.Tests { - [TestFixture, NUnit.Framework.Ignore("Fails to run on .NET Core 3.1 Mac")] + [TestFixture] //, NUnit.Framework.Ignore("Fails to run on .NET Core 3.1 Mac")] public class ConcurrencyTest { public class TestObj diff --git a/tests/SQLite.Tests/CreateTableFluentTest.cs b/tests/SQLite.Tests/CreateTableFluentTest.cs new file mode 100644 index 00000000..e9b2f55f --- /dev/null +++ b/tests/SQLite.Tests/CreateTableFluentTest.cs @@ -0,0 +1,239 @@ +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 () + { + Assert.That (() => { + var db = new TestDb (); + + var mapping = TableMapping.Build ().ToMapping (); + + db.CreateTable (mapping); + }, Throws.TypeOf ()); + } + + class DbSchema + { + public TableMapping Products { get; } + public TableMapping Orders { get; } + public TableMapping OrderLines { get; } + public TableMapping OrderHistory { get; } + + public DbSchema () + { + Products = TableMapping.Build () + .HasTableName ("Product") + .HasPrimaryKey (x => x.Id, autoIncrement: true) + .ToMapping (); + + Orders = TableMapping.Build () + .HasTableName ("Order") + .HasPrimaryKey (x => x.Id, autoIncrement: true) + .ToMapping (); + + OrderLines = TableMapping.Build () + .HasTableName ("OrderLine") + .HasPrimaryKey (x => x.Id, autoIncrement: true) + .HasIndex ("IX_OrderProduct", x => x.OrderId, x => x.ProductId) + .ToMapping (); + + OrderHistory = TableMapping.Build () + .HasTableName ("OrderHistory") + .HasPrimaryKey (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 () + .HasTableName ("Product") + .HasPrimaryKey (x => x.Id, autoIncrement: true) + .ToMapping (); + + var order = TableMapping.Build () + .HasTableName ("Order") + .HasPrimaryKey (x => x.Id, autoIncrement: true) + .ToMapping (); + + var orderLine = TableMapping.Build () + .HasTableName ("OrderLine") + .HasPrimaryKey (x => x.Id, autoIncrement: true) + .HasIndex ("IX_OrderProduct", x => x.OrderId, x => x.ProductId) + .ToMapping (); + + var orderHistory = TableMapping.Build () + .HasTableName ("OrderHistory") + .HasPrimaryKey (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 OrderLinePoco () { + 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 () + .HasPrimaryKey (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 (); + 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 () + .HasTableName ("sqlite_master") + .WithColumns(table => { + table.Column (x => x.Type).HasColumnName ("type"); + table.Column (x => x.Name).HasColumnName ("name"); + table.Column (x => x.TableName).HasColumnName ("tbl_name"); + table.Column (x => x.RootPage).HasColumnName ("rootpage"); + table.Column (x => x.Sql).HasColumnName ("sql"); + }) + .ToMapping (); + + // Configure SQLite to use a 'static' mapping for system level tables + SQLiteConnection.UseMapping (master); + + var wantsNoRowId = TableMapping.Build () + .HasPrimaryKey (x => x.Id) + .WithoutRowId () + .ToMapping (); + + var orderLine = TableMapping.Build () + .HasTableName ("OrderLine") + .HasPrimaryKey (x => x.Id, autoIncrement: true) + .HasIndex ("IX_OrderProduct", x => x.OrderId, x => x.ProductId) + .ToMapping (); + + conn.CreateTable (orderLine); + var info = conn.Table ().Where (m => m.TableName == "OrderLine").First (); + Assert.That (!info.Sql.Contains ("without rowid")); + + conn.CreateTable (wantsNoRowId); + info = conn.Table ().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/DbCommandTest.cs b/tests/SQLite.Tests/DbCommandTest.cs index 4d40eeb0..bdef7132 100644 --- a/tests/SQLite.Tests/DbCommandTest.cs +++ b/tests/SQLite.Tests/DbCommandTest.cs @@ -27,7 +27,7 @@ public void QueryCommand() db.Insert(b); var test = db.CreateCommand("select * from Product") - .ExecuteDeferredQuery(new TableMapping(typeof(Product))).ToList(); + .ExecuteDeferredQuery(new TableMappingFromAttributes(typeof(Product))).ToList(); Assert.AreEqual (test.Count, 1); @@ -44,7 +44,7 @@ public void QueryCommandCastToObject() db.Insert(b); var test = db.CreateCommand("select * from Product") - .ExecuteDeferredQuery(new TableMapping(typeof(Product))).ToList(); + .ExecuteDeferredQuery(new TableMappingFromAttributes(typeof(Product))).ToList(); Assert.AreEqual (test.Count, 1); diff --git a/tests/SQLite.Tests/MappingTest.cs b/tests/SQLite.Tests/MappingTest.cs index e1cda46d..89a330ac 100644 --- a/tests/SQLite.Tests/MappingTest.cs +++ b/tests/SQLite.Tests/MappingTest.cs @@ -145,7 +145,7 @@ public void OnlyKey () [Test] public void TableMapping_MapsValueTypes() { - var mapping = new TableMapping(typeof( (int a, string b, double? c) )); + var mapping = new TableMappingFromAttributes(typeof( (int a, string b, double? c) )); Assert.AreEqual(3, mapping.Columns.Length); Assert.AreEqual("Item1", mapping.Columns[0].Name);