Skip to content
This repository was archived by the owner on Dec 24, 2022. It is now read-only.

Commit 742f9ae

Browse files
committed
Use cloned SqlExpression in LoadSelect sub queries to avoid mutating original q
1 parent 944354a commit 742f9ae

File tree

4 files changed

+172
-24
lines changed

4 files changed

+172
-24
lines changed

src/ServiceStack.OrmLite/Expressions/SqlExpression.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1130,6 +1130,16 @@ public virtual string ToSelectStatement()
11301130
: sql;
11311131
}
11321132

1133+
/// <summary>
1134+
/// Merge params into an encapsulated SQL Statement with embedded param values
1135+
/// </summary>
1136+
public virtual string ToMergedParamsSelectStatement()
1137+
{
1138+
var sql = this.ToSelectStatement();
1139+
var mergedSql = DialectProvider.MergeParamsIntoSql(sql, Params);
1140+
return mergedSql;
1141+
}
1142+
11331143
public virtual string ToCountStatement()
11341144
{
11351145
SelectFilter?.Invoke(this);

src/ServiceStack.OrmLite/Support/LoadList.cs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,17 @@ protected LoadList(IDbCommand dbCmd, SqlExpression<From> q)
3434
this.dbCmd = dbCmd;
3535
this.q = q;
3636

37-
var sql = q.SelectInto<Into>();
38-
parentResults = dbCmd.ExprConvertToList<Into>(sql, q.Params, onlyFields:q.OnlyFields);
37+
//Use .Clone() to prevent SqlExpressionSelectFilter from adding params to original query
38+
var parentQ = q.Clone();
39+
var sql = parentQ.SelectInto<Into>();
40+
parentResults = dbCmd.ExprConvertToList<Into>(sql, parentQ.Params, onlyFields:q.OnlyFields);
3941

4042
modelDef = ModelDefinition<Into>.Definition;
4143
fieldDefs = modelDef.AllFieldDefinitionsArray.Where(x => x.IsReference).ToList();
4244

43-
subSql = dialectProvider.GetLoadChildrenSubSelect(q);
45+
var subQ = q.Clone();
46+
var subQSql = dialectProvider.GetLoadChildrenSubSelect(subQ);
47+
subSql = dialectProvider.MergeParamsIntoSql(subQSql, subQ.Params);
4448
}
4549

