@an5/orm 1.3.2 → 1.3.3

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 (62) hide show
  1. package/README.md +74 -11
  2. package/dist/cleanup.d.ts +1 -0
  3. package/dist/cleanup.js +177 -0
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +147 -0
  6. package/dist/generator/src/attributes.d.ts +57 -0
  7. package/dist/generator/src/attributes.js +225 -0
  8. package/dist/generator/src/code-generator.js +49 -16
  9. package/dist/generator/src/config.d.ts +2 -2
  10. package/dist/generator/src/config.js +4 -3
  11. package/dist/generator/src/dialect.d.ts +52 -1
  12. package/dist/generator/src/dialect.js +136 -3
  13. package/dist/generator/src/dotnet-generator.js +67 -23
  14. package/dist/generator/src/golang-generator.d.ts +8 -0
  15. package/dist/generator/src/golang-generator.js +86 -21
  16. package/dist/generator/src/java-generator.d.ts +8 -0
  17. package/dist/generator/src/java-generator.js +108 -3
  18. package/dist/generator/src/kotlin-generator.js +51 -0
  19. package/dist/generator/src/parser.d.ts +1 -0
  20. package/dist/generator/src/parser.js +90 -35
  21. package/dist/generator/src/pull-schema.d.ts +54 -0
  22. package/dist/generator/src/pull-schema.js +116 -0
  23. package/dist/generator/src/push-apply.d.ts +4 -2
  24. package/dist/generator/src/push-apply.js +53 -4
  25. package/dist/generator/src/push-schema.d.ts +21 -9
  26. package/dist/generator/src/push-schema.js +132 -32
  27. package/dist/generator/src/python-generator.js +35 -22
  28. package/dist/generator/src/rust-generator.js +79 -74
  29. package/dist/generator/src/swift-generator.js +54 -3
  30. package/dist/generator/src/types.d.ts +10 -0
  31. package/dist/generator/src/view-order.d.ts +33 -0
  32. package/dist/generator/src/view-order.js +123 -0
  33. package/dist/migrate.d.ts +12 -0
  34. package/dist/migrate.js +442 -0
  35. package/dist/migration-core.d.ts +32 -3
  36. package/dist/migration-core.js +213 -43
  37. package/dist/provider-support.d.ts +0 -1
  38. package/dist/provider-support.js +0 -1
  39. package/dist/pull.d.ts +1 -0
  40. package/dist/pull.js +425 -0
  41. package/dist/push.d.ts +1 -0
  42. package/dist/push.js +93 -0
  43. package/dist/seed.d.ts +1 -0
  44. package/dist/seed.js +33 -0
  45. package/generator/src/attributes.ts +231 -0
  46. package/generator/src/code-generator.ts +47 -16
  47. package/generator/src/config.ts +5 -3
  48. package/generator/src/dialect.ts +188 -5
  49. package/generator/src/dotnet-generator.ts +68 -24
  50. package/generator/src/golang-generator.ts +89 -22
  51. package/generator/src/java-generator.ts +109 -3
  52. package/generator/src/kotlin-generator.ts +51 -0
  53. package/generator/src/parser.ts +102 -30
  54. package/generator/src/pull-schema.ts +127 -0
  55. package/generator/src/push-apply.ts +62 -5
  56. package/generator/src/push-schema.ts +160 -32
  57. package/generator/src/python-generator.ts +33 -20
  58. package/generator/src/rust-generator.ts +79 -73
  59. package/generator/src/swift-generator.ts +55 -4
  60. package/generator/src/types.ts +10 -0
  61. package/generator/src/view-order.ts +133 -0
  62. package/package.json +19 -14
