@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
@@ -569,12 +569,13 @@ public enum An5Config {
569
569
  let accessors = models.map((model) => ({
570
570
  property: SwiftGenerator.toLowerCamelCase(model.name),
571
571
  name: model.name,
572
+ isView: model.isView === true,
572
573
  }));
573
574
  const unique = new Map();
574
575
  for (const entry of accessors) {
575
576
  const key = SwiftGenerator.toLowerCamelCase(entry.name);
576
577
  if (!unique.has(key))
577
- unique.set(key, { property: key, name: entry.name });
578
+ unique.set(key, { property: key, name: entry.name, isView: entry.isView });
578
579
  }
579
580
  let content = `// This file is auto-generated. Do not edit deliberately.
580
581
  import Foundation
@@ -669,6 +670,54 @@ public struct ModelClient<T> {
669
670
  }
670
671
  }
671
672
 
673
+ /// A read-only, typed client for one view.
674
+ ///
675
+ /// The name avoids the runtime's own \`ViewClient\`, which is wrapped here: the runtime
676
+ /// returns \`Row\` values and this maps them to the generated model. There are no write
677
+ /// methods, so writing through a view does not compile.
678
+ public struct ViewModelClient<T> {
679
+
680
+ private let client: ViewClient
681
+ private let read: (Row) -> T
682
+
683
+ init(view: ViewClient, read: @escaping (Row) -> T) {
684
+ self.client = view
685
+ self.read = read
686
+ }
687
+
688
+ /// The view this client reads.
689
+ public var view: String { client.view }
690
+
691
+ public func findMany() throws -> [T] { try client.findMany().map { read($0) } }
692
+
693
+ public func findMany(_ query: Query) throws -> [T] { try client.findMany(query).map { read($0) } }
694
+
695
+ public func findMany(filter: Where) throws -> [T] { try client.findMany(filter: filter).map { read($0) } }
696
+
697
+ public func findFirst(filter: Where) throws -> T? { try client.findFirst(filter: filter).map { read($0) } }
698
+
699
+ public func findUnique(filter: Where) throws -> T? { try client.findUnique(filter: filter).map { read($0) } }
700
+
701
+ public func count(_ filter: Where? = nil) throws -> Int { try client.count(filter) }
702
+
703
+ /// One row of aggregate values, keyed \`_count\`, \`_sum_<field>\` and so on.
704
+ public func aggregate(_ aggregate: Aggregate) throws -> Row { try client.aggregate(aggregate) }
705
+
706
+ /// One row per group, each carrying the group's count and aggregates.
707
+ public func groupBy(_ aggregate: Aggregate) throws -> [Row] { try client.groupBy(aggregate) }
708
+
709
+ /// The rows nearest a vector.
710
+ public func vectorSearch(
711
+ _ vector: [Double],
712
+ take: Int = 10,
713
+ filter: Where? = nil,
714
+ vectorField: String = "embedding",
715
+ metric: DistanceMetric = .cosine
716
+ ) throws -> [T] {
717
+ try client.vectorSearch(vector, take: take, filter: filter, vectorField: vectorField, metric: metric).map { read($0) }
718
+ }
719
+ }
720
+
672
721
  /// The AN5 entry point: one typed client per model, over one connection.
673
722
  ///
674
723
  /// \`\`\`
@@ -681,7 +730,7 @@ public final class An5Db {
681
730
  public let an5: An5Adapter
682
731
 
683
732
  ${Array.from(unique.values())
684
- .map((entry) => ` /// Queries for \`${entry.name}\`.\n public let ${entry.property}: ModelClient<${entry.name}>`)
733
+ .map((entry) => ` /// ${entry.isView ? 'Reads' : 'Queries'} for \`${entry.name}\`.\n public let ${entry.property}: ${entry.isView ? 'ViewModelClient' : 'ModelClient'}<${entry.name}>`)
685
734
  .join('\n\n')}
686
735
 
687
736
  /// Opens a database from a connection string, registering this schema with the runtime.
