@an5/orm 1.2.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +1 -1
  2. package/dist/generator/src/api.d.ts +3 -0
  3. package/dist/generator/src/api.js +7 -1
  4. package/dist/generator/src/code-generator.js +12 -1
  5. package/dist/generator/src/code-request.d.ts +2 -2
  6. package/dist/generator/src/code-request.js +24 -3
  7. package/dist/generator/src/config.d.ts +18 -0
  8. package/dist/generator/src/config.js +24 -0
  9. package/dist/generator/src/dialect.js +4 -2
  10. package/dist/generator/src/dotnet-generator.js +97 -1
  11. package/dist/generator/src/field-types.js +3 -0
  12. package/dist/generator/src/golang-generator.d.ts +8 -0
  13. package/dist/generator/src/golang-generator.js +183 -9
  14. package/dist/generator/src/index.js +18 -0
  15. package/dist/generator/src/java-generator.d.ts +56 -0
  16. package/dist/generator/src/java-generator.js +1408 -0
  17. package/dist/generator/src/kotlin-generator.d.ts +34 -0
  18. package/dist/generator/src/kotlin-generator.js +753 -0
  19. package/dist/generator/src/python-generator.js +4 -2
  20. package/dist/generator/src/rust-generator.js +36 -1
  21. package/dist/generator/src/swift-generator.d.ts +42 -0
  22. package/dist/generator/src/swift-generator.js +768 -0
  23. package/generator/src/api.ts +3 -0
  24. package/generator/src/code-generator.ts +13 -1
  25. package/generator/src/code-request.ts +17 -3
  26. package/generator/src/config.ts +48 -0
  27. package/generator/src/dialect.ts +5 -3
  28. package/generator/src/dotnet-generator.ts +97 -1
  29. package/generator/src/field-types.ts +3 -0
  30. package/generator/src/golang-generator.ts +184 -9
  31. package/generator/src/index.ts +21 -0
  32. package/generator/src/java-generator.ts +1459 -0
  33. package/generator/src/kotlin-generator.ts +798 -0
  34. package/generator/src/python-generator.ts +4 -2
  35. package/generator/src/rust-generator.ts +37 -1
  36. package/generator/src/swift-generator.ts +830 -0
  37. package/package.json +2 -2
@@ -25,6 +25,9 @@ export { PythonGenerator } from './python-generator';
25
25
  export { DotnetGenerator } from './dotnet-generator';
26
26
  export { GolangGenerator } from './golang-generator';
27
27
  export { RustGenerator } from './rust-generator';
28
+ export { JavaGenerator } from './java-generator';
29
+ export { KotlinGenerator } from './kotlin-generator';
30
+ export { SwiftGenerator } from './swift-generator';
28
31
  export { detectProvider, providerFromConfig, providerForProject } from './config';
29
32
  export { dialectFor, defaultClause, sqlLiteral } from './dialect';
30
33
  export { fieldKind } from './type-kinds';
@@ -2,6 +2,13 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { Model, Field, Relation } from './types';
4
4
 