@@ -624,11 +624,12 @@ public enum An5Config {
624
624
  let accessors = models.map((model) => ({
625
625
  property: SwiftGenerator.toLowerCamelCase(model.name),
626
626
  name: model.name,
627
+ isView: model.isView === true,
627
628
  }));
628
- const unique = new Map<string, { property: string; name: string }>();
629
+ const unique = new Map<string, { property: string; name: string; isView: boolean }>();
629
630
  for (const entry of accessors) {
630
631
  const key = SwiftGenerator.toLowerCamelCase(entry.name);
631
- if (!unique.has(key)) unique.set(key, { property: key, name: entry.name });
632
+ if (!unique.has(key)) unique.set(key, { property: key, name: entry.name, isView: entry.isView });
632
633
  }
633
634
 
634
635
  let content = `// This file is auto-generated. Do not edit deliberately.
@@ -724,6 +725,54 @@ public struct ModelClient<T> {
724
725
  }
725
726
  }
726
727
 
728
+ /// A read-only, typed client for one view.
729
+ ///
730
+ /// The name avoids the runtime's own \`ViewClient\`, which is wrapped here: the runtime
731
+ /// returns \`Row\` values and this maps them to the generated model. There are no write
732
+ /// methods, so writing through a view does not compile.
733
+ public struct ViewModelClient<T> {
734
+
735
+ private let client: ViewClient
736
+ private let read: (Row) -> T
737
+
738
+ init(view: ViewClient, read: @escaping (Row) -> T) {
739
+ self.client = view
740
+ self.read = read
741
+ }
742
+
743
+ /// The view this client reads.
744
+ public var view: String { client.view }
745
+
746
+ public func findMany() throws -> [T] { try client.findMany().map { read($0) } }
747
+
748
+ public func findMany(_ query: Query) throws -> [T] { try client.findMany(query).map { read($0) } }
749
+
750
+ public func findMany(filter: Where) throws -> [T] { try client.findMany(filter: filter).map { read($0) } }
751
+
752
+ public func findFirst(filter: Where) throws -> T? { try client.findFirst(filter: filter).map { read($0) } }
753
+
754
+ public func findUnique(filter: Where) throws -> T? { try client.findUnique(filter: filter).map { read($0) } }
755
+
756
+ public func count(_ filter: Where? = nil) throws -> Int { try client.count(filter) }
757
+
758
+ /// One row of aggregate values, keyed \`_count\`, \`_sum_<field>\` and so on.
759
+ public func aggregate(_ aggregate: Aggregate) throws -> Row { try client.aggregate(aggregate) }
760
+
761
+ /// One row per group, each carrying the group's count and aggregates.
762
+ public func groupBy(_ aggregate: Aggregate) throws -> [Row] { try client.groupBy(aggregate) }
763
+
764
+ /// The rows nearest a vector.
765
+ public func vectorSearch(
766
+ _ vector: [Double],
767
+ take: Int = 10,
768
+ filter: Where? = nil,
769
+ vectorField: String = "embedding",
770
+ metric: DistanceMetric = .cosine
771
+ ) throws -> [T] {
772
+ try client.vectorSearch(vector, take: take, filter: filter, vectorField: vectorField, metric: metric).map { read($0) }
773
+ }
774
+ }
775
+
727
776
  /// The AN5 entry point: one typed client per model, over one connection.
728
777
  ///
729
778
  /// \`\`\`
@@ -736,7 +785,7 @@ public final class An5Db {
736
785
  public let an5: An5Adapter
737
786
 
738
787
  ${Array.from(unique.values())
739
- .map((entry) => ` /// Queries for \`${entry.name}\`.\n public let ${entry.property}: ModelClient<${entry.name}>`)
788
+ .map((entry) => ` /// ${entry.isView ? 'Reads' : 'Queries'} for \`${entry.name}\`.\n public let ${entry.property}: ${entry.isView ? 'ViewModelClient' : 'ModelClient'}<${entry.name}>`)
740
789
  .join('\n\n')}
741
790
 
742
791
  /// Opens a database from a connection string, registering this schema with the runtime.
@@ -761,7 +810,9 @@ ${Array.from(unique.values())
761
810
  ${Array.from(unique.values())
762
811
  .map(
763
812
  (entry) =>
764
- ` self.${entry.property} = ModelClient(\n table: adapter.table("${entry.name}"),\n read: { ${entry.name}(row: $0) },\n write: { $0.values })`
813
+ entry.isView
814
+ ? ` self.${entry.property} = ViewModelClient(\n view: adapter.view("${entry.name}"),\n read: { ${entry.name}(row: $0) })`
815
+ : ` self.${entry.property} = ModelClient(\n table: adapter.table("${entry.name}"),\n read: { ${entry.name}(row: $0) },\n write: { $0.values })`
765
816
  )
766
817
  .join('\n')}
767
818
  }
@@ -36,6 +36,16 @@ export interface Model {
36
36
  relations: Relation[];
37
37
  compoundUniques?: string[][];
38
38
  description?: string;
39
+ /**
40
+ * A `view` block rather than a `model`.
41
+ *
42
+ * Views are described with the same field syntax so generated clients can type
43
+ * their rows, but they are read-only and their DDL comes from `viewSql` rather
44
+ * than from the fields.
45
+ */
46
+ isView?: boolean;
47
+ /** The `SELECT` body of `@@sql("...")`; required for a view. */
48
+ viewSql?: string;
39
49
  }
40
50
 
