com.backnd.database 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Attributes/ColumnAttribute.cs +46 -0
- package/Attributes/ColumnAttribute.cs.meta +11 -0
- package/Attributes/PrimaryKeyAttribute.cs +12 -0
- package/Attributes/PrimaryKeyAttribute.cs.meta +11 -0
- package/Attributes/TableAttribute.cs +44 -0
- package/Attributes/TableAttribute.cs.meta +11 -0
- package/Attributes.meta +8 -0
- package/BACKND.Database.asmdef +14 -0
- package/BACKND.Database.asmdef.meta +7 -0
- package/BaseModel.cs +22 -0
- package/BaseModel.cs.meta +11 -0
- package/Client.cs +490 -0
- package/Client.cs.meta +11 -0
- package/Editor/BACKND.Database.Editor.asmdef +18 -0
- package/Editor/BACKND.Database.Editor.asmdef.meta +7 -0
- package/Editor/PackageAssetInstaller.cs +251 -0
- package/Editor/PackageAssetInstaller.cs.meta +11 -0
- package/Editor.meta +8 -0
- package/Exceptions/DatabaseException.cs +34 -0
- package/Exceptions/DatabaseException.cs.meta +11 -0
- package/Exceptions.meta +8 -0
- package/Internal/ExpressionAnalyzer.cs +433 -0
- package/Internal/ExpressionAnalyzer.cs.meta +11 -0
- package/Internal/QueryTypes.cs +61 -0
- package/Internal/QueryTypes.cs.meta +11 -0
- package/Internal/SqlBuilder.cs +375 -0
- package/Internal/SqlBuilder.cs.meta +11 -0
- package/Internal/ValueFormatter.cs +103 -0
- package/Internal/ValueFormatter.cs.meta +11 -0
- package/Internal.meta +8 -0
- package/Network/DatabaseExecutor.cs +171 -0
- package/Network/DatabaseExecutor.cs.meta +11 -0
- package/Network/DatabaseRequest.cs +16 -0
- package/Network/DatabaseRequest.cs.meta +11 -0
- package/Network/DatabaseResponse.cs +81 -0
- package/Network/DatabaseResponse.cs.meta +11 -0
- package/Network.meta +8 -0
- package/QueryBuilder.cs +1001 -0
- package/QueryBuilder.cs.meta +11 -0
- package/README.md +24 -0
- package/TheBackend~/Plugins/Android/Backend.aar +0 -0
- package/TheBackend~/Plugins/Backend.dll +0 -0
- package/TheBackend~/Plugins/Editor/TheBackendMultiSettingEditor.dll +0 -0
- package/TheBackend~/Plugins/Editor/TheBackendSettingEditor.dll +0 -0
- package/TheBackend~/Plugins/LitJSON.dll +0 -0
- package/TheBackend~/Plugins/Settings/TheBackendHashKeySettings.dll +0 -0
- package/TheBackend~/Plugins/Settings/TheBackendMultiSettings.dll +0 -0
- package/TheBackend~/Plugins/Settings/TheBackendSettings.dll +0 -0
- package/Tools/BTask.cs +905 -0
- package/Tools/BTask.cs.meta +11 -0
- package/Tools/DatabaseLoop.cs +110 -0
- package/Tools/DatabaseLoop.cs.meta +11 -0
- package/Tools/JsonHelper.cs +82 -0
- package/Tools/JsonHelper.cs.meta +11 -0
- package/Tools.meta +8 -0
- package/TransactionBuilder.cs +154 -0
- package/TransactionBuilder.cs.meta +11 -0
- package/TransactionQueryBuilder.cs +205 -0
- package/TransactionQueryBuilder.cs.meta +11 -0
- package/TransactionResult.cs +33 -0
- package/TransactionResult.cs.meta +11 -0
- package/package.json +20 -0
- package/package.json.meta +7 -0
package/QueryBuilder.cs
ADDED
|
@@ -0,0 +1,1001 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Collections.Generic;
|
|
3
|
+
using System.Linq;
|
|
4
|
+
using System.Linq.Expressions;
|
|
5
|
+
using System.Text;
|
|
6
|
+
|
|
7
|
+
using BACKND.Database.Internal;
|
|
8
|
+
using BACKND.Database.Network;
|
|
9
|
+
|
|
10
|
+
using Newtonsoft.Json;
|
|
11
|
+
|
|
12
|
+
using UnityEngine;
|
|
13
|
+
|
|
14
|
+
namespace BACKND.Database
|
|
15
|
+
{
|
|
16
|
+
public class QueryBuilder<T> where T : BaseModel, new()
|
|
17
|
+
{
|
|
18
|
+
private readonly Client client;
|
|
19
|
+
private readonly T modelInstance;
|
|
20
|
+
|
|
21
|
+
private readonly List<WhereCondition> whereConditions = new();
|
|
22
|
+
private readonly List<OrderByInfo> orderByList = new();
|
|
23
|
+
private readonly List<SetClause> setClauses = new();
|
|
24
|
+
private int? limit;
|
|
25
|
+
private int? offset;
|
|
26
|
+
private bool isOfCurrentUser;
|
|
27
|
+
|
|
28
|
+
internal QueryBuilder(Client client)
|
|
29
|
+
{
|
|
30
|
+
this.client = client;
|
|
31
|
+
this.modelInstance = new();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public QueryBuilder<T> Where(Expression<Func<T, bool>> predicate)
|
|
35
|
+
{
|
|
36
|
+
var condition = AnalyzeExpression(predicate);
|
|
37
|
+
if (condition != null)
|
|
38
|
+
{
|
|
39
|
+
whereConditions.Add(condition);
|
|
40
|
+
}
|
|
41
|
+
return this;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public QueryBuilder<T> OrderBy<TKey>(Expression<Func<T, TKey>> keySelector, bool descending = false)
|
|
45
|
+
{
|
|
46
|
+
var columnName = GetColumnNameFromExpression(keySelector);
|
|
47
|
+
if (!string.IsNullOrEmpty(columnName))
|
|
48
|
+
{
|
|
49
|
+
orderByList.Add(new()
|
|
50
|
+
{
|
|
51
|
+
Column = columnName,
|
|
52
|
+
Descending = descending
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return this;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public QueryBuilder<T> OrderByDescending<TKey>(Expression<Func<T, TKey>> keySelector)
|
|
59
|
+
{
|
|
60
|
+
return OrderBy(keySelector, true);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public QueryBuilder<T> Take(int count)
|
|
64
|
+
{
|
|
65
|
+
limit = count;
|
|
66
|
+
return this;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
public QueryBuilder<T> Skip(int count)
|
|
70
|
+
{
|
|
71
|
+
offset = count;
|
|
72
|
+
return this;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
public QueryBuilder<T> OfCurrentUser()
|
|
76
|
+
{
|
|
77
|
+
isOfCurrentUser = true;
|
|
78
|
+
return this;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
public async BTask<T> First()
|
|
82
|
+
{
|
|
83
|
+
var list = await Take(1).ToList();
|
|
84
|
+
if (list.Count == 0)
|
|
85
|
+
throw new InvalidOperationException("Sequence contains no elements");
|
|
86
|
+
return list[0];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
public async BTask<T> FirstOrDefault()
|
|
90
|
+
{
|
|
91
|
+
var list = await Take(1).ToList();
|
|
92
|
+
return list.Count > 0 ? list[0] : null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
public async BTask<List<T>> ToList()
|
|
96
|
+
{
|
|
97
|
+
var query = BuildSelectQuery();
|
|
98
|
+
var parameters = GetQueryParameters();
|
|
99
|
+
|
|
100
|
+
var request = new DatabaseRequest
|
|
101
|
+
{
|
|
102
|
+
Query = query,
|
|
103
|
+
Parameters = parameters
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
var response = await client.ExecuteQuery(request);
|
|
107
|
+
return ParseResponse<T>(response);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
public async BTask<int> Count()
|
|
111
|
+
{
|
|
112
|
+
var query = BuildCountQuery();
|
|
113
|
+
var parameters = GetQueryParameters();
|
|
114
|
+
|
|
115
|
+
var request = new DatabaseRequest
|
|
116
|
+
{
|
|
117
|
+
Query = query,
|
|
118
|
+
Parameters = parameters
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
var response = await client.ExecuteQuery(request);
|
|
122
|
+
return ParseCountResponse(response);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
public async BTask<bool> Any()
|
|
126
|
+
{
|
|
127
|
+
return await Count() > 0;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
public async BTask<InsertResult> Insert(T model)
|
|
131
|
+
{
|
|
132
|
+
ValidateModel(model);
|
|
133
|
+
|
|
134
|
+
var query = BuildInsertQuery(model, out var parameters);
|
|
135
|
+
var request = new DatabaseRequest { Query = query, Parameters = parameters };
|
|
136
|
+
var response = await client.ExecuteMutation(request);
|
|
137
|
+
|
|
138
|
+
if (!response.Success)
|
|
139
|
+
throw new Exception($"Insert failed: {response.Error}");
|
|
140
|
+
|
|
141
|
+
var result = JsonConvert.DeserializeObject<InsertResult>(response.Result);
|
|
142
|
+
if (result == null)
|
|
143
|
+
throw new Exception("Failed to parse insert result");
|
|
144
|
+
|
|
145
|
+
UpdateAutoIncrementId(model, result);
|
|
146
|
+
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
public async BTask<MutationResult> Update(T model)
|
|
151
|
+
{
|
|
152
|
+
ValidateModel(model);
|
|
153
|
+
|
|
154
|
+
// WHERE 절이 없으면 PrimaryKey로 자동 생성 시도
|
|
155
|
+
if (whereConditions.Count == 0)
|
|
156
|
+
{
|
|
157
|
+
var primaryKeyColumns = model.GetPrimaryKeyColumnNames();
|
|
158
|
+
if (primaryKeyColumns.Length == 0)
|
|
159
|
+
throw new InvalidOperationException("Update requires a WHERE clause or a Primary Key");
|
|
160
|
+
|
|
161
|
+
// 복합 키 지원: 모든 PK 컬럼에 대해 WHERE 조건 생성
|
|
162
|
+
foreach (var primaryKeyColumn in primaryKeyColumns)
|
|
163
|
+
{
|
|
164
|
+
var primaryKeyValue = model.GetValue(primaryKeyColumn);
|
|
165
|
+
if (primaryKeyValue == null)
|
|
166
|
+
throw new InvalidOperationException($"Primary Key '{primaryKeyColumn}' has no value. Either set the Primary Key or provide a WHERE clause");
|
|
167
|
+
|
|
168
|
+
whereConditions.Add(new WhereCondition
|
|
169
|
+
{
|
|
170
|
+
ColumnName = primaryKeyColumn,
|
|
171
|
+
Operator = CompareOperator.Equal,
|
|
172
|
+
Value = primaryKeyValue,
|
|
173
|
+
LogicalOperator = LogicalOperator.And
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
var whereClause = BuildWhereClause();
|
|
179
|
+
var query = BuildUpdateQuery(model, whereClause, out var parameters);
|
|
180
|
+
|
|
181
|
+
foreach (var param in GetQueryParameters())
|
|
182
|
+
parameters[param.Key] = param.Value;
|
|
183
|
+
|
|
184
|
+
var request = new DatabaseRequest { Query = query, Parameters = parameters };
|
|
185
|
+
var response = await client.ExecuteMutation(request);
|
|
186
|
+
|
|
187
|
+
if (!response.Success)
|
|
188
|
+
throw new Exception($"Update failed: {response.Error}");
|
|
189
|
+
|
|
190
|
+
var result = JsonConvert.DeserializeObject<MutationResult>(response.Result);
|
|
191
|
+
if (result == null)
|
|
192
|
+
throw new Exception("Failed to parse update result");
|
|
193
|
+
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
public async BTask<MutationResult> Delete()
|
|
198
|
+
{
|
|
199
|
+
if (whereConditions.Count == 0)
|
|
200
|
+
throw new InvalidOperationException("Delete requires a WHERE clause");
|
|
201
|
+
|
|
202
|
+
var whereClause = BuildWhereClause();
|
|
203
|
+
var query = BuildDeleteQuery(whereClause);
|
|
204
|
+
var parameters = GetQueryParameters();
|
|
205
|
+
|
|
206
|
+
var request = new DatabaseRequest { Query = query, Parameters = parameters };
|
|
207
|
+
var response = await client.ExecuteMutation(request);
|
|
208
|
+
|
|
209
|
+
if (!response.Success)
|
|
210
|
+
throw new Exception($"Delete failed: {response.Error}");
|
|
211
|
+
|
|
212
|
+
var result = JsonConvert.DeserializeObject<MutationResult>(response.Result);
|
|
213
|
+
if (result == null)
|
|
214
|
+
throw new Exception("Failed to parse delete result");
|
|
215
|
+
|
|
216
|
+
return result;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
public QueryBuilder<T> Inc<TField>(Expression<Func<T, TField>> selector, TField value)
|
|
220
|
+
{
|
|
221
|
+
var columnName = GetColumnNameFromExpression(selector);
|
|
222
|
+
ValidateNoDuplicateSetClause(columnName);
|
|
223
|
+
setClauses.Add(new SetClause
|
|
224
|
+
{
|
|
225
|
+
ColumnName = columnName,
|
|
226
|
+
Operator = "+",
|
|
227
|
+
Value = value
|
|
228
|
+
});
|
|
229
|
+
return this;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
public QueryBuilder<T> Dec<TField>(Expression<Func<T, TField>> selector, TField value)
|
|
233
|
+
{
|
|
234
|
+
var columnName = GetColumnNameFromExpression(selector);
|
|
235
|
+
ValidateNoDuplicateSetClause(columnName);
|
|
236
|
+
setClauses.Add(new SetClause
|
|
237
|
+
{
|
|
238
|
+
ColumnName = columnName,
|
|
239
|
+
Operator = "-",
|
|
240
|
+
Value = value
|
|
241
|
+
});
|
|
242
|
+
return this;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private void ValidateNoDuplicateSetClause(string columnName)
|
|
246
|
+
{
|
|
247
|
+
if (setClauses.Any(c => c.ColumnName == columnName))
|
|
248
|
+
throw new InvalidOperationException($"'{columnName}' is already being modified. Each field can only be modified once.");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
public async BTask<MutationResult> Exec()
|
|
252
|
+
{
|
|
253
|
+
if (setClauses.Count == 0)
|
|
254
|
+
throw new InvalidOperationException("No modifications specified. Use Inc() or Dec() before Exec()");
|
|
255
|
+
|
|
256
|
+
if (whereConditions.Count == 0)
|
|
257
|
+
throw new InvalidOperationException("Exec requires a Where() condition");
|
|
258
|
+
|
|
259
|
+
var whereClause = BuildWhereClause();
|
|
260
|
+
var query = BuildUpdateQueryFromSetClauses(whereClause);
|
|
261
|
+
var parameters = GetQueryParameters();
|
|
262
|
+
|
|
263
|
+
var request = new DatabaseRequest { Query = query, Parameters = parameters };
|
|
264
|
+
var response = await client.ExecuteMutation(request);
|
|
265
|
+
|
|
266
|
+
if (!response.Success)
|
|
267
|
+
throw new Exception($"Update failed: {response.Error}");
|
|
268
|
+
|
|
269
|
+
var result = JsonConvert.DeserializeObject<MutationResult>(response.Result);
|
|
270
|
+
if (result == null)
|
|
271
|
+
throw new Exception("Failed to parse update result");
|
|
272
|
+
|
|
273
|
+
return result;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private string BuildUpdateQueryFromSetClauses(string whereClause)
|
|
277
|
+
{
|
|
278
|
+
var setStatements = setClauses.Select(clause =>
|
|
279
|
+
$"{clause.ColumnName} = {clause.ColumnName} {clause.Operator} {FormatValueForQuery(clause.Value)}"
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
return $"UPDATE {modelInstance.GetTableName()} SET {string.Join(", ", setStatements)} WHERE {whereClause}";
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private WhereCondition AnalyzeExpression(Expression<Func<T, bool>> expression)
|
|
286
|
+
{
|
|
287
|
+
try
|
|
288
|
+
{
|
|
289
|
+
var result = AnalyzeBinaryExpression(expression.Body);
|
|
290
|
+
if (result == null && Application.platform == RuntimePlatform.WebGLPlayer)
|
|
291
|
+
{
|
|
292
|
+
Debug.LogWarning("Complex expression analysis failed. Consider using simpler conditions or multiple Where() calls.");
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return result;
|
|
296
|
+
}
|
|
297
|
+
catch (Exception ex)
|
|
298
|
+
{
|
|
299
|
+
Debug.LogWarning($"Expression analysis failed: {ex.Message}. " +
|
|
300
|
+
$"Platform: {Application.platform}. " +
|
|
301
|
+
"Falling back to runtime evaluation may not work. " +
|
|
302
|
+
"Consider using multiple Where() calls instead.");
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
private WhereCondition AnalyzeBinaryExpression(Expression expression)
|
|
308
|
+
{
|
|
309
|
+
if (expression is BinaryExpression binaryExpr)
|
|
310
|
+
{
|
|
311
|
+
if (binaryExpr.NodeType == ExpressionType.AndAlso || binaryExpr.NodeType == ExpressionType.OrElse)
|
|
312
|
+
{
|
|
313
|
+
var leftCondition = AnalyzeBinaryExpression(binaryExpr.Left);
|
|
314
|
+
var rightCondition = AnalyzeBinaryExpression(binaryExpr.Right);
|
|
315
|
+
|
|
316
|
+
if (leftCondition != null && rightCondition != null)
|
|
317
|
+
{
|
|
318
|
+
if (binaryExpr.NodeType == ExpressionType.OrElse)
|
|
319
|
+
{
|
|
320
|
+
rightCondition.LogicalOperator = LogicalOperator.Or;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
whereConditions.Add(leftCondition);
|
|
324
|
+
return rightCondition;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (IsComparisonOperator(binaryExpr.NodeType))
|
|
329
|
+
{
|
|
330
|
+
var columnName = GetColumnNameFromMemberExpression(binaryExpr.Left);
|
|
331
|
+
var value = GetConstantValue(binaryExpr.Right);
|
|
332
|
+
var op = GetCompareOperator(binaryExpr.NodeType);
|
|
333
|
+
|
|
334
|
+
return new()
|
|
335
|
+
{
|
|
336
|
+
ColumnName = columnName,
|
|
337
|
+
Operator = op,
|
|
338
|
+
Value = value,
|
|
339
|
+
LogicalOperator = LogicalOperator.And
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
throw new NotSupportedException($"Binary operator {binaryExpr.NodeType} not yet supported");
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (expression is MethodCallExpression methodCall)
|
|
347
|
+
{
|
|
348
|
+
return AnalyzeMethodCall(methodCall);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (expression is UnaryExpression unaryExpr)
|
|
352
|
+
{
|
|
353
|
+
if (unaryExpr.NodeType == ExpressionType.Not)
|
|
354
|
+
{
|
|
355
|
+
var innerCondition = AnalyzeBinaryExpression(unaryExpr.Operand);
|
|
356
|
+
if (innerCondition != null)
|
|
357
|
+
{
|
|
358
|
+
innerCondition.Operator = GetNegatedOperator(innerCondition.Operator);
|
|
359
|
+
return innerCondition;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
throw new NotSupportedException($"Expression type {expression.NodeType} is not supported");
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private WhereCondition AnalyzeMethodCall(MethodCallExpression methodCall)
|
|
368
|
+
{
|
|
369
|
+
if (methodCall.Method.Name == "Contains" && methodCall.Method.DeclaringType == typeof(string))
|
|
370
|
+
{
|
|
371
|
+
throw new NotSupportedException("String.Contains is not supported because server doesn't support LIKE operator. Use exact equality comparison instead.");
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (methodCall.Method.Name == "Contains" && methodCall.Object != null)
|
|
375
|
+
{
|
|
376
|
+
var columnName = GetColumnNameFromMemberExpression(methodCall.Arguments[0]);
|
|
377
|
+
var values = GetConstantValue(methodCall.Object);
|
|
378
|
+
|
|
379
|
+
return new()
|
|
380
|
+
{
|
|
381
|
+
ColumnName = columnName,
|
|
382
|
+
Operator = CompareOperator.In,
|
|
383
|
+
Value = values,
|
|
384
|
+
LogicalOperator = LogicalOperator.And
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
throw new NotSupportedException($"Method {methodCall.Method.Name} is not supported");
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
private string GetColumnNameFromMemberExpression(Expression expression)
|
|
392
|
+
{
|
|
393
|
+
if (expression is MemberExpression memberExpr && memberExpr.Expression?.NodeType == ExpressionType.Parameter)
|
|
394
|
+
{
|
|
395
|
+
if (modelInstance is BaseModel baseModel)
|
|
396
|
+
{
|
|
397
|
+
return baseModel.GetColumnName(memberExpr.Member.Name);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
throw new NotSupportedException("Only simple property access is supported (e.g., x => x.PropertyName)");
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
private string GetColumnNameFromExpression<TKey>(Expression<Func<T, TKey>> keySelector)
|
|
405
|
+
{
|
|
406
|
+
return GetColumnNameFromMemberExpression(keySelector.Body);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
private object GetConstantValue(Expression expression)
|
|
410
|
+
{
|
|
411
|
+
if (expression is ConstantExpression constantExpr)
|
|
412
|
+
{
|
|
413
|
+
return constantExpr.Value;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (expression is MemberExpression memberExpr)
|
|
417
|
+
{
|
|
418
|
+
if (memberExpr.Expression is ConstantExpression containerExpr)
|
|
419
|
+
{
|
|
420
|
+
var container = containerExpr.Value;
|
|
421
|
+
if (container == null) return null;
|
|
422
|
+
|
|
423
|
+
if (memberExpr.Member is System.Reflection.FieldInfo field)
|
|
424
|
+
{
|
|
425
|
+
return field.GetValue(container);
|
|
426
|
+
}
|
|
427
|
+
if (memberExpr.Member is System.Reflection.PropertyInfo property)
|
|
428
|
+
{
|
|
429
|
+
return property.GetValue(container);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (memberExpr.Expression == null && memberExpr.Member is System.Reflection.FieldInfo staticField)
|
|
434
|
+
{
|
|
435
|
+
return staticField.GetValue(null);
|
|
436
|
+
}
|
|
437
|
+
if (memberExpr.Expression == null && memberExpr.Member is System.Reflection.PropertyInfo staticProperty)
|
|
438
|
+
{
|
|
439
|
+
return staticProperty.GetValue(null);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (memberExpr.Expression is MemberExpression nestedMember)
|
|
443
|
+
{
|
|
444
|
+
var parentValue = GetConstantValue(nestedMember);
|
|
445
|
+
if (parentValue == null) return null;
|
|
446
|
+
|
|
447
|
+
if (memberExpr.Member is System.Reflection.FieldInfo field)
|
|
448
|
+
{
|
|
449
|
+
return field.GetValue(parentValue);
|
|
450
|
+
}
|
|
451
|
+
if (memberExpr.Member is System.Reflection.PropertyInfo property)
|
|
452
|
+
{
|
|
453
|
+
return property.GetValue(parentValue);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (expression is UnaryExpression unaryExpr)
|
|
459
|
+
{
|
|
460
|
+
if (unaryExpr.NodeType == ExpressionType.Convert || unaryExpr.NodeType == ExpressionType.ConvertChecked)
|
|
461
|
+
{
|
|
462
|
+
var innerValue = GetConstantValue(unaryExpr.Operand);
|
|
463
|
+
if (innerValue != null && innerValue.GetType().IsEnum)
|
|
464
|
+
{
|
|
465
|
+
return Convert.ToInt32(innerValue);
|
|
466
|
+
}
|
|
467
|
+
return innerValue;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (unaryExpr.NodeType == ExpressionType.Quote)
|
|
471
|
+
{
|
|
472
|
+
return GetConstantValue(unaryExpr.Operand);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (expression is MethodCallExpression methodCallExpr)
|
|
477
|
+
{
|
|
478
|
+
try
|
|
479
|
+
{
|
|
480
|
+
if (methodCallExpr.Object == null)
|
|
481
|
+
{
|
|
482
|
+
var args = methodCallExpr.Arguments.Select(GetConstantValue).ToArray();
|
|
483
|
+
return methodCallExpr.Method.Invoke(null, args);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
var instance = GetConstantValue(methodCallExpr.Object);
|
|
487
|
+
if (instance != null)
|
|
488
|
+
{
|
|
489
|
+
var args = methodCallExpr.Arguments.Select(GetConstantValue).ToArray();
|
|
490
|
+
return methodCallExpr.Method.Invoke(instance, args);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
catch (Exception ex)
|
|
494
|
+
{
|
|
495
|
+
throw new NotSupportedException(
|
|
496
|
+
$"Method call {methodCallExpr.Method.Name} cannot be evaluated at compile time. " +
|
|
497
|
+
$"Platform: {Application.platform}. Use local variables instead.", ex);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (expression is NewExpression newExpr)
|
|
502
|
+
{
|
|
503
|
+
try
|
|
504
|
+
{
|
|
505
|
+
var args = newExpr.Arguments.Select(GetConstantValue).ToArray();
|
|
506
|
+
return Activator.CreateInstance(newExpr.Type, args);
|
|
507
|
+
}
|
|
508
|
+
catch (Exception ex)
|
|
509
|
+
{
|
|
510
|
+
throw new NotSupportedException(
|
|
511
|
+
$"Cannot create instance of {newExpr.Type.Name} on platform {Application.platform}. " +
|
|
512
|
+
"Use local variables instead.", ex);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (expression is ConditionalExpression conditionalExpr)
|
|
517
|
+
{
|
|
518
|
+
var testValue = GetConstantValue(conditionalExpr.Test);
|
|
519
|
+
if (testValue is bool testBool)
|
|
520
|
+
{
|
|
521
|
+
return testBool
|
|
522
|
+
? GetConstantValue(conditionalExpr.IfTrue)
|
|
523
|
+
: GetConstantValue(conditionalExpr.IfFalse);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (expression is BinaryExpression binaryExpr && !IsComparisonOperator(binaryExpr.NodeType))
|
|
528
|
+
{
|
|
529
|
+
var left = GetConstantValue(binaryExpr.Left);
|
|
530
|
+
var right = GetConstantValue(binaryExpr.Right);
|
|
531
|
+
|
|
532
|
+
return binaryExpr.NodeType switch
|
|
533
|
+
{
|
|
534
|
+
ExpressionType.Add => ExpressionAnalyzer.AddValues(left, right),
|
|
535
|
+
ExpressionType.Subtract => ExpressionAnalyzer.SubtractValues(left, right),
|
|
536
|
+
ExpressionType.Multiply => ExpressionAnalyzer.MultiplyValues(left, right),
|
|
537
|
+
ExpressionType.Divide => ExpressionAnalyzer.DivideValues(left, right),
|
|
538
|
+
ExpressionType.Modulo => ExpressionAnalyzer.ModuloValues(left, right),
|
|
539
|
+
ExpressionType.Coalesce => left ?? right,
|
|
540
|
+
_ => throw new NotSupportedException($"Binary operator {binaryExpr.NodeType} not supported in constant evaluation")
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
throw new NotSupportedException(
|
|
545
|
+
$"Expression {expression.NodeType} cannot be evaluated at compile time. " +
|
|
546
|
+
"Use local variables: var id = 5; Where(x => x.Id == id)");
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
private string BuildSelectQuery()
|
|
550
|
+
{
|
|
551
|
+
var sb = new StringBuilder();
|
|
552
|
+
sb.Append("SELECT ");
|
|
553
|
+
sb.Append(modelInstance.GetColumnList());
|
|
554
|
+
sb.Append($" FROM {modelInstance.GetTableName()}");
|
|
555
|
+
|
|
556
|
+
var whereClause = BuildWhereClause();
|
|
557
|
+
if (!string.IsNullOrEmpty(whereClause))
|
|
558
|
+
sb.Append($" WHERE {whereClause}");
|
|
559
|
+
|
|
560
|
+
var orderByClause = BuildOrderByClause();
|
|
561
|
+
if (!string.IsNullOrEmpty(orderByClause))
|
|
562
|
+
sb.Append($" ORDER BY {orderByClause}");
|
|
563
|
+
|
|
564
|
+
if (limit.HasValue)
|
|
565
|
+
sb.Append($" LIMIT {limit.Value}");
|
|
566
|
+
|
|
567
|
+
if (offset.HasValue)
|
|
568
|
+
sb.Append($" OFFSET {offset.Value}");
|
|
569
|
+
|
|
570
|
+
return sb.ToString();
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
private string BuildCountQuery()
|
|
574
|
+
{
|
|
575
|
+
var sb = new StringBuilder();
|
|
576
|
+
sb.Append($"SELECT COUNT(1) FROM {modelInstance.GetTableName()}");
|
|
577
|
+
|
|
578
|
+
var whereClause = BuildWhereClause();
|
|
579
|
+
if (!string.IsNullOrEmpty(whereClause))
|
|
580
|
+
sb.Append($" WHERE {whereClause}");
|
|
581
|
+
|
|
582
|
+
return sb.ToString();
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
private string BuildWhereClause()
|
|
586
|
+
{
|
|
587
|
+
if (whereConditions.Count == 0 && !isOfCurrentUser)
|
|
588
|
+
return null;
|
|
589
|
+
|
|
590
|
+
var sb = new StringBuilder();
|
|
591
|
+
|
|
592
|
+
var hasUserUuidCondition = whereConditions.Any(c => c.ColumnName == "user_uuid");
|
|
593
|
+
if (isOfCurrentUser && modelInstance.GetTableType() == TableType.UserTable && !hasUserUuidCondition)
|
|
594
|
+
{
|
|
595
|
+
sb.Append("user_uuid = @current_user_uuid");
|
|
596
|
+
if (whereConditions.Count > 0)
|
|
597
|
+
sb.Append(" AND ");
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
for (int i = 0; i < whereConditions.Count; i++)
|
|
601
|
+
{
|
|
602
|
+
var condition = whereConditions[i];
|
|
603
|
+
|
|
604
|
+
if (condition.IsGroupStart)
|
|
605
|
+
{
|
|
606
|
+
sb.Append("(");
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (i > 0 && !condition.IsGroupStart)
|
|
610
|
+
{
|
|
611
|
+
sb.Append(condition.LogicalOperator == LogicalOperator.And ? " AND " : " OR ");
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
sb.Append(BuildConditionString(condition));
|
|
615
|
+
|
|
616
|
+
if (condition.IsGroupEnd)
|
|
617
|
+
{
|
|
618
|
+
sb.Append(")");
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
return sb.ToString();
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
private string BuildConditionString(WhereCondition condition)
|
|
626
|
+
{
|
|
627
|
+
// NULL 비교 시 자동으로 IS NULL / IS NOT NULL로 변환
|
|
628
|
+
if (condition.Value == null)
|
|
629
|
+
{
|
|
630
|
+
return condition.Operator switch
|
|
631
|
+
{
|
|
632
|
+
CompareOperator.Equal => $"{condition.ColumnName} IS NULL",
|
|
633
|
+
CompareOperator.NotEqual => $"{condition.ColumnName} IS NOT NULL",
|
|
634
|
+
CompareOperator.IsNull => $"{condition.ColumnName} IS NULL",
|
|
635
|
+
CompareOperator.IsNotNull => $"{condition.ColumnName} IS NOT NULL",
|
|
636
|
+
_ => throw new NotSupportedException($"NULL comparison with operator {condition.Operator} is not supported. Use 'IS NULL' or 'IS NOT NULL'")
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
return condition.Operator switch
|
|
641
|
+
{
|
|
642
|
+
CompareOperator.Equal => $"{condition.ColumnName} = {FormatValueForQuery(condition.Value)}",
|
|
643
|
+
CompareOperator.NotEqual => $"{condition.ColumnName} != {FormatValueForQuery(condition.Value)}",
|
|
644
|
+
CompareOperator.GreaterThan => $"{condition.ColumnName} > {FormatValueForQuery(condition.Value)}",
|
|
645
|
+
CompareOperator.GreaterThanOrEqual => $"{condition.ColumnName} >= {FormatValueForQuery(condition.Value)}",
|
|
646
|
+
CompareOperator.LessThan => $"{condition.ColumnName} < {FormatValueForQuery(condition.Value)}",
|
|
647
|
+
CompareOperator.LessThanOrEqual => $"{condition.ColumnName} <= {FormatValueForQuery(condition.Value)}",
|
|
648
|
+
CompareOperator.Between => $"{condition.ColumnName} BETWEEN {FormatValueForQuery(condition.Value)} AND {FormatValueForQuery(condition.SecondValue)}",
|
|
649
|
+
CompareOperator.In => BuildInClause(condition),
|
|
650
|
+
CompareOperator.IsNull => $"{condition.ColumnName} IS NULL",
|
|
651
|
+
CompareOperator.IsNotNull => $"{condition.ColumnName} IS NOT NULL",
|
|
652
|
+
_ => throw new NotSupportedException($"Operator {condition.Operator} is not supported")
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
private string BuildInClause(WhereCondition condition)
|
|
657
|
+
{
|
|
658
|
+
if (condition.Value is Array array)
|
|
659
|
+
{
|
|
660
|
+
var values = new List<string>();
|
|
661
|
+
foreach (var item in array)
|
|
662
|
+
{
|
|
663
|
+
values.Add(FormatValueForQuery(item));
|
|
664
|
+
}
|
|
665
|
+
return $"{condition.ColumnName} IN ({string.Join(", ", values)})";
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
return $"{condition.ColumnName} = {FormatValueForQuery(condition.Value)}";
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
private string BuildOrderByClause()
|
|
672
|
+
{
|
|
673
|
+
if (orderByList.Count == 0)
|
|
674
|
+
return null;
|
|
675
|
+
|
|
676
|
+
var clauses = orderByList.Select(o => $"{o.Column} {(o.Descending ? "DESC" : "ASC")}");
|
|
677
|
+
return string.Join(", ", clauses);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
private bool IsComparisonOperator(ExpressionType nodeType)
|
|
681
|
+
{
|
|
682
|
+
return nodeType == ExpressionType.Equal ||
|
|
683
|
+
nodeType == ExpressionType.NotEqual ||
|
|
684
|
+
nodeType == ExpressionType.GreaterThan ||
|
|
685
|
+
nodeType == ExpressionType.GreaterThanOrEqual ||
|
|
686
|
+
nodeType == ExpressionType.LessThan ||
|
|
687
|
+
nodeType == ExpressionType.LessThanOrEqual;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
private CompareOperator GetCompareOperator(ExpressionType nodeType)
|
|
691
|
+
{
|
|
692
|
+
return nodeType switch
|
|
693
|
+
{
|
|
694
|
+
ExpressionType.Equal => CompareOperator.Equal,
|
|
695
|
+
ExpressionType.NotEqual => CompareOperator.NotEqual,
|
|
696
|
+
ExpressionType.GreaterThan => CompareOperator.GreaterThan,
|
|
697
|
+
ExpressionType.GreaterThanOrEqual => CompareOperator.GreaterThanOrEqual,
|
|
698
|
+
ExpressionType.LessThan => CompareOperator.LessThan,
|
|
699
|
+
ExpressionType.LessThanOrEqual => CompareOperator.LessThanOrEqual,
|
|
700
|
+
_ => throw new NotSupportedException($"Operation {nodeType} is not supported")
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
private CompareOperator GetNegatedOperator(CompareOperator originalOperator)
|
|
705
|
+
{
|
|
706
|
+
return originalOperator switch
|
|
707
|
+
{
|
|
708
|
+
CompareOperator.Equal => CompareOperator.NotEqual,
|
|
709
|
+
CompareOperator.NotEqual => CompareOperator.Equal,
|
|
710
|
+
CompareOperator.GreaterThan => CompareOperator.LessThanOrEqual,
|
|
711
|
+
CompareOperator.GreaterThanOrEqual => CompareOperator.LessThan,
|
|
712
|
+
CompareOperator.LessThan => CompareOperator.GreaterThanOrEqual,
|
|
713
|
+
CompareOperator.LessThanOrEqual => CompareOperator.GreaterThan,
|
|
714
|
+
CompareOperator.IsNull => CompareOperator.IsNotNull,
|
|
715
|
+
CompareOperator.IsNotNull => CompareOperator.IsNull,
|
|
716
|
+
_ => throw new NotSupportedException($"Cannot negate operator {originalOperator}")
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
private static readonly HashSet<string> SqlFunctionsAndKeywords = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
|
722
|
+
{
|
|
723
|
+
"NOW()", "CURRENT_TIMESTAMP", "CURRENT_TIMESTAMP()", "UUID()",
|
|
724
|
+
"CURRENT_DATE", "CURRENT_DATE()", "CURRENT_TIME", "CURRENT_TIME()",
|
|
725
|
+
"NULL"
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
private static bool IsSqlFunctionOrKeyword(string value)
|
|
729
|
+
{
|
|
730
|
+
if (string.IsNullOrWhiteSpace(value)) return false;
|
|
731
|
+
return SqlFunctionsAndKeywords.Contains(value.Trim());
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
private static string FormatValueForQuery(object value)
|
|
735
|
+
{
|
|
736
|
+
if (value == null) return "NULL";
|
|
737
|
+
|
|
738
|
+
// 값을 문자열로 변환
|
|
739
|
+
var stringValue = ConvertValueToString(value);
|
|
740
|
+
|
|
741
|
+
// SQL 함수 및 키워드는 따옴표 없이 그대로 반환
|
|
742
|
+
if (IsSqlFunctionOrKeyword(stringValue))
|
|
743
|
+
{
|
|
744
|
+
return stringValue.Trim();
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// 길이 제한 검사
|
|
748
|
+
const int maxLength = 1024 * 12; // 12KB
|
|
749
|
+
if (stringValue.Length > maxLength)
|
|
750
|
+
{
|
|
751
|
+
throw new ArgumentException($"Value too long. Maximum length is {maxLength} characters, but got {stringValue.Length}");
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// 함수와 NULL을 제외한 모든 값은 따옴표로 감싸기
|
|
755
|
+
return $"'{stringValue.Replace("'", "''").Replace("\\", "\\\\")}'";
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
private static string ConvertValueToString(object value)
|
|
759
|
+
{
|
|
760
|
+
if (value == null) return "NULL";
|
|
761
|
+
|
|
762
|
+
var valueType = value.GetType();
|
|
763
|
+
|
|
764
|
+
// DateTime → ISO 8601 포맷
|
|
765
|
+
if (valueType == typeof(DateTime))
|
|
766
|
+
return ((DateTime)value).ToString("yyyy-MM-ddTHH:mm:ss.fffZ");
|
|
767
|
+
|
|
768
|
+
// bool → 소문자
|
|
769
|
+
if (valueType == typeof(bool))
|
|
770
|
+
return ((bool)value) ? "true" : "false";
|
|
771
|
+
|
|
772
|
+
// Enum → 숫자
|
|
773
|
+
if (valueType.IsEnum)
|
|
774
|
+
return ((int)value).ToString();
|
|
775
|
+
|
|
776
|
+
// 복합 객체 → JSON
|
|
777
|
+
if (!valueType.IsPrimitive && valueType != typeof(string) && valueType != typeof(Guid))
|
|
778
|
+
{
|
|
779
|
+
try
|
|
780
|
+
{
|
|
781
|
+
return JsonConvert.SerializeObject(value, new JsonSerializerSettings
|
|
782
|
+
{
|
|
783
|
+
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
|
|
784
|
+
NullValueHandling = NullValueHandling.Ignore,
|
|
785
|
+
DefaultValueHandling = DefaultValueHandling.Ignore,
|
|
786
|
+
Formatting = Formatting.None
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
catch (JsonException ex)
|
|
790
|
+
{
|
|
791
|
+
throw new InvalidOperationException($"Failed to serialize object of type {valueType.Name} to JSON", ex);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// 기본: ToString()
|
|
796
|
+
return value.ToString();
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
private Dictionary<string, object> GetQueryParameters()
|
|
800
|
+
{
|
|
801
|
+
var parameters = new Dictionary<string, object>();
|
|
802
|
+
|
|
803
|
+
if (isOfCurrentUser && modelInstance.GetTableType() == TableType.UserTable && client.UserUUID != null)
|
|
804
|
+
{
|
|
805
|
+
parameters["@current_user_uuid"] = client.UserUUID;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
return parameters;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
private void ValidateModel(T model)
|
|
812
|
+
{
|
|
813
|
+
if (model == null)
|
|
814
|
+
throw new ArgumentNullException(nameof(model));
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
private string BuildInsertQuery(T model, out Dictionary<string, object> parameters)
|
|
818
|
+
{
|
|
819
|
+
parameters = new();
|
|
820
|
+
var columns = new List<string>();
|
|
821
|
+
var values = new List<string>();
|
|
822
|
+
|
|
823
|
+
var columnList = modelInstance.GetColumnList().Split(',').Select(c => c.Trim()).ToArray();
|
|
824
|
+
var autoIncrementColumn = model.GetAutoIncrementColumnName();
|
|
825
|
+
|
|
826
|
+
foreach (var columnName in columnList)
|
|
827
|
+
{
|
|
828
|
+
// AutoIncrement 컬럼은 INSERT에서 제외
|
|
829
|
+
if (!string.IsNullOrEmpty(autoIncrementColumn) &&
|
|
830
|
+
autoIncrementColumn.Equals(columnName, StringComparison.OrdinalIgnoreCase))
|
|
831
|
+
continue;
|
|
832
|
+
|
|
833
|
+
var value = model.GetValue(columnName);
|
|
834
|
+
var isNullableType = model.IsPropertyNullableType(columnName);
|
|
835
|
+
var isColumnNullable = model.IsColumnNullable(columnName);
|
|
836
|
+
|
|
837
|
+
// Nullable<T> 타입(int?, DateTime? 등)이고 값이 null인 경우
|
|
838
|
+
if (isNullableType && value == null)
|
|
839
|
+
{
|
|
840
|
+
if (isColumnNullable)
|
|
841
|
+
{
|
|
842
|
+
// NULL 허용 컬럼이면 NULL 값으로 INSERT
|
|
843
|
+
columns.Add(columnName);
|
|
844
|
+
values.Add("NULL");
|
|
845
|
+
continue;
|
|
846
|
+
}
|
|
847
|
+
else
|
|
848
|
+
{
|
|
849
|
+
// NotNull 컬럼인데 값이 null이면 기본값 확인
|
|
850
|
+
var defaultValue = model.GetColumnDefaultValue(columnName);
|
|
851
|
+
if (!string.IsNullOrEmpty(defaultValue))
|
|
852
|
+
continue;
|
|
853
|
+
|
|
854
|
+
throw new InvalidOperationException($"Column '{columnName}' cannot be null");
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// 참조 타입(string, class 등)이 null인 경우
|
|
859
|
+
if (!isColumnNullable && value == null)
|
|
860
|
+
{
|
|
861
|
+
var defaultValue = model.GetColumnDefaultValue(columnName);
|
|
862
|
+
if (!string.IsNullOrEmpty(defaultValue))
|
|
863
|
+
continue;
|
|
864
|
+
|
|
865
|
+
throw new InvalidOperationException($"Column '{columnName}' cannot be null");
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
columns.Add(columnName);
|
|
869
|
+
values.Add(FormatValueForQuery(value));
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
if (columns.Count == 0)
|
|
873
|
+
throw new InvalidOperationException("No columns to insert");
|
|
874
|
+
|
|
875
|
+
return $"INSERT INTO {modelInstance.GetTableName()} ({string.Join(", ", columns)}) VALUES ({string.Join(", ", values)})";
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
private string BuildUpdateQuery(T model, string whereClause, out Dictionary<string, object> parameters)
|
|
879
|
+
{
|
|
880
|
+
parameters = new();
|
|
881
|
+
var setClauses = new List<string>();
|
|
882
|
+
|
|
883
|
+
var columnList = modelInstance.GetColumnList().Split(',').Select(c => c.Trim()).ToArray();
|
|
884
|
+
var primaryKeyColumns = model.GetPrimaryKeyColumnNames();
|
|
885
|
+
|
|
886
|
+
foreach (var columnName in columnList)
|
|
887
|
+
{
|
|
888
|
+
// PK 컬럼은 UPDATE SET 절에서 제외
|
|
889
|
+
if (primaryKeyColumns.Any(pk => pk.Equals(columnName, StringComparison.OrdinalIgnoreCase)))
|
|
890
|
+
continue;
|
|
891
|
+
|
|
892
|
+
var value = model.GetValue(columnName);
|
|
893
|
+
var isNullableType = model.IsPropertyNullableType(columnName);
|
|
894
|
+
var isColumnNullable = model.IsColumnNullable(columnName);
|
|
895
|
+
|
|
896
|
+
// Nullable<T> 타입(int?, DateTime? 등)이고 값이 null인 경우
|
|
897
|
+
if (isNullableType && value == null)
|
|
898
|
+
{
|
|
899
|
+
if (isColumnNullable)
|
|
900
|
+
{
|
|
901
|
+
// NULL 허용 컬럼이면 NULL로 UPDATE
|
|
902
|
+
setClauses.Add($"{columnName} = NULL");
|
|
903
|
+
continue;
|
|
904
|
+
}
|
|
905
|
+
else
|
|
906
|
+
{
|
|
907
|
+
throw new InvalidOperationException($"Column '{columnName}' cannot be null");
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// 참조 타입(string, class 등)이 null인 경우
|
|
912
|
+
if (!isColumnNullable && value == null)
|
|
913
|
+
throw new InvalidOperationException($"Column '{columnName}' cannot be null");
|
|
914
|
+
|
|
915
|
+
setClauses.Add($"{columnName} = {FormatValueForQuery(value)}");
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
if (setClauses.Count == 0)
|
|
919
|
+
throw new InvalidOperationException("No columns to update");
|
|
920
|
+
|
|
921
|
+
return $"UPDATE {modelInstance.GetTableName()} SET {string.Join(", ", setClauses)} WHERE {whereClause}";
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
private string BuildDeleteQuery(string whereClause)
|
|
925
|
+
{
|
|
926
|
+
return $"DELETE FROM {modelInstance.GetTableName()} WHERE {whereClause}";
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
private void UpdateAutoIncrementId(T model, InsertResult result)
|
|
930
|
+
{
|
|
931
|
+
var autoIncrementColumn = model.GetAutoIncrementColumnName();
|
|
932
|
+
if (string.IsNullOrEmpty(autoIncrementColumn))
|
|
933
|
+
return;
|
|
934
|
+
|
|
935
|
+
try
|
|
936
|
+
{
|
|
937
|
+
if (result?.LastInsertId != null)
|
|
938
|
+
{
|
|
939
|
+
model.SetValue(autoIncrementColumn, result.LastInsertId);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
catch (Exception ex)
|
|
943
|
+
{
|
|
944
|
+
Debug.LogWarning($"Failed to update auto-increment ID: {ex.Message}");
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
private List<TModel> ParseResponse<TModel>(Response response) where TModel : BaseModel, new()
|
|
949
|
+
{
|
|
950
|
+
if (!response.Success)
|
|
951
|
+
throw new Exception($"Query failed: {response.Error}");
|
|
952
|
+
|
|
953
|
+
var result = JsonConvert.DeserializeObject<QueryResult>(response.Result);
|
|
954
|
+
if (result?.Data == null) return new List<TModel>();
|
|
955
|
+
|
|
956
|
+
var list = new List<TModel>();
|
|
957
|
+
foreach (var row in result.Data)
|
|
958
|
+
{
|
|
959
|
+
try
|
|
960
|
+
{
|
|
961
|
+
var model = new TModel();
|
|
962
|
+
foreach (var kvp in row)
|
|
963
|
+
{
|
|
964
|
+
try
|
|
965
|
+
{
|
|
966
|
+
model.SetValue(kvp.Key, kvp.Value);
|
|
967
|
+
}
|
|
968
|
+
catch (Exception ex)
|
|
969
|
+
{
|
|
970
|
+
Debug.LogWarning($"Failed to set value for column '{kvp.Key}', value: '{kvp.Value}'. Error: {ex.Message}");
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
list.Add(model);
|
|
974
|
+
}
|
|
975
|
+
catch (Exception ex)
|
|
976
|
+
{
|
|
977
|
+
Debug.LogError($"Row parsing failed: {ex.Message}");
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
return list;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
private int ParseCountResponse(Response response)
|
|
984
|
+
{
|
|
985
|
+
if (!response.Success)
|
|
986
|
+
throw new Exception($"Query failed: {response.Error}");
|
|
987
|
+
|
|
988
|
+
var result = JsonConvert.DeserializeObject<QueryResult>(response.Result);
|
|
989
|
+
if (result?.Data != null && result.Data.Count > 0)
|
|
990
|
+
{
|
|
991
|
+
var firstRow = result.Data[0];
|
|
992
|
+
if (firstRow.ContainsKey("count(1)"))
|
|
993
|
+
return Convert.ToInt32(firstRow["count(1)"]);
|
|
994
|
+
if (firstRow.ContainsKey("COUNT(1)"))
|
|
995
|
+
return Convert.ToInt32(firstRow["COUNT(1)"]);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
return 0;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
}
|