@an5/orm 1.3.4 → 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/an5Orm.js +4 -2
- package/dist/cli.js +0 -0
- package/dist/generator/src/dotnet-generator.js +160 -0
- package/dist/generator/src/golang-generator.js +171 -13
- package/dist/generator/src/pull-schema.d.ts +2 -0
- package/dist/generator/src/pull-schema.js +11 -1
- package/dist/generator/src/python-generator.js +12 -15
- package/dist/orm.js +4 -2
- package/dist/pull.js +18 -5
- package/generator/src/dotnet-generator.ts +160 -0
- package/generator/src/golang-generator.ts +171 -13
- package/generator/src/pull-schema.ts +12 -1
- package/generator/src/python-generator.ts +12 -15
- package/package.json +1 -1
package/dist/an5Orm.js
CHANGED
|
@@ -806,8 +806,9 @@ class TableClient {
|
|
|
806
806
|
const vectorJson = JSON.stringify(finalArgs.vector);
|
|
807
807
|
const col = (0, sql_utils_1.quoteIdentifier)(field);
|
|
808
808
|
try {
|
|
809
|
-
|
|
810
|
-
|
|
809
|
+
const maxDim = elementType === 'float16' ? 3996 : 1998;
|
|
810
|
+
if (dim > maxDim) {
|
|
811
|
+
throw new Error(`Vector dimension ${dim} exceeds SQL Server limit of ${maxDim} for ${elementType}`);
|
|
811
812
|
}
|
|
812
813
|
let sqlText = `SELECT TOP (${take}) *, `;
|
|
813
814
|
sqlText += `VECTOR_DISTANCE('${metric}', CAST(${col} AS VECTOR(${dim}, ${elementType})), CAST(@query_vector AS VECTOR(${dim}, ${elementType}))) AS distance `;
|
|
@@ -845,6 +846,7 @@ class TableClient {
|
|
|
845
846
|
msg.includes("not a recognized built-in function") ||
|
|
846
847
|
msg.includes("not a defined system type") ||
|
|
847
848
|
msg.includes("limit of 1998") ||
|
|
849
|
+
msg.includes("limit of 3996") ||
|
|
848
850
|
err?.number === 195 ||
|
|
849
851
|
err?.number === 102 ||
|
|
850
852
|
err?.number === 319 ||
|
package/dist/cli.js
CHANGED
|
File without changes
|
|
@@ -402,6 +402,19 @@ namespace An5Orm
|
|
|
402
402
|
// \`float[]\` does not convert to it.
|
|
403
403
|
if (value is float[] floats) return FormatVector(floats.Select(v => (double)v));
|
|
404
404
|
if (value is double[] doubles) return FormatVector(doubles);
|
|
405
|
+
if (value is ReadOnlyMemory<float> rom) return FormatVector(rom.ToArray().Select(v => (double)v));
|
|
406
|
+
if (value is Memory<float> mem) return FormatVector(mem.ToArray().Select(v => (double)v));
|
|
407
|
+
if (value is ReadOnlyMemory<double> romD) return FormatVector(romD.ToArray());
|
|
408
|
+
if (value is Memory<double> memD) return FormatVector(memD.ToArray());
|
|
409
|
+
if (value != null && value.GetType().Name.StartsWith("SqlVector"))
|
|
410
|
+
{
|
|
411
|
+
var memProp = value.GetType().GetProperty("Memory");
|
|
412
|
+
if (memProp != null)
|
|
413
|
+
{
|
|
414
|
+
var memVal = memProp.GetValue(value);
|
|
415
|
+
if (memVal != null) return Bindable(memVal);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
405
418
|
return value;
|
|
406
419
|
}
|
|
407
420
|
|
|
@@ -710,6 +723,14 @@ namespace An5Orm
|
|
|
710
723
|
return FindFirst("Id = @id", new Dictionary<string, object> { { "id", id } });
|
|
711
724
|
}
|
|
712
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
|
+
|
|
713
734
|
public T Create(T entity)
|
|
714
735
|
{
|
|
715
736
|
var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
|
@@ -721,6 +742,7 @@ namespace An5Orm
|
|
|
721
742
|
|
|
722
743
|
foreach (var prop in properties)
|
|
723
744
|
{
|
|
745
|
+
if (!IsScalarColumn(prop)) continue;
|
|
724
746
|
var val = prop.GetValue(entity);
|
|
725
747
|
if (val != null)
|
|
726
748
|
{
|
|
@@ -762,6 +784,7 @@ namespace An5Orm
|
|
|
762
784
|
|
|
763
785
|
foreach (var prop in properties)
|
|
764
786
|
{
|
|
787
|
+
if (!IsScalarColumn(prop)) continue;
|
|
765
788
|
var val = prop.GetValue(entity);
|
|
766
789
|
if (prop.Name.Equals("Id", StringComparison.OrdinalIgnoreCase))
|
|
767
790
|
{
|
|
@@ -1040,6 +1063,98 @@ namespace An5Orm
|
|
|
1040
1063
|
return output;
|
|
1041
1064
|
}
|
|
1042
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
|
+
|
|
1043
1158
|
private static List<double> ParseDoubleArray(string json)
|
|
1044
1159
|
{
|
|
1045
1160
|
var clean = json.Trim('[', ']');
|
|
@@ -1095,6 +1210,31 @@ namespace An5Orm
|
|
|
1095
1210
|
for (int i = 0; i < asDouble.Length; i++) fromDouble[i] = (float)asDouble[i];
|
|
1096
1211
|
return fromDouble;
|
|
1097
1212
|
}
|
|
1213
|
+
if (value is ReadOnlyMemory<float> rom) return rom.ToArray();
|
|
1214
|
+
if (value is Memory<float> mem) return mem.ToArray();
|
|
1215
|
+
if (value is ReadOnlyMemory<double> romD)
|
|
1216
|
+
{
|
|
1217
|
+
var arr = romD.ToArray();
|
|
1218
|
+
var fromDouble = new float[arr.Length];
|
|
1219
|
+
for (int i = 0; i < arr.Length; i++) fromDouble[i] = (float)arr[i];
|
|
1220
|
+
return fromDouble;
|
|
1221
|
+
}
|
|
1222
|
+
if (value is Memory<double> memD)
|
|
1223
|
+
{
|
|
1224
|
+
var arr = memD.ToArray();
|
|
1225
|
+
var fromDouble = new float[arr.Length];
|
|
1226
|
+
for (int i = 0; i < arr.Length; i++) fromDouble[i] = (float)arr[i];
|
|
1227
|
+
return fromDouble;
|
|
1228
|
+
}
|
|
1229
|
+
if (value != null && value.GetType().Name.StartsWith("SqlVector"))
|
|
1230
|
+
{
|
|
1231
|
+
var memProp = value.GetType().GetProperty("Memory");
|
|
1232
|
+
if (memProp != null)
|
|
1233
|
+
{
|
|
1234
|
+
var memVal = memProp.GetValue(value);
|
|
1235
|
+
if (memVal != null) return DecodeVectorColumn(memVal);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1098
1238
|
|
|
1099
1239
|
string text = value as string;
|
|
1100
1240
|
byte[] bytes = value as byte[];
|
|
@@ -1215,6 +1355,26 @@ namespace An5Orm
|
|
|
1215
1355
|
|
|
1216
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")
|
|
1217
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);
|
|
1218
1378
|
}
|
|
1219
1379
|
}
|
|
1220
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}
|
|
@@ -23,7 +23,17 @@ const dialect_1 = require("./dialect");
|
|
|
23
23
|
function sqlServerTypeForColumn(col) {
|
|
24
24
|
let sqlType = col.dataType.toUpperCase();
|
|
25
25
|
const dt = col.dataType.toLowerCase();
|
|
26
|
-
if (dt === '
|
|
26
|
+
if (dt === 'vector') {
|
|
27
|
+
if (col.vectorDimensions && col.vectorDimensions > 0) {
|
|
28
|
+
const baseType = col.vectorBaseType?.toLowerCase();
|
|
29
|
+
if (baseType && baseType !== 'float32') {
|
|
30
|
+
return `VECTOR(${col.vectorDimensions}, ${baseType})`;
|
|
31
|
+
}
|
|
32
|
+
return `VECTOR(${col.vectorDimensions})`;
|
|
33
|
+
}
|
|
34
|
+
return 'VECTOR';
|
|
35
|
+
}
|
|
36
|
+
else if (dt === 'nvarchar' || dt === 'varchar') {
|
|
27
37
|
sqlType = col.maxLength === -1 ? `${sqlType.toUpperCase()}(MAX)` : `${sqlType.toUpperCase()}(${col.maxLength / 2})`;
|
|
28
38
|
}
|
|
29
39
|
else if (dt === 'char' || dt === 'nchar') {
|
|
@@ -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';
|
package/dist/orm.js
CHANGED
|
@@ -759,8 +759,9 @@ class TableClient extends adapters_1.AdapterTableClient {
|
|
|
759
759
|
const vectorJson = JSON.stringify(finalArgs.vector);
|
|
760
760
|
const col = (0, sql_utils_1.quoteIdentifier)(field);
|
|
761
761
|
try {
|
|
762
|
-
|
|
763
|
-
|
|
762
|
+
const maxDim = elementType === 'float16' ? 3996 : 1998;
|
|
763
|
+
if (dim > maxDim) {
|
|
764
|
+
throw new Error(`Vector dimension ${dim} exceeds SQL Server limit of ${maxDim} for ${elementType}`);
|
|
764
765
|
}
|
|
765
766
|
let sqlText = `SELECT TOP (${take}) *, `;
|
|
766
767
|
sqlText += `VECTOR_DISTANCE('${metric}', CAST(${col} AS VECTOR(${dim}, ${elementType})), CAST(@query_vector AS VECTOR(${dim}, ${elementType}))) AS distance `;
|
|
@@ -798,6 +799,7 @@ class TableClient extends adapters_1.AdapterTableClient {
|
|
|
798
799
|
msg.includes("not a recognized built-in function") ||
|
|
799
800
|
msg.includes("not a defined system type") ||
|
|
800
801
|
msg.includes("limit of 1998") ||
|
|
802
|
+
msg.includes("limit of 3996") ||
|
|
801
803
|
err?.number === 195 ||
|
|
802
804
|
err?.number === 102 ||
|
|
803
805
|
err?.number === 319 ||
|
package/dist/pull.js
CHANGED
|
@@ -49,7 +49,20 @@ async function pull() {
|
|
|
49
49
|
// nothing about the connection string being wrong.
|
|
50
50
|
refuseIfNotSqlServer((0, config_1.providerFromConfig)(config, process.env), "db:pull");
|
|
51
51
|
console.log("🔍 Pulling schema from database...");
|
|
52
|
-
const
|
|
52
|
+
const db = await getDb();
|
|
53
|
+
// Safely detect if the SQL Server catalog supports native VECTOR metadata columns
|
|
54
|
+
let hasVectorCatalog = false;
|
|
55
|
+
try {
|
|
56
|
+
const vecCheck = await db.$queryRawUnsafe(`SELECT COL_LENGTH('sys.columns', 'vector_dimensions') AS hasVec`);
|
|
57
|
+
hasVectorCatalog = !!(vecCheck?.[0]?.hasVec);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
hasVectorCatalog = false;
|
|
61
|
+
}
|
|
62
|
+
const vectorColumnSelect = hasVectorCatalog
|
|
63
|
+
? ",\n c.vector_dimensions AS vectorDimensions,\n c.vector_base_type_desc AS vectorBaseType"
|
|
64
|
+
: "";
|
|
65
|
+
const tables = await db.$queryRawUnsafe(`
|
|
53
66
|
SELECT s.name AS schemaName, t.name AS tableName
|
|
54
67
|
FROM sys.tables t
|
|
55
68
|
JOIN sys.schemas s ON t.schema_id = s.schema_id
|
|
@@ -123,7 +136,7 @@ async function pull() {
|
|
|
123
136
|
const { tableName, schemaName } = table;
|
|
124
137
|
console.log(`Introspecting table [${tableName}]...`);
|
|
125
138
|
// Get columns
|
|
126
|
-
const columns = await
|
|
139
|
+
const columns = await db.$queryRawUnsafe(`
|
|
127
140
|
SELECT
|
|
128
141
|
c.name AS columnName,
|
|
129
142
|
ty.name AS dataType,
|
|
@@ -133,7 +146,7 @@ async function pull() {
|
|
|
133
146
|
c.is_nullable AS isNullable,
|
|
134
147
|
c.is_identity AS isIdentity,
|
|
135
148
|
pk.is_primary_key AS isPrimaryKey,
|
|
136
|
-
d.definition AS defaultValue
|
|
149
|
+
d.definition AS defaultValue${vectorColumnSelect}
|
|
137
150
|
FROM sys.columns c
|
|
138
151
|
JOIN sys.types ty ON c.user_type_id = ty.user_type_id
|
|
139
152
|
LEFT JOIN (
|
|
@@ -351,14 +364,14 @@ async function pull() {
|
|
|
351
364
|
?? (existingViewNames[legacy] ? legacy : (0, pull_schema_1.blockNameForObject)(viewName));
|
|
352
365
|
const targetFile = viewToFileMap[modelName] ?? viewToFileMap[key] ?? viewToFileMap[viewName] ?? defaultFile;
|
|
353
366
|
console.log(`Introspecting view [${key}]...`);
|
|
354
|
-
const columns = await
|
|
367
|
+
const columns = await db.$queryRawUnsafe(`
|
|
355
368
|
SELECT
|
|
356
369
|
c.name AS columnName,
|
|
357
370
|
ty.name AS dataType,
|
|
358
371
|
c.max_length AS maxLength,
|
|
359
372
|
c.precision AS precision,
|
|
360
373
|
c.scale AS scale,
|
|
361
|
-
c.is_nullable AS isNullable
|
|
374
|
+
c.is_nullable AS isNullable${vectorColumnSelect}
|
|
362
375
|
FROM sys.columns c
|
|
363
376
|
JOIN sys.types ty ON c.user_type_id = ty.user_type_id
|
|
364
377
|
WHERE c.object_id = OBJECT_ID(@p_0)
|
|
@@ -418,6 +418,19 @@ namespace An5Orm
|
|
|
418
418
|
// \`float[]\` does not convert to it.
|
|
419
419
|
if (value is float[] floats) return FormatVector(floats.Select(v => (double)v));
|
|
420
420
|
if (value is double[] doubles) return FormatVector(doubles);
|
|
421
|
+
if (value is ReadOnlyMemory<float> rom) return FormatVector(rom.ToArray().Select(v => (double)v));
|
|
422
|
+
if (value is Memory<float> mem) return FormatVector(mem.ToArray().Select(v => (double)v));
|
|
423
|
+
if (value is ReadOnlyMemory<double> romD) return FormatVector(romD.ToArray());
|
|
424
|
+
if (value is Memory<double> memD) return FormatVector(memD.ToArray());
|
|
425
|
+
if (value != null && value.GetType().Name.StartsWith("SqlVector"))
|
|
426
|
+
{
|
|
427
|
+
var memProp = value.GetType().GetProperty("Memory");
|
|
428
|
+
if (memProp != null)
|
|
429
|
+
{
|
|
430
|
+
var memVal = memProp.GetValue(value);
|
|
431
|
+
if (memVal != null) return Bindable(memVal);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
421
434
|
return value;
|
|
422
435
|
}
|
|
423
436
|
|
|
@@ -728,6 +741,14 @@ namespace An5Orm
|
|
|
728
741
|
return FindFirst("Id = @id", new Dictionary<string, object> { { "id", id } });
|
|
729
742
|
}
|
|
730
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
|
+
|
|
731
752
|
public T Create(T entity)
|
|
732
753
|
{
|
|
733
754
|
var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
|
@@ -739,6 +760,7 @@ namespace An5Orm
|
|
|
739
760
|
|
|
740
761
|
foreach (var prop in properties)
|
|
741
762
|
{
|
|
763
|
+
if (!IsScalarColumn(prop)) continue;
|
|
742
764
|
var val = prop.GetValue(entity);
|
|
743
765
|
if (val != null)
|
|
744
766
|
{
|
|
@@ -780,6 +802,7 @@ namespace An5Orm
|
|
|
780
802
|
|
|
781
803
|
foreach (var prop in properties)
|
|
782
804
|
{
|
|
805
|
+
if (!IsScalarColumn(prop)) continue;
|
|
783
806
|
var val = prop.GetValue(entity);
|
|
784
807
|
if (prop.Name.Equals("Id", StringComparison.OrdinalIgnoreCase))
|
|
785
808
|
{
|
|
@@ -1058,6 +1081,98 @@ namespace An5Orm
|
|
|
1058
1081
|
return output;
|
|
1059
1082
|
}
|
|
1060
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
|
+
|
|
1061
1176
|
private static List<double> ParseDoubleArray(string json)
|
|
1062
1177
|
{
|
|
1063
1178
|
var clean = json.Trim('[', ']');
|
|
@@ -1113,6 +1228,31 @@ namespace An5Orm
|
|
|
1113
1228
|
for (int i = 0; i < asDouble.Length; i++) fromDouble[i] = (float)asDouble[i];
|
|
1114
1229
|
return fromDouble;
|
|
1115
1230
|
}
|
|
1231
|
+
if (value is ReadOnlyMemory<float> rom) return rom.ToArray();
|
|
1232
|
+
if (value is Memory<float> mem) return mem.ToArray();
|
|
1233
|
+
if (value is ReadOnlyMemory<double> romD)
|
|
1234
|
+
{
|
|
1235
|
+
var arr = romD.ToArray();
|
|
1236
|
+
var fromDouble = new float[arr.Length];
|
|
1237
|
+
for (int i = 0; i < arr.Length; i++) fromDouble[i] = (float)arr[i];
|
|
1238
|
+
return fromDouble;
|
|
1239
|
+
}
|
|
1240
|
+
if (value is Memory<double> memD)
|
|
1241
|
+
{
|
|
1242
|
+
var arr = memD.ToArray();
|
|
1243
|
+
var fromDouble = new float[arr.Length];
|
|
1244
|
+
for (int i = 0; i < arr.Length; i++) fromDouble[i] = (float)arr[i];
|
|
1245
|
+
return fromDouble;
|
|
1246
|
+
}
|
|
1247
|
+
if (value != null && value.GetType().Name.StartsWith("SqlVector"))
|
|
1248
|
+
{
|
|
1249
|
+
var memProp = value.GetType().GetProperty("Memory");
|
|
1250
|
+
if (memProp != null)
|
|
1251
|
+
{
|
|
1252
|
+
var memVal = memProp.GetValue(value);
|
|
1253
|
+
if (memVal != null) return DecodeVectorColumn(memVal);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1116
1256
|
|
|
1117
1257
|
string text = value as string;
|
|
1118
1258
|
byte[] bytes = value as byte[];
|
|
@@ -1233,6 +1373,26 @@ namespace An5Orm
|
|
|
1233
1373
|
|
|
1234
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")
|
|
1235
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);
|
|
1236
1396
|
}
|
|
1237
1397
|
}
|
|
1238
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}
|
|
@@ -16,6 +16,8 @@ export interface PulledColumn {
|
|
|
16
16
|
precision: number;
|
|
17
17
|
scale: number;
|
|
18
18
|
isNullable: boolean | number;
|
|
19
|
+
vectorDimensions?: number | null;
|
|
20
|
+
vectorBaseType?: string | null;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
/**
|
|
@@ -28,7 +30,16 @@ export function sqlServerTypeForColumn(col: PulledColumn): string {
|
|
|
28
30
|
let sqlType = col.dataType.toUpperCase();
|
|
29
31
|
const dt = col.dataType.toLowerCase();
|
|
30
32
|
|
|
31
|
-
if (dt === '
|
|
33
|
+
if (dt === 'vector') {
|
|
34
|
+
if (col.vectorDimensions && col.vectorDimensions > 0) {
|
|
35
|
+
const baseType = col.vectorBaseType?.toLowerCase();
|
|
36
|
+
if (baseType && baseType !== 'float32') {
|
|
37
|
+
return `VECTOR(${col.vectorDimensions}, ${baseType})`;
|
|
38
|
+
}
|
|
39
|
+
return `VECTOR(${col.vectorDimensions})`;
|
|
40
|
+
}
|
|
41
|
+
return 'VECTOR';
|
|
42
|
+
} else if (dt === 'nvarchar' || dt === 'varchar') {
|
|
32
43
|
sqlType = col.maxLength === -1 ? `${sqlType.toUpperCase()}(MAX)` : `${sqlType.toUpperCase()}(${col.maxLength / 2})`;
|
|
33
44
|
} else if (dt === 'char' || dt === 'nchar') {
|
|
34
45
|
sqlType = `${sqlType.toUpperCase()}(${col.maxLength / 2})`;
|
|
@@ -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) {
|