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.
Files changed (63) hide show
  1. package/Attributes/ColumnAttribute.cs +46 -0
  2. package/Attributes/ColumnAttribute.cs.meta +11 -0
  3. package/Attributes/PrimaryKeyAttribute.cs +12 -0
  4. package/Attributes/PrimaryKeyAttribute.cs.meta +11 -0
  5. package/Attributes/TableAttribute.cs +44 -0
  6. package/Attributes/TableAttribute.cs.meta +11 -0
  7. package/Attributes.meta +8 -0
  8. package/BACKND.Database.asmdef +14 -0
  9. package/BACKND.Database.asmdef.meta +7 -0
  10. package/BaseModel.cs +22 -0
  11. package/BaseModel.cs.meta +11 -0
  12. package/Client.cs +490 -0
  13. package/Client.cs.meta +11 -0
  14. package/Editor/BACKND.Database.Editor.asmdef +18 -0
  15. package/Editor/BACKND.Database.Editor.asmdef.meta +7 -0
  16. package/Editor/PackageAssetInstaller.cs +251 -0
  17. package/Editor/PackageAssetInstaller.cs.meta +11 -0
  18. package/Editor.meta +8 -0
  19. package/Exceptions/DatabaseException.cs +34 -0
  20. package/Exceptions/DatabaseException.cs.meta +11 -0
  21. package/Exceptions.meta +8 -0
  22. package/Internal/ExpressionAnalyzer.cs +433 -0
  23. package/Internal/ExpressionAnalyzer.cs.meta +11 -0
  24. package/Internal/QueryTypes.cs +61 -0
  25. package/Internal/QueryTypes.cs.meta +11 -0
  26. package/Internal/SqlBuilder.cs +375 -0
  27. package/Internal/SqlBuilder.cs.meta +11 -0
  28. package/Internal/ValueFormatter.cs +103 -0
  29. package/Internal/ValueFormatter.cs.meta +11 -0
  30. package/Internal.meta +8 -0
  31. package/Network/DatabaseExecutor.cs +171 -0
  32. package/Network/DatabaseExecutor.cs.meta +11 -0
  33. package/Network/DatabaseRequest.cs +16 -0
  34. package/Network/DatabaseRequest.cs.meta +11 -0
  35. package/Network/DatabaseResponse.cs +81 -0
  36. package/Network/DatabaseResponse.cs.meta +11 -0
  37. package/Network.meta +8 -0
  38. package/QueryBuilder.cs +1001 -0
  39. package/QueryBuilder.cs.meta +11 -0
  40. package/README.md +24 -0
  41. package/TheBackend~/Plugins/Android/Backend.aar +0 -0
  42. package/TheBackend~/Plugins/Backend.dll +0 -0
  43. package/TheBackend~/Plugins/Editor/TheBackendMultiSettingEditor.dll +0 -0
  44. package/TheBackend~/Plugins/Editor/TheBackendSettingEditor.dll +0 -0
  45. package/TheBackend~/Plugins/LitJSON.dll +0 -0
  46. package/TheBackend~/Plugins/Settings/TheBackendHashKeySettings.dll +0 -0
  47. package/TheBackend~/Plugins/Settings/TheBackendMultiSettings.dll +0 -0
  48. package/TheBackend~/Plugins/Settings/TheBackendSettings.dll +0 -0
  49. package/Tools/BTask.cs +905 -0
  50. package/Tools/BTask.cs.meta +11 -0
  51. package/Tools/DatabaseLoop.cs +110 -0
  52. package/Tools/DatabaseLoop.cs.meta +11 -0
  53. package/Tools/JsonHelper.cs +82 -0
  54. package/Tools/JsonHelper.cs.meta +11 -0
  55. package/Tools.meta +8 -0
  56. package/TransactionBuilder.cs +154 -0
  57. package/TransactionBuilder.cs.meta +11 -0
  58. package/TransactionQueryBuilder.cs +205 -0
  59. package/TransactionQueryBuilder.cs.meta +11 -0
  60. package/TransactionResult.cs +33 -0
  61. package/TransactionResult.cs.meta +11 -0
  62. package/package.json +20 -0
  63. package/package.json.meta +7 -0
@@ -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
+ }