Skip to content

Commit 47957c5

Browse files
authored
Merge pull request #1012 from XavierAP/issue1007
Support for ValueTuples
2 parents d846ead + cc766d1 commit 47957c5

File tree

3 files changed

+180
-62
lines changed

3 files changed

+180
-62
lines changed

src/SQLite.cs

Lines changed: 130 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -2440,6 +2440,8 @@ public class TableMapping
24402440

24412441
public CreateFlags CreateFlags { get; private set; }
24422442

2443+
internal MapMethod Method { get; private set; } = MapMethod.ByName;
2444+
24432445
readonly Column _autoPk;
24442446
readonly Column[] _insertColumns;
24452447
readonly Column[] _insertOrReplaceColumns;
@@ -2463,33 +2465,13 @@ public TableMapping (Type type, CreateFlags createFlags = CreateFlags.None)
24632465
TableName = (tableAttr != null && !string.IsNullOrEmpty (tableAttr.Name)) ? tableAttr.Name : MappedType.Name;
24642466
WithoutRowId = tableAttr != null ? tableAttr.WithoutRowId : false;
24652467

2466-
var props = new List<PropertyInfo> ();
2467-
var baseType = type;
2468-
var propNames = new HashSet<string> ();
2469-
while (baseType != typeof (object)) {
2470-
var ti = baseType.GetTypeInfo ();
2471-
var newProps = (
2472-
from p in ti.DeclaredProperties
2473-
where
2474-
!propNames.Contains (p.Name) &&
2475-
p.CanRead && p.CanWrite &&
2476-
(p.GetMethod != null) && (p.SetMethod != null) &&
2477-
(p.GetMethod.IsPublic && p.SetMethod.IsPublic) &&
2478-
(!p.GetMethod.IsStatic) && (!p.SetMethod.IsStatic)
2479-
select p).ToList ();
2480-
foreach (var p in newProps) {
2481-
propNames.Add (p.Name);
2482-
}
2483-
props.AddRange (newProps);
2484-
baseType = ti.BaseType;
2485-
}
2486-
2487-
var cols = new List<Column> ();
2488-
foreach (var p in props) {
2489-
var ignore = p.IsDefined (typeof (IgnoreAttribute), true);
2490-
if (!ignore) {
2491-
cols.Add (new Column (p, createFlags));
2492-
}
2468+
var members = GetPublicMembers(type);
2469+
var cols = new List<Column>(members.Count);
2470+
foreach(var m in members)
2471+
{
2472+
var ignore = m.IsDefined(typeof(IgnoreAttribute), true);
2473+
if(!ignore)
2474+
cols.Add(new Column(m, createFlags));
24932475
}
24942476
Columns = cols.ToArray ();
24952477
foreach (var c in Columns) {
@@ -2515,6 +2497,51 @@ from p in ti.DeclaredProperties
25152497
_insertOrReplaceColumns = Columns.ToArray ();
25162498
}
25172499

2500+
private IReadOnlyCollection<MemberInfo> GetPublicMembers(Type type)
2501+
{
2502+
if(type.Name.StartsWith("ValueTuple`"))
2503+
return GetFieldsFromValueTuple(type);
2504+
2505+
var members = new List<MemberInfo>();
2506+
var memberNames = new HashSet<string>();
2507+
var newMembers = new List<MemberInfo>();
2508+
do
2509+
{
2510+
var ti = type.GetTypeInfo();
2511+
newMembers.Clear();
2512+
2513+
newMembers.AddRange(
2514+
from p in ti.DeclaredProperties
2515+
where !memberNames.Contains(p.Name) &&
2516+
p.CanRead && p.CanWrite &&
2517+
p.GetMethod != null && p.SetMethod != null &&
2518+
p.GetMethod.IsPublic && p.SetMethod.IsPublic &&
2519+
!p.GetMethod.IsStatic && !p.SetMethod.IsStatic
2520+
select p);
2521+
2522+
members.AddRange(newMembers);
2523+
foreach(var m in newMembers)
2524+
memberNames.Add(m.Name);
2525+
2526+
type = ti.BaseType;
2527+
}
2528+
while(type != typeof(object));
2529+
2530+
return members;
2531+
}
2532+
2533+
private IReadOnlyCollection<MemberInfo> GetFieldsFromValueTuple(Type type)
2534+
{
2535+
Method = MapMethod.ByPosition;
2536+
var fields = type.GetFields();
2537+
2538+
// https://docs.microsoft.com/en-us/dotnet/api/system.valuetuple-8.rest
2539+
if(fields.Length >= 8)
2540+
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");
2541+
2542+
return fields;
2543+
}
2544+
25182545
public bool HasAutoIncPK { get; private set; }
25192546

25202547
public void SetAutoIncPK (object obj, long id)
@@ -2544,19 +2571,22 @@ public Column FindColumnWithPropertyName (string propertyName)
25442571

25452572
public Column FindColumn (string columnName)
25462573
{
2574+
if(Method != MapMethod.ByName)
2575+
throw new InvalidOperationException($"This {nameof(TableMapping)} is not mapped by name, but {Method}.");
2576+
25472577
var exact = Columns.FirstOrDefault (c => c.Name.ToLower () == columnName.ToLower ());
25482578
return exact;
25492579
}
25502580

25512581
public class Column
25522582
{
2553-
PropertyInfo _prop;
2583+
MemberInfo _member;
25542584

25552585
public string Name { get; private set; }
25562586

2557-
public PropertyInfo PropertyInfo => _prop;
2587+
public PropertyInfo PropertyInfo => _member as PropertyInfo;
25582588

2559-
public string PropertyName { get { return _prop.Name; } }
2589+
public string PropertyName { get { return _member.Name; } }
25602590

25612591
public Type ColumnType { get; private set; }
25622592

@@ -2575,59 +2605,95 @@ public class Column
25752605

25762606
public bool StoreAsText { get; private set; }
25772607

2578-
public Column (PropertyInfo prop, CreateFlags createFlags = CreateFlags.None)
2608+
public Column (MemberInfo member, CreateFlags createFlags = CreateFlags.None)
25792609
{
2580-
var colAttr = prop.CustomAttributes.FirstOrDefault (x => x.AttributeType == typeof (ColumnAttribute));
2610+
_member = member;
2611+
var memberType = GetMemberType(member);
25812612

2582-
_prop = prop;
2613+
var colAttr = member.CustomAttributes.FirstOrDefault (x => x.AttributeType == typeof (ColumnAttribute));
25832614
#if ENABLE_IL2CPP
25842615
var ca = prop.GetCustomAttribute(typeof(ColumnAttribute)) as ColumnAttribute;
25852616
Name = ca == null ? prop.Name : ca.Name;
25862617
#else
25872618
Name = (colAttr != null && colAttr.ConstructorArguments.Count > 0) ?
25882619
colAttr.ConstructorArguments[0].Value?.ToString () :
2589-
prop.Name;
2620+
member.Name;
25902621
#endif
25912622
//If this type is Nullable<T> then Nullable.GetUnderlyingType returns the T, otherwise it returns null, so get the actual type instead
2592-
ColumnType = Nullable.GetUnderlyingType (prop.PropertyType) ?? prop.PropertyType;
2593-
Collation = Orm.Collation (prop);
2623+
ColumnType = Nullable.GetUnderlyingType (memberType) ?? memberType;
2624+
Collation = Orm.Collation (member);
25942625

2595-
IsPK = Orm.IsPK (prop) ||
2626+
IsPK = Orm.IsPK (member) ||
25962627
(((createFlags & CreateFlags.ImplicitPK) == CreateFlags.ImplicitPK) &&
2597-
string.Compare (prop.Name, Orm.ImplicitPkName, StringComparison.OrdinalIgnoreCase) == 0);
2628+
string.Compare (member.Name, Orm.ImplicitPkName, StringComparison.OrdinalIgnoreCase) == 0);
25982629

2599-
var isAuto = Orm.IsAutoInc (prop) || (IsPK && ((createFlags & CreateFlags.AutoIncPK) == CreateFlags.AutoIncPK));
2630+
var isAuto = Orm.IsAutoInc (member) || (IsPK && ((createFlags & CreateFlags.AutoIncPK) == CreateFlags.AutoIncPK));
26002631
IsAutoGuid = isAuto && ColumnType == typeof (Guid);
26012632
IsAutoInc = isAuto && !IsAutoGuid;
26022633

2603-
Indices = Orm.GetIndices (prop);
2634+
Indices = Orm.GetIndices (member);
26042635
if (!Indices.Any ()
26052636
&& !IsPK
26062637
&& ((createFlags & CreateFlags.ImplicitIndex) == CreateFlags.ImplicitIndex)
26072638
&& Name.EndsWith (Orm.ImplicitIndexSuffix, StringComparison.OrdinalIgnoreCase)
26082639
) {
26092640
Indices = new IndexedAttribute[] { new IndexedAttribute () };
26102641
}
2611-
IsNullable = !(IsPK || Orm.IsMarkedNotNull (prop));
2612-
MaxStringLength = Orm.MaxStringLength (prop);
2642+
IsNullable = !(IsPK || Orm.IsMarkedNotNull (member));
2643+
MaxStringLength = Orm.MaxStringLength (member);
26132644

2614-
StoreAsText = prop.PropertyType.GetTypeInfo ().CustomAttributes.Any (x => x.AttributeType == typeof (StoreAsTextAttribute));
2645+
StoreAsText = memberType.GetTypeInfo ().CustomAttributes.Any (x => x.AttributeType == typeof (StoreAsTextAttribute));
26152646
}
26162647

2648+
public Column (PropertyInfo member, CreateFlags createFlags = CreateFlags.None)
2649+
: this((MemberInfo)member, createFlags)
2650+
{ }
2651+
26172652
public void SetValue (object obj, object val)
26182653
{
2619-
if (val != null && ColumnType.GetTypeInfo ().IsEnum) {
2620-
_prop.SetValue (obj, Enum.ToObject (ColumnType, val));
2654+
if(_member is PropertyInfo propy)
2655+
{
2656+
if (val != null && ColumnType.GetTypeInfo ().IsEnum)
2657+
propy.SetValue (obj, Enum.ToObject (ColumnType, val));
2658+
else
2659+
propy.SetValue (obj, val);
26212660
}
2622-
else {
2623-
_prop.SetValue (obj, val, null);
2661+
else if(_member is FieldInfo field)
2662+
{
2663+
if (val != null && ColumnType.GetTypeInfo ().IsEnum)
2664+
field.SetValue (obj, Enum.ToObject (ColumnType, val));
2665+
else
2666+
field.SetValue (obj, val);
26242667
}
2668+
else
2669+
throw new InvalidProgramException("unreachable condition");
26252670
}
26262671

26272672
public object GetValue (object obj)
26282673
{
2629-
return _prop.GetValue (obj, null);
2674+
if(_member is PropertyInfo propy)
2675+
return propy.GetValue(obj);
2676+
else if(_member is FieldInfo field)
2677+
return field.GetValue(obj);
2678+
else
2679+
throw new InvalidProgramException("unreachable condition");
26302680
}
2681+
2682+
private static Type GetMemberType(MemberInfo m)
2683+
{
2684+
switch(m.MemberType)
2685+
{
2686+
case MemberTypes.Property: return ((PropertyInfo)m).PropertyType;
2687+
case MemberTypes.Field: return ((FieldInfo)m).FieldType;
2688+
default: throw new InvalidProgramException($"{nameof(TableMapping)} supports properties or fields only.");
2689+
}
2690+
}
2691+
}
2692+
2693+
internal enum MapMethod
2694+
{
2695+
ByName,
2696+
ByPosition
26312697
}
26322698
}
26332699

@@ -2836,7 +2902,7 @@ public static IEnumerable<IndexedAttribute> GetIndices (MemberInfo p)
28362902
#endif
28372903
}
28382904

