diff --git a/README.md b/README.md index 98773b17..6f17796f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,6 @@ +This is fork of official https://github.com/praeclarum/sqlite-net merged with code from old RoyGoode Fluent API fork. + +Main purpose is to play with it, see feasibility. I'm working on a project where this could be very useful. (Table mappings moved to storage layer instead of Model which is shared between multiple projects) # SQLite-net diff --git a/src/SQLite.cs b/src/SQLite.cs index 5ed96f2c..027acf43 100644 --- a/src/SQLite.cs +++ b/src/SQLite.cs @@ -76,7 +76,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) @@ -454,12 +454,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); } } @@ -481,6 +481,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; @@ -544,9 +561,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 @@ -614,6 +636,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 @@ -703,6 +740,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. /// @@ -816,7 +870,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; @@ -998,6 +1052,7 @@ public T ExecuteScalar (string query, params object[] args) return cmd.ExecuteQuery (); } + /// /// 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. @@ -1010,7 +1065,10 @@ public T ExecuteScalar (string query, params object[] args) /// Arguments to substitute for the occurences of '?' in the query. /// /// - /// An enumerable with one result for the first column of each row returned by 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 List QueryScalars (string query, params object[] args) { @@ -1042,6 +1100,33 @@ public List QueryScalars (string query, params object[] args) return cmd.ExecuteDeferredQuery (); } + /// + /// 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) where T : new() + { + 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. @@ -1068,6 +1153,30 @@ public List Query (TableMapping map, string query, params object[] 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) where T : new() + { + 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. @@ -1109,6 +1218,19 @@ 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 @@ -1147,6 +1269,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 /// associated with the specified type. @@ -1163,6 +1305,25 @@ 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 @@ -1201,6 +1362,26 @@ public object Find (object pk, TableMapping map) 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. + /// + /// + /// 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 the first object that matches the predicate from the table /// associated with the specified type. @@ -1217,6 +1398,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 /// associated with the specified type. @@ -1236,6 +1436,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 /// associated with the specified type. @@ -1507,9 +1729,9 @@ public void RunInTransaction (Action action) /// /// /// An of the objects to insert. - /// - /// A boolean indicating if the inserts should be wrapped in a transaction. /// + /// + /// A boolean indicating if the inserts should be wrapped in a transaction. /// /// The number of rows added to the table. /// @@ -2301,6 +2523,13 @@ public SQLiteConnectionString (string databasePath, SQLiteOpenFlags openFlags, b } } + public interface IColumnIndex + { + string Name { get; set; } + int Order { get; set; } + bool Unique { get; set; } + } + [AttributeUsage (AttributeTargets.Class)] public class TableAttribute : Attribute { @@ -2341,7 +2570,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; } @@ -2415,29 +2644,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; protected set; } + + public bool WithoutRowId { get; internal set; } + + public ColumnMapping[] Columns { get; internal set; } + + public ColumnMapping PK { get; internal set; } - public string TableName { get; private set; } + public string GetByPrimaryKeySql { get; internal set; } - public bool WithoutRowId { get; private set; } + public CreateFlags CreateFlags { get; protected set; } - public Column[] Columns { get; private set; } + protected ColumnMapping _autoPk; - public Column PK { get; private set; } + 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 string GetByPrimaryKeySql { get; private set; } + 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 (); @@ -2471,11 +2764,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 (); @@ -2488,7 +2781,7 @@ from p in ti.DeclaredProperties } } - HasAutoIncPK = _autoPk != null; + //HasAutoIncPK = _autoPk != null; if (PK != null) { GetByPrimaryKeySql = string.Format ("select * from \"{0}\" where \"{1}\" = ?", TableName, PK.Name); @@ -2498,118 +2791,406 @@ from p in ti.DeclaredProperties GetByPrimaryKeySql = string.Format ("select * from \"{0}\" limit 1", TableName); } - _insertColumns = Columns.Where (c => !c.IsAutoInc).ToArray (); - _insertOrReplaceColumns = Columns.ToArray (); + //_insertColumns = Columns.Where (c => !c.IsAutoInc).ToArray (); + //_insertOrReplaceColumns = Columns.ToArray (); } - public bool HasAutoIncPK { get; private set; } + //public bool HasAutoIncPK { get; private set; } - public void SetAutoIncPK (object obj, long id) + //public void SetAutoIncPK (object obj, long id) + //{ + // if (_autoPk != null) { + // _autoPk.SetValue (obj, Convert.ChangeType (id, _autoPk.ColumnType, null)); + // } + //} + + } + + 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; } + + 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) { - if (_autoPk != null) { - _autoPk.SetValue (obj, Convert.ChangeType (id, _autoPk.ColumnType, null)); + _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 Column[] InsertColumns { - get { - return _insertColumns; + public object GetValue (object obj) + { + return _prop.GetValue (obj, null); + } + } + + class ColumnMappingFromAttributes : ColumnMapping + { + PropertyInfo _prop; + + public 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; + //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[] InsertOrReplaceColumns { - get { - return _insertOrReplaceColumns; + 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 FindColumnWithPropertyName (string propertyName) + internal static void AddPropertyValue (this Dictionary dict, Expression> property, T value) { - var exact = Columns.FirstOrDefault (c => c.PropertyName == propertyName); - return exact; + var prop = AsPropertyInfo (property); + dict[prop] = value; } - public Column FindColumn (string columnName) + internal static void AddProperty (this List list, Expression> property) { - var exact = Columns.FirstOrDefault (c => c.Name.ToLower () == columnName.ToLower ()); - return exact; + var prop = AsPropertyInfo (property); + if (!list.Contains (prop)) { + list.Add (prop); + } } - public class Column + internal static void AddProperties (this List list, Expression>[] properties) { - PropertyInfo _prop; + foreach (var property in properties) { + AddProperty (list, property); + } + } - public string Name { get; private set; } + internal static T GetOrDefault (this Dictionary dict, PropertyInfo key, T defaultValue = default (T)) + { + if (dict.ContainsKey (key)) { + return dict[key]; + } - public PropertyInfo PropertyInfo => _prop; + return defaultValue; + } + } - public string PropertyName { get { return _prop.Name; } } + public class TableMappingBuilder + { + string _tableName; - public Type ColumnType { get; private set; } + readonly List _primaryKeys = new List (); + bool _withoutRowId; - public string Collation { get; private set; } + readonly List _ignore = new List (); + readonly List _autoInc = new List (); + readonly List _notNull = new List (); + readonly List _storeAsText = new List (); - public bool IsAutoInc { get; private set; } - public bool IsAutoGuid { get; private set; } + readonly Dictionary _columnNames = new Dictionary (); + readonly Dictionary _maxLengths = new Dictionary (); + readonly Dictionary _collations = new Dictionary (); + readonly Dictionary> _indices = new Dictionary> (); - public bool IsPK { get; private set; } + static Type MappedType => typeof (T); - public IEnumerable Indices { get; set; } + public TableMappingBuilder TableName (string name) + { + _tableName = name; + return this; + } - public bool IsNullable { get; private set; } + public TableMappingBuilder WithoutRowId (bool value = true) + { + _withoutRowId = value; + return this; + } - public int? MaxStringLength { get; private set; } + public TableMappingBuilder ColumnName (Expression> property, string name) + { + _columnNames.AddPropertyValue (property, name); + return this; + } - public bool StoreAsText { get; private set; } + public TableMappingBuilder MaxLength (Expression> property, int maxLength) + { + _maxLengths.AddPropertyValue (property, maxLength); + 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 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) + { + var propInfo = property.AsPropertyInfo (); + + _primaryKeys.Add (propInfo.Name); + if (autoIncrement) { + _autoInc.Add (propInfo.Name); + } + + return this; + } + + public TableMappingBuilder Ignore (Expression> property) + { + _ignore.Add (property.AsPropertyInfo ().Name); + return this; + } + + public TableMappingBuilder Ignore (params Expression>[] properties) + { + _ignore.AddRange (properties.Select (p => p.AsPropertyInfo ().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 = 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.Name)) { + 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 = _primaryKeys.Contains (p.Name) + }; + + bool isAuto = _autoInc.Contains (p.Name); + 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.Name)); + col.MaxStringLength = _maxLengths.GetOrDefault (p, null); + col.StoreAsText = _storeAsText.Contains (p.Name); + + 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; } } @@ -2679,7 +3260,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) + " "; @@ -2699,7 +3280,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)) { @@ -2901,7 +3482,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); @@ -3223,9 +3804,9 @@ object ReadCol (Sqlite3Statement stmt, int index, SQLite3.ColType type, Type clr var text = SQLite3.ColumnString (stmt, index); return new StringBuilder (text); } - else if (clrType == typeof(UriBuilder)) { - var text = SQLite3.ColumnString(stmt, index); - return new UriBuilder(text); + else if (clrType == typeof (UriBuilder)) { + var text = SQLite3.ColumnString (stmt, index); + return new UriBuilder (text); } else { throw new NotSupportedException ("Don't know how to read " + clrType); @@ -3365,7 +3946,7 @@ public class TableQuery : BaseTableQuery, IEnumerable Expression _selector; - TableQuery (SQLiteConnection conn, TableMapping table) + public TableQuery (SQLiteConnection conn, TableMapping table) { Connection = conn; Table = table; diff --git a/src/SQLiteAsync.cs b/src/SQLiteAsync.cs index b981d00e..dbcc1c9a 100644 --- a/src/SQLiteAsync.cs +++ b/src/SQLiteAsync.cs @@ -376,6 +376,32 @@ public Task CreateTablesAsync (CreateFlags createFlags = Cre return WriteAsync (conn => conn.CreateTables (createFlags, types)); } + /// + /// 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. + /// + /// + /// 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. /// @@ -752,6 +778,45 @@ public Task GetAsync (object pk, TableMapping map) { return ReadAsync (conn => conn.Get (pk, 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. + /// + /// + /// 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 Task GetAsync (TableMapping map, object pk) where T : new() + { + return ReadAsync (conn => conn.Get (map, pk)); + } + + /// + /// 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 Task GetAsync (TableMapping map, Expression> predicate) where T : new() + { + return ReadAsync (conn => conn.Get (map, predicate)); + } /// /// Attempts to retrieve the first object that matches the predicate from the table @@ -787,6 +852,45 @@ public Task FindAsync (object pk) { return ReadAsync (conn => conn.Find (pk)); } + + /// + /// 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 Task FindAsync (TableMapping map, Expression> predicate) where T : new() + { + return ReadAsync (conn => conn.Find (map, predicate)); + } + + /// + /// 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 Task FindAsync (TableMapping map, object pk) where T : new() + { + return ReadAsync (conn => conn.Find (map, pk)); + } /// /// Attempts to retrieve an object with the given primary key from the table @@ -844,6 +948,8 @@ public Task FindWithQueryAsync (string query, params object[] args) { return ReadAsync (conn => conn.FindWithQuery (query, args)); } + + /// /// Attempts to retrieve the first object that matches the query from the table @@ -866,6 +972,28 @@ public Task FindWithQueryAsync (TableMapping map, string query, params o { return ReadAsync (conn => conn.FindWithQuery (map, query, args)); } + + /// + /// 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 Task FindWithQueryAsync (TableMapping map, string query, params object[] args) where T : new() + { + return ReadAsync (conn => conn.FindWithQuery (map, query, args)); + } /// /// Retrieves the mapping that is automatically generated for the given type. @@ -938,7 +1066,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. /// /// @@ -1077,6 +1206,29 @@ public Task> QueryAsync (string query, params object[] args) { return ReadAsync (conn => conn.Query (query, args)); } + + /// + /// 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 Task> QueryAsync (TableMapping map, string query, params object[] args) where T : new() + { + return ReadAsync (conn => conn.Query (map, query, args)); + } /// /// Creates a SQLiteCommand given the command text (SQL) with arguments. Place a '?' @@ -1144,6 +1296,32 @@ public Task> DeferredQueryAsync (string query, params object[] { return ReadAsync (conn => (IEnumerable)conn.DeferredQuery (query, args).ToList ()); } + + /// + /// 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 Task> DeferredQueryAsync (TableMapping map, string query, params object[] args) where T : new() + { + return ReadAsync (conn => (IEnumerable)conn.DeferredQuery (map, query, args).ToList ()); + } /// /// Creates a SQLiteCommand given the command text (SQL) with arguments. Place a '?' @@ -1171,6 +1349,7 @@ public Task> DeferredQueryAsync (TableMapping map, string qu { return ReadAsync (conn => (IEnumerable)conn.DeferredQuery (map, query, args).ToList ()); } + } /// diff --git a/tests/CreateTableFluentTest.cs b/tests/CreateTableFluentTest.cs new file mode 100644 index 00000000..5ba5adbb --- /dev/null +++ b/tests/CreateTableFluentTest.cs @@ -0,0 +1,240 @@ +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() + .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 0c539cfd..219ee27c 100644 --- a/tests/SQLite.Tests.csproj +++ b/tests/SQLite.Tests.csproj @@ -46,6 +46,7 @@ +