@an5/orm 1.0.9 → 1.1.0
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 +10 -3
- package/dist/config.d.ts +9 -1
- package/dist/config.js +13 -18
- package/dist/generator/src/api.d.ts +35 -0
- package/dist/generator/src/api.js +58 -0
- package/dist/generator/src/config.d.ts +162 -0
- package/dist/generator/src/config.js +354 -0
- package/dist/generator/src/dialect.d.ts +88 -0
- package/dist/generator/src/dialect.js +318 -0
- package/dist/generator/src/dotnet-generator.d.ts +8 -0
- package/dist/generator/src/dotnet-generator.js +282 -101
- package/dist/generator/src/field-types.d.ts +96 -0
- package/dist/generator/src/field-types.js +433 -0
- package/dist/generator/src/golang-generator.d.ts +36 -0
- package/dist/generator/src/golang-generator.js +1229 -0
- package/dist/generator/src/index.js +69 -36
- package/dist/generator/src/metadata-generator.js +2 -1
- package/dist/generator/src/parser.d.ts +28 -2
- package/dist/generator/src/parser.js +92 -90
- package/dist/generator/src/push-apply.d.ts +37 -0
- package/dist/generator/src/push-apply.js +109 -0
- package/dist/generator/src/push-schema.d.ts +93 -0
- package/dist/generator/src/push-schema.js +189 -0
- package/dist/generator/src/python-generator.d.ts +19 -1
- package/dist/generator/src/python-generator.js +147 -64
- package/dist/generator/src/rust-generator.d.ts +38 -0
- package/dist/generator/src/rust-generator.js +971 -0
- package/dist/generator/src/suggest.d.ts +17 -0
- package/dist/generator/src/suggest.js +45 -0
- package/dist/generator/src/type-kinds.d.ts +27 -0
- package/dist/generator/src/type-kinds.js +72 -0
- package/dist/generator/src/types.d.ts +33 -0
- package/dist/generator/src/types.js +39 -0
- package/dist/migration-core.d.ts +16 -1
- package/dist/migration-core.js +60 -21
- package/dist/orm.d.ts +147 -0
- package/dist/orm.js +1654 -0
- package/dist/provider-support.d.ts +27 -0
- package/dist/provider-support.js +50 -0
- package/generator/src/api.ts +45 -0
- package/generator/src/config.ts +496 -0
- package/generator/src/dialect.ts +415 -0
- package/generator/src/dotnet-generator.ts +277 -88
- package/generator/src/field-types.ts +473 -0
- package/generator/src/golang-generator.ts +89 -64
- package/generator/src/index.ts +71 -36
- package/generator/src/metadata-generator.ts +2 -2
- package/generator/src/parser.ts +122 -90
- package/generator/src/push-apply.ts +148 -0
- package/generator/src/push-schema.ts +250 -0
- package/generator/src/python-generator.ts +118 -62
- package/generator/src/rust-generator.ts +974 -0
- package/generator/src/suggest.ts +41 -0
- package/generator/src/type-kinds.ts +78 -0
- package/generator/src/types.ts +48 -0
- package/package.json +26 -6
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Did you mean" for typos.
|
|
3
|
+
*
|
|
4
|
+
* Lives on its own because two callers need it and neither should own it:
|
|
5
|
+
* the config loader (a mistyped option key) and the field type registry (a
|
|
6
|
+
* type written for the wrong database). Both want the nearest candidate out
|
|
7
|
+
* of a known list, or nothing at all when the input is not close enough that
|
|
8
|
+
* a suggestion would be honest.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Levenshtein distance between two strings. */
|
|
12
|
+
export function editDistance(a: string, b: string): number {
|
|
13
|
+
const rows: number[][] = [];
|
|
14
|
+
for (let i = 0; i <= a.length; i++) rows.push([i, ...new Array<number>(b.length).fill(0)]);
|
|
15
|
+
for (let j = 0; j <= b.length; j++) rows[0]![j] = j;
|
|
16
|
+
for (let i = 1; i <= a.length; i++) {
|
|
17
|
+
for (let j = 1; j <= b.length; j++) {
|
|
18
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
19
|
+
rows[i]![j] = Math.min(rows[i - 1]![j]! + 1, rows[i]![j - 1]! + 1, rows[i - 1]![j - 1]! + cost);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return rows[a.length]![b.length]!;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The candidate closest to `input`, or null when none is close enough to be a
|
|
27
|
+
* plausible typo. Comparing case-insensitively keeps `nvarchar` from suggesting
|
|
28
|
+
* something unrelated just because of capitalisation.
|
|
29
|
+
*/
|
|
30
|
+
export function suggest(input: string, candidates: readonly string[]): string | null {
|
|
31
|
+
let best: string | null = null;
|
|
32
|
+
let bestDistance = Infinity;
|
|
33
|
+
for (const candidate of candidates) {
|
|
34
|
+
const distance = editDistance(input.toLowerCase(), candidate.toLowerCase());
|
|
35
|
+
if (distance < bestDistance) {
|
|
36
|
+
bestDistance = distance;
|
|
37
|
+
best = candidate;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return best !== null && bestDistance <= Math.max(2, Math.floor(input.length / 3)) ? best : null;
|
|
41
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a field *is*, for the generators that need a real type.
|
|
3
|
+
*
|
|
4
|
+
* The parser gives every generator the same TypeScript type, and that type loses
|
|
5
|
+
* what a language needs: `INT`, `FLOAT` and `DECIMAL` all arrive as `number`, so
|
|
6
|
+
* the Go client generated every numeric column as a `string`, and the Python, Rust
|
|
7
|
+
* and .NET clients turned a `DECIMAL` into an integer type. The declared type is
|
|
8
|
+
* right there in `Field.sqlType`, so it decides — with the TypeScript type as the
|
|
9
|
+
* fallback for metadata generated before that was recorded.
|
|
10
|
+
*/
|
|
11
|
+
import { Provider } from './field-types';
|
|
12
|
+
|
|
13
|
+
/** The categories a generated language type can be built from. */
|
|
14
|
+
export type FieldKind = 'int' | 'bigint' | 'float' | 'bool' | 'date' | 'bytes' | 'json' | 'vector' | 'string';
|
|
15
|
+
|
|
16
|
+
export interface KindInput {
|
|
17
|
+
/** The TypeScript type the parser produced, e.g. `number | bigint`. */
|
|
18
|
+
type: string;
|
|
19
|
+
/** The type as written in the schema, e.g. `DECIMAL(10,2)`. */
|
|
20
|
+
sqlType?: string | undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const INT = ['INT', 'INTEGER', 'SMALLINT', 'TINYINT', 'MEDIUMINT', 'INT2', 'INT4', 'SERIAL', 'SMALLSERIAL', 'YEAR'];
|
|
24
|
+
const BIGINT = ['BIGINT', 'INT8', 'BIGSERIAL', 'UNSIGNED BIG INT'];
|
|
25
|
+
const FLOAT = ['FLOAT', 'REAL', 'DOUBLE', 'DOUBLE PRECISION', 'DECIMAL', 'NUMERIC', 'MONEY', 'SMALLMONEY', 'FIXED'];
|
|
26
|
+
const BOOL = ['BIT', 'BOOL', 'BOOLEAN'];
|
|
27
|
+
const DATE = [
|
|
28
|
+
'DATE', 'DATETIME', 'DATETIME2', 'SMALLDATETIME', 'DATETIMEOFFSET',
|
|
29
|
+
'TIME', 'TIMETZ', 'TIMESTAMPTZ', 'TIME WITH TIME ZONE', 'TIME WITHOUT TIME ZONE',
|
|
30
|
+
'TIMESTAMP WITH TIME ZONE', 'TIMESTAMP WITHOUT TIME ZONE',
|
|
31
|
+
];
|
|
32
|
+
const BYTES = ['BINARY', 'VARBINARY', 'IMAGE', 'BLOB', 'TINYBLOB', 'MEDIUMBLOB', 'LONGBLOB', 'BYTEA', 'ROWVERSION', 'BYTES'];
|
|
33
|
+
const JSON = ['JSON', 'JSONB', 'SQL_VARIANT'];
|
|
34
|
+
|
|
35
|
+
/** `DECIMAL(10,2)` → `DECIMAL`. */
|
|
36
|
+
function declaredBase(sqlType: string): string {
|
|
37
|
+
const withoutParams = sqlType.replace(/\([^)]*\)/g, ' ').trim();
|
|
38
|
+
return withoutParams.replace(/\s+/g, ' ').toUpperCase();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The category a field belongs to.
|
|
43
|
+
*
|
|
44
|
+
* `TIMESTAMP` is the reason `provider` is a parameter: it is a rowversion on SQL
|
|
45
|
+
* Server and a point in time everywhere else, and the same word has to produce
|
|
46
|
+
* bytes in one and a date in the other.
|
|
47
|
+
*/
|
|
48
|
+
export function fieldKind(field: KindInput, provider?: Provider | undefined): FieldKind {
|
|
49
|
+
const base = field.sqlType ? declaredBase(field.sqlType) : '';
|
|
50
|
+
if (base) {
|
|
51
|
+
if (base === 'TIMESTAMP') return provider === 'mssql' ? 'bytes' : 'date';
|
|
52
|
+
if (INT.includes(base)) return 'int';
|
|
53
|
+
if (BIGINT.includes(base)) return 'bigint';
|
|
54
|
+
if (FLOAT.includes(base)) return 'float';
|
|
55
|
+
if (BOOL.includes(base)) return 'bool';
|
|
56
|
+
if (DATE.includes(base) || base === 'TIMESTAMP') return 'date';
|
|
57
|
+
if (BYTES.includes(base)) return 'bytes';
|
|
58
|
+
if (JSON.includes(base)) return 'json';
|
|
59
|
+
if (base === 'VECTOR') return 'vector';
|
|
60
|
+
return 'string';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// No declared type: fall back to what the TypeScript type can still tell us. A
|
|
64
|
+
// plain `number` is an integer here, which is what every generator assumed
|
|
65
|
+
// before `sqlType` was available.
|
|
66
|
+
const raw = field.type.trim().toLowerCase();
|
|
67
|
+
// Checked before the brackets are stripped, or `number[] | string` — the vector
|
|
68
|
+
// type — would read as a number.
|
|
69
|
+
if (raw.includes('[]')) return 'vector';
|
|
70
|
+
const ts = raw.replace(/[?\]]/g, '').trim();
|
|
71
|
+
if (ts.startsWith('number | bigint')) return 'bigint';
|
|
72
|
+
if (ts.startsWith('number')) return 'int';
|
|
73
|
+
if (ts.startsWith('date')) return 'date';
|
|
74
|
+
if (ts.startsWith('bool')) return 'bool';
|
|
75
|
+
if (ts.startsWith('buffer')) return 'bytes';
|
|
76
|
+
if (ts === 'any') return 'json';
|
|
77
|
+
return 'string';
|
|
78
|
+
}
|
package/generator/src/types.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { Provider } from './field-types';
|
|
2
|
+
|
|
1
3
|
export interface Field {
|
|
2
4
|
name: string;
|
|
3
5
|
type: string; // TypeScript type (string, number, boolean, Date, etc.)
|
|
@@ -16,14 +18,60 @@ export interface Relation {
|
|
|
16
18
|
foreignKey: string;
|
|
17
19
|
localKey: string;
|
|
18
20
|
relationName: string;
|
|
21
|
+
/** From a trailing @description("...") on the relation line. */
|
|
22
|
+
description?: string;
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
export interface Model {
|
|
22
26
|
name: string;
|
|
23
27
|
tableName: string;
|
|
28
|
+
/** Empty for a database with no schemas, or when `@@schema("")` says so. */
|
|
24
29
|
schemaName: string;
|
|
30
|
+
/**
|
|
31
|
+
* The database this schema targets. Set by `SchemaParser`, and what decides how a
|
|
32
|
+
* table name is written — see `bracketedTableName`.
|
|
33
|
+
*/
|
|
34
|
+
provider?: Provider | undefined;
|
|
25
35
|
fields: Field[];
|
|
26
36
|
relations: Relation[];
|
|
27
37
|
compoundUniques?: string[][];
|
|
28
38
|
description?: string;
|
|
29
39
|
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The table name as it goes into the generated metadata.
|
|
43
|
+
*
|
|
44
|
+
* SQL Server keeps the bracketed form it has always had. Every other provider gets
|
|
45
|
+
* the bare name, with the schema as a plain prefix, because the brackets are not
|
|
46
|
+
* portable: `[dbo].[users]` is invalid SQL in PostgreSQL, and on SQLite it fails with
|
|
47
|
+
* "no such table: dbo.users" — which is why the adapters had to strip it afterwards.
|
|
48
|
+
* A bare name is correct everywhere once the adapter quotes it, and the adapters
|
|
49
|
+
* already quote per dialect (`quote()` in `@an5/adapters`, `An5DbContext.Quote()` in
|
|
50
|
+
* the .NET client), so the one form that needs no translation is the one emitted.
|
|
51
|
+
*
|
|
52
|
+
* `dbo` is SQL Server's default schema and only SQL Server's: with `@@schema("")` —
|
|
53
|
+
* an empty schema, meant for databases that have no schema concept — the prefix has
|
|
54
|
+
* to go, because `[].[users]` is not valid SQL. The `[${schemaName}].[${tableName}]`
|
|
55
|
+
* formula used to be written out in six places, so every dialect got `[dbo]`.
|
|
56
|
+
*/
|
|
57
|
+
export function bracketedTableName(model: Model): string {
|
|
58
|
+
const qualified = model.schemaName
|
|
59
|
+
? `${model.schemaName}.${model.tableName}`
|
|
60
|
+
: model.tableName;
|
|
61
|
+
if ((model.provider ?? 'mssql') !== 'mssql') return qualified;
|
|
62
|
+
return model.schemaName
|
|
63
|
+
? `[${model.schemaName}].[${model.tableName}]`
|
|
64
|
+
: `[${model.tableName}]`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The table name as `schema.table`, for clients that pass a string instead of SQL.
|
|
69
|
+
*
|
|
70
|
+
* Unquoted for the same reason as `bracketedTableName`: these strings go to the same
|
|
71
|
+
* per-dialect quoting.
|
|
72
|
+
*/
|
|
73
|
+
export function dottedTableName(model: Model): string {
|
|
74
|
+
return model.schemaName
|
|
75
|
+
? `${model.schemaName}.${model.tableName}`
|
|
76
|
+
: model.tableName;
|
|
77
|
+
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@an5/orm",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "The lightweight ORM for SQL Server with support for TypeScript, Python, .NET (C#), and
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "The lightweight ORM for SQL Server with support for TypeScript, Python, .NET (C#), Go, and Rust.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./generator": {
|
|
14
|
+
"types": "./dist/generator/src/api.d.ts",
|
|
15
|
+
"default": "./dist/generator/src/api.js"
|
|
16
|
+
},
|
|
17
|
+
"./dist/*": "./dist/*",
|
|
18
|
+
"./package.json": "./package.json"
|
|
19
|
+
},
|
|
8
20
|
"files": [
|
|
9
21
|
"dist/**/*.{js,d.ts}",
|
|
10
22
|
"generator/src/**/*.ts",
|
|
@@ -17,9 +29,11 @@
|
|
|
17
29
|
"url": "https://github.com/an5ORM/an5Orm.git"
|
|
18
30
|
},
|
|
19
31
|
"scripts": {
|
|
20
|
-
"build": "tsc -p tsconfig.json",
|
|
32
|
+
"build": "tsc -p tsconfig.json && tsc -p generator/tsconfig.json && node scripts/normalize-eol.js",
|
|
33
|
+
"typecheck:cli": "tsc -p tsconfig.cli.json",
|
|
21
34
|
"pretest": "npm run build",
|
|
22
|
-
"prepack": "npm run build",
|
|
35
|
+
"prepack": "npm run build && npm run verify:pack",
|
|
36
|
+
"prepublishOnly": "npm run verify:pack",
|
|
23
37
|
"generate": "npx tsx generator/src/index.ts",
|
|
24
38
|
"db:push": "npx tsx push.ts",
|
|
25
39
|
"db:pull": "npx tsx pull.ts",
|
|
@@ -33,10 +47,16 @@
|
|
|
33
47
|
"db:migrate:status": "npx tsx migrate.ts status",
|
|
34
48
|
"release:cli": "npx tsx scripts/release-cli.ts",
|
|
35
49
|
"release": "node scripts/release.js",
|
|
36
|
-
"test": "node test/smoke.test.js && node test/unit.test.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",
|
|
37
51
|
"test:unit": "node test/unit.test.js",
|
|
38
52
|
"test:generator": "npm run generate && node test/generator.test.js",
|
|
39
|
-
"test:
|
|
53
|
+
"test:generator:regressions": "node test/generator-regressions.test.js",
|
|
54
|
+
"test:provider-types": "node --test test/provider-types.test.js",
|
|
55
|
+
"test:provider-push": "node --test test/provider-push.test.js",
|
|
56
|
+
"test:provider-push:sqlite": "node --test test/provider-push.sqlite.test.js",
|
|
57
|
+
"test:integration:live": "node test/live-db.integration.test.js",
|
|
58
|
+
"verify:pack": "node scripts/verify-pack.js",
|
|
59
|
+
"normalize:eol": "node scripts/normalize-eol.js"
|
|
40
60
|
},
|
|
41
61
|
"dependencies": {
|
|
42
62
|
"@an5/adapters": "^0.2.6",
|