@an5/orm 1.3.2 → 1.3.4
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 +67 -23
- package/dist/generator/src/golang-generator.d.ts +8 -0
- package/dist/generator/src/golang-generator.js +86 -21
- 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 +68 -24
- package/generator/src/golang-generator.ts +89 -22
- 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
|
@@ -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) =>
|
|
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';
|