latticesql 0.16.2 → 0.17.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 CHANGED
@@ -35,6 +35,7 @@ Lattice has no opinions about your schema, your agents, or your file format. You
35
35
  - [defineEntityContext()](#defineentitycontext-v05)
36
36
  - [defineWriteback()](#definewriteback)
37
37
  - [init() / close()](#init--close)
38
+ - [migrate()](#migrate-v017)
38
39
  - [CRUD operations](#crud-operations)
39
40
  - [Query operators](#query-operators)
40
41
  - [Render, sync, watch, and reconcile](#render-sync-watch-and-reconcile)
@@ -202,11 +203,12 @@ interface TableDefinition {
202
203
  * - A render function: (rows: Row[]) => string
203
204
  * - A built-in template name: 'default-list' | 'default-table' | 'default-detail' | 'default-json'
204
205
  * - A template spec with hooks: { template: BuiltinTemplateName, hooks?: RenderHooks }
206
+ * Optional (v0.17+) — omit render and outputFile for schema-only tables.
205
207
  */
206
- render: RenderSpec;
208
+ render?: RenderSpec;
207
209
 
208
- /** Output file path, relative to the outputDir passed to render()/watch() */
209
- outputFile: string;
210
+ /** Output file path, relative to the outputDir passed to render()/watch(). Optional (v0.17+). */
211
+ outputFile?: string;
210
212
 
211
213
  /** Optional row filter applied before rendering */
212
214
  filter?: (rows: Row[]) => Row[];
@@ -215,10 +217,12 @@ interface TableDefinition {
215
217
  * Primary key column name or [col1, col2] for composite PKs.
216
218
  * Defaults to 'id'. When 'id' is the PK and the field is absent on insert,
217
219
  * a UUID v4 is generated automatically.
220
+ * Composite PKs (v0.17+): auto-generates a PRIMARY KEY(...) constraint —
221
+ * no need to add it manually via tableConstraints.
218
222
  */
219
223
  primaryKey?: string | string[];
220
224
 
221
- /** Additional SQL constraints (required for composite PKs) */
225
+ /** Additional SQL constraints (e.g., UNIQUE, CHECK). No longer required for composite PKs (v0.17+). */
222
226
  tableConstraints?: string[];
223
227
 
224
228
  /** Declared relationships used by template rendering */
@@ -267,6 +271,22 @@ await db.update('pages', 'about-us', { title: 'About' });
267
271
  await db.delete('pages', 'about-us');
268
272
  ```
269
273
 
274
+ **Schema-only table (v0.17+):**
275
+
276
+ Tables without `render` and `outputFile` get full schema support (columns, indexes, constraints, CRUD) but produce no output files during `render()` or `watch()`. Useful for junction tables, internal tracking tables, or any table that doesn't need a context file.
277
+
278
+ ```typescript
279
+ db.define('agent_skills', {
280
+ columns: {
281
+ agent_id: 'TEXT NOT NULL',
282
+ skill_id: 'TEXT NOT NULL',
283
+ proficiency: 'TEXT DEFAULT "basic"',
284
+ },
285
+ primaryKey: ['agent_id', 'skill_id'],
286
+ // No render, no outputFile — schema-only
287
+ });
288
+ ```
289
+
270
290
  **Composite primary key:**
271
291
 
272
292
  ```typescript
@@ -276,7 +296,6 @@ db.define('event_seats', {
276
296
  seat_no: 'INTEGER NOT NULL',
277
297
  holder: 'TEXT',
278
298
  },
279
- tableConstraints: ['PRIMARY KEY (event_id, seat_no)'],
280
299
  primaryKey: ['event_id', 'seat_no'],
281
300
  render: 'default-table',
282
301
  outputFile: 'seats.md',
@@ -835,6 +854,26 @@ Migrations are idempotent — each `version` number is applied exactly once, tra
835
854
 
836
855
  `close()` closes the SQLite connection. Call it when the process shuts down.
837
856
 
857
+ ### `migrate()` (v0.17+)
858
+
859
+ ```typescript
860
+ await db.migrate(migrations: Migration[]): Promise<void>
861
+ ```
862
+
863
+ Run migrations after `init()`. Works exactly like `init({ migrations })` but callable any time — useful when migrations are loaded dynamically or added by plugins after startup.
864
+
865
+ ```typescript
866
+ await db.init();
867
+
868
+ // Later — e.g., after loading a plugin that needs new columns
869
+ await db.migrate([
870
+ { version: 'plugin-v1', sql: 'ALTER TABLE tasks ADD COLUMN tags TEXT' },
871
+ { version: 'plugin-v2', sql: 'CREATE INDEX IF NOT EXISTS idx_tasks_tags ON tasks (tags)' },
872
+ ]);
873
+ ```
874
+
875
+ `Migration.version` accepts `number | string` — use numbers for sequential migrations, or strings for named/namespaced versions (e.g., `'plugin-v1'`). Each version is applied at most once, tracked in the same `__lattice_migrations` table used by `init()`.
876
+
838
877
  ---
839
878
 
840
879
  ### CRUD operations
@@ -860,6 +899,24 @@ await db.insert('pages', { slug: 'about', title: 'About Us' });
860
899
  await db.insert('tasks', { id: 'task-001', title: 'Specific task' });
861
900
  ```
862
901
 
902
+ #### `insertReturning()` (v0.17+)
903
+
904
+ ```typescript
905
+ await db.insertReturning(table: string, row: Row): Promise<Row>
906
+ ```
907
+
908
+ Insert a row and get the full row back — including the auto-generated `id`, defaults, and any other columns. Equivalent to `insert()` + `get()` in a single call.
909
+
910
+ ```typescript
911
+ const task = await db.insertReturning('tasks', { title: 'Write docs', status: 'open' });
912
+ // task → { id: 'f47ac10b-...', title: 'Write docs', status: 'open', priority: 0, ... }
913
+
914
+ // Useful when you need the generated id or default values immediately
915
+ const agent = await db.insertReturning('agents', { name: 'Gamma' });
916
+ console.log(agent.id); // auto-generated UUID
917
+ console.log(agent.active); // default value from schema
918
+ ```
919
+
863
920
  #### `upsert()`
864
921
 
865
922
  ```typescript
@@ -899,6 +956,23 @@ await db.update('tasks', 'task-001', { status: 'done' });
899
956
  await db.update('event_seats', { event_id: 'e-1', seat_no: 3 }, { holder: 'Bob' });
900
957
  ```
901
958
 
959
+ #### `updateReturning()` (v0.17+)
960
+
961
+ ```typescript
962
+ await db.updateReturning(table: string, id: PkLookup, row: Partial<Row>): Promise<Row>
963
+ ```
964
+
965
+ Update specific columns and get the full updated row back. Equivalent to `update()` + `get()` in a single call.
966
+
967
+ ```typescript
968
+ const task = await db.updateReturning('tasks', 'task-001', { status: 'done' });
969
+ // task → { id: 'task-001', title: 'Write docs', status: 'done', priority: 3, ... }
970
+
971
+ // Composite PK
972
+ const seat = await db.updateReturning('event_seats', { event_id: 'e-1', seat_no: 3 }, { holder: 'Bob' });
973
+ // seat → { event_id: 'e-1', seat_no: 3, holder: 'Bob' }
974
+ ```
975
+
902
976
  #### `delete()`
903
977
 
904
978
  ```typescript
package/dist/cli.js CHANGED
@@ -492,24 +492,37 @@ var SchemaManager = class {
492
492
  */
493
493
  applySchema(adapter) {
494
494
  for (const [name, def] of this._tables) {
495
- this._ensureTable(adapter, name, def.columns, def.tableConstraints);
495
+ const pkCols = this._tablePK.get(name) ?? ["id"];
496
+ let constraints = def.tableConstraints ? [...def.tableConstraints] : [];
497
+ if (pkCols.length > 1) {
498
+ const alreadyHasPK = constraints.some((c) => c.toUpperCase().startsWith("PRIMARY KEY"));
499
+ if (!alreadyHasPK) {
500
+ constraints.unshift(`PRIMARY KEY (${pkCols.map((c) => `"${c}"`).join(", ")})`);
501
+ }
502
+ }
503
+ this._ensureTable(adapter, name, def.columns, constraints.length ? constraints : void 0);
496
504
  }
497
505
  this._ensureTable(adapter, "__lattice_migrations", {
498
- version: "INTEGER PRIMARY KEY",
506
+ version: "TEXT PRIMARY KEY",
499
507
  applied_at: "TEXT NOT NULL"
500
508
  });
501
509
  }
502
510
  /** Run explicit versioned migrations in order, idempotently */
503
511
  applyMigrations(adapter, migrations) {
504
- const sorted = [...migrations].sort((a, b) => a.version - b.version);
512
+ const sorted = [...migrations].sort((a, b) => {
513
+ const va = String(a.version);
514
+ const vb = String(b.version);
515
+ return va.localeCompare(vb, void 0, { numeric: true });
516
+ });
505
517
  for (const m of sorted) {
518
+ const versionStr = String(m.version);
506
519
  const exists = adapter.get("SELECT 1 FROM __lattice_migrations WHERE version = ?", [
507
- m.version
520
+ versionStr
508
521
  ]);
509
522
  if (!exists) {
510
523
  adapter.run(m.sql);
511
524
  adapter.run("INSERT INTO __lattice_migrations (version, applied_at) VALUES (?, ?)", [
512
- m.version,
525
+ versionStr,
513
526
  (/* @__PURE__ */ new Date()).toISOString()
514
527
  ]);
515
528
  }
@@ -1612,7 +1625,8 @@ var Lattice = class {
1612
1625
  this._assertNotInit("define");
1613
1626
  const compiledDef = {
1614
1627
  ...def,
1615
- render: compileRender(def, table, this._schema, this._adapter)
1628
+ render: def.render ? compileRender(def, table, this._schema, this._adapter) : () => "",
1629
+ outputFile: def.outputFile ?? `.schema-only/${table}.md`
1616
1630
  };
1617
1631
  this._schema.define(table, compiledDef);
1618
1632
  return this;
@@ -1654,6 +1668,23 @@ var Lattice = class {
1654
1668
  this._initialized = true;
1655
1669
  return Promise.resolve();
1656
1670
  }
1671
+ /**
1672
+ * Run additional migrations after init(). Useful for package-level schema
1673
+ * changes applied at runtime (e.g. update hooks that add columns).
1674
+ *
1675
+ * @since 0.17.0
1676
+ */
1677
+ migrate(migrations) {
1678
+ if (!this._initialized) {
1679
+ return Promise.reject(new Error("Lattice: not initialized \u2014 call init() first"));
1680
+ }
1681
+ this._schema.applyMigrations(this._adapter, migrations);
1682
+ for (const tableName of this._schema.getTables().keys()) {
1683
+ const rows = this._adapter.all(`PRAGMA table_info("${tableName}")`);
1684
+ this._columnCache.set(tableName, new Set(rows.map((r) => r.name)));
1685
+ }
1686
+ return Promise.resolve();
1687
+ }
1657
1688
  close() {
1658
1689
  this._adapter.close();
1659
1690
  this._columnCache.clear();
@@ -1686,6 +1717,17 @@ var Lattice = class {
1686
1717
  this._fireWriteHooks(table, "insert", rowWithPk, pkValue);
1687
1718
  return Promise.resolve(pkValue);
1688
1719
  }
1720
+ /**
1721
+ * Insert a row and return the full inserted row (including auto-generated
1722
+ * fields and defaults). Equivalent to `insert()` followed by `get()`.
1723
+ *
1724
+ * @since 0.17.0
1725
+ */
1726
+ insertReturning(table, row) {
1727
+ return this.insert(table, row).then(
1728
+ (pk) => this.get(table, pk).then((result) => result ?? { ...row, id: pk })
1729
+ );
1730
+ }
1689
1731
  upsert(table, row) {
1690
1732
  const notInit = this._notInitError();
1691
1733
  if (notInit) return notInit;
@@ -1740,6 +1782,17 @@ var Lattice = class {
1740
1782
  this._fireWriteHooks(table, "update", sanitized, auditId, Object.keys(sanitized));
1741
1783
  return Promise.resolve();
1742
1784
  }
1785
+ /**
1786
+ * Update a row and return the full updated row. Equivalent to `update()`
1787
+ * followed by `get()`.
1788
+ *
1789
+ * @since 0.17.0
1790
+ */
1791
+ updateReturning(table, id, row) {
1792
+ return this.update(table, id, row).then(
1793
+ () => this.get(table, id).then((result) => result ?? row)
1794
+ );
1795
+ }
1743
1796
  delete(table, id) {
1744
1797
  const notInit = this._notInitError();
1745
1798
  if (notInit) return notInit;
package/dist/index.cjs CHANGED
@@ -240,24 +240,37 @@ var SchemaManager = class {
240
240
  */
241
241
  applySchema(adapter) {
242
242
  for (const [name, def] of this._tables) {
243
- this._ensureTable(adapter, name, def.columns, def.tableConstraints);
243
+ const pkCols = this._tablePK.get(name) ?? ["id"];
244
+ let constraints = def.tableConstraints ? [...def.tableConstraints] : [];
245
+ if (pkCols.length > 1) {
246
+ const alreadyHasPK = constraints.some((c) => c.toUpperCase().startsWith("PRIMARY KEY"));
247
+ if (!alreadyHasPK) {
248
+ constraints.unshift(`PRIMARY KEY (${pkCols.map((c) => `"${c}"`).join(", ")})`);
249
+ }
250
+ }
251
+ this._ensureTable(adapter, name, def.columns, constraints.length ? constraints : void 0);
244
252
  }
245
253
  this._ensureTable(adapter, "__lattice_migrations", {
246
- version: "INTEGER PRIMARY KEY",
254
+ version: "TEXT PRIMARY KEY",
247
255
  applied_at: "TEXT NOT NULL"
248
256
  });
249
257
  }
250
258
  /** Run explicit versioned migrations in order, idempotently */
251
259
  applyMigrations(adapter, migrations) {
252
- const sorted = [...migrations].sort((a, b) => a.version - b.version);
260
+ const sorted = [...migrations].sort((a, b) => {
261
+ const va = String(a.version);
262
+ const vb = String(b.version);
263
+ return va.localeCompare(vb, void 0, { numeric: true });
264
+ });
253
265
  for (const m of sorted) {
266
+ const versionStr = String(m.version);
254
267
  const exists = adapter.get("SELECT 1 FROM __lattice_migrations WHERE version = ?", [
255
- m.version
268
+ versionStr
256
269
  ]);
257
270
  if (!exists) {
258
271
  adapter.run(m.sql);
259
272
  adapter.run("INSERT INTO __lattice_migrations (version, applied_at) VALUES (?, ?)", [
260
- m.version,
273
+ versionStr,
261
274
  (/* @__PURE__ */ new Date()).toISOString()
262
275
  ]);
263
276
  }
@@ -1648,7 +1661,8 @@ var Lattice = class {
1648
1661
  this._assertNotInit("define");
1649
1662
  const compiledDef = {
1650
1663
  ...def,
1651
- render: compileRender(def, table, this._schema, this._adapter)
1664
+ render: def.render ? compileRender(def, table, this._schema, this._adapter) : () => "",
1665
+ outputFile: def.outputFile ?? `.schema-only/${table}.md`
1652
1666
  };
1653
1667
  this._schema.define(table, compiledDef);
1654
1668
  return this;
@@ -1690,6 +1704,23 @@ var Lattice = class {
1690
1704
  this._initialized = true;
1691
1705
  return Promise.resolve();
1692
1706
  }
1707
+ /**
1708
+ * Run additional migrations after init(). Useful for package-level schema
1709
+ * changes applied at runtime (e.g. update hooks that add columns).
1710
+ *
1711
+ * @since 0.17.0
1712
+ */
1713
+ migrate(migrations) {
1714
+ if (!this._initialized) {
1715
+ return Promise.reject(new Error("Lattice: not initialized \u2014 call init() first"));
1716
+ }
1717
+ this._schema.applyMigrations(this._adapter, migrations);
1718
+ for (const tableName of this._schema.getTables().keys()) {
1719
+ const rows = this._adapter.all(`PRAGMA table_info("${tableName}")`);
1720
+ this._columnCache.set(tableName, new Set(rows.map((r) => r.name)));
1721
+ }
1722
+ return Promise.resolve();
1723
+ }
1693
1724
  close() {
1694
1725
  this._adapter.close();
1695
1726
  this._columnCache.clear();
@@ -1722,6 +1753,17 @@ var Lattice = class {
1722
1753
  this._fireWriteHooks(table, "insert", rowWithPk, pkValue);
1723
1754
  return Promise.resolve(pkValue);
1724
1755
  }
1756
+ /**
1757
+ * Insert a row and return the full inserted row (including auto-generated
1758
+ * fields and defaults). Equivalent to `insert()` followed by `get()`.
1759
+ *
1760
+ * @since 0.17.0
1761
+ */
1762
+ insertReturning(table, row) {
1763
+ return this.insert(table, row).then(
1764
+ (pk) => this.get(table, pk).then((result) => result ?? { ...row, id: pk })
1765
+ );
1766
+ }
1725
1767
  upsert(table, row) {
1726
1768
  const notInit = this._notInitError();
1727
1769
  if (notInit) return notInit;
@@ -1776,6 +1818,17 @@ var Lattice = class {
1776
1818
  this._fireWriteHooks(table, "update", sanitized, auditId, Object.keys(sanitized));
1777
1819
  return Promise.resolve();
1778
1820
  }
1821
+ /**
1822
+ * Update a row and return the full updated row. Equivalent to `update()`
1823
+ * followed by `get()`.
1824
+ *
1825
+ * @since 0.17.0
1826
+ */
1827
+ updateReturning(table, id, row) {
1828
+ return this.update(table, id, row).then(
1829
+ () => this.get(table, id).then((result) => result ?? row)
1830
+ );
1831
+ }
1779
1832
  delete(table, id) {
1780
1833
  const notInit = this._notInitError();
1781
1834
  if (notInit) return notInit;
package/dist/index.d.cts CHANGED
@@ -714,9 +714,9 @@ interface TableDefinition {
714
714
  * `'default-detail'`, `'default-json'`) to use a built-in template.
715
715
  * - Pass a `TemplateRenderSpec` to use a built-in template with lifecycle hooks.
716
716
  */
717
- render: RenderSpec;
717
+ render?: RenderSpec;
718
718
  /** Output path relative to the outputDir passed to render/watch */
719
- outputFile: string;
719
+ outputFile?: string;
720
720
  /** Optional pre-filter applied before render */
721
721
  filter?: (rows: Row[]) => Row[];
722
722
  /**
@@ -825,7 +825,7 @@ interface InitOptions {
825
825
  migrations?: Migration[];
826
826
  }
827
827
  interface Migration {
828
- version: number;
828
+ version: number | string;
829
829
  sql: string;
830
830
  }
831
831
  interface WatchOptions {
@@ -1110,11 +1110,32 @@ declare class Lattice {
1110
1110
  defineWriteHook(hook: WriteHook): this;
1111
1111
  defineWriteback(def: WritebackDefinition): this;
1112
1112
  init(options?: InitOptions): Promise<void>;
1113
+ /**
1114
+ * Run additional migrations after init(). Useful for package-level schema
1115
+ * changes applied at runtime (e.g. update hooks that add columns).
1116
+ *
1117
+ * @since 0.17.0
1118
+ */
1119
+ migrate(migrations: Migration[]): Promise<void>;
1113
1120
  close(): void;
1114
1121
  insert(table: string, row: Row): Promise<string>;
1122
+ /**
1123
+ * Insert a row and return the full inserted row (including auto-generated
1124
+ * fields and defaults). Equivalent to `insert()` followed by `get()`.
1125
+ *
1126
+ * @since 0.17.0
1127
+ */
1128
+ insertReturning(table: string, row: Row): Promise<Row>;
1115
1129
  upsert(table: string, row: Row): Promise<string>;
1116
1130
  upsertBy(table: string, col: string, val: unknown, row: Row): Promise<string>;
1117
1131
  update(table: string, id: PkLookup, row: Partial<Row>): Promise<void>;
1132
+ /**
1133
+ * Update a row and return the full updated row. Equivalent to `update()`
1134
+ * followed by `get()`.
1135
+ *
1136
+ * @since 0.17.0
1137
+ */
1138
+ updateReturning(table: string, id: PkLookup, row: Partial<Row>): Promise<Row>;
1118
1139
  delete(table: string, id: PkLookup): Promise<void>;
1119
1140
  get(table: string, id: PkLookup): Promise<Row | null>;
1120
1141
  /**
package/dist/index.d.ts CHANGED
@@ -714,9 +714,9 @@ interface TableDefinition {
714
714
  * `'default-detail'`, `'default-json'`) to use a built-in template.
715
715
  * - Pass a `TemplateRenderSpec` to use a built-in template with lifecycle hooks.
716
716
  */
717
- render: RenderSpec;
717
+ render?: RenderSpec;
718
718
  /** Output path relative to the outputDir passed to render/watch */
719
- outputFile: string;
719
+ outputFile?: string;
720
720
  /** Optional pre-filter applied before render */
721
721
  filter?: (rows: Row[]) => Row[];
722
722
  /**
@@ -825,7 +825,7 @@ interface InitOptions {
825
825
  migrations?: Migration[];
826
826
  }
827
827
  interface Migration {
828
- version: number;
828
+ version: number | string;
829
829
  sql: string;
830
830
  }
831
831
  interface WatchOptions {
@@ -1110,11 +1110,32 @@ declare class Lattice {
1110
1110
  defineWriteHook(hook: WriteHook): this;
1111
1111
  defineWriteback(def: WritebackDefinition): this;
1112
1112
  init(options?: InitOptions): Promise<void>;
1113
+ /**
1114
+ * Run additional migrations after init(). Useful for package-level schema
1115
+ * changes applied at runtime (e.g. update hooks that add columns).
1116
+ *
1117
+ * @since 0.17.0
1118
+ */
1119
+ migrate(migrations: Migration[]): Promise<void>;
1113
1120
  close(): void;
1114
1121
  insert(table: string, row: Row): Promise<string>;
1122
+ /**
1123
+ * Insert a row and return the full inserted row (including auto-generated
1124
+ * fields and defaults). Equivalent to `insert()` followed by `get()`.
1125
+ *
1126
+ * @since 0.17.0
1127
+ */
1128
+ insertReturning(table: string, row: Row): Promise<Row>;
1115
1129
  upsert(table: string, row: Row): Promise<string>;
1116
1130
  upsertBy(table: string, col: string, val: unknown, row: Row): Promise<string>;
1117
1131
  update(table: string, id: PkLookup, row: Partial<Row>): Promise<void>;
1132
+ /**
1133
+ * Update a row and return the full updated row. Equivalent to `update()`
1134
+ * followed by `get()`.
1135
+ *
1136
+ * @since 0.17.0
1137
+ */
1138
+ updateReturning(table: string, id: PkLookup, row: Partial<Row>): Promise<Row>;
1118
1139
  delete(table: string, id: PkLookup): Promise<void>;
1119
1140
  get(table: string, id: PkLookup): Promise<Row | null>;
1120
1141
  /**
package/dist/index.js CHANGED
@@ -178,24 +178,37 @@ var SchemaManager = class {
178
178
  */
179
179
  applySchema(adapter) {
180
180
  for (const [name, def] of this._tables) {
181
- this._ensureTable(adapter, name, def.columns, def.tableConstraints);
181
+ const pkCols = this._tablePK.get(name) ?? ["id"];
182
+ let constraints = def.tableConstraints ? [...def.tableConstraints] : [];
183
+ if (pkCols.length > 1) {
184
+ const alreadyHasPK = constraints.some((c) => c.toUpperCase().startsWith("PRIMARY KEY"));
185
+ if (!alreadyHasPK) {
186
+ constraints.unshift(`PRIMARY KEY (${pkCols.map((c) => `"${c}"`).join(", ")})`);
187
+ }
188
+ }
189
+ this._ensureTable(adapter, name, def.columns, constraints.length ? constraints : void 0);
182
190
  }
183
191
  this._ensureTable(adapter, "__lattice_migrations", {
184
- version: "INTEGER PRIMARY KEY",
192
+ version: "TEXT PRIMARY KEY",
185
193
  applied_at: "TEXT NOT NULL"
186
194
  });
187
195
  }
188
196
  /** Run explicit versioned migrations in order, idempotently */
189
197
  applyMigrations(adapter, migrations) {
190
- const sorted = [...migrations].sort((a, b) => a.version - b.version);
198
+ const sorted = [...migrations].sort((a, b) => {
199
+ const va = String(a.version);
200
+ const vb = String(b.version);
201
+ return va.localeCompare(vb, void 0, { numeric: true });
202
+ });
191
203
  for (const m of sorted) {
204
+ const versionStr = String(m.version);
192
205
  const exists = adapter.get("SELECT 1 FROM __lattice_migrations WHERE version = ?", [
193
- m.version
206
+ versionStr
194
207
  ]);
195
208
  if (!exists) {
196
209
  adapter.run(m.sql);
197
210
  adapter.run("INSERT INTO __lattice_migrations (version, applied_at) VALUES (?, ?)", [
198
- m.version,
211
+ versionStr,
199
212
  (/* @__PURE__ */ new Date()).toISOString()
200
213
  ]);
201
214
  }
@@ -1586,7 +1599,8 @@ var Lattice = class {
1586
1599
  this._assertNotInit("define");
1587
1600
  const compiledDef = {
1588
1601
  ...def,
1589
- render: compileRender(def, table, this._schema, this._adapter)
1602
+ render: def.render ? compileRender(def, table, this._schema, this._adapter) : () => "",
1603
+ outputFile: def.outputFile ?? `.schema-only/${table}.md`
1590
1604
  };
1591
1605
  this._schema.define(table, compiledDef);
1592
1606
  return this;
@@ -1628,6 +1642,23 @@ var Lattice = class {
1628
1642
  this._initialized = true;
1629
1643
  return Promise.resolve();
1630
1644
  }
1645
+ /**
1646
+ * Run additional migrations after init(). Useful for package-level schema
1647
+ * changes applied at runtime (e.g. update hooks that add columns).
1648
+ *
1649
+ * @since 0.17.0
1650
+ */
1651
+ migrate(migrations) {
1652
+ if (!this._initialized) {
1653
+ return Promise.reject(new Error("Lattice: not initialized \u2014 call init() first"));
1654
+ }
1655
+ this._schema.applyMigrations(this._adapter, migrations);
1656
+ for (const tableName of this._schema.getTables().keys()) {
1657
+ const rows = this._adapter.all(`PRAGMA table_info("${tableName}")`);
1658
+ this._columnCache.set(tableName, new Set(rows.map((r) => r.name)));
1659
+ }
1660
+ return Promise.resolve();
1661
+ }
1631
1662
  close() {
1632
1663
  this._adapter.close();
1633
1664
  this._columnCache.clear();
@@ -1660,6 +1691,17 @@ var Lattice = class {
1660
1691
  this._fireWriteHooks(table, "insert", rowWithPk, pkValue);
1661
1692
  return Promise.resolve(pkValue);
1662
1693
  }
1694
+ /**
1695
+ * Insert a row and return the full inserted row (including auto-generated
1696
+ * fields and defaults). Equivalent to `insert()` followed by `get()`.
1697
+ *
1698
+ * @since 0.17.0
1699
+ */
1700
+ insertReturning(table, row) {
1701
+ return this.insert(table, row).then(
1702
+ (pk) => this.get(table, pk).then((result) => result ?? { ...row, id: pk })
1703
+ );
1704
+ }
1663
1705
  upsert(table, row) {
1664
1706
  const notInit = this._notInitError();
1665
1707
  if (notInit) return notInit;
@@ -1714,6 +1756,17 @@ var Lattice = class {
1714
1756
  this._fireWriteHooks(table, "update", sanitized, auditId, Object.keys(sanitized));
1715
1757
  return Promise.resolve();
1716
1758
  }
1759
+ /**
1760
+ * Update a row and return the full updated row. Equivalent to `update()`
1761
+ * followed by `get()`.
1762
+ *
1763
+ * @since 0.17.0
1764
+ */
1765
+ updateReturning(table, id, row) {
1766
+ return this.update(table, id, row).then(
1767
+ () => this.get(table, id).then((result) => result ?? row)
1768
+ );
1769
+ }
1717
1770
  delete(table, id) {
1718
1771
  const notInit = this._notInitError();
1719
1772
  if (notInit) return notInit;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "latticesql",
3
- "version": "0.16.2",
3
+ "version": "0.17.0",
4
4
  "description": "Persistent structured memory for AI agent systems — SQLite ↔ LLM context bridge",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",