41
51
  /**
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Orders views so one that reads another is created after it.
3
+ *
4
+ * Tables are created before any view, but views are not all independent of each
5
+ * other: a summary view may select from a detail view. PostgreSQL and SQLite
6
+ * reject `CREATE VIEW` when the referenced relation does not exist yet, so
7
+ * declaration order in the schema file is not enough.
8
+ *
9
+ * The reference check is deliberately textual: a full SQL parser would be the
10
+ * only way to be exact, and a false positive here merely reorders two independent
11
+ * views. Missing an edge leaves the old declaration order, which is what this
12
+ * function improves on rather than replaces.
13
+ */
14
+
15
+ /** A view plus every identifier its SQL may use to reference it. */
16
+ export interface ViewOrderEntry {
17
+ /** Physical name, model name, or both; qualified names are reduced to their last part too. */
18
+ names: readonly string[];
19
+ sql: string;
20
+ }
21
+
22
+ export interface ViewOrderResult<T> {
23
+ ordered: T[];
24
+ /** True when the references form a cycle, which no view definition can satisfy. */
25
+ cyclic: boolean;
26
+ }
27
+
28
+ /** Escapes a name for a regular expression. */
29
+ function escapeRegExp(value: string): string {
30
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
31
+ }
32
+
33
+ /** Identifiers in `sql` lose their quoting so `[s].[v]`, `"s"."v"` and `s.v` compare alike. */
34
+ function unquoteIdentifiers(sql: string): string {
35
+ return sql.replace(/[[\]"`]/g, '');
36
+ }
37
+
38
+ function mentions(sql: string, name: string): boolean {
39
+ if (!name) return false;
40
+ const pattern = new RegExp(`(^|[^A-Za-z0-9_$])${escapeRegExp(name)}([^A-Za-z0-9_$]|$)`, 'i');
41
+ return pattern.test(sql);
42
+ }
43
+
44
+ /** Every way a view in this schema can be referenced: `schema.name`, `name`, or a mapped name. */
45
+ function referenceNames(names: readonly string[]): string[] {
46
+ const result = new Set<string>();
47
+ for (const name of names) {
48
+ const trimmed = name.trim();
49
+ if (!trimmed) continue;
50
+ result.add(trimmed);
51
+ const dot = trimmed.lastIndexOf('.');
52
+ if (dot >= 0 && dot < trimmed.length - 1) result.add(trimmed.slice(dot + 1));
53
+ }
54
+ return Array.from(result);
55
+ }
56
+
57
+ /**
58
+ * Stable topological sort of `views`.
59
+ *
60
+ * Kahn's algorithm in declaration order, so a schema that was already correct is
61
+ * left untouched. A cycle cannot be created by a database view definition; when
62
+ * one is detected the remaining views keep their original order and the caller
63
+ * can decide whether to warn or let the database report the error.
64
+ */
65
+ export function orderViewsByDependency<T extends ViewOrderEntry>(views: readonly T[]): ViewOrderResult<T> {
66
+ if (views.length < 2) return { ordered: [...views], cyclic: false };
67
+
68
+ const references = views.map((view) => referenceNames(view.names));
69
+ const normalizedSql = views.map((view) => unquoteIdentifiers(view.sql));
70
+
71
+ const byName = new Map<string, number[]>();
72
+ const owners = new Map<string, Set<number>>();
73
+ for (let index = 0; index < references.length; index += 1) {
74
+ for (const name of references[index] ?? []) {
75
+ const key = name.toLowerCase();
76
+ const existing = owners.get(key);
77
+ if (existing) existing.add(index);
78
+ else owners.set(key, new Set([index]));
79
+ }
80
+ }
81
+ // A bare `summary` is ambiguous when two schemas both have one; a dependency
82
+ // guessed from it could reorder unrelated views or invent a cycle. Only names
83
+ // that identify one view take part.
84
+ for (const [name, indices] of owners) {
85
+ if (indices.size === 1) byName.set(name, Array.from(indices));
86
+ }
87
+
88
+ const dependencies = views.map(() => new Set<number>());
89
+ for (let index = 0; index < views.length; index += 1) {
90
+ for (const [name, candidates] of byName) {
91
+ if (!mentions(normalizedSql[index] ?? '', name)) continue;
92
+ for (const candidate of candidates) {
93
+ if (candidate !== index) dependencies[index]?.add(candidate);
94
+ }
95
+ }
96
+ }
97
+
98
+ const dependents: number[][] = views.map(() => []);
99
+ const indegree = views.map((_, index) => dependencies[index]?.size ?? 0);
100
+ for (let index = 0; index < dependencies.length; index += 1) {
101
+ for (const dependency of dependencies[index] ?? []) {
102
+ dependents[dependency]?.push(index);
103
+ }
104
+ }
105
+
106
+ const ordered: T[] = [];
107
+ const queued = new Set<number>();
108
+ const ready: number[] = [];
109
+ for (let index = 0; index < views.length; index += 1) {
110
+ if ((indegree[index] ?? 0) === 0) ready.push(index);
111
+ }
112
+
113
+ while (ready.length > 0) {
114
+ const index = ready.shift() as number;
115
+ if (queued.has(index)) continue;
116
+ queued.add(index);
117
+ ordered.push(views[index] as T);
118
+ for (const dependent of dependents[index] ?? []) {
119
+ const remaining = (indegree[dependent] ?? 0) - 1;
120
+ indegree[dependent] = remaining;
121
+ if (remaining === 0) ready.push(dependent);
122
+ }
123
+ }
124
+
125
+ const cyclic = ordered.length !== views.length;
126
+ // A cycle is preserved as written, which is the only order the caller can act
127
+ // on; dropping the views would silently change the schema.
128
+ for (let index = 0; index < views.length; index += 1) {
129
+ if (!queued.has(index)) ordered.push(views[index] as T);
130
+ }
131
+
132
+ return { ordered, cyclic };
133
+ }
package/package.json CHANGED
@@ -1,8 +1,13 @@
1
1
  {
2
2
  "name": "@an5/orm",
3
- "version": "1.3.2",
3
+ "version": "1.3.3",
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
+ "bin": {
7
+ "an5": "./dist/cli.js",
8
+ "an5-orm": "./dist/cli.js",
9
+ "orm": "./dist/cli.js"
10
+ },
6
11
  "main": "dist/index.js",
7
12
  "types": "dist/index.d.ts",
8
13
  "exports": {
@@ -29,25 +34,25 @@
29
34
  "url": "https://github.com/an5ORM/an5Orm.git"
30
35
  },
31
36
  "scripts": {
32
- "build": "tsc -p tsconfig.json && tsc -p generator/tsconfig.json && node scripts/normalize-eol.js",
37
+ "build": "tsc -p tsconfig.json && tsc -p generator/tsconfig.json && tsc -p tsconfig.cli.json && node scripts/normalize-eol.js",
33
38
  "typecheck:cli": "tsc -p tsconfig.cli.json",
34
39
  "pretest": "npm run build",
35
40
  "prepack": "npm run build && npm run verify:pack",
36
41
  "prepublishOnly": "npm run verify:pack",
37
- "generate": "npx tsx generator/src/index.ts",
38
- "db:push": "npx tsx push.ts",
39
- "db:pull": "npx tsx pull.ts",
40
- "db:seed": "npx tsx seed.ts",
41
- "db:cleanup": "npx tsx cleanup.ts",
42
- "db:migrate": "npx tsx migrate.ts",
43
- "db:migrate:diff": "npx tsx migrate.ts diff",
44
- "db:migrate:generate": "npx tsx migrate.ts generate",
45
- "db:migrate:apply": "npx tsx migrate.ts apply",
46
- "db:migrate:rollback": "npx tsx migrate.ts rollback",
47
- "db:migrate:status": "npx tsx migrate.ts status",
42
+ "generate": "node dist/cli.js generate",
43
+ "db:push": "node dist/cli.js db:push",
44
+ "db:pull": "node dist/cli.js db:pull",
45
+ "db:seed": "node dist/cli.js db:seed",
46
+ "db:cleanup": "node dist/cli.js db:cleanup",
47
+ "db:migrate": "node dist/cli.js db:migrate",
48
+ "db:migrate:diff": "node dist/cli.js db:migrate diff",
49
+ "db:migrate:generate": "node dist/cli.js db:migrate generate",
50
+ "db:migrate:apply": "node dist/cli.js db:migrate apply",
51
+ "db:migrate:rollback": "node dist/cli.js db:migrate rollback",
52
+ "db:migrate:status": "node dist/cli.js db:migrate status",
48
53
  "release:cli": "npx tsx scripts/release-cli.ts",
49
54
  "release": "node scripts/release.js",
50
- "test": "npm run typecheck:cli && node test/smoke.test.js && node test/unit.test.js && node test/generator-regressions.test.js && node --test test/config.test.js && node --test test/provider-types.test.js && node --test test/provider-push.test.js && node --test test/provider-push.sqlite.test.js && node --test test/type-kinds.test.js && node --test test/declarations.test.js && node --test test/version-sync.test.js && node --test test/query-semantics.test.js",
55
+ "test": "npm run typecheck:cli && node test/smoke.test.js && node test/unit.test.js && node test/generator-regressions.test.js && node --test test/config.test.js && node --test test/cli.test.js && node --test test/provider-types.test.js && node --test test/provider-push.test.js && node --test test/provider-push.sqlite.test.js && node --test test/type-kinds.test.js && node --test test/declarations.test.js && node --test test/version-sync.test.js && node --test test/query-semantics.test.js",
51
56
  "test:unit": "node test/unit.test.js",
52
57
  "test:generator": "npm run generate && node test/generator.test.js",
53
58
  "test:generator:regressions": "node test/generator-regressions.test.js",