@@ -704,7 +753,9 @@ ${Array.from(unique.values())
704
753
  public init(adapter: An5Adapter) {
705
754
  self.an5 = adapter
706
755
  ${Array.from(unique.values())
707
- .map((entry) => ` self.${entry.property} = ModelClient(\n table: adapter.table("${entry.name}"),\n read: { ${entry.name}(row: $0) },\n write: { $0.values })`)
756
+ .map((entry) => entry.isView
757
+ ? ` self.${entry.property} = ViewModelClient(\n view: adapter.view("${entry.name}"),\n read: { ${entry.name}(row: $0) })`
758
+ : ` self.${entry.property} = ModelClient(\n table: adapter.table("${entry.name}"),\n read: { ${entry.name}(row: $0) },\n write: { $0.values })`)
708
759
  .join('\n')}
709
760
  }
710
761
 
@@ -33,6 +33,16 @@ export interface Model {
33
33
  relations: Relation[];
34
34
  compoundUniques?: string[][];
35
35
  description?: string;
36
+ /**
37
+ * A `view` block rather than a `model`.
38
+ *
39
+ * Views are described with the same field syntax so generated clients can type
40
+ * their rows, but they are read-only and their DDL comes from `viewSql` rather
41
+ * than from the fields.
42
+ */
43
+ isView?: boolean;
44
+ /** The `SELECT` body of `@@sql("...")`; required for a view. */
45
+ viewSql?: string;
36
46
  }
37
47
  /**
38
48
  * The table name as it goes into the generated metadata.
@@ -0,0 +1,33 @@
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
+ /** A view plus every identifier its SQL may use to reference it. */
15
+ export interface ViewOrderEntry {
16
+ /** Physical name, model name, or both; qualified names are reduced to their last part too. */
17
+ names: readonly string[];
18
+ sql: string;
19
+ }
20
+ export interface ViewOrderResult<T> {
21
+ ordered: T[];
22
+ /** True when the references form a cycle, which no view definition can satisfy. */
23
+ cyclic: boolean;
24
+ }
25
+ /**
26
+ * Stable topological sort of `views`.
27
+ *
28
+ * Kahn's algorithm in declaration order, so a schema that was already correct is
29
+ * left untouched. A cycle cannot be created by a database view definition; when
30
+ * one is detected the remaining views keep their original order and the caller
31
+ * can decide whether to warn or let the database report the error.
32
+ */
33
+ export declare function orderViewsByDependency<T extends ViewOrderEntry>(views: readonly T[]): ViewOrderResult<T>;
@@ -0,0 +1,123 @@
1
+ "use strict";
2
+ /**
3
+ * Orders views so one that reads another is created after it.
4
+ *
5
+ * Tables are created before any view, but views are not all independent of each
6
+ * other: a summary view may select from a detail view. PostgreSQL and SQLite
7
+ * reject `CREATE VIEW` when the referenced relation does not exist yet, so
8
+ * declaration order in the schema file is not enough.
9
+ *
10
+ * The reference check is deliberately textual: a full SQL parser would be the
11
+ * only way to be exact, and a false positive here merely reorders two independent
12
+ * views. Missing an edge leaves the old declaration order, which is what this
13
+ * function improves on rather than replaces.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.orderViewsByDependency = orderViewsByDependency;
17
+ /** Escapes a name for a regular expression. */
18
+ function escapeRegExp(value) {
19
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
20
+ }
21
+ /** Identifiers in `sql` lose their quoting so `[s].[v]`, `"s"."v"` and `s.v` compare alike. */
22
+ function unquoteIdentifiers(sql) {
23
+ return sql.replace(/[[\]"`]/g, '');
24
+ }
25
+ function mentions(sql, name) {
26
+ if (!name)
27
+ return false;
28
+ const pattern = new RegExp(`(^|[^A-Za-z0-9_$])${escapeRegExp(name)}([^A-Za-z0-9_$]|$)`, 'i');
29
+ return pattern.test(sql);
30
+ }
31
+ /** Every way a view in this schema can be referenced: `schema.name`, `name`, or a mapped name. */
32
+ function referenceNames(names) {
33
+ const result = new Set();
34
+ for (const name of names) {
35
+ const trimmed = name.trim();
36
+ if (!trimmed)
37
+ continue;
38
+ result.add(trimmed);
39
+ const dot = trimmed.lastIndexOf('.');
40
+ if (dot >= 0 && dot < trimmed.length - 1)
41
+ result.add(trimmed.slice(dot + 1));
42
+ }
43
+ return Array.from(result);
44
+ }
45
+ /**
46
+ * Stable topological sort of `views`.
47
+ *
48
+ * Kahn's algorithm in declaration order, so a schema that was already correct is
49
+ * left untouched. A cycle cannot be created by a database view definition; when
50
+ * one is detected the remaining views keep their original order and the caller
51
+ * can decide whether to warn or let the database report the error.
52
+ */
53
+ function orderViewsByDependency(views) {
54
+ if (views.length < 2)
55
+ return { ordered: [...views], cyclic: false };
56
+ const references = views.map((view) => referenceNames(view.names));
57
+ const normalizedSql = views.map((view) => unquoteIdentifiers(view.sql));
58
+ const byName = new Map();
59
+ const owners = new Map();
60
+ for (let index = 0; index < references.length; index += 1) {
61
+ for (const name of references[index] ?? []) {
62
+ const key = name.toLowerCase();
63
+ const existing = owners.get(key);
64
+ if (existing)
65
+ existing.add(index);
66
+ else
67
+ owners.set(key, new Set([index]));
68
+ }
69
+ }
70
+ // A bare `summary` is ambiguous when two schemas both have one; a dependency
71
+ // guessed from it could reorder unrelated views or invent a cycle. Only names
72
+ // that identify one view take part.
73
+ for (const [name, indices] of owners) {
74
+ if (indices.size === 1)
75
+ byName.set(name, Array.from(indices));
76
+ }
77
+ const dependencies = views.map(() => new Set());
78
+ for (let index = 0; index < views.length; index += 1) {
79
+ for (const [name, candidates] of byName) {
80
+ if (!mentions(normalizedSql[index] ?? '', name))
81
+ continue;
82
+ for (const candidate of candidates) {
83
+ if (candidate !== index)
84
+ dependencies[index]?.add(candidate);
85
+ }
86
+ }
87
+ }
88
+ const dependents = views.map(() => []);
89
+ const indegree = views.map((_, index) => dependencies[index]?.size ?? 0);
90
+ for (let index = 0; index < dependencies.length; index += 1) {
91
+ for (const dependency of dependencies[index] ?? []) {
92
+ dependents[dependency]?.push(index);
93
+ }
94
+ }
95
+ const ordered = [];
96
+ const queued = new Set();
97
+ const ready = [];
98
+ for (let index = 0; index < views.length; index += 1) {
99
+ if ((indegree[index] ?? 0) === 0)
100
+ ready.push(index);
101
+ }
102
+ while (ready.length > 0) {
103
+ const index = ready.shift();
104
+ if (queued.has(index))
105
+ continue;
106
+ queued.add(index);
107
+ ordered.push(views[index]);
108
+ for (const dependent of dependents[index] ?? []) {
109
+ const remaining = (indegree[dependent] ?? 0) - 1;
110
+ indegree[dependent] = remaining;
111
+ if (remaining === 0)
112
+ ready.push(dependent);
113
+ }
114
+ }
115
+ const cyclic = ordered.length !== views.length;
116
+ // A cycle is preserved as written, which is the only order the caller can act
117
+ // on; dropping the views would silently change the schema.
118
+ for (let index = 0; index < views.length; index += 1) {
119
+ if (!queued.has(index))
120
+ ordered.push(views[index]);
121
+ }
122
+ return { ordered, cyclic };
123
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * an5Orm Migration Tool
3
+ * Compares schema files with database and generates migration SQL.
4
+ *
5
+ * Usage:
6
+ * npx tsx migrate.ts diff # Show differences
7
+ * npx tsx migrate.ts generate # Generate migration file
8
+ * npx tsx migrate.ts apply # Apply pending migrations
9
+ * npx tsx migrate.ts rollback [steps|--to file] # Roll back applied migrations
10
+ * npx tsx migrate.ts status # Show migration status
11
+ */
12
+ import 'dotenv/config';