2839-
public static int? MaxStringLength (PropertyInfo p)
2905+
public static int? MaxStringLength (MemberInfo p)
28402906
{
28412907
#if ENABLE_IL2CPP
28422908
return p.GetCustomAttribute<MaxLengthAttribute> ()?.Value;
@@ -2850,6 +2916,8 @@ public static IEnumerable<IndexedAttribute> GetIndices (MemberInfo p)
28502916
#endif
28512917
}
28522918

2919+
public static int? MaxStringLength (PropertyInfo p) => MaxStringLength((MemberInfo)p);
2920+
28532921
public static bool IsMarkedNotNull (MemberInfo p)
28542922
{
28552923
return p.CustomAttributes.Any (x => x.AttributeType == typeof (NotNullAttribute));
@@ -2938,11 +3006,19 @@ public IEnumerable<T> ExecuteDeferredQuery<T> (TableMapping map)
29383006
var cols = new TableMapping.Column[SQLite3.ColumnCount (stmt)];
29393007
var fastColumnSetters = new Action<T, Sqlite3Statement, int>[SQLite3.ColumnCount (stmt)];
29403008

2941-
for (int i = 0; i < cols.Length; i++) {
2942-
var name = SQLite3.ColumnName16 (stmt, i);
2943-
cols[i] = map.FindColumn (name);
2944-
if (cols[i] != null)
2945-
fastColumnSetters[i] = FastColumnSetter.GetFastSetter<T> (_conn, cols[i]);
3009+
if (map.Method == TableMapping.MapMethod.ByPosition)
3010+
{
3011+
Array.Copy(map.Columns, cols, Math.Min(cols.Length, map.Columns.Length));
3012+
}
3013+
else if (map.Method == TableMapping.MapMethod.ByName)
3014+
{
3015+
for (int i = 0; i < cols.Length; i++)
3016+
{
3017+
var name = SQLite3.ColumnName16 (stmt, i);
3018+
cols[i] = map.FindColumn (name);
3019+
if (cols[i] != null)
3020+
fastColumnSetters[i] = FastColumnSetter.GetFastSetter<T> (_conn, cols[i]);
3021+
}
29463022
}
29473023

29483024
while (SQLite3.Step (stmt) == SQLite3.Result.Row) {

tests/SQLite.Tests/MappingTest.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,23 @@ public void OnlyKey ()
136136
db.Update (new OnlyKeyModel { MyModelId = "Foo" });
137137
var foo2 = db.Get<OnlyKeyModel> ("Foo");
138138
Assert.AreEqual (foo2.MyModelId, "Foo");
139-
}
139+
}
140+
141+
#endregion
142+
143+
#region Issue #1007
140144

145+
[Test]
146+
public void TableMapping_MapsValueTypes()
147+
{
148+
var mapping = new TableMapping(typeof( (int a, string b, double? c) ));
149+
150+
Assert.AreEqual(3, mapping.Columns.Length);
151+
Assert.AreEqual("Item1", mapping.Columns[0].Name);
152+
Assert.AreEqual("Item2", mapping.Columns[1].Name);
153+
Assert.AreEqual("Item3", mapping.Columns[2].Name);
154+
}
155+
141156
#endregion
142157
}
143158
}

