@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 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
- if (dim > 1998) {
810
- throw new Error("Vector dimension exceeds SQL Server limit of 1998");
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
- \tEquals *time.Time
270
- \tNot *time.Time
271
- \tIn []time.Time
272
- \tNotIn []time.Time
273
- \tGt *time.Time
274
- \tGte *time.Time
275
- \tLt *time.Time
276
- \tLte *time.Time
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.parseArgs(whereArgs)
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() && !whereField.IsNil() {
1061
- \t\t// where is a pointer to a model-specific WhereInput struct
1062
- \t\t// Delegate to buildWhereFromStruct
1063
- \t\twc, wa := c.buildWhereFromStruct(whereField.Elem(), 1)
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}
@@ -6,6 +6,8 @@ export interface PulledColumn {
6
6
  precision: number;
7
7
  scale: number;
8
8
  isNullable: boolean | number;
9
+ vectorDimensions?: number | null;
10
+ vectorBaseType?: string | null;
9
11
  }
10
12
  /**
11
13
  * The schema type of an introspected SQL Server column.
@@ -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 === 'nvarchar' || dt === 'varchar') {
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. Required keys stay required via a total=True base;
242
- // everything else is optional (total=False), keeping partial
243
- // selects valid without extra dependencies.
244
- const rowRequired = model.fields.filter(f => !f.isOptional && !f.hasDefault);
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 (rowRequired.length === 0) {
247
+ if (model.fields.length === 0) {
249
248
  content += ' pass\n';
250
249
  }
251
- for (const f of rowRequired) {
252
- content += ` ${this.toSnakeCase(f.name)}: ${this.mapPyType(f)}\n`;
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 (rowOptional.length === 0 && model.relations.length === 0) {
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
- if (dim > 1998) {
763
- throw new Error("Vector dimension exceeds SQL Server limit of 1998");
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 tables = await (await getDb()).$queryRawUnsafe(`
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 (await getDb()).$queryRawUnsafe(`
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 (await getDb()).$queryRawUnsafe(`
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
- \tEquals *time.Time
277
- \tNot *time.Time
278
- \tIn []time.Time
279
- \tNotIn []time.Time
280
- \tGt *time.Time
281
- \tGte *time.Time
282
- \tLt *time.Time
283
- \tLte *time.Time
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.parseArgs(whereArgs)
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() && !whereField.IsNil() {
1084
- \t\t// where is a pointer to a model-specific WhereInput struct
1085
- \t\t// Delegate to buildWhereFromStruct
1086
- \t\twc, wa := c.buildWhereFromStruct(whereField.Elem(), 1)
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 === 'nvarchar' || dt === 'varchar') {
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. Required keys stay required via a total=True base;
275
- // everything else is optional (total=False), keeping partial
276
- // selects valid without extra dependencies.
277
- const rowRequired = model.fields.filter(f => !f.isOptional && !f.hasDefault);
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 (rowRequired.length === 0) {
280
+ if (model.fields.length === 0) {
282
281
  content += ' pass\n';
283
282
  }
284
- for (const f of rowRequired) {
285
- content += ` ${this.toSnakeCase(f.name)}: ${this.mapPyType(f)}\n`;
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 (rowOptional.length === 0 && model.relations.length === 0) {
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@an5/orm",
3
- "version": "1.3.4",
3
+ "version": "1.3.6",
4
4
  "description": "The lightweight ORM for SQL Server with support for TypeScript, Python, .NET (C#), Go, Rust, Java, Kotlin, and Swift.",
5
5
  "license": "MIT",
6
6
  "bin": {