rip-lang 3.14.0 → 3.14.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/src/schema.js CHANGED
@@ -1432,6 +1432,11 @@ function serializeLiteral(v) {
1432
1432
  // its own arg shape — we centralize the parsing here so Layer 2 can rely
1433
1433
  // on normalized structures.
1434
1434
  function compileDirectiveArgsLiteral(name, tokens) {
1435
+ // @idStart requires its arg, so validate before the generic empty-bail.
1436
+ if (name === 'idStart' && !tokens.length) {
1437
+ throw schemaError(null,
1438
+ '@idStart requires an integer literal, e.g. @idStart 10001.');
1439
+ }
1435
1440
  if (!tokens.length) return null;
1436
1441
 
1437
1442
  // Relation directives: `@belongs_to Org`, `@belongs_to Org?`,
@@ -1489,6 +1494,29 @@ function compileDirectiveArgsLiteral(name, tokens) {
1489
1494
  return `[{${parts.join(', ')}}]`;
1490
1495
  }
1491
1496
 
1497
+ // @idStart N sets the seed value for the table's auto-id sequence.
1498
+ // Accepts a single integer literal (optionally negative). Consumed by
1499
+ // .toSQL(); models that never call .toSQL() simply ignore it.
1500
+ if (name === 'idStart') {
1501
+ let tok = tokens[0];
1502
+ let sign = 1;
1503
+ let numTok = tok;
1504
+ if (tok && tok[0] === '-' && tokens[1] && tokens[1][0] === 'NUMBER') {
1505
+ sign = -1;
1506
+ numTok = tokens[1];
1507
+ }
1508
+ if (!numTok || numTok[0] !== 'NUMBER') {
1509
+ throw schemaError(tok || tokens[tokens.length - 1],
1510
+ '@idStart requires an integer literal, e.g. @idStart 10001.');
1511
+ }
1512
+ let n = sign * Number(numTok[1]);
1513
+ if (!Number.isInteger(n)) {
1514
+ throw schemaError(numTok,
1515
+ '@idStart requires an integer literal; got ' + numTok[1] + '.');
1516
+ }
1517
+ return '[{value: ' + n + '}]';
1518
+ }
1519
+
1492
1520
  // Bare flag-like directives (@timestamps, @softDelete) don't take args.
1493
1521
  // Anything else — capture as raw literal tokens conservatively.
1494
1522
  return null;
@@ -1731,6 +1759,14 @@ function schemaError(tok, message) {
1731
1759
  // :mixin — non-instantiable; raises `Cannot parse :mixin`
1732
1760
  // :model — Phase 4 (the class additionally wires ORM methods)
1733
1761
 
1762
+ // Schema runtime ABI version. Bump when the shape of a __schema({...})
1763
+ // descriptor or any cross-bundle-visible runtime surface changes
1764
+ // incompatibly. Two bundles that disagree on this number can't share
1765
+ // one runtime, so a mismatch at load time throws rather than silently
1766
+ // fragmenting. Tracks runtime contract — not the rip-lang product
1767
+ // semver.
1768
+ const SCHEMA_RUNTIME_ABI_VERSION = 1;
1769
+
1734
1770
  const SCHEMA_RUNTIME = `
1735
1771
  // ---- Rip Schema Runtime ----------------------------------------------------
1736
1772
  // Four layers, lazy compilation:
@@ -1740,6 +1776,26 @@ const SCHEMA_RUNTIME = `
1740
1776
  // 3 (validator) compiled validator plan. Built on first .parse.
1741
1777
  // 4a (ORM plan) built on first .find/.create/.save.
1742
1778
  // 4b (DDL plan) built on first .toSQL(). Independent of 4a.
1779
+ //
1780
+ // Instance-singleton model:
1781
+ // The runtime installs itself on globalThis.__ripSchema the first time a
1782
+ // compiled bundle executes. Subsequent bundles that inject the same runtime
1783
+ // template detect the existing installation and bind to it instead of
1784
+ // re-running the body — giving every bundle a single shared registry,
1785
+ // adapter, and class identity. The IIFE wrapper below enforces that.
1786
+
1787
+ var { __schema, SchemaError, __SchemaRegistry, __schemaSetAdapter } = (function() {
1788
+ if (typeof globalThis !== 'undefined' && globalThis.__ripSchema) {
1789
+ if (globalThis.__ripSchema.__version !== ${SCHEMA_RUNTIME_ABI_VERSION}) {
1790
+ throw new Error(
1791
+ "rip-schema runtime version mismatch: loaded runtime is v" +
1792
+ globalThis.__ripSchema.__version +
1793
+ ", but this bundle expects v" + ${SCHEMA_RUNTIME_ABI_VERSION} +
1794
+ ". Two compiled Rip bundles with incompatible schema runtimes are loaded in the same process."
1795
+ );
1796
+ }
1797
+ return globalThis.__ripSchema;
1798
+ }
1743
1799
 
1744
1800
  class SchemaError extends Error {
1745
1801
  constructor(issues, schemaName, schemaKind) {
@@ -2051,13 +2107,12 @@ class __SchemaDef {
2051
2107
  hooks.set(e.name, e.fn);
2052
2108
  break;
2053
2109
  case 'directive': {
2054
- if (e.name === 'mixin') {
2055
- // Deferred to the post-pass so we can dedupe diamond includes
2056
- // and detect cycles with a full expansion stack.
2057
- directives.push({ name: e.name, args: e.args || [] });
2058
- break;
2059
- }
2060
2110
  directives.push({ name: e.name, args: e.args || [] });
2111
+ // @mixin is recorded but further handling is deferred to the
2112
+ // post-pass so we can dedupe diamond includes and detect
2113
+ // cycles with a full expansion stack. All other directives
2114
+ // get their relation / timestamps / softDelete processing now.
2115
+ if (e.name === 'mixin') break;
2061
2116
  if (e.name === 'timestamps') timestamps = true;
2062
2117
  if (e.name === 'softDelete') softDelete = true;
2063
2118
  const rel = __schemaNormalizeDirectiveRelation(e, this.name);
@@ -2232,11 +2287,20 @@ class __SchemaDef {
2232
2287
  enumerable: false, configurable: true, writable: true,
2233
2288
  value: function() { return def._validateFields(this, true); },
2234
2289
  });
2290
+ // toJSON mirrors the instance's own enumerable properties, which by
2291
+ // construction are: the primary key, declared fields, @timestamps
2292
+ // columns, @softDelete timestamp, @belongs_to FK columns, and any
2293
+ // !> eager-derived fields. Internal state (_dirty, _persisted,
2294
+ // _snapshot) is defined non-enumerable; methods and ~> computed
2295
+ // getters live on the prototype. So iterating own keys picks up
2296
+ // exactly the user-facing wire shape without special-casing each
2297
+ // category — and stays correct when new implicit columns get added
2298
+ // to the runtime.
2235
2299
  Object.defineProperty(klass.prototype, 'toJSON', {
2236
2300
  enumerable: false, configurable: true, writable: true,
2237
2301
  value: function() {
2238
2302
  const out = {};
2239
- for (const k of norm.fields.keys()) out[k] = this[k];
2303
+ for (const k of Object.keys(this)) out[k] = this[k];
2240
2304
  return out;
2241
2305
  },
2242
2306
  });
@@ -2818,6 +2882,13 @@ async function __schemaSave(def, inst) {
2818
2882
  }
2819
2883
  }
2820
2884
  }
2885
+ // Now that the RETURNING columns (id, @timestamps, FKs) are on the
2886
+ // instance, !> eager-derived fields can see them. Mirrors the hydrate
2887
+ // path, which runs _applyEagerDerived once all declared fields are
2888
+ // populated. Per-docs semantics ("materialize once, not reactive")
2889
+ // still hold — we're firing once, at end of construction, not on
2890
+ // subsequent mutations.
2891
+ def._applyEagerDerived(inst);
2821
2892
  inst._persisted = true;
2822
2893
  } else {
2823
2894
  const sets = [], values = [];
@@ -2874,7 +2945,8 @@ const __SCHEMA_SQL_TYPES = {
2874
2945
  };
2875
2946
 
2876
2947
  function __schemaToSQL(def, options) {
2877
- const { dropFirst = false, header } = options || {};
2948
+ const opts = options || {};
2949
+ const { dropFirst = false, header } = opts;
2878
2950
  const norm = def._normalize();
2879
2951
  const blocks = [];
2880
2952
  if (header) blocks.push(header);
@@ -2885,6 +2957,23 @@ function __schemaToSQL(def, options) {
2885
2957
  blocks.push('DROP TABLE IF EXISTS ' + table + ' CASCADE;\\nDROP SEQUENCE IF EXISTS ' + seq + ';');
2886
2958
  }
2887
2959
 
2960
+ // Sequence seed: explicit option wins over @idStart directive wins over 1.
2961
+ // DuckDB 1.5.2 does not implement ALTER SEQUENCE ... RESTART WITH N, so the
2962
+ // baseline has to be set at creation — hence the knob lives here, not in a
2963
+ // post-create migration.
2964
+ let idStart = 1;
2965
+ for (const d of norm.directives) {
2966
+ if (d.name === 'idStart' && d.args?.[0] && Number.isInteger(d.args[0].value)) {
2967
+ idStart = d.args[0].value;
2968
+ }
2969
+ }
2970
+ if (opts.idStart !== undefined) {
2971
+ if (!Number.isInteger(opts.idStart)) {
2972
+ throw new Error('schema.toSQL(): idStart must be an integer; got ' + String(opts.idStart));
2973
+ }
2974
+ idStart = opts.idStart;
2975
+ }
2976
+
2888
2977
  const columns = [];
2889
2978
  const indexes = [];
2890
2979
  columns.push(' ' + norm.primaryKey + " INTEGER PRIMARY KEY DEFAULT nextval('" + seq + "')");
@@ -2921,7 +3010,7 @@ function __schemaToSQL(def, options) {
2921
3010
  indexes.push('CREATE ' + u + 'INDEX idx_' + table + '_' + fields.join('_') + ' ON ' + table + ' (' + fields.map(f => '"' + f + '"').join(', ') + ');');
2922
3011
  }
2923
3012
 
2924
- blocks.push('CREATE SEQUENCE ' + seq + ' START 1;');
3013
+ blocks.push('CREATE SEQUENCE ' + seq + ' START ' + idStart + ';');
2925
3014
  blocks.push('CREATE TABLE ' + table + ' (\\n' + columns.join(',\\n') + '\\n);');
2926
3015
  if (indexes.length) blocks.push(indexes.join('\\n'));
2927
3016
 
@@ -2963,11 +3052,13 @@ function __schema(descriptor) {
2963
3052
  return def;
2964
3053
  }
2965
3054
 
2966
- if (typeof globalThis !== 'undefined') {
2967
- globalThis.__ripSchema = {
3055
+ const exports = {
2968
3056
  __schema, SchemaError, __SchemaRegistry, __schemaSetAdapter,
3057
+ __version: ${SCHEMA_RUNTIME_ABI_VERSION},
2969
3058
  };
2970
- }
3059
+ if (typeof globalThis !== 'undefined') globalThis.__ripSchema = exports;
3060
+ return exports;
3061
+ })();
2971
3062
 
2972
3063
  // === End Schema Runtime ===
2973
3064
  `;
@@ -3042,7 +3133,7 @@ export const SCHEMA_INTRINSIC_DECLS = [
3042
3133
  ' first(): Promise<Instance | null>;',
3043
3134
  ' count(cond?: Record<string, unknown>): Promise<number>;',
3044
3135
  ' create(data: Partial<Data>): Promise<Instance>;',
3045
- ' toSQL(options?: { dropFirst?: boolean; header?: string }): string;',
3136
+ ' toSQL(options?: { dropFirst?: boolean; header?: string; idStart?: number }): string;',
3046
3137
  '}',
3047
3138
  ];
3048
3139