tests/SQLite.Tests/QueryTest.cs

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,50 @@ namespace SQLite.Tests
1818
[TestFixture]
1919
public class QueryTest
2020
{
21+
private readonly SQLiteConnection _db = new SQLiteConnection (Path.GetTempFileName(), true);
22+
private readonly (int Value, double Walue)[] _records = new[]
23+
{
24+
(42, 0.5)
25+
};
26+
27+
public QueryTest ()
28+
{
29+
_db.Execute ("create table G(Value integer not null, Walue real not null)");
30+
31+
for (int i = 0; i < _records.Length; i++) {
32+
_db.Execute ("insert into G(Value, Walue) values (?, ?)",
33+
_records[i].Value, _records[i].Walue);
34+
}
35+
}
36+
2137
class GenericObject
2238
{
2339
public int Value { get; set; }
40+
public double Walue { get; set; }
2441
}
2542

2643
[Test]
2744
public void QueryGenericObject ()
2845
{
29-
var path = Path.GetTempFileName ();
30-
var db = new SQLiteConnection (path, true);
46+
var r = _db.Query<GenericObject> ("select * from G");
47+
48+
Assert.AreEqual (_records.Length, r.Count);
49+
Assert.AreEqual (_records[0].Value, r[0].Value);
50+
Assert.AreEqual (_records[0].Walue, r[0].Walue);
51+
}
52+
53+
#region Issue #1007
3154

32-
db.Execute ("create table G(Value integer not null)");
33-
db.Execute ("insert into G(Value) values (?)", 42);
34-
var r = db.Query<GenericObject> ("select * from G");
55+
[Test]
56+
public void QueryValueTuple()
57+
{
58+
var r = _db.Query<(int Value, double Walue)> ("select * from G");
3559

36-
Assert.AreEqual (1, r.Count);
37-
Assert.AreEqual (42, r[0].Value);
60+
Assert.AreEqual(_records.Length, r.Count);
61+
Assert.AreEqual(_records[0].Value, r[0].Value);
62+
Assert.AreEqual(_records[0].Walue, r[0].Walue);
3863
}
64+
65+
#endregion
3966
}
4067
}

0 commit comments

Comments
 (0)