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
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Collections.Generic;
|
|
3
|
+
using System.Linq;
|
|
4
|
+
using System.Text;
|
|
5
|
+
|
|
6
|
+
namespace BACKND.Database.Internal
|
|
7
|
+
{
|
|
8
|
+
/// <summary>
|
|
9
|
+
/// SQL 쿼리 생성 유틸리티
|
|
10
|
+
/// </summary>
|
|
11
|
+
public static class SqlBuilder
|
|
12
|
+
{
|
|
13
|
+
#region WHERE 절 생성
|
|
14
|
+
|
|
15
|
+
/// <summary>
|
|
16
|
+
/// WhereCondition 목록에서 WHERE 절 생성
|
|
17
|
+
/// </summary>
|
|
18
|
+
public static string BuildWhereClause(
|
|
19
|
+
List<WhereCondition> whereConditions,
|
|
20
|
+
bool isOfCurrentUser = false,
|
|
21
|
+
TableType? tableType = null)
|
|
22
|
+
{
|
|
23
|
+
if (whereConditions.Count == 0 && !isOfCurrentUser)
|
|
24
|
+
return null;
|
|
25
|
+
|
|
26
|
+
var sb = new StringBuilder();
|
|
27
|
+
|
|
28
|
+
var hasUserUuidCondition = whereConditions.Any(c => c.ColumnName == "user_uuid");
|
|
29
|
+
if (isOfCurrentUser && tableType == TableType.UserTable && !hasUserUuidCondition)
|
|
30
|
+
{
|
|
31
|
+
sb.Append("user_uuid = @current_user_uuid");
|
|
32
|
+
if (whereConditions.Count > 0)
|
|
33
|
+
sb.Append(" AND ");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for (int i = 0; i < whereConditions.Count; i++)
|
|
37
|
+
{
|
|
38
|
+
var condition = whereConditions[i];
|
|
39
|
+
|
|
40
|
+
if (condition.IsGroupStart)
|
|
41
|
+
{
|
|
42
|
+
sb.Append("(");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (i > 0 && !condition.IsGroupStart)
|
|
46
|
+
{
|
|
47
|
+
sb.Append(condition.LogicalOperator == LogicalOperator.And ? " AND " : " OR ");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
sb.Append(BuildConditionString(condition));
|
|
51
|
+
|
|
52
|
+
if (condition.IsGroupEnd)
|
|
53
|
+
{
|
|
54
|
+
sb.Append(")");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return sb.ToString();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// <summary>
|
|
62
|
+
/// 단일 WhereCondition을 SQL 조건 문자열로 변환
|
|
63
|
+
/// </summary>
|
|
64
|
+
public static string BuildConditionString(WhereCondition condition)
|
|
65
|
+
{
|
|
66
|
+
// NULL 비교 시 자동으로 IS NULL / IS NOT NULL로 변환
|
|
67
|
+
if (condition.Value == null)
|
|
68
|
+
{
|
|
69
|
+
return condition.Operator switch
|
|
70
|
+
{
|
|
71
|
+
CompareOperator.Equal => $"{condition.ColumnName} IS NULL",
|
|
72
|
+
CompareOperator.NotEqual => $"{condition.ColumnName} IS NOT NULL",
|
|
73
|
+
CompareOperator.IsNull => $"{condition.ColumnName} IS NULL",
|
|
74
|
+
CompareOperator.IsNotNull => $"{condition.ColumnName} IS NOT NULL",
|
|
75
|
+
_ => throw new NotSupportedException($"NULL comparison with operator {condition.Operator} is not supported. Use 'IS NULL' or 'IS NOT NULL'")
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return condition.Operator switch
|
|
80
|
+
{
|
|
81
|
+
CompareOperator.Equal => $"{condition.ColumnName} = {ValueFormatter.FormatValueForQuery(condition.Value)}",
|
|
82
|
+
CompareOperator.NotEqual => $"{condition.ColumnName} != {ValueFormatter.FormatValueForQuery(condition.Value)}",
|
|
83
|
+
CompareOperator.GreaterThan => $"{condition.ColumnName} > {ValueFormatter.FormatValueForQuery(condition.Value)}",
|
|
84
|
+
CompareOperator.GreaterThanOrEqual => $"{condition.ColumnName} >= {ValueFormatter.FormatValueForQuery(condition.Value)}",
|
|
85
|
+
CompareOperator.LessThan => $"{condition.ColumnName} < {ValueFormatter.FormatValueForQuery(condition.Value)}",
|
|
86
|
+
CompareOperator.LessThanOrEqual => $"{condition.ColumnName} <= {ValueFormatter.FormatValueForQuery(condition.Value)}",
|
|
87
|
+
CompareOperator.Between => $"{condition.ColumnName} BETWEEN {ValueFormatter.FormatValueForQuery(condition.Value)} AND {ValueFormatter.FormatValueForQuery(condition.SecondValue)}",
|
|
88
|
+
CompareOperator.In => BuildInClause(condition),
|
|
89
|
+
CompareOperator.IsNull => $"{condition.ColumnName} IS NULL",
|
|
90
|
+
CompareOperator.IsNotNull => $"{condition.ColumnName} IS NOT NULL",
|
|
91
|
+
_ => throw new NotSupportedException($"Operator {condition.Operator} is not supported")
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/// <summary>
|
|
96
|
+
/// IN 절 생성
|
|
97
|
+
/// </summary>
|
|
98
|
+
public static string BuildInClause(WhereCondition condition)
|
|
99
|
+
{
|
|
100
|
+
if (condition.Value is Array array)
|
|
101
|
+
{
|
|
102
|
+
var values = new List<string>();
|
|
103
|
+
foreach (var item in array)
|
|
104
|
+
{
|
|
105
|
+
values.Add(ValueFormatter.FormatValueForQuery(item));
|
|
106
|
+
}
|
|
107
|
+
return $"{condition.ColumnName} IN ({string.Join(", ", values)})";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return $"{condition.ColumnName} = {ValueFormatter.FormatValueForQuery(condition.Value)}";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
#endregion
|
|
114
|
+
|
|
115
|
+
#region ORDER BY 절 생성
|
|
116
|
+
|
|
117
|
+
/// <summary>
|
|
118
|
+
/// OrderByInfo 목록에서 ORDER BY 절 생성
|
|
119
|
+
/// </summary>
|
|
120
|
+
public static string BuildOrderByClause(List<OrderByInfo> orderByList)
|
|
121
|
+
{
|
|
122
|
+
if (orderByList == null || orderByList.Count == 0)
|
|
123
|
+
return null;
|
|
124
|
+
|
|
125
|
+
var clauses = orderByList.Select(o => $"{o.Column} {(o.Descending ? "DESC" : "ASC")}");
|
|
126
|
+
return string.Join(", ", clauses);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
#endregion
|
|
130
|
+
|
|
131
|
+
#region INSERT 쿼리 생성
|
|
132
|
+
|
|
133
|
+
/// <summary>
|
|
134
|
+
/// INSERT 쿼리 생성
|
|
135
|
+
/// </summary>
|
|
136
|
+
public static string BuildInsertQuery(BaseModel model, out Dictionary<string, object> parameters)
|
|
137
|
+
{
|
|
138
|
+
parameters = new Dictionary<string, object>();
|
|
139
|
+
var columns = new List<string>();
|
|
140
|
+
var values = new List<string>();
|
|
141
|
+
|
|
142
|
+
var tableName = model.GetTableName();
|
|
143
|
+
var columnList = model.GetColumnList().Split(',').Select(c => c.Trim()).ToArray();
|
|
144
|
+
var autoIncrementColumn = model.GetAutoIncrementColumnName();
|
|
145
|
+
|
|
146
|
+
foreach (var columnName in columnList)
|
|
147
|
+
{
|
|
148
|
+
// AutoIncrement 컬럼은 INSERT에서 제외
|
|
149
|
+
if (!string.IsNullOrEmpty(autoIncrementColumn) &&
|
|
150
|
+
autoIncrementColumn.Equals(columnName, StringComparison.OrdinalIgnoreCase))
|
|
151
|
+
continue;
|
|
152
|
+
|
|
153
|
+
var value = model.GetValue(columnName);
|
|
154
|
+
var isNullableType = model.IsPropertyNullableType(columnName);
|
|
155
|
+
var isColumnNullable = model.IsColumnNullable(columnName);
|
|
156
|
+
|
|
157
|
+
// Nullable<T> 타입(int?, DateTime? 등)이고 값이 null인 경우
|
|
158
|
+
if (isNullableType && value == null)
|
|
159
|
+
{
|
|
160
|
+
if (isColumnNullable)
|
|
161
|
+
{
|
|
162
|
+
// NULL 허용 컬럼이면 NULL 값으로 INSERT
|
|
163
|
+
columns.Add(columnName);
|
|
164
|
+
values.Add("NULL");
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
else
|
|
168
|
+
{
|
|
169
|
+
// NotNull 컬럼인데 값이 null이면 기본값 확인
|
|
170
|
+
var defaultValue = model.GetColumnDefaultValue(columnName);
|
|
171
|
+
if (!string.IsNullOrEmpty(defaultValue))
|
|
172
|
+
continue;
|
|
173
|
+
|
|
174
|
+
throw new InvalidOperationException($"Column '{columnName}' cannot be null");
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 참조 타입(string, class 등)이 null인 경우
|
|
179
|
+
if (!isColumnNullable && value == null)
|
|
180
|
+
{
|
|
181
|
+
var defaultValue = model.GetColumnDefaultValue(columnName);
|
|
182
|
+
if (!string.IsNullOrEmpty(defaultValue))
|
|
183
|
+
continue;
|
|
184
|
+
|
|
185
|
+
throw new InvalidOperationException($"Column '{columnName}' cannot be null");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
columns.Add(columnName);
|
|
189
|
+
values.Add(ValueFormatter.FormatValueForQuery(value));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (columns.Count == 0)
|
|
193
|
+
throw new InvalidOperationException("No columns to insert");
|
|
194
|
+
|
|
195
|
+
return $"INSERT INTO {tableName} ({string.Join(", ", columns)}) VALUES ({string.Join(", ", values)})";
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
#endregion
|
|
199
|
+
|
|
200
|
+
#region UPDATE 쿼리 생성
|
|
201
|
+
|
|
202
|
+
/// <summary>
|
|
203
|
+
/// 모델 기반 UPDATE 쿼리 생성
|
|
204
|
+
/// </summary>
|
|
205
|
+
public static string BuildUpdateQuery(BaseModel model, string whereClause, out Dictionary<string, object> parameters)
|
|
206
|
+
{
|
|
207
|
+
parameters = new Dictionary<string, object>();
|
|
208
|
+
var setClauses = new List<string>();
|
|
209
|
+
|
|
210
|
+
var tableName = model.GetTableName();
|
|
211
|
+
var columnList = model.GetColumnList().Split(',').Select(c => c.Trim()).ToArray();
|
|
212
|
+
var primaryKeyColumns = model.GetPrimaryKeyColumnNames();
|
|
213
|
+
|
|
214
|
+
foreach (var columnName in columnList)
|
|
215
|
+
{
|
|
216
|
+
// PK 컬럼은 UPDATE SET 절에서 제외
|
|
217
|
+
if (primaryKeyColumns.Any(pk => pk.Equals(columnName, StringComparison.OrdinalIgnoreCase)))
|
|
218
|
+
continue;
|
|
219
|
+
|
|
220
|
+
var value = model.GetValue(columnName);
|
|
221
|
+
var isNullableType = model.IsPropertyNullableType(columnName);
|
|
222
|
+
var isColumnNullable = model.IsColumnNullable(columnName);
|
|
223
|
+
|
|
224
|
+
// Nullable<T> 타입(int?, DateTime? 등)이고 값이 null인 경우
|
|
225
|
+
if (isNullableType && value == null)
|
|
226
|
+
{
|
|
227
|
+
if (isColumnNullable)
|
|
228
|
+
{
|
|
229
|
+
// NULL 허용 컬럼이면 NULL로 UPDATE
|
|
230
|
+
setClauses.Add($"{columnName} = NULL");
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
else
|
|
234
|
+
{
|
|
235
|
+
throw new InvalidOperationException($"Column '{columnName}' cannot be null");
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// 참조 타입(string, class 등)이 null인 경우
|
|
240
|
+
if (!isColumnNullable && value == null)
|
|
241
|
+
throw new InvalidOperationException($"Column '{columnName}' cannot be null");
|
|
242
|
+
|
|
243
|
+
setClauses.Add($"{columnName} = {ValueFormatter.FormatValueForQuery(value)}");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (setClauses.Count == 0)
|
|
247
|
+
throw new InvalidOperationException("No columns to update");
|
|
248
|
+
|
|
249
|
+
return $"UPDATE {tableName} SET {string.Join(", ", setClauses)} WHERE {whereClause}";
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/// <summary>
|
|
253
|
+
/// SetClause 목록 기반 UPDATE 쿼리 생성 (Inc/Dec용)
|
|
254
|
+
/// </summary>
|
|
255
|
+
public static string BuildUpdateQueryFromSetClauses(string tableName, List<SetClause> setClauses, string whereClause)
|
|
256
|
+
{
|
|
257
|
+
if (setClauses == null || setClauses.Count == 0)
|
|
258
|
+
throw new InvalidOperationException("No set clauses specified");
|
|
259
|
+
|
|
260
|
+
var setStatements = setClauses.Select(clause =>
|
|
261
|
+
$"{clause.ColumnName} = {clause.ColumnName} {clause.Operator} {ValueFormatter.FormatValueForQuery(clause.Value)}"
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
return $"UPDATE {tableName} SET {string.Join(", ", setStatements)} WHERE {whereClause}";
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
#endregion
|
|
268
|
+
|
|
269
|
+
#region DELETE 쿼리 생성
|
|
270
|
+
|
|
271
|
+
/// <summary>
|
|
272
|
+
/// DELETE 쿼리 생성
|
|
273
|
+
/// </summary>
|
|
274
|
+
public static string BuildDeleteQuery(string tableName, string whereClause)
|
|
275
|
+
{
|
|
276
|
+
if (string.IsNullOrEmpty(whereClause))
|
|
277
|
+
throw new InvalidOperationException("DELETE requires a WHERE clause");
|
|
278
|
+
|
|
279
|
+
return $"DELETE FROM {tableName} WHERE {whereClause}";
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
#endregion
|
|
283
|
+
|
|
284
|
+
#region SELECT 쿼리 생성
|
|
285
|
+
|
|
286
|
+
/// <summary>
|
|
287
|
+
/// SELECT 쿼리 생성
|
|
288
|
+
/// </summary>
|
|
289
|
+
public static string BuildSelectQuery(
|
|
290
|
+
string tableName,
|
|
291
|
+
string columnList,
|
|
292
|
+
List<WhereCondition> whereConditions,
|
|
293
|
+
List<OrderByInfo> orderByList,
|
|
294
|
+
int? limit = null,
|
|
295
|
+
int? offset = null,
|
|
296
|
+
bool isOfCurrentUser = false,
|
|
297
|
+
TableType? tableType = null)
|
|
298
|
+
{
|
|
299
|
+
var sb = new StringBuilder();
|
|
300
|
+
sb.Append("SELECT ");
|
|
301
|
+
sb.Append(columnList);
|
|
302
|
+
sb.Append($" FROM {tableName}");
|
|
303
|
+
|
|
304
|
+
var whereClause = BuildWhereClause(whereConditions, isOfCurrentUser, tableType);
|
|
305
|
+
if (!string.IsNullOrEmpty(whereClause))
|
|
306
|
+
sb.Append($" WHERE {whereClause}");
|
|
307
|
+
|
|
308
|
+
var orderByClause = BuildOrderByClause(orderByList);
|
|
309
|
+
if (!string.IsNullOrEmpty(orderByClause))
|
|
310
|
+
sb.Append($" ORDER BY {orderByClause}");
|
|
311
|
+
|
|
312
|
+
if (limit.HasValue)
|
|
313
|
+
sb.Append($" LIMIT {limit.Value}");
|
|
314
|
+
|
|
315
|
+
if (offset.HasValue)
|
|
316
|
+
sb.Append($" OFFSET {offset.Value}");
|
|
317
|
+
|
|
318
|
+
return sb.ToString();
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/// <summary>
|
|
322
|
+
/// COUNT 쿼리 생성
|
|
323
|
+
/// </summary>
|
|
324
|
+
public static string BuildCountQuery(
|
|
325
|
+
string tableName,
|
|
326
|
+
List<WhereCondition> whereConditions,
|
|
327
|
+
bool isOfCurrentUser = false,
|
|
328
|
+
TableType? tableType = null)
|
|
329
|
+
{
|
|
330
|
+
var sb = new StringBuilder();
|
|
331
|
+
sb.Append($"SELECT COUNT(1) FROM {tableName}");
|
|
332
|
+
|
|
333
|
+
var whereClause = BuildWhereClause(whereConditions, isOfCurrentUser, tableType);
|
|
334
|
+
if (!string.IsNullOrEmpty(whereClause))
|
|
335
|
+
sb.Append($" WHERE {whereClause}");
|
|
336
|
+
|
|
337
|
+
return sb.ToString();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
#endregion
|
|
341
|
+
|
|
342
|
+
#region Primary Key WHERE 절 생성
|
|
343
|
+
|
|
344
|
+
/// <summary>
|
|
345
|
+
/// 모델의 Primary Key로 WHERE 조건 목록 생성
|
|
346
|
+
/// </summary>
|
|
347
|
+
public static List<WhereCondition> BuildPrimaryKeyConditions(BaseModel model)
|
|
348
|
+
{
|
|
349
|
+
var conditions = new List<WhereCondition>();
|
|
350
|
+
var primaryKeyColumns = model.GetPrimaryKeyColumnNames();
|
|
351
|
+
|
|
352
|
+
if (primaryKeyColumns.Length == 0)
|
|
353
|
+
throw new InvalidOperationException("Model has no Primary Key defined");
|
|
354
|
+
|
|
355
|
+
foreach (var primaryKeyColumn in primaryKeyColumns)
|
|
356
|
+
{
|
|
357
|
+
var primaryKeyValue = model.GetValue(primaryKeyColumn);
|
|
358
|
+
if (primaryKeyValue == null)
|
|
359
|
+
throw new InvalidOperationException($"Primary Key '{primaryKeyColumn}' has no value");
|
|
360
|
+
|
|
361
|
+
conditions.Add(new WhereCondition
|
|
362
|
+
{
|
|
363
|
+
ColumnName = primaryKeyColumn,
|
|
364
|
+
Operator = CompareOperator.Equal,
|
|
365
|
+
Value = primaryKeyValue,
|
|
366
|
+
LogicalOperator = LogicalOperator.And
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return conditions;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
#endregion
|
|
374
|
+
}
|
|
375
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Collections.Generic;
|
|
3
|
+
|
|
4
|
+
using Newtonsoft.Json;
|
|
5
|
+
|
|
6
|
+
namespace BACKND.Database.Internal
|
|
7
|
+
{
|
|
8
|
+
/// <summary>
|
|
9
|
+
/// SQL 쿼리용 값 포맷팅 유틸리티
|
|
10
|
+
/// </summary>
|
|
11
|
+
public static class ValueFormatter
|
|
12
|
+
{
|
|
13
|
+
/// <summary>
|
|
14
|
+
/// SQL 함수 및 키워드 목록
|
|
15
|
+
/// </summary>
|
|
16
|
+
private static readonly HashSet<string> SqlFunctionsAndKeywords = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
|
17
|
+
{
|
|
18
|
+
"NOW()", "CURRENT_TIMESTAMP", "CURRENT_TIMESTAMP()", "UUID()",
|
|
19
|
+
"CURRENT_DATE", "CURRENT_DATE()", "CURRENT_TIME", "CURRENT_TIME()",
|
|
20
|
+
"NULL"
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/// <summary>
|
|
24
|
+
/// 값이 SQL 함수 또는 키워드인지 확인
|
|
25
|
+
/// </summary>
|
|
26
|
+
public static bool IsSqlFunctionOrKeyword(string value)
|
|
27
|
+
{
|
|
28
|
+
if (string.IsNullOrWhiteSpace(value)) return false;
|
|
29
|
+
return SqlFunctionsAndKeywords.Contains(value.Trim());
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/// <summary>
|
|
33
|
+
/// 값을 SQL 쿼리용 문자열로 포맷팅
|
|
34
|
+
/// </summary>
|
|
35
|
+
public static string FormatValueForQuery(object value)
|
|
36
|
+
{
|
|
37
|
+
if (value == null) return "NULL";
|
|
38
|
+
|
|
39
|
+
// 값을 문자열로 변환
|
|
40
|
+
var stringValue = ConvertValueToString(value);
|
|
41
|
+
|
|
42
|
+
// SQL 함수 및 키워드는 따옴표 없이 그대로 반환
|
|
43
|
+
if (IsSqlFunctionOrKeyword(stringValue))
|
|
44
|
+
{
|
|
45
|
+
return stringValue.Trim();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 길이 제한 검사
|
|
49
|
+
const int maxLength = 1024 * 12; // 12KB
|
|
50
|
+
if (stringValue.Length > maxLength)
|
|
51
|
+
{
|
|
52
|
+
throw new ArgumentException($"Value too long. Maximum length is {maxLength} characters, but got {stringValue.Length}");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 함수와 NULL을 제외한 모든 값은 따옴표로 감싸기
|
|
56
|
+
return $"'{stringValue.Replace("'", "''").Replace("\\", "\\\\")}'";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// <summary>
|
|
60
|
+
/// 값을 문자열로 변환
|
|
61
|
+
/// </summary>
|
|
62
|
+
public static string ConvertValueToString(object value)
|
|
63
|
+
{
|
|
64
|
+
if (value == null) return "NULL";
|
|
65
|
+
|
|
66
|
+
var valueType = value.GetType();
|
|
67
|
+
|
|
68
|
+
// DateTime → ISO 8601 포맷
|
|
69
|
+
if (valueType == typeof(DateTime))
|
|
70
|
+
return ((DateTime)value).ToString("yyyy-MM-ddTHH:mm:ss.fffZ");
|
|
71
|
+
|
|
72
|
+
// bool → 소문자
|
|
73
|
+
if (valueType == typeof(bool))
|
|
74
|
+
return ((bool)value) ? "true" : "false";
|
|
75
|
+
|
|
76
|
+
// Enum → 숫자
|
|
77
|
+
if (valueType.IsEnum)
|
|
78
|
+
return ((int)value).ToString();
|
|
79
|
+
|
|
80
|
+
// 복합 객체 → JSON
|
|
81
|
+
if (!valueType.IsPrimitive && valueType != typeof(string) && valueType != typeof(Guid))
|
|
82
|
+
{
|
|
83
|
+
try
|
|
84
|
+
{
|
|
85
|
+
return JsonConvert.SerializeObject(value, new JsonSerializerSettings
|
|
86
|
+
{
|
|
87
|
+
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
|
|
88
|
+
NullValueHandling = NullValueHandling.Ignore,
|
|
89
|
+
DefaultValueHandling = DefaultValueHandling.Ignore,
|
|
90
|
+
Formatting = Formatting.None
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
catch (JsonException ex)
|
|
94
|
+
{
|
|
95
|
+
throw new InvalidOperationException($"Failed to serialize object of type {valueType.Name} to JSON", ex);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 기본: ToString()
|
|
100
|
+
return value.ToString();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
package/Internal.meta
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Collections.Generic;
|
|
3
|
+
using System.Text;
|
|
4
|
+
using System.Threading;
|
|
5
|
+
|
|
6
|
+
using Newtonsoft.Json;
|
|
7
|
+
|
|
8
|
+
using UnityEngine;
|
|
9
|
+
using UnityEngine.Networking;
|
|
10
|
+
|
|
11
|
+
namespace BACKND.Database.Network
|
|
12
|
+
{
|
|
13
|
+
public static class DatabaseExecutor
|
|
14
|
+
{
|
|
15
|
+
//private static readonly string SERVER_URL = "http://localhost:10002";
|
|
16
|
+
//private static readonly string SERVER_URL = "https://api.alpha.thebackend.io";
|
|
17
|
+
//private static readonly string SERVER_URL = "https://api.beta.thebackend.io";
|
|
18
|
+
|
|
19
|
+
private static readonly string SERVER_URL = "https://api.thebackend.io";
|
|
20
|
+
private static readonly string ENDPOINT = "/v1/database/store";
|
|
21
|
+
private static readonly int MAX_RETRIES = 3;
|
|
22
|
+
private static readonly float RETRY_DELAY = 1.0f;
|
|
23
|
+
private static readonly int REQUEST_TIMEOUT = 60;
|
|
24
|
+
|
|
25
|
+
public static async BTask<Response> Execute(DatabaseRequest request, Dictionary<string, string> headers, CancellationToken cancellationToken = default)
|
|
26
|
+
{
|
|
27
|
+
var query = ReplacePlaceholders(request.Query, request.Parameters);
|
|
28
|
+
|
|
29
|
+
var requestBody = new StoreRequest
|
|
30
|
+
{
|
|
31
|
+
query = query
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
var json = JsonConvert.SerializeObject(requestBody);
|
|
35
|
+
var bytes = Encoding.UTF8.GetBytes(json);
|
|
36
|
+
|
|
37
|
+
int retryCount = 0;
|
|
38
|
+
Exception lastException = null;
|
|
39
|
+
|
|
40
|
+
while (retryCount <= MAX_RETRIES)
|
|
41
|
+
{
|
|
42
|
+
using var webRequest = new UnityWebRequest($"{SERVER_URL}{ENDPOINT}", "POST");
|
|
43
|
+
webRequest.uploadHandler = new UploadHandlerRaw(bytes);
|
|
44
|
+
webRequest.downloadHandler = new DownloadHandlerBuffer();
|
|
45
|
+
webRequest.timeout = REQUEST_TIMEOUT;
|
|
46
|
+
|
|
47
|
+
webRequest.SetRequestHeader("Content-Type", "application/json");
|
|
48
|
+
foreach (var header in headers)
|
|
49
|
+
{
|
|
50
|
+
webRequest.SetRequestHeader(header.Key, header.Value);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try
|
|
54
|
+
{
|
|
55
|
+
using var timeoutCts = new CancellationTokenSource();
|
|
56
|
+
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
|
|
57
|
+
|
|
58
|
+
timeoutCts.CancelAfter(REQUEST_TIMEOUT * 1000);
|
|
59
|
+
|
|
60
|
+
var operation = webRequest.SendWebRequestAsBTask(linkedCts.Token);
|
|
61
|
+
|
|
62
|
+
try
|
|
63
|
+
{
|
|
64
|
+
var result = await operation;
|
|
65
|
+
|
|
66
|
+
if (result.result == UnityWebRequest.Result.Success)
|
|
67
|
+
{
|
|
68
|
+
var responseText = result.downloadHandler.text;
|
|
69
|
+
var response = JsonConvert.DeserializeObject<Response>(responseText);
|
|
70
|
+
|
|
71
|
+
return response;
|
|
72
|
+
}
|
|
73
|
+
else
|
|
74
|
+
{
|
|
75
|
+
lastException = new Exception($"Network error: {result.error}");
|
|
76
|
+
retryCount++;
|
|
77
|
+
|
|
78
|
+
if (retryCount <= MAX_RETRIES)
|
|
79
|
+
{
|
|
80
|
+
Debug.LogWarning($"[DatabaseExecutor] Retry {retryCount}/{MAX_RETRIES}: {lastException.Message}");
|
|
81
|
+
await BTask.Delay(RETRY_DELAY * retryCount);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch (OperationCanceledException)
|
|
87
|
+
{
|
|
88
|
+
webRequest.Abort();
|
|
89
|
+
|
|
90
|
+
if (timeoutCts.IsCancellationRequested && !cancellationToken.IsCancellationRequested)
|
|
91
|
+
{
|
|
92
|
+
lastException = new Exception($"Request timeout after {REQUEST_TIMEOUT} seconds");
|
|
93
|
+
retryCount++;
|
|
94
|
+
|
|
95
|
+
if (retryCount <= MAX_RETRIES)
|
|
96
|
+
{
|
|
97
|
+
Debug.LogWarning($"[DatabaseExecutor] Retry {retryCount}/{MAX_RETRIES}: {lastException.Message}");
|
|
98
|
+
await BTask.Delay(RETRY_DELAY * retryCount);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else
|
|
103
|
+
{
|
|
104
|
+
throw;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch (OperationCanceledException)
|
|
109
|
+
{
|
|
110
|
+
throw;
|
|
111
|
+
}
|
|
112
|
+
catch (Exception ex)
|
|
113
|
+
{
|
|
114
|
+
lastException = ex;
|
|
115
|
+
retryCount++;
|
|
116
|
+
|
|
117
|
+
if (retryCount <= MAX_RETRIES)
|
|
118
|
+
{
|
|
119
|
+
Debug.LogWarning($"[DatabaseExecutor] Retry {retryCount}/{MAX_RETRIES}: {ex.Message}");
|
|
120
|
+
await BTask.Delay(RETRY_DELAY * retryCount);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return new Response
|
|
127
|
+
{
|
|
128
|
+
Success = false,
|
|
129
|
+
Error = $"Request failed after {MAX_RETRIES} retries: {lastException?.Message ?? "Unknown error"}"
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private static string ReplacePlaceholders(string query, Dictionary<string, object> parameters)
|
|
134
|
+
{
|
|
135
|
+
if (parameters == null || parameters.Count == 0)
|
|
136
|
+
return query;
|
|
137
|
+
|
|
138
|
+
var result = query;
|
|
139
|
+
foreach (var param in parameters)
|
|
140
|
+
{
|
|
141
|
+
var value = FormatValue(param.Value);
|
|
142
|
+
result = result.Replace(param.Key, value);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private static string FormatValue(object value)
|
|
149
|
+
{
|
|
150
|
+
if (value == null)
|
|
151
|
+
return "NULL";
|
|
152
|
+
|
|
153
|
+
if (value is string str)
|
|
154
|
+
return $"'{str.Replace("'", "''")}'";
|
|
155
|
+
|
|
156
|
+
if (value is bool b)
|
|
157
|
+
return b ? "true" : "false";
|
|
158
|
+
|
|
159
|
+
if (value is DateTime dt)
|
|
160
|
+
return $"'{dt:yyyy-MM-ddTHH:mm:ss.fffZ}'";
|
|
161
|
+
|
|
162
|
+
return value.ToString();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private class StoreRequest
|
|
166
|
+
{
|
|
167
|
+
[JsonProperty("query")]
|
|
168
|
+
public string query;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|