@an5/orm 1.3.5 → 1.3.6
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/dist/generator/src/dotnet-generator.js +122 -0
- package/dist/generator/src/golang-generator.js +171 -13
- package/dist/generator/src/python-generator.js +12 -15
- package/generator/src/dotnet-generator.ts +122 -0
- package/generator/src/golang-generator.ts +171 -13
- package/generator/src/python-generator.ts +12 -15
- package/package.json +1 -1
|
@@ -723,6 +723,14 @@ namespace An5Orm
|
|
|
723
723
|
return FindFirst("Id = @id", new Dictionary<string, object> { { "id", id } });
|
|
724
724
|
}
|
|
725
725
|
|
|
726
|
+
private static bool IsScalarColumn(PropertyInfo prop)
|
|
727
|
+
{
|
|
728
|
+
var t = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType;
|
|
729
|
+
if (t.IsPrimitive || t.IsEnum || t.IsValueType) return true;
|
|
730
|
+
if (t == typeof(string) || t == typeof(byte[]) || t == typeof(float[]) || t == typeof(double[])) return true;
|
|
731
|
+
return false;
|
|
732
|
+
}
|
|
733
|
+
|
|
726
734
|
public T Create(T entity)
|
|
727
735
|
{
|
|
728
736
|
var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
|
@@ -734,6 +742,7 @@ namespace An5Orm
|
|
|
734
742
|
|
|
735
743
|
foreach (var prop in properties)
|
|
736
744
|
{
|
|
745
|
+
if (!IsScalarColumn(prop)) continue;
|
|
737
746
|
var val = prop.GetValue(entity);
|
|
738
747
|
if (val != null)
|
|
739
748
|
{
|
|
@@ -775,6 +784,7 @@ namespace An5Orm
|
|
|
775
784
|
|
|
776
785
|
foreach (var prop in properties)
|
|
777
786
|
{
|
|
787
|
+
if (!IsScalarColumn(prop)) continue;
|
|
778
788
|
var val = prop.GetValue(entity);
|
|
779
789
|
if (prop.Name.Equals("Id", StringComparison.OrdinalIgnoreCase))
|
|
780
790
|
{
|
|
@@ -1053,6 +1063,98 @@ namespace An5Orm
|
|
|
1053
1063
|
return output;
|
|
1054
1064
|
}
|
|
1055
1065
|
|
|
1066
|
+
public Dictionary<string, object> Aggregate(
|
|
1067
|
+
string whereClause = null,
|
|
1068
|
+
Dictionary<string, object> parameters = null,
|
|
1069
|
+
bool count = false,
|
|
1070
|
+
List<string> sum = null,
|
|
1071
|
+
List<string> avg = null,
|
|
1072
|
+
List<string> min = null,
|
|
1073
|
+
List<string> max = null)
|
|
1074
|
+
{
|
|
1075
|
+
var selectParts = new List<string>();
|
|
1076
|
+
if (count) selectParts.Add("COUNT(*) AS _count");
|
|
1077
|
+
if (sum != null) foreach (var col in sum) selectParts.Add($"SUM({An5Provider.Quote(col, Dialect)}) AS _sum_{col}");
|
|
1078
|
+
if (avg != null) foreach (var col in avg) selectParts.Add($"AVG({An5Provider.Quote(col, Dialect)}) AS _avg_{col}");
|
|
1079
|
+
if (min != null) foreach (var col in min) selectParts.Add($"MIN({An5Provider.Quote(col, Dialect)}) AS _min_{col}");
|
|
1080
|
+
if (max != null) foreach (var col in max) selectParts.Add($"MAX({An5Provider.Quote(col, Dialect)}) AS _max_{col}");
|
|
1081
|
+
if (selectParts.Count == 0) selectParts.Add("COUNT(*) AS _count");
|
|
1082
|
+
|
|
1083
|
+
var query = $"SELECT {string.Join(", ", selectParts)} FROM {TableName}";
|
|
1084
|
+
if (!string.IsNullOrEmpty(whereClause)) query += $" WHERE {whereClause}";
|
|
1085
|
+
|
|
1086
|
+
var conn = An5DbContext.GetActiveConnection(ConnectionString, out bool isTx);
|
|
1087
|
+
try
|
|
1088
|
+
{
|
|
1089
|
+
using var cmd = CreateCommand(conn, query);
|
|
1090
|
+
if (parameters != null)
|
|
1091
|
+
{
|
|
1092
|
+
foreach (var kvp in parameters) An5Provider.Bind(cmd, kvp.Key, kvp.Value);
|
|
1093
|
+
}
|
|
1094
|
+
using var reader = cmd.ExecuteReader();
|
|
1095
|
+
var result = new Dictionary<string, object>();
|
|
1096
|
+
if (reader.Read())
|
|
1097
|
+
{
|
|
1098
|
+
for (int i = 0; i < reader.FieldCount; i++)
|
|
1099
|
+
{
|
|
1100
|
+
var name = reader.GetName(i);
|
|
1101
|
+
var val = reader[i];
|
|
1102
|
+
result[name] = val != DBNull.Value ? val : null;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
return result;
|
|
1106
|
+
}
|
|
1107
|
+
finally { if (!isTx) conn.Dispose(); }
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
public List<Dictionary<string, object>> GroupBy(
|
|
1111
|
+
List<string> by,
|
|
1112
|
+
string whereClause = null,
|
|
1113
|
+
Dictionary<string, object> parameters = null,
|
|
1114
|
+
List<string> sum = null,
|
|
1115
|
+
List<string> avg = null,
|
|
1116
|
+
List<string> min = null,
|
|
1117
|
+
List<string> max = null)
|
|
1118
|
+
{
|
|
1119
|
+
if (by == null || by.Count == 0) throw new ArgumentException("GroupBy requires at least one column in 'by'");
|
|
1120
|
+
var byCols = by.Select(c => An5Provider.Quote(c, Dialect)).ToList();
|
|
1121
|
+
var selectParts = new List<string>(byCols);
|
|
1122
|
+
selectParts.Add("COUNT(*) AS _count");
|
|
1123
|
+
if (sum != null) foreach (var col in sum) selectParts.Add($"SUM({An5Provider.Quote(col, Dialect)}) AS _sum_{col}");
|
|
1124
|
+
if (avg != null) foreach (var col in avg) selectParts.Add($"AVG({An5Provider.Quote(col, Dialect)}) AS _avg_{col}");
|
|
1125
|
+
if (min != null) foreach (var col in min) selectParts.Add($"MIN({An5Provider.Quote(col, Dialect)}) AS _min_{col}");
|
|
1126
|
+
if (max != null) foreach (var col in max) selectParts.Add($"MAX({An5Provider.Quote(col, Dialect)}) AS _max_{col}");
|
|
1127
|
+
|
|
1128
|
+
var query = $"SELECT {string.Join(", ", selectParts)} FROM {TableName}";
|
|
1129
|
+
if (!string.IsNullOrEmpty(whereClause)) query += $" WHERE {whereClause}";
|
|
1130
|
+
query += $" GROUP BY {string.Join(", ", byCols)}";
|
|
1131
|
+
|
|
1132
|
+
var conn = An5DbContext.GetActiveConnection(ConnectionString, out bool isTx);
|
|
1133
|
+
try
|
|
1134
|
+
{
|
|
1135
|
+
using var cmd = CreateCommand(conn, query);
|
|
1136
|
+
if (parameters != null)
|
|
1137
|
+
{
|
|
1138
|
+
foreach (var kvp in parameters) An5Provider.Bind(cmd, kvp.Key, kvp.Value);
|
|
1139
|
+
}
|
|
1140
|
+
using var reader = cmd.ExecuteReader();
|
|
1141
|
+
var rows = new List<Dictionary<string, object>>();
|
|
1142
|
+
while (reader.Read())
|
|
1143
|
+
{
|
|
1144
|
+
var row = new Dictionary<string, object>();
|
|
1145
|
+
for (int i = 0; i < reader.FieldCount; i++)
|
|
1146
|
+
{
|
|
1147
|
+
var name = reader.GetName(i);
|
|
1148
|
+
var val = reader[i];
|
|
1149
|
+
row[name] = val != DBNull.Value ? val : null;
|
|
1150
|
+
}
|
|
1151
|
+
rows.Add(row);
|
|
1152
|
+
}
|
|
1153
|
+
return rows;
|
|
1154
|
+
}
|
|
1155
|
+
finally { if (!isTx) conn.Dispose(); }
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1056
1158
|
private static List<double> ParseDoubleArray(string json)
|
|
1057
1159
|
{
|
|
1058
1160
|
var clean = json.Trim('[', ']');
|
|
@@ -1253,6 +1355,26 @@ namespace An5Orm
|
|
|
1253
1355
|
|
|
1254
1356
|
public List<T> VectorSearch(List<double> vector, int take = 10, string whereClause = null, Dictionary<string, object> parameters = null, string vectorField = "Embedding", string distanceMetric = "cosine")
|
|
1255
1357
|
=> _table.VectorSearch(vector, take, whereClause, parameters, vectorField, distanceMetric);
|
|
1358
|
+
|
|
1359
|
+
public Dictionary<string, object> Aggregate(
|
|
1360
|
+
string whereClause = null,
|
|
1361
|
+
Dictionary<string, object> parameters = null,
|
|
1362
|
+
bool count = false,
|
|
1363
|
+
List<string> sum = null,
|
|
1364
|
+
List<string> avg = null,
|
|
1365
|
+
List<string> min = null,
|
|
1366
|
+
List<string> max = null)
|
|
1367
|
+
=> _table.Aggregate(whereClause, parameters, count, sum, avg, min, max);
|
|
1368
|
+
|
|
1369
|
+
public List<Dictionary<string, object>> GroupBy(
|
|
1370
|
+
List<string> by,
|
|
1371
|
+
string whereClause = null,
|
|
1372
|
+
Dictionary<string, object> parameters = null,
|
|
1373
|
+
List<string> sum = null,
|
|
1374
|
+
List<string> avg = null,
|
|
1375
|
+
List<string> min = null,
|
|
1376
|
+
List<string> max = null)
|
|
1377
|
+
=> _table.GroupBy(by, whereClause, parameters, sum, avg, min, max);
|
|
1256
1378
|
}
|
|
1257
1379
|
}
|
|
1258
1380
|
`;
|
|
@@ -266,14 +266,34 @@ type BoolFilter struct {
|
|
|
266
266
|
|
|
267
267
|
// DateTimeFilter provides type-safe datetime field filtering options.
|
|
268
268
|
type DateTimeFilter struct {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
269
|
+
Equals *time.Time
|
|
270
|
+
Not *time.Time
|
|
271
|
+
In []time.Time
|
|
272
|
+
NotIn []time.Time
|
|
273
|
+
Gt *time.Time
|
|
274
|
+
Gte *time.Time
|
|
275
|
+
Lt *time.Time
|
|
276
|
+
Lte *time.Time
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// AggregateArgs configures an aggregate query.
|
|
280
|
+
type AggregateArgs struct {
|
|
281
|
+
Where interface{}
|
|
282
|
+
Count bool
|
|
283
|
+
Sum []string
|
|
284
|
+
Avg []string
|
|
285
|
+
Min []string
|
|
286
|
+
Max []string
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// GroupByArgs configures a GroupBy query.
|
|
290
|
+
type GroupByArgs struct {
|
|
291
|
+
By []string
|
|
292
|
+
Where interface{}
|
|
293
|
+
Sum []string
|
|
294
|
+
Avg []string
|
|
295
|
+
Min []string
|
|
296
|
+
Max []string
|
|
277
297
|
}
|
|
278
298
|
|
|
279
299
|
// Dialect identifies the database engine in use.
|
|
@@ -442,6 +462,16 @@ func (c *ViewClient[T]) VectorSearch(ctx context.Context, vector []float64, opts
|
|
|
442
462
|
\treturn c.table.VectorSearch(ctx, vector, opts...)
|
|
443
463
|
}
|
|
444
464
|
|
|
465
|
+
// Aggregate returns aggregate calculations on the view.
|
|
466
|
+
func (c *ViewClient[T]) Aggregate(ctx context.Context, args AggregateArgs) (map[string]interface{}, error) {
|
|
467
|
+
\treturn c.table.Aggregate(ctx, args)
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// GroupBy groups view rows by specified columns and computes aggregates.
|
|
471
|
+
func (c *ViewClient[T]) GroupBy(ctx context.Context, args GroupByArgs) ([]map[string]interface{}, error) {
|
|
472
|
+
\treturn c.table.GroupBy(ctx, args)
|
|
473
|
+
}
|
|
474
|
+
|
|
445
475
|
`;
|
|
446
476
|
}
|
|
447
477
|
// ─── queryAndScan ──────────────────────────────────────────────────────────
|
|
@@ -953,7 +983,7 @@ func (c *TableClient[T]) UpdateMany(ctx context.Context, whereArgs interface{},
|
|
|
953
983
|
\t\tsets = append(sets, c.quoteName(col)+" = "+c.ph(len(execArgs)))
|
|
954
984
|
\t}
|
|
955
985
|
|
|
956
|
-
\tclause, _, _, _, _, whereQueryArgs := c.
|
|
986
|
+
\tclause, _, _, _, _, whereQueryArgs := c.parseArgsWithOffset(whereArgs, len(execArgs)+1)
|
|
957
987
|
\texecArgs = append(execArgs, whereQueryArgs...)
|
|
958
988
|
\tquery := fmt.Sprintf("UPDATE %s SET %s", c.quoteTable(), strings.Join(sets, ", "))
|
|
959
989
|
\tif clause != "" {
|
|
@@ -1038,9 +1068,129 @@ func (c *TableClient[T]) Transaction(ctx context.Context, fn func(tx *sql.Tx) er
|
|
|
1038
1068
|
\treturn tx.Commit()
|
|
1039
1069
|
}
|
|
1040
1070
|
|
|
1071
|
+
// Aggregate returns aggregate calculations (count, sum, avg, min, max).
|
|
1072
|
+
func (c *TableClient[T]) Aggregate(ctx context.Context, args AggregateArgs) (map[string]interface{}, error) {
|
|
1073
|
+
\tclause, _, _, _, _, queryArgs := c.parseArgs(args.Where)
|
|
1074
|
+
\tvar selectParts []string
|
|
1075
|
+
\tif args.Count || (len(args.Sum) == 0 && len(args.Avg) == 0 && len(args.Min) == 0 && len(args.Max) == 0) {
|
|
1076
|
+
\t\tselectParts = append(selectParts, "COUNT(*) AS _count")
|
|
1077
|
+
\t}
|
|
1078
|
+
\tfor _, col := range args.Sum {
|
|
1079
|
+
\t\tselectParts = append(selectParts, fmt.Sprintf("SUM(%s) AS _sum_%s", c.quoteName(col), col))
|
|
1080
|
+
\t}
|
|
1081
|
+
\tfor _, col := range args.Avg {
|
|
1082
|
+
\t\tselectParts = append(selectParts, fmt.Sprintf("AVG(%s) AS _avg_%s", c.quoteName(col), col))
|
|
1083
|
+
\t}
|
|
1084
|
+
\tfor _, col := range args.Min {
|
|
1085
|
+
\t\tselectParts = append(selectParts, fmt.Sprintf("MIN(%s) AS _min_%s", c.quoteName(col), col))
|
|
1086
|
+
\t}
|
|
1087
|
+
\tfor _, col := range args.Max {
|
|
1088
|
+
\t\tselectParts = append(selectParts, fmt.Sprintf("MAX(%s) AS _max_%s", c.quoteName(col), col))
|
|
1089
|
+
\t}
|
|
1090
|
+
|
|
1091
|
+
\tquery := fmt.Sprintf("SELECT %s FROM %s", strings.Join(selectParts, ", "), c.quoteTable())
|
|
1092
|
+
\tif clause != "" {
|
|
1093
|
+
\t\tquery += " WHERE " + clause
|
|
1094
|
+
\t}
|
|
1095
|
+
|
|
1096
|
+
\trows, err := c.DB.QueryContext(ctx, query, queryArgs...)
|
|
1097
|
+
\tif err != nil {
|
|
1098
|
+
\t\treturn nil, err
|
|
1099
|
+
\t}
|
|
1100
|
+
\tdefer rows.Close()
|
|
1101
|
+
|
|
1102
|
+
\tcols, err := rows.Columns()
|
|
1103
|
+
\tif err != nil {
|
|
1104
|
+
\t\treturn nil, err
|
|
1105
|
+
\t}
|
|
1106
|
+
|
|
1107
|
+
\tres := make(map[string]interface{})
|
|
1108
|
+
\tif rows.Next() {
|
|
1109
|
+
\t\tcolVals := make([]interface{}, len(cols))
|
|
1110
|
+
\t\tcolPtrs := make([]interface{}, len(cols))
|
|
1111
|
+
\t\tfor i := range colVals {
|
|
1112
|
+
\t\t\tcolPtrs[i] = &colVals[i]
|
|
1113
|
+
\t\t}
|
|
1114
|
+
\t\tif err := rows.Scan(colPtrs...); err != nil {
|
|
1115
|
+
\t\t\treturn nil, err
|
|
1116
|
+
\t\t}
|
|
1117
|
+
\t\tfor i, name := range cols {
|
|
1118
|
+
\t\t\tres[name] = colVals[i]
|
|
1119
|
+
\t\t}
|
|
1120
|
+
\t}
|
|
1121
|
+
\treturn res, nil
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// GroupBy groups records by specified columns and computes aggregates.
|
|
1125
|
+
func (c *TableClient[T]) GroupBy(ctx context.Context, args GroupByArgs) ([]map[string]interface{}, error) {
|
|
1126
|
+
\tif len(args.By) == 0 {
|
|
1127
|
+
\t\treturn nil, fmt.Errorf("GroupBy requires at least one column in 'By'")
|
|
1128
|
+
\t}
|
|
1129
|
+
\tclause, _, _, _, _, queryArgs := c.parseArgs(args.Where)
|
|
1130
|
+
\tbyCols := make([]string, len(args.By))
|
|
1131
|
+
\tfor i, col := range args.By {
|
|
1132
|
+
\t\tbyCols[i] = c.quoteName(col)
|
|
1133
|
+
\t}
|
|
1134
|
+
|
|
1135
|
+
\tselectParts := make([]string, len(byCols))
|
|
1136
|
+
\tcopy(selectParts, byCols)
|
|
1137
|
+
\tselectParts = append(selectParts, "COUNT(*) AS _count")
|
|
1138
|
+
\tfor _, col := range args.Sum {
|
|
1139
|
+
\t\tselectParts = append(selectParts, fmt.Sprintf("SUM(%s) AS _sum_%s", c.quoteName(col), col))
|
|
1140
|
+
\t}
|
|
1141
|
+
\tfor _, col := range args.Avg {
|
|
1142
|
+
\t\tselectParts = append(selectParts, fmt.Sprintf("AVG(%s) AS _avg_%s", c.quoteName(col), col))
|
|
1143
|
+
\t}
|
|
1144
|
+
\tfor _, col := range args.Min {
|
|
1145
|
+
\t\tselectParts = append(selectParts, fmt.Sprintf("MIN(%s) AS _min_%s", c.quoteName(col), col))
|
|
1146
|
+
\t}
|
|
1147
|
+
\tfor _, col := range args.Max {
|
|
1148
|
+
\t\tselectParts = append(selectParts, fmt.Sprintf("MAX(%s) AS _max_%s", c.quoteName(col), col))
|
|
1149
|
+
\t}
|
|
1150
|
+
|
|
1151
|
+
\tquery := fmt.Sprintf("SELECT %s FROM %s", strings.Join(selectParts, ", "), c.quoteTable())
|
|
1152
|
+
\tif clause != "" {
|
|
1153
|
+
\t\tquery += " WHERE " + clause
|
|
1154
|
+
\t}
|
|
1155
|
+
\tquery += " GROUP BY " + strings.Join(byCols, ", ")
|
|
1156
|
+
|
|
1157
|
+
\trows, err := c.DB.QueryContext(ctx, query, queryArgs...)
|
|
1158
|
+
\tif err != nil {
|
|
1159
|
+
\t\treturn nil, err
|
|
1160
|
+
\t}
|
|
1161
|
+
\tdefer rows.Close()
|
|
1162
|
+
|
|
1163
|
+
\tcols, err := rows.Columns()
|
|
1164
|
+
\tif err != nil {
|
|
1165
|
+
\t\treturn nil, err
|
|
1166
|
+
\t}
|
|
1167
|
+
|
|
1168
|
+
\tvar results []map[string]interface{}
|
|
1169
|
+
\tfor rows.Next() {
|
|
1170
|
+
\t\tcolVals := make([]interface{}, len(cols))
|
|
1171
|
+
\t\tcolPtrs := make([]interface{}, len(cols))
|
|
1172
|
+
\t\tfor i := range colVals {
|
|
1173
|
+
\t\t\tcolPtrs[i] = &colVals[i]
|
|
1174
|
+
\t\t}
|
|
1175
|
+
\t\tif err := rows.Scan(colPtrs...); err != nil {
|
|
1176
|
+
\t\t\treturn nil, err
|
|
1177
|
+
\t\t}
|
|
1178
|
+
\t\trowMap := make(map[string]interface{})
|
|
1179
|
+
\t\tfor i, name := range cols {
|
|
1180
|
+
\t\t\trowMap[name] = colVals[i]
|
|
1181
|
+
\t\t}
|
|
1182
|
+
\t\tresults = append(results, rowMap)
|
|
1183
|
+
\t}
|
|
1184
|
+
\treturn results, nil
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1041
1187
|
// parseArgs is the generic where/orderBy/take/skip extractor.
|
|
1042
1188
|
// It uses reflection to read WhereClause, OrderBy, Take, Skip, Select from any args struct.
|
|
1043
1189
|
func (c *TableClient[T]) parseArgs(args interface{}) (whereClause string, orderBy string, take int, skip int, selectCols []string, queryArgs []interface{}) {
|
|
1190
|
+
\treturn c.parseArgsWithOffset(args, 1)
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
func (c *TableClient[T]) parseArgsWithOffset(args interface{}, offset int) (whereClause string, orderBy string, take int, skip int, selectCols []string, queryArgs []interface{}) {
|
|
1044
1194
|
\tif args == nil {
|
|
1045
1195
|
\t\treturn
|
|
1046
1196
|
\t}
|
|
@@ -1057,10 +1207,18 @@ func (c *TableClient[T]) parseArgs(args interface{}) (whereClause string, orderB
|
|
|
1057
1207
|
|
|
1058
1208
|
\t// Where
|
|
1059
1209
|
\twhereField := v.FieldByName("Where")
|
|
1060
|
-
\tif whereField.IsValid()
|
|
1061
|
-
\t\
|
|
1062
|
-
\t\t
|
|
1063
|
-
\t\
|
|
1210
|
+
\tif whereField.IsValid() {
|
|
1211
|
+
\t\tif !whereField.IsNil() {
|
|
1212
|
+
\t\t\telem := whereField
|
|
1213
|
+
\t\t\tif elem.Kind() == reflect.Ptr {
|
|
1214
|
+
\t\t\t\telem = elem.Elem()
|
|
1215
|
+
\t\t\t}
|
|
1216
|
+
\t\t\twc, wa := c.buildWhereFromStruct(elem, offset)
|
|
1217
|
+
\t\t\twhereClause = wc
|
|
1218
|
+
\t\t\tqueryArgs = wa
|
|
1219
|
+
\t\t}
|
|
1220
|
+
\t} else {
|
|
1221
|
+
\t\twc, wa := c.buildWhereFromStruct(v, offset)
|
|
1064
1222
|
\t\twhereClause = wc
|
|
1065
1223
|
\t\tqueryArgs = wa
|
|
1066
1224
|
\t}
|
|
@@ -238,27 +238,24 @@ class PythonGenerator {
|
|
|
238
238
|
content += '\n';
|
|
239
239
|
// TypedDict row shape: mirrors what the runtime actually returns
|
|
240
240
|
// (plain dicts), so `AdapterTableClient["<Model>Row"]` delegates
|
|
241
|
-
// type-check.
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
|
|
245
|
-
const rowOptional = model.fields.filter(f => f.isOptional || f.hasDefault);
|
|
241
|
+
// type-check. All table columns are guaranteed keys in fetched rows
|
|
242
|
+
// (nullable columns take Optional[...] values); relations stay
|
|
243
|
+
// optional keys (total=False) because they are only present when
|
|
244
|
+
// joined/included.
|
|
246
245
|
content += `class _${model.name}Required(TypedDict):\n`;
|
|
247
246
|
content += ` """Required keys of a ${model.name} row."""\n`;
|
|
248
|
-
if (
|
|
247
|
+
if (model.fields.length === 0) {
|
|
249
248
|
content += ' pass\n';
|
|
250
249
|
}
|
|
251
|
-
for (const f of
|
|
252
|
-
|
|
250
|
+
for (const f of model.fields) {
|
|
251
|
+
const pyType = f.isOptional ? `Optional[${this.mapPyType(f)}]` : this.mapPyType(f);
|
|
252
|
+
content += ` ${this.toSnakeCase(f.name)}: ${pyType}\n`;
|
|
253
253
|
}
|
|
254
254
|
content += `\nclass ${model.name}Row(_${model.name}Required, total=False):\n`;
|
|
255
255
|
content += ` """Row shape returned for ${model.name} queries."""\n`;
|
|
256
|
-
if (
|
|
256
|
+
if (model.relations.length === 0) {
|
|
257
257
|
content += ' pass\n';
|
|
258
258
|
}
|
|
259
|
-
for (const f of rowOptional) {
|
|
260
|
-
content += ` ${this.toSnakeCase(f.name)}: ${this.mapPyType(f)}\n`;
|
|
261
|
-
}
|
|
262
259
|
for (const rel of model.relations) {
|
|
263
260
|
const relName = this.toSnakeCase(rel.name);
|
|
264
261
|
content += rel.isArray ? ` ${relName}: List[Any]\n` : ` ${relName}: Any\n`;
|
|
@@ -305,9 +302,9 @@ class PythonGenerator {
|
|
|
305
302
|
content += `from an5_adapter import An5Adapter, AdapterTableClient${hasViews ? ', ViewClient' : ''}, create_an5_adapter, set_adapter_metadata\n\n`;
|
|
306
303
|
const metadataModule = this.metadataModule();
|
|
307
304
|
content += 'try:\n';
|
|
308
|
-
content += ` from .${metadataModule} import MODEL_TO_TABLE, MODEL_FIELDS\n`;
|
|
305
|
+
content += ` from .${metadataModule} import MODEL_TO_TABLE, MODEL_FIELDS, RELATION_MAP\n`;
|
|
309
306
|
content += 'except ImportError:\n';
|
|
310
|
-
content += ` from ${metadataModule} import MODEL_TO_TABLE, MODEL_FIELDS\n\n`;
|
|
307
|
+
content += ` from ${metadataModule} import MODEL_TO_TABLE, MODEL_FIELDS, RELATION_MAP\n\n`;
|
|
311
308
|
if (models.length > 0) {
|
|
312
309
|
content += 'if TYPE_CHECKING:\n';
|
|
313
310
|
for (const m of models) {
|
|
@@ -332,7 +329,7 @@ class PythonGenerator {
|
|
|
332
329
|
content += ' conn_str = (connection_string or \n';
|
|
333
330
|
content += ' os.getenv("DATABASE_URL") or \n';
|
|
334
331
|
content += ' "Server=localhost;Database=master;Trusted_Connection=True;TrustServerCertificate=True;")\n';
|
|
335
|
-
content += ' set_adapter_metadata({"model_to_table": MODEL_TO_TABLE, "model_fields": MODEL_FIELDS})\n';
|
|
332
|
+
content += ' set_adapter_metadata({"model_to_table": MODEL_TO_TABLE, "model_fields": MODEL_FIELDS, "relation_map": RELATION_MAP})\n';
|
|
336
333
|
content += ' self.adapter: An5Adapter = create_an5_adapter(conn_str)\n\n';
|
|
337
334
|
for (const model of models) {
|
|
338
335
|
const propName = this.toSnakeCase(model.name) + 's';
|
|
@@ -741,6 +741,14 @@ namespace An5Orm
|
|
|
741
741
|
return FindFirst("Id = @id", new Dictionary<string, object> { { "id", id } });
|
|
742
742
|
}
|
|
743
743
|
|
|
744
|
+
private static bool IsScalarColumn(PropertyInfo prop)
|
|
745
|
+
{
|
|
746
|
+
var t = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType;
|
|
747
|
+
if (t.IsPrimitive || t.IsEnum || t.IsValueType) return true;
|
|
748
|
+
if (t == typeof(string) || t == typeof(byte[]) || t == typeof(float[]) || t == typeof(double[])) return true;
|
|
749
|
+
return false;
|
|
750
|
+
}
|
|
751
|
+
|
|
744
752
|
public T Create(T entity)
|
|
745
753
|
{
|
|
746
754
|
var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
|
@@ -752,6 +760,7 @@ namespace An5Orm
|
|
|
752
760
|
|
|
753
761
|
foreach (var prop in properties)
|
|
754
762
|
{
|
|
763
|
+
if (!IsScalarColumn(prop)) continue;
|
|
755
764
|
var val = prop.GetValue(entity);
|
|
756
765
|
if (val != null)
|
|
757
766
|
{
|
|
@@ -793,6 +802,7 @@ namespace An5Orm
|
|
|
793
802
|
|
|
794
803
|
foreach (var prop in properties)
|
|
795
804
|
{
|
|
805
|
+
if (!IsScalarColumn(prop)) continue;
|
|
796
806
|
var val = prop.GetValue(entity);
|
|
797
807
|
if (prop.Name.Equals("Id", StringComparison.OrdinalIgnoreCase))
|
|
798
808
|
{
|
|
@@ -1071,6 +1081,98 @@ namespace An5Orm
|
|
|
1071
1081
|
return output;
|
|
1072
1082
|
}
|
|
1073
1083
|
|
|
1084
|
+
public Dictionary<string, object> Aggregate(
|
|
1085
|
+
string whereClause = null,
|
|
1086
|
+
Dictionary<string, object> parameters = null,
|
|
1087
|
+
bool count = false,
|
|
1088
|
+
List<string> sum = null,
|
|
1089
|
+
List<string> avg = null,
|
|
1090
|
+
List<string> min = null,
|
|
1091
|
+
List<string> max = null)
|
|
1092
|
+
{
|
|
1093
|
+
var selectParts = new List<string>();
|
|
1094
|
+
if (count) selectParts.Add("COUNT(*) AS _count");
|
|
1095
|
+
if (sum != null) foreach (var col in sum) selectParts.Add($"SUM({An5Provider.Quote(col, Dialect)}) AS _sum_{col}");
|
|
1096
|
+
if (avg != null) foreach (var col in avg) selectParts.Add($"AVG({An5Provider.Quote(col, Dialect)}) AS _avg_{col}");
|
|
1097
|
+
if (min != null) foreach (var col in min) selectParts.Add($"MIN({An5Provider.Quote(col, Dialect)}) AS _min_{col}");
|
|
1098
|
+
if (max != null) foreach (var col in max) selectParts.Add($"MAX({An5Provider.Quote(col, Dialect)}) AS _max_{col}");
|
|
1099
|
+
if (selectParts.Count == 0) selectParts.Add("COUNT(*) AS _count");
|
|
1100
|
+
|
|
1101
|
+
var query = $"SELECT {string.Join(", ", selectParts)} FROM {TableName}";
|
|
1102
|
+
if (!string.IsNullOrEmpty(whereClause)) query += $" WHERE {whereClause}";
|
|
1103
|
+
|
|
1104
|
+
var conn = An5DbContext.GetActiveConnection(ConnectionString, out bool isTx);
|
|
1105
|
+
try
|
|
1106
|
+
{
|
|
1107
|
+
using var cmd = CreateCommand(conn, query);
|
|
1108
|
+
if (parameters != null)
|
|
1109
|
+
{
|
|
1110
|
+
foreach (var kvp in parameters) An5Provider.Bind(cmd, kvp.Key, kvp.Value);
|
|
1111
|
+
}
|
|
1112
|
+
using var reader = cmd.ExecuteReader();
|
|
1113
|
+
var result = new Dictionary<string, object>();
|
|
1114
|
+
if (reader.Read())
|
|
1115
|
+
{
|
|
1116
|
+
for (int i = 0; i < reader.FieldCount; i++)
|
|
1117
|
+
{
|
|
1118
|
+
var name = reader.GetName(i);
|
|
1119
|
+
var val = reader[i];
|
|
1120
|
+
result[name] = val != DBNull.Value ? val : null;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
return result;
|
|
1124
|
+
}
|
|
1125
|
+
finally { if (!isTx) conn.Dispose(); }
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
public List<Dictionary<string, object>> GroupBy(
|
|
1129
|
+
List<string> by,
|
|
1130
|
+
string whereClause = null,
|
|
1131
|
+
Dictionary<string, object> parameters = null,
|
|
1132
|
+
List<string> sum = null,
|
|
1133
|
+
List<string> avg = null,
|
|
1134
|
+
List<string> min = null,
|
|
1135
|
+
List<string> max = null)
|
|
1136
|
+
{
|
|
1137
|
+
if (by == null || by.Count == 0) throw new ArgumentException("GroupBy requires at least one column in 'by'");
|
|
1138
|
+
var byCols = by.Select(c => An5Provider.Quote(c, Dialect)).ToList();
|
|
1139
|
+
var selectParts = new List<string>(byCols);
|
|
1140
|
+
selectParts.Add("COUNT(*) AS _count");
|
|
1141
|
+
if (sum != null) foreach (var col in sum) selectParts.Add($"SUM({An5Provider.Quote(col, Dialect)}) AS _sum_{col}");
|
|
1142
|
+
if (avg != null) foreach (var col in avg) selectParts.Add($"AVG({An5Provider.Quote(col, Dialect)}) AS _avg_{col}");
|
|
1143
|
+
if (min != null) foreach (var col in min) selectParts.Add($"MIN({An5Provider.Quote(col, Dialect)}) AS _min_{col}");
|
|
1144
|
+
if (max != null) foreach (var col in max) selectParts.Add($"MAX({An5Provider.Quote(col, Dialect)}) AS _max_{col}");
|
|
1145
|
+
|
|
1146
|
+
var query = $"SELECT {string.Join(", ", selectParts)} FROM {TableName}";
|
|
1147
|
+
if (!string.IsNullOrEmpty(whereClause)) query += $" WHERE {whereClause}";
|
|
1148
|
+
query += $" GROUP BY {string.Join(", ", byCols)}";
|
|
1149
|
+
|
|
1150
|
+
var conn = An5DbContext.GetActiveConnection(ConnectionString, out bool isTx);
|
|
1151
|
+
try
|
|
1152
|
+
{
|
|
1153
|
+
using var cmd = CreateCommand(conn, query);
|
|
1154
|
+
if (parameters != null)
|
|
1155
|
+
{
|
|
1156
|
+
foreach (var kvp in parameters) An5Provider.Bind(cmd, kvp.Key, kvp.Value);
|
|
1157
|
+
}
|
|
1158
|
+
using var reader = cmd.ExecuteReader();
|
|
1159
|
+
var rows = new List<Dictionary<string, object>>();
|
|
1160
|
+
while (reader.Read())
|
|
1161
|
+
{
|
|
1162
|
+
var row = new Dictionary<string, object>();
|
|
1163
|
+
for (int i = 0; i < reader.FieldCount; i++)
|
|
1164
|
+
{
|
|
1165
|
+
var name = reader.GetName(i);
|
|
1166
|
+
var val = reader[i];
|
|
1167
|
+
row[name] = val != DBNull.Value ? val : null;
|
|
1168
|
+
}
|
|
1169
|
+
rows.Add(row);
|
|
1170
|
+
}
|
|
1171
|
+
return rows;
|
|
1172
|
+
}
|
|
1173
|
+
finally { if (!isTx) conn.Dispose(); }
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1074
1176
|
private static List<double> ParseDoubleArray(string json)
|
|
1075
1177
|
{
|
|
1076
1178
|
var clean = json.Trim('[', ']');
|
|
@@ -1271,6 +1373,26 @@ namespace An5Orm
|
|
|
1271
1373
|
|
|
1272
1374
|
public List<T> VectorSearch(List<double> vector, int take = 10, string whereClause = null, Dictionary<string, object> parameters = null, string vectorField = "Embedding", string distanceMetric = "cosine")
|
|
1273
1375
|
=> _table.VectorSearch(vector, take, whereClause, parameters, vectorField, distanceMetric);
|
|
1376
|
+
|
|
1377
|
+
public Dictionary<string, object> Aggregate(
|
|
1378
|
+
string whereClause = null,
|
|
1379
|
+
Dictionary<string, object> parameters = null,
|
|
1380
|
+
bool count = false,
|
|
1381
|
+
List<string> sum = null,
|
|
1382
|
+
List<string> avg = null,
|
|
1383
|
+
List<string> min = null,
|
|
1384
|
+
List<string> max = null)
|
|
1385
|
+
=> _table.Aggregate(whereClause, parameters, count, sum, avg, min, max);
|
|
1386
|
+
|
|
1387
|
+
public List<Dictionary<string, object>> GroupBy(
|
|
1388
|
+
List<string> by,
|
|
1389
|
+
string whereClause = null,
|
|
1390
|
+
Dictionary<string, object> parameters = null,
|
|
1391
|
+
List<string> sum = null,
|
|
1392
|
+
List<string> avg = null,
|
|
1393
|
+
List<string> min = null,
|
|
1394
|
+
List<string> max = null)
|
|
1395
|
+
=> _table.GroupBy(by, whereClause, parameters, sum, avg, min, max);
|
|
1274
1396
|
}
|
|
1275
1397
|
}
|
|
1276
1398
|
`;
|
|
@@ -273,14 +273,34 @@ type BoolFilter struct {
|
|
|
273
273
|
|
|
274
274
|
// DateTimeFilter provides type-safe datetime field filtering options.
|
|
275
275
|
type DateTimeFilter struct {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
276
|
+
Equals *time.Time
|
|
277
|
+
Not *time.Time
|
|
278
|
+
In []time.Time
|
|
279
|
+
NotIn []time.Time
|
|
280
|
+
Gt *time.Time
|
|
281
|
+
Gte *time.Time
|
|
282
|
+
Lt *time.Time
|
|
283
|
+
Lte *time.Time
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// AggregateArgs configures an aggregate query.
|
|
287
|
+
type AggregateArgs struct {
|
|
288
|
+
Where interface{}
|
|
289
|
+
Count bool
|
|
290
|
+
Sum []string
|
|
291
|
+
Avg []string
|
|
292
|
+
Min []string
|
|
293
|
+
Max []string
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// GroupByArgs configures a GroupBy query.
|
|
297
|
+
type GroupByArgs struct {
|
|
298
|
+
By []string
|
|
299
|
+
Where interface{}
|
|
300
|
+
Sum []string
|
|
301
|
+
Avg []string
|
|
302
|
+
Min []string
|
|
303
|
+
Max []string
|
|
284
304
|
}
|
|
285
305
|
|
|
286
306
|
// Dialect identifies the database engine in use.
|
|
@@ -458,6 +478,16 @@ func (c *ViewClient[T]) VectorSearch(ctx context.Context, vector []float64, opts
|
|
|
458
478
|
\treturn c.table.VectorSearch(ctx, vector, opts...)
|
|
459
479
|
}
|
|
460
480
|
|
|
481
|
+
// Aggregate returns aggregate calculations on the view.
|
|
482
|
+
func (c *ViewClient[T]) Aggregate(ctx context.Context, args AggregateArgs) (map[string]interface{}, error) {
|
|
483
|
+
\treturn c.table.Aggregate(ctx, args)
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// GroupBy groups view rows by specified columns and computes aggregates.
|
|
487
|
+
func (c *ViewClient[T]) GroupBy(ctx context.Context, args GroupByArgs) ([]map[string]interface{}, error) {
|
|
488
|
+
\treturn c.table.GroupBy(ctx, args)
|
|
489
|
+
}
|
|
490
|
+
|
|
461
491
|
`;
|
|
462
492
|
}
|
|
463
493
|
|
|
@@ -976,7 +1006,7 @@ func (c *TableClient[T]) UpdateMany(ctx context.Context, whereArgs interface{},
|
|
|
976
1006
|
\t\tsets = append(sets, c.quoteName(col)+" = "+c.ph(len(execArgs)))
|
|
977
1007
|
\t}
|
|
978
1008
|
|
|
979
|
-
\tclause, _, _, _, _, whereQueryArgs := c.
|
|
1009
|
+
\tclause, _, _, _, _, whereQueryArgs := c.parseArgsWithOffset(whereArgs, len(execArgs)+1)
|
|
980
1010
|
\texecArgs = append(execArgs, whereQueryArgs...)
|
|
981
1011
|
\tquery := fmt.Sprintf("UPDATE %s SET %s", c.quoteTable(), strings.Join(sets, ", "))
|
|
982
1012
|
\tif clause != "" {
|
|
@@ -1061,9 +1091,129 @@ func (c *TableClient[T]) Transaction(ctx context.Context, fn func(tx *sql.Tx) er
|
|
|
1061
1091
|
\treturn tx.Commit()
|
|
1062
1092
|
}
|
|
1063
1093
|
|
|
1094
|
+
// Aggregate returns aggregate calculations (count, sum, avg, min, max).
|
|
1095
|
+
func (c *TableClient[T]) Aggregate(ctx context.Context, args AggregateArgs) (map[string]interface{}, error) {
|
|
1096
|
+
\tclause, _, _, _, _, queryArgs := c.parseArgs(args.Where)
|
|
1097
|
+
\tvar selectParts []string
|
|
1098
|
+
\tif args.Count || (len(args.Sum) == 0 && len(args.Avg) == 0 && len(args.Min) == 0 && len(args.Max) == 0) {
|
|
1099
|
+
\t\tselectParts = append(selectParts, "COUNT(*) AS _count")
|
|
1100
|
+
\t}
|
|
1101
|
+
\tfor _, col := range args.Sum {
|
|
1102
|
+
\t\tselectParts = append(selectParts, fmt.Sprintf("SUM(%s) AS _sum_%s", c.quoteName(col), col))
|
|
1103
|
+
\t}
|
|
1104
|
+
\tfor _, col := range args.Avg {
|
|
1105
|
+
\t\tselectParts = append(selectParts, fmt.Sprintf("AVG(%s) AS _avg_%s", c.quoteName(col), col))
|
|
1106
|
+
\t}
|
|
1107
|
+
\tfor _, col := range args.Min {
|
|
1108
|
+
\t\tselectParts = append(selectParts, fmt.Sprintf("MIN(%s) AS _min_%s", c.quoteName(col), col))
|
|
1109
|
+
\t}
|
|
1110
|
+
\tfor _, col := range args.Max {
|
|
1111
|
+
\t\tselectParts = append(selectParts, fmt.Sprintf("MAX(%s) AS _max_%s", c.quoteName(col), col))
|
|
1112
|
+
\t}
|
|
1113
|
+
|
|
1114
|
+
\tquery := fmt.Sprintf("SELECT %s FROM %s", strings.Join(selectParts, ", "), c.quoteTable())
|
|
1115
|
+
\tif clause != "" {
|
|
1116
|
+
\t\tquery += " WHERE " + clause
|
|
1117
|
+
\t}
|
|
1118
|
+
|
|
1119
|
+
\trows, err := c.DB.QueryContext(ctx, query, queryArgs...)
|
|
1120
|
+
\tif err != nil {
|
|
1121
|
+
\t\treturn nil, err
|
|
1122
|
+
\t}
|
|
1123
|
+
\tdefer rows.Close()
|
|
1124
|
+
|
|
1125
|
+
\tcols, err := rows.Columns()
|
|
1126
|
+
\tif err != nil {
|
|
1127
|
+
\t\treturn nil, err
|
|
1128
|
+
\t}
|
|
1129
|
+
|
|
1130
|
+
\tres := make(map[string]interface{})
|
|
1131
|
+
\tif rows.Next() {
|
|
1132
|
+
\t\tcolVals := make([]interface{}, len(cols))
|
|
1133
|
+
\t\tcolPtrs := make([]interface{}, len(cols))
|
|
1134
|
+
\t\tfor i := range colVals {
|
|
1135
|
+
\t\t\tcolPtrs[i] = &colVals[i]
|
|
1136
|
+
\t\t}
|
|
1137
|
+
\t\tif err := rows.Scan(colPtrs...); err != nil {
|
|
1138
|
+
\t\t\treturn nil, err
|
|
1139
|
+
\t\t}
|
|
1140
|
+
\t\tfor i, name := range cols {
|
|
1141
|
+
\t\t\tres[name] = colVals[i]
|
|
1142
|
+
\t\t}
|
|
1143
|
+
\t}
|
|
1144
|
+
\treturn res, nil
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
// GroupBy groups records by specified columns and computes aggregates.
|
|
1148
|
+
func (c *TableClient[T]) GroupBy(ctx context.Context, args GroupByArgs) ([]map[string]interface{}, error) {
|
|
1149
|
+
\tif len(args.By) == 0 {
|
|
1150
|
+
\t\treturn nil, fmt.Errorf("GroupBy requires at least one column in 'By'")
|
|
1151
|
+
\t}
|
|
1152
|
+
\tclause, _, _, _, _, queryArgs := c.parseArgs(args.Where)
|
|
1153
|
+
\tbyCols := make([]string, len(args.By))
|
|
1154
|
+
\tfor i, col := range args.By {
|
|
1155
|
+
\t\tbyCols[i] = c.quoteName(col)
|
|
1156
|
+
\t}
|
|
1157
|
+
|
|
1158
|
+
\tselectParts := make([]string, len(byCols))
|
|
1159
|
+
\tcopy(selectParts, byCols)
|
|
1160
|
+
\tselectParts = append(selectParts, "COUNT(*) AS _count")
|
|
1161
|
+
\tfor _, col := range args.Sum {
|
|
1162
|
+
\t\tselectParts = append(selectParts, fmt.Sprintf("SUM(%s) AS _sum_%s", c.quoteName(col), col))
|
|
1163
|
+
\t}
|
|
1164
|
+
\tfor _, col := range args.Avg {
|
|
1165
|
+
\t\tselectParts = append(selectParts, fmt.Sprintf("AVG(%s) AS _avg_%s", c.quoteName(col), col))
|
|
1166
|
+
\t}
|
|
1167
|
+
\tfor _, col := range args.Min {
|
|
1168
|
+
\t\tselectParts = append(selectParts, fmt.Sprintf("MIN(%s) AS _min_%s", c.quoteName(col), col))
|
|
1169
|
+
\t}
|
|
1170
|
+
\tfor _, col := range args.Max {
|
|
1171
|
+
\t\tselectParts = append(selectParts, fmt.Sprintf("MAX(%s) AS _max_%s", c.quoteName(col), col))
|
|
1172
|
+
\t}
|
|
1173
|
+
|
|
1174
|
+
\tquery := fmt.Sprintf("SELECT %s FROM %s", strings.Join(selectParts, ", "), c.quoteTable())
|
|
1175
|
+
\tif clause != "" {
|
|
1176
|
+
\t\tquery += " WHERE " + clause
|
|
1177
|
+
\t}
|
|
1178
|
+
\tquery += " GROUP BY " + strings.Join(byCols, ", ")
|
|
1179
|
+
|
|
1180
|
+
\trows, err := c.DB.QueryContext(ctx, query, queryArgs...)
|
|
1181
|
+
\tif err != nil {
|
|
1182
|
+
\t\treturn nil, err
|
|
1183
|
+
\t}
|
|
1184
|
+
\tdefer rows.Close()
|
|
1185
|
+
|
|
1186
|
+
\tcols, err := rows.Columns()
|
|
1187
|
+
\tif err != nil {
|
|
1188
|
+
\t\treturn nil, err
|
|
1189
|
+
\t}
|
|
1190
|
+
|
|
1191
|
+
\tvar results []map[string]interface{}
|
|
1192
|
+
\tfor rows.Next() {
|
|
1193
|
+
\t\tcolVals := make([]interface{}, len(cols))
|
|
1194
|
+
\t\tcolPtrs := make([]interface{}, len(cols))
|
|
1195
|
+
\t\tfor i := range colVals {
|
|
1196
|
+
\t\t\tcolPtrs[i] = &colVals[i]
|
|
1197
|
+
\t\t}
|
|
1198
|
+
\t\tif err := rows.Scan(colPtrs...); err != nil {
|
|
1199
|
+
\t\t\treturn nil, err
|
|
1200
|
+
\t\t}
|
|
1201
|
+
\t\trowMap := make(map[string]interface{})
|
|
1202
|
+
\t\tfor i, name := range cols {
|
|
1203
|
+
\t\t\trowMap[name] = colVals[i]
|
|
1204
|
+
\t\t}
|
|
1205
|
+
\t\tresults = append(results, rowMap)
|
|
1206
|
+
\t}
|
|
1207
|
+
\treturn results, nil
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1064
1210
|
// parseArgs is the generic where/orderBy/take/skip extractor.
|
|
1065
1211
|
// It uses reflection to read WhereClause, OrderBy, Take, Skip, Select from any args struct.
|
|
1066
1212
|
func (c *TableClient[T]) parseArgs(args interface{}) (whereClause string, orderBy string, take int, skip int, selectCols []string, queryArgs []interface{}) {
|
|
1213
|
+
\treturn c.parseArgsWithOffset(args, 1)
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
func (c *TableClient[T]) parseArgsWithOffset(args interface{}, offset int) (whereClause string, orderBy string, take int, skip int, selectCols []string, queryArgs []interface{}) {
|
|
1067
1217
|
\tif args == nil {
|
|
1068
1218
|
\t\treturn
|
|
1069
1219
|
\t}
|
|
@@ -1080,10 +1230,18 @@ func (c *TableClient[T]) parseArgs(args interface{}) (whereClause string, orderB
|
|
|
1080
1230
|
|
|
1081
1231
|
\t// Where
|
|
1082
1232
|
\twhereField := v.FieldByName("Where")
|
|
1083
|
-
\tif whereField.IsValid()
|
|
1084
|
-
\t\
|
|
1085
|
-
\t\t
|
|
1086
|
-
\t\
|
|
1233
|
+
\tif whereField.IsValid() {
|
|
1234
|
+
\t\tif !whereField.IsNil() {
|
|
1235
|
+
\t\t\telem := whereField
|
|
1236
|
+
\t\t\tif elem.Kind() == reflect.Ptr {
|
|
1237
|
+
\t\t\t\telem = elem.Elem()
|
|
1238
|
+
\t\t\t}
|
|
1239
|
+
\t\t\twc, wa := c.buildWhereFromStruct(elem, offset)
|
|
1240
|
+
\t\t\twhereClause = wc
|
|
1241
|
+
\t\t\tqueryArgs = wa
|
|
1242
|
+
\t\t}
|
|
1243
|
+
\t} else {
|
|
1244
|
+
\t\twc, wa := c.buildWhereFromStruct(v, offset)
|
|
1087
1245
|
\t\twhereClause = wc
|
|
1088
1246
|
\t\tqueryArgs = wa
|
|
1089
1247
|
\t}
|
|
@@ -271,27 +271,24 @@ export class PythonGenerator {
|
|
|
271
271
|
|
|
272
272
|
// TypedDict row shape: mirrors what the runtime actually returns
|
|
273
273
|
// (plain dicts), so `AdapterTableClient["<Model>Row"]` delegates
|
|
274
|
-
// type-check.
|
|
275
|
-
//
|
|
276
|
-
//
|
|
277
|
-
|
|
278
|
-
const rowOptional = model.fields.filter(f => f.isOptional || f.hasDefault);
|
|
274
|
+
// type-check. All table columns are guaranteed keys in fetched rows
|
|
275
|
+
// (nullable columns take Optional[...] values); relations stay
|
|
276
|
+
// optional keys (total=False) because they are only present when
|
|
277
|
+
// joined/included.
|
|
279
278
|
content += `class _${model.name}Required(TypedDict):\n`;
|
|
280
279
|
content += ` """Required keys of a ${model.name} row."""\n`;
|
|
281
|
-
if (
|
|
280
|
+
if (model.fields.length === 0) {
|
|
282
281
|
content += ' pass\n';
|
|
283
282
|
}
|
|
284
|
-
for (const f of
|
|
285
|
-
|
|
283
|
+
for (const f of model.fields) {
|
|
284
|
+
const pyType = f.isOptional ? `Optional[${this.mapPyType(f)}]` : this.mapPyType(f);
|
|
285
|
+
content += ` ${this.toSnakeCase(f.name)}: ${pyType}\n`;
|
|
286
286
|
}
|
|
287
287
|
content += `\nclass ${model.name}Row(_${model.name}Required, total=False):\n`;
|
|
288
288
|
content += ` """Row shape returned for ${model.name} queries."""\n`;
|
|
289
|
-
if (
|
|
289
|
+
if (model.relations.length === 0) {
|
|
290
290
|
content += ' pass\n';
|
|
291
291
|
}
|
|
292
|
-
for (const f of rowOptional) {
|
|
293
|
-
content += ` ${this.toSnakeCase(f.name)}: ${this.mapPyType(f)}\n`;
|
|
294
|
-
}
|
|
295
292
|
for (const rel of model.relations) {
|
|
296
293
|
const relName = this.toSnakeCase(rel.name);
|
|
297
294
|
content += rel.isArray ? ` ${relName}: List[Any]\n` : ` ${relName}: Any\n`;
|
|
@@ -343,9 +340,9 @@ export class PythonGenerator {
|
|
|
343
340
|
|
|
344
341
|
const metadataModule = this.metadataModule();
|
|
345
342
|
content += 'try:\n';
|
|
346
|
-
content += ` from .${metadataModule} import MODEL_TO_TABLE, MODEL_FIELDS\n`;
|
|
343
|
+
content += ` from .${metadataModule} import MODEL_TO_TABLE, MODEL_FIELDS, RELATION_MAP\n`;
|
|
347
344
|
content += 'except ImportError:\n';
|
|
348
|
-
content += ` from ${metadataModule} import MODEL_TO_TABLE, MODEL_FIELDS\n\n`;
|
|
345
|
+
content += ` from ${metadataModule} import MODEL_TO_TABLE, MODEL_FIELDS, RELATION_MAP\n\n`;
|
|
349
346
|
|
|
350
347
|
if (models.length > 0) {
|
|
351
348
|
content += 'if TYPE_CHECKING:\n';
|
|
@@ -372,7 +369,7 @@ export class PythonGenerator {
|
|
|
372
369
|
content += ' conn_str = (connection_string or \n';
|
|
373
370
|
content += ' os.getenv("DATABASE_URL") or \n';
|
|
374
371
|
content += ' "Server=localhost;Database=master;Trusted_Connection=True;TrustServerCertificate=True;")\n';
|
|
375
|
-
content += ' set_adapter_metadata({"model_to_table": MODEL_TO_TABLE, "model_fields": MODEL_FIELDS})\n';
|
|
372
|
+
content += ' set_adapter_metadata({"model_to_table": MODEL_TO_TABLE, "model_fields": MODEL_FIELDS, "relation_map": RELATION_MAP})\n';
|
|
376
373
|
content += ' self.adapter: An5Adapter = create_an5_adapter(conn_str)\n\n';
|
|
377
374
|
|
|
378
375
|
for (const model of models) {
|