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.
Files changed (40) hide show
  1. package/README.md +2 -3
  2. package/bin/rip +39 -8
  3. package/bin/rip-schema +175 -0
  4. package/docs/RIP-APP.md +91 -2
  5. package/docs/RIP-DUCKDB.md +64 -1
  6. package/docs/RIP-INTRO.md +4 -4
  7. package/docs/RIP-LANG.md +32 -33
  8. package/docs/RIP-SCHEMA.md +1204 -364
  9. package/docs/dist/rip.js +3245 -611
  10. package/docs/dist/rip.min.js +1161 -289
  11. package/docs/dist/rip.min.js.br +0 -0
  12. package/docs/extensions/vscode/print/print-1.0.14.vsix +0 -0
  13. package/docs/extensions/vscode/print/print-latest.vsix +0 -0
  14. package/docs/extensions/vscode/rip/index.html +2 -1
  15. package/docs/extensions/vscode/rip/rip-latest.vsix +0 -0
  16. package/docs/extensions/vscode/rip/vscode-rip-0.6.0.vsix +0 -0
  17. package/docs/index.html +1 -1
  18. package/docs/ui/hljs-rip.js +1 -1
  19. package/package.json +7 -4
  20. package/src/AGENTS.md +39 -8
  21. package/src/compiler.js +220 -36
  22. package/src/components.js +315 -14
  23. package/src/dts.js +18 -3
  24. package/src/grammar/README.md +29 -170
  25. package/src/grammar/grammar.rip +17 -12
  26. package/src/grammar/solar.rip +4 -17
  27. package/src/lexer.js +24 -17
  28. package/src/parser.js +229 -229
  29. package/src/schema/dts.js +328 -54
  30. package/src/schema/loader-server.js +2 -1
  31. package/src/schema/runtime-browser-stubs.js +20 -9
  32. package/src/schema/runtime-ddl.js +161 -44
  33. package/src/schema/runtime-migrate.js +681 -0
  34. package/src/schema/runtime-orm.js +698 -54
  35. package/src/schema/runtime-validate.js +808 -24
  36. package/src/schema/runtime.generated.js +2395 -135
  37. package/src/schema/schema.js +1049 -89
  38. package/src/typecheck.js +283 -55
  39. package/src/types.js +5 -1
  40. 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
- function __schemaToSQL(def, options) {
23
- const opts = options || {};
24
- const { dropFirst = false, header } = opts;
25
- const norm = def._normalize();
26
- const blocks = [];
27
- if (header) blocks.push(header);
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.2 does not implement ALTER SEQUENCE ... RESTART WITH N, so the
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
- const indexes = [];
54
- columns.push(' ' + norm.primaryKey + " INTEGER PRIMARY KEY DEFAULT nextval('" + seq + "')");
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(__schemaColumnDDL(n, f));
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
- const refTable = __schemaTableName(rel.target);
66
- const notNull = rel.optional ? '' : ' NOT NULL';
67
- columns.push(' ' + rel.foreignKey + ' INTEGER' + notNull + ' REFERENCES ' + refTable + '(id)');
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(' created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP');
72
- columns.push(' updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP');
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(' deleted_at TIMESTAMP');
126
+ columns.push({ name: 'deleted_at', type: 'TIMESTAMP', notNull: false, unique: false, default: null, was: null });
76
127
  }
77
128
 
78
- // @index directives
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 fields = (ixArgs.fields || []).map(__schemaSnake);
83
- if (!fields.length) continue;
84
- const u = ixArgs.unique ? 'UNIQUE ' : '';
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
- blocks.push('CREATE SEQUENCE ' + seq + ' START ' + idStart + ';');
89
- blocks.push('CREATE TABLE ' + table + ' (\n' + columns.join(',\n') + '\n);');
90
- if (indexes.length) blocks.push(indexes.join('\n'));
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
- return blocks.join('\n\n') + '\n';
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 __schemaColumnDDL(name, field) {
96
- let base = __SCHEMA_SQL_TYPES[field.typeName] || 'VARCHAR';
97
- if (field.array) base = 'JSON';
98
- if (base === 'VARCHAR' && field.constraints?.max != null) {
99
- base = 'VARCHAR(' + field.constraints.max + ')';
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 parts = [' ' + __schemaSnake(name) + ' ' + base];
102
- if (field.required) parts.push('NOT NULL');
103
- if (field.unique) parts.push('UNIQUE');
104
- if (field.constraints?.default !== undefined) {
105
- parts.push('DEFAULT ' + __schemaSQLDefault(field.constraints.default));
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
- return parts.join(' ');
223
+ blocks.push(...__schemaRenderCreate(spec));
224
+ return blocks.join('\n\n') + '\n';
108
225
  }
109
226
 
110
227
  function __schemaSQLDefault(v) {