latticesql 0.16.2 → 0.17.1

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
  }
@@ -544,6 +557,7 @@ var SchemaManager = class {
544
557
  const existing = adapter.all(`PRAGMA table_info("${table}")`).map((r) => r.name);
545
558
  for (const [col, type] of Object.entries(columns)) {
546
559
  if (!existing.includes(col)) {
560
+ if (type.toUpperCase().includes("PRIMARY KEY")) continue;
547
561
  adapter.run(`ALTER TABLE "${table}" ADD COLUMN "${col}" ${type}`);
548
562
  }
549
563
  }
@@ -1612,7 +1626,8 @@ var Lattice = class {
1612
1626
  this._assertNotInit("define");
1613
1627
  const compiledDef = {
1614
1628
  ...def,
1615
- render: compileRender(def, table, this._schema, this._adapter)
1629
+ render: def.render ? compileRender(def, table, this._schema, this._adapter) : () => "",
1630
+ outputFile: def.outputFile ?? `.schema-only/${table}.md`
1616
1631
  };
1617
1632
  this._schema.define(table, compiledDef);
1618
1633
  return this;
@@ -1654,6 +1669,23 @@ var Lattice = class {
1654
1669
  this._initialized = true;
1655
1670
  return Promise.resolve();
1656
1671
  }
1672
+ /**
1673
+ * Run additional migrations after init(). Useful for package-level schema
1674
+ * changes applied at runtime (e.g. update hooks that add columns).
1675
+ *
1676
+ * @since 0.17.0
1677
+ */
1678
+ migrate(migrations) {
1679
+ if (!this._initialized) {
1680
+ return Promise.reject(new Error("Lattice: not initialized \u2014 call init() first"));
1681
+ }
1682
+ this._schema.applyMigrations(this._adapter, migrations);
1683
+ for (const tableName of this._schema.getTables().keys()) {
1684
+ const rows = this._adapter.all(`PRAGMA table_info("${tableName}")`);
1685
+ this._columnCache.set(tableName, new Set(rows.map((r) => r.name)));
1686
+ }
1687
+ return Promise.resolve();
1688
+ }
1657
1689
  close() {
1658
1690
  this._adapter.close();
1659
1691
  this._columnCache.clear();
@@ -1686,6 +1718,17 @@ var Lattice = class {
1686
1718
  this._fireWriteHooks(table, "insert", rowWithPk, pkValue);
1687
1719
  return Promise.resolve(pkValue);
1688
1720
  }
1721
+ /**
1722
+ * Insert a row and return the full inserted row (including auto-generated
1723
+ * fields and defaults). Equivalent to `insert()` followed by `get()`.
1724
+ *
1725
+ * @since 0.17.0
1726
+ */
1727
+ insertReturning(table, row) {
1728
+ return this.insert(table, row).then(
1729
+ (pk) => this.get(table, pk).then((result) => result ?? { ...row, id: pk })
1730
+ );
1731
+ }
1689
1732
  upsert(table, row) {
1690
1733
  const notInit = this._notInitError();
1691
1734
  if (notInit) return notInit;
@@ -1740,6 +1783,17 @@ var Lattice = class {
1740
1783
  this._fireWriteHooks(table, "update", sanitized, auditId, Object.keys(sanitized));
1741
1784
  return Promise.resolve();
1742
1785
  }
1786
+ /**
1787
+ * Update a row and return the full updated row. Equivalent to `update()`
1788
+ * followed by `get()`.
1789
+ *
1790
+ * @since 0.17.0
1791
+ */
1792
+ updateReturning(table, id, row) {
1793
+ return this.update(table, id, row).then(
1794
+ () => this.get(table, id).then((result) => result ?? row)
1795
+ );
1796
+ }
1743
1797
  delete(table, id) {
1744
1798
  const notInit = this._notInitError();
1745
1799
  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
  }
@@ -292,6 +305,7 @@ var SchemaManager = class {
292
305
  const existing = adapter.all(`PRAGMA table_info("${table}")`).map((r) => r.name);
293
306
  for (const [col, type] of Object.entries(columns)) {
294
307
  if (!existing.includes(col)) {
308
+ if (type.toUpperCase().includes("PRIMARY KEY")) continue;
295
309
  adapter.run(`ALTER TABLE "${table}" ADD COLUMN "${col}" ${type}`);
296
310
  }
297
311
  }
@@ -1648,7 +1662,8 @@ var Lattice = class {
1648
1662
  this._assertNotInit("define");
1649
1663
  const compiledDef = {
1650
1664
  ...def,
1651
- render: compileRender(def, table, this._schema, this._adapter)
1665
+ render: def.render ? compileRender(def, table, this._schema, this._adapter) : () => "",
1666
+ outputFile: def.outputFile ?? `.schema-only/${table}.md`
1652
1667
  };
1653
1668
  this._schema.define(table, compiledDef);
1654
1669
  return this;
@@ -1690,6 +1705,23 @@ var Lattice = class {
1690
1705
  this._initialized = true;
1691
1706
  return Promise.resolve();
1692
1707
  }
1708
+ /**
1709
+ * Run additional migrations after init(). Useful for package-level schema
1710
+ * changes applied at runtime (e.g. update hooks that add columns).
1711
+ *
1712
+ * @since 0.17.0
1713
+ */
1714
+ migrate(migrations) {
1715
+ if (!this._initialized) {
1716
+ return Promise.reject(new Error("Lattice: not initialized \u2014 call init() first"));
1717
+ }
1718
+ this._schema.applyMigrations(this._adapter, migrations);
1719
+ for (const tableName of this._schema.getTables().keys()) {
1720
+ const rows = this._adapter.all(`PRAGMA table_info("${tableName}")`);
1721
+ this._columnCache.set(tableName, new Set(rows.map((r) => r.name)));
1722
+ }
1723
+ return Promise.resolve();
1724
+ }
1693
1725
  close() {
1694
1726
  this._adapter.close();
1695
1727
  this._columnCache.clear();
@@ -1722,6 +1754,17 @@ var Lattice = class {
1722
1754
  this._fireWriteHooks(table, "insert", rowWithPk, pkValue);
1723
1755
  return Promise.resolve(pkValue);
1724
1756
  }
1757
+ /**
1758
+ * Insert a row and return the full inserted row (including auto-generated
1759
+ * fields and defaults). Equivalent to `insert()` followed by `get()`.
1760
+ *
1761
+ * @since 0.17.0
1762
+ */
1763
+ insertReturning(table, row) {
1764
+ return this.insert(table, row).then(
1765
+ (pk) => this.get(table, pk).then((result) => result ?? { ...row, id: pk })
1766
+ );
1767
+ }
1725
1768
  upsert(table, row) {
1726
1769
  const notInit = this._notInitError();
1727
1770
  if (notInit) return notInit;
@@ -1776,6 +1819,17 @@ var Lattice = class {
1776
1819
  this._fireWriteHooks(table, "update", sanitized, auditId, Object.keys(sanitized));
1777
1820
  return Promise.resolve();
1778
1821
  }
1822
+ /**
1823
+ * Update a row and return the full updated row. Equivalent to `update()`
1824
+ * followed by `get()`.
1825
+ *
1826
+ * @since 0.17.0
1827
+ */
1828
+ updateReturning(table, id, row) {
1829
+ return this.update(table, id, row).then(
1830
+ () => this.get(table, id).then((result) => result ?? row)
1831
+ );
1832
+ }
1779
1833
  delete(table, id) {
1780
1834
  const notInit = this._notInitError();
1781
1835
  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
  }
@@ -230,6 +243,7 @@ var SchemaManager = class {
230
243
  const existing = adapter.all(`PRAGMA table_info("${table}")`).map((r) => r.name);
231
244
  for (const [col, type] of Object.entries(columns)) {
232
245
  if (!existing.includes(col)) {
246
+ if (type.toUpperCase().includes("PRIMARY KEY")) continue;
233
247
  adapter.run(`ALTER TABLE "${table}" ADD COLUMN "${col}" ${type}`);
234
248
  }
235
249
  }
@@ -1586,7 +1600,8 @@ var Lattice = class {
1586
1600
  this._assertNotInit("define");
1587
1601
  const compiledDef = {
1588
1602
  ...def,
1589
- render: compileRender(def, table, this._schema, this._adapter)
1603
+ render: def.render ? compileRender(def, table, this._schema, this._adapter) : () => "",
1604
+ outputFile: def.outputFile ?? `.schema-only/${table}.md`
1590
1605
  };
1591
1606
  this._schema.define(table, compiledDef);
1592
1607
  return this;
@@ -1628,6 +1643,23 @@ var Lattice = class {
1628
1643
  this._initialized = true;
1629
1644
  return Promise.resolve();
1630
1645
  }
1646
+ /**
1647
+ * Run additional migrations after init(). Useful for package-level schema
1648
+ * changes applied at runtime (e.g. update hooks that add columns).
1649
+ *
1650
+ * @since 0.17.0
1651
+ */
1652
+ migrate(migrations) {
1653
+ if (!this._initialized) {
1654
+ return Promise.reject(new Error("Lattice: not initialized \u2014 call init() first"));
1655
+ }
1656
+ this._schema.applyMigrations(this._adapter, migrations);
1657
+ for (const tableName of this._schema.getTables().keys()) {
1658
+ const rows = this._adapter.all(`PRAGMA table_info("${tableName}")`);
1659
+ this._columnCache.set(tableName, new Set(rows.map((r) => r.name)));
1660
+ }
1661
+ return Promise.resolve();
1662
+ }
1631
1663
  close() {
1632
1664
  this._adapter.close();
1633
1665
  this._columnCache.clear();
@@ -1660,6 +1692,17 @@ var Lattice = class {
1660
1692
  this._fireWriteHooks(table, "insert", rowWithPk, pkValue);
1661
1693
  return Promise.resolve(pkValue);
1662
1694
  }
1695
+ /**
1696
+ * Insert a row and return the full inserted row (including auto-generated
1697
+ * fields and defaults). Equivalent to `insert()` followed by `get()`.
1698
+ *
1699
+ * @since 0.17.0
1700
+ */
1701
+ insertReturning(table, row) {
1702
+ return this.insert(table, row).then(
1703
+ (pk) => this.get(table, pk).then((result) => result ?? { ...row, id: pk })
1704
+ );
1705
+ }
1663
1706
  upsert(table, row) {
1664
1707
  const notInit = this._notInitError();
1665
1708
  if (notInit) return notInit;
@@ -1714,6 +1757,17 @@ var Lattice = class {
1714
1757
  this._fireWriteHooks(table, "update", sanitized, auditId, Object.keys(sanitized));
1715
1758
  return Promise.resolve();
1716
1759
  }
1760
+ /**
1761
+ * Update a row and return the full updated row. Equivalent to `update()`
1762
+ * followed by `get()`.
1763
+ *
1764
+ * @since 0.17.0
1765
+ */
1766
+ updateReturning(table, id, row) {
1767
+ return this.update(table, id, row).then(
1768
+ () => this.get(table, id).then((result) => result ?? row)
1769
+ );
1770
+ }
1717
1771
  delete(table, id) {
1718
1772
  const notInit = this._notInitError();
1719
1773
  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.1",
4
4
  "description": "Persistent structured memory for AI agent systems — SQLite ↔ LLM context bridge",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",