diff --git a/src/SQLite.cs b/src/SQLite.cs index c276066fe..360353945 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) @@ -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); } } @@ -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. + public 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 { @@ -490,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 @@ -501,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; @@ -515,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"; } @@ -559,6 +580,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 @@ -649,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. /// @@ -762,7 +815,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; @@ -966,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 '?' @@ -995,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. @@ -1007,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. @@ -1029,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. @@ -1045,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 @@ -1061,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. @@ -1079,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. @@ -1115,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 @@ -1134,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 @@ -1406,7 +1623,8 @@ public void RunInTransaction (Action action) /// /// /// An of the objects to insert. - /// + /// + /// /// A boolean indicating if the inserts should be wrapped in a transaction. /// /// @@ -2054,6 +2272,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 { @@ -2094,7 +2319,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; } @@ -2168,29 +2393,93 @@ 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; 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; internal set; } - public Column[] Columns { get; private set; } + public ColumnMapping[] Columns { get; internal set; } - public Column PK { get; private set; } + public ColumnMapping PK { get; internal set; } - public string GetByPrimaryKeySql { get; private set; } + public string GetByPrimaryKeySql { get; internal set; } + + public CreateFlags CreateFlags { get; protected set; } + + protected ColumnMapping _autoPk; + + internal ColumnMapping AutoIncPK { + get { return _autoPk; } + set { _autoPk = value; } + } + + public bool HasAutoIncPK => _autoPk != null; + + public void SetAutoIncPK (object obj, long id) + { + if (_autoPk != null) { + _autoPk.SetValue (obj, Convert.ChangeType (id, _autoPk.ColumnType, null)); + } + } + + public ColumnMapping[] InsertColumns => Columns.Where (c => !c.IsAutoInc).ToArray (); + public ColumnMapping[] InsertOrReplaceColumns => Columns.ToArray (); - public CreateFlags CreateFlags { get; private set; } + public ColumnMapping FindColumnWithPropertyName (string propertyName) + { + var exact = Columns.FirstOrDefault (c => c.PropertyName == propertyName); + return exact; + } - readonly Column _autoPk; - readonly Column[] _insertColumns; - readonly Column[] _insertOrReplaceColumns; + public ColumnMapping FindColumn (string columnName) + { + var exact = Columns.FirstOrDefault (c => c.Name.ToLower () == columnName.ToLower ()); + return exact; + } - public TableMapping (Type type, CreateFlags createFlags = CreateFlags.None) + 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(); + } + + /// + /// 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 (); @@ -2224,11 +2513,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 ColumnMappingFromAttributes (p, createFlags)); } } Columns = cols.ToArray (); @@ -2241,8 +2530,6 @@ from p in ti.DeclaredProperties } } - HasAutoIncPK = _autoPk != null; - if (PK != null) { GetByPrimaryKeySql = string.Format ("select * from \"{0}\" where \"{1}\" = ?", TableName, PK.Name); } @@ -2250,119 +2537,388 @@ from p in ti.DeclaredProperties // People should not be calling Get/Find without a PK GetByPrimaryKeySql = string.Format ("select * from \"{0}\" limit 1", TableName); } + } + } + + public class ColumnMapping + { + readonly PropertyInfo _prop; + + public string Name { get; internal set; } + + public PropertyInfo PropertyInfo => _prop; + + public string PropertyName { get { return _prop.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; } - _insertColumns = Columns.Where (c => !c.IsAutoInc).ToArray (); - _insertOrReplaceColumns = Columns.ToArray (); + 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 (PropertyInfo prop) + { + _prop = prop; } - public bool HasAutoIncPK { get; private set; } + 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 void SetAutoIncPK (object obj, long id) + public object GetValue (object obj) { - if (_autoPk != null) { - _autoPk.SetValue (obj, Convert.ChangeType (id, _autoPk.ColumnType, null)); + return _prop.GetValue (obj, null); + } + } + + class ColumnMappingFromAttributes : ColumnMapping + { + internal ColumnMappingFromAttributes (PropertyInfo 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; + //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 IColumnIndex[] { new IndexedAttribute () }; } + IsNullable = !(IsPK || Orm.IsMarkedNotNull (prop)); + MaxStringLength = Orm.MaxStringLength (prop); + + StoreAsText = prop.PropertyType.GetTypeInfo ().CustomAttributes.Any (x => x.AttributeType == typeof (StoreAsTextAttribute)); } + } - public Column[] InsertColumns { - get { - return _insertColumns; + 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; } - public Column[] InsertOrReplaceColumns { - get { - return _insertOrReplaceColumns; + 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); } } - public Column FindColumnWithPropertyName (string propertyName) + internal static void AddProperties (this List list, Expression>[] properties) { - var exact = Columns.FirstOrDefault (c => c.PropertyName == propertyName); - return exact; + foreach (var property in properties) { + AddProperty (list, property); + } } - public Column FindColumn (string columnName) + internal static T GetOrDefault (this Dictionary dict, PropertyInfo key, T defaultValue = default(T)) { - var exact = Columns.FirstOrDefault (c => c.Name.ToLower () == columnName.ToLower ()); - return exact; + if (dict.ContainsKey (key)) { + return dict[key]; + } + + 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 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 WithoutRowId (bool value = true) + { + _withoutRowId = value; + return this; + } + + public TableMappingBuilder ColumnName (Expression> property, string name) + { + _columnNames.AddPropertyValue (property, name); + return this; + } + + public TableMappingBuilder MaxLength (Expression> property, int maxLength) + { + _maxLengths.AddPropertyValue (property, maxLength); + return this; } - public class Column + public TableMappingBuilder Collation (Expression> property, string collation) { - PropertyInfo _prop; + _collations.AddPropertyValue (property, collation); + return this; + } - public string Name { get; private set; } + 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 (); + } - public PropertyInfo PropertyInfo => _prop; + _indices[prop].Add (new ColumnIndex { + Name = indexName, + Order = order, + Unique = unique + }); + return this; + } - public string PropertyName { get { return _prop.Name; } } + public TableMappingBuilder Index (string indexName, Expression> property, bool unique = false, int order = 0) + { + return Index (property, unique, indexName, order); + } - public Type ColumnType { get; private set; } + public TableMappingBuilder Unique (Expression> property, string indexName = null, int order = 0) + { + return Index (property, true, indexName, order); + } - public string Collation { get; private set; } + public TableMappingBuilder Unique (string indexName, Expression> property, int order = 0) + { + return Index (property, true, indexName, order); + } - public bool IsAutoInc { get; private set; } - public bool IsAutoGuid { get; private set; } + public TableMappingBuilder Index (params Expression>[] properties) + { + for (int i = 0; i < properties.Length; i++) { + Index (properties[i], false, null, i); + } - public bool IsPK { get; private set; } + return this; + } - public IEnumerable Indices { get; set; } + public TableMappingBuilder Index (string indexName, params Expression>[] properties) + { + for (int i = 0; i < properties.Length; i++) { + Index (properties[i], false, indexName, i); + } - public bool IsNullable { get; private set; } + return this; + } - public int? MaxStringLength { get; private set; } + public TableMappingBuilder Unique (params Expression>[] properties) + { + for (int i = 0; i < properties.Length; i++) { + Index (properties[i], true, null, i); + } - public bool StoreAsText { get; private set; } + return this; + } - 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 () }; + 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) { + WithoutRowId = _withoutRowId + }; + + 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); } - IsNullable = !(IsPK || Orm.IsMarkedNotNull (prop)); - MaxStringLength = Orm.MaxStringLength (prop); - StoreAsText = prop.PropertyType.GetTypeInfo ().CustomAttributes.Any (x => x.AttributeType == typeof (StoreAsTextAttribute)); + props.AddRange (newProps); + baseType = ti.BaseType; } - public void SetValue (object obj, object val) - { - if (val != null && ColumnType.GetTypeInfo ().IsEnum) { - _prop.SetValue (obj, Enum.ToObject (ColumnType, val)); + 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); } - else { - _prop.SetValue (obj, val, null); + } + + tableMapping.Columns = cols.ToArray (); + + foreach (var c in tableMapping.Columns) { + if (c.IsAutoInc && c.IsPK) { + tableMapping.AutoIncPK = c; + } + + if (c.IsPK) { + tableMapping.PK = c; } } - public object GetValue (object obj) - { - return _prop.GetValue (obj, null); + 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; } } @@ -2432,7 +2988,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) + " "; @@ -2452,7 +3008,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)) { @@ -2656,7 +3212,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); @@ -3073,12 +3629,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; diff --git a/src/SQLiteAsync.cs b/src/SQLiteAsync.cs index 2823748a9..badb3c7d9 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. /// @@ -911,7 +937,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. /// /// diff --git a/tests/CreateTableFluentTest.cs b/tests/CreateTableFluentTest.cs new file mode 100644 index 000000000..2baae89e4 --- /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 674f1cbe2..a664dc996 100644 --- a/tests/SQLite.Tests.csproj +++ b/tests/SQLite.Tests.csproj @@ -44,6 +44,7 @@ +