rip-lang 3.16.1 → 3.16.2
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 +2 -3
- package/bin/rip +39 -8
- package/bin/rip-schema +175 -0
- package/docs/RIP-APP.md +91 -2
- package/docs/RIP-DUCKDB.md +64 -1
- package/docs/RIP-INTRO.md +4 -4
- package/docs/RIP-LANG.md +32 -33
- package/docs/RIP-SCHEMA.md +1204 -364
- package/docs/dist/rip.js +3245 -611
- package/docs/dist/rip.min.js +1161 -289
- package/docs/dist/rip.min.js.br +0 -0
- package/docs/extensions/vscode/print/print-1.0.14.vsix +0 -0
- package/docs/extensions/vscode/print/print-latest.vsix +0 -0
- package/docs/extensions/vscode/rip/index.html +2 -1
- package/docs/extensions/vscode/rip/rip-latest.vsix +0 -0
- package/docs/extensions/vscode/rip/vscode-rip-0.6.0.vsix +0 -0
- package/docs/index.html +1 -1
- package/docs/ui/hljs-rip.js +1 -1
- package/package.json +7 -4
- package/src/AGENTS.md +39 -8
- package/src/compiler.js +220 -36
- package/src/components.js +315 -14
- package/src/dts.js +18 -3
- package/src/grammar/README.md +29 -170
- package/src/grammar/grammar.rip +17 -12
- package/src/grammar/solar.rip +4 -17
- package/src/lexer.js +24 -17
- package/src/parser.js +229 -229
- package/src/schema/dts.js +328 -54
- package/src/schema/loader-server.js +2 -1
- package/src/schema/runtime-browser-stubs.js +20 -9
- package/src/schema/runtime-ddl.js +161 -44
- package/src/schema/runtime-migrate.js +681 -0
- package/src/schema/runtime-orm.js +698 -54
- package/src/schema/runtime-validate.js +808 -24
- package/src/schema/runtime.generated.js +2395 -135
- package/src/schema/schema.js +1049 -89
- package/src/typecheck.js +283 -55
- package/src/types.js +5 -1
- package/src/grammar/lunar.rip +0 -2412
|
@@ -19,21 +19,53 @@ const __SCHEMA_SQL_TYPES = {
|
|
|
19
19
|
url: 'VARCHAR', uuid: 'UUID', phone: 'VARCHAR', zip: 'VARCHAR', json: 'JSON', any: 'JSON',
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
// ---- Canonical table spec ----------------------------------------------------
|
|
23
|
+
//
|
|
24
|
+
// The DDL emitter's internal model, exposed (roadmap §2.2). One structure
|
|
25
|
+
// serves both directions: `_tableSpec()` builds it from Layer 2 metadata,
|
|
26
|
+
// `__schemaIntrospect()` (migrate fragment) builds the same shape from the
|
|
27
|
+
// deployed database, and the differ operates on two values of the same type.
|
|
28
|
+
//
|
|
29
|
+
// {
|
|
30
|
+
// name, // table name
|
|
31
|
+
// sequence: { name, start } | null, // auto-id sequence
|
|
32
|
+
// primaryKey, // pk column name
|
|
33
|
+
// columns: [{ name, type, notNull, unique, default, primary?, was? }],
|
|
34
|
+
// indexes: [{ name, columns: [..], unique }],
|
|
35
|
+
// foreignKeys: [{ column, refTable, refColumn }],
|
|
36
|
+
// tableWas, // rename annotation or null
|
|
37
|
+
// }
|
|
38
|
+
//
|
|
39
|
+
// `type` is the RENDER type (VARCHAR(100) keeps its length hint); the
|
|
40
|
+
// differ compares via __schemaTypeKey, which normalizes away parts DuckDB
|
|
41
|
+
// doesn't persist. `default` is the rendered SQL default string or null.
|
|
42
|
+
|
|
43
|
+
function __schemaColumnSpec(name, field) {
|
|
44
|
+
let base = __SCHEMA_SQL_TYPES[field.typeName] || 'VARCHAR';
|
|
45
|
+
if (field.array) base = 'JSON';
|
|
46
|
+
if (base === 'VARCHAR' && field.constraints?.max != null) {
|
|
47
|
+
base = 'VARCHAR(' + field.constraints.max + ')';
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
name: __schemaSnake(name),
|
|
51
|
+
type: base,
|
|
52
|
+
notNull: field.required === true,
|
|
53
|
+
unique: field.unique === true,
|
|
54
|
+
default: field.constraints?.default !== undefined
|
|
55
|
+
? __schemaSQLDefault(field.constraints.default) : null,
|
|
56
|
+
was: field.attrs?.was || null,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
28
59
|
|
|
60
|
+
__SchemaDef.prototype._tableSpec = function (options) {
|
|
61
|
+
this._assertModel('_tableSpec');
|
|
62
|
+
const opts = options || {};
|
|
63
|
+
const norm = this._normalize();
|
|
29
64
|
const table = norm.tableName;
|
|
30
65
|
const seq = table + '_seq';
|
|
31
|
-
if (dropFirst) {
|
|
32
|
-
blocks.push('DROP TABLE IF EXISTS ' + table + ' CASCADE;\nDROP SEQUENCE IF EXISTS ' + seq + ';');
|
|
33
|
-
}
|
|
34
66
|
|
|
35
67
|
// Sequence seed: explicit option wins over @idStart directive wins over 1.
|
|
36
|
-
// DuckDB 1.5.
|
|
68
|
+
// DuckDB 1.5.x does not implement ALTER SEQUENCE ... RESTART WITH N, so the
|
|
37
69
|
// baseline has to be set at creation — hence the knob lives here, not in a
|
|
38
70
|
// post-create migration.
|
|
39
71
|
let idStart = 1;
|
|
@@ -50,61 +82,146 @@ function __schemaToSQL(def, options) {
|
|
|
50
82
|
}
|
|
51
83
|
|
|
52
84
|
const columns = [];
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
85
|
+
columns.push({
|
|
86
|
+
name: norm.primaryKey, type: 'INTEGER',
|
|
87
|
+
notNull: true, unique: false, primary: true,
|
|
88
|
+
default: "nextval('" + seq + "')", was: null,
|
|
89
|
+
});
|
|
56
90
|
for (const [n, f] of norm.fields) {
|
|
57
|
-
columns.push(
|
|
58
|
-
if (f.unique) {
|
|
59
|
-
indexes.push('CREATE UNIQUE INDEX idx_' + table + '_' + __schemaSnake(n) + ' ON ' + table + ' ("' + __schemaSnake(n) + '");');
|
|
60
|
-
}
|
|
91
|
+
columns.push(__schemaColumnSpec(n, f));
|
|
61
92
|
}
|
|
62
93
|
|
|
94
|
+
const foreignKeys = [];
|
|
95
|
+
const notes = [];
|
|
63
96
|
for (const [, rel] of norm.relations) {
|
|
64
97
|
if (rel.kind !== 'belongsTo') continue;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
98
|
+
columns.push({
|
|
99
|
+
name: rel.foreignKey, type: 'INTEGER',
|
|
100
|
+
notNull: !rel.optional, unique: false, default: null, was: null,
|
|
101
|
+
});
|
|
102
|
+
// A relation whose target lives on a DIFFERENT adapter cannot carry
|
|
103
|
+
// a database FK constraint — the referenced table is in another
|
|
104
|
+
// database. The accessor still works (it's just a second query);
|
|
105
|
+
// the DDL suppresses the constraint with a note.
|
|
106
|
+
const targetDef = __SchemaRegistry.get(rel.target);
|
|
107
|
+
const crossAdapter = targetDef &&
|
|
108
|
+
(targetDef._adapter || null) !== (this._adapter || null);
|
|
109
|
+
if (crossAdapter) {
|
|
110
|
+
notes.push('-- NOTE: ' + rel.foreignKey + ' references ' + __schemaTableName(rel.target) +
|
|
111
|
+
'(id) on a different adapter; FK constraint suppressed (cross-database constraints are impossible)');
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
foreignKeys.push({
|
|
115
|
+
column: rel.foreignKey,
|
|
116
|
+
refTable: __schemaTableName(rel.target),
|
|
117
|
+
refColumn: 'id',
|
|
118
|
+
});
|
|
68
119
|
}
|
|
69
120
|
|
|
70
121
|
if (norm.timestamps) {
|
|
71
|
-
columns.push('
|
|
72
|
-
columns.push('
|
|
122
|
+
columns.push({ name: 'created_at', type: 'TIMESTAMP', notNull: false, unique: false, default: 'CURRENT_TIMESTAMP', was: null });
|
|
123
|
+
columns.push({ name: 'updated_at', type: 'TIMESTAMP', notNull: false, unique: false, default: 'CURRENT_TIMESTAMP', was: null });
|
|
73
124
|
}
|
|
74
125
|
if (norm.softDelete) {
|
|
75
|
-
columns.push('
|
|
126
|
+
columns.push({ name: 'deleted_at', type: 'TIMESTAMP', notNull: false, unique: false, default: null, was: null });
|
|
76
127
|
}
|
|
77
128
|
|
|
78
|
-
//
|
|
129
|
+
// Index names are derived from their column set (`idx_<table>_<cols>`),
|
|
130
|
+
// so two declarations on the same columns collide. That's always a
|
|
131
|
+
// redundant/contradictory schema (a `@unique` index already serves as an
|
|
132
|
+
// index for those columns) — reject it loudly rather than emit duplicate
|
|
133
|
+
// CREATE INDEX statements.
|
|
134
|
+
const indexes = [];
|
|
135
|
+
const indexByName = new Map();
|
|
136
|
+
const addIndex = (ix) => {
|
|
137
|
+
if (indexByName.has(ix.name)) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
`Table '${table}': duplicate index '${ix.name}' on (${ix.columns.join(', ')}). ` +
|
|
140
|
+
`Those columns are declared unique/indexed more than once — a '@unique' already ` +
|
|
141
|
+
`creates an index, so remove the redundant '@unique'/'@index' declaration.`);
|
|
142
|
+
}
|
|
143
|
+
indexByName.set(ix.name, ix);
|
|
144
|
+
indexes.push(ix);
|
|
145
|
+
};
|
|
146
|
+
for (const [n, f] of norm.fields) {
|
|
147
|
+
if (!f.unique) continue;
|
|
148
|
+
const col = __schemaSnake(n);
|
|
149
|
+
addIndex({ name: 'idx_' + table + '_' + col, columns: [col], unique: true });
|
|
150
|
+
}
|
|
79
151
|
for (const d of norm.directives) {
|
|
80
|
-
if (d.name !== 'index') continue;
|
|
152
|
+
if (d.name !== 'index' && d.name !== 'unique') continue;
|
|
81
153
|
const ixArgs = d.args?.[0] || {};
|
|
82
|
-
const
|
|
83
|
-
if (!
|
|
84
|
-
|
|
85
|
-
indexes.push('CREATE ' + u + 'INDEX idx_' + table + '_' + fields.join('_') + ' ON ' + table + ' (' + fields.map(f => '"' + f + '"').join(', ') + ');');
|
|
154
|
+
const cols = (ixArgs.fields || []).map(__schemaSnake);
|
|
155
|
+
if (!cols.length) continue;
|
|
156
|
+
addIndex({ name: 'idx_' + table + '_' + cols.join('_'), columns: cols, unique: d.name === 'unique' });
|
|
86
157
|
}
|
|
87
158
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
159
|
+
return {
|
|
160
|
+
name: table,
|
|
161
|
+
sequence: { name: seq, start: idStart },
|
|
162
|
+
primaryKey: norm.primaryKey,
|
|
163
|
+
columns, indexes, foreignKeys, notes,
|
|
164
|
+
tableWas: norm.tableWas || null,
|
|
165
|
+
};
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// ---- DDL rendering ------------------------------------------------------------
|
|
91
169
|
|
|
92
|
-
|
|
170
|
+
// Render one column line for CREATE TABLE — also used by the differ's
|
|
171
|
+
// ADD COLUMN steps (minus the parts DuckDB can't ALTER in).
|
|
172
|
+
function __schemaRenderColumn(spec, col, fkByColumn) {
|
|
173
|
+
const parts = [' ' + col.name + ' ' + col.type];
|
|
174
|
+
if (col.primary) {
|
|
175
|
+
parts[0] = ' ' + col.name + ' ' + col.type + ' PRIMARY KEY';
|
|
176
|
+
} else {
|
|
177
|
+
if (col.notNull) parts.push('NOT NULL');
|
|
178
|
+
// Uniqueness is emitted as a single named index (`idx_<table>_<col>`),
|
|
179
|
+
// never as an inline column `UNIQUE`. Inline UNIQUE created a second,
|
|
180
|
+
// auto-named index the migrate differ's fold (__schemaFoldSpec) can't
|
|
181
|
+
// normalize; the named index is what ADD COLUMN and introspection
|
|
182
|
+
// already round-trip through. `col.unique` stays the canonical spec
|
|
183
|
+
// flag — it drives the index below and the differ — it just no longer
|
|
184
|
+
// renders here.
|
|
185
|
+
}
|
|
186
|
+
const fk = fkByColumn ? fkByColumn.get(col.name) : null;
|
|
187
|
+
if (fk) parts.push('REFERENCES ' + fk.refTable + '(' + fk.refColumn + ')');
|
|
188
|
+
if (col.default != null) parts.push('DEFAULT ' + col.default);
|
|
189
|
+
return parts.join(' ');
|
|
93
190
|
}
|
|
94
191
|
|
|
95
|
-
function
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
192
|
+
function __schemaRenderIndex(spec, ix) {
|
|
193
|
+
const u = ix.unique ? 'UNIQUE ' : '';
|
|
194
|
+
return 'CREATE ' + u + 'INDEX ' + ix.name + ' ON ' + spec.name +
|
|
195
|
+
' (' + ix.columns.map(c => '"' + c + '"').join(', ') + ');';
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Render the CREATE SEQUENCE / CREATE TABLE / CREATE INDEX blocks for a
|
|
199
|
+
// table spec. toSQL() joins these; the differ's ADD TABLE step reuses them.
|
|
200
|
+
function __schemaRenderCreate(spec) {
|
|
201
|
+
const blocks = [];
|
|
202
|
+
const fkByColumn = new Map(spec.foreignKeys.map(fk => [fk.column, fk]));
|
|
203
|
+
if (spec.sequence) {
|
|
204
|
+
blocks.push('CREATE SEQUENCE ' + spec.sequence.name + ' START ' + spec.sequence.start + ';');
|
|
100
205
|
}
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if (
|
|
105
|
-
|
|
206
|
+
const lines = spec.columns.map(c => __schemaRenderColumn(spec, c, fkByColumn));
|
|
207
|
+
blocks.push('CREATE TABLE ' + spec.name + ' (\n' + lines.join(',\n') + '\n);');
|
|
208
|
+
const ix = spec.indexes.map(i => __schemaRenderIndex(spec, i));
|
|
209
|
+
if (ix.length) blocks.push(ix.join('\n'));
|
|
210
|
+
if (spec.notes && spec.notes.length) blocks.push(spec.notes.join('\n'));
|
|
211
|
+
return blocks;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function __schemaToSQL(def, options) {
|
|
215
|
+
const opts = options || {};
|
|
216
|
+
const { dropFirst = false, header } = opts;
|
|
217
|
+
const spec = def._tableSpec(opts);
|
|
218
|
+
const blocks = [];
|
|
219
|
+
if (header) blocks.push(header);
|
|
220
|
+
if (dropFirst) {
|
|
221
|
+
blocks.push('DROP TABLE IF EXISTS ' + spec.name + ' CASCADE;\nDROP SEQUENCE IF EXISTS ' + spec.sequence.name + ';');
|
|
106
222
|
}
|
|
107
|
-
|
|
223
|
+
blocks.push(...__schemaRenderCreate(spec));
|
|
224
|
+
return blocks.join('\n\n') + '\n';
|
|
108
225
|
}
|
|
109
226
|
|
|
110
227
|
function __schemaSQLDefault(v) {
|