4650
protected string GetRefListSql(ModelDefinition refModelDef, FieldDefinition refField)
@@ -83,9 +87,10 @@ protected void SetListChildResults(FieldDefinition fieldDef, Type refType, IList
8387
protected string GetRefSelfSql(ModelDefinition modelDef, FieldDefinition refSelf, ModelDefinition refModelDef)
8488
{
8589
//Load Self Table.RefTableId PK
86-
q.Select(dialectProvider.GetQuotedColumnName(modelDef, refSelf));
90+
var refQ = q.Clone();
91+
refQ.Select(dialectProvider.GetQuotedColumnName(modelDef, refSelf));
8792

88-
var subSqlRef = q.ToSelectStatement();
93+
var subSqlRef = refQ.ToMergedParamsSelectStatement();
8994

9095
var sqlRef = $"SELECT {dialectProvider.GetColumnNames(refModelDef)} " +
9196
$"FROM {dialectProvider.GetQuotedTableName(refModelDef)} " +

tests/ServiceStack.OrmLite.Tests/Async/SqlExpressionTests.cs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,56 @@ public async Task Can_Select_as_Dictionary_Object_Async()
7676
Assert.That(result["sum"], Is.EqualTo(55));
7777
}
7878
}
79+
80+
[Test]
81+
public void Can_select_limit_on_Table_with_References()
82+
{
83+
if (Dialect == Dialect.MySql)
84+
return; //= This version of MySQL doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery'
85+
86+
if (Dialect == Dialect.SqlServer)
87+
return; // generates Windowing function "... WHERE CustomerId IN (SELECT * FROM ...)"
88+
// when should generate "... WHERE CustomerId IN (SELECT Id FROM ...)"
89+
// both on .NET and .NET Core
90+
91+
using (var db = OpenDbConnection())
92+
{
93+
CustomerOrdersUseCase.DropTables(db); //Has conflicting 'Order' table
94+
db.DropAndCreateTable<Order>();
95+
db.DropAndCreateTable<Customer>();
96+
db.DropAndCreateTable<CustomerAddress>();
97+
98+
var customer1 = LoadReferencesTests.GetCustomerWithOrders("1");
99+
db.Save(customer1, references: true);
100+
101+
var customer2 = LoadReferencesTests.GetCustomerWithOrders("2");
102+
db.Save(customer2, references: true);
103+
104+
var results = db.LoadSelect(db.From<Customer>()
105+
.OrderBy(x => x.Id)
106+
.Limit(1, 1));
107+
108+
//db.GetLastSql().Print();
109+
110+
Assert.That(results.Count, Is.EqualTo(1));
111+
Assert.That(results[0].Name, Is.EqualTo("Customer 2"));
112+
Assert.That(results[0].PrimaryAddress.AddressLine1, Is.EqualTo("2 Humpty Street"));
113+
Assert.That(results[0].Orders.Count, Is.EqualTo(2));
114+
115+
results = db.LoadSelect(db.From<Customer>()
116+
.Join<CustomerAddress>()
117+
.OrderBy(x => x.Id)
118+
.Limit(1, 1));
119+
120+
db.GetLastSql().Print();
121+
122+
Assert.That(results.Count, Is.EqualTo(1));
123+
Assert.That(results[0].Name, Is.EqualTo("Customer 2"));
124+
Assert.That(results[0].PrimaryAddress.AddressLine1, Is.EqualTo("2 Humpty Street"));
125+
Assert.That(results[0].Orders.Count, Is.EqualTo(2));
126+
}
127+
}
128+
79129
[Test]
80130
public async Task Can_select_limit_on_Table_with_References_Async()
81131
{

tests/ServiceStack.OrmLite.Tests/Issues/LoadSelectIssue.cs

Lines changed: 102 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,41 @@
1-
using NUnit.Framework;
1+
using System;
2+
using NUnit.Framework;
3+
using ServiceStack.Data;
24
using ServiceStack.DataAnnotations;
5+
using ServiceStack.Logging;
6+
using ServiceStack.OrmLite.SqlServer;
37
using ServiceStack.Text;
48

59
namespace ServiceStack.OrmLite.Tests.Issues
610
{
711
[TestFixture]
812
public class LoadSelectIssue : OrmLiteTestBase
913
{
14+
public class PlayerEquipment
15+
{
16+
public string Id => PlayerId + "/" + ItemId;
17+
18+
public int PlayerId { get; set; }
19+
20+
[References(typeof(ItemData))]
21+
public int ItemId { get; set; }
22+
23+
public int Quantity { get; set; }
24+
25+
public bool IsEquipped { get; set; }
26+
27+
[Reference]
28+
public ItemData ItemData { get; set; }
29+
}
30+
31+
public class ItemData
32+
{
33+
[AutoIncrement]
34+
public int Id { get; set; }
35+
36+
public string Data { get; set; }
37+
}
38+
1039
[Test]
1140
public void Can_LoadSelect_PlayerEquipment()
1241
{
@@ -46,33 +75,87 @@ public void Can_LoadSelect_PlayerEquipment()
4675
results.PrintDump();
4776
}
4877
}
49-
}
5078

51-
public class PlayerEquipment
52-
{
53-
public string Id
79+
[Alias("EventCategory")]
80+
public class EventCategoryTbl : IHasSoftDelete, IHasTimeStamp
5481
{
55-
get { return PlayerId + "/" + ItemId; }
82+
[PrimaryKey]
83+
public Guid EventCategoryId { get; set; }
84+
85+
86+
[System.ComponentModel.DataAnnotations.Required]
87+
public string Name { get; set; }
88+
89+
/// <summary>
90+
/// Link to the file record that contains any image related to this category
91+
/// </summary>
92+
[References(typeof(FileTbl))]
93+
public Guid LinkedImageId { get; set; }
94+
95+
[Reference]
96+
public FileTbl LinkedImage { get; set; }
97+
98+
public bool IsDeleted { get; set; }
99+
100+
[RowVersion]
101+
public ulong RowVersion { get; set; }
56102
}
57103

58-
public int PlayerId { get; set; }
104+
[Alias("File")]
105+
public class FileTbl : IHasSoftDelete, IHasTimeStamp
106+
{
107+
[PrimaryKey]
108+
public Guid FileId { get; set; }
59109

60-
[References(typeof(ItemData))]
61-
public int ItemId { get; set; }
110+
public string Name { get; set; }
62111

63-
public int Quantity { get; set; }
112+
public string Extension { get; set; }
64113

65-
public bool IsEquipped { get; set; }
114+
public long FileSizeBytes { get; set; }
66115

67-
[Reference]
68-
public ItemData ItemData { get; set; }
69-
}
116+
public bool IsDeleted { get; set; }
70117

71-
public class ItemData
72-
{
73-
[AutoIncrement]
74-
public int Id { get; set; }
118+
[RowVersion]
119+
public ulong RowVersion { get; set; }
120+
}
121+
122+
public interface IHasTimeStamp
123+
{
124+
[RowVersion]
125+
ulong RowVersion { get; set; }
126+
}
127+
128+
public interface IHasSoftDelete
129+
{
130+
bool IsDeleted { get; set; }
131+
}
132+
133+
[Test]
134+
public void Can_execute_LoadSelect_when_child_references_implement_IHasSoftDelete()
135+
{
136+
LogManager.LogFactory = new ConsoleLogFactory(debugEnabled:true);
137+
// Automatically filter out all soft deleted records, for ALL table types.
138+
OrmLiteConfig.SqlExpressionSelectFilter = q =>
139+
{
140+
if (q.ModelDef.ModelType.HasInterface(typeof(IHasSoftDelete)))
141+
{
142+
q.Where<IHasSoftDelete>(x => x.IsDeleted != true);
143+
}
144+
};
145+
146+
using (var db = OpenDbConnection())
147+
{
148+
db.DropTable<EventCategoryTbl>();
149+
db.DropTable<FileTbl>();
150+
151+
db.CreateTable<FileTbl>();
152+
db.CreateTable<EventCategoryTbl>();
153+
154+
var results = db.LoadSelect<EventCategoryTbl>();
155+
}
156+
157+
OrmLiteConfig.SqlExpressionSelectFilter = null;
158+
}
75159

76-
public string Data { get; set; }
77160
}
78161
}

0 commit comments

Comments
 (0)