5
+ /**
6
+ * Types `resolveFieldType` can return that the SQL keyword table in
7
+ * `normalizeType` does not cover. Kept explicit so a new resolver output is a
8
+ * deliberate addition rather than silently becoming `any`.
9
+ */
10
+ const RESOLVED_TS_TYPES = new Set(['Buffer', 'number | bigint', 'number[] | string']);
11
+
5
12
  export class CodeGenerator {
6
13
  constructor(private outputDir: string) {
7
14
  if (!fs.existsSync(this.outputDir)) {
@@ -249,7 +256,12 @@ export interface TableClient<T, WhereInput = any, _Select = any, Include = any,
249
256
  if (['bytes', 'binary', 'varbinary', 'image'].includes(lowerType)) {
250
257
  return 'string';
251
258
  }
252
- return 'any';
259
+ // `Field.type` already holds the TypeScript type the parser resolved for the
260
+ // provider, and the resolver returns a few types this SQL table does not list:
261
+ // `Buffer` (BLOB), `number | bigint` (BIGINT) and `number[] | string` (VECTOR).
262
+ // Those used to fall through to `any`, so the generated client silently lost the
263
+ // type of every byte and vector column.
264
+ return RESOLVED_TS_TYPES.has(type) ? type : 'any';
253
265
  }
254
266
 
255
267
  private toCamelCase(str: string): string {
@@ -8,9 +8,12 @@ import { PythonGenerator } from './python-generator';
8
8
  import { DotnetGenerator } from './dotnet-generator';
9
9
  import { GolangGenerator } from './golang-generator';
10
10
  import { RustGenerator } from './rust-generator';
11
+ import { JavaGenerator } from './java-generator';
12
+ import { KotlinGenerator } from './kotlin-generator';
13
+ import { SwiftGenerator } from './swift-generator';
11
14
  import type { Provider } from './field-types';
12
15
 
13
- export const CODE_LANGUAGES = ['typescript', 'python', 'dotnet', 'golang', 'rust'] as const;
16
+ export const CODE_LANGUAGES = ['typescript', 'python', 'dotnet', 'golang', 'rust', 'java', 'kotlin', 'swift'] as const;
14
17
  export type CodeLanguage = typeof CODE_LANGUAGES[number];
15
18
  export interface CodeRequest {
16
19
  request: string;
@@ -31,6 +34,14 @@ export function detectCodeLanguage(root: string): CodeLanguage {
31
34
  if (names.some(name => /\.(csproj|sln|slnx)$/.test(name))) candidates.push('dotnet');
32
35
  if (has('go.mod')) candidates.push('golang');
33
36
  if (has('Cargo.toml')) candidates.push('rust');
37
+ // Java and Kotlin are told apart by their build file, not by a directory name: a Gradle
38
+ // project has both a build.gradle and a pom.xml, and only one says which language it is.
39
+ const hasJavaBuild = has('pom.xml') || has('build.gradle') || has('build.gradle.kts');
40
+ if (hasJavaBuild) {
41
+ const kotlinMarker = names.some((name) => /\.(kt|kts)$/.test(name)) || has('settings.gradle.kts');
42
+ candidates.push(kotlinMarker ? 'kotlin' : 'java');
43
+ }
44
+ if (has('Package.swift')) candidates.push('swift');
34
45
  if (candidates.length !== 1) throw new Error(`Cannot select one project language (${candidates.join(', ') || 'none'}). Specify language explicitly or select the application directory.`);
35
46
  return candidates[0]!;
36
47
  }
@@ -42,7 +53,7 @@ export async function prepareCodeRequest(input: CodeRequest) {
42
53
  if (!CODE_LANGUAGES.includes(language)) throw new Error(`Unsupported language: ${language}`);
43
54
  const loaded = loadConfig(input.projectRoot);
44
55
  const output = loaded.outputs;
45
- const clientOutput = { typescript: output.typescriptDir, python: path.dirname(output.pythonMetadataFile), dotnet: output.dotnetDir, golang: output.golangDir, rust: output.rustDir }[language];
56
+ const clientOutput = { typescript: output.typescriptDir, python: path.dirname(output.pythonMetadataFile), dotnet: output.dotnetDir, golang: output.golangDir, rust: output.rustDir, java: output.javaDir, kotlin: output.kotlinDir, swift: output.swiftDir }[language];
46
57
  const schemaPath = path.resolve(input.projectRoot, input.schemaPath);
47
58
  const schemaDir = fs.statSync(schemaPath).isDirectory() ? schemaPath : path.dirname(schemaPath);
48
59
  const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'an5-code-request-'));
@@ -62,13 +73,16 @@ export async function prepareCodeRequest(input: CodeRequest) {
62
73
  case 'dotnet': new DotnetGenerator(scratch).generate(models); break;
63
74
  case 'golang': new GolangGenerator(scratch).generate(models); break;
64
75
  case 'rust': new RustGenerator(scratch).generate(models); break;
76
+ case 'java': new JavaGenerator(scratch).generate(models); break;
77
+ case 'kotlin': new KotlinGenerator(scratch).generate(models); break;
78
+ case 'swift': new SwiftGenerator(scratch).generate(models); break;
65
79
  }
66
80
  const files: { path: string; content: string }[] = [];
67
81
  function collect(dir: string) {
68
82
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
69
83
  const full = path.join(dir, entry.name);
70
84
  if (entry.isDirectory()) collect(full);
71
- else if (/\.(ts|py|cs|go|rs|mod|toml)$/.test(entry.name)) files.push({ path: path.relative(scratch, full), content: fs.readFileSync(full, 'utf8') });
85
+ else if (/\.(ts|py|cs|go|rs|mod|toml|java|kt|swift)$/.test(entry.name)) files.push({ path: path.relative(scratch, full), content: fs.readFileSync(full, 'utf8') });
72
86
  }
73
87
  }
74
88
  collect(scratch);
@@ -41,6 +41,21 @@ export interface RustOutput {
41
41
  outputDir: string;
42
42
  }
43
43
 
44
+ /** Java client output. */
45
+ export interface JavaOutput {
46
+ outputDir: string;
47
+ }
48
+
49
+ /** Kotlin client output. */
50
+ export interface KotlinOutput {
51
+ outputDir: string;
52
+ }
53
+
54
+ /** Swift client output. */
55
+ export interface SwiftOutput {
56
+ outputDir: string;
57
+ }
58
+
44
59
  export interface An5OrmConfig {
45
60
  /**
46
61
  * Database connection for the commands that need one — db:push, db:pull,
@@ -62,6 +77,9 @@ export interface An5OrmConfig {
62
77
  dotnet: DotnetOutput;
63
78
  golang: GolangOutput;
64
79
  rust: RustOutput;
80
+ java: JavaOutput;
81
+ kotlin: KotlinOutput;
82
+ swift: SwiftOutput;
65
83
  };
66
84
  pull: {
67
85
  /** Regex patterns; tables matching any of them are skipped by db:pull. */
@@ -86,6 +104,9 @@ export const DEFAULT_CONFIG: An5OrmConfig = {
86
104
  dotnet: { outputDir: 'an5Client/dotnet' },
87
105
  golang: { outputDir: 'an5Client/golang' },
88
106
  rust: { outputDir: 'an5Client/rust' },
107
+ java: { outputDir: 'an5Client/java' },
108
+ kotlin: { outputDir: 'an5Client/kotlin' },
109
+ swift: { outputDir: 'an5Client/swift' },
89
110
  },
90
111
  pull: {
91
112
  exclude: ['^__', '^sys\\.', '^igrations'],
@@ -105,6 +126,9 @@ export interface ResolvedOutputs {
105
126
  dotnetDir: string;
106
127
  golangDir: string;
107
128
  rustDir: string;
129
+ javaDir: string;
130
+ kotlinDir: string;
131
+ swiftDir: string;
108
132
  }
109
133
 
110
134
  export function resolveOutputs(config: An5OrmConfig, rootDir: string): ResolvedOutputs {
@@ -117,6 +141,9 @@ export function resolveOutputs(config: An5OrmConfig, rootDir: string): ResolvedO
117
141
  dotnetDir: resolve(config.outputs.dotnet.outputDir),
118
142
  golangDir: resolve(config.outputs.golang.outputDir),
119
143
  rustDir: resolve(config.outputs.rust.outputDir),
144
+ javaDir: resolve(config.outputs.java.outputDir),
145
+ kotlinDir: resolve(config.outputs.kotlin.outputDir),
146
+ swiftDir: resolve(config.outputs.swift.outputDir),
120
147
  };
121
148
  }
122
149
 
@@ -232,6 +259,9 @@ const OUTPUT_SPEC: Record<string, FieldSpec> = {
232
259
  dotnet: 'object',
233
260
  golang: 'object',
234
261
  rust: 'object',
262
+ java: 'object',
263
+ kotlin: 'object',
264
+ swift: 'object',
235
265
  };
236
266
 
237
267
  const OUTPUT_SECTION_SPEC: Record<string, Record<string, FieldSpec>> = {
@@ -240,6 +270,9 @@ const OUTPUT_SECTION_SPEC: Record<string, Record<string, FieldSpec>> = {
240
270
  dotnet: { outputDir: 'string' },
241
271
  golang: { outputDir: 'string' },
242
272
  rust: { outputDir: 'string' },
273
+ java: { outputDir: 'string' },
274
+ kotlin: { outputDir: 'string' },
275
+ swift: { outputDir: 'string' },
243
276
  };
244
277
 
245
278
  const TOP_LEVEL_SPEC: Record<string, FieldSpec> = {
@@ -317,6 +350,21 @@ export function validateConfig(raw: unknown): An5OrmConfig {
317
350
  (outputs.rust as Record<string, unknown> | undefined)?.outputDir as string ??
318
351
  DEFAULT_CONFIG.outputs.rust.outputDir,
319
352
  },
353
+ java: {
354
+ outputDir:
355
+ (outputs.java as Record<string, unknown> | undefined)?.outputDir as string ??
356
+ DEFAULT_CONFIG.outputs.java.outputDir,
357
+ },
358
+ kotlin: {
359
+ outputDir:
360
+ (outputs.kotlin as Record<string, unknown> | undefined)?.outputDir as string ??
361
+ DEFAULT_CONFIG.outputs.kotlin.outputDir,
362
+ },
363
+ swift: {
364
+ outputDir:
365
+ (outputs.swift as Record<string, unknown> | undefined)?.outputDir as string ??
366
+ DEFAULT_CONFIG.outputs.swift.outputDir,
367
+ },
320
368
  },
321
369
  pull: {
322
370
  exclude: (pull.exclude as string[] | undefined) ?? DEFAULT_CONFIG.pull.exclude,
@@ -293,13 +293,15 @@ function createDialect(provider: Provider, quote: (name: string) => string): Pus
293
293
  // destructured by a caller.
294
294
  const columnDefinition = (column: ColumnSpec): string => {
295
295
  const identity = identityKeyword(column, provider);
296
+ const sqlType = provider === 'sqlite' && /^VECTOR\s*(\(|$)/i.test(column.sqlType)
297
+ ? 'BLOB' : column.sqlType;
296
298
  // Where the identity keyword goes is not a detail: SQL Server takes it as part
297
299
  // of the column type (`INT IDENTITY(1,1)`), while SQLite only accepts it after
298
300
  // `PRIMARY KEY` and only spells the documented form `INTEGER PRIMARY KEY
299
301
  // AUTOINCREMENT`.
300
302
  const type = identity && provider !== 'sqlite'
301
- ? `${quote(column.name)} ${column.sqlType} ${identity}`
302
- : `${quote(column.name)} ${column.sqlType}`;
303
+ ? `${quote(column.name)} ${sqlType} ${identity}`
304
+ : `${quote(column.name)} ${sqlType}`;
303
305
  const nullable = column.isOptional ? '' : ' NOT NULL';
304
306
  const unique = column.isUnique && !column.isId ? ' UNIQUE' : '';
305
307
  const defaultSql = defaultClause(column.defaultExpr, provider);
@@ -412,4 +414,4 @@ export function defaultClause(expression: string | undefined, provider: Provider
412
414
  if (/^".*"$/.test(value)) return `DEFAULT '${value.slice(1, -1).replace(/'/g, "''")}'`;
413
415
  return `DEFAULT ${value}`;
414
416
  }
415
- }
417
+ }
@@ -305,6 +305,7 @@ using System;
305
305
  using System.Collections.Generic;
306
306
  using System.Data;
307
307
  using System.Data.Common;
308
+ using System.Linq;
308
309
  using System.Reflection;
309
310
  using System.Text.Json;
310
311
  using An5Orm.Entities;
@@ -393,11 +394,41 @@ namespace An5Orm
393
394
  {
394
395
  var p = cmd.CreateParameter();
395
396
  p.ParameterName = (name ?? "").TrimStart('@');
396
- p.Value = value ?? DBNull.Value;
397
+ p.Value = Bindable(value) ?? DBNull.Value;
397
398
  cmd.Parameters.Add(p);
398
399
  return p;
399
400
  }
400
401
 
402
+ /// <summary>
403
+ /// Converts a value into something the provider can bind.
404
+ /// </summary>
405
+ /// <remarks>
406
+ /// A <c>VECTOR(n)</c> column is transmitted as a JSON array over TDS, so a float or
407
+ /// double array is written in that form. The array itself is not bindable: the
408
+ /// provider has no mapping for it, and the failure names the array rather than the
409
+ /// column it was meant for.
410
+ /// </remarks>
411
+ public static object Bindable(object value)
412
+ {
413
+ // \`float[]\` and \`double[]\` are listed separately rather than through a shared
414
+ // \`IEnumerable<double>\`: an array of value type is not covariant, so
415
+ // \`float[]\` does not convert to it.
416
+ if (value is float[] floats) return FormatVector(floats.Select(v => (double)v));
417
+ if (value is double[] doubles) return FormatVector(doubles);
418
+ return value;
419
+ }
420
+
421
+ /// <summary>Renders a vector as the JSON array SQL Server expects.</summary>
422
+ public static string FormatVector(IEnumerable<double> vector)
423
+ {
424
+ var parts = new List<string>();
425
+ foreach (var value in vector)
426
+ {
427
+ parts.Add(value.ToString("R", System.Globalization.CultureInfo.InvariantCulture));
428
+ }
429
+ return "[" + string.Join(", ", parts) + "]";
430
+ }
431
+
401
432
  /// <summary>Quotes an identifier for the dialect.</summary>
402
433
  public static string Quote(string name, An5Dialect dialect)
403
434
  {
@@ -1024,6 +1055,60 @@ namespace An5Orm
1024
1055
  /// property would otherwise fail to set. A value that cannot be
1025
1056
  /// converted is skipped rather than aborting the whole read.
1026
1057
  /// </summary>
1058
+ /// <summary>
1059
+ /// Reads a stored vector into the \`float[]\` an entity declares.
1060
+ /// </summary>
1061
+ /// <remarks>
1062
+ /// A \`VECTOR(n)\` column arrives as a JSON array over TDS, so the JSON form is
1063
+ /// what is parsed. A float32 BLOB is also accepted because that is what the SQLite
1064
+ /// provider stores in the same column. Returns <c>null</c> for anything else, which
1065
+ /// leaves the property at its default rather than aborting the whole read.
1066
+ /// </remarks>
1067
+ private static float[] DecodeVectorColumn(object value)
1068
+ {
1069
+ if (value is float[] already) return already;
1070
+ if (value is double[] asDouble)
1071
+ {
1072
+ var fromDouble = new float[asDouble.Length];
1073
+ for (int i = 0; i < asDouble.Length; i++) fromDouble[i] = (float)asDouble[i];
1074
+ return fromDouble;
1075
+ }
1076
+
1077
+ string text = value as string;
1078
+ byte[] bytes = value as byte[];
1079
+ if (bytes != null && bytes.Length > 0 && bytes[0] != (byte)'[' && bytes.Length % 4 == 0)
1080
+ {
1081
+ var fromBlob = new float[bytes.Length / 4];
1082
+ for (int i = 0; i < fromBlob.Length; i++)
1083
+ {
1084
+ int offset = i * 4;
1085
+ uint bits = (uint)(bytes[offset] | (bytes[offset + 1] << 8) | (bytes[offset + 2] << 16) | (bytes[offset + 3] << 24));
1086
+ fromBlob[i] = BitConverter.ToSingle(BitConverter.GetBytes(bits), 0);
1087
+ }
1088
+ return fromBlob;
1089
+ }
1090
+ if (text == null && bytes != null)
1091
+ {
1092
+ text = System.Text.Encoding.UTF8.GetString(bytes);
1093
+ }
1094
+ if (text == null) return null;
1095
+
1096
+ var open = text.IndexOf('[');
1097
+ var close = text.LastIndexOf(']');
1098
+ if (open < 0 || close <= open) return null;
1099
+ var body = text.Substring(open + 1, close - open - 1).Trim();
1100
+ if (body.Length == 0) return null;
1101
+
1102
+ var parts = body.Split(',');
1103
+ var parsed = new float[parts.Length];
1104
+ for (int i = 0; i < parts.Length; i++)
1105
+ {
1106
+ if (!float.TryParse(parts[i].Trim(), System.Globalization.NumberStyles.Float,
1107
+ System.Globalization.CultureInfo.InvariantCulture, out parsed[i])) return null;
1108
+ }
1109
+ return parsed;
1110
+ }
1111
+
1027
1112
  private static void SetValue(System.Reflection.PropertyInfo prop, object target, object value)
1028
1113
  {
1029
1114
  var wanted = prop.PropertyType;
@@ -1041,6 +1126,17 @@ namespace An5Orm
1041
1126
  prop.SetValue(target, Convert.ChangeType(value, underlying));
1042
1127
  return;
1043
1128
  }
1129
+ // A \`VECTOR(n)\` column is transmitted as a JSON array over TDS, which no
1130
+ // converter bridges to the \`float[]\` the entity declares.
1131
+ if (wanted == typeof(float[]))
1132
+ {
1133
+ var vector = DecodeVectorColumn(value);
1134
+ if (vector != null)
1135
+ {
1136
+ prop.SetValue(target, vector);
1137
+ return;
1138
+ }
1139
+ }
1044
1140
  if (value is string text && wanted == typeof(Guid)) { prop.SetValue(target, Guid.Parse(text)); return; }
1045
1141
  if (value is string when && wanted == typeof(DateTime)) { prop.SetValue(target, DateTime.Parse(when, System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.RoundtripKind)); return; }
1046
1142
  prop.SetValue(target, Convert.ChangeType(value, wanted));
@@ -266,6 +266,9 @@ export const PROVIDER_FIELD_TYPES: Record<Provider, Readonly<Record<string, TsTy
266
266
  // Everything else
267
267
  JSON: 'any',
268
268
  UUID: 'string',
269
+ // Stored as a BLOB of little-endian float32: a third of the JSON text it replaces,
270
+ // and what the adapters' in-database distance functions read. A column already
271
+ // holding JSON text keeps working, because the adapters decode both.
269
272
  VECTOR: 'number[] | string',
270
273
  },
271
274
 
@@ -189,6 +189,7 @@ func GetDefaultConnectionString() string {
189
189
  content += this.generateWhereBuilder();
190
190
  content += this.generateCrudMethods();
191
191
  content += this.generateVectorSearch();
192
+ content += this.generateVectorHelpers();
192
193
  content += this.generateHelpers();
193
194
 
194
195
  // NOTE: per-model ORM types live in each `<Model>.go` file.
@@ -417,9 +418,18 @@ func (c *TableClient[T]) queryAndScan(ctx context.Context, query string, args ..
417
418
  \t\tvar item T
418
419
  \t\tval := reflect.ValueOf(&item).Elem()
419
420
  \t\tscanArgs := make([]interface{}, len(cols))
421
+ \t\tvectorCols := make([]bool, len(cols))
420
422
  \t\tfor i, colName := range cols {
421
423
  \t\t\tfieldVal := findFieldByDbTag(val, colName)
422
424
  \t\t\tif fieldVal.IsValid() && fieldVal.CanSet() {
425
+ \t\t\t\t// A \`VECTOR(n)\` column is stored as bytes, which database/sql cannot
426
+ \t\t\t\t// scan into a slice, so it is read as bytes and decoded afterwards.
427
+ \t\t\t\tif isVectorGoType(fieldVal.Type()) {
428
+ \t\t\t\t\tvar raw []byte
429
+ \t\t\t\t\tscanArgs[i] = &raw
430
+ \t\t\t\t\tvectorCols[i] = true
431
+ \t\t\t\t\tcontinue
432
+ \t\t\t\t}
423
433
  \t\t\t\tscanArgs[i] = fieldVal.Addr().Interface()
424
434
  \t\t\t} else {
425
435
  \t\t\t\tvar unused interface{}
@@ -429,6 +439,20 @@ func (c *TableClient[T]) queryAndScan(ctx context.Context, query string, args ..
429
439
  \t\tif err := rows.Scan(scanArgs...); err != nil {
430
440
  \t\t\treturn nil, err
431
441
  \t\t}
442
+ \t\tfor i, isVector := range vectorCols {
443
+ \t\t\tif !isVector {
444
+ \t\t\t\tcontinue
445
+ \t\t\t}
446
+ \t\t\tdecoded, ok := decodeVectorBytes(*(scanArgs[i].(*[]byte)))
447
+ \t\t\tif !ok {
448
+ \t\t\t\tcontinue
449
+ \t\t\t}
450
+ \t\t\tfieldVal := findFieldByDbTag(val, cols[i])
451
+ \t\t\tif !fieldVal.IsValid() || !fieldVal.CanSet() {
452
+ \t\t\t\tcontinue
453
+ \t\t\t}
454
+ \t\t\tsetVectorField(fieldVal, decoded)
455
+ \t\t}
432
456
  \t\tresults = append(results, item)
433
457
  \t}
434
458
  \tif err := rows.Err(); err != nil {
@@ -437,6 +461,161 @@ func (c *TableClient[T]) queryAndScan(ctx context.Context, query string, args ..
437
461
  \treturn results, nil
438
462
  }
439
463
 
464
+ `;
465
+ }
466
+
467
+ /**
468
+ * The vector codec the generated client needs on both sides of a query.
469
+ *
470
+ * A \`VECTOR(n)\` column stores float32 bytes, which is a third of the JSON text it
471
+ * replaces and is what the in-database distance functions read. A column written
472
+ * before that encoding holds JSON text, so both are still accepted on the way in.
473
+ */
474
+ private generateVectorHelpers(): string {
475
+ return `// bytesPerVectorFloat is the width of one stored vector component.
476
+ const bytesPerVectorFloat = 4
477
+
478
+ // isVectorGoType reports whether a model field is a vector column, which the
479
+ // generator declares as a float32 slice, optionally behind a pointer because the
480
+ // column is nullable.
481
+ func isVectorGoType(t reflect.Type) bool {
482
+ \treturn vectorSliceType(t) != nil
483
+ }
484
+
485
+ // vectorSliceType returns the slice type a vector field holds, or nil for any
486
+ // other field. A pointer to a slice is unwrapped so both the nullable and the
487
+ // required spelling are recognised.
488
+ func vectorSliceType(t reflect.Type) reflect.Type {
489
+ \tif t.Kind() == reflect.Ptr {
490
+ \t\tt = t.Elem()
491
+ \t}
492
+ \tif t.Kind() == reflect.Slice && t.Elem().Kind() == reflect.Float32 {
493
+ \t\treturn t
494
+ \t}
495
+ \treturn nil
496
+ }
497
+
498
+ // vectorComponents reads a vector field as float64 components.
499
+ //
500
+ // A field can arrive as JSON text, as a slice, or behind a pointer to either, and a
501
+ // pointer is how the generator spells a nullable column. Anything else is not a vector.
502
+ func vectorComponents(field reflect.Value) []float64 {
503
+ \tfor field.Kind() == reflect.Ptr {
504
+ \t\tif field.IsNil() {
505
+ \t\t\treturn nil
506
+ \t\t}
507
+ \t\tfield = field.Elem()
508
+ \t}
509
+ \tswitch {
510
+ \tcase field.Kind() == reflect.String:
511
+ \t\tvar parsed []float64
512
+ \t\tif err := json.Unmarshal([]byte(field.String()), &parsed); err != nil {
513
+ \t\t\treturn nil
514
+ \t\t}
515
+ \t\treturn parsed
516
+ \tcase field.Kind() == reflect.Slice:
517
+ \t\tout := make([]float64, field.Len())
518
+ \t\tfor i := range out {
519
+ \t\t\tout[i] = field.Index(i).Float()
520
+ \t\t}
521
+ \t\treturn out
522
+ \tdefault:
523
+ \t\treturn nil
524
+ \t}
525
+ }
526
+
527
+ // setVectorField stores decoded components into a vector field, matching whether
528
+ // the field holds the slice directly or behind a pointer.
529
+ func setVectorField(field reflect.Value, values []float64) {
530
+ \tsliceType := vectorSliceType(field.Type())
531
+ \tif sliceType == nil {
532
+ \t\treturn
533
+ \t}
534
+ \tslice := reflect.MakeSlice(sliceType, len(values), len(values))
535
+ \tfor i, value := range values {
536
+ \t\tslice.Index(i).SetFloat(value)
537
+ \t}
538
+ \tif field.Kind() == reflect.Ptr {
539
+ \t\tptr := reflect.New(sliceType)
540
+ \t\tptr.Elem().Set(slice)
541
+ \t\tfield.Set(ptr)
542
+ \t\treturn
543
+ \t}
544
+ \tfield.Set(slice)
545
+ }
546
+
547
+ // encodeVectorArg converts a vector field into the bytes the column stores.
548
+ //
549
+ // database/sql accepts only a fixed set of types, so a slice otherwise reaches the
550
+ // driver as an error. Values that already arrived as stored form pass through.
551
+ func encodeVectorArg(value interface{}) interface{} {
552
+ \tswitch v := value.(type) {
553
+ \tcase *[]float32:
554
+ \t\tif v == nil {
555
+ \t\t\treturn nil
556
+ \t\t}
557
+ \t\treturn encodeVectorFloats(floatsFrom32(*v))
558
+ \tcase []float32:
559
+ \t\treturn encodeVectorFloats(floatsFrom32(v))
560
+ \tcase *[]float64:
561
+ \t\tif v == nil {
562
+ \t\t\treturn nil
563
+ \t\t}
564
+ \t\treturn encodeVectorFloats(*v)
565
+ \tcase []float64:
566
+ \t\treturn encodeVectorFloats(v)
567
+ \tdefault:
568
+ \t\treturn value
569
+ \t}
570
+ }
571
+
572
+ func floatsFrom32(values []float32) []float64 {
573
+ \tout := make([]float64, len(values))
574
+ \tfor i, value := range values {
575
+ \t\tout[i] = float64(value)
576
+ \t}
577
+ \treturn out
578
+ }
579
+
580
+ // encodeVectorFloats writes float32 values little-endian, which is the order the
581
+ // stored bytes are read back in.
582
+ func encodeVectorFloats(values []float64) []byte {
583
+ \tout := make([]byte, len(values)*bytesPerVectorFloat)
584
+ \tfor i, value := range values {
585
+ \t\toffset := i * bytesPerVectorFloat
586
+ \t\tbits := math.Float32bits(float32(value))
587
+ \t\tout[offset] = byte(bits)
588
+ \t\tout[offset+1] = byte(bits >> 8)
589
+ \t\tout[offset+2] = byte(bits >> 16)
590
+ \t\tout[offset+3] = byte(bits >> 24)
591
+ \t}
592
+ \treturn out
593
+ }
594
+
595
+ // decodeVectorBytes reads a stored vector: float32 bytes, legacy JSON text, or
596
+ // nothing at all.
597
+ func decodeVectorBytes(raw []byte) ([]float64, bool) {
598
+ \tif len(raw) == 0 {
599
+ \t\treturn nil, false
600
+ \t}
601
+ \t// JSON text is not a multiple of four bytes once encoded and starts with '[',
602
+ \t// so the two are told apart by length and first byte.
603
+ \tif raw[0] == '[' || len(raw)%bytesPerVectorFloat != 0 {
604
+ \t\tvar parsed []float64
605
+ \t\tif err := json.Unmarshal(raw, &parsed); err != nil || len(parsed) == 0 {
606
+ \t\t\treturn nil, false
607
+ \t\t}
608
+ \t\treturn parsed, true
609
+ \t}
610
+ \tout := make([]float64, len(raw)/bytesPerVectorFloat)
611
+ \tfor i := range out {
612
+ \t\toffset := i * bytesPerVectorFloat
613
+ \t\tbits := uint32(raw[offset]) | uint32(raw[offset+1])<<8 | uint32(raw[offset+2])<<16 | uint32(raw[offset+3])<<24
614
+ \t\tout[i] = float64(math.Float32frombits(bits))
615
+ \t}
616
+ \treturn out, true
617
+ }
618
+
440
619
  `;
441
620
  }
442
621
 
@@ -657,7 +836,7 @@ func (c *TableClient[T]) Create(ctx context.Context, entity *T) (*T, error) {
657
836
  \t\tfieldVal := val.Field(i)
658
837
  \t\tif !fieldVal.IsZero() {
659
838
  \t\t\tcols = append(cols, c.quoteName(dbTag))
660
- \t\t\targs = append(args, fieldVal.Interface())
839
+ \t\t\targs = append(args, encodeVectorArg(fieldVal.Interface()))
661
840
  \t\t\tphs = append(phs, c.ph(len(args)))
662
841
  \t\t}
663
842
  \t}
@@ -704,7 +883,7 @@ func (c *TableClient[T]) Update(ctx context.Context, entity *T) (*T, error) {
704
883
  \t\t\tidVal = fieldVal.Interface()
705
884
  \t\t\tidCol = dbTag
706
885
  \t\t} else if !fieldVal.IsZero() {
707
- \t\t\targs = append(args, fieldVal.Interface())
886
+ \t\t\targs = append(args, encodeVectorArg(fieldVal.Interface()))
708
887
  \t\t\tsets = append(sets, c.quoteName(dbTag)+" = "+c.ph(len(args)))
709
888
  \t\t}
710
889
  \t}
@@ -1053,13 +1232,9 @@ func (c *TableClient[T]) VectorSearch(ctx context.Context, vector []float64, opt
1053
1232
  \t\t\tcontinue
1054
1233
  \t\t}
1055
1234
 
1056
- \t\tvar rowVec []float64
1057
- \t\tif fieldVal.Kind() == reflect.String {
1058
- \t\t\t_ = json.Unmarshal([]byte(fieldVal.String()), &rowVec)
1059
- \t\t} else if fieldVal.Kind() == reflect.Slice {
1060
- \t\t\tfor i := 0; i < fieldVal.Len(); i++ {
1061
- \t\t\t\trowVec = append(rowVec, fieldVal.Index(i).Float())
1062
- \t\t\t}
1235
+ \t\trowVec := vectorComponents(fieldVal)
1236
+ \t\tif len(rowVec) == 0 {
1237
+ \t\t\tcontinue
1063
1238
  \t\t}
1064
1239
  \t\tif len(rowVec) != len(vector) {
1065
1240
  \t\t\tcontinue
@@ -8,6 +8,9 @@ import { PythonGenerator } from './python-generator';
8
8
  import { DotnetGenerator } from './dotnet-generator';
9
9
  import { GolangGenerator } from './golang-generator';
10
10
  import { RustGenerator } from './rust-generator';
11
+ import { JavaGenerator } from './java-generator';
12
+ import { KotlinGenerator } from './kotlin-generator';
13
+ import { SwiftGenerator } from './swift-generator';
11
14
 
12
15
  import fs from 'fs';
13
16
 
@@ -69,6 +72,9 @@ async function main() {
69
72
  const outputDotnetDir = config.outputs.dotnetDir;
70
73
  const outputGolangDir = config.outputs.golangDir;
71
74
  const outputRustDir = config.outputs.rustDir;
75
+ const outputJavaDir = config.outputs.javaDir;
76
+ const outputKotlinDir = config.outputs.kotlinDir;
77
+ const outputSwiftDir = config.outputs.swiftDir;
72
78
  const generateMetadata = config.config.generation.generateMetadata;
73
79
 
74
80
  console.log('🚀 Starting ORM generation...');
@@ -83,6 +89,9 @@ async function main() {
83
89
  clearGeneratedFiles(outputDotnetDir, '.cs');
84
90
  clearGeneratedFiles(outputGolangDir, '.go');
85
91
  clearGeneratedFiles(path.join(outputRustDir, 'src'), '.rs');
92
+ clearGeneratedFiles(outputJavaDir, '.java');
93
+ clearGeneratedFiles(outputKotlinDir, '.kt');
94
+ clearGeneratedFiles(path.join(outputSwiftDir, 'Sources'), '.swift');
86
95
  const pythonDirEarly = path.dirname(outputPythonMetadataPath);
87
96
  clearGeneratedPythonFiles(pythonDirEarly);
88
97
  if (config.config.generation.generateMetadata && fs.existsSync(outputMetadataPath)) {
@@ -129,6 +138,18 @@ async function main() {
129
138
  rustGen.generate(models);
130
139
  console.log(`✨ Generated Rust client in ${outputRustDir}`);
131
140
 
141
+ const javaGen = new JavaGenerator(outputJavaDir);
142
+ javaGen.generate(models);
143
+ console.log(`✨ Generated Java client in ${outputJavaDir}`);
144
+
145
+ const kotlinGen = new KotlinGenerator(outputKotlinDir);
146
+ kotlinGen.generate(models);
147
+ console.log(`✨ Generated Kotlin client in ${outputKotlinDir}`);
148
+
149
+ const swiftGen = new SwiftGenerator(outputSwiftDir);
150
+ swiftGen.generate(models);
151
+ console.log(`✨ Generated Swift client in ${outputSwiftDir}`);
152
+
132
153
  console.log('✅ ORM generation completed successfully.');
133
154
  } catch (error) {
134
155
  // Print every bad field type with its provider, instead of the parser's