@an5/orm 1.3.1 → 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.
- package/README.md +74 -11
- package/dist/cleanup.d.ts +1 -0
- package/dist/cleanup.js +177 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +147 -0
- package/dist/generator/src/attributes.d.ts +57 -0
- package/dist/generator/src/attributes.js +225 -0
- package/dist/generator/src/code-generator.js +49 -16
- package/dist/generator/src/config.d.ts +2 -2
- package/dist/generator/src/config.js +4 -3
- package/dist/generator/src/dialect.d.ts +52 -1
- package/dist/generator/src/dialect.js +136 -3
- package/dist/generator/src/dotnet-generator.js +107 -27
- package/dist/generator/src/golang-generator.d.ts +8 -0
- package/dist/generator/src/golang-generator.js +116 -49
- package/dist/generator/src/java-generator.d.ts +8 -0
- package/dist/generator/src/java-generator.js +108 -3
- package/dist/generator/src/kotlin-generator.js +51 -0
- package/dist/generator/src/parser.d.ts +1 -0
- package/dist/generator/src/parser.js +90 -35
- package/dist/generator/src/pull-schema.d.ts +54 -0
- package/dist/generator/src/pull-schema.js +116 -0
- package/dist/generator/src/push-apply.d.ts +4 -2
- package/dist/generator/src/push-apply.js +53 -4
- package/dist/generator/src/push-schema.d.ts +21 -9
- package/dist/generator/src/push-schema.js +132 -32
- package/dist/generator/src/python-generator.js +35 -22
- package/dist/generator/src/rust-generator.js +79 -74
- package/dist/generator/src/swift-generator.js +54 -3
- package/dist/generator/src/types.d.ts +10 -0
- package/dist/generator/src/view-order.d.ts +33 -0
- package/dist/generator/src/view-order.js +123 -0
- package/dist/migrate.d.ts +12 -0
- package/dist/migrate.js +442 -0
- package/dist/migration-core.d.ts +32 -3
- package/dist/migration-core.js +213 -43
- package/dist/provider-support.d.ts +0 -1
- package/dist/provider-support.js +0 -1
- package/dist/pull.d.ts +1 -0
- package/dist/pull.js +425 -0
- package/dist/push.d.ts +1 -0
- package/dist/push.js +93 -0
- package/dist/seed.d.ts +1 -0
- package/dist/seed.js +33 -0
- package/generator/src/attributes.ts +231 -0
- package/generator/src/code-generator.ts +47 -16
- package/generator/src/config.ts +5 -3
- package/generator/src/dialect.ts +188 -5
- package/generator/src/dotnet-generator.ts +108 -28
- package/generator/src/golang-generator.ts +119 -50
- package/generator/src/java-generator.ts +109 -3
- package/generator/src/kotlin-generator.ts +51 -0
- package/generator/src/parser.ts +102 -30
- package/generator/src/pull-schema.ts +127 -0
- package/generator/src/push-apply.ts +62 -5
- package/generator/src/push-schema.ts +160 -32
- package/generator/src/python-generator.ts +33 -20
- package/generator/src/rust-generator.ts +79 -73
- package/generator/src/swift-generator.ts +55 -4
- package/generator/src/types.ts +10 -0
- package/generator/src/view-order.ts +133 -0
- 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
|
-
|
|
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
|
}
|
package/generator/src/types.ts
CHANGED
|
@@ -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.
|
|
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": "
|
|
38
|
-
"db:push": "
|
|
39
|
-
"db:pull": "
|
|
40
|
-
"db:seed": "
|
|
41
|
-
"db:cleanup": "
|
|
42
|
-
"db:migrate": "
|
|
43
|
-
"db:migrate:diff": "
|
|
44
|
-
"db:migrate:generate": "
|
|
45
|
-
"db:migrate:apply": "
|
|
46
|
-
"db:migrate:rollback": "
|
|
47
|
-
"db:migrate: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",
|