agnes-cli 0.0.5 → 0.0.6
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 +5 -0
- package/package.json +2 -2
- package/src/diff.ts +6 -1
- package/src/generate.ts +18 -1
- package/src/introspect.ts +13 -7
- package/src/ir.ts +7 -1
- package/src/print.ts +42 -5
package/README.md
CHANGED
|
@@ -92,6 +92,11 @@ schema/
|
|
|
92
92
|
index.ts ← import { schema } from "./schema"
|
|
93
93
|
```
|
|
94
94
|
|
|
95
|
+
**Defaults & auto-increment.** `pull` renders defaults in the column's type
|
|
96
|
+
(`bool("x").default(true)`, `int("n").default(1)` — not strings). Serial /
|
|
97
|
+
identity / `AUTO_INCREMENT` columns become `.autoincrement()`, and SQL
|
|
98
|
+
expressions that can't be a literal (e.g. `CURRENT_TIMESTAMP`) are omitted.
|
|
99
|
+
|
|
95
100
|
### `migrate` — versioned SQL files
|
|
96
101
|
|
|
97
102
|
1. Diffs schema vs DB and, if there's drift, writes a timestamped file to
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agnes-cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "Schema migration CLI for agnes-rs — push, pull and migrate from schema.ts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"test": "bun test"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"agnes-library": "0.0.
|
|
26
|
+
"agnes-library": "0.0.6"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@types/bun": "latest"
|
package/src/diff.ts
CHANGED
|
@@ -22,7 +22,12 @@ function byName<T extends { name: string }>(items: T[]): Map<string, T> {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
function columnChanged(a: ColumnIR, b: ColumnIR): boolean {
|
|
25
|
-
return
|
|
25
|
+
return (
|
|
26
|
+
a.type !== b.type ||
|
|
27
|
+
a.nullable !== b.nullable ||
|
|
28
|
+
a.primary !== b.primary ||
|
|
29
|
+
!!a.autoincrement !== !!b.autoincrement
|
|
30
|
+
);
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
/**
|
package/src/generate.ts
CHANGED
|
@@ -3,7 +3,24 @@ import { qualifiedName, type ColumnIR, type ForeignKeyIR, type TableIR } from ".
|
|
|
3
3
|
import type { Operation } from "./diff";
|
|
4
4
|
|
|
5
5
|
function columnDef(dialect: Dialect, col: ColumnIR): string {
|
|
6
|
-
|
|
6
|
+
const name = ident(dialect, col.name);
|
|
7
|
+
const type = physicalType(dialect, col.type);
|
|
8
|
+
|
|
9
|
+
if (col.autoincrement) {
|
|
10
|
+
// SQLite only auto-increments an INTEGER PRIMARY KEY.
|
|
11
|
+
if (dialect === "sqlite") return `${name} INTEGER PRIMARY KEY AUTOINCREMENT`;
|
|
12
|
+
if (dialect === "postgres") {
|
|
13
|
+
let s = `${name} ${type} GENERATED BY DEFAULT AS IDENTITY`;
|
|
14
|
+
if (col.primary) s += " PRIMARY KEY";
|
|
15
|
+
return s;
|
|
16
|
+
}
|
|
17
|
+
// mysql
|
|
18
|
+
let s = `${name} ${type} NOT NULL AUTO_INCREMENT`;
|
|
19
|
+
if (col.primary) s += " PRIMARY KEY";
|
|
20
|
+
return s;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let s = `${name} ${type}`;
|
|
7
24
|
if (col.primary) s += " PRIMARY KEY";
|
|
8
25
|
if (!col.nullable && !col.primary) s += " NOT NULL";
|
|
9
26
|
if (col.default !== undefined) s += ` DEFAULT ${defaultLiteral(dialect, col.default)}`;
|
package/src/introspect.ts
CHANGED
|
@@ -64,13 +64,19 @@ async function introspectPostgres(db: QueryClient, schemas: string[]): Promise<D
|
|
|
64
64
|
);
|
|
65
65
|
const pkSet = new Set(pks.map((r) => str(r.column_name)));
|
|
66
66
|
|
|
67
|
-
const columns: ColumnIR[] = cols.map((c) =>
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
67
|
+
const columns: ColumnIR[] = cols.map((c) => {
|
|
68
|
+
const rawDefault = c.column_default == null ? undefined : str(c.column_default);
|
|
69
|
+
// serial/identity columns default to nextval(...) — surface them as autoincrement.
|
|
70
|
+
const autoincrement = rawDefault != null && /nextval\(/i.test(rawDefault);
|
|
71
|
+
return {
|
|
72
|
+
name: str(c.column_name),
|
|
73
|
+
type: logicalType(str(c.data_type)),
|
|
74
|
+
nullable: str(c.is_nullable) === "YES",
|
|
75
|
+
primary: pkSet.has(str(c.column_name)),
|
|
76
|
+
default: autoincrement ? undefined : rawDefault,
|
|
77
|
+
autoincrement,
|
|
78
|
+
};
|
|
79
|
+
});
|
|
74
80
|
|
|
75
81
|
const idxRows = await db.query<Row>(
|
|
76
82
|
`SELECT i.relname AS index_name, a.attname AS column_name,
|
package/src/ir.ts
CHANGED
|
@@ -11,6 +11,8 @@ export interface ColumnIR {
|
|
|
11
11
|
primary: boolean;
|
|
12
12
|
/** default value as a literal; undefined = no default. */
|
|
13
13
|
default?: unknown;
|
|
14
|
+
/** DB assigns the value (serial/identity/AUTO_INCREMENT). Mutually exclusive with `default`. */
|
|
15
|
+
autoincrement?: boolean;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
export interface IndexIR {
|
|
@@ -59,6 +61,7 @@ interface DslColumn {
|
|
|
59
61
|
primary?: boolean;
|
|
60
62
|
nullable?: boolean;
|
|
61
63
|
default?: unknown;
|
|
64
|
+
autoincrement?: boolean;
|
|
62
65
|
index?: { name: string; unique: boolean };
|
|
63
66
|
};
|
|
64
67
|
}
|
|
@@ -115,12 +118,15 @@ export function schemaToIR(schema: DslSchema): DatabaseIR {
|
|
|
115
118
|
for (const fieldKey in def) {
|
|
116
119
|
const field = def[fieldKey]!;
|
|
117
120
|
if (field._kind === "column") {
|
|
121
|
+
const autoincrement = field.flags.autoincrement ?? false;
|
|
118
122
|
columns.push({
|
|
119
123
|
name: field.name,
|
|
120
124
|
type: field.type,
|
|
121
125
|
nullable: field.flags.nullable ?? false,
|
|
122
126
|
primary: field.flags.primary ?? false,
|
|
123
|
-
default
|
|
127
|
+
// Auto-increment supplies the value; never emit an explicit default too.
|
|
128
|
+
default: autoincrement ? undefined : field.flags.default,
|
|
129
|
+
autoincrement,
|
|
124
130
|
});
|
|
125
131
|
if (field.flags.index) {
|
|
126
132
|
indexes.push({
|
package/src/print.ts
CHANGED
|
@@ -25,10 +25,42 @@ function onAction(rule: string): string {
|
|
|
25
25
|
return ON_ACTION[rule.toUpperCase()] ?? "OnAction.None";
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Render a raw DB default into the `.default(...)` argument for a given column
|
|
30
|
+
* type, or `null` to omit it. Introspection hands us raw strings ("true", "1",
|
|
31
|
+
* "'x'::text", "CURRENT_TIMESTAMP"); we coerce them to the type the DSL expects
|
|
32
|
+
* and drop SQL expressions we can't represent as a literal.
|
|
33
|
+
*/
|
|
34
|
+
function renderDefault(type: ColumnType, raw: unknown): string | null {
|
|
35
|
+
if (raw === undefined || raw === null) return null;
|
|
36
|
+
const s = String(raw).trim();
|
|
37
|
+
// Strip a trailing type cast: 'x'::text, 5::integer, 'a'::character varying.
|
|
38
|
+
const cast = s.replace(/::[\w\s"]+$/, "").trim();
|
|
39
|
+
const quoted = cast.match(/^'([\s\S]*)'$/);
|
|
40
|
+
const inner = quoted ? quoted[1]!.replace(/''/g, "'") : undefined;
|
|
41
|
+
const scalar = inner ?? cast;
|
|
42
|
+
|
|
43
|
+
switch (type) {
|
|
44
|
+
case "bool": {
|
|
45
|
+
const t = scalar.toLowerCase();
|
|
46
|
+
if (t === "true" || t === "t" || t === "1") return "true";
|
|
47
|
+
if (t === "false" || t === "f" || t === "0") return "false";
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
case "int":
|
|
51
|
+
case "float": {
|
|
52
|
+
const n = Number(scalar);
|
|
53
|
+
return scalar !== "" && Number.isFinite(n) ? String(n) : null;
|
|
54
|
+
}
|
|
55
|
+
case "bigint":
|
|
56
|
+
return /^-?\d+$/.test(scalar) ? `${scalar}n` : null;
|
|
57
|
+
case "text":
|
|
58
|
+
case "json":
|
|
59
|
+
// Only string literals; skip expressions (CURRENT_TIMESTAMP, now(), …).
|
|
60
|
+
return inner !== undefined ? JSON.stringify(inner) : null;
|
|
61
|
+
default:
|
|
62
|
+
return null; // bytes
|
|
63
|
+
}
|
|
32
64
|
}
|
|
33
65
|
|
|
34
66
|
/** A qualified name → a valid JS identifier (for the `schema` object keys / relation keys). */
|
|
@@ -45,7 +77,12 @@ function tableSource(t: TableIR): string {
|
|
|
45
77
|
let expr = `${HELPER[col.type]}(${JSON.stringify(col.name)})`;
|
|
46
78
|
if (col.primary) expr += ".primary()";
|
|
47
79
|
else if (col.nullable) expr += ".nullable()";
|
|
48
|
-
if (col.
|
|
80
|
+
if (col.autoincrement) {
|
|
81
|
+
expr += ".autoincrement()";
|
|
82
|
+
} else {
|
|
83
|
+
const d = renderDefault(col.type, col.default);
|
|
84
|
+
if (d !== null) expr += `.default(${d})`;
|
|
85
|
+
}
|
|
49
86
|
for (const idx of t.indexes) {
|
|
50
87
|
if (idx.columns.length === 1 && idx.columns[0] === col.name) {
|
|
51
88
|
expr += idx.unique
|